diff --git a/imports/NeoChat/Component/Timeline/AudioDelegate.qml b/imports/NeoChat/Component/Timeline/AudioDelegate.qml index 184763c7b..32930a1f6 100644 --- a/imports/NeoChat/Component/Timeline/AudioDelegate.qml +++ b/imports/NeoChat/Component/Timeline/AudioDelegate.qml @@ -15,47 +15,62 @@ import NeoChat.Component 1.0 import NeoChat.Dialog 1.0 import NeoChat.Menu.Timeline 1.0 -Control { - id: root +TimelineContainer { + id: audioDelegate - Layout.fillWidth: true + width: ListView.view.width + onReplyClicked: ListView.view.goToEvent(eventID) + hoverComponent: hoverActions + innerObject: Control { + Layout.fillWidth: true + Layout.maximumWidth: audioDelegate.bubbleMaxWidth - Audio { - id: audio - source: currentRoom.urlToMxcUrl(content.url) - autoLoad: false - } + Audio { + id: audio + source: currentRoom.urlToMxcUrl(content.url) + autoLoad: false + } - contentItem: ColumnLayout { - RowLayout { - ToolButton { - icon.name: audio.playbackState == Audio.PlayingState ? "media-playback-pause" : "media-playback-start" + TapHandler { + acceptedButtons: Qt.RightButton + onTapped: openFileContext(model, parent) + } + TapHandler { + acceptedButtons: Qt.LeftButton + onLongPressed: openFileContext(model, parent) + } - onClicked: { - if (audio.playbackState == Audio.PlayingState) { - audio.pause() - } else { - audio.play() + contentItem: ColumnLayout { + RowLayout { + ToolButton { + icon.name: audio.playbackState == Audio.PlayingState ? "media-playback-pause" : "media-playback-start" + + onClicked: { + if (audio.playbackState == Audio.PlayingState) { + audio.pause() + } else { + audio.play() + } } } + Label { + text: model.display + } } - Label { - text: model.display - } - } - 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 - to: audio.duration - value: audio.position - } + 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 + to: audio.duration + value: audio.position + } - Label { - text: Controller.formatDuration(audio.position) + "/" + Controller.formatDuration(audio.duration) + Label { + text: Controller.formatDuration(audio.position) + "/" + Controller.formatDuration(audio.duration) + } } } } diff --git a/imports/NeoChat/Component/Timeline/EncryptedDelegate.qml b/imports/NeoChat/Component/Timeline/EncryptedDelegate.qml index d4d23088b..f9ef21462 100644 --- a/imports/NeoChat/Component/Timeline/EncryptedDelegate.qml +++ b/imports/NeoChat/Component/Timeline/EncryptedDelegate.qml @@ -3,16 +3,23 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 import org.kde.kirigami 2.15 as Kirigami import org.kde.neochat 1.0 -TextEdit { - text: i18n("This message is encrypted and the sender has not shared the key with this device.") - color: Kirigami.Theme.disabledTextColor - font.pointSize: Kirigami.Theme.defaultFont.pointSize - selectByMouse: !Kirigami.Settings.isMobile - readOnly: true - wrapMode: Text.WordWrap - textFormat: Text.RichText +TimelineContainer { + id: encryptedDelegate + width: ListView.view.width + + innerObject: TextEdit { + text: i18n("This message is encrypted and the sender has not shared the key with this device.") + color: Kirigami.Theme.disabledTextColor + font.pointSize: Kirigami.Theme.defaultFont.pointSize + selectByMouse: !Kirigami.Settings.isMobile + readOnly: true + wrapMode: Text.WordWrap + textFormat: Text.RichText + Layout.maximumWidth: encryptedDelegate.bubbleMaxWidth + } } diff --git a/imports/NeoChat/Component/Timeline/EventDelegate.qml b/imports/NeoChat/Component/Timeline/EventDelegate.qml new file mode 100644 index 000000000..49542d8b4 --- /dev/null +++ b/imports/NeoChat/Component/Timeline/EventDelegate.qml @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: 2021 Tobias Fella +// SPDX-License-Identifier: GPL-3.0-only + +import QtQuick 2.15 +import QtQuick.Controls 2.15 as QQC2 +import QtQuick.Layouts 1.15 + +import Qt.labs.qmlmodels 1.0 +import org.kde.kirigami 2.15 as Kirigami + +import org.kde.neochat 1.0 + +DelegateChooser { + role: "eventType" + + DelegateChoice { + roleValue: "state" + delegate: StateDelegate {} + } + + DelegateChoice { + roleValue: "emote" + delegate: MessageDelegate { + isEmote: true + } + } + + DelegateChoice { + roleValue: "message" + delegate: MessageDelegate {} + } + + DelegateChoice { + roleValue: "notice" + delegate: MessageDelegate {} + } + + DelegateChoice { + roleValue: "image" + delegate: ImageDelegate {} + } + + DelegateChoice { + roleValue: "sticker" + delegate: ImageDelegate { + cardBackground: false + } + } + + DelegateChoice { + roleValue: "audio" + delegate: AudioDelegate {} + } + + DelegateChoice { + roleValue: "video" + delegate: VideoDelegate {} + } + + DelegateChoice { + roleValue: "file" + delegate: FileDelegate {} + } + + DelegateChoice { + roleValue: "encrypted" + delegate: EncryptedDelegate {} + } + + DelegateChoice { + roleValue: "readMarker" + delegate: ReadMarkerDelegate {} + } + + DelegateChoice { + roleValue: "other" + delegate: Item {} + } +} diff --git a/imports/NeoChat/Component/Timeline/FileDelegate.qml b/imports/NeoChat/Component/Timeline/FileDelegate.qml index bcd9aa71e..d98834794 100644 --- a/imports/NeoChat/Component/Timeline/FileDelegate.qml +++ b/imports/NeoChat/Component/Timeline/FileDelegate.qml @@ -14,113 +14,131 @@ 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 +TimelineContainer { + id: fileDelegate + width: ListView.view.width - Layout.margins: Kirigami.Units.largeSpacing + onReplyClicked: ListView.view.goToEvent(eventID) + hoverComponent: hoverActions - spacing: Kirigami.Units.largeSpacing + innerObject: RowLayout { + property bool openOnFinished: false + readonly property bool downloaded: progressInfo && progressInfo.completed - states: [ - State { - name: "downloaded" - when: progressInfo.completed - - PropertyChanges { - target: downloadButton - - icon.name: "document-open" - - QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to open its downloaded file with an appropriate application", "Open File") - - onClicked: openSavedFile() - } - }, - State { - name: "downloading" - when: progressInfo.active - - PropertyChanges { - target: sizeLabel - text: i18nc("file download progress", "%1 / %2", Controller.formatByteSize(progressInfo.progress), Controller.formatByteSize(progressInfo.total)) - } - PropertyChanges { - target: downloadButton - icon.name: "media-playback-stop" - - QQC2.ToolTip.text: i18nc("tooltip for a button on a message; stops downloading the message's file", "Stop Download") - onClicked: currentRoom.cancelFileTransfer(eventId) - } - }, - State { - name: "raw" - when: true - - PropertyChanges { - target: downloadButton - - onClicked: root.saveFileAs() - } - } - ] - - Kirigami.Icon { - id: ikon - source: model.fileMimetypeIcon - fallback: "unknown" - } - ColumnLayout { - Layout.alignment: Qt.AlignVCenter Layout.fillWidth: true + Layout.maximumWidth: fileDelegate.bubbleMaxWidth + Layout.margins: Kirigami.Units.largeSpacing - spacing: 0 + spacing: Kirigami.Units.largeSpacing - QQC2.Label { - text: model.display - wrapMode: Text.Wrap + states: [ + State { + name: "downloaded" + when: progressInfo.completed - Layout.fillWidth: true + PropertyChanges { + target: downloadButton + + icon.name: "document-open" + + QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to open its downloaded file with an appropriate application", "Open File") + + onClicked: openSavedFile() + } + }, + State { + name: "downloading" + when: progressInfo.active + + PropertyChanges { + target: sizeLabel + text: i18nc("file download progress", "%1 / %2", Controller.formatByteSize(progressInfo.progress), Controller.formatByteSize(progressInfo.total)) + } + PropertyChanges { + target: downloadButton + icon.name: "media-playback-stop" + + QQC2.ToolTip.text: i18nc("tooltip for a button on a message; stops downloading the message's file", "Stop Download") + onClicked: currentRoom.cancelFileTransfer(eventId) + } + }, + State { + name: "raw" + when: true + + PropertyChanges { + target: downloadButton + + onClicked: root.saveFileAs() + } + } + ] + + Kirigami.Icon { + id: ikon + source: model.fileMimetypeIcon + fallback: "unknown" } - QQC2.Label { - id: sizeLabel - - text: Controller.formatByteSize(content.info ? content.info.size : 0) - opacity: 0.7 - + ColumnLayout { + Layout.alignment: Qt.AlignVCenter Layout.fillWidth: true - } - } - QQC2.Button { - id: downloadButton - icon.name: "download" + spacing: 0 - QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to download its file", "Download") - QQC2.ToolTip.visible: hovered - } + QQC2.Label { + text: model.display + wrapMode: Text.Wrap - Component { - id: fileDialog + Layout.fillWidth: true + } + QQC2.Label { + id: sizeLabel - FileDialog { - fileMode: FileDialog.SaveFile - folder: StandardPaths.writableLocation(StandardPaths.DownloadLocation) - onAccepted: { - currentRoom.downloadFile(eventId, file) + text: Controller.formatByteSize(content.info ? content.info.size : 0) + opacity: 0.7 + + Layout.fillWidth: true } } - } - function saveFileAs() { - var dialog = fileDialog.createObject(QQC2.ApplicationWindow.overlay) - dialog.open() - dialog.currentFile = dialog.folder + "/" + currentRoom.fileNameToDownload(eventId) - } + QQC2.Button { + id: downloadButton + icon.name: "download" - function openSavedFile() { - if (Qt.openUrlExternally(progressInfo.localPath)) return; - if (Qt.openUrlExternally(progressInfo.localDir)) return; + QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to download its file", "Download") + QQC2.ToolTip.visible: hovered + } + + Component { + id: fileDialog + + FileDialog { + fileMode: FileDialog.SaveFile + folder: StandardPaths.writableLocation(StandardPaths.DownloadLocation) + onAccepted: { + currentRoom.downloadFile(eventId, file) + } + } + } + + TapHandler { + acceptedButtons: Qt.RightButton + onTapped: openFileContext(model, parent) + } + TapHandler { + acceptedButtons: Qt.LeftButton + onLongPressed: openFileContext(model, parent) + } + + function saveFileAs() { + var dialog = fileDialog.createObject(QQC2.ApplicationWindow.overlay) + dialog.open() + dialog.currentFile = dialog.folder + "/" + currentRoom.fileNameToDownload(eventId) + } + + function openSavedFile() { + if (Qt.openUrlExternally(progressInfo.localPath)) return; + if (Qt.openUrlExternally(progressInfo.localDir)) return; + } } } diff --git a/imports/NeoChat/Component/Timeline/ImageDelegate.qml b/imports/NeoChat/Component/Timeline/ImageDelegate.qml index 8747faced..b8a1d26d2 100644 --- a/imports/NeoChat/Component/Timeline/ImageDelegate.qml +++ b/imports/NeoChat/Component/Timeline/ImageDelegate.qml @@ -12,88 +12,116 @@ import NeoChat.Component 1.0 import NeoChat.Dialog 1.0 import NeoChat.Menu.Timeline 1.0 -Image { - id: img - property var content: model.content - readonly property bool isAnimated: contentType === "image/gif" +TimelineContainer { + id: imageDelegate - property bool openOnFinished: false - readonly property bool downloaded: progressInfo && progressInfo.completed + width: ListView.view.width - readonly property bool isThumbnail: !(content.info.thumbnail_info == null || content.thumbnailMediaId == null) - // readonly property var info: isThumbnail ? content.info.thumbnail_info : content.info - readonly property var info: content.info - readonly property string mediaId: isThumbnail ? content.thumbnailMediaId : content.mediaId - property bool readonly: false + onReplyClicked: ListView.view.goToEvent(eventID) + hoverComponent: hoverActions - source: "image://mxc/" + mediaId + innerObject: Image { + id: img - Image { - anchors.fill: parent - source: content.info["xyz.amorgan.blurhash"] ? ("image://blurhash/" + content.info["xyz.amorgan.blurhash"]) : "" - visible: parent.status !== Image.Ready - } + property var content: model.content + readonly property bool isAnimated: contentType === "image/gif" - fillMode: Image.PreserveAspectFit + property bool openOnFinished: false + readonly property bool downloaded: progressInfo && progressInfo.completed - ToolTip.text: display - ToolTip.visible: hoverHandler.hovered + readonly property bool isThumbnail: !(content.info.thumbnail_info == null || content.thumbnailMediaId == null) + // readonly property var info: isThumbnail ? content.info.thumbnail_info : content.info + readonly property var info: content.info + readonly property string mediaId: isThumbnail ? content.thumbnailMediaId : content.mediaId - HoverHandler { - id: hoverHandler - enabled: img.readonly - } + Layout.maximumWidth: imageDelegate.bubbleMaxWidth + source: "image://mxc/" + mediaId - Rectangle { - anchors.fill: parent - - visible: progressInfo.active && !downloaded - - color: "#BB000000" - - ProgressBar { - anchors.centerIn: parent - - width: parent.width * 0.8 - - from: 0 - to: progressInfo.total - value: progressInfo.progress + Image { + anchors.fill: parent + source: content.info["xyz.amorgan.blurhash"] ? ("image://blurhash/" + content.info["xyz.amorgan.blurhash"]) : "" + visible: parent.status !== Image.Ready } - } - function saveFileAs() { - var dialog = fileDialog.createObject(ApplicationWindow.overlay) - dialog.open() - dialog.currentFile = dialog.folder + "/" + currentRoom.fileNameToDownload(eventId) - } + fillMode: Image.PreserveAspectFit - Component { - id: fileDialog + ToolTip.text: display + ToolTip.visible: hoverHandler.hovered - FileDialog { - fileMode: FileDialog.SaveFile - folder: StandardPaths.writableLocation(StandardPaths.DownloadLocation) - onAccepted: { - currentRoom.downloadFile(eventId, file) + HoverHandler { + id: hoverHandler + } + + Rectangle { + anchors.fill: parent + + visible: progressInfo.active && !downloaded + + color: "#BB000000" + + ProgressBar { + anchors.centerIn: parent + + width: parent.width * 0.8 + + from: 0 + to: progressInfo.total + value: progressInfo.progress } } - } - function downloadAndOpen() - { - if (downloaded) openSavedFile() - else + function saveFileAs() { + var dialog = fileDialog.createObject(ApplicationWindow.overlay) + dialog.open() + dialog.currentFile = dialog.folder + "/" + currentRoom.fileNameToDownload(eventId) + } + + Component { + id: fileDialog + + FileDialog { + fileMode: FileDialog.SaveFile + folder: StandardPaths.writableLocation(StandardPaths.DownloadLocation) + onAccepted: { + currentRoom.downloadFile(eventId, file) + } + } + } + + TapHandler { + acceptedButtons: Qt.RightButton + onTapped: openFileContext(model, parent) + } + + TapHandler { + acceptedButtons: Qt.LeftButton + onLongPressed: openFileContext(model, parent) + onTapped: { + fullScreenImage.createObject(parent, { + filename: eventId, + localPath: currentRoom.urlToDownload(eventId), + blurhash: model.content.info["xyz.amorgan.blurhash"], + imageWidth: content.info.w, + imageHeight: content.info.h + }).showFullScreen(); + } + } + + function downloadAndOpen() { - openOnFinished = true - currentRoom.downloadFile(eventId, StandardPaths.writableLocation(StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId)) + if (downloaded) openSavedFile() + else + { + openOnFinished = true + currentRoom.downloadFile(eventId, StandardPaths.writableLocation(StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId)) + } + } + + function openSavedFile() + { + if (Qt.openUrlExternally(progressInfo.localPath)) return; + if (Qt.openUrlExternally(progressInfo.localDir)) return; } } - - function openSavedFile() - { - if (Qt.openUrlExternally(progressInfo.localPath)) return; - if (Qt.openUrlExternally(progressInfo.localDir)) return; - } } diff --git a/imports/NeoChat/Component/Timeline/MessageDelegate.qml b/imports/NeoChat/Component/Timeline/MessageDelegate.qml new file mode 100644 index 000000000..1d338efae --- /dev/null +++ b/imports/NeoChat/Component/Timeline/MessageDelegate.qml @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2021 Tobias Fella +// SPDX-License-Identifier: GPL-3.0-only + +import QtQuick 2.15 +import QtQuick.Controls 2.15 as QQC2 +import QtQuick.Layouts 1.15 + +import Qt.labs.qmlmodels 1.0 +import org.kde.kirigami 2.15 as Kirigami + +import org.kde.neochat 1.0 + +TimelineContainer { + id: messageDelegate + + width: ListView.view.width + property bool isEmote: false + + onReplyClicked: ListView.view.goToEvent(eventID) + hoverComponent: hoverActions + + innerObject: TextDelegate { + isEmote: messageDelegate.isEmote + Layout.maximumWidth: messageDelegate.bubbleMaxWidth + onRequestOpenMessageContext: openMessageContext(model, parent.selectedText) + } +} diff --git a/imports/NeoChat/Component/Timeline/ReadMarkerDelegate.qml b/imports/NeoChat/Component/Timeline/ReadMarkerDelegate.qml new file mode 100644 index 000000000..fb45d3ce8 --- /dev/null +++ b/imports/NeoChat/Component/Timeline/ReadMarkerDelegate.qml @@ -0,0 +1,78 @@ +// SPDX-FileCopyrightText: 2020 Carl Schwan +// SPDX-License-Identifier: GPL-3.0-only + +import QtQuick 2.15 +import QtQuick.Controls 2.15 as QQC2 +import QtQuick.Layouts 1.15 + +import Qt.labs.qmlmodels 1.0 +import org.kde.kirigami 2.15 as Kirigami + +import org.kde.neochat 1.0 + +QQC2.ItemDelegate { + padding: Kirigami.Units.largeSpacing + topInset: Kirigami.Units.largeSpacing + topPadding: Kirigami.Units.largeSpacing * 2 + width: ListView.view.width - Kirigami.Units.gridUnit + x: Kirigami.Units.gridUnit / 2 + contentItem: QQC2.Label { + text: i18nc("Relative time since the room was last read", "Last read: %1", time) + } + + background: Kirigami.ShadowedRectangle { + color: Kirigami.Theme.backgroundColor + opacity: 0.6 + radius: Kirigami.Units.smallSpacing + shadow.size: Kirigami.Units.smallSpacing + shadow.color: Qt.rgba(0.0, 0.0, 0.0, 0.10) + border.color: Kirigami.ColorUtils.tintWithAlpha(color, Kirigami.Theme.textColor, 0.15) + border.width: 1 + } + + Timer { + id: makeMeDisapearTimer + interval: Kirigami.Units.humanMoment * 2 + onTriggered: if (QQC2.ApplicationWindow.window.visibility !== QQC2.ApplicationWindow.Hidden) { + currentRoom.markAllMessagesAsRead(); + } + } + + ListView.onPooled: makeMeDisapearTimer.stop() + + ListView.onAdd: { + const view = ListView.view; + if (view.atYEnd) { + makeMeDisapearTimer.start() + } + } + + // When the read marker is visible and we are at the end of the list, + // start the makeMeDisapearTimer + Connections { + target: ListView.view + function onAtYEndChanged() { + makeMeDisapearTimer.start(); + } + } + + + ListView.onRemove: { + const view = ListView.view; + + if (view.atYEnd) { + // easy case just mark everything as read + if (QQC2.ApplicationWindow.window.visibility !== QQC2.ApplicationWindow.Hidden) { + currentRoom.markAllMessagesAsRead(); + } + return; + } + + // mark the last visible index + const lastVisibleIdx = lastVisibleIndex(); + + if (lastVisibleIdx < index) { + currentRoom.readMarkerEventId = sortedMessageEventModel.data(sortedMessageEventModel.index(lastVisibleIdx, 0), MessageEventModel.EventIdRole) + } + } +} diff --git a/imports/NeoChat/Component/Timeline/StateDelegate.qml b/imports/NeoChat/Component/Timeline/StateDelegate.qml index c0872dd39..43b149057 100644 --- a/imports/NeoChat/Component/Timeline/StateDelegate.qml +++ b/imports/NeoChat/Component/Timeline/StateDelegate.qml @@ -11,9 +11,9 @@ import NeoChat.Component 1.0 import NeoChat.Dialog 1.0 RowLayout { - id: row - + x: Kirigami.Units.gridUnit * 1.5 + Kirigami.Units.smallSpacing height: label.contentHeight + width: ListView.view.width - Kirigami.Units.largeSpacing - x Kirigami.Avatar { id: icon diff --git a/imports/NeoChat/Component/Timeline/VideoDelegate.qml b/imports/NeoChat/Component/Timeline/VideoDelegate.qml index 3ac74c3bd..d6041f0e1 100644 --- a/imports/NeoChat/Component/Timeline/VideoDelegate.qml +++ b/imports/NeoChat/Component/Timeline/VideoDelegate.qml @@ -15,130 +15,147 @@ import NeoChat.Component 1.0 import NeoChat.Dialog 1.0 import NeoChat.Menu.Timeline 1.0 -Video { - id: vid +TimelineContainer { + id: videoDelegate - property bool playOnFinished: false - readonly property bool downloaded: progressInfo && progressInfo.completed + width: ListView.view.width - property bool supportStreaming: true + onReplyClicked: ListView.view.goToEvent(eventID) + hoverComponent: hoverActions - onDownloadedChanged: { - if (downloaded) { - vid.source = progressInfo.localPath + innerObject: Video { + id: vid + + property bool playOnFinished: false + readonly property bool downloaded: progressInfo && progressInfo.completed + + property bool supportStreaming: true + + Layout.maximumWidth: videoDelegate.bubbleMaxWidth + Layout.fillWidth: true + Layout.maximumHeight: Kirigami.Units.gridUnit * 15 + Layout.minimumHeight: Kirigami.Units.gridUnit * 5 + + onDownloadedChanged: { + if (downloaded) { + vid.source = progressInfo.localPath + } + + if (downloaded && playOnFinished) { + playSavedFile() + playOnFinished = false + } } - if (downloaded && playOnFinished) { - playSavedFile() - playOnFinished = false + readonly property int maxWidth: 1000 // TODO messageListView.width + + Layout.preferredWidth: content.info.w > maxWidth ? maxWidth : content.info.w + Layout.preferredHeight: content.info.w > maxWidth ? (content.info.h / content.info.w * maxWidth) : content.info.h + + loops: MediaPlayer.Infinite + + fillMode: VideoOutput.PreserveAspectFit + + Component.onCompleted: { + if (downloaded) { + source = progressInfo.localPath + } else { + source = currentRoom.urlToMxcUrl(content.url) + } } - } - - readonly property int maxWidth: 1000 // TODO messageListView.width - - Layout.preferredWidth: content.info.w > maxWidth ? maxWidth : content.info.w - Layout.preferredHeight: content.info.w > maxWidth ? (content.info.h / content.info.w * maxWidth) : content.info.h - - loops: MediaPlayer.Infinite - - fillMode: VideoOutput.PreserveAspectFit - - Component.onCompleted: { - if (downloaded) { - source = progressInfo.localPath - } else { - source = currentRoom.urlToMxcUrl(content.url) + onDurationChanged: { + if (!duration) { + vid.supportStreaming = false; + } } - } - onDurationChanged: { - if (!duration) { - supportStreaming = false; + onErrorChanged: { + if (error != MediaPlayer.NoError) { + vid.supportStreaming = false; + } } - } - onErrorChanged: { - if (error != MediaPlayer.NoError) { - supportStreaming = false; + Image { + anchors.fill: parent + + visible: vid.playbackState == MediaPlayer.StoppedState || vid.error != MediaPlayer.NoError + + source: "image://mxc/" + content.thumbnailMediaId + + fillMode: Image.PreserveAspectFit } - } - Image { - readonly property bool isThumbnail: content.info.thumbnail_info && content.thumbnailMediaId - readonly property var info: isThumbnail ? content.info.thumbnail_info : content.info - - anchors.fill: parent - - visible: isThumbnail && (vid.playbackState == MediaPlayer.StoppedState || vid.error != MediaPlayer.NoError) - - source: "image://mxc/" + (isThumbnail ? content.thumbnailMediaId : "") - - sourceSize.width: info.w - sourceSize.height: info.h - - fillMode: Image.PreserveAspectFit - } - - Label { - anchors.centerIn: parent - - visible: vid.playbackState == MediaPlayer.StoppedState || vid.error != MediaPlayer.NoError - color: "white" - text: i18n("Video") - font.pixelSize: 16 - - padding: 8 - - background: Rectangle { - radius: Kirigami.Units.smallSpacing - color: "black" - opacity: 0.3 - } - } - - Rectangle { - anchors.fill: parent - - visible: progressInfo.active && !downloaded - - color: "#BB000000" - - ProgressBar { + Label { anchors.centerIn: parent - width: parent.width * 0.8 + visible: vid.playbackState == MediaPlayer.StoppedState || vid.error != MediaPlayer.NoError + color: "white" + text: i18n("Video") + font.pixelSize: 16 - from: 0 - to: progressInfo.total - value: progressInfo.progress - } - } + padding: 8 - TapHandler { - acceptedButtons: Qt.LeftButton - onTapped: if (supportStreaming || progressInfo.completed) { - if (vid.playbackState == MediaPlayer.PlayingState) { - vid.pause() - } else { - vid.play() + background: Rectangle { + radius: Kirigami.Units.smallSpacing + color: "black" + opacity: 0.3 } - } else { - downloadAndPlay() } - } - function downloadAndPlay() { - if (downloaded) { - playSavedFile() - } else { - playOnFinished = true - currentRoom.downloadFile(eventId, Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId)) + Rectangle { + anchors.fill: parent + + visible: progressInfo.active && !vid.downloaded + + color: "#BB000000" + + ProgressBar { + anchors.centerIn: parent + + width: parent.width * 0.8 + + from: 0 + to: progressInfo.total + value: progressInfo.progress + } } - } - function playSavedFile() { - vid.stop() - vid.play() + TapHandler { + acceptedButtons: Qt.LeftButton + onTapped: if (vid.supportStreaming || progressInfo.completed) { + if (vid.playbackState == MediaPlayer.PlayingState) { + vid.pause() + } else { + vid.play() + } + } else { + vid.downloadAndPlay() + } + } + + TapHandler { + acceptedButtons: Qt.RightButton + onTapped: openFileContext(model, parent) + } + + TapHandler { + acceptedButtons: Qt.LeftButton + onLongPressed: openFileContext(model, parent) + } + + function downloadAndPlay() { + if (vid.downloaded) { + playSavedFile() + } else { + playOnFinished = true + currentRoom.downloadFile(eventId, Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId)) + } + } + + function playSavedFile() { + vid.stop() + vid.play() + } } } diff --git a/imports/NeoChat/Component/Timeline/qmldir b/imports/NeoChat/Component/Timeline/qmldir index 96d442c82..afec3670d 100644 --- a/imports/NeoChat/Component/Timeline/qmldir +++ b/imports/NeoChat/Component/Timeline/qmldir @@ -8,4 +8,7 @@ FileDelegate 1.0 FileDelegate.qml VideoDelegate 1.0 VideoDelegate.qml ReactionDelegate 1.0 ReactionDelegate.qml AudioDelegate 1.0 AudioDelegate.qml -EncryptedDelegate 1.0 EncryptedDelegate.qml \ No newline at end of file +EncryptedDelegate 1.0 EncryptedDelegate.qml +EventDelegate 1.0 EventDelegate.qml +MessageDelegate 1.0 MessageDelegate.qml +ReadMarkerDelegate 1.0 ReadMarkerDelegate.qml diff --git a/imports/NeoChat/Page/RoomPage.qml b/imports/NeoChat/Page/RoomPage.qml index f7ac7f8b5..840f58b5f 100644 --- a/imports/NeoChat/Page/RoomPage.qml +++ b/imports/NeoChat/Page/RoomPage.qml @@ -341,282 +341,7 @@ Kirigami.ScrollablePage { sourceModel: messageEventModel } - delegate: DelegateChooser { - id: timelineDelegateChooser - role: "eventType" - - DelegateChoice { - roleValue: "state" - delegate: QQC2.Control { - leftPadding: Kirigami.Units.gridUnit * 1.5 + Kirigami.Units.smallSpacing - topPadding: 0 - bottomPadding: 0 - height: contentItem.height - contentItem: StateDelegate { } - implicitWidth: messageListView.width - Kirigami.Units.largeSpacing - } - } - - DelegateChoice { - roleValue: "emote" - delegate: TimelineContainer { - id: emoteContainer - width: messageListView.width - isEmote: true - onReplyClicked: goToEvent(eventID) - hoverComponent: hoverActions - - innerObject: TextDelegate { - isEmote: true - Layout.maximumWidth: emoteContainer.bubbleMaxWidth - onRequestOpenMessageContext: openMessageContext(model, parent.selectedText) - } - } - } - - DelegateChoice { - roleValue: "message" - delegate: TimelineContainer { - id: messageContainer - width: messageListView.width - onReplyClicked: goToEvent(eventID) - hoverComponent: hoverActions - - innerObject: TextDelegate { - Layout.maximumWidth: messageContainer.bubbleMaxWidth - onRequestOpenMessageContext: openMessageContext(model, parent.selectedText) - } - } - } - - DelegateChoice { - roleValue: "notice" - delegate: TimelineContainer { - id: noticeContainer - width: messageListView.width - onReplyClicked: goToEvent(eventID) - - innerObject: TextDelegate { - Layout.fillWidth: !Config.compactLayout - hasContextMenu: false - Layout.maximumWidth: noticeContainer.bubbleMaxWidth - } - } - } - - DelegateChoice { - roleValue: "image" - delegate: TimelineContainer { - id: imageContainer - width: messageListView.width - onReplyClicked: goToEvent(eventID) - hoverComponent: hoverActions - - innerObject: ImageDelegate { - Layout.preferredWidth: Kirigami.Units.gridUnit * 15 - Layout.maximumWidth: imageContainer.bubbleMaxWidth - Layout.preferredHeight: info.h / info.w * width - Layout.maximumHeight: Kirigami.Units.gridUnit * 20 - TapHandler { - acceptedButtons: Qt.RightButton - onTapped: openFileContext(model, parent) - } - TapHandler { - acceptedButtons: Qt.LeftButton - onLongPressed: openFileContext(model, parent) - onTapped: { - fullScreenImage.createObject(parent, { - filename: eventId, - localPath: currentRoom.urlToDownload(eventId), - blurhash: model.content.info["xyz.amorgan.blurhash"], - imageWidth: content.info.w, - imageHeight: content.info.h - }).showFullScreen(); - } - } - } - } - } - - DelegateChoice { - roleValue: "sticker" - delegate: TimelineContainer { - width: messageListView.width - onReplyClicked: goToEvent(eventID) - 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 - } - } - } - - DelegateChoice { - roleValue: "audio" - delegate: TimelineContainer { - id: audioContainer - width: messageListView.width - onReplyClicked: goToEvent(eventID) - hoverComponent: hoverActions - - innerObject: AudioDelegate { - Layout.fillWidth: true - Layout.maximumWidth: audioContainer.bubbleMaxWidth - TapHandler { - acceptedButtons: Qt.RightButton - onTapped: openFileContext(model, parent) - } - TapHandler { - acceptedButtons: Qt.LeftButton - onLongPressed: openFileContext(model, parent) - } - } - } - } - - DelegateChoice { - roleValue: "video" - delegate: TimelineContainer { - id: videoContainer - width: messageListView.width - onReplyClicked: goToEvent(eventID) - hoverComponent: hoverActions - - innerObject: VideoDelegate { - Layout.fillWidth: true - Layout.maximumWidth: videoContainer.bubbleMaxWidth - Layout.preferredHeight: content.info.h / content.info.w * width - Layout.maximumHeight: Kirigami.Units.gridUnit * 15 - Layout.minimumHeight: Kirigami.Units.gridUnit * 5 - - TapHandler { - acceptedButtons: Qt.RightButton - onTapped: openFileContext(model, parent) - } - TapHandler { - acceptedButtons: Qt.LeftButton - onLongPressed: openFileContext(model, parent) - } - } - } - } - - DelegateChoice { - roleValue: "file" - delegate: TimelineContainer { - id: fileContainer - width: messageListView.width - onReplyClicked: goToEvent(eventID) - - innerObject: FileDelegate { - Layout.fillWidth: true - Layout.maximumWidth: fileContainer.bubbleMaxWidth - TapHandler { - acceptedButtons: Qt.RightButton - onTapped: openFileContext(model, parent) - } - TapHandler { - acceptedButtons: Qt.LeftButton - onLongPressed: openFileContext(model, parent) - } - } - } - } - - DelegateChoice { - roleValue: "encrypted" - delegate: TimelineContainer { - id: encryptedContainer - width: messageListView.width - - innerObject: EncryptedDelegate { - Layout.fillWidth: Config.compactLayout - Layout.maximumWidth: encryptedContainer.bubbleMaxWidth - Layout.rightMargin: Kirigami.Units.largeSpacing - Layout.leftMargin: Config.showAvatarInTimeline ? Kirigami.Units.largeSpacing : 0 - } - } - } - - DelegateChoice { - roleValue: "readMarker" - delegate: QQC2.ItemDelegate { - padding: Kirigami.Units.largeSpacing - topInset: Kirigami.Units.largeSpacing - topPadding: Kirigami.Units.largeSpacing * 2 - width: ListView.view.width - Kirigami.Units.gridUnit - x: Kirigami.Units.gridUnit / 2 - contentItem: QQC2.Label { - text: i18nc("Relative time since the room was last read", "Last read: %1", time) - } - - background: Kirigami.ShadowedRectangle { - color: Kirigami.Theme.backgroundColor - opacity: 0.6 - radius: Kirigami.Units.smallSpacing - shadow.size: Kirigami.Units.smallSpacing - shadow.color: Qt.rgba(0.0, 0.0, 0.0, 0.10) - border.color: Kirigami.ColorUtils.tintWithAlpha(color, Kirigami.Theme.textColor, 0.15) - border.width: 1 - } - - Timer { - id: makeMeDisapearTimer - interval: Kirigami.Units.humanMoment * 2 - onTriggered: if (QQC2.ApplicationWindow.window.visibility !== QQC2.ApplicationWindow.Hidden) { - currentRoom.markAllMessagesAsRead(); - } - } - - ListView.onPooled: makeMeDisapearTimer.stop() - - ListView.onAdd: { - const view = ListView.view; - if (view.atYEnd) { - makeMeDisapearTimer.start() - } - } - - // When the read marker is visible and we are at the end of the list, - // start the makeMeDisapearTimer - Connections { - target: ListView.view - function onAtYEndChanged() { - makeMeDisapearTimer.start(); - } - } - - - ListView.onRemove: { - const view = ListView.view; - - if (view.atYEnd) { - // easy case just mark everything as read - if (QQC2.ApplicationWindow.window.visibility !== QQC2.ApplicationWindow.Hidden) { - currentRoom.markAllMessagesAsRead(); - } - return; - } - - // mark the last visible index - const lastVisibleIdx = lastVisibleIndex(); - - if (lastVisibleIdx < index) { - currentRoom.readMarkerEventId = sortedMessageEventModel.data(sortedMessageEventModel.index(lastVisibleIdx, 0), MessageEventModel.EventIdRole) - } - } - } - } - - DelegateChoice { - roleValue: "other" - delegate: Item {} - } - } + delegate: EventDelegate {} QQC2.RoundButton { anchors.right: parent.right @@ -631,7 +356,7 @@ Kirigami.ScrollablePage { visible: currentRoom && currentRoom.hasUnreadMessages && currentRoom.readMarkerLoaded action: Kirigami.Action { onTriggered: { - goToEvent(currentRoom.readMarkerEventId) + messageListView.goToEvent(currentRoom.readMarkerEventId) } icon.name: "go-up" } @@ -739,6 +464,9 @@ Kirigami.ScrollablePage { } headerPositioning: ListView.OverlayHeader + function goToEvent(eventID) { + messageListView.positionViewAtIndex(eventToIndex(eventID), ListView.Contain) + } } @@ -820,10 +548,6 @@ Kirigami.ScrollablePage { messageListView.positionViewAtIndex(0, ListView.End) } - function goToEvent(eventID) { - messageListView.positionViewAtIndex(eventToIndex(eventID), ListView.Contain) - } - function eventToIndex(eventID) { const index = messageEventModel.eventIDToIndex(eventID) if (index === -1) diff --git a/res.qrc b/res.qrc index b5943acfd..2fd04b91a 100644 --- a/res.qrc +++ b/res.qrc @@ -42,6 +42,9 @@ imports/NeoChat/Component/Timeline/FileDelegate.qml imports/NeoChat/Component/Timeline/ImageDelegate.qml imports/NeoChat/Component/Timeline/EncryptedDelegate.qml + imports/NeoChat/Component/Timeline/EventDelegate.qml + imports/NeoChat/Component/Timeline/MessageDelegate.qml + imports/NeoChat/Component/Timeline/ReadMarkerDelegate.qml imports/NeoChat/Component/Login/qmldir imports/NeoChat/Component/Login/LoginStep.qml imports/NeoChat/Component/Login/Login.qml diff --git a/src/messageeventmodel.cpp b/src/messageeventmodel.cpp index 512c172b4..fb60b8d84 100644 --- a/src/messageeventmodel.cpp +++ b/src/messageeventmodel.cpp @@ -423,8 +423,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const const KFormat format; return format.formatRelativeDateTime(eventDate, QLocale::ShortFormat); } - case SpecialMarksRole: - return EventStatus::Hidden; } return {}; }