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

@@ -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))
}
}