Timeline required properties

Move to using required properties for timeline delegates.
This commit is contained in:
James Graham
2023-05-27 14:57:34 +00:00
committed by Tobias Fella
parent a94f46f904
commit 8ad23e7a40
12 changed files with 766 additions and 227 deletions

View File

@@ -14,16 +14,34 @@ import org.kde.neochat 1.0
Components.AlbumMaximizeComponent {
id: root
property var modelData
required property string eventId
required property var time
required property var author
required property int delegateType
required property string plainText
required property string caption
required property var mediaInfo
required property var progressInfo
required property var mimeType
required property var source
property list<Components.AlbumModelItem> items: [
Components.AlbumModelItem {
type: root.modelData.delegateType === MessageEventModel.Image || root.modelData.delegateType === MessageEventModel.Sticker ? Components.AlbumModelItem.Image : Components.AlbumModelItem.Video
source: root.modelData.delegateType === MessageEventModel.Video ? modelData.progressInfo.localPath : modelData.mediaInfo.source
tempSource: modelData.mediaInfo.tempInfo.source
caption: modelData.display
sourceWidth: modelData.mediaInfo.width
sourceHeight: modelData.mediaInfo.height
type: root.delegateType === MessageEventModel.Image || root.delegateType === MessageEventModel.Sticker ? Components.AlbumModelItem.Image : Components.AlbumModelItem.Video
source: root.delegateType === MessageEventModel.Video ? root.progressInfo.localPath : root.mediaInfo.source
tempSource: root.mediaInfo.tempInfo.source
caption: root.caption
sourceWidth: root.mediaInfo.width
sourceHeight: root.mediaInfo.height
}
]
@@ -36,22 +54,22 @@ Components.AlbumMaximizeComponent {
implicitWidth: Kirigami.Units.iconSizes.medium
implicitHeight: Kirigami.Units.iconSizes.medium
name: modelData.author.name ?? modelData.author.displayName
source: modelData.author.avatarSource
color: modelData.author.color
name: root.author.name ?? root.author.displayName
source: root.author.avatarSource
color: root.author.color
}
ColumnLayout {
spacing: 0
QQC2.Label {
id: userLabel
text: modelData.author.name ?? modelData.author.displayName
color: modelData.author.color
text: root.author.name ?? root.author.displayName
color: root.author.color
font.weight: Font.Bold
elide: Text.ElideRight
}
QQC2.Label {
id: dateTimeLabel
text: modelData.time.toLocaleString(Qt.locale(), Locale.ShortFormat)
text: root.time.toLocaleString(Qt.locale(), Locale.ShortFormat)
color: Kirigami.Theme.disabledTextColor
elide: Text.ElideRight
}
@@ -59,14 +77,14 @@ Components.AlbumMaximizeComponent {
}
onItemRightClicked: {
const contextMenu = fileDelegateContextMenu.createObject(parent, {
author: modelData.author,
message: modelData.plainText,
eventId: modelData.eventId,
source: modelData.source,
author: root.author,
message: root.plainText,
eventId: root.eventId,
source: root.source,
file: parent,
mimeType: modelData.mimeType,
progressInfo: modelData.progressInfo,
plainMessage: modelData.plainText,
mimeType: root.mimeType,
progressInfo: root.progressInfo,
plainMessage: root.plainText,
});
contextMenu.closeFullscreen.connect(root.close)
contextMenu.open();
@@ -74,7 +92,7 @@ Components.AlbumMaximizeComponent {
onSaveItem: {
var dialog = saveAsDialog.createObject(QQC2.ApplicationWindow.overlay)
dialog.open()
dialog.currentFile = dialog.folder + "/" + currentRoom.fileNameToDownload(modelData.eventId)
dialog.currentFile = dialog.folder + "/" + currentRoom.fileNameToDownload(root.eventId)
}
Component {
@@ -88,7 +106,7 @@ Components.AlbumMaximizeComponent {
if (!currentFile) {
return;
}
currentRoom.downloadFile(eventId, currentFile)
currentRoom.downloadFile(rooteventId, currentFile)
}
}
}

View File

@@ -10,38 +10,58 @@ import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
/**
* @brief A timeline delegate for an audio message.
*
* @inherit TimelineContainer
*/
TimelineContainer {
id: audioDelegate
id: root
onOpenContextMenu: openFileContext(model, audioDelegate)
/**
* @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
readonly property bool downloaded: model.progressInfo && model.progressInfo.completed
/**
* @brief Whether the media has been downloaded.
*/
readonly property bool downloaded: root.progressInfo && root.progressInfo.completed
onDownloadedChanged: audio.play()
onOpenContextMenu: openFileContext(root)
innerObject: ColumnLayout {
Layout.fillWidth: true
Layout.maximumWidth: audioDelegate.contentMaxWidth
Layout.maximumWidth: root.contentMaxWidth
Audio {
id: audio
source: model.progressInfo.localPath
source: root.progressInfo.localPath
autoLoad: false
}
states: [
State {
name: "notDownloaded"
when: !model.progressInfo.completed && !model.progressInfo.active
when: !root.progressInfo.completed && !root.progressInfo.active
PropertyChanges {
target: playButton
icon.name: "media-playback-start"
onClicked: currentRoom.downloadFile(model.eventId)
onClicked: currentRoom.downloadFile(root.eventId)
}
},
State {
name: "downloading"
when: model.progressInfo.active && !model.progressInfo.completed
when: root.progressInfo.active && !root.progressInfo.completed
PropertyChanges {
target: downloadBar
visible: true
@@ -50,13 +70,13 @@ TimelineContainer {
target: playButton
icon.name: "media-playback-stop"
onClicked: {
currentRoom.cancelFileTransfer(model.eventId)
currentRoom.cancelFileTransfer(root.eventId)
}
}
},
State {
name: "paused"
when: model.progressInfo.completed && (audio.playbackState === Audio.StoppedState || audio.playbackState === Audio.PausedState)
when: root.progressInfo.completed && (audio.playbackState === Audio.StoppedState || audio.playbackState === Audio.PausedState)
PropertyChanges {
target: playButton
icon.name: "media-playback-start"
@@ -67,7 +87,7 @@ TimelineContainer {
},
State {
name: "playing"
when: model.progressInfo.completed && audio.playbackState === Audio.PlayingState
when: root.progressInfo.completed && audio.playbackState === Audio.PlayingState
PropertyChanges {
target: playButton
@@ -84,7 +104,7 @@ TimelineContainer {
id: playButton
}
QQC2.Label {
text: model.display
text: root.display
wrapMode: Text.Wrap
Layout.fillWidth: true
}
@@ -94,8 +114,8 @@ TimelineContainer {
visible: false
Layout.fillWidth: true
from: 0
to: model.mediaInfo.size
value: model.progressInfo.progress
to: root.mediaInfo.size
value: root.progressInfo.progress
}
RowLayout {
visible: audio.hasAudio
@@ -109,7 +129,7 @@ TimelineContainer {
}
QQC2.Label {
visible: audioDelegate.contentMaxWidth > Kirigami.Units.gridUnit * 12
visible: root.contentMaxWidth > Kirigami.Units.gridUnit * 12
text: Controller.formatDuration(audio.position) + "/" + Controller.formatDuration(audio.duration)
}
@@ -117,7 +137,7 @@ TimelineContainer {
QQC2.Label {
Layout.alignment: Qt.AlignRight
Layout.rightMargin: Kirigami.Units.smallSpacing
visible: audio.hasAudio && audioDelegate.contentMaxWidth < Kirigami.Units.gridUnit * 12
visible: audio.hasAudio && root.contentMaxWidth < Kirigami.Units.gridUnit * 12
text: Controller.formatDuration(audio.position) + "/" + Controller.formatDuration(audio.duration)
}

View File

@@ -8,6 +8,11 @@ import QtQuick.Layouts 1.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
/**
* @brief A timeline delegate for an encrypted message that can't be decrypted.
*
* @inherit TimelineContainer
*/
TimelineContainer {
id: encryptedDelegate

View File

@@ -10,37 +10,60 @@ import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
/**
* @brief A timeline delegate for an file message.
*
* @inherit TimelineContainer
*/
TimelineContainer {
id: fileDelegate
id: root
onOpenContextMenu: openFileContext(model, fileDelegate)
/**
* @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
readonly property bool downloaded: progressInfo && progressInfo.completed
/**
* @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: if (autoOpenFile) {
openSavedFile();
}
onOpenContextMenu: openFileContext(root)
function saveFileAs() {
const dialog = fileDialog.createObject(QQC2.ApplicationWindow.overlay)
dialog.open()
dialog.currentFile = dialog.folder + "/" + currentRoom.fileNameToDownload(eventId)
dialog.currentFile = dialog.folder + "/" + currentRoom.fileNameToDownload(root.eventId)
}
function openSavedFile() {
UrlHelper.openUrl(progressInfo.localPath);
UrlHelper.openUrl(root.progressInfo.localPath);
}
innerObject: RowLayout {
Layout.maximumWidth: Math.min(fileDelegate.contentMaxWidth, implicitWidth)
Layout.maximumWidth: Math.min(root.contentMaxWidth, implicitWidth)
spacing: Kirigami.Units.largeSpacing
states: [
State {
name: "downloadedInstant"
when: progressInfo.completed && autoOpenFile
when: root.progressInfo.completed && autoOpenFile
PropertyChanges {
target: openButton
@@ -57,7 +80,7 @@ TimelineContainer {
},
State {
name: "downloaded"
when: progressInfo.completed && !autoOpenFile
when: root.progressInfo.completed && !autoOpenFile
PropertyChanges {
target: openButton
@@ -73,7 +96,7 @@ TimelineContainer {
},
State {
name: "downloading"
when: progressInfo.active
when: root.progressInfo.active
PropertyChanges {
target: openButton
@@ -82,13 +105,13 @@ TimelineContainer {
PropertyChanges {
target: sizeLabel
text: i18nc("file download progress", "%1 / %2", Controller.formatByteSize(progressInfo.progress), Controller.formatByteSize(progressInfo.total))
text: i18nc("file download progress", "%1 / %2", Controller.formatByteSize(root.progressInfo.progress), Controller.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: currentRoom.cancelFileTransfer(eventId)
onClicked: currentRoom.cancelFileTransfer(root.eventId)
}
},
State {
@@ -97,13 +120,13 @@ TimelineContainer {
PropertyChanges {
target: downloadButton
onClicked: fileDelegate.saveFileAs()
onClicked: root.saveFileAs()
}
}
]
Kirigami.Icon {
source: model.mediaInfo.mimeIcon
source: root.mediaInfo.mimeIcon
fallback: "unknown"
}
@@ -111,14 +134,14 @@ TimelineContainer {
spacing: 0
QQC2.Label {
Layout.fillWidth: true
text: model.display
text: root.display
wrapMode: Text.Wrap
elide: Text.ElideRight
}
QQC2.Label {
id: sizeLabel
Layout.fillWidth: true
text: Controller.formatByteSize(model.mediaInfo.size)
text: Controller.formatByteSize(root.mediaInfo.size)
opacity: 0.7
elide: Text.ElideRight
maximumLineCount: 1
@@ -130,7 +153,7 @@ TimelineContainer {
icon.name: "document-open"
onClicked: {
autoOpenFile = true;
currentRoom.downloadTempFile(eventId);
currentRoom.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")
@@ -157,9 +180,9 @@ TimelineContainer {
Config.lastSaveDirectory = folder
Config.save()
if (autoOpenFile) {
UrlHelper.copyTo(progressInfo.localPath, file)
UrlHelper.copyTo(root.progressInfo.localPath, file)
} else {
currentRoom.download(eventId, file);
currentRoom.download(root.eventId, file);
}
}
}

View File

@@ -11,37 +11,70 @@ import org.kde.kirigamiaddons.labs.components 1.0 as Components
import org.kde.neochat 1.0
/**
* @brief A timeline delegate for an image message.
*
* @inherit TimelineContainer
*/
TimelineContainer {
id: imageDelegate
id: root
onOpenContextMenu: openFileContext(model, imageDelegate)
/**
* @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
readonly property bool downloaded: progressInfo && progressInfo.completed
/**
* @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: openFileContext(root)
innerObject: AnimatedImage {
id: img
property var imageWidth: {
if (model.mediaInfo.width > 0) {
return model.mediaInfo.width;
if (root.mediaInfo.width > 0) {
return root.mediaInfo.width;
} else if (sourceSize.width && sourceSize.width > 0) {
return sourceSize.width;
} else {
return imageDelegate.contentMaxWidth;
return root.contentMaxWidth;
}
}
property var imageHeight: {
if (model.mediaInfo.height > 0) {
return model.mediaInfo.height;
if (root.mediaInfo.height > 0) {
return root.mediaInfo.height;
} else if (sourceSize.height && sourceSize.height > 0) {
return sourceSize.height;
} else {
// Default to a 16:9 placeholder
return imageDelegate.contentMaxWidth / 16 * 9;
return root.contentMaxWidth / 16 * 9;
}
}
@@ -56,11 +89,11 @@ TimelineContainer {
readonly property size maxSize: {
if (limitWidth) {
let width = Math.min(imageDelegate.contentMaxWidth, imageDelegate.maxWidth);
let width = Math.min(root.contentMaxWidth, root.maxWidth);
let height = width / aspectRatio;
return Qt.size(width, height);
} else {
let height = Math.min(imageDelegate.maxHeight, imageDelegate.contentMaxWidth / aspectRatio);
let height = Math.min(root.maxHeight, root.contentMaxWidth / aspectRatio);
let width = height * aspectRatio;
return Qt.size(width, height);
}
@@ -70,17 +103,17 @@ TimelineContainer {
Layout.maximumHeight: maxSize.height
Layout.preferredWidth: imageWidth
Layout.preferredHeight: imageHeight
source: model.mediaInfo.source
source: root.mediaInfo.source
Image {
anchors.fill: parent
source: model.mediaInfo.tempInfo.source
source: root.mediaInfo.tempInfo.source
visible: parent.status !== Image.Ready
}
fillMode: Image.PreserveAspectFit
QQC2.ToolTip.text: model.display
QQC2.ToolTip.text: root.display
QQC2.ToolTip.visible: hoverHandler.hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
@@ -93,7 +126,7 @@ TimelineContainer {
Rectangle {
anchors.fill: parent
visible: progressInfo.active && !downloaded
visible: root.progressInfo.active && !downloaded
color: "#BB000000"
@@ -103,8 +136,8 @@ TimelineContainer {
width: parent.width * 0.8
from: 0
to: progressInfo.total
value: progressInfo.progress
to: root.progressInfo.total
value: root.progressInfo.progress
}
}
@@ -113,12 +146,21 @@ TimelineContainer {
onTapped: {
img.QQC2.ToolTip.hide()
img.paused = true
imageDelegate.ListView.view.interactive = false
root.ListView.view.interactive = false
var popup = maximizeImageComponent.createObject(QQC2.ApplicationWindow.overlay, {
modelData: model,
eventId: root.eventId,
time: root.time,
author: root.author,
delegateType: root.delegateType,
plainText: root.plainText,
caption: root.display,
mediaInfo: root.mediaInfo,
progressInfo: root.progressInfo,
mimeType: root.mimeType,
source: root.source
})
popup.closed.connect(() => {
imageDelegate.ListView.view.interactive = true
root.ListView.view.interactive = true
img.paused = false
popup.destroy()
})
@@ -136,13 +178,13 @@ TimelineContainer {
openSavedFile()
} else {
openOnFinished = true
currentRoom.downloadFile(eventId, StandardPaths.writableLocation(StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId))
currentRoom.downloadFile(root.eventId, StandardPaths.writableLocation(StandardPaths.CacheLocation) + "/" + root.eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(root.eventId))
}
}
function openSavedFile() {
if (UrlHelper.openUrl(progressInfo.localPath)) return;
if (UrlHelper.openUrl(progressInfo.localDir)) return;
if (UrlHelper.openUrl(root.progressInfo.localPath)) return;
if (UrlHelper.openUrl(root.progressInfo.localDir)) return;
}
}
}

View File

@@ -11,18 +11,41 @@ import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
/**
* @brief A timeline delegate for a location message.
*
* @inherit TimelineContainer
*/
TimelineContainer {
id: locationDelegate
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
ColumnLayout {
Layout.maximumWidth: locationDelegate.contentMaxWidth
Layout.preferredWidth: locationDelegate.contentMaxWidth
Layout.maximumWidth: root.contentMaxWidth
Layout.preferredWidth: root.contentMaxWidth
Map {
id: map
Layout.fillWidth: true
Layout.preferredHeight: locationDelegate.contentMaxWidth / 16 * 9
Layout.preferredHeight: root.contentMaxWidth / 16 * 9
center: QtPositioning.coordinate(model.latitude, model.longitude)
center: QtPositioning.coordinate(root.latitude, root.longitude)
zoomLevel: 15
plugin: Plugin {
name: "osm"
@@ -44,7 +67,7 @@ TimelineContainer {
anchorPoint.x: sourceItem.width / 2
anchorPoint.y: sourceItem.height
coordinate: QtPositioning.coordinate(model.latitude, model.longitude)
coordinate: QtPositioning.coordinate(rot.latitude, root.longitude)
autoFadeIn: false
sourceItem: Kirigami.Icon {
@@ -67,23 +90,23 @@ TimelineContainer {
Kirigami.Avatar {
anchors.centerIn: parent
anchors.verticalCenterOffset: -parent.height / 8
visible: model.asset === "m.self"
visible: root.asset === "m.self"
width: height
height: parent.height / 3 + 1
name: model.author.name ?? model.author.displayName
source: model.author.avatarSource
color: model.author.color
name: root.author.name ?? root.author.displayName
source: root.author.avatarSource
color: root.author.color
}
}
}
TapHandler {
acceptedButtons: Qt.LeftButton
onLongPressed: openMessageContext(model, "", model.plainText)
onLongPressed: openMessageContext("")
}
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: openMessageContext(model, "", model.plainText)
onTapped: openMessageContext("")
}
}
}

View File

@@ -10,34 +10,63 @@ import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
/**
* @brief A timeline delegate for an text message.
*
* @inherit TimelineContainer
*/
TimelineContainer {
id: messageDelegate
id: root
onOpenContextMenu: openMessageContext(model, label.selectedText, model.plainText)
/**
* @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: openMessageContext(label.selectedText)
innerObject: ColumnLayout {
Layout.maximumWidth: messageDelegate.contentMaxWidth
Layout.maximumWidth: root.contentMaxWidth
RichLabel {
id: label
Layout.fillWidth: true
visible: currentRoom.chatBoxEditId !== model.eventId
visible: currentRoom.chatBoxEditId !== root.eventId
isReply: root.isReply
textMessage: root.display
}
Loader {
Layout.fillWidth: true
Layout.minimumHeight: item ? item.minimumHeight : -1
Layout.preferredWidth: item ? item.preferredWidth : -1
visible: currentRoom.chatBoxEditId === model.eventId
visible: currentRoom.chatBoxEditId === root.eventId
active: visible
sourceComponent: MessageEditComponent {
room: currentRoom
messageId: model.eventId
messageId: root.eventId
}
}
LinkPreviewDelegate {
Layout.fillWidth: true
active: !currentRoom.usesEncryption && currentRoom.urlPreviewEnabled && Config.showLinkPreview && model.showLinkPreview
linkPreviewer: model.linkPreview
indicatorEnabled: messageDelegate.isVisibleInTimeline()
active: !currentRoom.usesEncryption && currentRoom.urlPreviewEnabled && Config.showLinkPreview && root.showLinkPreview
linkPreviewer: root.linkPreview
indicatorEnabled: root.isVisibleInTimeline()
}
}
}

View File

@@ -10,25 +10,40 @@ import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
/**
* @brief A timeline delegate for a poll message.
*
* @inherit TimelineContainer
*/
TimelineContainer {
id: pollDelegate
id: root
readonly property var data: model
property var pollHandler: currentRoom.poll(model.eventId)
/**
* @brief The matrix message content.
*/
required property var content
/**
* @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.
*/
property var pollHandler: currentRoom.poll(root.eventId)
innerObject: ColumnLayout {
Label {
id: questionLabel
text: pollDelegate.data.content["org.matrix.msc3381.poll.start"]["question"]["body"]
text: root.content["org.matrix.msc3381.poll.start"]["question"]["body"]
}
Repeater {
model: pollDelegate.data.content["org.matrix.msc3381.poll.start"]["answers"]
model: root.content["org.matrix.msc3381.poll.start"]["answers"]
delegate: RowLayout {
width: pollDelegate.innerObject.width
width: root.innerObject.width
CheckBox {
checked: pollDelegate.pollHandler.answers[currentRoom.localUser.id] ? pollDelegate.pollHandler.answers[currentRoom.localUser.id].includes(modelData["id"]) : false
onClicked: pollDelegate.pollHandler.sendPollAnswer(pollDelegate.data.eventId, modelData["id"])
enabled: !pollDelegate.pollHandler.hasEnded
checked: root.pollHandler.answers[currentRoom.localUser.id] ? root.pollHandler.answers[currentRoom.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"]
@@ -37,15 +52,15 @@ TimelineContainer {
Layout.fillWidth: true
}
Label {
visible: pollDelegate.data.content["org.matrix.msc3381.poll.start"]["kind"] == "org.matrix.msc3381.poll.disclosed" || pollHandler.hasEnded
visible: root.content["org.matrix.msc3381.poll.start"]["kind"] == "org.matrix.msc3381.poll.disclosed" || pollHandler.hasEnded
Layout.preferredWidth: contentWidth
text: pollDelegate.pollHandler.counts[modelData["id"]] ?? "0"
text: root.pollHandler.counts[modelData["id"]] ?? "0"
}
}
}
Label {
visible: pollDelegate.data.content["org.matrix.msc3381.poll.start"]["kind"] == "org.matrix.msc3381.poll.disclosed" || pollDelegate.pollHandler.hasEnded
text: i18np("Based on votes by %1 user", "Based on votes by %1 users", pollDelegate.pollHandler.answerCount) + (pollDelegate.pollHandler.hasEnded ? (" " + i18nc("as in 'this vote has ended'", "(Ended)")) : "")
visible: root.content["org.matrix.msc3381.poll.start"]["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
}
}

View File

@@ -10,16 +10,67 @@ import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
/**
* @brief A component to show a message that has been replied to.
*
* Similar to the main timeline delegate a reply delegate is chosen based on the type
* of message being replied to. The main difference is that not all messages can be
* show in their original form and are instead visualised with a MIME type delegate
* e.g. Videos.
*/
Item {
id: replyComponent
id: root
/**
* @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 NeoChatUser object for the reply author.
*
* @sa NeoChatUser
*/
required property var author
/**
* @brief The delegate type of the reply message.
*/
required property int type
/**
* @brief The display text of the message.
*/
required property string display
/**
* @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 mediaInfo
/**
* @brief The reply has been clicked.
*/
signal replyClicked()
property var name
property alias avatar: replyAvatar.source
property var color
property var mediaInfo
implicitWidth: mainLayout.implicitWidth
implicitHeight: mainLayout.implicitHeight
@@ -40,7 +91,7 @@ Item {
Layout.rowSpan: 2
implicitWidth: Kirigami.Units.smallSpacing
color: replyComponent.color
color: root.author.color
}
Kirigami.Avatar {
id: replyAvatar
@@ -48,25 +99,26 @@ Item {
implicitWidth: Kirigami.Units.iconSizes.small
implicitHeight: Kirigami.Units.iconSizes.small
name: replyComponent.name || ""
color: replyComponent.color
source: root.author.avatarSource
name: root.author.displayName || ""
color: root.author.color
}
QQC2.Label {
Layout.fillWidth: true
color: replyComponent.color
text: replyComponent.name
color: root.author.color
text: root.author.displayName
elide: Text.ElideRight
}
Loader {
id: loader
Layout.fillWidth: true
Layout.maximumHeight: loader.item && (model.reply.type == MessageEventModel.Image || model.reply.type == MessageEventModel.Sticker) ? loader.item.height : -1
Layout.maximumHeight: loader.item && (root.type == MessageEventModel.Image || root.type == MessageEventModel.Sticker) ? loader.item.height : -1
Layout.columnSpan: 2
sourceComponent: {
switch (model.reply.type) {
switch (root.type) {
case MessageEventModel.Image:
case MessageEventModel.Sticker:
return imageComponent;
@@ -89,14 +141,14 @@ Item {
}
TapHandler {
acceptedButtons: Qt.LeftButton
onTapped: replyComponent.replyClicked()
onTapped: root.replyClicked()
}
}
Component {
id: textComponent
RichLabel {
textMessage: model.reply.display
textMessage: root.display
textFormat: Text.RichText
HoverHandler {
@@ -106,7 +158,7 @@ Item {
TapHandler {
enabled: !hoveredLink
acceptedButtons: Qt.LeftButton
onTapped: replyComponent.replyClicked()
onTapped: root.replyClicked()
}
}
}
@@ -116,15 +168,15 @@ Item {
id: image
property var imageWidth: {
if (replyComponent.mediaInfo.width > 0) {
return replyComponent.mediaInfo.width;
if (root.mediaInfo.width > 0) {
return root.mediaInfo.width;
} else {
return sourceSize.width;
}
}
property var imageHeight: {
if (replyComponent.mediaInfo.height > 0) {
return replyComponent.mediaInfo.height;
if (root.mediaInfo.height > 0) {
return root.mediaInfo.height;
} else {
return sourceSize.height;
}
@@ -134,15 +186,15 @@ Item {
height: width / aspectRatio
fillMode: Image.PreserveAspectFit
source: mediaInfo.source
source: root.mediaInfo.source
}
}
Component {
id: mimeComponent
MimeComponent {
mimeIconSource: replyComponent.mediaInfo.mimeIcon
label: model.reply.display
subLabel: model.reply.type === MessageEventModel.File ? Controller.formatByteSize(replyComponent.mediaInfo.size) : Controller.formatDuration(replyComponent.mediaInfo.duration)
mimeIconSource: root.mediaInfo.mimeIcon
label: root.display
subLabel: root.type === MessageEventModel.File ? Controller.formatByteSize(root.mediaInfo.size) : Controller.formatDuration(root.mediaInfo.duration)
}
}
Component {

View File

@@ -8,13 +8,35 @@ import QtQuick.Layouts 1.15
import org.kde.neochat 1.0
import org.kde.kirigami 2.15 as Kirigami
/**
* @brief A component to show the rich display text of text message.
*/
TextEdit {
id: contentLabel
id: root
/**
* @brief The rich text message to display.
*/
property string textMessage
/**
* @brief Whether this message is replying to another.
*/
property bool isReply
/**
* @brief Regex for detecting a message with a single emoji.
*/
readonly property var isEmoji: /^(<span style='.*'>)?(\u00a9|\u00ae|[\u20D0-\u2fff]|[\u3190-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])+(<\/span>)?$/
/**
* @brief Regex for detecting a message with a spoiler.
*/
readonly property var hasSpoiler: /data-mx-spoiler/g
property string textMessage: model.display
/**
* @brief Whether a spoiler should be revealed.
*/
property bool spoilerRevealed: !hasSpoiler.test(textMessage)
ListView.onReused: Qt.binding(() => !hasSpoiler.test(textMessage))
@@ -23,7 +45,7 @@ TextEdit {
// Work around QTBUG 93281
Component.onCompleted: if (text.includes("<img")) {
Controller.forceRefreshTextDocument(contentLabel.textDocument, contentLabel)
Controller.forceRefreshTextDocument(root.textDocument, root)
}
text: "<style>
@@ -63,7 +85,7 @@ a{
color: Kirigami.Theme.textColor
selectedTextColor: Kirigami.Theme.highlightedTextColor
selectionColor: Kirigami.Theme.highlightColor
font.pointSize: model.reply === undefined && isEmoji.test(model.display) ? Kirigami.Theme.defaultFont.pointSize * 4 : Kirigami.Theme.defaultFont.pointSize
font.pointSize: !root.isReply && isEmoji.test(textMessage) ? Kirigami.Theme.defaultFont.pointSize * 4 : Kirigami.Theme.defaultFont.pointSize
selectByMouse: !Kirigami.Settings.isMobile
readOnly: true
wrapMode: Text.Wrap

View File

@@ -9,34 +9,276 @@ import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
/**
* @brief The base delegate for all messages in the timeline.
*
* This supports a message bubble plus sender avatar for each message as well as reactions
* and read markers. A date section can be show for when the message is on a different
* day to the previous one.
*
* The component is designed for all messages, positioning them in the timeline with
* variable padding depending on the window width. Local user messages are highlighted
* and can also be aligned to the right if configured.
*
* This component also supports a compact mode where the padding is adjusted, the
* background is hidden and the delegate spans the full width of the timeline.
*/
ColumnLayout {
id: root
property string eventId: model.eventId
/**
* @brief The index of the delegate in the model.
*/
required property var index
property var author: model.author
/**
* @brief The matrix ID of the message event.
*/
required property string eventId
property int delegateType: model.delegateType
/**
* @brief The timestamp of the message.
*/
required property var time
property bool verified: model.verified
/**
* @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 NeoChatUser object for the author.
*
* @sa NeoChatUser
*/
required property var author
/**
* @brief Whether the author should be shown.
*/
required property bool showAuthor
/**
* @brief The delegate type 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
/**
* @brief The formatted body of the message.
*/
required property string formattedBody
/**
* @brief The date of the event as a string.
*/
required property string section
/**
* @brief Whether the section header should be shown.
*/
required property bool showSection
/**
* @brief A model with the reactions to the message in.
*/
required property var reaction
/**
* @brief Whether the reaction component should be shown.
*/
required property bool showReactions
/**
* @brief A model with the first 5 other user read markers for this message.
*/
required property var readMarkers
/**
* @brief String with the display name and matrix ID of the other user read markers.
*/
required property string readMarkersString
/**
* @brief The number of other users at the event after the first 5.
*/
required property var excessReadMarkers
/**
* @brief Whether the other user read marker component should be shown.
*/
required property bool showReadMarkers
/**
* @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 NeoChatUser object for the reply author.
*
* @sa NeoChatUser
*/
required property var replyAuthor
/**
* @brief The reply content.
*
* This should consist of the following:
* - display - The display text of the reply.
* - type - The delegate type of the reply.
*/
required property var reply
/**
* @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
/**
* @brief Whether this message is replying to another.
*/
required property bool isReply
/**
* @brief Whether this message has a local user mention.
*/
required property bool isHighlighted
/**
* @brief Whether an event is waiting to be accepted by the server.
*/
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.
*/
required property bool verified
/**
* @brief The mime type of the message's file or media.
*/
required property var mimeType
/**
* @brief The full message source JSON.
*/
required property var source
/**
* @brief The x position of the message bubble.
*
* @note Used for positioning the hover actions.
*/
readonly property real bubbleX: bubble.x + bubble.anchors.leftMargin
/**
* @brief The y position of the message bubble.
*
* @note Used for positioning the hover actions.
*/
readonly property alias bubbleY: mainContainer.y
/**
* @brief The width of the message bubble.
*
* @note Used for sizing the hover actions.
*/
readonly property alias bubbleWidth: bubble.width
/**
* @brief Whether this message is hovered.
*/
readonly property alias hovered: bubble.hovered
/**
* @brief Open the context menu for the message.
*/
signal openContextMenu
/**
* @brief Open the any message media externally.
*/
signal openExternally()
/**
* @brief The reply has been clicked.
*/
signal replyClicked(string eventID)
onReplyClicked: ListView.view.goToEvent(eventID)
/**
* @brief The component to display the delegate type.
*
* This is used by the inherited delegates to assign a component to visualise
* the message content for that delegate type.
*/
default property alias innerObject : column.children
property Item hoverComponent: hoverActions ?? null
/**
* @brief Whether the bubble background is enabled.
*/
property bool cardBackground: true
property bool showUserMessageOnRight: Config.showLocalMessagesOnRight && model.author.isLocalUser && !Config.compactLayout
property bool isHighlighted: model.isHighlighted || isTemporaryHighlighted
/**
* @brief Whether local user messages should be aligned right.
*
* TODO: make private
*/
property bool showUserMessageOnRight: Config.showLocalMessagesOnRight && root.author.isLocalUser && !Config.compactLayout
/**
* @brief Whether the message should be highlighted.
*/
property bool showHighlight: root.isHighlighted || isTemporaryHighlighted
/**
* @brief Whether the message should temporarily be highlighted.
*
* Normally triggered when jumping to the event in the timeline, e.g. when a reply
* is clicked.
*/
property bool isTemporaryHighlighted: false
onIsTemporaryHighlightedChanged: if (isTemporaryHighlighted) temporaryHighlightTimer.start()
@@ -48,6 +290,7 @@ ColumnLayout {
onTriggered: isTemporaryHighlighted = false
}
// TODO: make these private
// The bubble and delegate widths are allowed to grow once the ListView gets beyond a certain size
// extraWidth defines this as the excess after a certain ListView width, capped to a max value
readonly property int extraWidth: parent ? (parent.width >= Kirigami.Units.gridUnit * 46 ? Math.min((parent.width - Kirigami.Units.gridUnit * 46), Kirigami.Units.gridUnit * 20) : 0) : 0
@@ -89,23 +332,23 @@ ColumnLayout {
id: sectionDelegate
Layout.fillWidth: true
visible: model.showSection
labelText: model.showSection ? section : ""
visible: root.showSection
labelText: root.section
}
QQC2.ItemDelegate {
id: mainContainer
Layout.fillWidth: true
Layout.topMargin: showAuthor ? Kirigami.Units.largeSpacing : (Config.compactLayout ? 1 : Kirigami.Units.smallSpacing)
Layout.topMargin: root.showAuthor ? Kirigami.Units.largeSpacing : (Config.compactLayout ? 1 : Kirigami.Units.smallSpacing)
Layout.leftMargin: Kirigami.Units.smallSpacing
Layout.rightMargin: Kirigami.Units.smallSpacing
implicitHeight: Math.max(model.showAuthor ? avatar.implicitHeight : 0, bubble.height)
implicitHeight: Math.max(root.showAuthor ? avatar.implicitHeight : 0, bubble.height)
Component.onCompleted: {
if (model.isReply && model.reply === undefined) {
messageEventModel.loadReply(sortedMessageEventModel.mapToSource(collapseStateProxyModel.mapToSource(collapseStateProxyModel.index(model.index, 0))))
if (root.isReply && root.reply === undefined) {
messageEventModel.loadReply(sortedMessageEventModel.mapToSource(collapseStateProxyModel.mapToSource(collapseStateProxyModel.index(root.index, 0))))
}
}
@@ -130,20 +373,20 @@ ColumnLayout {
leftMargin: Kirigami.Units.smallSpacing
}
visible: model.showAuthor &&
visible: root.showAuthor &&
Config.showAvatarInTimeline &&
(Config.compactLayout || !showUserMessageOnRight)
name: model.author.name ?? model.author.displayName
source: model.author.avatarSource
color: model.author.color
name: root.author.name ?? root.author.displayName
source: root.author.avatarSource
color: root.author.color
MouseArea {
anchors.fill: parent
onClicked: {
userDetailDialog.createObject(QQC2.ApplicationWindow.overlay, {
room: currentRoom,
user: author.object,
displayName: author.displayName
user: root.author.object,
displayName: root.author.displayName
}).open();
}
cursorShape: Qt.PointingHandCursor
@@ -199,7 +442,7 @@ ColumnLayout {
width: height
Layout.preferredWidth: Kirigami.Units.iconSizes.small
Layout.preferredHeight: Kirigami.Units.iconSizes.small
visible: model.isPending && Config.showLocalMessagesOnRight
visible: root.isPending && Config.showLocalMessagesOnRight
}
ColumnLayout {
id: column
@@ -208,17 +451,17 @@ ColumnLayout {
id: rowLayout
spacing: Kirigami.Units.smallSpacing
visible: model.showAuthor
visible: root.showAuthor
QQC2.Label {
id: nameLabel
Layout.maximumWidth: contentMaxWidth - timeLabel.implicitWidth - rowLayout.spacing
text: visible ? author.displayName : ""
text: visible ? root.author.displayName : ""
textFormat: Text.PlainText
font.weight: Font.Bold
color: author.color
color: root.author.color
elide: Text.ElideRight
MouseArea {
anchors.fill: parent
@@ -226,9 +469,9 @@ ColumnLayout {
onClicked: {
userDetailDialog.createObject(QQC2.ApplicationWindow.overlay, {
room: currentRoom,
user: author.object,
displayName: author.displayName,
avatarSource: author.avatarSource
user: root.author.object,
displayName: root.author.displayName,
avatarSource: root.author.avatarSource
}).open();
}
}
@@ -236,10 +479,10 @@ ColumnLayout {
QQC2.Label {
id: timeLabel
text: visible ? model.time.toLocaleTimeString(Qt.locale(), Locale.ShortFormat) : ""
text: visible ? root.time.toLocaleTimeString(Qt.locale(), Locale.ShortFormat) : ""
color: Kirigami.Theme.disabledTextColor
QQC2.ToolTip.visible: hoverHandler.hovered
QQC2.ToolTip.text: model.time.toLocaleString(Qt.locale(), Locale.LongFormat)
QQC2.ToolTip.text: root.time.toLocaleString(Qt.locale(), Locale.LongFormat)
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
HoverHandler {
@@ -252,20 +495,20 @@ ColumnLayout {
Layout.maximumWidth: contentMaxWidth
active: model.isReply
active: root.isReply
visible: active
sourceComponent: ReplyComponent {
name: currentRoom.htmlSafeMemberName(model.replyAuthor.id)
avatar: model.replyAuthor.avatarSource
color: model.replyAuthor.color
mediaInfo: model.replyMediaInfo
author: root.replyAuthor
type: root.reply.type
display: root.reply.display
mediaInfo: root.replyMediaInfo
}
Connections {
target: replyLoader.item
function onReplyClicked() {
replyClicked(reply.eventId)
replyClicked(root.reply.eventId)
}
}
}
@@ -275,7 +518,7 @@ ColumnLayout {
width: height
Layout.preferredWidth: Kirigami.Units.iconSizes.small
Layout.preferredHeight: Kirigami.Units.iconSizes.small
visible: model.isPending && !Config.showLocalMessagesOnRight
visible: root.isPending && !Config.showLocalMessagesOnRight
}
}
@@ -286,9 +529,9 @@ ColumnLayout {
anchors.fill: parent
Kirigami.Theme.colorSet: Kirigami.Theme.View
color: {
if (model.author.isLocalUser) {
if (root.author.isLocalUser) {
return Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.15)
} else if (root.isHighlighted) {
} else if (root.showHighlight) {
return Kirigami.Theme.positiveBackgroundColor
} else {
return Kirigami.Theme.backgroundColor
@@ -296,7 +539,7 @@ ColumnLayout {
}
radius: Kirigami.Units.smallSpacing
shadow.size: Kirigami.Units.smallSpacing
shadow.color: root.isHighlighted ? Qt.rgba(0.0, 0.0, 0.0, 0.10) : Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.10)
shadow.color: root.showHighlight ? Qt.rgba(0.0, 0.0, 0.0, 0.10) : Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.10)
border.color: Kirigami.ColorUtils.tintWithAlpha(color, Kirigami.Theme.textColor, 0.15)
border.width: 1
@@ -332,18 +575,18 @@ ColumnLayout {
Layout.leftMargin: showUserMessageOnRight ? 0 : bubble.x + bubble.anchors.leftMargin
Layout.rightMargin: showUserMessageOnRight ? Kirigami.Units.largeSpacing : 0
visible: showReactions
model: reaction
visible: root.showReactions
model: root.reaction
onReactionClicked: (reaction) => currentRoom.toggleReaction(eventId, reaction)
onReactionClicked: (reaction) => currentRoom.toggleReaction(root.eventId, reaction)
}
AvatarFlow {
Layout.alignment: Qt.AlignRight
Layout.rightMargin: Kirigami.Units.largeSpacing
visible: showReadMarkers
model: readMarkers
toolTipText: readMarkersString
excessAvatars: excessReadMarkers
visible: root.showReadMarkers
model: root.readMarkers
toolTipText: root.readMarkersString
excessAvatars: root.excessReadMarkers
}
function isVisibleInTimeline() {
@@ -352,31 +595,31 @@ ColumnLayout {
}
/// Open message context dialog for file and videos
function openFileContext(event, file) {
function openFileContext(file) {
const contextMenu = fileDelegateContextMenu.createObject(root, {
author: event.author,
message: event.plainText,
eventId: event.eventId,
source: event.source,
author: root.author,
message: root.plainText,
eventId: root.eventId,
source: root.source,
file: file,
mimeType: event.mimeType,
progressInfo: event.progressInfo,
plainMessage: event.plainText,
mimeType: root.mimeType,
progressInfo: root.progressInfo,
plainMessage: root.plainText,
});
contextMenu.open();
}
/// Open context menu for normal message
function openMessageContext(event, selectedText, plainMessage) {
function openMessageContext(selectedText) {
const contextMenu = messageDelegateContextMenu.createObject(root, {
selectedText: selectedText,
author: event.author,
message: event.plainText,
eventId: event.eventId,
formattedBody: event.formattedBody,
source: event.source,
eventType: event.eventType,
plainMessage: event.plainText,
author: root.author,
message: root.plainText,
eventId: root.eventId,
formattedBody: root.formattedBody,
source: root.source,
eventType: root.delegateType,
plainMessage: root.plainText,
});
contextMenu.open();
}

View File

@@ -12,21 +12,59 @@ import org.kde.kirigamiaddons.labs.components 1.0 as Components
import org.kde.neochat 1.0
/**
* @brief A timeline delegate for a video message.
*
* @inherit TimelineContainer
*/
TimelineContainer {
id: videoDelegate
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
readonly property bool downloaded: progressInfo && progressInfo.completed
/**
* @brief Whether the video can be streamed.
*/
property bool supportStreaming: true
/**
* @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: openFileContext(model, vid)
onOpenContextMenu: openFileContext(vid)
onDownloadedChanged: {
if (downloaded) {
vid.source = progressInfo.localPath
vid.source = root.progressInfo.localPath
}
if (downloaded && playOnFinished) {
@@ -39,22 +77,22 @@ TimelineContainer {
id: vid
property var videoWidth: {
if (model.mediaInfo.width > 0) {
return model.mediaInfo.width;
if (root.mediaInfo.width > 0) {
return root.mediaInfo.width;
} else if (metaData.resolution && metaData.resolution.width) {
return metaData.resolution.width;
} else {
return videoDelegate.contentMaxWidth;
return root.contentMaxWidth;
}
}
property var videoHeight: {
if (model.mediaInfo.height > 0) {
return model.mediaInfo.height;
if (root.mediaInfo.height > 0) {
return root.mediaInfo.height;
} else if (metaData.resolution && metaData.resolution.height) {
return metaData.resolution.height;
} else {
// Default to a 16:9 placeholder
return videoDelegate.contentMaxWidth / 16 * 9;
return root.contentMaxWidth / 16 * 9;
}
}
@@ -69,11 +107,11 @@ TimelineContainer {
readonly property size maxSize: {
if (limitWidth) {
let width = Math.min(videoDelegate.contentMaxWidth, videoDelegate.maxWidth);
let width = Math.min(root.contentMaxWidth, root.maxWidth);
let height = width / aspectRatio;
return Qt.size(width, height);
} else {
let height = Math.min(videoDelegate.maxHeight, videoDelegate.contentMaxWidth / aspectRatio);
let height = Math.min(root.maxHeight, root.contentMaxWidth / aspectRatio);
let width = height * aspectRatio;
return Qt.size(width, height);
}
@@ -91,7 +129,7 @@ TimelineContainer {
states: [
State {
name: "notDownloaded"
when: !model.progressInfo.completed && !model.progressInfo.active
when: !root.progressInfo.completed && !root.progressInfo.active
PropertyChanges {
target: noDownloadLabel
visible: true
@@ -103,7 +141,7 @@ TimelineContainer {
},
State {
name: "downloading"
when: model.progressInfo.active && !model.progressInfo.completed
when: root.progressInfo.active && !root.progressInfo.completed
PropertyChanges {
target: downloadBar
visible: true
@@ -111,7 +149,7 @@ TimelineContainer {
},
State {
name: "paused"
when: model.progressInfo.completed && (vid.playbackState === MediaPlayer.StoppedState || vid.playbackState === MediaPlayer.PausedState)
when: root.progressInfo.completed && (vid.playbackState === MediaPlayer.StoppedState || vid.playbackState === MediaPlayer.PausedState)
PropertyChanges {
target: videoControls
stateVisible: true
@@ -124,7 +162,7 @@ TimelineContainer {
},
State {
name: "playing"
when: model.progressInfo.completed && vid.playbackState === MediaPlayer.PlayingState
when: root.progressInfo.completed && vid.playbackState === MediaPlayer.PlayingState
PropertyChanges {
target: videoControls
stateVisible: true
@@ -154,7 +192,7 @@ TimelineContainer {
anchors.fill: parent
visible: false
source: model.mediaInfo.tempInfo.source
source: root.mediaInfo.tempInfo.source
fillMode: Image.PreserveAspectFit
}
@@ -190,8 +228,8 @@ TimelineContainer {
width: parent.width * 0.8
from: 0
to: progressInfo.total
value: progressInfo.progress
to: root.progressInfo.total
value: root.progressInfo.progress
}
}
@@ -315,13 +353,22 @@ TimelineContainer {
text: i18n("Maximize")
icon.name: "view-fullscreen"
onTriggered: {
videoDelegate.ListView.view.interactive = false
root.ListView.view.interactive = false
vid.pause()
var popup = maximizeVideoComponent.createObject(QQC2.ApplicationWindow.overlay, {
modelData: model,
eventId: root.eventId,
time: root.time,
author: root.author,
delegateType: root.delegateType,
plainText: root.plainText,
caption: root.display,
mediaInfo: root.mediaInfo,
progressInfo: root.progressInfo,
mimeType: root.mimeType,
source: root.source
})
popup.closed.connect(() => {
videoDelegate.ListView.view.interactive = true
root.ListView.view.interactive = true
popup.destroy()
})
popup.open()
@@ -364,14 +411,14 @@ TimelineContainer {
TapHandler {
acceptedButtons: Qt.LeftButton
onTapped: if (vid.supportStreaming || progressInfo.completed) {
onTapped: if (vid.supportStreaming || root.progressInfo.completed) {
if (vid.playbackState == MediaPlayer.PlayingState) {
vid.pause()
} else {
vid.play()
}
} else {
videoDelegate.downloadAndPlay()
root.downloadAndPlay()
}
}
}
@@ -381,7 +428,7 @@ TimelineContainer {
playSavedFile()
} else {
playOnFinished = true
currentRoom.downloadFile(eventId, Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId))
currentRoom.downloadFile(root.eventId, Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/" + root.eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(root.eventId))
}
}