Message Content Rework
For now everything should look identical. However this moves to using a model for the content of the message and is intended to lay the foundation for improved message content representation, e.g. splitting up a text message in multiple sections and using different delegates for things like code and quotes.
This commit is contained in:
168
src/qml/AudioComponent.qml
Normal file
168
src/qml/AudioComponent.qml
Normal file
@@ -0,0 +1,168 @@
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
import QtMultimedia
|
||||
|
||||
import org.kde.coreaddons
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
/**
|
||||
* @brief A component to show audio from a message.
|
||||
*/
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The NeoChatRoom the delegate is being displayed in.
|
||||
*/
|
||||
required property NeoChatRoom room
|
||||
|
||||
/**
|
||||
* @brief The matrix ID of the message event.
|
||||
*/
|
||||
required property string eventId
|
||||
|
||||
/**
|
||||
* @brief The display text of the message.
|
||||
*/
|
||||
required property string display
|
||||
|
||||
/**
|
||||
* @brief The media info for the event.
|
||||
*
|
||||
* This should consist of the following:
|
||||
* - source - The mxc URL for the media.
|
||||
* - mimeType - The MIME type of the media (should be image/xxx for this delegate).
|
||||
* - mimeIcon - The MIME icon name (should be image-xxx).
|
||||
* - size - The file size in bytes.
|
||||
* - width - The width in pixels of the audio media.
|
||||
* - height - The height in pixels of the audio media.
|
||||
* - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads.
|
||||
*/
|
||||
required property var mediaInfo
|
||||
|
||||
/**
|
||||
* @brief FileTransferInfo for any downloading files.
|
||||
*/
|
||||
required property var fileTransferInfo
|
||||
|
||||
/**
|
||||
* @brief Whether the media has been downloaded.
|
||||
*/
|
||||
readonly property bool downloaded: root.fileTransferInfo && root.fileTransferInfo.completed
|
||||
onDownloadedChanged: if (downloaded) {
|
||||
audio.play()
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The maximum width that the bubble's content can be.
|
||||
*/
|
||||
property real maxContentWidth: -1
|
||||
|
||||
MediaPlayer {
|
||||
id: audio
|
||||
onErrorOccurred: (error, errorString) => console.warn("Audio playback error:" + error + errorString)
|
||||
audioOutput: AudioOutput {}
|
||||
}
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: "notDownloaded"
|
||||
when: !root.fileTransferInfo.completed && !root.fileTransferInfo.active
|
||||
|
||||
PropertyChanges {
|
||||
target: playButton
|
||||
icon.name: "media-playback-start"
|
||||
onClicked: root.room.downloadFile(root.eventId)
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "downloading"
|
||||
when: root.fileTransferInfo.active && !root.fileTransferInfo.completed
|
||||
PropertyChanges {
|
||||
target: downloadBar
|
||||
visible: true
|
||||
}
|
||||
PropertyChanges {
|
||||
target: playButton
|
||||
icon.name: "media-playback-stop"
|
||||
onClicked: {
|
||||
root.room.cancelFileTransfer(root.eventId)
|
||||
}
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "paused"
|
||||
when: root.fileTransferInfo.completed && (audio.playbackState === MediaPlayer.StoppedState || audio.playbackState === MediaPlayer.PausedState)
|
||||
PropertyChanges {
|
||||
target: playButton
|
||||
icon.name: "media-playback-start"
|
||||
onClicked: {
|
||||
audio.source = root.progressInfo.localPath;
|
||||
audio.play()
|
||||
}
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "playing"
|
||||
when: root.fileTransferInfo.completed && audio.playbackState === MediaPlayer.PlayingState
|
||||
|
||||
PropertyChanges {
|
||||
target: playButton
|
||||
|
||||
icon.name: "media-playback-pause"
|
||||
|
||||
onClicked: audio.pause()
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
RowLayout {
|
||||
QQC2.ToolButton {
|
||||
id: playButton
|
||||
}
|
||||
QQC2.Label {
|
||||
text: root.display
|
||||
wrapMode: Text.Wrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
QQC2.ProgressBar {
|
||||
id: downloadBar
|
||||
visible: false
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: root.mediaInfo.size
|
||||
value: root.fileTransferInfo.progress
|
||||
}
|
||||
RowLayout {
|
||||
visible: audio.hasAudio
|
||||
|
||||
QQC2.Slider {
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: audio.duration
|
||||
value: audio.position
|
||||
onMoved: audio.seek(value)
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
visible: root.maxContentWidth > Kirigami.Units.gridUnit * 12
|
||||
|
||||
text: Format.formatDuration(audio.position) + "/" + Format.formatDuration(audio.duration)
|
||||
}
|
||||
}
|
||||
QQC2.Label {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
Layout.rightMargin: Kirigami.Units.smallSpacing
|
||||
visible: audio.hasAudio && root.maxContentWidth < Kirigami.Units.gridUnit * 12
|
||||
|
||||
text: Format.formatDuration(audio.position) + "/" + Format.formatDuration(audio.duration)
|
||||
}
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
import QtMultimedia
|
||||
|
||||
import org.kde.coreaddons
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
/**
|
||||
* @brief A timeline delegate for an audio message.
|
||||
*
|
||||
* @inherit MessageDelegate
|
||||
*/
|
||||
MessageDelegate {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The media info for the event.
|
||||
*
|
||||
* This should consist of the following:
|
||||
* - source - The mxc URL for the media.
|
||||
* - mimeType - The MIME type of the media (should be audio/xxx for this delegate).
|
||||
* - mimeIcon - The MIME icon name (should be audio-xxx).
|
||||
* - size - The file size in bytes.
|
||||
* - duration - The length in seconds of the audio media.
|
||||
*/
|
||||
required property var mediaInfo
|
||||
|
||||
/**
|
||||
* @brief Whether the media has been downloaded.
|
||||
*/
|
||||
readonly property bool downloaded: root.progressInfo && root.progressInfo.completed
|
||||
onDownloadedChanged: audio.play()
|
||||
|
||||
onOpenContextMenu: RoomManager.viewEventMenu(eventId, author, delegateType, plainText, "", "", mediaInfo.mimeType, progressInfo)
|
||||
|
||||
bubbleContent: ColumnLayout {
|
||||
MediaPlayer {
|
||||
id: audio
|
||||
onErrorOccurred: (error, errorString) => console.warn("Audio playback error:" + error + errorString)
|
||||
audioOutput: AudioOutput {}
|
||||
}
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: "notDownloaded"
|
||||
when: !root.progressInfo.completed && !root.progressInfo.active
|
||||
|
||||
PropertyChanges {
|
||||
target: playButton
|
||||
icon.name: "media-playback-start"
|
||||
onClicked: root.room.downloadFile(root.eventId)
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "downloading"
|
||||
when: root.progressInfo.active && !root.progressInfo.completed
|
||||
PropertyChanges {
|
||||
target: downloadBar
|
||||
visible: true
|
||||
}
|
||||
PropertyChanges {
|
||||
target: playButton
|
||||
icon.name: "media-playback-stop"
|
||||
onClicked: {
|
||||
root.room.cancelFileTransfer(root.eventId);
|
||||
}
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "paused"
|
||||
when: root.progressInfo.completed && (audio.playbackState === MediaPlayer.StoppedState || audio.playbackState === MediaPlayer.PausedState)
|
||||
PropertyChanges {
|
||||
target: playButton
|
||||
icon.name: "media-playback-start"
|
||||
onClicked: {
|
||||
audio.source = root.progressInfo.localPath;
|
||||
audio.play();
|
||||
}
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "playing"
|
||||
when: root.progressInfo.completed && audio.playbackState === MediaPlayer.PlayingState
|
||||
|
||||
PropertyChanges {
|
||||
target: playButton
|
||||
|
||||
icon.name: "media-playback-pause"
|
||||
|
||||
onClicked: audio.pause()
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
RowLayout {
|
||||
QQC2.ToolButton {
|
||||
id: playButton
|
||||
}
|
||||
QQC2.Label {
|
||||
text: root.display
|
||||
wrapMode: Text.Wrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
QQC2.ProgressBar {
|
||||
id: downloadBar
|
||||
visible: false
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: root.mediaInfo.size
|
||||
value: root.progressInfo.progress
|
||||
}
|
||||
RowLayout {
|
||||
visible: audio.hasAudio
|
||||
|
||||
QQC2.Slider {
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: audio.duration
|
||||
value: audio.position
|
||||
onMoved: audio.seek(value)
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
visible: root.contentMaxWidth > Kirigami.Units.gridUnit * 12
|
||||
|
||||
text: Format.formatDuration(audio.position) + "/" + Format.formatDuration(audio.duration)
|
||||
}
|
||||
}
|
||||
QQC2.Label {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
Layout.rightMargin: Kirigami.Units.smallSpacing
|
||||
visible: audio.hasAudio && root.contentMaxWidth < Kirigami.Units.gridUnit * 12
|
||||
|
||||
text: Format.formatDuration(audio.position) + "/" + Format.formatDuration(audio.duration)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,11 @@ import org.kde.neochat
|
||||
QQC2.Control {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The NeoChatRoom the delegate is being displayed in.
|
||||
*/
|
||||
required property NeoChatRoom room
|
||||
|
||||
/**
|
||||
* @brief The message author.
|
||||
*
|
||||
@@ -61,70 +66,28 @@ QQC2.Control {
|
||||
property bool showHighlight: false
|
||||
|
||||
/**
|
||||
* @brief The main delegate content item to show in the bubble.
|
||||
* @brief The model to visualise the content of the message.
|
||||
*/
|
||||
property Item content
|
||||
required property MessageContentModel contentModel
|
||||
|
||||
/**
|
||||
* @brief Whether this message is replying to another.
|
||||
*/
|
||||
property bool isReply: false
|
||||
|
||||
/**
|
||||
* @brief The matrix ID of the reply event.
|
||||
*/
|
||||
required property var replyId
|
||||
|
||||
/**
|
||||
* @brief The reply author.
|
||||
* @brief The ActionsHandler object to use.
|
||||
*
|
||||
* This should consist of the following:
|
||||
* - id - The matrix ID of the reply author.
|
||||
* - isLocalUser - Whether the reply author is the local user.
|
||||
* - avatarSource - The mxc URL for the reply author's avatar in the current room.
|
||||
* - avatarMediaId - The media ID of the reply author's avatar.
|
||||
* - avatarUrl - The mxc URL for the reply author's avatar.
|
||||
* - displayName - The display name of the reply author.
|
||||
* - display - The name of the reply author.
|
||||
* - color - The color for the reply author.
|
||||
* - object - The Quotient::User object for the reply author.
|
||||
*
|
||||
* @sa Quotient::User
|
||||
* This is expected to have the correct room set otherwise messages will be sent
|
||||
* to the wrong room.
|
||||
*/
|
||||
required property var replyAuthor
|
||||
|
||||
/**
|
||||
* @brief The delegate type of the message replied to.
|
||||
*/
|
||||
required property int replyDelegateType
|
||||
|
||||
/**
|
||||
* @brief The display text of the message replied to.
|
||||
*/
|
||||
required property string replyDisplay
|
||||
|
||||
/**
|
||||
* @brief The media info for the reply event.
|
||||
*
|
||||
* This could be an image, audio, video or file.
|
||||
*
|
||||
* This should consist of the following:
|
||||
* - source - The mxc URL for the media.
|
||||
* - mimeType - The MIME type of the media.
|
||||
* - mimeIcon - The MIME icon name.
|
||||
* - size - The file size in bytes.
|
||||
* - duration - The length in seconds of the audio media (audio/video only).
|
||||
* - width - The width in pixels of the audio media (image/video only).
|
||||
* - height - The height in pixels of the audio media (image/video only).
|
||||
* - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads (image/video only).
|
||||
*/
|
||||
required property var replyMediaInfo
|
||||
property ActionsHandler actionsHandler
|
||||
|
||||
/**
|
||||
* @brief Whether the bubble background should be shown.
|
||||
*/
|
||||
property alias showBackground: bubbleBackground.visible
|
||||
|
||||
/**
|
||||
* @brief The timeline ListView this component is being used in.
|
||||
*/
|
||||
required property ListView timeline
|
||||
|
||||
/**
|
||||
* @brief The maximum width that the bubble's content can be.
|
||||
*/
|
||||
@@ -135,11 +98,26 @@ QQC2.Control {
|
||||
*/
|
||||
signal replyClicked(string eventID)
|
||||
|
||||
/**
|
||||
* @brief The user selected text has changed.
|
||||
*/
|
||||
signal selectedTextChanged(string selectedText)
|
||||
|
||||
/**
|
||||
* @brief Request a context menu be show for the message.
|
||||
*/
|
||||
signal showMessageMenu()
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
id: contentColumn
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
RowLayout {
|
||||
id: headerRow
|
||||
Layout.maximumWidth: root.maxContentWidth
|
||||
implicitHeight: Math.max(nameButton.implicitHeight, timeLabel.implicitHeight)
|
||||
visible: root.showAuthor
|
||||
QQC2.AbstractButton {
|
||||
id: nameButton
|
||||
Layout.fillWidth: true
|
||||
contentItem: QQC2.Label {
|
||||
text: root.author.displayName
|
||||
@@ -152,6 +130,7 @@ QQC2.Control {
|
||||
onClicked: RoomManager.resolveResource(root.author.id, "mention")
|
||||
}
|
||||
QQC2.Label {
|
||||
id: timeLabel
|
||||
text: root.timeString
|
||||
horizontalAlignment: Text.AlignRight
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
@@ -164,35 +143,19 @@ QQC2.Control {
|
||||
}
|
||||
}
|
||||
}
|
||||
Loader {
|
||||
id: replyLoader
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: root.maxContentWidth
|
||||
Repeater {
|
||||
id: contentRepeater
|
||||
model: root.contentModel
|
||||
delegate: MessageComponentChooser {
|
||||
room: root.room
|
||||
actionsHandler: root.actionsHandler
|
||||
timeline: root.timeline
|
||||
maxContentWidth: root.maxContentWidth
|
||||
|
||||
active: root.isReply && root.replyDelegateType !== DelegateType.Other
|
||||
visible: active
|
||||
|
||||
sourceComponent: ReplyComponent {
|
||||
author: root.replyAuthor
|
||||
type: root.replyDelegateType
|
||||
display: root.replyDisplay
|
||||
mediaInfo: root.replyMediaInfo
|
||||
contentMaxWidth: root.maxContentWidth
|
||||
onReplyClicked: (eventId) => {root.replyClicked(eventId)}
|
||||
onSelectedTextChanged: (selectedText) => {root.selectedTextChanged(selectedText);}
|
||||
onShowMessageMenu: root.showMessageMenu()
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: replyLoader.item
|
||||
function onReplyClicked() {
|
||||
replyClicked(root.replyId);
|
||||
}
|
||||
}
|
||||
}
|
||||
Item {
|
||||
id: contentParent
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: root.maxContentWidth
|
||||
implicitWidth: root.content ? root.content.implicitWidth : 0
|
||||
implicitHeight: root.content ? root.content.implicitHeight : 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,12 +183,4 @@ QQC2.Control {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onContentChanged: {
|
||||
if (!root.content) {
|
||||
return;
|
||||
}
|
||||
root.content.parent = contentParent;
|
||||
root.content.anchors.fill = contentParent;
|
||||
}
|
||||
}
|
||||
|
||||
30
src/qml/EncryptedComponent.qml
Normal file
30
src/qml/EncryptedComponent.qml
Normal file
@@ -0,0 +1,30 @@
|
||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
/**
|
||||
* @brief A component for an encrypted message that can't be decrypted.
|
||||
*/
|
||||
TextEdit {
|
||||
/**
|
||||
* @brief The maximum width that the bubble's content can be.
|
||||
*/
|
||||
property real maxContentWidth: -1
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: root.maxContentWidth
|
||||
|
||||
text: i18n("This message is encrypted and the sender has not shared the key with this device.")
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
selectedTextColor: Kirigami.Theme.highlightedTextColor
|
||||
selectionColor: Kirigami.Theme.highlightColor
|
||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize
|
||||
selectByMouse: !Kirigami.Settings.isMobile
|
||||
readOnly: true
|
||||
wrapMode: Text.WordWrap
|
||||
textFormat: Text.RichText
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.neochat
|
||||
import org.kde.neochat.config
|
||||
|
||||
/**
|
||||
* @brief A timeline delegate for an encrypted message that can't be decrypted.
|
||||
*
|
||||
* @inherit MessageDelegate
|
||||
*/
|
||||
MessageDelegate {
|
||||
id: encryptedDelegate
|
||||
|
||||
bubbleContent: TextEdit {
|
||||
text: i18n("This message is encrypted and the sender has not shared the key with this device.")
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
selectedTextColor: Kirigami.Theme.highlightedTextColor
|
||||
selectionColor: Kirigami.Theme.highlightColor
|
||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize
|
||||
selectByMouse: !Kirigami.Settings.isMobile
|
||||
readOnly: true
|
||||
wrapMode: Text.WordWrap
|
||||
textFormat: Text.RichText
|
||||
Layout.leftMargin: Config.showAvatarInTimeline ? Kirigami.Units.largeSpacing : 0
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
import QtQuick
|
||||
@@ -23,65 +24,9 @@ DelegateChooser {
|
||||
delegate: StateDelegate {}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: DelegateType.Emote
|
||||
delegate: TextDelegate {
|
||||
room: root.room
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: DelegateType.Message
|
||||
delegate: TextDelegate {
|
||||
room: root.room
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: DelegateType.Notice
|
||||
delegate: TextDelegate {
|
||||
room: root.room
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: DelegateType.Image
|
||||
delegate: ImageDelegate {
|
||||
room: root.room
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: DelegateType.Sticker
|
||||
delegate: ImageDelegate {
|
||||
room: root.room
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: DelegateType.Audio
|
||||
delegate: AudioDelegate {
|
||||
room: root.room
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: DelegateType.Video
|
||||
delegate: VideoDelegate {
|
||||
room: root.room
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: DelegateType.File
|
||||
delegate: FileDelegate {
|
||||
room: root.room
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: DelegateType.Encrypted
|
||||
delegate: EncryptedDelegate {
|
||||
delegate: MessageDelegate {
|
||||
room: root.room
|
||||
}
|
||||
}
|
||||
@@ -91,27 +36,6 @@ DelegateChooser {
|
||||
delegate: ReadMarkerDelegate {}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: DelegateType.Poll
|
||||
delegate: PollDelegate {
|
||||
room: root.room
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: DelegateType.Location
|
||||
delegate: LocationDelegate {
|
||||
room: root.room
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: DelegateType.LiveLocation
|
||||
delegate: LiveLocationDelegate {
|
||||
room: root.room
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: DelegateType.Loading
|
||||
delegate: LoadingDelegate {}
|
||||
|
||||
302
src/qml/FileComponent.qml
Normal file
302
src/qml/FileComponent.qml
Normal file
@@ -0,0 +1,302 @@
|
||||
// SPDX-FileCopyrightText: 2018-2019 Black Hat <bhat@encom.eu.org>
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
import Qt.labs.platform
|
||||
import Qt.labs.qmlmodels
|
||||
|
||||
import org.kde.coreaddons
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
import org.kde.neochat.config
|
||||
|
||||
/**
|
||||
* @brief A component to show a file from a message.
|
||||
*/
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The NeoChatRoom the delegate is being displayed in.
|
||||
*/
|
||||
required property NeoChatRoom room
|
||||
|
||||
/**
|
||||
* @brief The matrix ID of the message event.
|
||||
*/
|
||||
required property string eventId
|
||||
|
||||
/**
|
||||
* @brief The display text of the message.
|
||||
*/
|
||||
required property string display
|
||||
|
||||
/**
|
||||
* @brief The media info for the event.
|
||||
*
|
||||
* This should consist of the following:
|
||||
* - source - The mxc URL for the media.
|
||||
* - mimeType - The MIME type of the media (should be image/xxx for this delegate).
|
||||
* - mimeIcon - The MIME icon name (should be image-xxx).
|
||||
* - size - The file size in bytes.
|
||||
* - width - The width in pixels of the audio media.
|
||||
* - height - The height in pixels of the audio media.
|
||||
* - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads.
|
||||
*/
|
||||
required property var mediaInfo
|
||||
|
||||
/**
|
||||
* @brief FileTransferInfo for any downloading files.
|
||||
*/
|
||||
required property var fileTransferInfo
|
||||
|
||||
/**
|
||||
* @brief Whether the media has been downloaded.
|
||||
*/
|
||||
readonly property bool downloaded: root.fileTransferInfo && root.fileTransferInfo.completed
|
||||
onDownloadedChanged: {
|
||||
itineraryModel.path = root.fileTransferInfo.localPath
|
||||
if (autoOpenFile) {
|
||||
openSavedFile();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Whether the file should be automatically opened when downloaded.
|
||||
*/
|
||||
property bool autoOpenFile: false
|
||||
|
||||
/**
|
||||
* @brief The maximum width that the bubble's content can be.
|
||||
*/
|
||||
property real maxContentWidth: -1
|
||||
|
||||
function saveFileAs() {
|
||||
const dialog = fileDialog.createObject(QQC2.ApplicationWindow.overlay)
|
||||
dialog.open()
|
||||
dialog.currentFile = dialog.folder + "/" + root.room.fileNameToDownload(root.eventId)
|
||||
}
|
||||
|
||||
function openSavedFile() {
|
||||
UrlHelper.openUrl(root.fileTransferInfo.localPath);
|
||||
}
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: root.maxContentWidth
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
RowLayout {
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: "downloadedInstant"
|
||||
when: root.fileTransferInfo.completed && autoOpenFile
|
||||
|
||||
PropertyChanges {
|
||||
target: openButton
|
||||
icon.name: "document-open"
|
||||
onClicked: openSavedFile()
|
||||
}
|
||||
|
||||
PropertyChanges {
|
||||
target: downloadButton
|
||||
icon.name: "download"
|
||||
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to download its file", "Download")
|
||||
onClicked: saveFileAs()
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "downloaded"
|
||||
when: root.fileTransferInfo.completed && !autoOpenFile
|
||||
|
||||
PropertyChanges {
|
||||
target: openButton
|
||||
visible: false
|
||||
}
|
||||
|
||||
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: root.fileTransferInfo.active
|
||||
|
||||
PropertyChanges {
|
||||
target: openButton
|
||||
visible: false
|
||||
}
|
||||
|
||||
PropertyChanges {
|
||||
target: sizeLabel
|
||||
text: i18nc("file download progress", "%1 / %2", Format.formatByteSize(root.fileTransferInfo.progress), Format.formatByteSize(root.fileTransferInfo.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: root.room.cancelFileTransfer(root.eventId)
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "raw"
|
||||
when: true
|
||||
|
||||
PropertyChanges {
|
||||
target: downloadButton
|
||||
onClicked: root.saveFileAs()
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Kirigami.Icon {
|
||||
source: root.mediaInfo.mimeIcon
|
||||
fallback: "unknown"
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 0
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
text: root.display
|
||||
wrapMode: Text.Wrap
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
QQC2.Label {
|
||||
id: sizeLabel
|
||||
Layout.fillWidth: true
|
||||
text: Format.formatByteSize(root.mediaInfo.size)
|
||||
opacity: 0.7
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.Button {
|
||||
id: openButton
|
||||
icon.name: "document-open"
|
||||
onClicked: {
|
||||
autoOpenFile = true;
|
||||
root.room.downloadTempFile(root.eventId);
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to open its downloaded file with an appropriate application", "Open File")
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
|
||||
QQC2.Button {
|
||||
id: downloadButton
|
||||
icon.name: "download"
|
||||
|
||||
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to download its file", "Download")
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
|
||||
Component {
|
||||
id: fileDialog
|
||||
|
||||
FileDialog {
|
||||
fileMode: FileDialog.SaveFile
|
||||
folder: Config.lastSaveDirectory.length > 0 ? Config.lastSaveDirectory : StandardPaths.writableLocation(StandardPaths.DownloadLocation)
|
||||
onAccepted: {
|
||||
Config.lastSaveDirectory = folder
|
||||
Config.save()
|
||||
if (autoOpenFile) {
|
||||
UrlHelper.copyTo(root.fileTransferInfo.localPath, file)
|
||||
} else {
|
||||
root.room.download(root.eventId, file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Repeater {
|
||||
id: itinerary
|
||||
model: ItineraryModel {
|
||||
id: itineraryModel
|
||||
connection: root.room.connection
|
||||
}
|
||||
delegate: DelegateChooser {
|
||||
role: "type"
|
||||
DelegateChoice {
|
||||
roleValue: "TrainReservation"
|
||||
delegate: ColumnLayout {
|
||||
Kirigami.Separator {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
RowLayout {
|
||||
QQC2.Label {
|
||||
text: model.name
|
||||
}
|
||||
QQC2.Label {
|
||||
text: model.coach ? i18n("Coach: %1, Seat: %2", model.coach, model.seat) : ""
|
||||
visible: model.coach
|
||||
opacity: 0.7
|
||||
}
|
||||
}
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
ColumnLayout {
|
||||
QQC2.Label {
|
||||
text: model.departureStation + (model.departurePlatform ? (" [" + model.departurePlatform + "]") : "")
|
||||
}
|
||||
QQC2.Label {
|
||||
text: model.departureTime
|
||||
opacity: 0.7
|
||||
}
|
||||
}
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
ColumnLayout {
|
||||
QQC2.Label {
|
||||
text: model.arrivalStation + (model.arrivalPlatform ? (" [" + model.arrivalPlatform + "]") : "")
|
||||
}
|
||||
QQC2.Label {
|
||||
text: model.arrivalTime
|
||||
opacity: 0.7
|
||||
Layout.alignment: Qt.AlignRight
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
DelegateChoice {
|
||||
roleValue: "LodgingReservation"
|
||||
delegate: ColumnLayout {
|
||||
Kirigami.Separator {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
QQC2.Label {
|
||||
text: model.name
|
||||
}
|
||||
QQC2.Label {
|
||||
text: i18nc("<start time> - <end time>", "%1 - %2", model.startTime, model.endTime)
|
||||
}
|
||||
QQC2.Label {
|
||||
text: model.address
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
QQC2.Button {
|
||||
icon.name: "map-globe"
|
||||
text: i18nc("@action", "Send to KDE Itinerary")
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
onClicked: itineraryModel.sendToItinerary()
|
||||
visible: itinerary.count > 0
|
||||
}
|
||||
}
|
||||
@@ -1,277 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2018-2019 Black Hat <bhat@encom.eu.org>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
import Qt.labs.platform
|
||||
import Qt.labs.qmlmodels
|
||||
|
||||
import org.kde.coreaddons
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
import org.kde.neochat.config
|
||||
|
||||
/**
|
||||
* @brief A timeline delegate for an file message.
|
||||
*
|
||||
* @inherit MessageDelegate
|
||||
*/
|
||||
MessageDelegate {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The media info for the event.
|
||||
*
|
||||
* This should consist of the following:
|
||||
* - source - The mxc URL for the media.
|
||||
* - mimeType - The MIME type of the media.
|
||||
* - mimeIcon - The MIME icon name.
|
||||
* - size - The file size in bytes.
|
||||
*/
|
||||
required property var mediaInfo
|
||||
|
||||
/**
|
||||
* @brief Whether the media has been downloaded.
|
||||
*/
|
||||
readonly property bool downloaded: root.progressInfo && root.progressInfo.completed
|
||||
|
||||
/**
|
||||
* @brief Whether the file should be automatically opened when downloaded.
|
||||
*/
|
||||
property bool autoOpenFile: false
|
||||
|
||||
onDownloadedChanged: {
|
||||
itineraryModel.path = root.progressInfo.localPath;
|
||||
if (autoOpenFile) {
|
||||
openSavedFile();
|
||||
}
|
||||
}
|
||||
|
||||
onOpenContextMenu: RoomManager.viewEventMenu(eventId, author, delegateType, plainText, "", "", mediaInfo.mimeType, progressInfo)
|
||||
|
||||
function saveFileAs() {
|
||||
const dialog = fileDialog.createObject(QQC2.ApplicationWindow.overlay);
|
||||
dialog.open();
|
||||
dialog.currentFile = dialog.folder + "/" + root.room.fileNameToDownload(root.eventId);
|
||||
}
|
||||
|
||||
function openSavedFile() {
|
||||
UrlHelper.openUrl(root.progressInfo.localPath);
|
||||
}
|
||||
|
||||
bubbleContent: ColumnLayout {
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
RowLayout {
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: "downloadedInstant"
|
||||
when: root.progressInfo.completed && autoOpenFile
|
||||
|
||||
PropertyChanges {
|
||||
target: openButton
|
||||
icon.name: "document-open"
|
||||
onClicked: openSavedFile()
|
||||
}
|
||||
|
||||
PropertyChanges {
|
||||
target: downloadButton
|
||||
icon.name: "download"
|
||||
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to download its file", "Download")
|
||||
onClicked: saveFileAs()
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "downloaded"
|
||||
when: root.progressInfo.completed && !autoOpenFile
|
||||
|
||||
PropertyChanges {
|
||||
target: openButton
|
||||
visible: false
|
||||
}
|
||||
|
||||
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: root.progressInfo.active
|
||||
|
||||
PropertyChanges {
|
||||
target: openButton
|
||||
visible: false
|
||||
}
|
||||
|
||||
PropertyChanges {
|
||||
target: sizeLabel
|
||||
text: i18nc("file download progress", "%1 / %2", Format.formatByteSize(root.progressInfo.progress), Format.formatByteSize(root.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: root.room.cancelFileTransfer(root.eventId)
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "raw"
|
||||
when: true
|
||||
|
||||
PropertyChanges {
|
||||
target: downloadButton
|
||||
onClicked: root.saveFileAs()
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Kirigami.Icon {
|
||||
source: root.mediaInfo.mimeIcon
|
||||
fallback: "unknown"
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 0
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
text: root.display
|
||||
wrapMode: Text.Wrap
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
QQC2.Label {
|
||||
id: sizeLabel
|
||||
Layout.fillWidth: true
|
||||
text: Format.formatByteSize(root.mediaInfo.size)
|
||||
opacity: 0.7
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.Button {
|
||||
id: openButton
|
||||
icon.name: "document-open"
|
||||
onClicked: {
|
||||
autoOpenFile = true;
|
||||
root.room.downloadTempFile(root.eventId);
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to open its downloaded file with an appropriate application", "Open File")
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
|
||||
QQC2.Button {
|
||||
id: downloadButton
|
||||
icon.name: "download"
|
||||
|
||||
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to download its file", "Download")
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
|
||||
Component {
|
||||
id: fileDialog
|
||||
|
||||
FileDialog {
|
||||
fileMode: FileDialog.SaveFile
|
||||
folder: Config.lastSaveDirectory.length > 0 ? Config.lastSaveDirectory : StandardPaths.writableLocation(StandardPaths.DownloadLocation)
|
||||
onAccepted: {
|
||||
Config.lastSaveDirectory = folder;
|
||||
Config.save();
|
||||
if (autoOpenFile) {
|
||||
UrlHelper.copyTo(root.progressInfo.localPath, file);
|
||||
} else {
|
||||
root.room.download(root.eventId, file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Repeater {
|
||||
id: itinerary
|
||||
model: ItineraryModel {
|
||||
id: itineraryModel
|
||||
connection: root.room.connection
|
||||
}
|
||||
delegate: DelegateChooser {
|
||||
role: "type"
|
||||
DelegateChoice {
|
||||
roleValue: "TrainReservation"
|
||||
delegate: ColumnLayout {
|
||||
Kirigami.Separator {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
RowLayout {
|
||||
QQC2.Label {
|
||||
text: model.name
|
||||
}
|
||||
QQC2.Label {
|
||||
text: model.coach ? i18n("Coach: %1, Seat: %2", model.coach, model.seat) : ""
|
||||
visible: model.coach
|
||||
opacity: 0.7
|
||||
}
|
||||
}
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
ColumnLayout {
|
||||
QQC2.Label {
|
||||
text: model.departureStation + (model.departurePlatform ? (" [" + model.departurePlatform + "]") : "")
|
||||
}
|
||||
QQC2.Label {
|
||||
text: model.departureTime
|
||||
opacity: 0.7
|
||||
}
|
||||
}
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
ColumnLayout {
|
||||
QQC2.Label {
|
||||
text: model.arrivalStation + (model.arrivalPlatform ? (" [" + model.arrivalPlatform + "]") : "")
|
||||
}
|
||||
QQC2.Label {
|
||||
text: model.arrivalTime
|
||||
opacity: 0.7
|
||||
Layout.alignment: Qt.AlignRight
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
DelegateChoice {
|
||||
roleValue: "LodgingReservation"
|
||||
delegate: ColumnLayout {
|
||||
Kirigami.Separator {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
QQC2.Label {
|
||||
text: model.name
|
||||
}
|
||||
QQC2.Label {
|
||||
text: i18nc("<start time> - <end time>", "%1 - %2", model.startTime, model.endTime)
|
||||
}
|
||||
QQC2.Label {
|
||||
text: model.address
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
QQC2.Button {
|
||||
icon.name: "map-globe"
|
||||
text: i18nc("@action", "Send to KDE Itinerary")
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
onClicked: itineraryModel.sendToItinerary()
|
||||
visible: itinerary.count > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
174
src/qml/ImageComponent.qml
Normal file
174
src/qml/ImageComponent.qml
Normal file
@@ -0,0 +1,174 @@
|
||||
// SPDX-FileCopyrightText: 2018-2020 Black Hat <bhat@encom.eu.org>
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
/**
|
||||
* @brief A component to show the image from a message.
|
||||
*/
|
||||
Item {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The NeoChatRoom the delegate is being displayed in.
|
||||
*/
|
||||
required property NeoChatRoom room
|
||||
|
||||
/**
|
||||
* @brief The matrix ID of the message event.
|
||||
*/
|
||||
required property string eventId
|
||||
|
||||
/**
|
||||
* @brief The display text of the message.
|
||||
*/
|
||||
required property string display
|
||||
|
||||
/**
|
||||
* @brief The media info for the event.
|
||||
*
|
||||
* This should consist of the following:
|
||||
* - source - The mxc URL for the media.
|
||||
* - mimeType - The MIME type of the media (should be image/xxx for this delegate).
|
||||
* - mimeIcon - The MIME icon name (should be image-xxx).
|
||||
* - size - The file size in bytes.
|
||||
* - width - The width in pixels of the audio media.
|
||||
* - height - The height in pixels of the audio media.
|
||||
* - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads.
|
||||
*/
|
||||
required property var mediaInfo
|
||||
|
||||
/**
|
||||
* @brief FileTransferInfo for any downloading files.
|
||||
*/
|
||||
required property var fileTransferInfo
|
||||
|
||||
/**
|
||||
* @brief The timeline ListView this component is being used in.
|
||||
*/
|
||||
required property ListView timeline
|
||||
|
||||
/**
|
||||
* @brief The maximum width that the bubble's content can be.
|
||||
*/
|
||||
property real maxContentWidth: -1
|
||||
|
||||
implicitWidth: mediaSizeHelper.currentSize.width
|
||||
implicitHeight: mediaSizeHelper.currentSize.height
|
||||
|
||||
Loader {
|
||||
id: imageLoader
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
active: !root.mediaInfo.animated
|
||||
sourceComponent: Image {
|
||||
source: root.mediaInfo.source
|
||||
sourceSize.width: mediaSizeHelper.currentSize.width * Screen.devicePixelRatio
|
||||
sourceSize.height: mediaSizeHelper.currentSize.height * Screen.devicePixelRatio
|
||||
|
||||
fillMode: Image.PreserveAspectFit
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: animatedImageLoader
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
active: root?.mediaInfo.animated ?? false
|
||||
sourceComponent: AnimatedImage {
|
||||
source: root.mediaInfo.source
|
||||
|
||||
fillMode: Image.PreserveAspectFit
|
||||
|
||||
paused: !applicationWindow().active
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
source: root?.mediaInfo.tempInfo.source ?? ""
|
||||
visible: _private.imageItem.status !== Image.Ready
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: root.display
|
||||
QQC2.ToolTip.visible: hoverHandler.hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
|
||||
HoverHandler {
|
||||
id: hoverHandler
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
|
||||
visible: _private.imageItem.status !== Image.Ready
|
||||
|
||||
color: "#BB000000"
|
||||
|
||||
QQC2.ProgressBar {
|
||||
anchors.centerIn: parent
|
||||
|
||||
width: parent.width * 0.8
|
||||
|
||||
from: 0
|
||||
to: 1.0
|
||||
value: _private.imageItem.progress
|
||||
}
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
gesturePolicy: TapHandler.ReleaseWithinBounds | TapHandler.WithinBounds
|
||||
onTapped: {
|
||||
root.QQC2.ToolTip.hide()
|
||||
if (root.mediaInfo.animated) {
|
||||
_private.imageItem.paused = true
|
||||
}
|
||||
root.timeline.interactive = false
|
||||
// We need to make sure the index is that of the MediaMessageFilterModel.
|
||||
if (root.timeline.model instanceof MessageFilterModel) {
|
||||
RoomManager.maximizeMedia(RoomManager.mediaMessageFilterModel.getRowForSourceItem(root.index))
|
||||
} else {
|
||||
RoomManager.maximizeMedia(root.index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function downloadAndOpen() {
|
||||
if (_private.downloaded) {
|
||||
openSavedFile()
|
||||
} else {
|
||||
openOnFinished = true
|
||||
root.room.downloadFile(root.eventId, StandardPaths.writableLocation(StandardPaths.CacheLocation) + "/" + root.eventId.replace(":", "_").replace("/", "_").replace("+", "_") + root.room.fileNameToDownload(root.eventId))
|
||||
}
|
||||
}
|
||||
|
||||
function openSavedFile() {
|
||||
if (UrlHelper.openUrl(root.fileTransferInfo.localPath)) return;
|
||||
if (UrlHelper.openUrl(root.fileTransferInfo.localDir)) return;
|
||||
}
|
||||
|
||||
MediaSizeHelper {
|
||||
id: mediaSizeHelper
|
||||
contentMaxWidth: root.maxContentWidth
|
||||
mediaWidth: root?.mediaInfo.width ?? 0
|
||||
mediaHeight: root?.mediaInfo.height ?? 0
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: _private
|
||||
readonly property var imageItem: root.mediaInfo.animated ? animatedImageLoader.item : imageLoader.item
|
||||
|
||||
// The space available for the component after taking away the border
|
||||
readonly property real downloaded: root.fileTransferInfo && root.fileTransferInfo.completed
|
||||
}
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2018-2020 Black Hat <bhat@encom.eu.org>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Window
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
import QtQml.Models
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
/**
|
||||
* @brief A timeline delegate for an image message.
|
||||
*
|
||||
* @inherit MessageDelegate
|
||||
*/
|
||||
MessageDelegate {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The media info for the event.
|
||||
*
|
||||
* This should consist of the following:
|
||||
* - source - The mxc URL for the media.
|
||||
* - mimeType - The MIME type of the media (should be image/xxx for this delegate).
|
||||
* - mimeIcon - The MIME icon name (should be image-xxx).
|
||||
* - size - The file size in bytes.
|
||||
* - width - The width in pixels of the audio media.
|
||||
* - height - The height in pixels of the audio media.
|
||||
* - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads.
|
||||
*/
|
||||
required property var mediaInfo
|
||||
|
||||
/**
|
||||
* @brief Whether the media has been downloaded.
|
||||
*/
|
||||
readonly property bool downloaded: root.progressInfo && root.progressInfo.completed
|
||||
|
||||
/**
|
||||
* @brief Whether the image should be automatically opened when downloaded.
|
||||
*/
|
||||
property bool openOnFinished: false
|
||||
|
||||
/**
|
||||
* @brief The maximum width of the image.
|
||||
*/
|
||||
readonly property var maxWidth: Kirigami.Units.gridUnit * 30
|
||||
|
||||
/**
|
||||
* @brief The maximum height of the image.
|
||||
*/
|
||||
readonly property var maxHeight: Kirigami.Units.gridUnit * 30
|
||||
|
||||
onOpenContextMenu: RoomManager.viewEventMenu(eventId, author, delegateType, plainText, "", "", mediaInfo.mimeType, progressInfo)
|
||||
|
||||
bubbleContent: Item {
|
||||
id: imageContainer
|
||||
|
||||
property var imageItem: root.mediaInfo.animated ? animatedImageLoader.item : imageLoader.item
|
||||
|
||||
implicitWidth: mediaSizeHelper.currentSize.width
|
||||
implicitHeight: mediaSizeHelper.currentSize.height
|
||||
|
||||
Loader {
|
||||
id: imageLoader
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
active: !root.mediaInfo.animated
|
||||
sourceComponent: Image {
|
||||
source: root.mediaInfo.source
|
||||
sourceSize.width: mediaSizeHelper.currentSize.width * Screen.devicePixelRatio
|
||||
sourceSize.height: mediaSizeHelper.currentSize.height * Screen.devicePixelRatio
|
||||
|
||||
fillMode: Image.PreserveAspectFit
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: animatedImageLoader
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
active: root.mediaInfo.animated
|
||||
sourceComponent: AnimatedImage {
|
||||
source: root.mediaInfo.source
|
||||
|
||||
fillMode: Image.PreserveAspectFit
|
||||
|
||||
paused: !applicationWindow().active
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
source: root.mediaInfo.tempInfo.source
|
||||
visible: imageContainer.imageItem.status !== Image.Ready
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: root.display
|
||||
QQC2.ToolTip.visible: hoverHandler.hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
|
||||
HoverHandler {
|
||||
id: hoverHandler
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
|
||||
visible: (root.progressInfo.active && !downloaded) || imageContainer.imageItem.status !== Image.Ready
|
||||
|
||||
color: "#BB000000"
|
||||
|
||||
QQC2.ProgressBar {
|
||||
anchors.centerIn: parent
|
||||
|
||||
width: parent.width * 0.8
|
||||
|
||||
from: 0
|
||||
to: root.progressInfo.total
|
||||
value: root.progressInfo.progress
|
||||
}
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
gesturePolicy: TapHandler.ReleaseWithinBounds | TapHandler.WithinBounds
|
||||
onTapped: {
|
||||
imageContainer.QQC2.ToolTip.hide();
|
||||
if (root.mediaInfo.animated) {
|
||||
imageContainer.imageItem.paused = true;
|
||||
}
|
||||
root.ListView.view.interactive = false;
|
||||
// We need to make sure the index is that of the MediaMessageFilterModel.
|
||||
if (root.ListView.view.model instanceof MessageFilterModel) {
|
||||
RoomManager.maximizeMedia(RoomManager.mediaMessageFilterModel.getRowForSourceItem(root.index));
|
||||
} else {
|
||||
RoomManager.maximizeMedia(root.index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function downloadAndOpen() {
|
||||
if (downloaded) {
|
||||
openSavedFile();
|
||||
} else {
|
||||
openOnFinished = true;
|
||||
root.room.downloadFile(root.eventId, StandardPaths.writableLocation(StandardPaths.CacheLocation) + "/" + root.eventId.replace(":", "_").replace("/", "_").replace("+", "_") + root.room.fileNameToDownload(root.eventId));
|
||||
}
|
||||
}
|
||||
|
||||
function openSavedFile() {
|
||||
if (UrlHelper.openUrl(root.progressInfo.localPath))
|
||||
return;
|
||||
if (UrlHelper.openUrl(root.progressInfo.localDir))
|
||||
return;
|
||||
}
|
||||
|
||||
MediaSizeHelper {
|
||||
id: mediaSizeHelper
|
||||
contentMaxWidth: root.contentMaxWidth
|
||||
mediaWidth: root.mediaInfo.width
|
||||
mediaHeight: root.mediaInfo.height
|
||||
}
|
||||
}
|
||||
}
|
||||
131
src/qml/LinkPreviewComponent.qml
Normal file
131
src/qml/LinkPreviewComponent.qml
Normal file
@@ -0,0 +1,131 @@
|
||||
// SPDX-FileCopyrightText: 2022 Bharadwaj Raju <bharadwaj.raju777@protonmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-or-later OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
/**
|
||||
* @brief A component to show a link preview from a message.
|
||||
*/
|
||||
QQC2.Control {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The link preview properties.
|
||||
*
|
||||
* This is a list or object containing the following:
|
||||
* - url - The URL being previewed.
|
||||
* - loaded - Whether the URL preview has been loaded.
|
||||
* - title - the title of the URL preview.
|
||||
* - description - the description of the URL preview.
|
||||
* - imageSource - a source URL for the preview image.
|
||||
*/
|
||||
required property var linkPreviewer
|
||||
|
||||
/**
|
||||
* @brief Standard height for the link preview.
|
||||
*
|
||||
* When the content of the link preview is larger than this it will be
|
||||
* elided/hidden until maximized.
|
||||
*/
|
||||
property var defaultHeight: Kirigami.Units.gridUnit * 3 + Kirigami.Units.smallSpacing * 2
|
||||
|
||||
property bool truncated: linkPreviewDescription.truncated || !linkPreviewDescription.visible
|
||||
|
||||
/**
|
||||
* @brief The maximum width that the bubble's content can be.
|
||||
*/
|
||||
property real maxContentWidth: -1
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: root.maxContentWidth
|
||||
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
|
||||
contentItem: RowLayout {
|
||||
id: contentRow
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
Rectangle {
|
||||
id: separator
|
||||
Layout.fillHeight: true
|
||||
width: Kirigami.Units.smallSpacing
|
||||
color: Kirigami.Theme.highlightColor
|
||||
}
|
||||
Image {
|
||||
id: previewImage
|
||||
Layout.preferredWidth: root.defaultHeight
|
||||
Layout.preferredHeight: root.defaultHeight
|
||||
visible: root.linkPreviewer.imageSource.length > 0
|
||||
source: root.linkPreviewer.imageSource
|
||||
fillMode: Image.PreserveAspectFit
|
||||
}
|
||||
ColumnLayout {
|
||||
id: column
|
||||
implicitWidth: Math.max(linkPreviewTitle.implicitWidth, linkPreviewDescription.implicitWidth)
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
Kirigami.Heading {
|
||||
id: linkPreviewTitle
|
||||
Layout.fillWidth: true
|
||||
level: 3
|
||||
wrapMode: Text.Wrap
|
||||
textFormat: Text.RichText
|
||||
text: "<style>
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
<a href=\"" + root.linkPreviewer.url + "\">" + (maximizeButton.checked ? root.linkPreviewer.title : titleTextMetrics.elidedText).replace("–", "—") + "</a>"
|
||||
onLinkActivated: RoomManager.resolveResource(link, "join")
|
||||
|
||||
TextMetrics {
|
||||
id: titleTextMetrics
|
||||
text: root.linkPreviewer.title
|
||||
font: linkPreviewTitle.font
|
||||
elide: Text.ElideRight
|
||||
elideWidth: (linkPreviewTitle.availableWidth()) * 3
|
||||
}
|
||||
|
||||
function availableWidth() {
|
||||
let previewImageWidth = (previewImage.visible ? previewImage.width + contentRow.spacing : 0);
|
||||
return root.maxContentWidth - contentRow.spacing - separator.width - previewImageWidth;
|
||||
}
|
||||
}
|
||||
QQC2.Label {
|
||||
id: linkPreviewDescription
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumHeight: maximizeButton.checked ? -1 : root.defaultHeight - linkPreviewTitle.height - column.spacing
|
||||
visible: linkPreviewTitle.height + column.spacing <= root.defaultHeight || maximizeButton.checked
|
||||
text: linkPreviewer.description
|
||||
wrapMode: Text.Wrap
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.Button {
|
||||
id: maximizeButton
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
visible: root.hovered && (root.truncated || checked)
|
||||
checkable: true
|
||||
text: checked ? i18n("Shrink preview") : i18n("Expand preview")
|
||||
icon.name: checked ? "go-up" : "go-down"
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
|
||||
QQC2.ToolTip {
|
||||
text: maximizeButton.text
|
||||
visible: hovered
|
||||
delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2022 Bharadwaj Raju <bharadwaj.raju777@protonmail.com>
|
||||
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-or-later OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
Loader {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The link preview properties.
|
||||
*
|
||||
* This is a list or object containing the following:
|
||||
* - url - The URL being previewed.
|
||||
* - loaded - Whether the URL preview has been loaded.
|
||||
* - title - the title of the URL preview.
|
||||
* - description - the description of the URL preview.
|
||||
* - imageSource - a source URL for the preview image.
|
||||
*/
|
||||
required property var linkPreviewer
|
||||
|
||||
/**
|
||||
* @brief Standard height for the link preview.
|
||||
*
|
||||
* When the content of the link preview is larger than this it will be
|
||||
* elided/hidden until maximized.
|
||||
*/
|
||||
property var defaultHeight: Kirigami.Units.gridUnit * 3 + Kirigami.Units.smallSpacing * 2
|
||||
|
||||
/**
|
||||
* @brief Whether the loading indicator should animate if visible.
|
||||
*/
|
||||
property bool indicatorEnabled: false
|
||||
|
||||
visible: active
|
||||
sourceComponent: linkPreviewer && linkPreviewer.loaded ? linkPreviewComponent : loadingComponent
|
||||
|
||||
Component {
|
||||
id: linkPreviewComponent
|
||||
QQC2.Control {
|
||||
id: componentRoot
|
||||
property bool truncated: linkPreviewDescription.truncated || !linkPreviewDescription.visible
|
||||
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
Rectangle {
|
||||
Layout.fillHeight: true
|
||||
width: Kirigami.Units.smallSpacing
|
||||
color: Kirigami.Theme.highlightColor
|
||||
}
|
||||
Image {
|
||||
visible: root.linkPreviewer.imageSource
|
||||
Layout.maximumHeight: root.defaultHeight
|
||||
Layout.maximumWidth: root.defaultHeight
|
||||
source: root.linkPreviewer.imageSource
|
||||
fillMode: Image.PreserveAspectFit
|
||||
}
|
||||
ColumnLayout {
|
||||
id: column
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
Kirigami.Heading {
|
||||
id: linkPreviewTitle
|
||||
Layout.fillWidth: true
|
||||
level: 3
|
||||
wrapMode: Text.Wrap
|
||||
textFormat: Text.RichText
|
||||
text: "<style>
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
<a href=\"" + root.linkPreviewer.url + "\">" + (maximizeButton.checked ? root.linkPreviewer.title : titleTextMetrics.elidedText).replace("–", "—") + "</a>"
|
||||
onLinkActivated: RoomManager.resolveResource(link, "join")
|
||||
|
||||
TextMetrics {
|
||||
id: titleTextMetrics
|
||||
text: root.linkPreviewer.title
|
||||
font: linkPreviewTitle.font
|
||||
elide: Text.ElideRight
|
||||
elideWidth: (linkPreviewTitle.width - Kirigami.Units.largeSpacing * 2.5) * 3
|
||||
}
|
||||
}
|
||||
QQC2.Label {
|
||||
id: linkPreviewDescription
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumHeight: maximizeButton.checked ? -1 : root.defaultHeight - linkPreviewTitle.height - column.spacing
|
||||
visible: linkPreviewTitle.height + column.spacing <= root.defaultHeight || maximizeButton.checked
|
||||
text: linkPreviewer.description
|
||||
wrapMode: Text.Wrap
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.Button {
|
||||
id: maximizeButton
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
visible: componentRoot.hovered && (componentRoot.truncated || checked)
|
||||
checkable: true
|
||||
text: checked ? i18n("Shrink preview") : i18n("Expand preview")
|
||||
icon.name: checked ? "go-up" : "go-down"
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
|
||||
QQC2.ToolTip {
|
||||
text: maximizeButton.text
|
||||
visible: hovered
|
||||
delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: loadingComponent
|
||||
RowLayout {
|
||||
id: componentRoot
|
||||
property bool truncated: false
|
||||
|
||||
Rectangle {
|
||||
Layout.fillHeight: true
|
||||
width: Kirigami.Units.smallSpacing
|
||||
color: Kirigami.Theme.highlightColor
|
||||
}
|
||||
QQC2.BusyIndicator {
|
||||
running: root.indicatorEnabled
|
||||
}
|
||||
Kirigami.Heading {
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: root.defaultHeight
|
||||
level: 2
|
||||
text: i18n("Loading URL preview")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
92
src/qml/LiveLocationComponent.qml
Normal file
92
src/qml/LiveLocationComponent.qml
Normal file
@@ -0,0 +1,92 @@
|
||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-FileCopyrightText: 2023 Volker Krause <vkrause@kde.org>
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtLocation
|
||||
import QtPositioning
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
/**
|
||||
* @brief A component to show a live location from a message.
|
||||
*/
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The NeoChatRoom the delegate is being displayed in.
|
||||
*/
|
||||
required property NeoChatRoom room
|
||||
|
||||
/**
|
||||
* @brief The matrix ID of the message event.
|
||||
*/
|
||||
required property string eventId
|
||||
|
||||
/**
|
||||
* @brief The display text of the message.
|
||||
*/
|
||||
required property string display
|
||||
|
||||
/**
|
||||
* @brief The maximum width that the bubble's content can be.
|
||||
*/
|
||||
property real maxContentWidth: -1
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: root.maxContentWidth
|
||||
|
||||
LiveLocationsModel {
|
||||
id: liveLocationModel
|
||||
eventId: root.eventId
|
||||
room: root.room
|
||||
}
|
||||
MapView {
|
||||
id: mapView
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: root.maxContentWidth
|
||||
Layout.preferredHeight: root.maxContentWidth / 16 * 9
|
||||
|
||||
map.center: QtPositioning.coordinate(liveLocationModel.boundingBox.y, liveLocationModel.boundingBox.x)
|
||||
map.zoomLevel: 15
|
||||
|
||||
map.plugin: OsmLocationPlugin.plugin
|
||||
|
||||
MapItemView {
|
||||
model: liveLocationModel
|
||||
delegate: LocationMapItem {}
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onTapped: {
|
||||
let map = fullScreenMap.createObject(parent, {liveLocationModel: liveLocationModel});
|
||||
map.open()
|
||||
}
|
||||
onLongPressed: openMessageContext("")
|
||||
}
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.RightButton
|
||||
onTapped: openMessageContext("")
|
||||
}
|
||||
Connections {
|
||||
target: mapView.map
|
||||
function onCopyrightLinkActivated() {
|
||||
Qt.openUrlExternally(link)
|
||||
}
|
||||
}
|
||||
}
|
||||
Component {
|
||||
id: fullScreenMap
|
||||
FullScreenMap {}
|
||||
}
|
||||
|
||||
TextComponent {
|
||||
display: root.display
|
||||
visible: root.display !== ""
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-FileCopyrightText: 2023 Volker Krause <vkrause@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtLocation
|
||||
import QtPositioning
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
/**
|
||||
* @brief A timeline delegate for a location message.
|
||||
*
|
||||
* @inherit MessageDelegate
|
||||
*/
|
||||
MessageDelegate {
|
||||
id: root
|
||||
|
||||
bubbleContent: ColumnLayout {
|
||||
LiveLocationsModel {
|
||||
id: liveLocationModel
|
||||
eventId: root.eventId
|
||||
room: root.room
|
||||
}
|
||||
MapView {
|
||||
id: mapView
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: root.contentMaxWidth / 16 * 9
|
||||
|
||||
map.center: QtPositioning.coordinate(liveLocationModel.boundingBox.y, liveLocationModel.boundingBox.x)
|
||||
map.zoomLevel: 15
|
||||
|
||||
map.plugin: OsmLocationPlugin.plugin
|
||||
|
||||
MapItemView {
|
||||
model: liveLocationModel
|
||||
delegate: LocationMapItem {}
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onTapped: {
|
||||
let map = fullScreenMap.createObject(parent, {
|
||||
liveLocationModel: liveLocationModel
|
||||
});
|
||||
map.open();
|
||||
}
|
||||
onLongPressed: openMessageContext("")
|
||||
}
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.RightButton
|
||||
onTapped: openMessageContext("")
|
||||
}
|
||||
Connections {
|
||||
target: mapView.map
|
||||
function onCopyrightLinkActivated() {
|
||||
Qt.openUrlExternally(link);
|
||||
}
|
||||
}
|
||||
}
|
||||
Component {
|
||||
id: fullScreenMap
|
||||
FullScreenMap {}
|
||||
}
|
||||
|
||||
RichLabel {
|
||||
textMessage: root.display
|
||||
visible: root.display !== ""
|
||||
}
|
||||
}
|
||||
}
|
||||
60
src/qml/LoadComponent.qml
Normal file
60
src/qml/LoadComponent.qml
Normal file
@@ -0,0 +1,60 @@
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-or-later OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
/**
|
||||
* @brief A component to show a link preview loading from a message.
|
||||
*/
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
required property int type
|
||||
|
||||
/**
|
||||
* @brief Standard height for the link preview.
|
||||
*
|
||||
* When the content of the link preview is larger than this it will be
|
||||
* elided/hidden until maximized.
|
||||
*/
|
||||
property var defaultHeight: Kirigami.Units.gridUnit * 3 + Kirigami.Units.smallSpacing * 2
|
||||
|
||||
/**
|
||||
* @brief The maximum width that the bubble's content can be.
|
||||
*/
|
||||
property real maxContentWidth: -1
|
||||
|
||||
enum Type {
|
||||
Reply,
|
||||
LinkPreview
|
||||
}
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: root.maxContentWidth
|
||||
|
||||
Rectangle {
|
||||
Layout.fillHeight: true
|
||||
width: Kirigami.Units.smallSpacing
|
||||
color: Kirigami.Theme.highlightColor
|
||||
}
|
||||
QQC2.BusyIndicator {}
|
||||
Kirigami.Heading {
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: root.defaultHeight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
level: 2
|
||||
text: {
|
||||
switch (root.type) {
|
||||
case LoadComponent.Reply:
|
||||
return i18n("Loading reply");
|
||||
case LoadComponent.LinkPreview:
|
||||
return i18n("Loading URL preview");
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
116
src/qml/LocationComponent.qml
Normal file
116
src/qml/LocationComponent.qml
Normal file
@@ -0,0 +1,116 @@
|
||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtLocation
|
||||
import QtPositioning
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
/**
|
||||
* @brief A component to show a location from a message.
|
||||
*/
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The message author.
|
||||
*
|
||||
* This should consist of the following:
|
||||
* - id - The matrix ID of the author.
|
||||
* - isLocalUser - Whether the author is the local user.
|
||||
* - avatarSource - The mxc URL for the author's avatar in the current room.
|
||||
* - avatarMediaId - The media ID of the author's avatar.
|
||||
* - avatarUrl - The mxc URL for the author's avatar.
|
||||
* - displayName - The display name of the author.
|
||||
* - display - The name of the author.
|
||||
* - color - The color for the author.
|
||||
* - object - The Quotient::User object for the author.
|
||||
*
|
||||
* @sa Quotient::User
|
||||
*/
|
||||
required property var author
|
||||
|
||||
/**
|
||||
* @brief The display text of the message.
|
||||
*/
|
||||
required property string display
|
||||
|
||||
/**
|
||||
* @brief The latitude of the location marker in the message.
|
||||
*/
|
||||
required property real latitude
|
||||
|
||||
/**
|
||||
* @brief The longitude of the location marker in the message.
|
||||
*/
|
||||
required property real longitude
|
||||
|
||||
/**
|
||||
* @brief What type of marker the location message is.
|
||||
*
|
||||
* The main options are m.pin for a general location or m.self for a pin to show
|
||||
* a user's location.
|
||||
*/
|
||||
required property string asset
|
||||
|
||||
/**
|
||||
* @brief The maximum width that the bubble's content can be.
|
||||
*/
|
||||
property real maxContentWidth: -1
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: root.maxContentWidth
|
||||
|
||||
MapView {
|
||||
id: mapView
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: root.maxContentWidth
|
||||
Layout.preferredHeight: root.maxContentWidth / 16 * 9
|
||||
|
||||
map.center: QtPositioning.coordinate(root.latitude, root.longitude)
|
||||
map.zoomLevel: 15
|
||||
|
||||
map.plugin: OsmLocationPlugin.plugin
|
||||
|
||||
LocationMapItem {
|
||||
latitude: root.latitude
|
||||
longitude: root.longitude
|
||||
asset: root.asset
|
||||
author: root.author
|
||||
isLive: true
|
||||
heading: NaN
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onTapped: {
|
||||
let map = fullScreenMap.createObject(parent, {latitude: root.latitude, longitude: root.longitude, asset: root.asset, author: root.author});
|
||||
map.open()
|
||||
}
|
||||
onLongPressed: openMessageContext("")
|
||||
}
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.RightButton
|
||||
onTapped: openMessageContext("")
|
||||
}
|
||||
Connections {
|
||||
target: mapView.map
|
||||
function onCopyrightLinkActivated() {
|
||||
Qt.openUrlExternally(link)
|
||||
}
|
||||
}
|
||||
}
|
||||
Component {
|
||||
id: fullScreenMap
|
||||
FullScreenMap { }
|
||||
}
|
||||
|
||||
TextComponent {
|
||||
display: root.display
|
||||
visible: root.display !== ""
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtLocation
|
||||
import QtPositioning
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
/**
|
||||
* @brief A timeline delegate for a location message.
|
||||
*
|
||||
* @inherit MessageDelegate
|
||||
*/
|
||||
MessageDelegate {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The latitude of the location marker in the message.
|
||||
*/
|
||||
required property real latitude
|
||||
|
||||
/**
|
||||
* @brief The longitude of the location marker in the message.
|
||||
*/
|
||||
required property real longitude
|
||||
|
||||
/**
|
||||
* @brief What type of marker the location message is.
|
||||
*
|
||||
* The main options are m.pin for a general location or m.self for a pin to show
|
||||
* a user's location.
|
||||
*/
|
||||
required property string asset
|
||||
|
||||
bubbleContent: ColumnLayout {
|
||||
MapView {
|
||||
id: mapView
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: root.contentMaxWidth / 16 * 9
|
||||
|
||||
map.center: QtPositioning.coordinate(root.latitude, root.longitude)
|
||||
map.zoomLevel: 15
|
||||
|
||||
map.plugin: OsmLocationPlugin.plugin
|
||||
|
||||
LocationMapItem {
|
||||
latitude: root.latitude
|
||||
longitude: root.longitude
|
||||
asset: root.asset
|
||||
author: root.author
|
||||
isLive: true
|
||||
heading: NaN
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onTapped: {
|
||||
let map = fullScreenMap.createObject(parent, {
|
||||
latitude: root.latitude,
|
||||
longitude: root.longitude,
|
||||
asset: root.asset,
|
||||
author: root.author
|
||||
});
|
||||
map.open();
|
||||
}
|
||||
onLongPressed: openMessageContext("")
|
||||
}
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.RightButton
|
||||
onTapped: openMessageContext("")
|
||||
}
|
||||
Connections {
|
||||
target: mapView.map
|
||||
function onCopyrightLinkActivated() {
|
||||
Qt.openUrlExternally(link);
|
||||
}
|
||||
}
|
||||
}
|
||||
Component {
|
||||
id: fullScreenMap
|
||||
FullScreenMap {}
|
||||
}
|
||||
|
||||
RichLabel {
|
||||
textMessage: root.display
|
||||
visible: root.display !== ""
|
||||
}
|
||||
}
|
||||
}
|
||||
172
src/qml/MessageComponentChooser.qml
Normal file
172
src/qml/MessageComponentChooser.qml
Normal file
@@ -0,0 +1,172 @@
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtQuick
|
||||
import Qt.labs.qmlmodels
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
/**
|
||||
* @brief Select a message component based on a MessageComponentType.
|
||||
*/
|
||||
DelegateChooser {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The NeoChatRoom the delegate is being displayed in.
|
||||
*/
|
||||
required property NeoChatRoom room
|
||||
|
||||
/**
|
||||
* @brief The ActionsHandler object to use.
|
||||
*
|
||||
* This is expected to have the correct room set otherwise messages will be sent
|
||||
* to the wrong room.
|
||||
*/
|
||||
required property ActionsHandler actionsHandler
|
||||
|
||||
/**
|
||||
* @brief The timeline ListView this component is being used in.
|
||||
*/
|
||||
required property ListView timeline
|
||||
|
||||
/**
|
||||
* @brief The maximum width that the bubble's content can be.
|
||||
*/
|
||||
property real maxContentWidth: -1
|
||||
|
||||
/**
|
||||
* @brief The reply has been clicked.
|
||||
*/
|
||||
signal replyClicked(string eventID)
|
||||
|
||||
/**
|
||||
* @brief The user selected text has changed.
|
||||
*/
|
||||
signal selectedTextChanged(string selectedText)
|
||||
|
||||
/**
|
||||
* @brief Request a context menu be show for the message.
|
||||
*/
|
||||
signal showMessageMenu()
|
||||
|
||||
role: "componentType"
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.Text
|
||||
delegate: TextComponent {
|
||||
maxContentWidth: root.maxContentWidth
|
||||
onSelectedTextChanged: root.selectedTextChanged(selectedText);
|
||||
onShowMessageMenu: root.showMessageMenu()
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.Image
|
||||
delegate: ImageComponent {
|
||||
room: root.room
|
||||
timeline: root.timeline
|
||||
maxContentWidth: root.maxContentWidth
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.Video
|
||||
delegate: VideoComponent {
|
||||
room: root.room
|
||||
timeline: root.timeline
|
||||
maxContentWidth: root.maxContentWidth
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.Audio
|
||||
delegate: AudioComponent {
|
||||
room: root.room
|
||||
maxContentWidth: root.maxContentWidth
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.File
|
||||
delegate: FileComponent {
|
||||
room: root.room
|
||||
maxContentWidth: root.maxContentWidth
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.Poll
|
||||
delegate: PollComponent {
|
||||
room: root.room
|
||||
maxContentWidth: root.maxContentWidth
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.Location
|
||||
delegate: LocationComponent {
|
||||
maxContentWidth: root.maxContentWidth
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.LiveLocation
|
||||
delegate: LiveLocationComponent {
|
||||
room: root.room
|
||||
maxContentWidth: root.maxContentWidth
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.Encrypted
|
||||
delegate: EncryptedComponent {
|
||||
maxContentWidth: root.maxContentWidth
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.Reply
|
||||
delegate: ReplyComponent {
|
||||
maxContentWidth: root.maxContentWidth
|
||||
onReplyClicked: (eventId) => {root.replyClicked(eventId)}
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.ReplyLoad
|
||||
delegate: LoadComponent {
|
||||
type: LoadComponent.Reply
|
||||
maxContentWidth: root.maxContentWidth
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.LinkPreview
|
||||
delegate: LinkPreviewComponent {
|
||||
maxContentWidth: root.maxContentWidth
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.LinkPreviewLoad
|
||||
delegate: LoadComponent {
|
||||
type: LoadComponent.LinkPreview
|
||||
maxContentWidth: root.maxContentWidth
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.Edit
|
||||
delegate: MessageEditComponent {
|
||||
room: root.room
|
||||
actionsHandler: root.actionsHandler
|
||||
maxContentWidth: root.maxContentWidth
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.Other
|
||||
delegate: Item {}
|
||||
}
|
||||
}
|
||||
@@ -88,19 +88,9 @@ TimelineDelegate {
|
||||
property bool alwaysShowAuthor: false
|
||||
|
||||
/**
|
||||
* @brief The delegate type of the message.
|
||||
* @brief The model to visualise the content of the message.
|
||||
*/
|
||||
required property int delegateType
|
||||
|
||||
/**
|
||||
* @brief The display text of the message.
|
||||
*/
|
||||
required property string display
|
||||
|
||||
/**
|
||||
* @brief The display text of the message as plain text.
|
||||
*/
|
||||
required property string plainText
|
||||
required property MessageContentModel contentModel
|
||||
|
||||
/**
|
||||
* @brief The date of the event as a string.
|
||||
@@ -142,65 +132,10 @@ TimelineDelegate {
|
||||
*/
|
||||
required property bool showReadMarkers
|
||||
|
||||
/**
|
||||
* @brief The matrix ID of the reply event.
|
||||
*/
|
||||
required property var replyId
|
||||
|
||||
/**
|
||||
* @brief The reply author.
|
||||
*
|
||||
* This should consist of the following:
|
||||
* - id - The matrix ID of the reply author.
|
||||
* - isLocalUser - Whether the reply author is the local user.
|
||||
* - avatarSource - The mxc URL for the reply author's avatar in the current room.
|
||||
* - avatarMediaId - The media ID of the reply author's avatar.
|
||||
* - avatarUrl - The mxc URL for the reply author's avatar.
|
||||
* - displayName - The display name of the reply author.
|
||||
* - display - The name of the reply author.
|
||||
* - color - The color for the reply author.
|
||||
* - object - The Quotient::User object for the reply author.
|
||||
*
|
||||
* @sa Quotient::User
|
||||
*/
|
||||
required property var replyAuthor
|
||||
|
||||
/**
|
||||
* @brief The delegate type of the message replied to.
|
||||
*/
|
||||
required property int replyDelegateType
|
||||
|
||||
/**
|
||||
* @brief The display text of the message replied to.
|
||||
*/
|
||||
required property string replyDisplay
|
||||
|
||||
/**
|
||||
* @brief The media info for the reply event.
|
||||
*
|
||||
* This could be an image, audio, video or file.
|
||||
*
|
||||
* This should consist of the following:
|
||||
* - source - The mxc URL for the media.
|
||||
* - mimeType - The MIME type of the media.
|
||||
* - mimeIcon - The MIME icon name.
|
||||
* - size - The file size in bytes.
|
||||
* - duration - The length in seconds of the audio media (audio/video only).
|
||||
* - width - The width in pixels of the audio media (image/video only).
|
||||
* - height - The height in pixels of the audio media (image/video only).
|
||||
* - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads (image/video only).
|
||||
*/
|
||||
required property var replyMediaInfo
|
||||
|
||||
required property bool isThreaded
|
||||
|
||||
required property string threadRoot
|
||||
|
||||
/**
|
||||
* @brief Whether this message is replying to another.
|
||||
*/
|
||||
required property bool isReply
|
||||
|
||||
/**
|
||||
* @brief Whether this message has a local user mention.
|
||||
*/
|
||||
@@ -211,13 +146,6 @@ TimelineDelegate {
|
||||
*/
|
||||
required property bool isPending
|
||||
|
||||
/**
|
||||
* @brief Progress info when downloading files.
|
||||
*
|
||||
* @sa Quotient::FileTransferInfo
|
||||
*/
|
||||
required property var progressInfo
|
||||
|
||||
/**
|
||||
* @brief Whether an encrypted message is sent in a verified session.
|
||||
*/
|
||||
@@ -249,11 +177,6 @@ TimelineDelegate {
|
||||
*/
|
||||
readonly property alias hovered: bubble.hovered
|
||||
|
||||
/**
|
||||
* @brief Open the context menu for the message.
|
||||
*/
|
||||
signal openContextMenu
|
||||
|
||||
/**
|
||||
* @brief Open the any message media externally.
|
||||
*/
|
||||
@@ -268,7 +191,7 @@ TimelineDelegate {
|
||||
/**
|
||||
* @brief The main delegate content item to show in the bubble.
|
||||
*/
|
||||
property alias bubbleContent: bubble.content
|
||||
property var bubbleContent
|
||||
|
||||
/**
|
||||
* @brief Whether the bubble background is enabled.
|
||||
@@ -293,6 +216,11 @@ TimelineDelegate {
|
||||
*/
|
||||
property bool isTemporaryHighlighted: false
|
||||
|
||||
/**
|
||||
* @brief The user selected text.
|
||||
*/
|
||||
property string selectedText: ""
|
||||
|
||||
onIsTemporaryHighlightedChanged: if (isTemporaryHighlighted) {
|
||||
temporaryHighlightTimer.start();
|
||||
}
|
||||
@@ -329,12 +257,6 @@ TimelineDelegate {
|
||||
|
||||
implicitHeight: Math.max(root.showAuthor || root.alwaysShowAuthor ? avatar.implicitHeight : 0, bubble.height)
|
||||
|
||||
Component.onCompleted: {
|
||||
if (root.isReply && root.replyDelegateType === DelegateType.Other) {
|
||||
root.room.loadReply(root.eventId, root.replyId);
|
||||
}
|
||||
}
|
||||
|
||||
// show hover actions
|
||||
onHoveredChanged: {
|
||||
if (hovered && !Kirigami.Settings.isMobile) {
|
||||
@@ -395,23 +317,24 @@ TimelineDelegate {
|
||||
}
|
||||
]
|
||||
|
||||
room: root.room
|
||||
|
||||
author: root.author
|
||||
showAuthor: root.showAuthor || root.alwaysShowAuthor
|
||||
time: root.time
|
||||
timeString: root.timeString
|
||||
|
||||
showHighlight: root.showHighlight
|
||||
contentModel: root.contentModel
|
||||
actionsHandler: root.ListView.view?.actionsHandler ?? null
|
||||
timeline: root.ListView.view
|
||||
|
||||
isReply: root.isReply
|
||||
replyId: root.replyId
|
||||
replyAuthor: root.replyAuthor
|
||||
replyDelegateType: root.replyDelegateType
|
||||
replyDisplay: root.replyDisplay
|
||||
replyMediaInfo: root.replyMediaInfo
|
||||
showHighlight: root.showHighlight
|
||||
|
||||
onReplyClicked: eventId => {
|
||||
root.replyClicked(eventId);
|
||||
}
|
||||
onSelectedTextChanged: (selectedText) => {root.selectedText = selectedText;}
|
||||
onShowMessageMenu: _private.showMessageMenu()
|
||||
|
||||
showBackground: root.cardBackground && !Config.compactLayout
|
||||
}
|
||||
@@ -424,12 +347,12 @@ TimelineDelegate {
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.RightButton
|
||||
onTapped: root.openContextMenu()
|
||||
onTapped: _private.showMessageMenu()
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onLongPressed: root.openContextMenu()
|
||||
onLongPressed: _private.showMessageMenu()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -482,5 +405,9 @@ TimelineDelegate {
|
||||
* @brief Whether local user messages should be aligned right.
|
||||
*/
|
||||
property bool showUserMessageOnRight: Config.showLocalMessagesOnRight && root.author.isLocalUser && !Config.compactLayout && !root.alwaysMaxWidth
|
||||
|
||||
function showMessageMenu() {
|
||||
RoomManager.viewEventMenu(root.eventId, root.room, root.selectedText)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ Loader {
|
||||
/**
|
||||
* @brief The delegate type of the message.
|
||||
*/
|
||||
required property int delegateType
|
||||
required property int messageComponentType
|
||||
|
||||
/**
|
||||
* @brief The display text of the message as plain text.
|
||||
@@ -96,7 +96,7 @@ Loader {
|
||||
currentRoom.editCache.editId = eventId;
|
||||
currentRoom.mainCache.replyId = "";
|
||||
}
|
||||
visible: author.isLocalUser && (root.delegateType === DelegateType.Emote || root.delegateType === DelegateType.Message)
|
||||
visible: author.isLocalUser && (root.messageComponentType === MessageComponentType.Emote || root.messageComponentType === MessageComponentType.Message)
|
||||
},
|
||||
Kirigami.Action {
|
||||
text: i18n("Reply")
|
||||
|
||||
@@ -9,13 +9,19 @@ import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
/**
|
||||
* @brief A component to show an edit text field for a text message being edited.
|
||||
*/
|
||||
QQC2.TextArea {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The NeoChatRoom the delegate is being displayed in.
|
||||
*/
|
||||
required property NeoChatRoom room
|
||||
onRoomChanged: {
|
||||
_private.chatBarCache = room.editCache;
|
||||
_private.chatBarCache.relationIdChanged.connect(_private.updateEditText);
|
||||
_private.chatBarCache.relationIdChanged.connect(_private.updateEditText());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -26,10 +32,19 @@ QQC2.TextArea {
|
||||
*/
|
||||
required property ActionsHandler actionsHandler
|
||||
|
||||
property string messageId
|
||||
|
||||
property var minimumHeight: editButtons.height + topPadding + bottomPadding
|
||||
property var preferredWidth: editTextMetrics.advanceWidth + rightPadding + Kirigami.Units.smallSpacing + Kirigami.Units.gridUnit
|
||||
|
||||
/**
|
||||
* @brief The maximum width that the bubble's content can be.
|
||||
*/
|
||||
property real maxContentWidth: -1
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: root.maxContentWidth
|
||||
|
||||
Component.onCompleted: _private.updateEditText()
|
||||
|
||||
rightPadding: editButtons.width + editButtons.anchors.rightMargin * 2
|
||||
|
||||
color: Kirigami.Theme.textColor
|
||||
|
||||
@@ -7,6 +7,9 @@ import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
/**
|
||||
* @brief A component to show media based upon its mime type.
|
||||
*/
|
||||
RowLayout {
|
||||
property alias mimeIconSource: icon.source
|
||||
property alias label: nameLabel.text
|
||||
|
||||
@@ -26,12 +26,6 @@ Components.AlbumMaximizeComponent {
|
||||
|
||||
readonly property var currentTime: model.data(model.index(content.currentIndex, 0), MessageEventModel.TimeRole)
|
||||
|
||||
readonly property var currentDelegateType: model.data(model.index(content.currentIndex, 0), MessageEventModel.DelegateTypeRole)
|
||||
|
||||
readonly property string currentPlainText: model.data(model.index(content.currentIndex, 0), MessageEventModel.PlainText)
|
||||
|
||||
readonly property var currentMimeType: model.data(model.index(content.currentIndex, 0), MessageEventModel.MimeTypeRole)
|
||||
|
||||
readonly property var currentProgressInfo: model.data(model.index(content.currentIndex, 0), MessageEventModel.ProgressInfoRole)
|
||||
|
||||
downloadAction: Components.DownloadAction {
|
||||
@@ -87,7 +81,8 @@ Components.AlbumMaximizeComponent {
|
||||
}
|
||||
}
|
||||
}
|
||||
onItemRightClicked: RoomManager.viewEventMenu(root.currentEventId, root.currentAuthor, root.currentDelegateType, root.currentPlainText, "", "", root.currentMimeType, root.currentProgressInfo)
|
||||
|
||||
onItemRightClicked: RoomManager.viewEventMenu(root.currentEventId, root.currentRoom)
|
||||
|
||||
onSaveItem: {
|
||||
var dialog = saveAsDialog.createObject(QQC2.ApplicationWindow.overlay);
|
||||
|
||||
76
src/qml/PollComponent.qml
Normal file
76
src/qml/PollComponent.qml
Normal file
@@ -0,0 +1,76 @@
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Qt.labs.platform
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
/**
|
||||
* @brief A component to show a poll from a message.
|
||||
*/
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The NeoChatRoom the delegate is being displayed in.
|
||||
*/
|
||||
required property NeoChatRoom room
|
||||
|
||||
/**
|
||||
* @brief The matrix ID of the message event.
|
||||
*/
|
||||
required property string eventId
|
||||
|
||||
/**
|
||||
* @brief The poll handler for this poll.
|
||||
*
|
||||
* This contains the required information like what the question, answers and
|
||||
* current number of votes for each is.
|
||||
*/
|
||||
required property var pollHandler
|
||||
|
||||
/**
|
||||
* @brief The maximum width that the bubble's content can be.
|
||||
*/
|
||||
property real maxContentWidth: -1
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: root.maxContentWidth
|
||||
|
||||
Label {
|
||||
id: questionLabel
|
||||
text: root.pollHandler.question
|
||||
wrapMode: Text.Wrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Repeater {
|
||||
model: root.pollHandler.options
|
||||
delegate: RowLayout {
|
||||
Layout.fillWidth: true
|
||||
CheckBox {
|
||||
checked: root.pollHandler.answers[root.room.localUser.id] ? root.pollHandler.answers[root.room.localUser.id].includes(modelData["id"]) : false
|
||||
onClicked: root.pollHandler.sendPollAnswer(root.eventId, modelData["id"])
|
||||
enabled: !root.pollHandler.hasEnded
|
||||
}
|
||||
Label {
|
||||
text: modelData["org.matrix.msc1767.text"]
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
Label {
|
||||
visible: root.pollHandler.kind == "org.matrix.msc3381.poll.disclosed" || pollHandler.hasEnded
|
||||
Layout.preferredWidth: contentWidth
|
||||
text: root.pollHandler.counts[modelData["id"]] ?? "0"
|
||||
}
|
||||
}
|
||||
}
|
||||
Label {
|
||||
visible: root.pollHandler.kind == "org.matrix.msc3381.poll.disclosed" || root.pollHandler.hasEnded
|
||||
text: i18np("Based on votes by %1 user", "Based on votes by %1 users", root.pollHandler.answerCount) + (root.pollHandler.hasEnded ? (" " + i18nc("as in 'this vote has ended'", "(Ended)")) : "")
|
||||
font.pointSize: questionLabel.font.pointSize * 0.8
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Qt.labs.platform
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
/**
|
||||
* @brief A timeline delegate for a poll message.
|
||||
*
|
||||
* @inherit MessageDelegate
|
||||
*/
|
||||
MessageDelegate {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The poll handler for this poll.
|
||||
*
|
||||
* This contains the required information like what the question, answers and
|
||||
* current number of votes for each is.
|
||||
*/
|
||||
required property var pollHandler
|
||||
|
||||
bubbleContent: ColumnLayout {
|
||||
Label {
|
||||
id: questionLabel
|
||||
text: root.pollHandler.question
|
||||
wrapMode: Text.Wrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Repeater {
|
||||
model: root.pollHandler.options
|
||||
delegate: RowLayout {
|
||||
Layout.fillWidth: true
|
||||
CheckBox {
|
||||
checked: root.pollHandler.answers[root.room.localUser.id] ? root.pollHandler.answers[root.room.localUser.id].includes(modelData["id"]) : false
|
||||
onClicked: root.pollHandler.sendPollAnswer(root.eventId, modelData["id"])
|
||||
enabled: !root.pollHandler.hasEnded
|
||||
}
|
||||
Label {
|
||||
text: modelData["org.matrix.msc1767.text"]
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
Label {
|
||||
visible: root.pollHandler.kind == "org.matrix.msc3381.poll.disclosed" || pollHandler.hasEnded
|
||||
Layout.preferredWidth: contentWidth
|
||||
text: root.pollHandler.counts[modelData["id"]] ?? "0"
|
||||
}
|
||||
}
|
||||
}
|
||||
Label {
|
||||
visible: root.pollHandler.kind == "org.matrix.msc3381.poll.disclosed" || root.pollHandler.hasEnded
|
||||
text: i18np("Based on votes by %1 user", "Based on votes by %1 users", root.pollHandler.answerCount) + (root.pollHandler.hasEnded ? (" " + i18nc("as in 'this vote has ended'", "(Ended)")) : "")
|
||||
font.pointSize: questionLabel.font.pointSize * 0.8
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
import Qt.labs.qmlmodels
|
||||
|
||||
import org.kde.coreaddons
|
||||
import org.kde.kirigami as Kirigami
|
||||
@@ -23,6 +24,16 @@ import org.kde.neochat
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The matrix ID of the reply event.
|
||||
*/
|
||||
required property var replyComponentType
|
||||
|
||||
/**
|
||||
* @brief The matrix ID of the reply event.
|
||||
*/
|
||||
required property var replyEventId
|
||||
|
||||
/**
|
||||
* @brief The reply author.
|
||||
*
|
||||
@@ -39,17 +50,12 @@ RowLayout {
|
||||
*
|
||||
* @sa Quotient::User
|
||||
*/
|
||||
required property var author
|
||||
required property var replyAuthor
|
||||
|
||||
/**
|
||||
* @brief The delegate type of the reply message.
|
||||
* @brief The display text of the message replied to.
|
||||
*/
|
||||
required property int type
|
||||
|
||||
/**
|
||||
* @brief The display text of the message.
|
||||
*/
|
||||
required property string display
|
||||
required property string replyDisplay
|
||||
|
||||
/**
|
||||
* @brief The media info for the reply event.
|
||||
@@ -66,15 +72,19 @@ RowLayout {
|
||||
* - height - The height in pixels of the audio media (image/video only).
|
||||
* - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads (image/video only).
|
||||
*/
|
||||
required property var mediaInfo
|
||||
required property var replyMediaInfo
|
||||
|
||||
property real contentMaxWidth
|
||||
/**
|
||||
* @brief The maximum width that the bubble's content can be.
|
||||
*/
|
||||
property real maxContentWidth: -1
|
||||
|
||||
/**
|
||||
* @brief The reply has been clicked.
|
||||
*/
|
||||
signal replyClicked
|
||||
signal replyClicked(string eventID)
|
||||
|
||||
implicitHeight: contentColumn.implicitHeight
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
Rectangle {
|
||||
@@ -82,12 +92,17 @@ RowLayout {
|
||||
Layout.fillHeight: true
|
||||
|
||||
implicitWidth: Kirigami.Units.smallSpacing
|
||||
color: root.author.color
|
||||
color: root.replyAuthor.color
|
||||
}
|
||||
ColumnLayout {
|
||||
id: contentColumn
|
||||
implicitHeight: headerRow.implicitHeight + (root.replyComponentType != MessageComponentType.Other ? contentRepeater.itemAt(0).implicitHeight + spacing : 0)
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
RowLayout {
|
||||
id: headerRow
|
||||
implicitHeight: Math.max(replyAvatar.implicitHeight, replyName.implicitHeight)
|
||||
Layout.maximumWidth: root.maxContentWidth
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
KirigamiComponents.Avatar {
|
||||
@@ -96,42 +111,87 @@ RowLayout {
|
||||
implicitWidth: Kirigami.Units.iconSizes.small
|
||||
implicitHeight: Kirigami.Units.iconSizes.small
|
||||
|
||||
source: root.author.avatarSource
|
||||
name: root.author.displayName
|
||||
color: root.author.color
|
||||
source: root.replyAuthor.avatarSource
|
||||
name: root.replyAuthor.displayName
|
||||
color: root.replyAuthor.color
|
||||
}
|
||||
QQC2.Label {
|
||||
id: replyName
|
||||
Layout.fillWidth: true
|
||||
|
||||
color: root.author.color
|
||||
text: root.author.displayName
|
||||
color: root.replyAuthor.color
|
||||
text: root.replyAuthor.displayName
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
Loader {
|
||||
id: loader
|
||||
Repeater {
|
||||
id: contentRepeater
|
||||
model: [root.replyComponentType]
|
||||
delegate: DelegateChooser {
|
||||
role: "modelData"
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumHeight: loader.item && (root.type == DelegateType.Image || root.type == DelegateType.Sticker) ? loader.item.height : loader.item.implicitHeight
|
||||
Layout.columnSpan: 2
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.Text
|
||||
delegate: TextComponent {
|
||||
display: root.replyDisplay
|
||||
maxContentWidth: _private.availableContentWidth
|
||||
|
||||
sourceComponent: {
|
||||
switch (root.type) {
|
||||
case DelegateType.Image:
|
||||
case DelegateType.Sticker:
|
||||
return imageComponent;
|
||||
case DelegateType.Message:
|
||||
case DelegateType.Notice:
|
||||
return textComponent;
|
||||
case DelegateType.File:
|
||||
case DelegateType.Video:
|
||||
case DelegateType.Audio:
|
||||
return mimeComponent;
|
||||
case DelegateType.Encrypted:
|
||||
return encryptedComponent;
|
||||
default:
|
||||
return textComponent;
|
||||
HoverHandler {
|
||||
enabled: !hoveredLink
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
TapHandler {
|
||||
enabled: !hoveredLink
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onTapped: root.replyClicked(root.replyEventId)
|
||||
}
|
||||
}
|
||||
}
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.Image
|
||||
delegate: Image {
|
||||
id: image
|
||||
Layout.maximumWidth: mediaSizeHelper.currentSize.width
|
||||
Layout.maximumHeight: mediaSizeHelper.currentSize.height
|
||||
source: root?.replyMediaInfo.source ?? ""
|
||||
|
||||
MediaSizeHelper {
|
||||
id: mediaSizeHelper
|
||||
contentMaxWidth: _private.availableContentWidth
|
||||
mediaWidth: root?.replyMediaInfo.width ?? -1
|
||||
mediaHeight: root?.replyMediaInfo.height ?? -1
|
||||
}
|
||||
}
|
||||
}
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.File
|
||||
delegate: MimeComponent {
|
||||
mimeIconSource: root.replyMediaInfo.mimeIcon
|
||||
label: root.replyDisplay
|
||||
subLabel: root.replyComponentType === DelegateType.File ? Format.formatByteSize(root.replyMediaInfo.size) : Format.formatDuration(root.replyMediaInfo.duration)
|
||||
}
|
||||
}
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.Video
|
||||
delegate: MimeComponent {
|
||||
mimeIconSource: root.replyMediaInfo.mimeIcon
|
||||
label: root.replyDisplay
|
||||
subLabel: root.replyComponentType === DelegateType.File ? Format.formatByteSize(root.replyMediaInfo.size) : Format.formatDuration(root.replyMediaInfo.duration)
|
||||
}
|
||||
}
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.Audio
|
||||
delegate: MimeComponent {
|
||||
mimeIconSource: root.replyMediaInfo.mimeIcon
|
||||
label: root.replyDisplay
|
||||
subLabel: root.replyComponentType === DelegateType.File ? Format.formatByteSize(root.replyMediaInfo.size) : Format.formatDuration(root.replyMediaInfo.duration)
|
||||
}
|
||||
}
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.Encrypted
|
||||
delegate: TextComponent {
|
||||
display: i18n("This message is encrypted and the sender has not shared the key with this device.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,55 +201,11 @@ RowLayout {
|
||||
}
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onTapped: root.replyClicked()
|
||||
onTapped: root.replyClicked(root.replyEventId)
|
||||
}
|
||||
|
||||
Component {
|
||||
id: textComponent
|
||||
RichLabel {
|
||||
textMessage: root.display
|
||||
|
||||
HoverHandler {
|
||||
enabled: !hoveredLink
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
TapHandler {
|
||||
enabled: !hoveredLink
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onTapped: root.replyClicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
Component {
|
||||
id: imageComponent
|
||||
Image {
|
||||
id: image
|
||||
width: mediaSizeHelper.currentSize.width
|
||||
height: mediaSizeHelper.currentSize.height
|
||||
fillMode: Image.PreserveAspectFit
|
||||
source: root?.mediaInfo.source ?? ""
|
||||
|
||||
MediaSizeHelper {
|
||||
id: mediaSizeHelper
|
||||
contentMaxWidth: root.contentMaxWidth - verticalBorder.width - root.spacing
|
||||
mediaWidth: root?.mediaInfo.width ?? -1
|
||||
mediaHeight: root?.mediaInfo.height ?? -1
|
||||
}
|
||||
}
|
||||
}
|
||||
Component {
|
||||
id: mimeComponent
|
||||
MimeComponent {
|
||||
mimeIconSource: root.mediaInfo.mimeIcon
|
||||
label: root.display
|
||||
subLabel: root.type === DelegateType.File ? Format.formatByteSize(root.mediaInfo.size) : Format.formatDuration(root.mediaInfo.duration)
|
||||
}
|
||||
}
|
||||
Component {
|
||||
id: encryptedComponent
|
||||
RichLabel {
|
||||
textMessage: i18n("This message is encrypted and the sender has not shared the key with this device.")
|
||||
textFormat: Text.RichText
|
||||
}
|
||||
QtObject {
|
||||
id: _private
|
||||
// The space available for the component after taking away the border
|
||||
readonly property real availableContentWidth: root.maxContentWidth - verticalBorder.implicitWidth - root.spacing
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,8 +47,8 @@ QQC2.ScrollView {
|
||||
role: "type"
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: 0//MediaMessageFilterModel.Image
|
||||
delegate: ImageDelegate {
|
||||
roleValue: MediaMessageFilterModel.Image
|
||||
delegate: MessageDelegate {
|
||||
alwaysShowAuthor: true
|
||||
alwaysMaxWidth: true
|
||||
cardBackground: false
|
||||
@@ -57,8 +57,8 @@ QQC2.ScrollView {
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: 1//MediaMessageFilterModel.Video
|
||||
delegate: VideoDelegate {
|
||||
roleValue: MediaMessageFilterModel.Video
|
||||
delegate: MessageDelegate {
|
||||
alwaysShowAuthor: true
|
||||
alwaysMaxWidth: true
|
||||
cardBackground: false
|
||||
|
||||
@@ -256,23 +256,23 @@ Kirigami.Page {
|
||||
});
|
||||
}
|
||||
|
||||
function onShowMessageMenu(eventId, author, delegateType, plainText, htmlText, selectedText) {
|
||||
function onShowMessageMenu(eventId, author, messageComponentType, plainText, htmlText, selectedText) {
|
||||
const contextMenu = messageDelegateContextMenu.createObject(root, {
|
||||
selectedText: selectedText,
|
||||
author: author,
|
||||
eventId: eventId,
|
||||
delegateType: delegateType,
|
||||
messageComponentType: messageComponentType,
|
||||
plainText: plainText,
|
||||
htmlText: htmlText
|
||||
});
|
||||
contextMenu.open();
|
||||
}
|
||||
|
||||
function onShowFileMenu(eventId, author, delegateType, plainText, mimeType, progressInfo) {
|
||||
function onShowFileMenu(eventId, author, messageComponentType, plainText, mimeType, progressInfo) {
|
||||
const contextMenu = fileDelegateContextMenu.createObject(root, {
|
||||
author: author,
|
||||
eventId: eventId,
|
||||
delegateType: delegateType,
|
||||
messageComponentType: messageComponentType,
|
||||
plainText: plainText,
|
||||
mimeType: mimeType,
|
||||
progressInfo: progressInfo
|
||||
|
||||
@@ -1,27 +1,29 @@
|
||||
// SPDX-FileCopyrightText: 2020 Black Hat <bhat@encom.eu.org>
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.neochat
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
/**
|
||||
* @brief A component to show the rich display text of text message.
|
||||
* @brief A component to show rich text from a message.
|
||||
*/
|
||||
TextEdit {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The rich text message to display.
|
||||
* @brief The display text of the message.
|
||||
*/
|
||||
property string textMessage
|
||||
required property string display
|
||||
|
||||
/**
|
||||
* @brief Whether this message is replying to another.
|
||||
*/
|
||||
property bool isReply
|
||||
property bool isReply: false
|
||||
|
||||
/**
|
||||
* @brief Regex for detecting a message with a single emoji.
|
||||
@@ -31,7 +33,7 @@ TextEdit {
|
||||
/**
|
||||
* @brief Whether the message is an emoji
|
||||
*/
|
||||
readonly property var isEmoji: isEmojiRegex.test(textMessage)
|
||||
readonly property var isEmoji: isEmojiRegex.test(display)
|
||||
|
||||
/**
|
||||
* @brief Regex for detecting a message with a spoiler.
|
||||
@@ -41,9 +43,23 @@ TextEdit {
|
||||
/**
|
||||
* @brief Whether a spoiler should be revealed.
|
||||
*/
|
||||
property bool spoilerRevealed: !hasSpoiler.test(textMessage)
|
||||
property bool spoilerRevealed: !hasSpoiler.test(display)
|
||||
|
||||
ListView.onReused: Qt.binding(() => !hasSpoiler.test(textMessage))
|
||||
/**
|
||||
* @brief The maximum width that the bubble's content can be.
|
||||
*/
|
||||
property real maxContentWidth: -1
|
||||
|
||||
/**
|
||||
* @brief Request a context menu be show for the message.
|
||||
*/
|
||||
signal showMessageMenu()
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.maximumWidth: root.maxContentWidth
|
||||
|
||||
ListView.onReused: Qt.binding(() => !hasSpoiler.test(display))
|
||||
|
||||
persistentSelection: true
|
||||
|
||||
@@ -91,7 +107,7 @@ a{
|
||||
background: " + Kirigami.Theme.textColor + ";
|
||||
}
|
||||
" : "") + "
|
||||
</style>" + textMessage
|
||||
</style>" + display
|
||||
|
||||
color: Kirigami.Theme.textColor
|
||||
selectedTextColor: Kirigami.Theme.highlightedTextColor
|
||||
@@ -106,8 +122,8 @@ a{
|
||||
textFormat: Text.RichText
|
||||
|
||||
onLinkActivated: link => {
|
||||
spoilerRevealed = true;
|
||||
RoomManager.resolveResource(link, "join");
|
||||
spoilerRevealed = true
|
||||
RoomManager.resolveResource(link, "join")
|
||||
}
|
||||
onHoveredLinkChanged: if (hoveredLink.length > 0 && hoveredLink !== "1") {
|
||||
applicationWindow().hoverLinkIndicator.text = hoveredLink;
|
||||
@@ -116,11 +132,16 @@ a{
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
cursorShape: (parent.hoveredLink || !spoilerRevealed) ? Qt.PointingHandCursor : Qt.IBeamCursor
|
||||
cursorShape: (root.hoveredLink || !spoilerRevealed) ? Qt.PointingHandCursor : Qt.IBeamCursor
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
enabled: !parent.hoveredLink && !spoilerRevealed
|
||||
enabled: !root.hoveredLink && !spoilerRevealed
|
||||
onTapped: spoilerRevealed = true
|
||||
}
|
||||
TapHandler {
|
||||
enabled: !root.hoveredLink
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onLongPressed: root.showMessageMenu()
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
import Qt.labs.qmlmodels
|
||||
|
||||
import org.kde.neochat
|
||||
import org.kde.neochat.config
|
||||
|
||||
/**
|
||||
* @brief A timeline delegate for a text message.
|
||||
*
|
||||
* @inherit MessageDelegate
|
||||
*/
|
||||
MessageDelegate {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The link preview properties.
|
||||
*
|
||||
* This is a list or object containing the following:
|
||||
* - url - The URL being previewed.
|
||||
* - loaded - Whether the URL preview has been loaded.
|
||||
* - title - the title of the URL preview.
|
||||
* - description - the description of the URL preview.
|
||||
* - imageSource - a source URL for the preview image.
|
||||
*
|
||||
* @note An empty link previewer should be passed if there are no links to
|
||||
* preview.
|
||||
*/
|
||||
required property var linkPreview
|
||||
|
||||
/**
|
||||
* @brief Whether there are any links to preview.
|
||||
*/
|
||||
required property bool showLinkPreview
|
||||
|
||||
onOpenContextMenu: RoomManager.viewEventMenu(eventId, author, delegateType, plainText, display, label.selectedText)
|
||||
|
||||
bubbleContent: ColumnLayout {
|
||||
RichLabel {
|
||||
id: label
|
||||
Layout.fillWidth: true
|
||||
visible: root.room.editCache.editId !== root.eventId
|
||||
|
||||
isReply: root.isReply
|
||||
|
||||
textMessage: root.display
|
||||
|
||||
TapHandler {
|
||||
enabled: !label.hoveredLink
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onLongPressed: root.openContextMenu()
|
||||
}
|
||||
}
|
||||
Loader {
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: item ? item.minimumHeight : -1
|
||||
Layout.preferredWidth: item ? item.preferredWidth : -1
|
||||
visible: root.room.editCache.editId === root.eventId
|
||||
active: visible
|
||||
sourceComponent: MessageEditComponent {
|
||||
room: root.room
|
||||
actionsHandler: root.ListView.view.actionsHandler
|
||||
messageId: root.eventId
|
||||
}
|
||||
}
|
||||
LinkPreviewDelegate {
|
||||
Layout.fillWidth: true
|
||||
active: !root.room.usesEncryption && root.room.urlPreviewEnabled && Config.showLinkPreview && root.showLinkPreview && !root.linkPreview.empty
|
||||
linkPreviewer: root.linkPreview
|
||||
indicatorEnabled: root.isVisibleInTimeline()
|
||||
}
|
||||
}
|
||||
}
|
||||
381
src/qml/VideoComponent.qml
Normal file
381
src/qml/VideoComponent.qml
Normal file
@@ -0,0 +1,381 @@
|
||||
// SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
import QtMultimedia
|
||||
import Qt.labs.platform as Platform
|
||||
|
||||
import org.kde.coreaddons
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
/**
|
||||
* @brief A component to show a video from a message.
|
||||
*/
|
||||
Video {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The NeoChatRoom the delegate is being displayed in.
|
||||
*/
|
||||
required property NeoChatRoom room
|
||||
|
||||
/**
|
||||
* @brief The matrix ID of the message event.
|
||||
*/
|
||||
required property string eventId
|
||||
|
||||
/**
|
||||
* @brief The display text of the message.
|
||||
*/
|
||||
required property string display
|
||||
|
||||
/**
|
||||
* @brief The media info for the event.
|
||||
*
|
||||
* This should consist of the following:
|
||||
* - source - The mxc URL for the media.
|
||||
* - mimeType - The MIME type of the media (should be image/xxx for this delegate).
|
||||
* - mimeIcon - The MIME icon name (should be image-xxx).
|
||||
* - size - The file size in bytes.
|
||||
* - width - The width in pixels of the audio media.
|
||||
* - height - The height in pixels of the audio media.
|
||||
* - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads.
|
||||
*/
|
||||
required property var mediaInfo
|
||||
|
||||
/**
|
||||
* @brief FileTransferInfo for any downloading files.
|
||||
*/
|
||||
required property var fileTransferInfo
|
||||
|
||||
/**
|
||||
* @brief Whether the media has been downloaded.
|
||||
*/
|
||||
readonly property bool downloaded: root.fileTransferInfo && root.fileTransferInfo.completed
|
||||
onDownloadedChanged: {
|
||||
if (downloaded) {
|
||||
root.source = root.fileTransferInfo.localPath
|
||||
}
|
||||
|
||||
if (downloaded && playOnFinished) {
|
||||
playSavedFile()
|
||||
playOnFinished = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Whether the video should be played when downloaded.
|
||||
*/
|
||||
property bool playOnFinished: false
|
||||
|
||||
/**
|
||||
* @brief The timeline ListView this component is being used in.
|
||||
*/
|
||||
required property ListView timeline
|
||||
|
||||
/**
|
||||
* @brief The maximum width that the bubble's content can be.
|
||||
*/
|
||||
property real maxContentWidth: -1
|
||||
|
||||
Layout.preferredWidth: mediaSizeHelper.currentSize.width
|
||||
Layout.preferredHeight: mediaSizeHelper.currentSize.height
|
||||
|
||||
fillMode: VideoOutput.PreserveAspectFit
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: "notDownloaded"
|
||||
when: !root.fileTransferInfo.completed && !root.fileTransferInfo.active
|
||||
PropertyChanges {
|
||||
target: noDownloadLabel
|
||||
visible: true
|
||||
}
|
||||
PropertyChanges {
|
||||
target: mediaThumbnail
|
||||
visible: true
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "downloading"
|
||||
when: root.fileTransferInfo.active && !root.fileTransferInfo.completed
|
||||
PropertyChanges {
|
||||
target: downloadBar
|
||||
visible: true
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "paused"
|
||||
when: root.fileTransferInfo.completed && (root.playbackState === MediaPlayer.StoppedState || root.playbackState === MediaPlayer.PausedState)
|
||||
PropertyChanges {
|
||||
target: videoControls
|
||||
stateVisible: true
|
||||
}
|
||||
PropertyChanges {
|
||||
target: playButton
|
||||
icon.name: "media-playback-start"
|
||||
onClicked: root.play()
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "playing"
|
||||
when: root.fileTransferInfo.completed && root.playbackState === MediaPlayer.PlayingState
|
||||
PropertyChanges {
|
||||
target: videoControls
|
||||
stateVisible: true
|
||||
}
|
||||
PropertyChanges {
|
||||
target: playButton
|
||||
icon.name: "media-playback-pause"
|
||||
onClicked: root.pause()
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Image {
|
||||
id: mediaThumbnail
|
||||
anchors.fill: parent
|
||||
visible: false
|
||||
|
||||
source: root.mediaInfo.tempInfo.source
|
||||
fillMode: Image.PreserveAspectFit
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
id: noDownloadLabel
|
||||
anchors.centerIn: parent
|
||||
|
||||
visible: false
|
||||
color: "white"
|
||||
text: i18n("Video")
|
||||
font.pixelSize: 16
|
||||
|
||||
padding: 8
|
||||
|
||||
background: Rectangle {
|
||||
radius: Kirigami.Units.smallSpacing
|
||||
color: "black"
|
||||
opacity: 0.3
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: downloadBar
|
||||
anchors.fill: parent
|
||||
visible: false
|
||||
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
radius: Kirigami.Units.smallSpacing
|
||||
|
||||
QQC2.ProgressBar {
|
||||
anchors.centerIn: parent
|
||||
|
||||
width: parent.width * 0.8
|
||||
|
||||
from: 0
|
||||
to: root.fileTransferInfo.total
|
||||
value: root.fileTransferInfo.progress
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.Control {
|
||||
id: videoControls
|
||||
property bool stateVisible: false
|
||||
|
||||
anchors.bottom: root.bottom
|
||||
anchors.left: root.left
|
||||
anchors.right: root.right
|
||||
visible: stateVisible && (videoHoverHandler.hovered || volumePopupHoverHandler.hovered || volumeSlider.hovered || videoControlTimer.running)
|
||||
|
||||
contentItem: RowLayout {
|
||||
id: controlRow
|
||||
QQC2.ToolButton {
|
||||
id: playButton
|
||||
}
|
||||
QQC2.Slider {
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: root.duration
|
||||
value: root.position
|
||||
onMoved: root.seek(value)
|
||||
}
|
||||
QQC2.Label {
|
||||
text: Format.formatDuration(root.position) + "/" + Format.formatDuration(root.duration)
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
id: volumeButton
|
||||
property var unmuteVolume: root.volume
|
||||
|
||||
icon.name: root.volume <= 0 ? "player-volume-muted" : "player-volume"
|
||||
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
QQC2.ToolTip.timeout: Kirigami.Units.toolTipDelay
|
||||
QQC2.ToolTip.text: i18nc("@action:button", "Volume")
|
||||
|
||||
onClicked: {
|
||||
if (root.volume > 0) {
|
||||
root.volume = 0
|
||||
} else {
|
||||
if (unmuteVolume === 0) {
|
||||
root.volume = 1
|
||||
} else {
|
||||
root.volume = unmuteVolume
|
||||
}
|
||||
}
|
||||
}
|
||||
onHoveredChanged: {
|
||||
if (!hovered && (root.state === "paused" || root.state === "playing")) {
|
||||
videoControlTimer.restart()
|
||||
volumePopupTimer.restart()
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.Popup {
|
||||
id: volumePopup
|
||||
y: -height
|
||||
width: volumeButton.width
|
||||
visible: videoControls.stateVisible && (volumeButton.hovered || volumePopupHoverHandler.hovered || volumeSlider.hovered || volumePopupTimer.running)
|
||||
|
||||
focus: true
|
||||
padding: Kirigami.Units.smallSpacing
|
||||
closePolicy: QQC2.Popup.NoAutoClose
|
||||
|
||||
QQC2.Slider {
|
||||
id: volumeSlider
|
||||
anchors.centerIn: parent
|
||||
implicitHeight: Kirigami.Units.gridUnit * 7
|
||||
orientation: Qt.Vertical
|
||||
padding: 0
|
||||
from: 0
|
||||
to: 1
|
||||
value: root.volume
|
||||
onMoved: {
|
||||
root.volume = value
|
||||
volumeButton.unmuteVolume = value
|
||||
}
|
||||
onHoveredChanged: {
|
||||
if (!hovered && (root.state === "paused" || root.state === "playing")) {
|
||||
rooteoControlTimer.restart()
|
||||
volumePopupTimer.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
Timer {
|
||||
id: volumePopupTimer
|
||||
interval: 500
|
||||
}
|
||||
HoverHandler {
|
||||
id: volumePopupHoverHandler
|
||||
onHoveredChanged: {
|
||||
if (!hovered && (root.state === "paused" || root.state === "playing")) {
|
||||
videoControlTimer.restart()
|
||||
volumePopupTimer.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
background: Kirigami.ShadowedRectangle {
|
||||
radius: 4
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
opacity: 0.8
|
||||
|
||||
property color borderColor: Kirigami.Theme.textColor
|
||||
border.color: Qt.rgba(borderColor.r, borderColor.g, borderColor.b, 0.3)
|
||||
border.width: 1
|
||||
|
||||
shadow.xOffset: 0
|
||||
shadow.yOffset: 4
|
||||
shadow.color: Qt.rgba(0, 0, 0, 0.3)
|
||||
shadow.size: 8
|
||||
}
|
||||
}
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
id: maximizeButton
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
|
||||
action: Kirigami.Action {
|
||||
text: i18n("Maximize")
|
||||
icon.name: "view-fullscreen"
|
||||
onTriggered: {
|
||||
root.timeline.interactive = false
|
||||
root.pause()
|
||||
// We need to make sure the index is that of the MediaMessageFilterModel.
|
||||
if (root.timeline.model instanceof MessageFilterModel) {
|
||||
RoomManager.maximizeMedia(RoomManager.mediaMessageFilterModel.getRowForSourceItem(root.index))
|
||||
} else {
|
||||
RoomManager.maximizeMedia(root.index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
background: Kirigami.ShadowedRectangle {
|
||||
radius: 4
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
opacity: 0.8
|
||||
|
||||
property color borderColor: Kirigami.Theme.textColor
|
||||
border.color: Qt.rgba(borderColor.r, borderColor.g, borderColor.b, 0.3)
|
||||
border.width: 1
|
||||
|
||||
shadow.xOffset: 0
|
||||
shadow.yOffset: 4
|
||||
shadow.color: Qt.rgba(0, 0, 0, 0.3)
|
||||
shadow.size: 8
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: videoControlTimer
|
||||
interval: 1000
|
||||
}
|
||||
HoverHandler {
|
||||
id: videoHoverHandler
|
||||
onHoveredChanged: {
|
||||
if (!hovered && (root.state === "paused" || root.state === "playing")) {
|
||||
videoControlTimer.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
gesturePolicy: TapHandler.ReleaseWithinBounds | TapHandler.WithinBounds
|
||||
onTapped: if (root.fileTransferInfo.completed) {
|
||||
if (root.playbackState == MediaPlayer.PlayingState) {
|
||||
root.pause()
|
||||
} else {
|
||||
root.play()
|
||||
}
|
||||
} else {
|
||||
root.downloadAndPlay()
|
||||
}
|
||||
}
|
||||
|
||||
MediaSizeHelper {
|
||||
id: mediaSizeHelper
|
||||
contentMaxWidth: root.maxContentWidth
|
||||
mediaWidth: root.mediaInfo.width
|
||||
mediaHeight: root.mediaInfo.height
|
||||
}
|
||||
|
||||
function downloadAndPlay() {
|
||||
if (root.downloaded) {
|
||||
playSavedFile()
|
||||
} else {
|
||||
playOnFinished = true
|
||||
root.room.downloadFile(root.eventId, Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/" + root.eventId.replace(":", "_").replace("/", "_").replace("+", "_") + root.room.fileNameToDownload(root.eventId))
|
||||
}
|
||||
}
|
||||
|
||||
function playSavedFile() {
|
||||
root.stop()
|
||||
root.play()
|
||||
}
|
||||
}
|
||||
@@ -1,368 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
import QtMultimedia
|
||||
import Qt.labs.platform as Platform
|
||||
|
||||
import org.kde.coreaddons
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
/**
|
||||
* @brief A timeline delegate for a video message.
|
||||
*
|
||||
* @inherit MessageDelegate
|
||||
*/
|
||||
MessageDelegate {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The media info for the event.
|
||||
*
|
||||
* This should consist of the following:
|
||||
* - source - The mxc URL for the media.
|
||||
* - mimeType - The MIME type of the media (should be video/xxx for this delegate).
|
||||
* - mimeIcon - The MIME icon name (should be video-xxx).
|
||||
* - size - The file size in bytes.
|
||||
* - duration - The length in seconds of the audio media.
|
||||
* - width - The width in pixels of the audio media.
|
||||
* - height - The height in pixels of the audio media.
|
||||
* - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads.
|
||||
*/
|
||||
required property var mediaInfo
|
||||
|
||||
/**
|
||||
* @brief Whether the media has been downloaded.
|
||||
*/
|
||||
readonly property bool downloaded: root.progressInfo && root.progressInfo.completed
|
||||
|
||||
/**
|
||||
* @brief Whether the video should be played when downloaded.
|
||||
*/
|
||||
property bool playOnFinished: false
|
||||
|
||||
/**
|
||||
* @brief The maximum width of the image.
|
||||
*/
|
||||
readonly property var maxWidth: Kirigami.Units.gridUnit * 30
|
||||
|
||||
/**
|
||||
* @brief The maximum height of the image.
|
||||
*/
|
||||
readonly property var maxHeight: Kirigami.Units.gridUnit * 30
|
||||
|
||||
onOpenContextMenu: RoomManager.viewEventMenu(eventId, author, delegateType, plainText, "", "", mediaInfo.mimeType, progressInfo)
|
||||
|
||||
onDownloadedChanged: {
|
||||
if (downloaded) {
|
||||
vid.source = root.progressInfo.localPath;
|
||||
}
|
||||
if (downloaded && playOnFinished) {
|
||||
playSavedFile();
|
||||
playOnFinished = false;
|
||||
}
|
||||
}
|
||||
|
||||
bubbleContent: Video {
|
||||
id: vid
|
||||
implicitWidth: mediaSizeHelper.currentSize.width
|
||||
implicitHeight: mediaSizeHelper.currentSize.height
|
||||
|
||||
fillMode: VideoOutput.PreserveAspectFit
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: "notDownloaded"
|
||||
when: !root.progressInfo.completed && !root.progressInfo.active
|
||||
PropertyChanges {
|
||||
target: noDownloadLabel
|
||||
visible: true
|
||||
}
|
||||
PropertyChanges {
|
||||
target: mediaThumbnail
|
||||
visible: true
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "downloading"
|
||||
when: root.progressInfo.active && !root.progressInfo.completed
|
||||
PropertyChanges {
|
||||
target: downloadBar
|
||||
visible: true
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "paused"
|
||||
when: root.progressInfo.completed && (vid.playbackState === MediaPlayer.StoppedState || vid.playbackState === MediaPlayer.PausedState)
|
||||
PropertyChanges {
|
||||
target: videoControls
|
||||
stateVisible: true
|
||||
}
|
||||
PropertyChanges {
|
||||
target: playButton
|
||||
icon.name: "media-playback-start"
|
||||
onClicked: vid.play()
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "playing"
|
||||
when: root.progressInfo.completed && vid.playbackState === MediaPlayer.PlayingState
|
||||
PropertyChanges {
|
||||
target: videoControls
|
||||
stateVisible: true
|
||||
}
|
||||
PropertyChanges {
|
||||
target: playButton
|
||||
icon.name: "media-playback-pause"
|
||||
onClicked: vid.pause()
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Image {
|
||||
id: mediaThumbnail
|
||||
anchors.fill: parent
|
||||
visible: false
|
||||
|
||||
source: root.mediaInfo.tempInfo.source
|
||||
fillMode: Image.PreserveAspectFit
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
id: noDownloadLabel
|
||||
anchors.centerIn: parent
|
||||
|
||||
visible: false
|
||||
color: "white"
|
||||
text: i18n("Video")
|
||||
font.pixelSize: 16
|
||||
|
||||
padding: 8
|
||||
|
||||
background: Rectangle {
|
||||
radius: Kirigami.Units.smallSpacing
|
||||
color: "black"
|
||||
opacity: 0.3
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: downloadBar
|
||||
anchors.fill: parent
|
||||
visible: false
|
||||
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
radius: Kirigami.Units.smallSpacing
|
||||
|
||||
QQC2.ProgressBar {
|
||||
anchors.centerIn: parent
|
||||
|
||||
width: parent.width * 0.8
|
||||
|
||||
from: 0
|
||||
to: root.progressInfo.total
|
||||
value: root.progressInfo.progress
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.Control {
|
||||
id: videoControls
|
||||
property bool stateVisible: false
|
||||
|
||||
anchors.bottom: vid.bottom
|
||||
anchors.left: vid.left
|
||||
anchors.right: vid.right
|
||||
visible: stateVisible && (videoHoverHandler.hovered || volumePopupHoverHandler.hovered || volumeSlider.hovered || videoControlTimer.running)
|
||||
|
||||
contentItem: RowLayout {
|
||||
id: controlRow
|
||||
QQC2.ToolButton {
|
||||
id: playButton
|
||||
}
|
||||
QQC2.Slider {
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: vid.duration
|
||||
value: vid.position
|
||||
onMoved: vid.seek(value)
|
||||
}
|
||||
QQC2.Label {
|
||||
text: Format.formatDuration(vid.position) + "/" + Format.formatDuration(vid.duration)
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
id: volumeButton
|
||||
property var unmuteVolume: vid.volume
|
||||
|
||||
icon.name: vid.volume <= 0 ? "player-volume-muted" : "player-volume"
|
||||
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
QQC2.ToolTip.timeout: Kirigami.Units.toolTipDelay
|
||||
QQC2.ToolTip.text: i18nc("@action:button", "Volume")
|
||||
|
||||
onClicked: {
|
||||
if (vid.volume > 0) {
|
||||
vid.volume = 0;
|
||||
} else {
|
||||
if (unmuteVolume === 0) {
|
||||
vid.volume = 1;
|
||||
} else {
|
||||
vid.volume = unmuteVolume;
|
||||
}
|
||||
}
|
||||
}
|
||||
onHoveredChanged: {
|
||||
if (!hovered && (vid.state === "paused" || vid.state === "playing")) {
|
||||
videoControlTimer.restart();
|
||||
volumePopupTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.Popup {
|
||||
id: volumePopup
|
||||
y: -height
|
||||
width: volumeButton.width
|
||||
visible: videoControls.stateVisible && (volumeButton.hovered || volumePopupHoverHandler.hovered || volumeSlider.hovered || volumePopupTimer.running)
|
||||
|
||||
focus: true
|
||||
padding: Kirigami.Units.smallSpacing
|
||||
closePolicy: QQC2.Popup.NoAutoClose
|
||||
|
||||
QQC2.Slider {
|
||||
id: volumeSlider
|
||||
anchors.centerIn: parent
|
||||
implicitHeight: Kirigami.Units.gridUnit * 7
|
||||
orientation: Qt.Vertical
|
||||
padding: 0
|
||||
from: 0
|
||||
to: 1
|
||||
value: vid.volume
|
||||
onMoved: {
|
||||
vid.volume = value;
|
||||
volumeButton.unmuteVolume = value;
|
||||
}
|
||||
onHoveredChanged: {
|
||||
if (!hovered && (vid.state === "paused" || vid.state === "playing")) {
|
||||
videoControlTimer.restart();
|
||||
volumePopupTimer.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
Timer {
|
||||
id: volumePopupTimer
|
||||
interval: 500
|
||||
}
|
||||
HoverHandler {
|
||||
id: volumePopupHoverHandler
|
||||
onHoveredChanged: {
|
||||
if (!hovered && (vid.state === "paused" || vid.state === "playing")) {
|
||||
videoControlTimer.restart();
|
||||
volumePopupTimer.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
background: Kirigami.ShadowedRectangle {
|
||||
radius: 4
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
opacity: 0.8
|
||||
|
||||
property color borderColor: Kirigami.Theme.textColor
|
||||
border.color: Qt.rgba(borderColor.r, borderColor.g, borderColor.b, 0.3)
|
||||
border.width: 1
|
||||
|
||||
shadow.xOffset: 0
|
||||
shadow.yOffset: 4
|
||||
shadow.color: Qt.rgba(0, 0, 0, 0.3)
|
||||
shadow.size: 8
|
||||
}
|
||||
}
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
id: maximizeButton
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
|
||||
action: Kirigami.Action {
|
||||
text: i18n("Maximize")
|
||||
icon.name: "view-fullscreen"
|
||||
onTriggered: {
|
||||
root.ListView.view.interactive = false;
|
||||
vid.pause();
|
||||
// We need to make sure the index is that of the MediaMessageFilterModel.
|
||||
if (root.ListView.view.model instanceof MessageFilterModel) {
|
||||
RoomManager.maximizeMedia(RoomManager.mediaMessageFilterModel.getRowForSourceItem(root.index));
|
||||
} else {
|
||||
RoomManager.maximizeMedia(root.index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
background: Kirigami.ShadowedRectangle {
|
||||
radius: 4
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
opacity: 0.8
|
||||
|
||||
property color borderColor: Kirigami.Theme.textColor
|
||||
border.color: Qt.rgba(borderColor.r, borderColor.g, borderColor.b, 0.3)
|
||||
border.width: 1
|
||||
|
||||
shadow.xOffset: 0
|
||||
shadow.yOffset: 4
|
||||
shadow.color: Qt.rgba(0, 0, 0, 0.3)
|
||||
shadow.size: 8
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: videoControlTimer
|
||||
interval: 1000
|
||||
}
|
||||
HoverHandler {
|
||||
id: videoHoverHandler
|
||||
onHoveredChanged: {
|
||||
if (!hovered && (vid.state === "paused" || vid.state === "playing")) {
|
||||
videoControlTimer.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
gesturePolicy: TapHandler.ReleaseWithinBounds | TapHandler.WithinBounds
|
||||
onTapped: if (root.progressInfo.completed) {
|
||||
if (vid.playbackState == MediaPlayer.PlayingState) {
|
||||
vid.pause();
|
||||
} else {
|
||||
vid.play();
|
||||
}
|
||||
} else {
|
||||
root.downloadAndPlay();
|
||||
}
|
||||
}
|
||||
|
||||
MediaSizeHelper {
|
||||
id: mediaSizeHelper
|
||||
contentMaxWidth: root.contentMaxWidth
|
||||
mediaWidth: root.mediaInfo.width
|
||||
mediaHeight: root.mediaInfo.height
|
||||
}
|
||||
}
|
||||
|
||||
function downloadAndPlay() {
|
||||
if (vid.downloaded) {
|
||||
playSavedFile();
|
||||
} else {
|
||||
playOnFinished = true;
|
||||
root.room.downloadFile(root.eventId, Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/" + root.eventId.replace(":", "_").replace("/", "_").replace("+", "_") + root.room.fileNameToDownload(root.eventId));
|
||||
}
|
||||
}
|
||||
|
||||
function playSavedFile() {
|
||||
vid.stop();
|
||||
vid.play();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user