diff --git a/imports/NeoChat/Component/FancyEffectsContainer.qml b/imports/NeoChat/Component/FancyEffectsContainer.qml index 89d2e75de..2844a4d0b 100644 --- a/imports/NeoChat/Component/FancyEffectsContainer.qml +++ b/imports/NeoChat/Component/FancyEffectsContainer.qml @@ -31,8 +31,8 @@ Rectangle { // backgroundColor color: Kirigami.Theme.backgroundColor - Kirigami.Theme.colorSet: Kirigami.Theme.View - Kirigami.Theme.inherit: true + Kirigami.Theme.colorSet: Kirigami.Theme.Window + Kirigami.Theme.inherit: false // Confetti diff --git a/imports/NeoChat/Component/Timeline/AudioDelegate.qml b/imports/NeoChat/Component/Timeline/AudioDelegate.qml index cc7525cd0..596988147 100644 --- a/imports/NeoChat/Component/Timeline/AudioDelegate.qml +++ b/imports/NeoChat/Component/Timeline/AudioDelegate.qml @@ -17,6 +17,7 @@ import NeoChat.Setting 1.0 import NeoChat.Component 1.0 import NeoChat.Dialog 1.0 import NeoChat.Menu.Timeline 1.0 +import org.kde.kcoreaddons 1.0 as KCA Control { id: root @@ -29,27 +30,6 @@ Control { autoLoad: false } - Kirigami.Action { - id: saveFileAction - onTriggered: { - let contextMenu = fileDelegateContextMenu.createObject(root, {'room': currentRoom, 'author': author}); - contextMenu.viewSource.connect(function() { - messagerSourceSheet.createObject(ApplicationWindow.overlay, {"sourceText": toolTip}).open() - }) - contextMenu.downloadAndOpen.connect(downloadAndOpen) - contextMenu.saveFileAs.connect(saveFileAs) - contextMenu.reply.connect(function() { - roomPanelInput.replyModel = Object.assign({}, model) - roomPanelInput.isReply = true - roomPanelInput.focus() - }) - contextMenu.redact.connect(function() { - currentRoom.redactEvent(eventId) - }) - contextMenu.popup() - } - } - contentItem: ColumnLayout { RowLayout { ToolButton { @@ -69,6 +49,8 @@ Control { } RowLayout { visible: audio.hasAudio + Layout.leftMargin: Kirigami.Units.largeSpacing + Layout.rightMargin: Kirigami.Units.largeSpacing // Server doesn't support seeking, so use ProgressBar instead of Slider :( ProgressBar { from: 0 @@ -77,72 +59,8 @@ Control { } Label { - text: humanSize(audio.position) + "/" + humanSize(audio.duration) + text: KCA.Format.formatDuration(audio.position) + "/" + KCA.Format.formatDuration(audio.duration) } } } - - background: AutoMouseArea { - anchors.fill: parent - - id: messageMouseArea - - onSecondaryClicked: saveFileAction.trigger() - - Component { - id: messagerSourceSheet - - MessageSourceSheet {} - } - - Component { - id: openFolderDialog - - OpenFolderDialog {} - } - - Component { - id: fileDelegateContextMenu - - FileDelegateContextMenu {} - } - } - - function saveFileAs() { - var folderDialog = openFolderDialog.createObject(ApplicationWindow.overlay) - - folderDialog.chosen.connect(function(path) { - if (!path) return - - currentRoom.downloadFile(eventId, path + "/" + currentRoom.fileNameToDownload(eventId)) - }) - - folderDialog.open() - } - - function downloadAndOpen() { - if (downloaded) { - openSavedFile() - } else { - openOnFinished = true - currentRoom.downloadFile(eventId, Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId)) - } - } - - function openSavedFile() { - if (Qt.openUrlExternally(progressInfo.localPath)) return; - if (Qt.openUrlExternally(progressInfo.localDir)) return; - } - - function humanSize(duration) { - if (!duration) { - return i18n("Unknown duration") - } - - if (duration > 1000 * 60 * 60) { - return new Date(duration).toLocaleTimeString(Qt.locale(), "hh:mm:ss") - } - - return new Date(duration).toLocaleTimeString(Qt.locale(), "mm:ss") - } } diff --git a/imports/NeoChat/Component/Timeline/FileDelegate.qml b/imports/NeoChat/Component/Timeline/FileDelegate.qml index ebac3334e..5c3a3fb50 100644 --- a/imports/NeoChat/Component/Timeline/FileDelegate.qml +++ b/imports/NeoChat/Component/Timeline/FileDelegate.qml @@ -10,6 +10,7 @@ import QtQuick.Controls.Material 2.12 import QtGraphicalEffects 1.0 import Qt.labs.platform 1.0 import org.kde.kirigami 2.13 as Kirigami +import org.kde.kcoreaddons 1.0 as KCA import org.kde.neochat 1.0 import NeoChat.Setting 1.0 @@ -18,88 +19,51 @@ import NeoChat.Component 1.0 import NeoChat.Dialog 1.0 import NeoChat.Menu.Timeline 1.0 + + RowLayout { + id: root property bool openOnFinished: false readonly property bool downloaded: progressInfo && progressInfo.completed - id: root - spacing: 4 - onDownloadedChanged: if (downloaded && openOnFinished) openSavedFile() + onDownloadedChanged: if (downloaded && openOnFinished) { + openSavedFile(); + } z: -5 - Control { - contentItem: RowLayout { - ToolButton { - icon.name: progressInfo.completed ? "document-open" : "document-save" - onClicked: progressInfo.completed ? openSavedFile() : saveFileAs() - } + ToolButton { + icon.name: progressInfo.completed ? "document-open" : "document-save" + onClicked: progressInfo.completed ? openSavedFile() : saveFileAs() + } - ColumnLayout { - Kirigami.Heading { - Layout.fillWidth: true - level: 4 - text: display - wrapMode: Label.Wrap - } - - Label { - Layout.fillWidth: true - text: !progressInfo.completed && progressInfo.active ? (humanSize(progressInfo.progress) + "/" + humanSize(progressInfo.total)) : humanSize(content.info ? content.info.size : 0) - color: Kirigami.Theme.disabledTextColor - wrapMode: Label.Wrap - } - } + ColumnLayout { + Kirigami.Heading { + Layout.fillWidth: true + level: 4 + text: display + wrapMode: Label.Wrap } - background: Item { - MouseArea { - id: messageMouseArea - anchors.fill: parent - acceptedButtons: Qt.RightButton - onClicked: { - var contextMenu = fileDelegateContextMenu.createObject(root, {'room': currentRoom, 'author': author}); - contextMenu.viewSource.connect(function() { - messageSourceSheet.createObject(ApplicationWindow.overlay, {"sourceText": toolTip}).open() - }) - contextMenu.downloadAndOpen.connect(downloadAndOpen) - contextMenu.saveFileAs.connect(saveFileAs) - contextMenu.reply.connect(function() { - roomPanelInput.replyModel = Object.assign({}, model) - roomPanelInput.isReply = true - roomPanelInput.focus() - }) - contextMenu.redact.connect(function() { - currentRoom.redactEvent(eventId) - }) - contextMenu.popup() - } + Label { + Layout.fillWidth: true + text: !progressInfo.completed && progressInfo.active ? (KCA.Format.formatByteSize(progressInfo.progress) + "/" + KCA.Format.formatByteSize(progressInfo.total)) : KCA.Format.formatByteSize(content.info ? content.info.size : 0) + color: Kirigami.Theme.disabledTextColor + wrapMode: Label.Wrap + } + Layout.rightMargin: Kirigami.Units.largeSpacing + } - Component { - id: messageSourceSheet + Component { + id: fileDialog - MessageSourceSheet {} - } - - Component { - id: fileDialog - - FileDialog { - fileMode: FileDialog.SaveFile - folder: StandardPaths.writableLocation(StandardPaths.DownloadLocation) - onAccepted: { - currentRoom.downloadFile(eventId, file) - } - } - } - - Component { - id: fileDelegateContextMenu - - FileDelegateContextMenu {} - } + FileDialog { + fileMode: FileDialog.SaveFile + folder: StandardPaths.writableLocation(StandardPaths.DownloadLocation) + onAccepted: { + currentRoom.downloadFile(eventId, file) } } } @@ -110,34 +74,18 @@ RowLayout { dialog.currentFile = dialog.folder + "/" + currentRoom.fileNameToDownload(eventId) } - function downloadAndOpen() - { - if (downloaded) openSavedFile() - else - { - openOnFinished = true - currentRoom.downloadFile(eventId, StandardPaths.writableLocation(StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId)) + function downloadAndOpen() { + if (downloaded) { + openSavedFile(); + } else { + openOnFinished = true; + currentRoom.downloadFile(eventId, StandardPaths.writableLocation(StandardPaths.CacheLocation) + "/" + + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId)); } } - function openSavedFile() - { + function openSavedFile() { if (Qt.openUrlExternally(progressInfo.localPath)) return; if (Qt.openUrlExternally(progressInfo.localDir)) return; } - - function humanSize(bytes) - { - if (!bytes) - return i18nc("Unknown attachment size", "Unknown") - if (bytes < 4000) - return i18np("%1 byte", "%1 bytes", bytes) - bytes = Math.round(bytes / 100) / 10 - if (bytes < 2000) - return i18nc("KB as in kilobytes", "%1 KB", bytes) - bytes = Math.round(bytes / 100) / 10 - if (bytes < 2000) - return i18nc("MB as in megabytes", "%1 MB", bytes) - return i18nc("GB as in gigabytes", "%1 GB", Math.round(bytes / 100) / 10) - } } diff --git a/imports/NeoChat/Component/Timeline/MessageDelegate.qml b/imports/NeoChat/Component/Timeline/MessageDelegate.qml deleted file mode 100644 index a6ff6f68b..000000000 --- a/imports/NeoChat/Component/Timeline/MessageDelegate.qml +++ /dev/null @@ -1,158 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2019 Black Hat - * SPDX-FileCopyrightText: 2020 Carl Schwan - * - * SPDX-License-Identifier: GPL-3.0-only - */ -import QtQuick 2.12 -import QtQuick.Controls 2.12 as QQC2 -import QtQuick.Layouts 1.12 -import QtGraphicalEffects 1.12 - -import org.kde.kirigami 2.13 as Kirigami - -import org.kde.neochat 1.0 -import NeoChat.Setting 1.0 -import NeoChat.Component 1.0 -import NeoChat.Dialog 1.0 - -RowLayout { - default property alias innerObject : column.children - - readonly property bool sentByMe: author.isLocalUser - readonly property bool darkBackground: !sentByMe - readonly property bool replyVisible: reply ?? false - readonly property bool failed: marks == EventStatus.SendingFailed - readonly property color authorColor: eventType == "notice" ? Kirigami.Theme.activeTextColor : author.color - readonly property color replyAuthorColor: replyVisible ? reply.author.color : Kirigami.Theme.focusColor - - property alias mouseArea: controlContainer.children - property bool isEmote: false - - signal saveFileAs() - signal openExternally() - signal replyClicked(string eventID) - signal replyToMessageClicked(var replyUser, string replyContent, string eventID) - - id: root - - spacing: Kirigami.Units.smallSpacing - Layout.leftMargin: Kirigami.Units.largeSpacing - Layout.rightMargin: Kirigami.Units.smallSpacing - Layout.bottomMargin: 0 - Layout.topMargin: showAuthor ? Kirigami.Units.smallSpacing : 0 - - Kirigami.Avatar { - Layout.minimumWidth: Kirigami.Units.gridUnit * 2 - Layout.minimumHeight: Kirigami.Units.gridUnit * 2 - Layout.maximumWidth: Kirigami.Units.gridUnit * 2 - Layout.maximumHeight: Kirigami.Units.gridUnit * 2 - - Layout.alignment: Qt.AlignTop - - visible: showAuthor && Config.showAvatarInTimeline - name: author.name ?? author.displayName - source: author.avatarMediaId ? ("image://mxc/" + author.avatarMediaId) : "" - color: author.color - - Component { - id: userDetailDialog - - UserDetailDialog {} - } - - MouseArea { - anchors.fill: parent - onClicked: userDetailDialog.createObject(QQC2.ApplicationWindow.overlay, {"room": currentRoom, "user": author.object, "displayName": author.displayName, "avatarMediaId": author.avatarMediaId, "avatarUrl": author.avatarUrl}).open() - cursorShape: Qt.PointingHandCursor - } - } - - Item { - Layout.minimumWidth: Kirigami.Units.gridUnit * 2 - Layout.preferredHeight: 1 - visible: !showAuthor && Config.showAvatarInTimeline - } - - - QQC2.Control { - id: controlContainer - Layout.fillWidth: true - topPadding: 0 - bottomPadding: 0 - hoverEnabled: true - contentItem: ColumnLayout { - id: column - spacing: showAuthor ? Kirigami.Units.smallSpacing : 0 - - RowLayout { - id: rowLayout - Layout.fillWidth: true - QQC2.Label { - Layout.fillWidth: true - topInset: 0 - - visible: showAuthor && !isEmote - - text: author.displayName - font.weight: Font.Bold - color: author.color - wrapMode: Text.Wrap - } - QQC2.Label { - visible: showAuthor && !isEmote - text: time.toLocaleTimeString(Locale.ShortFormat) - color: Kirigami.Theme.disabledTextColor - } - } - Loader { - id: replyLoader - source: 'qrc:imports/NeoChat/Component/Timeline/ReplyComponent.qml' - active: replyVisible - } - Connections { - target: replyLoader.item - function onClicked() { - replyClicked(reply.eventId) - } - } - } - RowLayout { - z: 2 - anchors.bottom: controlContainer.top - anchors.bottomMargin: -Kirigami.Units.gridUnit - anchors.right: controlContainer.right - spacing: 0 - QQC2.Button { - QQC2.ToolTip.text: i18n("React") - QQC2.ToolTip.visible: hovered - visible: controlContainer.hovered - icon.name: "preferences-desktop-emoticons" - onClicked: emojiDialog.open(); - EmojiDialog { - id: emojiDialog - onReact: currentRoom.toggleReaction(eventId, emoji) - } - } - QQC2.Button { - QQC2.ToolTip.text: i18n("Edit") - QQC2.ToolTip.visible: hovered - visible: controlContainer.hovered && author.id === Controller.activeConnection.localUserId && (model.eventType === "emote" || model.eventType === "message") - icon.name: "document-edit" - onClicked: chatTextInput.edit(message, model.formattedBody, eventId) - } - QQC2.Button { - QQC2.ToolTip.text: i18n("Reply") - QQC2.ToolTip.visible: hovered - visible: controlContainer.hovered - icon.name: "mail-replied-symbolic" - onClicked: replyToMessage(author, message, eventId) - } - } - background: Rectangle { - Kirigami.Theme.colorSet: Kirigami.Theme.Window - color: !model.isHighlighted ? Kirigami.Theme.backgroundColor : Kirigami.Theme.positiveBackgroundColor - opacity: controlContainer.hovered || model.isHighlighted ? 1 : 0 - } - } -} diff --git a/imports/NeoChat/Component/Timeline/ReactionDelegate.qml b/imports/NeoChat/Component/Timeline/ReactionDelegate.qml index 26b17738b..c0a907153 100644 --- a/imports/NeoChat/Component/Timeline/ReactionDelegate.qml +++ b/imports/NeoChat/Component/Timeline/ReactionDelegate.qml @@ -10,12 +10,9 @@ import QtQuick.Layouts 1.12 import org.kde.kirigami 2.13 as Kirigami Flow { - visible: (reaction && reaction.length > 0) ?? false - spacing: Kirigami.Units.largeSpacing - Repeater { - model: reaction + model: reaction ?? null delegate: AbstractButton { width: Math.max(implicitWidth, height) @@ -31,8 +28,8 @@ Flow { radius: height / 2 Kirigami.Theme.colorSet: Kirigami.Theme.Button color: checked ? Kirigami.Theme.positiveBackgroundColor : Kirigami.Theme.backgroundColor - border.color: checked ? Kirigami.Theme.positiveTextColor : Kirigami.Theme.textColor - border.width: 1 + border.color: checked ? Kirigami.Theme.positiveTextColor : Kirigami.Theme.textColor + border.width: 1 } checkable: true diff --git a/imports/NeoChat/Component/Timeline/ReplyComponent.qml b/imports/NeoChat/Component/Timeline/ReplyComponent.qml index 641883871..bafdabe7c 100644 --- a/imports/NeoChat/Component/Timeline/ReplyComponent.qml +++ b/imports/NeoChat/Component/Timeline/ReplyComponent.qml @@ -31,7 +31,7 @@ QQC2.AbstractButton { Layout.alignment: Qt.AlignTop visible: Config.showAvatarInTimeline source: replyVisible && reply.author.avatarMediaId ? ("image://mxc/" + reply.author.avatarMediaId) : "" - name: replyVisible ? reply.author.name : "H" + name: replyVisible ? (reply.author.name || "") : "H" color: replyVisible ? reply.author.color : Kirigami.Theme.highlightColor } @@ -48,6 +48,7 @@ QQC2.AbstractButton { TextDelegate { Layout.fillWidth: true + Layout.leftMargin: 0 text: replyVisible ? ("" + reply.display) : "" textFormat: Text.RichText wrapMode: Text.WordWrap diff --git a/imports/NeoChat/Component/Timeline/StateDelegate.qml b/imports/NeoChat/Component/Timeline/StateDelegate.qml index 7ced5476c..fe8ef8bd5 100644 --- a/imports/NeoChat/Component/Timeline/StateDelegate.qml +++ b/imports/NeoChat/Component/Timeline/StateDelegate.qml @@ -16,11 +16,6 @@ import NeoChat.Setting 1.0 RowLayout { id: row - Item { - Layout.minimumWidth: Kirigami.Units.iconSizes.medium - Layout.preferredHeight: 1 - } - Kirigami.Avatar { Layout.preferredWidth: Kirigami.Units.iconSizes.small Layout.preferredHeight: Kirigami.Units.iconSizes.small diff --git a/imports/NeoChat/Component/Timeline/TextDelegate.qml b/imports/NeoChat/Component/Timeline/TextDelegate.qml index 6e3e5e4f9..d6445966e 100644 --- a/imports/NeoChat/Component/Timeline/TextDelegate.qml +++ b/imports/NeoChat/Component/Timeline/TextDelegate.qml @@ -5,12 +5,16 @@ */ import QtQuick 2.12 import QtQuick.Controls 2.12 as QQC2 +import QtQuick.Layouts 1.12 import org.kde.kirigami 2.4 as Kirigami TextEdit { id: contentLabel + Layout.margins: Kirigami.Units.largeSpacing + Layout.topMargin: 0 + readonly property var isEmoji: /^(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])+$/ property bool isEmote: false diff --git a/imports/NeoChat/Component/Timeline/TimelineContainer.qml b/imports/NeoChat/Component/Timeline/TimelineContainer.qml index 86ece1206..8f39bf6d9 100644 --- a/imports/NeoChat/Component/Timeline/TimelineContainer.qml +++ b/imports/NeoChat/Component/Timeline/TimelineContainer.qml @@ -3,34 +3,240 @@ * * SPDX-License-Identifier: GPL-3.0-only */ -import QtQuick 2.12 -import QtQuick.Controls 2.12 as Controls +import QtQuick 2.15 import QtQuick.Layouts 1.12 +import QtQuick.Controls 2.12 as QQC2 +import QtGraphicalEffects 1.12 -import org.kde.kirigami 2.4 as Kirigami +import org.kde.kirigami 2.15 as Kirigami + +import org.kde.neochat 1.0 +import NeoChat.Setting 1.0 +import NeoChat.Component 1.0 +import NeoChat.Dialog 1.0 Item { default property alias innerObject : column.children + readonly property bool sentByMe: author.isLocalUser + readonly property bool darkBackground: !sentByMe + readonly property bool replyVisible: reply ?? false + readonly property bool failed: marks == EventStatus.SendingFailed + readonly property color authorColor: eventType == "notice" ? Kirigami.Theme.activeTextColor : author.color + readonly property color replyAuthorColor: replyVisible ? reply.author.color : Kirigami.Theme.focusColor - height: column.implicitHeight + (readMarker ? 2 * Kirigami.Units.smallSpacing : 0) + property alias mouseArea: controlContainer.children + property bool isEmote: false + property bool cardBackground: true + property bool isLoaded + + property var hoverComponent + + signal saveFileAs() + signal openExternally() + signal replyClicked(string eventID) + signal replyToMessageClicked(var replyUser, string replyContent, string eventID) + + property alias hovered: controlContainer.hovered + + implicitHeight: mainColumn.implicitHeight + (readMarker ? Kirigami.Units.smallSpacing : 0) + + property int hoverComponentX: column.width - hoverComponent.childWidth + Kirigami.Units.largeSpacing + property int hoverComponentY: -Kirigami.Units.largeSpacing - hoverComponent.childHeight * 1.5 + + // show hover actions + onHoveredChanged: { + if (hovered && !Kirigami.Settings.isMobile) { + hoverComponent.x = Qt.binding(() => column.mapToItem(page, hoverComponentX, hoverComponentY).x); + hoverComponent.y = Qt.binding(() => column.mapToItem(page, hoverComponentX, hoverComponentY).y); + hoverComponent.hovered = Qt.binding(() => controlContainer.hovered); + hoverComponent.showEdit = author.id === Controller.activeConnection.localUserId && (model.eventType === "emote" || model.eventType === "message"); + + hoverComponent.editClicked = () => { + chatTextInput.edit(message, model.formattedBody, eventId); + }; + hoverComponent.replyClicked = () => { + replyToMessage(author, message, eventId); + }; + hoverComponent.reacted = emoji => { + currentRoom.toggleReaction(eventId, emoji); + }; + } + } + + DragHandler { + enabled: Kirigami.Settings.isMobile + yAxis.enabled: false + xAxis.enabled: true + xAxis.maximum: 0 + xAxis.minimum: -Kirigami.Units.gridUnit * 4 + onActiveChanged: { + applicationWindow().pageStack.interactive = true; + if (!active && parent.x < -Kirigami.Units.gridUnit * 3) { + replyToMessage(author, message, eventId) + } + parent.x = 0; + } + } + onXChanged: if (x !== 0) { + applicationWindow().pageStack.interactive = false; + } else { + applicationWindow().pageStack.interactive = true; + } ColumnLayout { - id: column + id: mainColumn width: parent.width + spacing: 0 SectionDelegate { + id: sectionDelegate Layout.maximumWidth: parent.width Layout.alignment: Qt.AlignHCenter visible: showSection } + + RowLayout { + id: root + + spacing: Kirigami.Units.smallSpacing + Layout.leftMargin: Kirigami.Units.largeSpacing + Layout.rightMargin: Kirigami.Units.smallSpacing + Layout.bottomMargin: 0 + Layout.topMargin: showAuthor ? Kirigami.Units.smallSpacing : 0 + + Kirigami.Avatar { + Layout.minimumWidth: Kirigami.Units.gridUnit * 2 + Layout.minimumHeight: Kirigami.Units.gridUnit * 2 + Layout.maximumWidth: Kirigami.Units.gridUnit * 2 + Layout.maximumHeight: Kirigami.Units.gridUnit * 2 + + Layout.alignment: Qt.AlignTop + + visible: showAuthor && Config.showAvatarInTimeline + name: author.name ?? author.displayName + source: author.avatarMediaId ? ("image://mxc/" + author.avatarMediaId) : "" + color: author.color + + Component { + id: userDetailDialog + + UserDetailDialog {} + } + + MouseArea { + anchors.fill: parent + onClicked: userDetailDialog.createObject(QQC2.ApplicationWindow.overlay, {"room": currentRoom, "user": author.object, "displayName": author.displayName, "avatarMediaId": author.avatarMediaId, "avatarUrl": author.avatarUrl}).open() + cursorShape: Qt.PointingHandCursor + } + } + + Item { + Layout.minimumWidth: Kirigami.Units.gridUnit * 2 + Layout.preferredHeight: 1 + visible: !showAuthor && Config.showAvatarInTimeline + } + + // bubble + QQC2.Control { + id: controlContainer + //Layout.fillWidth: true + Layout.maximumWidth: mainColumn.width - Kirigami.Units.gridUnit * 2 - Kirigami.Units.largeSpacing * 2 + topPadding: 0 + bottomPadding: 0 + leftPadding: 0 + rightPadding: 0 + hoverEnabled: true + + contentItem: ColumnLayout { + id: column + spacing: 0 + Item { // top padding + Layout.topMargin: Kirigami.Units.largeSpacing + } + // HACK: reload author when the delegate is reloaded, since there are strange issues with ListView reuseItems and the author displayName disappearing + Loader { + id: topRow + active: isLoaded && showAuthor && !isEmote + visible: active + Layout.fillWidth: true + Layout.leftMargin: Kirigami.Units.largeSpacing + Layout.rightMargin: Kirigami.Units.largeSpacing + Layout.bottomMargin: visible ? Kirigami.Units.smallSpacing : 0 + + sourceComponent: RowLayout { + id: rowLayout + + QQC2.Label { + Layout.fillWidth: true + topInset: 0 + + visible: showAuthor && !isEmote + + text: author.displayName + font.weight: Font.Bold + color: author.color + wrapMode: Text.Wrap + } + QQC2.Label { + visible: showAuthor && !isEmote + text: time.toLocaleTimeString(Locale.ShortFormat) + color: Kirigami.Theme.disabledTextColor + } + } + } + Loader { + id: replyLoader + source: 'qrc:imports/NeoChat/Component/Timeline/ReplyComponent.qml' + active: replyVisible + visible: active + Layout.bottomMargin: Kirigami.Units.smallSpacing + + Connections { + target: replyLoader.item + function onClicked() { + replyClicked(reply.eventId) + } + } + } + } + + background: Kirigami.ShadowedRectangle { + visible: cardBackground + color: Kirigami.Theme.backgroundColor + radius: Kirigami.Units.smallSpacing + shadow.size: Kirigami.Units.smallSpacing + shadow.color: !model.isHighlighted ? Qt.rgba(0.0, 0.0, 0.0, 0.10) : Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.10) + border.color: Kirigami.ColorUtils.tintWithAlpha(color, Kirigami.Theme.textColor, 0.15) + border.width: Kirigami.Units.devicePixelRatio + } + } + } + Loader { + id: loader + Layout.fillWidth: true + Layout.leftMargin: Kirigami.Units.gridUnit * 2 + Kirigami.Units.largeSpacing * 2 + Layout.topMargin: active ? Kirigami.Units.smallSpacing : 0 + active: eventType !== "state" && eventType !== "notice" && reaction != undefined && reaction.length > 0 + visible: active + sourceComponent: ReactionDelegate { } + } + } + + Kirigami.Icon { + id: replyButton + visible: parent.x < - Kirigami.Units.gridUnit * 1 + opacity: -(parent.x + Kirigami.Units.gridUnit) / Kirigami.Units.gridUnit / 3 + anchors.left: parent.right + anchors.top: parent.top + source: "mail-replied-symbolic" } Rectangle { width: parent.width * 0.9 x: parent.width * 0.05 - height: Kirigami.Units.smallSpacing - anchors.top: column.bottom + height: Kirigami.Units.smallSpacing / 2 + anchors.top: mainColumn.bottom anchors.topMargin: Kirigami.Units.smallSpacing visible: readMarker color: Kirigami.Theme.positiveTextColor diff --git a/imports/NeoChat/Component/Timeline/VideoDelegate.qml b/imports/NeoChat/Component/Timeline/VideoDelegate.qml index 9bf2e8389..6cac32cca 100644 --- a/imports/NeoChat/Component/Timeline/VideoDelegate.qml +++ b/imports/NeoChat/Component/Timeline/VideoDelegate.qml @@ -21,7 +21,6 @@ import NeoChat.Menu.Timeline 1.0 Video { id: vid - property bool openOnFinished: false property bool playOnFinished: false readonly property bool downloaded: progressInfo && progressInfo.completed @@ -32,10 +31,6 @@ Video { vid.source = progressInfo.localPath } - if (downloaded && openOnFinished) { - openSavedFile() - openOnFinished = false - } if (downloaded && playOnFinished) { playSavedFile() playOnFinished = false @@ -105,36 +100,6 @@ Video { } } - Control { - anchors.bottom: parent.bottom - anchors.bottomMargin: 8 - anchors.right: parent.right - anchors.rightMargin: 8 - - horizontalPadding: 8 - verticalPadding: 4 - - contentItem: RowLayout { - Label { - text: Qt.formatTime(time) - color: "white" - font.pixelSize: 12 - } - - Label { - text: author.displayName - color: "white" - font.pixelSize: 12 - } - } - - background: Rectangle { - radius: height / 2 - color: "black" - opacity: 0.3 - } - } - Rectangle { anchors.fill: parent @@ -153,100 +118,29 @@ Video { } } - AutoMouseArea { - anchors.fill: parent - - id: messageMouseArea - - onPrimaryClicked: { - if (supportStreaming || progressInfo.completed) { - if (vid.playbackState == MediaPlayer.PlayingState) { - vid.pause() - } else { - vid.play() - } + TapHandler { + acceptedButtons: Qt.LeftButton + onTapped: if (supportStreaming || progressInfo.completed) { + if (vid.playbackState == MediaPlayer.PlayingState) { + vid.pause() } else { - downloadAndPlay() + vid.play() } - } - - onSecondaryClicked: { - var contextMenu = imageDelegateContextMenu.createObject(vid, {'room': currentRoom, 'author': author}) - contextMenu.viewSource.connect(function() { - messageSourceSheet.createObject(ApplicationWindow.overlay, {"sourceText": toolTip}).open() - }) - contextMenu.downloadAndOpen.connect(downloadAndOpen) - contextMenu.saveFileAs.connect(saveFileAs) - contextMenu.reply.connect(function() { - roomPanelInput.replyModel = Object.assign({}, model) - roomPanelInput.isReply = true - roomPanelInput.focus() - }) - contextMenu.redact.connect(function() { - currentRoom.redactEvent(eventId) - }) - contextMenu.popup() - } - - Component { - id: messageSourceSheet - - MessageSourceSheet {} - } - - Component { - id: openFolderDialog - - OpenFolderDialog {} - } - - Component { - id: imageDelegateContextMenu - - FileDelegateContextMenu {} + } else { + downloadAndPlay() } } - function saveFileAs() { - var folderDialog = openFolderDialog.createObject(ApplicationWindow.overlay) - - folderDialog.chosen.connect(function(path) { - if (!path) return - - currentRoom.downloadFile(eventId, path + "/" + currentRoom.fileNameToDownload(eventId)) - }) - - folderDialog.open() - } - - function downloadAndOpen() - { - if (downloaded) openSavedFile() - else - { - openOnFinished = true - currentRoom.downloadFile(eventId, Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId)) - } - } - - function downloadAndPlay() - { - if (downloaded) playSavedFile() - else - { + function downloadAndPlay() { + if (downloaded) { + playSavedFile() + } else { playOnFinished = true currentRoom.downloadFile(eventId, Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId)) } } - function openSavedFile() - { - if (Qt.openUrlExternally(progressInfo.localPath)) return; - if (Qt.openUrlExternally(progressInfo.localDir)) return; - } - - function playSavedFile() - { + function playSavedFile() { vid.stop() vid.play() } diff --git a/imports/NeoChat/Component/Timeline/qmldir b/imports/NeoChat/Component/Timeline/qmldir index 83469407d..c70f8d63a 100644 --- a/imports/NeoChat/Component/Timeline/qmldir +++ b/imports/NeoChat/Component/Timeline/qmldir @@ -1,6 +1,5 @@ module NeoChat.Component.Timeline TimelineContainer 1.0 TimelineContainer.qml -MessageDelegate 1.0 MessageDelegate.qml TextDelegate 1.0 TextDelegate.qml StateDelegate 1.0 StateDelegate.qml SectionDelegate 1.0 SectionDelegate.qml diff --git a/imports/NeoChat/Menu/Timeline/FileDelegateContextMenu.qml b/imports/NeoChat/Menu/Timeline/FileDelegateContextMenu.qml index f3e3f79e2..c16d98c3b 100644 --- a/imports/NeoChat/Menu/Timeline/FileDelegateContextMenu.qml +++ b/imports/NeoChat/Menu/Timeline/FileDelegateContextMenu.qml @@ -5,50 +5,43 @@ */ import QtQuick 2.12 import QtQuick.Controls 2.12 - +import org.kde.kirigami 2.14 as Kirigami import NeoChat.Dialog 1.0 +import NeoChat.Menu 1.0 -Menu { +MessageDelegateContextMenu { id: root - required property var room - required property var author - - signal viewSource() signal downloadAndOpen() signal saveFileAs() - signal reply() - signal redact() - MenuItem { - text: i18n("View Source") - - onTriggered: viewSource() - } - - MenuItem { - text: i18n("Open Externally") - - onTriggered: downloadAndOpen() - } - - MenuItem { - text: i18n("Save As") - - onTriggered: saveFileAs() - } - - MenuItem { - text: i18n("Reply") - - onTriggered: reply() - } - - MenuItem { - visible: room.canSendState("redact") || room.localUser.id === author.id - text: i18n("Redact") - onTriggered: redact() - } - - onClosed: destroy() + property list actions: [ + Kirigami.Action { + text: i18n("Open Externally") + icon.name: "document-open" + onTriggered: downloadAndOpen() + }, + Kirigami.Action { + text: i18n("Save As") + icon.name: "document-save" + onTriggered: saveFileAs() + }, + Kirigami.Action { + text: i18n("Reply") + icon.name: "mail-replied-symbolic" + onTriggered: reply(author, message) + }, + Kirigami.Action { + visible: author.id === currentRoom.localUser.id || currentRoom.canSendState("redact") + text: i18n("Remove") + icon.name: "edit-delete-remove" + icon.color: "red" + onTriggered: remove() + }, + Kirigami.Action { + text: i18n("View Source") + icon.name: "code-context" + onTriggered: viewSource() + } + ] } diff --git a/imports/NeoChat/Menu/Timeline/MessageDelegateContextMenu.qml b/imports/NeoChat/Menu/Timeline/MessageDelegateContextMenu.qml index bdfc328d7..6c0706969 100644 --- a/imports/NeoChat/Menu/Timeline/MessageDelegateContextMenu.qml +++ b/imports/NeoChat/Menu/Timeline/MessageDelegateContextMenu.qml @@ -4,16 +4,16 @@ * * SPDX-License-Identifier: GPL-3.0-only */ -import QtQuick 2.12 -import QtQuick.Controls 2.12 as QQC2 +import QtQuick 2.15 +import QtQuick.Controls 2.15 as QQC2 import QtQuick.Layouts 1.12 -import org.kde.kirigami 2.13 as Kirigami +import org.kde.kirigami 2.14 as Kirigami import NeoChat.Dialog 1.0 import org.kde.neochat 1.0 -Kirigami.OverlaySheet { - id: root +Loader { + id: loadRoot required property var author required property string message @@ -23,99 +23,235 @@ Kirigami.OverlaySheet { signal reply(var author, string message) signal remove() - parent: applicationWindow().overlay - - leftPadding: 0 - rightPadding: 0 - - ColumnLayout { - spacing: 0 - RowLayout { - id: headerLayout - Layout.fillWidth: true - Layout.leftMargin: Kirigami.Units.largeSpacing - Layout.rightMargin: Kirigami.Units.largeSpacing - spacing: Kirigami.Units.largeSpacing - Kirigami.Avatar { - id: avatar - source: author.avatarMediaId ? ("image://mxc/" + author.avatarMediaId) : "" - Layout.preferredWidth: Kirigami.Units.gridUnit * 3 - Layout.preferredHeight: Kirigami.Units.gridUnit * 3 - Layout.alignment: Qt.AlignTop - } - ColumnLayout { - Layout.fillWidth: true - Kirigami.Heading { - level: 3 - Layout.fillWidth: true - text: author.displayName - wrapMode: Text.WordWrap - } - QQC2.Label { - text: message - Layout.fillWidth: true - wrapMode: Text.WordWrap - - onLinkActivated: { - applicationWindow().handleLink(link, currentRoom) - } - } - } - } - Row { - spacing: 0 - Repeater { - model: ["👍", "👎️", "😄", "🎉", "🚀", "👀"] - delegate: QQC2.ItemDelegate { - width: 32 - height: 32 - - contentItem: QQC2.Label { - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - - font.pixelSize: 16 - font.family: "emoji" - text: modelData - - } - - onClicked: { - currentRoom.toggleReaction(eventId, modelData) - root.close(); - } - } - } - } - Kirigami.BasicListItem { - action: Kirigami.Action { - text: i18n("Reply") - icon.name: "mail-replied-symbolic" - onTriggered: reply(author, message) - } - } - Kirigami.BasicListItem { + property list actions: [ + Kirigami.Action { + text: i18n("Reply") + icon.name: "mail-replied-symbolic" + onTriggered: reply(author, message) + }, + Kirigami.Action { visible: author.id === currentRoom.localUser.id || currentRoom.canSendState("redact") - action: Kirigami.Action { - text: i18n("Remove") - icon.name: "edit-delete-remove" - icon.color: "red" - onTriggered: remove() - } + text: i18n("Remove") + icon.name: "edit-delete-remove" + icon.color: "red" + onTriggered: remove() + }, + Kirigami.Action { + text: i18n("Copy") + icon.name: "edit-copy" + onTriggered: Clipboard.saveText(message) + }, + Kirigami.Action { + text: i18n("View Source") + icon.name: "code-context" + onTriggered: viewSource() } - Kirigami.BasicListItem { - action: Kirigami.Action { - text: i18n("Copy") - icon.name: "edit-copy" - onTriggered: Clipboard.saveText(message) - } - } - Kirigami.BasicListItem { - action: Kirigami.Action { - text: i18n("View Source") - icon.name: "code-context" - onTriggered: viewSource() + ] + + Component { + id: regularMenu + + Kirigami.OverlaySheet { + id: root + + parent: applicationWindow().overlay + + leftPadding: 0 + rightPadding: 0 + + contentItem: ColumnLayout { + spacing: 0 + RowLayout { + id: headerLayout + Layout.fillWidth: true + Layout.leftMargin: Kirigami.Units.largeSpacing + Layout.rightMargin: Kirigami.Units.largeSpacing + spacing: Kirigami.Units.largeSpacing + Kirigami.Avatar { + id: avatar + source: author.avatarMediaId ? ("image://mxc/" + author.avatarMediaId) : "" + Layout.preferredWidth: Kirigami.Units.gridUnit * 3 + Layout.preferredHeight: Kirigami.Units.gridUnit * 3 + Layout.alignment: Qt.AlignTop + } + ColumnLayout { + Layout.fillWidth: true + Kirigami.Heading { + level: 3 + Layout.fillWidth: true + text: author.displayName + wrapMode: Text.WordWrap + } + QQC2.Label { + text: message + Layout.fillWidth: true + Layout.maximumWidth: Kirigami.Units.gridUnit * 24 + wrapMode: Text.WordWrap + + onLinkActivated: { + applicationWindow().handleLink(link, currentRoom) + } + } + } + } + Kirigami.Separator { + Layout.fillWidth: true + } + RowLayout { + spacing: 0 + Layout.fillWidth: true + Layout.preferredHeight: Kirigami.Units.gridUnit * 2.5 + Repeater { + model: ["👍", "👎️", "😄", "🎉", "🚀", "👀"] + delegate: QQC2.ItemDelegate { + Layout.fillWidth: true + Layout.fillHeight: true + + contentItem: QQC2.Label { + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + + font.pixelSize: 16 + font.family: "emoji" + text: modelData + + } + + onClicked: { + currentRoom.toggleReaction(eventId, modelData) + loadRoot.item.close(); + } + } + } + } + Kirigami.Separator { + Layout.fillWidth: true + } + Repeater { + model: loadRoot.actions + Kirigami.BasicListItem { + visible: modelData.visible + action: modelData + onClicked: { + modelData.triggered(); + loadRoot.item.close(); + } + } + } } } } + Component { + id: mobileMenu + + Kirigami.OverlayDrawer { + id: drawer + height: popupContent.implicitHeight + edge: Qt.BottomEdge + padding: 0 + leftPadding: 0 + rightPadding: 0 + bottomPadding: 0 + topPadding: 0 + + parent: applicationWindow().overlay + + ColumnLayout { + id: popupContent + width: parent.width + spacing: 0 + RowLayout { + id: headerLayout + Layout.fillWidth: true + Layout.margins: Kirigami.Units.largeSpacing + spacing: Kirigami.Units.largeSpacing + Kirigami.Avatar { + id: avatar + source: author.avatarMediaId ? ("image://mxc/" + author.avatarMediaId) : "" + Layout.preferredWidth: Kirigami.Units.gridUnit * 3 + Layout.preferredHeight: Kirigami.Units.gridUnit * 3 + Layout.alignment: Qt.AlignTop + } + ColumnLayout { + Layout.fillWidth: true + Kirigami.Heading { + level: 3 + Layout.fillWidth: true + text: author.displayName + wrapMode: Text.WordWrap + } + QQC2.Label { + text: message + Layout.fillWidth: true + wrapMode: Text.WordWrap + + onLinkActivated: { + applicationWindow().handleLink(link, currentRoom) + } + } + } + } + Kirigami.Separator { + Layout.fillWidth: true + } + RowLayout { + spacing: 0 + Layout.fillWidth: true + Layout.preferredHeight: Kirigami.Units.gridUnit * 2.5 + Repeater { + model: ["👍", "👎️", "😄", "🎉", "🚀", "👀"] + delegate: QQC2.ItemDelegate { + Layout.fillWidth: true + Layout.fillHeight: true + + contentItem: Kirigami.Heading { + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + + font.family: "emoji" + text: modelData + } + + onClicked: { + currentRoom.toggleReaction(eventId, modelData); + loadRoot.item.close(); + } + } + } + } + Kirigami.Separator { + Layout.fillWidth: true + } + Repeater { + id: listViewAction + model: loadRoot.actions + + Kirigami.BasicListItem { + icon: modelData.icon.name + iconColor: modelData.icon.color ?? undefined + enabled: modelData.enabled + visible: modelData.visible + text: modelData.text + onClicked: { + modelData.triggered() + loadRoot.item.close(); + } + implicitHeight: visible ? Kirigami.Units.gridUnit * 3 : 0 + } + } + } + } + } + + asynchronous: true + sourceComponent: Kirigami.Settings.isMobile ? mobileMenu : regularMenu + + function open() { + active = true; + } + + onStatusChanged: if (status == Loader.Ready) { + item.open(); + } } + diff --git a/imports/NeoChat/Page/RoomPage.qml b/imports/NeoChat/Page/RoomPage.qml index 9dce8ca07..7f8c7e472 100644 --- a/imports/NeoChat/Page/RoomPage.qml +++ b/imports/NeoChat/Page/RoomPage.qml @@ -127,6 +127,58 @@ Kirigami.ScrollablePage { } } + // hover actions on a delegate, activated in TimelineContainer.qml + Item { + id: hoverActions + property bool showEdit + property bool hovered: false + + visible: (hovered || hoverHandler.hovered) && !Kirigami.Settings.isMobile + + property var editClicked + property var replyClicked + property var reacted + + property alias childWidth: hoverActionsRow.width + property alias childHeight: hoverActionsRow.height + + RowLayout { + id: hoverActionsRow + z: 4 + spacing: 0 + HoverHandler { + id: hoverHandler + margin: Kirigami.Units.smallSpacing + } + + QQC2.Button { + QQC2.ToolTip.text: i18n("React") + QQC2.ToolTip.visible: hovered + visible: actions.hovered + icon.name: "preferences-desktop-emoticons" + onClicked: emojiDialog.open(); + EmojiDialog { + id: emojiDialog + onReact: hoverActions.reacted(emoji) + } + } + QQC2.Button { + QQC2.ToolTip.text: i18n("Edit") + QQC2.ToolTip.visible: hovered + visible: actions.hovered && showEdit + icon.name: "document-edit" + onClicked: hoverActions.editClicked() + } + QQC2.Button { + QQC2.ToolTip.text: i18n("Reply") + QQC2.ToolTip.visible: hovered + visible: actions.hovered + icon.name: "mail-replied-symbolic" + onClicked: hoverActions.replyClicked() + } + } + } + ListView { id: messageListView @@ -137,7 +189,7 @@ Kirigami.ScrollablePage { readonly property bool isLoaded: page.width * page.height > 10 spacing: Kirigami.Units.smallSpacing - clip: true + reuseItems: true verticalLayoutDirection: ListView.BottomToTop highlightMoveDuration: 500 @@ -232,51 +284,62 @@ Kirigami.ScrollablePage { sourceModel: messageEventModel } - // populate: Transition { - // NumberAnimation { - // property: "opacity"; from: 0; to: 1 - // duration: 200 - // } - // } + populate: Transition { + NumberAnimation { + property: "opacity"; from: 0; to: 1 + duration: Kirigami.Units.shortDuration + } + } - // add: Transition { - // NumberAnimation { - // property: "opacity"; from: 0; to: 1 - // duration: 200 - // } - // } + add: Transition { + NumberAnimation { + property: "opacity"; from: 0; to: 1 + duration: Kirigami.Units.shortDuration + } + } - // move: Transition { - // NumberAnimation { - // property: "y"; duration: 200 - // } - // NumberAnimation { - // property: "opacity"; to: 1 - // } - // } + move: Transition { + NumberAnimation { + property: "y" + duration: Kirigami.Units.shortDuration + } + NumberAnimation { + property: "opacity"; to: 1 + } + } - // displaced: Transition { - // NumberAnimation { - // property: "y"; duration: 200 - // easing.type: Easing.OutQuad - // } - // NumberAnimation { - // property: "opacity"; to: 1 - // } - // } + displaced: Transition { + NumberAnimation { + property: "y" + duration: Kirigami.Units.shortDuration + easing.type: Easing.OutQuad + } + NumberAnimation { + property: "opacity"; to: 1 + } + } delegate: DelegateChooser { id: timelineDelegateChooser role: "eventType" + property bool delegateLoaded: true + ListView.onPooled: delegateLoaded = false + ListView.onReused: delegateLoaded = true + DelegateChoice { roleValue: "state" delegate: TimelineContainer { - width: messageListView.width + id: container + width: messageListView.width - Kirigami.Units.largeSpacing + isLoaded: timelineDelegateChooser.delegateLoaded + cardBackground: false + + hoverComponent: hoverActions innerObject: StateDelegate { - Layout.maximumWidth: parent.width - Layout.alignment: Qt.AlignHCenter + Layout.maximumWidth: container.width + Layout.alignment: Qt.AlignLeft } } } @@ -284,30 +347,24 @@ Kirigami.ScrollablePage { DelegateChoice { roleValue: "emote" delegate: TimelineContainer { - width: messageListView.width - innerObject: MessageDelegate { - Layout.fillWidth: true - Layout.maximumWidth: messageListView.width + width: messageListView.width - Kirigami.Units.largeSpacing + isLoaded: timelineDelegateChooser.delegateLoaded + isEmote: true + mouseArea: MouseArea { + acceptedButtons: Qt.RightButton + anchors.fill: parent + onClicked: openMessageContext(author, display, eventId, toolTip); + } + onReplyClicked: goToEvent(eventID) + onReplyToMessageClicked: replyToMessage(replyUser, replyContent, eventId); + + hoverComponent: hoverActions + + innerObject: TextDelegate { isEmote: true - mouseArea: MouseArea { - acceptedButtons: Qt.RightButton - anchors.fill: parent - onClicked: openMessageContext(author, display, eventId, toolTip); - } - onReplyClicked: goToEvent(eventID) - onReplyToMessageClicked: replyToMessage(replyUser, replyContent, eventId); - innerObject: [ - TextDelegate { - isEmote: true - Layout.fillWidth: true - Layout.rightMargin: Kirigami.Units.largeSpacing - }, - ReactionDelegate { - Layout.fillWidth: true - Layout.topMargin: 0 - Layout.bottomMargin: Kirigami.Units.largeSpacing * 2 - } - ] + Layout.fillWidth: true + Layout.rightMargin: Kirigami.Units.largeSpacing + Layout.leftMargin: Kirigami.Units.largeSpacing } } } @@ -315,31 +372,27 @@ Kirigami.ScrollablePage { DelegateChoice { roleValue: "message" delegate: TimelineContainer { - width: messageListView.width - innerObject: MessageDelegate { + id: timeline + width: messageListView.width - Kirigami.Units.largeSpacing + + isLoaded: timelineDelegateChooser.delegateLoaded + onReplyClicked: goToEvent(eventID) + onReplyToMessageClicked: replyToMessage(replyUser, replyContent, eventId); + + hoverComponent: hoverActions + + innerObject: TextDelegate { Layout.fillWidth: true - Layout.maximumWidth: messageListView.width - onReplyClicked: goToEvent(eventID) - onReplyToMessageClicked: replyToMessage(replyUser, replyContent, eventId); - innerObject: [ - TextDelegate { - Layout.fillWidth: true - Layout.rightMargin: Kirigami.Units.largeSpacing - TapHandler { - acceptedButtons: Qt.RightButton - onTapped: openMessageContext(author, display, eventId, toolTip) - } - TapHandler { - acceptedButtons: Qt.LeftButton - onLongPressed: openMessageContext(author, display, eventId, toolTip) - } - }, - ReactionDelegate { - Layout.fillWidth: true - Layout.topMargin: 0 - Layout.bottomMargin: Kirigami.Units.largeSpacing - } - ] + Layout.rightMargin: Kirigami.Units.largeSpacing + Layout.leftMargin: Kirigami.Units.largeSpacing + TapHandler { + acceptedButtons: Qt.RightButton + onTapped: openMessageContext(author, display, eventId, toolTip) + } + TapHandler { + acceptedButtons: Qt.LeftButton + onLongPressed: openMessageContext(author, display, eventId, toolTip) + } } } } @@ -347,16 +400,16 @@ Kirigami.ScrollablePage { DelegateChoice { roleValue: "notice" delegate: TimelineContainer { - width: messageListView.width + width: messageListView.width - Kirigami.Units.largeSpacing + isLoaded: timelineDelegateChooser.delegateLoaded + onReplyClicked: goToEvent(eventID) + onReplyToMessageClicked: replyToMessage(replyUser, replyContent, eventId); - innerObject: MessageDelegate { + hoverComponent: hoverActions + innerObject: TextDelegate { Layout.fillWidth: true - onReplyClicked: goToEvent(eventID) - onReplyToMessageClicked: replyToMessage(replyUser, replyContent, eventId); - - innerObject: TextDelegate { - Layout.fillWidth: true - } + Layout.rightMargin: Kirigami.Units.largeSpacing + Layout.leftMargin: Kirigami.Units.largeSpacing } } } @@ -364,26 +417,20 @@ Kirigami.ScrollablePage { DelegateChoice { roleValue: "image" delegate: TimelineContainer { - width: messageListView.width + width: messageListView.width - Kirigami.Units.largeSpacing - innerObject: MessageDelegate { + isLoaded: timelineDelegateChooser.delegateLoaded + onReplyClicked: goToEvent(eventID) + onReplyToMessageClicked: replyToMessage(replyUser, replyContent, eventId); + + hoverComponent: hoverActions + + innerObject: ImageDelegate { + Layout.minimumWidth: Kirigami.Units.gridUnit * 10 Layout.fillWidth: true - onReplyClicked: goToEvent(eventID) - onReplyToMessageClicked: replyToMessage(replyUser, replyContent, eventId); - - innerObject: [ - ImageDelegate { - Layout.maximumWidth: parent.width - Layout.minimumWidth: 320 - Layout.preferredHeight: info.h / info.w * width - }, - ReactionDelegate { - Layout.fillWidth: true - Layout.topMargin: 0 - Layout.maximumHeight: 320 - Layout.bottomMargin: Kirigami.Units.largeSpacing - } - ] + Layout.preferredHeight: info.h / info.w * width + Layout.maximumHeight: Kirigami.Units.gridUnit * 15 + Layout.maximumWidth: Kirigami.Units.gridUnit * 30 } } } @@ -391,27 +438,20 @@ Kirigami.ScrollablePage { DelegateChoice { roleValue: "sticker" delegate: TimelineContainer { - width: messageListView.width + width: messageListView.width - Kirigami.Units.largeSpacing - innerObject: MessageDelegate { - Layout.fillWidth: true - onReplyClicked: goToEvent(eventID) - onReplyToMessageClicked: replyToMessage(replyUser, replyContent, eventId); + isLoaded: timelineDelegateChooser.delegateLoaded + onReplyClicked: goToEvent(eventID) + onReplyToMessageClicked: replyToMessage(replyUser, replyContent, eventId); - innerObject: [ - ImageDelegate { - readonly: true - Layout.maximumWidth: parent.width / 2 - Layout.minimumWidth: 320 - Layout.preferredHeight: info.h / info.w * width - }, - ReactionDelegate { - Layout.fillWidth: true - Layout.topMargin: 0 - Layout.maximumHeight: 320 - Layout.bottomMargin: Kirigami.Units.largeSpacing - } - ] + hoverComponent: hoverActions + cardBackground: false + + innerObject: ImageDelegate { + readonly: true + Layout.maximumWidth: Kirigami.Units.gridUnit * 10 + Layout.minimumWidth: Kirigami.Units.gridUnit * 10 + Layout.preferredHeight: info.h / info.w * width } } } @@ -419,25 +459,23 @@ Kirigami.ScrollablePage { DelegateChoice { roleValue: "audio" delegate: TimelineContainer { - width: messageListView.width + width: messageListView.width - Kirigami.Units.largeSpacing - innerObject: MessageDelegate { + hoverComponent: hoverActions + + isLoaded: timelineDelegateChooser.delegateLoaded + onReplyClicked: goToEvent(eventID) + onReplyToMessageClicked: replyToMessage(replyUser, replyContent, eventId); + + innerObject: AudioDelegate { Layout.fillWidth: true - onReplyClicked: goToEvent(eventID) - mouseArea: MouseArea { - acceptedButtons: (Kirigami.Settings.isMobile ? Qt.LeftButton : 0) | Qt.RightButton - anchors.fill: parent - onClicked: { - if (mouse.button == Qt.RightButton) { - openMessageContext(author, display, eventId, toolTip); - } - } - onPressAndHold: openMessageContext(author, display, eventId, toolTip); + TapHandler { + acceptedButtons: Qt.RightButton + onTapped: openFileContext(author, display, eventId, toolTip, progressInfo, parent) } - onReplyToMessageClicked: replyToMessage(replyUser, replyContent, eventId); - - innerObject: AudioDelegate { - Layout.fillWidth: true + TapHandler { + acceptedButtons: Qt.LeftButton + onLongPressed: openFileContext(author, display, eventId, toolTip, progressInfo, parent) } } } @@ -446,28 +484,29 @@ Kirigami.ScrollablePage { DelegateChoice { roleValue: "video" delegate: TimelineContainer { - width: messageListView.width + width: messageListView.width - Kirigami.Units.largeSpacing - innerObject: MessageDelegate { + hoverComponent: hoverActions + + isLoaded: timelineDelegateChooser.delegateLoaded + onReplyClicked: goToEvent(eventID) + onReplyToMessageClicked: replyToMessage(replyUser, replyContent, eventId); + + innerObject: VideoDelegate { Layout.fillWidth: true - onReplyClicked: goToEvent(eventID) - mouseArea: MouseArea { - acceptedButtons: (Kirigami.Settings.isMobile ? Qt.LeftButton : 0) | Qt.RightButton - anchors.fill: parent - onClicked: { - if (mouse.button == Qt.RightButton) { - openMessageContext(author, display, eventId, toolTip); - } - } - onPressAndHold: openMessageContext(author, display, eventId, toolTip); - } - onReplyToMessageClicked: replyToMessage(replyUser, replyContent, eventId); + Layout.minimumWidth: Kirigami.Units.gridUnit * 10 + Layout.maximumWidth: Kirigami.Units.gridUnit * 30 + Layout.preferredHeight: content.info.h / content.info.w * width + Layout.maximumHeight: Kirigami.Units.gridUnit * 15 + Layout.minimumHeight: Kirigami.Units.gridUnit * 5 - innerObject: VideoDelegate { - Layout.maximumWidth: parent.width - Layout.minimumWidth: 320 - Layout.maximumHeight: 320 - Layout.preferredHeight: content.info.h / content.info.w * width + TapHandler { + acceptedButtons: Qt.RightButton + onTapped: openFileContext(author, display, eventId, toolTip, progressInfo, parent) + } + TapHandler { + acceptedButtons: Qt.LeftButton + onLongPressed: openFileContext(author, display, eventId, toolTip, progressInfo, parent) } } } @@ -476,15 +515,23 @@ Kirigami.ScrollablePage { DelegateChoice { roleValue: "file" delegate: TimelineContainer { - width: messageListView.width + width: messageListView.width - Kirigami.Units.largeSpacing - innerObject: MessageDelegate { + hoverComponent: hoverActions + + isLoaded: timelineDelegateChooser.delegateLoaded + onReplyClicked: goToEvent(eventID) + onReplyToMessageClicked: replyToMessage(replyUser, replyContent, eventId); + + innerObject: FileDelegate { Layout.fillWidth: true - onReplyClicked: goToEvent(eventID) - onReplyToMessageClicked: replyToMessage(replyUser, replyContent, eventId); - - innerObject: FileDelegate { - Layout.fillWidth: true + TapHandler { + acceptedButtons: Qt.RightButton + onTapped: openFileContext(author, display, eventId, toolTip, progressInfo, parent) + } + TapHandler { + acceptedButtons: Qt.LeftButton + onLongPressed: openFileContext(author, display, eventId, toolTip, progressInfo, parent) } } } @@ -583,6 +630,18 @@ Kirigami.ScrollablePage { MessageSourceSheet {} } + + Component { + id: openFolderDialog + + OpenFolderDialog {} + } + + Component { + id: fileDelegateContextMenu + + FileDelegateContextMenu {} + } } footer: ChatTextInput { @@ -626,8 +685,6 @@ Kirigami.ScrollablePage { } } - Kirigami.Theme.colorSet: Kirigami.Theme.View - function goToEvent(eventID) { messageListView.positionViewAtIndex(eventToIndex(eventID), ListView.Contain) } @@ -661,7 +718,56 @@ Kirigami.ScrollablePage { return index; } - function openMessageContext(author, message, eventId, toolTip, model) { + /// Open message context dialog for file and videos + function openFileContext(author, message, eventId, toolTip, progressInfo, file) { + const contextMenu = fileDelegateContextMenu.createObject(page, { + 'author': author, + 'message': message, + 'eventId': eventId, + }); + contextMenu.downloadAndOpen.connect(function() { + if (file.downloaded) { + if (!Qt.openUrlExternally(progressInfo.localPath)) { + Qt.openUrlExternally(progressInfo.localDir); + } + } else { + file.onDownloadedChanged.connect(function() { + if (!Qt.openUrlExternally(progressInfo.localPath)) { + Qt.openUrlExternally(progressInfo.localDir); + } + }); + currentRoom.downloadFile(eventId, Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId)) + } + }); + contextMenu.saveFileAs.connect(function() { + const folderDialog = openFolderDialog.createObject(ApplicationWindow.overlay) + + folderDialog.chosen.connect(function(path) { + if (!path) { + return; + } + + currentRoom.downloadFile(eventId, path + "/" + currentRoom.fileNameToDownload(eventId)) + }) + + folderDialog.open() + }) + contextMenu.viewSource.connect(function() { + messageSourceSheet.createObject(page, { + 'sourceText': toolTip, + }).open(); + }); + contextMenu.reply.connect(function(replyUser, replyContent) { + replyToMessage(replyUser, replyContent, eventId); + }) + contextMenu.remove.connect(function() { + currentRoom.redactEvent(eventId); + }) + contextMenu.open(); + } + + /// Open context menu for normal message + function openMessageContext(author, message, eventId, toolTip) { const contextMenu = messageDelegateContextMenu.createObject(page, { 'author': author, 'message': message, @@ -671,17 +777,14 @@ Kirigami.ScrollablePage { messageSourceSheet.createObject(page, { 'sourceText': toolTip, }).open(); - contextMenu.close(); }); contextMenu.reply.connect(function(replyUser, replyContent) { replyToMessage(replyUser, replyContent, eventId); - contextMenu.close(); }) contextMenu.remove.connect(function() { currentRoom.redactEvent(eventId); - contextMenu.close(); }) - contextMenu.open() + contextMenu.open(); } function replyToMessage(replyUser, replyContent, eventId) { diff --git a/qml/main.qml b/qml/main.qml index ac3b7fe5e..5f0b11f9a 100644 --- a/qml/main.qml +++ b/qml/main.qml @@ -162,6 +162,8 @@ Kirigami.ApplicationWindow { handleVisible: enabled && pageStack.layers.depth < 2 && pageStack.depth < 3 } + pageStack.columnView.columnWidth: Kirigami.Units.gridUnit * 17 + globalDrawer: Kirigami.GlobalDrawer { property bool hasLayer contentItem.implicitWidth: columnWidth diff --git a/res.qrc b/res.qrc index b85580498..ce7809b13 100644 --- a/res.qrc +++ b/res.qrc @@ -22,7 +22,6 @@ imports/NeoChat/Component/Emoji/EmojiPicker.qml imports/NeoChat/Component/Emoji/qmldir imports/NeoChat/Component/Timeline/qmldir - imports/NeoChat/Component/Timeline/MessageDelegate.qml imports/NeoChat/Component/Timeline/ReplyComponent.qml imports/NeoChat/Component/Timeline/StateDelegate.qml imports/NeoChat/Component/Timeline/TextDelegate.qml