From b5d51ebbf2cc1463ee4730ccc567224c06ba8433 Mon Sep 17 00:00:00 2001 From: Black Hat Date: Fri, 10 May 2019 19:26:17 +0800 Subject: [PATCH] Improve message bubble. --- imports/Spectral/Component/Avatar.qml | 4 +- .../Component/Timeline/MessageDelegate.qml | 401 ++++++++++-------- imports/Spectral/Panel/RoomListPanel.qml | 2 - imports/Spectral/Panel/RoomPanelInput.qml | 2 +- src/messageeventmodel.cpp | 61 +-- 5 files changed, 255 insertions(+), 215 deletions(-) diff --git a/imports/Spectral/Component/Avatar.qml b/imports/Spectral/Component/Avatar.qml index fe546eb..d7972d5 100644 --- a/imports/Spectral/Component/Avatar.qml +++ b/imports/Spectral/Component/Avatar.qml @@ -45,7 +45,9 @@ Item { color: "white" text: hint[0].toUpperCase() font.pixelSize: root.width / 2 - font.bold: true + font.weight: Font.Light + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter } } diff --git a/imports/Spectral/Component/Timeline/MessageDelegate.qml b/imports/Spectral/Component/Timeline/MessageDelegate.qml index e324727..154c152 100644 --- a/imports/Spectral/Component/Timeline/MessageDelegate.qml +++ b/imports/Spectral/Component/Timeline/MessageDelegate.qml @@ -11,7 +11,7 @@ import Spectral.Dialog 2.0 import Spectral.Menu.Timeline 2.0 import Spectral.Effect 2.0 -RowLayout { +ColumnLayout { readonly property bool avatarVisible: !sentByMe && showAuthor readonly property bool sentByMe: author === currentRoom.localUser readonly property bool darkBackground: !sentByMe @@ -24,229 +24,258 @@ RowLayout { z: -5 - spacing: 4 + spacing: 0 - Avatar { - Layout.preferredWidth: 32 - Layout.preferredHeight: 32 - Layout.alignment: Qt.AlignTop + RowLayout { + id: messageRow - visible: avatarVisible - hint: author.displayName - source: author.avatarMediaId + spacing: 4 - Component { - id: userDetailDialog + Avatar { + Layout.preferredWidth: 32 + Layout.preferredHeight: 32 + Layout.alignment: Qt.AlignTop - UserDetailDialog {} - } + visible: avatarVisible + hint: author.displayName + source: author.avatarMediaId - RippleEffect { - anchors.fill: parent + Component { + id: userDetailDialog - circular: true - - onClicked: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": currentRoom, "user": author}).open() - } - } - - Item { - Layout.preferredWidth: 32 - Layout.preferredHeight: 32 - - visible: !(sentByMe || avatarVisible) - } - - Control { - Layout.maximumWidth: messageListView.width - (!sentByMe ? 32 + root.spacing : 0) - 48 - - verticalPadding: 8 - horizontalPadding: 16 - - background: Rectangle { - color: sentByMe ? MPalette.background : eventType === "notice" ? MPalette.primary : MPalette.accent - radius: 18 - antialiasing: true - - Rectangle { - anchors.top: parent.top - anchors.left: parent.left - - width: parent.width / 2 - height: parent.height / 2 - - visible: !sentByMe && (bubbleShape == 3 || bubbleShape == 2) - - color: sentByMe ? MPalette.background : eventType === "notice" ? MPalette.primary : MPalette.accent - radius: 2 + UserDetailDialog {} } - Rectangle { - anchors.top: parent.top - anchors.right: parent.right - - width: parent.width / 2 - height: parent.height / 2 - - visible: sentByMe && (bubbleShape == 3 || bubbleShape == 2) - - color: sentByMe ? MPalette.background : eventType === "notice" ? MPalette.primary : MPalette.accent - radius: 2 - } - - Rectangle { - anchors.bottom: parent.bottom - anchors.left: parent.left - - width: parent.width / 2 - height: parent.height / 2 - - visible: !sentByMe && (bubbleShape == 1 || bubbleShape == 2) - - color: sentByMe ? MPalette.background : eventType === "notice" ? MPalette.primary : MPalette.accent - radius: 2 - } - - Rectangle { - anchors.bottom: parent.bottom - anchors.right: parent.right - - width: parent.width / 2 - height: parent.height / 2 - - visible: sentByMe && (bubbleShape == 1 || bubbleShape == 2) - - color: sentByMe ? MPalette.background : eventType === "notice" ? MPalette.primary : MPalette.accent - radius: 2 - } - - AutoMouseArea { + RippleEffect { anchors.fill: parent - id: messageMouseArea + circular: true - onSecondaryClicked: { - var contextMenu = messageDelegateContextMenu.createObject(ApplicationWindow.overlay) - contextMenu.viewSource.connect(function() { - messageSourceDialog.createObject(ApplicationWindow.overlay, {"sourceText": toolTip}).open() - }) - contextMenu.reply.connect(function() { - roomPanelInput.replyUser = author - roomPanelInput.replyEventID = eventId - roomPanelInput.replyContent = contentLabel.selectedText || message - roomPanelInput.isReply = true - roomPanelInput.focus() - }) - contextMenu.redact.connect(function() { - currentRoom.redactEvent(eventId) - }) - contextMenu.popup() - } - - - Component { - id: messageDelegateContextMenu - - MessageDelegateContextMenu {} - } - - Component { - id: messageSourceDialog - - MessageSourceDialog {} - } + onClicked: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": currentRoom, "user": author}).open() } } - contentItem: ColumnLayout { - RowLayout { - Layout.fillWidth: true + Item { + Layout.preferredWidth: 32 + Layout.preferredHeight: 32 - visible: replyVisible + visible: !(sentByMe || avatarVisible) + } - Avatar { - Layout.preferredWidth: 28 - Layout.preferredHeight: 28 - Layout.alignment: Qt.AlignTop + Control { + Layout.maximumWidth: messageListView.width - (!sentByMe ? 32 + messageRow.spacing : 0) - 48 - source: replyVisible ? replyAuthor.avatarMediaId : "" - hint: replyVisible ? replyAuthor.displayName : "H" + verticalPadding: 8 + horizontalPadding: 16 - RippleEffect { - anchors.fill: parent + background: Rectangle { + color: sentByMe ? MPalette.background : eventType === "notice" ? MPalette.primary : MPalette.accent + radius: 18 + antialiasing: true - circular: true + Rectangle { + anchors.top: parent.top + anchors.left: parent.left - onClicked: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": currentRoom, "user": replyAuthor}).open() - } + width: parent.width / 2 + height: parent.height / 2 + + visible: !sentByMe && (bubbleShape == 3 || bubbleShape == 2) + + color: sentByMe ? MPalette.background : eventType === "notice" ? MPalette.primary : MPalette.accent + radius: 2 } - Control { + Rectangle { + anchors.top: parent.top + anchors.right: parent.right + + width: parent.width / 2 + height: parent.height / 2 + + visible: sentByMe && (bubbleShape == 3 || bubbleShape == 2) + + color: sentByMe ? MPalette.background : eventType === "notice" ? MPalette.primary : MPalette.accent + radius: 2 + } + + Rectangle { + anchors.bottom: parent.bottom + anchors.left: parent.left + + width: parent.width / 2 + height: parent.height / 2 + + visible: !sentByMe && (bubbleShape == 1 || bubbleShape == 2) + + color: sentByMe ? MPalette.background : eventType === "notice" ? MPalette.primary : MPalette.accent + radius: 2 + } + + Rectangle { + anchors.bottom: parent.bottom + anchors.right: parent.right + + width: parent.width / 2 + height: parent.height / 2 + + visible: sentByMe && (bubbleShape == 1 || bubbleShape == 2) + + color: sentByMe ? MPalette.background : eventType === "notice" ? MPalette.primary : MPalette.accent + radius: 2 + } + + AutoMouseArea { + anchors.fill: parent + + id: messageMouseArea + + onSecondaryClicked: { + var contextMenu = messageDelegateContextMenu.createObject(ApplicationWindow.overlay) + contextMenu.viewSource.connect(function() { + messageSourceDialog.createObject(ApplicationWindow.overlay, {"sourceText": toolTip}).open() + }) + contextMenu.reply.connect(function() { + roomPanelInput.replyUser = author + roomPanelInput.replyEventID = eventId + roomPanelInput.replyContent = contentLabel.selectedText || message + roomPanelInput.isReply = true + roomPanelInput.focus() + }) + contextMenu.redact.connect(function() { + currentRoom.redactEvent(eventId) + }) + contextMenu.popup() + } + + + Component { + id: messageDelegateContextMenu + + MessageDelegateContextMenu {} + } + + Component { + id: messageSourceDialog + + MessageSourceDialog {} + } + } + } + + contentItem: ColumnLayout { + RowLayout { Layout.fillWidth: true - padding: 0 + visible: replyVisible - background: RippleEffect { - onClicked: goToEvent(replyEventId) + Avatar { + Layout.preferredWidth: 28 + Layout.preferredHeight: 28 + Layout.alignment: Qt.AlignTop + + source: replyVisible ? replyAuthor.avatarMediaId : "" + hint: replyVisible ? replyAuthor.displayName : "H" + + RippleEffect { + anchors.fill: parent + + circular: true + + onClicked: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": currentRoom, "user": replyAuthor}).open() + } } - contentItem: Label { + Control { Layout.fillWidth: true - visible: replyVisible - color: darkBackground ? "white" : MPalette.lighter - text: "" + (replyDisplay || "") + padding: 0 - wrapMode: Label.Wrap - textFormat: Label.RichText - } - } - } + background: RippleEffect { + onClicked: goToEvent(replyEventId) + } - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 1 + contentItem: Label { + Layout.fillWidth: true - visible: replyVisible - color: darkBackground ? "white" : MPalette.lighter - } + visible: replyVisible + color: darkBackground ? "white" : MPalette.lighter + text: "" + (replyDisplay || "") - TextEdit { - Layout.fillWidth: true - - id: contentLabel - - text: "" + display - - color: darkBackground ? "white" : MPalette.foreground - - font.family: window.font.family - font.pixelSize: 14 - selectByMouse: true - readOnly: true - wrapMode: Label.Wrap - selectedTextColor: darkBackground ? MPalette.accent : "white" - selectionColor: darkBackground ? "white" : MPalette.accent - textFormat: Text.RichText - - onLinkActivated: { - if (link.startsWith("https://matrix.to/")) { - var result = link.replace(/\?.*/, "").match("https://matrix.to/#/(!.*:.*)/(\\$.*:.*)") - if (!result || result.length < 3) return - if (result[1] != currentRoom.id) return - if (!result[2]) return - goToEvent(result[2]) - } else { - Qt.openUrlExternally(link) + wrapMode: Label.Wrap + textFormat: Label.RichText + } } } - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.NoButton - cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + + visible: replyVisible + color: darkBackground ? "white" : MPalette.lighter + } + + TextEdit { + Layout.fillWidth: true + + id: contentLabel + + text: "" + display + + color: darkBackground ? "white" : MPalette.foreground + + font.family: window.font.family + font.pixelSize: 14 + selectByMouse: true + readOnly: true + wrapMode: Label.Wrap + selectedTextColor: darkBackground ? MPalette.accent : "white" + selectionColor: darkBackground ? "white" : MPalette.accent + textFormat: Text.RichText + + onLinkActivated: { + if (link.startsWith("https://matrix.to/")) { + var result = link.replace(/\?.*/, "").match("https://matrix.to/#/(!.*:.*)/(\\$.*:.*)") + if (!result || result.length < 3) return + if (result[1] != currentRoom.id) return + if (!result[2]) return + goToEvent(result[2]) + } else { + Qt.openUrlExternally(link) + } + } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.NoButton + cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor + } } } } } + + RowLayout { + Layout.alignment: sentByMe ? Qt.AlignRight : Qt.AlignLeft + Layout.leftMargin: sentByMe ? undefined : 32 + messageRow.spacing + 12 + Layout.rightMargin: sentByMe ? 12 : undefined + Layout.bottomMargin: 4 + + visible: showTimestamp || (showAuthor && !sentByMe) + + Label { + visible: showTimestamp + + text: Qt.formatDateTime(time, "hh:mm") + color: MPalette.lighter + } + + Label { + visible: showAuthor && !sentByMe + + text: author.displayName + color: MPalette.lighter + } + } } diff --git a/imports/Spectral/Panel/RoomListPanel.qml b/imports/Spectral/Panel/RoomListPanel.qml index 00cf0b1..c120722 100644 --- a/imports/Spectral/Panel/RoomListPanel.qml +++ b/imports/Spectral/Panel/RoomListPanel.qml @@ -201,8 +201,6 @@ Item { background: Rectangle { color: Material.background - opacity: listView.atYBeginning ? 0 : 1 - layer.enabled: true layer.effect: ElevationEffect { elevation: 2 diff --git a/imports/Spectral/Panel/RoomPanelInput.qml b/imports/Spectral/Panel/RoomPanelInput.qml index a821fcb..6dc897d 100644 --- a/imports/Spectral/Panel/RoomPanelInput.qml +++ b/imports/Spectral/Panel/RoomPanelInput.qml @@ -31,7 +31,7 @@ Control { layer.enabled: true layer.effect: ElevationEffect { - elevation: 2 + elevation: 1 } } diff --git a/src/messageeventmodel.cpp b/src/messageeventmodel.cpp index cc9b202..32bc11f 100644 --- a/src/messageeventmodel.cpp +++ b/src/messageeventmodel.cpp @@ -415,40 +415,51 @@ QVariant MessageEventModel::data(const QModelIndex& idx, int role) const { return {}; } - if (role == ShowTimestampRole || role == ShowAuthorRole) - for (auto r = row + 1; r < rowCount(); ++r) { - auto i = index(r); - if (data(i, SpecialMarksRole) != EventStatus::Hidden) { - switch (role) { - case ShowTimestampRole: - return data(i, TimeRole) - .toDateTime() - .msecsTo(data(idx, TimeRole).toDateTime()) > 600000; - case ShowAuthorRole: - return data(i, AuthorRole) != data(idx, AuthorRole); - } - } - } - - if (role == BubbleShapeRole) { // TODO: Convoluted logic. - int belowRow = -1; // Invalid - + if (role == ShowTimestampRole) { for (auto r = row - 1; r >= 0; --r) { auto i = index(r); if (data(i, SpecialMarksRole) != EventStatus::Hidden) { - belowRow = r; + return data(idx, TimeRole) + .toDateTime() + .msecsTo(data(i, TimeRole).toDateTime()) > 600000; + } + } + + return true; + } + + if (role == ShowAuthorRole) { + for (auto r = row - 1; r >= 0; --r) { + auto i = index(r); + if (data(i, SpecialMarksRole) != EventStatus::Hidden) { + return data(i, AuthorRole) != data(idx, AuthorRole) || + data(i, EventTypeRole) != data(idx, EventTypeRole); + } + } + + return true; + } + + if (role == BubbleShapeRole) { // TODO: Convoluted logic. + int aboveRow = -1; // Invalid + + for (auto r = row + 1; r < rowCount(); ++r) { + auto i = index(r); + if (data(i, SpecialMarksRole) != EventStatus::Hidden) { + aboveRow = r; break; } } bool aboveShow, belowShow; - aboveShow = data(idx, ShowAuthorRole).toBool() || + if (aboveRow == -1) { + aboveShow = true; + } else { + aboveShow = data(index(aboveRow), ShowAuthorRole).toBool() || + data(index(aboveRow), ShowTimestampRole).toBool(); + } + belowShow = data(idx, ShowAuthorRole).toBool() || data(idx, ShowTimestampRole).toBool(); - if (belowRow == -1) - belowShow = true; - else - belowShow = data(index(belowRow), ShowAuthorRole).toBool() || - data(index(belowRow), ShowTimestampRole).toBool(); if (aboveShow && belowShow) return BubbleShapes::NoShape;