Start implementing bubbles

This commit is contained in:
Carl Schwan
2021-03-06 20:19:41 +00:00
parent 724a579f0d
commit 612fb4924e
16 changed files with 823 additions and 786 deletions

View File

@@ -31,8 +31,8 @@ Rectangle {
// backgroundColor // backgroundColor
color: Kirigami.Theme.backgroundColor color: Kirigami.Theme.backgroundColor
Kirigami.Theme.colorSet: Kirigami.Theme.View Kirigami.Theme.colorSet: Kirigami.Theme.Window
Kirigami.Theme.inherit: true Kirigami.Theme.inherit: false
// Confetti // Confetti

View File

@@ -17,6 +17,7 @@ import NeoChat.Setting 1.0
import NeoChat.Component 1.0 import NeoChat.Component 1.0
import NeoChat.Dialog 1.0 import NeoChat.Dialog 1.0
import NeoChat.Menu.Timeline 1.0 import NeoChat.Menu.Timeline 1.0
import org.kde.kcoreaddons 1.0 as KCA
Control { Control {
id: root id: root
@@ -29,27 +30,6 @@ Control {
autoLoad: false autoLoad: false
} }
Kirigami.Action {
id: saveFileAction
onTriggered: {
let contextMenu = fileDelegateContextMenu.createObject(root, {'room': currentRoom, 'author': author});
contextMenu.viewSource.connect(function() {
messagerSourceSheet.createObject(ApplicationWindow.overlay, {"sourceText": toolTip}).open()
})
contextMenu.downloadAndOpen.connect(downloadAndOpen)
contextMenu.saveFileAs.connect(saveFileAs)
contextMenu.reply.connect(function() {
roomPanelInput.replyModel = Object.assign({}, model)
roomPanelInput.isReply = true
roomPanelInput.focus()
})
contextMenu.redact.connect(function() {
currentRoom.redactEvent(eventId)
})
contextMenu.popup()
}
}
contentItem: ColumnLayout { contentItem: ColumnLayout {
RowLayout { RowLayout {
ToolButton { ToolButton {
@@ -69,6 +49,8 @@ Control {
} }
RowLayout { RowLayout {
visible: audio.hasAudio visible: audio.hasAudio
Layout.leftMargin: Kirigami.Units.largeSpacing
Layout.rightMargin: Kirigami.Units.largeSpacing
// Server doesn't support seeking, so use ProgressBar instead of Slider :( // Server doesn't support seeking, so use ProgressBar instead of Slider :(
ProgressBar { ProgressBar {
from: 0 from: 0
@@ -77,72 +59,8 @@ Control {
} }
Label { Label {
text: humanSize(audio.position) + "/" + humanSize(audio.duration) text: KCA.Format.formatDuration(audio.position) + "/" + KCA.Format.formatDuration(audio.duration)
} }
} }
} }
background: AutoMouseArea {
anchors.fill: parent
id: messageMouseArea
onSecondaryClicked: saveFileAction.trigger()
Component {
id: messagerSourceSheet
MessageSourceSheet {}
}
Component {
id: openFolderDialog
OpenFolderDialog {}
}
Component {
id: fileDelegateContextMenu
FileDelegateContextMenu {}
}
}
function saveFileAs() {
var folderDialog = openFolderDialog.createObject(ApplicationWindow.overlay)
folderDialog.chosen.connect(function(path) {
if (!path) return
currentRoom.downloadFile(eventId, path + "/" + currentRoom.fileNameToDownload(eventId))
})
folderDialog.open()
}
function downloadAndOpen() {
if (downloaded) {
openSavedFile()
} else {
openOnFinished = true
currentRoom.downloadFile(eventId, Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId))
}
}
function openSavedFile() {
if (Qt.openUrlExternally(progressInfo.localPath)) return;
if (Qt.openUrlExternally(progressInfo.localDir)) return;
}
function humanSize(duration) {
if (!duration) {
return i18n("Unknown duration")
}
if (duration > 1000 * 60 * 60) {
return new Date(duration).toLocaleTimeString(Qt.locale(), "hh:mm:ss")
}
return new Date(duration).toLocaleTimeString(Qt.locale(), "mm:ss")
}
} }

View File

@@ -10,6 +10,7 @@ import QtQuick.Controls.Material 2.12
import QtGraphicalEffects 1.0 import QtGraphicalEffects 1.0
import Qt.labs.platform 1.0 import Qt.labs.platform 1.0
import org.kde.kirigami 2.13 as Kirigami import org.kde.kirigami 2.13 as Kirigami
import org.kde.kcoreaddons 1.0 as KCA
import org.kde.neochat 1.0 import org.kde.neochat 1.0
import NeoChat.Setting 1.0 import NeoChat.Setting 1.0
@@ -18,88 +19,51 @@ import NeoChat.Component 1.0
import NeoChat.Dialog 1.0 import NeoChat.Dialog 1.0
import NeoChat.Menu.Timeline 1.0 import NeoChat.Menu.Timeline 1.0
RowLayout { RowLayout {
id: root
property bool openOnFinished: false property bool openOnFinished: false
readonly property bool downloaded: progressInfo && progressInfo.completed readonly property bool downloaded: progressInfo && progressInfo.completed
id: root
spacing: 4 spacing: 4
onDownloadedChanged: if (downloaded && openOnFinished) openSavedFile() onDownloadedChanged: if (downloaded && openOnFinished) {
openSavedFile();
}
z: -5 z: -5
Control { ToolButton {
contentItem: RowLayout { icon.name: progressInfo.completed ? "document-open" : "document-save"
ToolButton { onClicked: progressInfo.completed ? openSavedFile() : saveFileAs()
icon.name: progressInfo.completed ? "document-open" : "document-save" }
onClicked: progressInfo.completed ? openSavedFile() : saveFileAs()
}
ColumnLayout { ColumnLayout {
Kirigami.Heading { Kirigami.Heading {
Layout.fillWidth: true Layout.fillWidth: true
level: 4 level: 4
text: display text: display
wrapMode: Label.Wrap wrapMode: Label.Wrap
}
Label {
Layout.fillWidth: true
text: !progressInfo.completed && progressInfo.active ? (humanSize(progressInfo.progress) + "/" + humanSize(progressInfo.total)) : humanSize(content.info ? content.info.size : 0)
color: Kirigami.Theme.disabledTextColor
wrapMode: Label.Wrap
}
}
} }
background: Item { Label {
MouseArea { Layout.fillWidth: true
id: messageMouseArea text: !progressInfo.completed && progressInfo.active ? (KCA.Format.formatByteSize(progressInfo.progress) + "/" + KCA.Format.formatByteSize(progressInfo.total)) : KCA.Format.formatByteSize(content.info ? content.info.size : 0)
anchors.fill: parent color: Kirigami.Theme.disabledTextColor
acceptedButtons: Qt.RightButton wrapMode: Label.Wrap
onClicked: { }
var contextMenu = fileDelegateContextMenu.createObject(root, {'room': currentRoom, 'author': author}); Layout.rightMargin: Kirigami.Units.largeSpacing
contextMenu.viewSource.connect(function() { }
messageSourceSheet.createObject(ApplicationWindow.overlay, {"sourceText": toolTip}).open()
})
contextMenu.downloadAndOpen.connect(downloadAndOpen)
contextMenu.saveFileAs.connect(saveFileAs)
contextMenu.reply.connect(function() {
roomPanelInput.replyModel = Object.assign({}, model)
roomPanelInput.isReply = true
roomPanelInput.focus()
})
contextMenu.redact.connect(function() {
currentRoom.redactEvent(eventId)
})
contextMenu.popup()
}
Component { Component {
id: messageSourceSheet id: fileDialog
MessageSourceSheet {} FileDialog {
} fileMode: FileDialog.SaveFile
folder: StandardPaths.writableLocation(StandardPaths.DownloadLocation)
Component { onAccepted: {
id: fileDialog currentRoom.downloadFile(eventId, file)
FileDialog {
fileMode: FileDialog.SaveFile
folder: StandardPaths.writableLocation(StandardPaths.DownloadLocation)
onAccepted: {
currentRoom.downloadFile(eventId, file)
}
}
}
Component {
id: fileDelegateContextMenu
FileDelegateContextMenu {}
}
} }
} }
} }
@@ -110,34 +74,18 @@ RowLayout {
dialog.currentFile = dialog.folder + "/" + currentRoom.fileNameToDownload(eventId) dialog.currentFile = dialog.folder + "/" + currentRoom.fileNameToDownload(eventId)
} }
function downloadAndOpen() function downloadAndOpen() {
{ if (downloaded) {
if (downloaded) openSavedFile() openSavedFile();
else } else {
{ openOnFinished = true;
openOnFinished = true currentRoom.downloadFile(eventId, StandardPaths.writableLocation(StandardPaths.CacheLocation) + "/"
currentRoom.downloadFile(eventId, StandardPaths.writableLocation(StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId)) + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId));
} }
} }
function openSavedFile() function openSavedFile() {
{
if (Qt.openUrlExternally(progressInfo.localPath)) return; if (Qt.openUrlExternally(progressInfo.localPath)) return;
if (Qt.openUrlExternally(progressInfo.localDir)) return; if (Qt.openUrlExternally(progressInfo.localDir)) return;
} }
function humanSize(bytes)
{
if (!bytes)
return i18nc("Unknown attachment size", "Unknown")
if (bytes < 4000)
return i18np("%1 byte", "%1 bytes", bytes)
bytes = Math.round(bytes / 100) / 10
if (bytes < 2000)
return i18nc("KB as in kilobytes", "%1 KB", bytes)
bytes = Math.round(bytes / 100) / 10
if (bytes < 2000)
return i18nc("MB as in megabytes", "%1 MB", bytes)
return i18nc("GB as in gigabytes", "%1 GB", Math.round(bytes / 100) / 10)
}
} }

View File

@@ -1,158 +0,0 @@
/**
* SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
* SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
*
* SPDX-License-Identifier: GPL-3.0-only
*/
import QtQuick 2.12
import QtQuick.Controls 2.12 as QQC2
import QtQuick.Layouts 1.12
import QtGraphicalEffects 1.12
import org.kde.kirigami 2.13 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Setting 1.0
import NeoChat.Component 1.0
import NeoChat.Dialog 1.0
RowLayout {
default property alias innerObject : column.children
readonly property bool sentByMe: author.isLocalUser
readonly property bool darkBackground: !sentByMe
readonly property bool replyVisible: reply ?? false
readonly property bool failed: marks == EventStatus.SendingFailed
readonly property color authorColor: eventType == "notice" ? Kirigami.Theme.activeTextColor : author.color
readonly property color replyAuthorColor: replyVisible ? reply.author.color : Kirigami.Theme.focusColor
property alias mouseArea: controlContainer.children
property bool isEmote: false
signal saveFileAs()
signal openExternally()
signal replyClicked(string eventID)
signal replyToMessageClicked(var replyUser, string replyContent, string eventID)
id: root
spacing: Kirigami.Units.smallSpacing
Layout.leftMargin: Kirigami.Units.largeSpacing
Layout.rightMargin: Kirigami.Units.smallSpacing
Layout.bottomMargin: 0
Layout.topMargin: showAuthor ? Kirigami.Units.smallSpacing : 0
Kirigami.Avatar {
Layout.minimumWidth: Kirigami.Units.gridUnit * 2
Layout.minimumHeight: Kirigami.Units.gridUnit * 2
Layout.maximumWidth: Kirigami.Units.gridUnit * 2
Layout.maximumHeight: Kirigami.Units.gridUnit * 2
Layout.alignment: Qt.AlignTop
visible: showAuthor && Config.showAvatarInTimeline
name: author.name ?? author.displayName
source: author.avatarMediaId ? ("image://mxc/" + author.avatarMediaId) : ""
color: author.color
Component {
id: userDetailDialog
UserDetailDialog {}
}
MouseArea {
anchors.fill: parent
onClicked: userDetailDialog.createObject(QQC2.ApplicationWindow.overlay, {"room": currentRoom, "user": author.object, "displayName": author.displayName, "avatarMediaId": author.avatarMediaId, "avatarUrl": author.avatarUrl}).open()
cursorShape: Qt.PointingHandCursor
}
}
Item {
Layout.minimumWidth: Kirigami.Units.gridUnit * 2
Layout.preferredHeight: 1
visible: !showAuthor && Config.showAvatarInTimeline
}
QQC2.Control {
id: controlContainer
Layout.fillWidth: true
topPadding: 0
bottomPadding: 0
hoverEnabled: true
contentItem: ColumnLayout {
id: column
spacing: showAuthor ? Kirigami.Units.smallSpacing : 0
RowLayout {
id: rowLayout
Layout.fillWidth: true
QQC2.Label {
Layout.fillWidth: true
topInset: 0
visible: showAuthor && !isEmote
text: author.displayName
font.weight: Font.Bold
color: author.color
wrapMode: Text.Wrap
}
QQC2.Label {
visible: showAuthor && !isEmote
text: time.toLocaleTimeString(Locale.ShortFormat)
color: Kirigami.Theme.disabledTextColor
}
}
Loader {
id: replyLoader
source: 'qrc:imports/NeoChat/Component/Timeline/ReplyComponent.qml'
active: replyVisible
}
Connections {
target: replyLoader.item
function onClicked() {
replyClicked(reply.eventId)
}
}
}
RowLayout {
z: 2
anchors.bottom: controlContainer.top
anchors.bottomMargin: -Kirigami.Units.gridUnit
anchors.right: controlContainer.right
spacing: 0
QQC2.Button {
QQC2.ToolTip.text: i18n("React")
QQC2.ToolTip.visible: hovered
visible: controlContainer.hovered
icon.name: "preferences-desktop-emoticons"
onClicked: emojiDialog.open();
EmojiDialog {
id: emojiDialog
onReact: currentRoom.toggleReaction(eventId, emoji)
}
}
QQC2.Button {
QQC2.ToolTip.text: i18n("Edit")
QQC2.ToolTip.visible: hovered
visible: controlContainer.hovered && author.id === Controller.activeConnection.localUserId && (model.eventType === "emote" || model.eventType === "message")
icon.name: "document-edit"
onClicked: chatTextInput.edit(message, model.formattedBody, eventId)
}
QQC2.Button {
QQC2.ToolTip.text: i18n("Reply")
QQC2.ToolTip.visible: hovered
visible: controlContainer.hovered
icon.name: "mail-replied-symbolic"
onClicked: replyToMessage(author, message, eventId)
}
}
background: Rectangle {
Kirigami.Theme.colorSet: Kirigami.Theme.Window
color: !model.isHighlighted ? Kirigami.Theme.backgroundColor : Kirigami.Theme.positiveBackgroundColor
opacity: controlContainer.hovered || model.isHighlighted ? 1 : 0
}
}
}

View File

@@ -10,12 +10,9 @@ import QtQuick.Layouts 1.12
import org.kde.kirigami 2.13 as Kirigami import org.kde.kirigami 2.13 as Kirigami
Flow { Flow {
visible: (reaction && reaction.length > 0) ?? false
spacing: Kirigami.Units.largeSpacing spacing: Kirigami.Units.largeSpacing
Repeater { Repeater {
model: reaction model: reaction ?? null
delegate: AbstractButton { delegate: AbstractButton {
width: Math.max(implicitWidth, height) width: Math.max(implicitWidth, height)
@@ -31,8 +28,8 @@ Flow {
radius: height / 2 radius: height / 2
Kirigami.Theme.colorSet: Kirigami.Theme.Button Kirigami.Theme.colorSet: Kirigami.Theme.Button
color: checked ? Kirigami.Theme.positiveBackgroundColor : Kirigami.Theme.backgroundColor color: checked ? Kirigami.Theme.positiveBackgroundColor : Kirigami.Theme.backgroundColor
border.color: checked ? Kirigami.Theme.positiveTextColor : Kirigami.Theme.textColor border.color: checked ? Kirigami.Theme.positiveTextColor : Kirigami.Theme.textColor
border.width: 1 border.width: 1
} }
checkable: true checkable: true

View File

@@ -31,7 +31,7 @@ QQC2.AbstractButton {
Layout.alignment: Qt.AlignTop Layout.alignment: Qt.AlignTop
visible: Config.showAvatarInTimeline visible: Config.showAvatarInTimeline
source: replyVisible && reply.author.avatarMediaId ? ("image://mxc/" + reply.author.avatarMediaId) : "" source: replyVisible && reply.author.avatarMediaId ? ("image://mxc/" + reply.author.avatarMediaId) : ""
name: replyVisible ? reply.author.name : "H" name: replyVisible ? (reply.author.name || "") : "H"
color: replyVisible ? reply.author.color : Kirigami.Theme.highlightColor color: replyVisible ? reply.author.color : Kirigami.Theme.highlightColor
} }
@@ -48,6 +48,7 @@ QQC2.AbstractButton {
TextDelegate { TextDelegate {
Layout.fillWidth: true Layout.fillWidth: true
Layout.leftMargin: 0
text: replyVisible ? ("<style>pre {white-space: pre-wrap} a{color: " + Kirigami.Theme.linkColor + ";} .user-pill{}</style>" + reply.display) : "" text: replyVisible ? ("<style>pre {white-space: pre-wrap} a{color: " + Kirigami.Theme.linkColor + ";} .user-pill{}</style>" + reply.display) : ""
textFormat: Text.RichText textFormat: Text.RichText
wrapMode: Text.WordWrap wrapMode: Text.WordWrap

View File

@@ -16,11 +16,6 @@ import NeoChat.Setting 1.0
RowLayout { RowLayout {
id: row id: row
Item {
Layout.minimumWidth: Kirigami.Units.iconSizes.medium
Layout.preferredHeight: 1
}
Kirigami.Avatar { Kirigami.Avatar {
Layout.preferredWidth: Kirigami.Units.iconSizes.small Layout.preferredWidth: Kirigami.Units.iconSizes.small
Layout.preferredHeight: Kirigami.Units.iconSizes.small Layout.preferredHeight: Kirigami.Units.iconSizes.small

View File

@@ -5,12 +5,16 @@
*/ */
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Controls 2.12 as QQC2 import QtQuick.Controls 2.12 as QQC2
import QtQuick.Layouts 1.12
import org.kde.kirigami 2.4 as Kirigami import org.kde.kirigami 2.4 as Kirigami
TextEdit { TextEdit {
id: contentLabel id: contentLabel
Layout.margins: Kirigami.Units.largeSpacing
Layout.topMargin: 0
readonly property var isEmoji: /^(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])+$/ readonly property var isEmoji: /^(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])+$/
property bool isEmote: false property bool isEmote: false

View File

@@ -3,34 +3,240 @@
* *
* SPDX-License-Identifier: GPL-3.0-only * SPDX-License-Identifier: GPL-3.0-only
*/ */
import QtQuick 2.12 import QtQuick 2.15
import QtQuick.Controls 2.12 as Controls
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import QtQuick.Controls 2.12 as QQC2
import QtGraphicalEffects 1.12
import org.kde.kirigami 2.4 as Kirigami import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Setting 1.0
import NeoChat.Component 1.0
import NeoChat.Dialog 1.0
Item { Item {
default property alias innerObject : column.children default property alias innerObject : column.children
readonly property bool sentByMe: author.isLocalUser
readonly property bool darkBackground: !sentByMe
readonly property bool replyVisible: reply ?? false
readonly property bool failed: marks == EventStatus.SendingFailed
readonly property color authorColor: eventType == "notice" ? Kirigami.Theme.activeTextColor : author.color
readonly property color replyAuthorColor: replyVisible ? reply.author.color : Kirigami.Theme.focusColor
height: column.implicitHeight + (readMarker ? 2 * Kirigami.Units.smallSpacing : 0) property alias mouseArea: controlContainer.children
property bool isEmote: false
property bool cardBackground: true
property bool isLoaded
property var hoverComponent
signal saveFileAs()
signal openExternally()
signal replyClicked(string eventID)
signal replyToMessageClicked(var replyUser, string replyContent, string eventID)
property alias hovered: controlContainer.hovered
implicitHeight: mainColumn.implicitHeight + (readMarker ? Kirigami.Units.smallSpacing : 0)
property int hoverComponentX: column.width - hoverComponent.childWidth + Kirigami.Units.largeSpacing
property int hoverComponentY: -Kirigami.Units.largeSpacing - hoverComponent.childHeight * 1.5
// show hover actions
onHoveredChanged: {
if (hovered && !Kirigami.Settings.isMobile) {
hoverComponent.x = Qt.binding(() => column.mapToItem(page, hoverComponentX, hoverComponentY).x);
hoverComponent.y = Qt.binding(() => column.mapToItem(page, hoverComponentX, hoverComponentY).y);
hoverComponent.hovered = Qt.binding(() => controlContainer.hovered);
hoverComponent.showEdit = author.id === Controller.activeConnection.localUserId && (model.eventType === "emote" || model.eventType === "message");
hoverComponent.editClicked = () => {
chatTextInput.edit(message, model.formattedBody, eventId);
};
hoverComponent.replyClicked = () => {
replyToMessage(author, message, eventId);
};
hoverComponent.reacted = emoji => {
currentRoom.toggleReaction(eventId, emoji);
};
}
}
DragHandler {
enabled: Kirigami.Settings.isMobile
yAxis.enabled: false
xAxis.enabled: true
xAxis.maximum: 0
xAxis.minimum: -Kirigami.Units.gridUnit * 4
onActiveChanged: {
applicationWindow().pageStack.interactive = true;
if (!active && parent.x < -Kirigami.Units.gridUnit * 3) {
replyToMessage(author, message, eventId)
}
parent.x = 0;
}
}
onXChanged: if (x !== 0) {
applicationWindow().pageStack.interactive = false;
} else {
applicationWindow().pageStack.interactive = true;
}
ColumnLayout { ColumnLayout {
id: column id: mainColumn
width: parent.width width: parent.width
spacing: 0
SectionDelegate { SectionDelegate {
id: sectionDelegate
Layout.maximumWidth: parent.width Layout.maximumWidth: parent.width
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
visible: showSection visible: showSection
} }
RowLayout {
id: root
spacing: Kirigami.Units.smallSpacing
Layout.leftMargin: Kirigami.Units.largeSpacing
Layout.rightMargin: Kirigami.Units.smallSpacing
Layout.bottomMargin: 0
Layout.topMargin: showAuthor ? Kirigami.Units.smallSpacing : 0
Kirigami.Avatar {
Layout.minimumWidth: Kirigami.Units.gridUnit * 2
Layout.minimumHeight: Kirigami.Units.gridUnit * 2
Layout.maximumWidth: Kirigami.Units.gridUnit * 2
Layout.maximumHeight: Kirigami.Units.gridUnit * 2
Layout.alignment: Qt.AlignTop
visible: showAuthor && Config.showAvatarInTimeline
name: author.name ?? author.displayName
source: author.avatarMediaId ? ("image://mxc/" + author.avatarMediaId) : ""
color: author.color
Component {
id: userDetailDialog
UserDetailDialog {}
}
MouseArea {
anchors.fill: parent
onClicked: userDetailDialog.createObject(QQC2.ApplicationWindow.overlay, {"room": currentRoom, "user": author.object, "displayName": author.displayName, "avatarMediaId": author.avatarMediaId, "avatarUrl": author.avatarUrl}).open()
cursorShape: Qt.PointingHandCursor
}
}
Item {
Layout.minimumWidth: Kirigami.Units.gridUnit * 2
Layout.preferredHeight: 1
visible: !showAuthor && Config.showAvatarInTimeline
}
// bubble
QQC2.Control {
id: controlContainer
//Layout.fillWidth: true
Layout.maximumWidth: mainColumn.width - Kirigami.Units.gridUnit * 2 - Kirigami.Units.largeSpacing * 2
topPadding: 0
bottomPadding: 0
leftPadding: 0
rightPadding: 0
hoverEnabled: true
contentItem: ColumnLayout {
id: column
spacing: 0
Item { // top padding
Layout.topMargin: Kirigami.Units.largeSpacing
}
// HACK: reload author when the delegate is reloaded, since there are strange issues with ListView reuseItems and the author displayName disappearing
Loader {
id: topRow
active: isLoaded && showAuthor && !isEmote
visible: active
Layout.fillWidth: true
Layout.leftMargin: Kirigami.Units.largeSpacing
Layout.rightMargin: Kirigami.Units.largeSpacing
Layout.bottomMargin: visible ? Kirigami.Units.smallSpacing : 0
sourceComponent: RowLayout {
id: rowLayout
QQC2.Label {
Layout.fillWidth: true
topInset: 0
visible: showAuthor && !isEmote
text: author.displayName
font.weight: Font.Bold
color: author.color
wrapMode: Text.Wrap
}
QQC2.Label {
visible: showAuthor && !isEmote
text: time.toLocaleTimeString(Locale.ShortFormat)
color: Kirigami.Theme.disabledTextColor
}
}
}
Loader {
id: replyLoader
source: 'qrc:imports/NeoChat/Component/Timeline/ReplyComponent.qml'
active: replyVisible
visible: active
Layout.bottomMargin: Kirigami.Units.smallSpacing
Connections {
target: replyLoader.item
function onClicked() {
replyClicked(reply.eventId)
}
}
}
}
background: Kirigami.ShadowedRectangle {
visible: cardBackground
color: Kirigami.Theme.backgroundColor
radius: Kirigami.Units.smallSpacing
shadow.size: Kirigami.Units.smallSpacing
shadow.color: !model.isHighlighted ? Qt.rgba(0.0, 0.0, 0.0, 0.10) : Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.10)
border.color: Kirigami.ColorUtils.tintWithAlpha(color, Kirigami.Theme.textColor, 0.15)
border.width: Kirigami.Units.devicePixelRatio
}
}
}
Loader {
id: loader
Layout.fillWidth: true
Layout.leftMargin: Kirigami.Units.gridUnit * 2 + Kirigami.Units.largeSpacing * 2
Layout.topMargin: active ? Kirigami.Units.smallSpacing : 0
active: eventType !== "state" && eventType !== "notice" && reaction != undefined && reaction.length > 0
visible: active
sourceComponent: ReactionDelegate { }
}
}
Kirigami.Icon {
id: replyButton
visible: parent.x < - Kirigami.Units.gridUnit * 1
opacity: -(parent.x + Kirigami.Units.gridUnit) / Kirigami.Units.gridUnit / 3
anchors.left: parent.right
anchors.top: parent.top
source: "mail-replied-symbolic"
} }
Rectangle { Rectangle {
width: parent.width * 0.9 width: parent.width * 0.9
x: parent.width * 0.05 x: parent.width * 0.05
height: Kirigami.Units.smallSpacing height: Kirigami.Units.smallSpacing / 2
anchors.top: column.bottom anchors.top: mainColumn.bottom
anchors.topMargin: Kirigami.Units.smallSpacing anchors.topMargin: Kirigami.Units.smallSpacing
visible: readMarker visible: readMarker
color: Kirigami.Theme.positiveTextColor color: Kirigami.Theme.positiveTextColor

View File

@@ -21,7 +21,6 @@ import NeoChat.Menu.Timeline 1.0
Video { Video {
id: vid id: vid
property bool openOnFinished: false
property bool playOnFinished: false property bool playOnFinished: false
readonly property bool downloaded: progressInfo && progressInfo.completed readonly property bool downloaded: progressInfo && progressInfo.completed
@@ -32,10 +31,6 @@ Video {
vid.source = progressInfo.localPath vid.source = progressInfo.localPath
} }
if (downloaded && openOnFinished) {
openSavedFile()
openOnFinished = false
}
if (downloaded && playOnFinished) { if (downloaded && playOnFinished) {
playSavedFile() playSavedFile()
playOnFinished = false playOnFinished = false
@@ -105,36 +100,6 @@ Video {
} }
} }
Control {
anchors.bottom: parent.bottom
anchors.bottomMargin: 8
anchors.right: parent.right
anchors.rightMargin: 8
horizontalPadding: 8
verticalPadding: 4
contentItem: RowLayout {
Label {
text: Qt.formatTime(time)
color: "white"
font.pixelSize: 12
}
Label {
text: author.displayName
color: "white"
font.pixelSize: 12
}
}
background: Rectangle {
radius: height / 2
color: "black"
opacity: 0.3
}
}
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
@@ -153,100 +118,29 @@ Video {
} }
} }
AutoMouseArea { TapHandler {
anchors.fill: parent acceptedButtons: Qt.LeftButton
onTapped: if (supportStreaming || progressInfo.completed) {
id: messageMouseArea if (vid.playbackState == MediaPlayer.PlayingState) {
vid.pause()
onPrimaryClicked: {
if (supportStreaming || progressInfo.completed) {
if (vid.playbackState == MediaPlayer.PlayingState) {
vid.pause()
} else {
vid.play()
}
} else { } else {
downloadAndPlay() vid.play()
} }
} } else {
downloadAndPlay()
onSecondaryClicked: {
var contextMenu = imageDelegateContextMenu.createObject(vid, {'room': currentRoom, 'author': author})
contextMenu.viewSource.connect(function() {
messageSourceSheet.createObject(ApplicationWindow.overlay, {"sourceText": toolTip}).open()
})
contextMenu.downloadAndOpen.connect(downloadAndOpen)
contextMenu.saveFileAs.connect(saveFileAs)
contextMenu.reply.connect(function() {
roomPanelInput.replyModel = Object.assign({}, model)
roomPanelInput.isReply = true
roomPanelInput.focus()
})
contextMenu.redact.connect(function() {
currentRoom.redactEvent(eventId)
})
contextMenu.popup()
}
Component {
id: messageSourceSheet
MessageSourceSheet {}
}
Component {
id: openFolderDialog
OpenFolderDialog {}
}
Component {
id: imageDelegateContextMenu
FileDelegateContextMenu {}
} }
} }
function saveFileAs() { function downloadAndPlay() {
var folderDialog = openFolderDialog.createObject(ApplicationWindow.overlay) if (downloaded) {
playSavedFile()
folderDialog.chosen.connect(function(path) { } else {
if (!path) return
currentRoom.downloadFile(eventId, path + "/" + currentRoom.fileNameToDownload(eventId))
})
folderDialog.open()
}
function downloadAndOpen()
{
if (downloaded) openSavedFile()
else
{
openOnFinished = true
currentRoom.downloadFile(eventId, Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId))
}
}
function downloadAndPlay()
{
if (downloaded) playSavedFile()
else
{
playOnFinished = true playOnFinished = true
currentRoom.downloadFile(eventId, Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId)) currentRoom.downloadFile(eventId, Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId))
} }
} }
function openSavedFile() function playSavedFile() {
{
if (Qt.openUrlExternally(progressInfo.localPath)) return;
if (Qt.openUrlExternally(progressInfo.localDir)) return;
}
function playSavedFile()
{
vid.stop() vid.stop()
vid.play() vid.play()
} }

View File

@@ -1,6 +1,5 @@
module NeoChat.Component.Timeline module NeoChat.Component.Timeline
TimelineContainer 1.0 TimelineContainer.qml TimelineContainer 1.0 TimelineContainer.qml
MessageDelegate 1.0 MessageDelegate.qml
TextDelegate 1.0 TextDelegate.qml TextDelegate 1.0 TextDelegate.qml
StateDelegate 1.0 StateDelegate.qml StateDelegate 1.0 StateDelegate.qml
SectionDelegate 1.0 SectionDelegate.qml SectionDelegate 1.0 SectionDelegate.qml

View File

@@ -5,50 +5,43 @@
*/ */
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Controls 2.12 import QtQuick.Controls 2.12
import org.kde.kirigami 2.14 as Kirigami
import NeoChat.Dialog 1.0 import NeoChat.Dialog 1.0
import NeoChat.Menu 1.0
Menu { MessageDelegateContextMenu {
id: root id: root
required property var room
required property var author
signal viewSource()
signal downloadAndOpen() signal downloadAndOpen()
signal saveFileAs() signal saveFileAs()
signal reply()
signal redact()
MenuItem { property list<Kirigami.Action> actions: [
text: i18n("View Source") Kirigami.Action {
text: i18n("Open Externally")
onTriggered: viewSource() icon.name: "document-open"
} onTriggered: downloadAndOpen()
},
MenuItem { Kirigami.Action {
text: i18n("Open Externally") text: i18n("Save As")
icon.name: "document-save"
onTriggered: downloadAndOpen() onTriggered: saveFileAs()
} },
Kirigami.Action {
MenuItem { text: i18n("Reply")
text: i18n("Save As") icon.name: "mail-replied-symbolic"
onTriggered: reply(author, message)
onTriggered: saveFileAs() },
} Kirigami.Action {
visible: author.id === currentRoom.localUser.id || currentRoom.canSendState("redact")
MenuItem { text: i18n("Remove")
text: i18n("Reply") icon.name: "edit-delete-remove"
icon.color: "red"
onTriggered: reply() onTriggered: remove()
} },
Kirigami.Action {
MenuItem { text: i18n("View Source")
visible: room.canSendState("redact") || room.localUser.id === author.id icon.name: "code-context"
text: i18n("Redact") onTriggered: viewSource()
onTriggered: redact() }
} ]
onClosed: destroy()
} }

View File

@@ -4,16 +4,16 @@
* *
* SPDX-License-Identifier: GPL-3.0-only * SPDX-License-Identifier: GPL-3.0-only
*/ */
import QtQuick 2.12 import QtQuick 2.15
import QtQuick.Controls 2.12 as QQC2 import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import org.kde.kirigami 2.13 as Kirigami import org.kde.kirigami 2.14 as Kirigami
import NeoChat.Dialog 1.0 import NeoChat.Dialog 1.0
import org.kde.neochat 1.0 import org.kde.neochat 1.0
Kirigami.OverlaySheet { Loader {
id: root id: loadRoot
required property var author required property var author
required property string message required property string message
@@ -23,99 +23,235 @@ Kirigami.OverlaySheet {
signal reply(var author, string message) signal reply(var author, string message)
signal remove() signal remove()
parent: applicationWindow().overlay property list<Kirigami.Action> actions: [
Kirigami.Action {
leftPadding: 0 text: i18n("Reply")
rightPadding: 0 icon.name: "mail-replied-symbolic"
onTriggered: reply(author, message)
ColumnLayout { },
spacing: 0 Kirigami.Action {
RowLayout {
id: headerLayout
Layout.fillWidth: true
Layout.leftMargin: Kirigami.Units.largeSpacing
Layout.rightMargin: Kirigami.Units.largeSpacing
spacing: Kirigami.Units.largeSpacing
Kirigami.Avatar {
id: avatar
source: author.avatarMediaId ? ("image://mxc/" + author.avatarMediaId) : ""
Layout.preferredWidth: Kirigami.Units.gridUnit * 3
Layout.preferredHeight: Kirigami.Units.gridUnit * 3
Layout.alignment: Qt.AlignTop
}
ColumnLayout {
Layout.fillWidth: true
Kirigami.Heading {
level: 3
Layout.fillWidth: true
text: author.displayName
wrapMode: Text.WordWrap
}
QQC2.Label {
text: message
Layout.fillWidth: true
wrapMode: Text.WordWrap
onLinkActivated: {
applicationWindow().handleLink(link, currentRoom)
}
}
}
}
Row {
spacing: 0
Repeater {
model: ["👍", "👎️", "😄", "🎉", "🚀", "👀"]
delegate: QQC2.ItemDelegate {
width: 32
height: 32
contentItem: QQC2.Label {
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.pixelSize: 16
font.family: "emoji"
text: modelData
}
onClicked: {
currentRoom.toggleReaction(eventId, modelData)
root.close();
}
}
}
}
Kirigami.BasicListItem {
action: Kirigami.Action {
text: i18n("Reply")
icon.name: "mail-replied-symbolic"
onTriggered: reply(author, message)
}
}
Kirigami.BasicListItem {
visible: author.id === currentRoom.localUser.id || currentRoom.canSendState("redact") visible: author.id === currentRoom.localUser.id || currentRoom.canSendState("redact")
action: Kirigami.Action { text: i18n("Remove")
text: i18n("Remove") icon.name: "edit-delete-remove"
icon.name: "edit-delete-remove" icon.color: "red"
icon.color: "red" onTriggered: remove()
onTriggered: remove() },
} Kirigami.Action {
text: i18n("Copy")
icon.name: "edit-copy"
onTriggered: Clipboard.saveText(message)
},
Kirigami.Action {
text: i18n("View Source")
icon.name: "code-context"
onTriggered: viewSource()
} }
Kirigami.BasicListItem { ]
action: Kirigami.Action {
text: i18n("Copy") Component {
icon.name: "edit-copy" id: regularMenu
onTriggered: Clipboard.saveText(message)
} Kirigami.OverlaySheet {
} id: root
Kirigami.BasicListItem {
action: Kirigami.Action { parent: applicationWindow().overlay
text: i18n("View Source")
icon.name: "code-context" leftPadding: 0
onTriggered: viewSource() rightPadding: 0
contentItem: ColumnLayout {
spacing: 0
RowLayout {
id: headerLayout
Layout.fillWidth: true
Layout.leftMargin: Kirigami.Units.largeSpacing
Layout.rightMargin: Kirigami.Units.largeSpacing
spacing: Kirigami.Units.largeSpacing
Kirigami.Avatar {
id: avatar
source: author.avatarMediaId ? ("image://mxc/" + author.avatarMediaId) : ""
Layout.preferredWidth: Kirigami.Units.gridUnit * 3
Layout.preferredHeight: Kirigami.Units.gridUnit * 3
Layout.alignment: Qt.AlignTop
}
ColumnLayout {
Layout.fillWidth: true
Kirigami.Heading {
level: 3
Layout.fillWidth: true
text: author.displayName
wrapMode: Text.WordWrap
}
QQC2.Label {
text: message
Layout.fillWidth: true
Layout.maximumWidth: Kirigami.Units.gridUnit * 24
wrapMode: Text.WordWrap
onLinkActivated: {
applicationWindow().handleLink(link, currentRoom)
}
}
}
}
Kirigami.Separator {
Layout.fillWidth: true
}
RowLayout {
spacing: 0
Layout.fillWidth: true
Layout.preferredHeight: Kirigami.Units.gridUnit * 2.5
Repeater {
model: ["👍", "👎️", "😄", "🎉", "🚀", "👀"]
delegate: QQC2.ItemDelegate {
Layout.fillWidth: true
Layout.fillHeight: true
contentItem: QQC2.Label {
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.pixelSize: 16
font.family: "emoji"
text: modelData
}
onClicked: {
currentRoom.toggleReaction(eventId, modelData)
loadRoot.item.close();
}
}
}
}
Kirigami.Separator {
Layout.fillWidth: true
}
Repeater {
model: loadRoot.actions
Kirigami.BasicListItem {
visible: modelData.visible
action: modelData
onClicked: {
modelData.triggered();
loadRoot.item.close();
}
}
}
} }
} }
} }
Component {
id: mobileMenu
Kirigami.OverlayDrawer {
id: drawer
height: popupContent.implicitHeight
edge: Qt.BottomEdge
padding: 0
leftPadding: 0
rightPadding: 0
bottomPadding: 0
topPadding: 0
parent: applicationWindow().overlay
ColumnLayout {
id: popupContent
width: parent.width
spacing: 0
RowLayout {
id: headerLayout
Layout.fillWidth: true
Layout.margins: Kirigami.Units.largeSpacing
spacing: Kirigami.Units.largeSpacing
Kirigami.Avatar {
id: avatar
source: author.avatarMediaId ? ("image://mxc/" + author.avatarMediaId) : ""
Layout.preferredWidth: Kirigami.Units.gridUnit * 3
Layout.preferredHeight: Kirigami.Units.gridUnit * 3
Layout.alignment: Qt.AlignTop
}
ColumnLayout {
Layout.fillWidth: true
Kirigami.Heading {
level: 3
Layout.fillWidth: true
text: author.displayName
wrapMode: Text.WordWrap
}
QQC2.Label {
text: message
Layout.fillWidth: true
wrapMode: Text.WordWrap
onLinkActivated: {
applicationWindow().handleLink(link, currentRoom)
}
}
}
}
Kirigami.Separator {
Layout.fillWidth: true
}
RowLayout {
spacing: 0
Layout.fillWidth: true
Layout.preferredHeight: Kirigami.Units.gridUnit * 2.5
Repeater {
model: ["👍", "👎️", "😄", "🎉", "🚀", "👀"]
delegate: QQC2.ItemDelegate {
Layout.fillWidth: true
Layout.fillHeight: true
contentItem: Kirigami.Heading {
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.family: "emoji"
text: modelData
}
onClicked: {
currentRoom.toggleReaction(eventId, modelData);
loadRoot.item.close();
}
}
}
}
Kirigami.Separator {
Layout.fillWidth: true
}
Repeater {
id: listViewAction
model: loadRoot.actions
Kirigami.BasicListItem {
icon: modelData.icon.name
iconColor: modelData.icon.color ?? undefined
enabled: modelData.enabled
visible: modelData.visible
text: modelData.text
onClicked: {
modelData.triggered()
loadRoot.item.close();
}
implicitHeight: visible ? Kirigami.Units.gridUnit * 3 : 0
}
}
}
}
}
asynchronous: true
sourceComponent: Kirigami.Settings.isMobile ? mobileMenu : regularMenu
function open() {
active = true;
}
onStatusChanged: if (status == Loader.Ready) {
item.open();
}
} }

View File

@@ -127,6 +127,58 @@ Kirigami.ScrollablePage {
} }
} }
// hover actions on a delegate, activated in TimelineContainer.qml
Item {
id: hoverActions
property bool showEdit
property bool hovered: false
visible: (hovered || hoverHandler.hovered) && !Kirigami.Settings.isMobile
property var editClicked
property var replyClicked
property var reacted
property alias childWidth: hoverActionsRow.width
property alias childHeight: hoverActionsRow.height
RowLayout {
id: hoverActionsRow
z: 4
spacing: 0
HoverHandler {
id: hoverHandler
margin: Kirigami.Units.smallSpacing
}
QQC2.Button {
QQC2.ToolTip.text: i18n("React")
QQC2.ToolTip.visible: hovered
visible: actions.hovered
icon.name: "preferences-desktop-emoticons"
onClicked: emojiDialog.open();
EmojiDialog {
id: emojiDialog
onReact: hoverActions.reacted(emoji)
}
}
QQC2.Button {
QQC2.ToolTip.text: i18n("Edit")
QQC2.ToolTip.visible: hovered
visible: actions.hovered && showEdit
icon.name: "document-edit"
onClicked: hoverActions.editClicked()
}
QQC2.Button {
QQC2.ToolTip.text: i18n("Reply")
QQC2.ToolTip.visible: hovered
visible: actions.hovered
icon.name: "mail-replied-symbolic"
onClicked: hoverActions.replyClicked()
}
}
}
ListView { ListView {
id: messageListView id: messageListView
@@ -137,7 +189,7 @@ Kirigami.ScrollablePage {
readonly property bool isLoaded: page.width * page.height > 10 readonly property bool isLoaded: page.width * page.height > 10
spacing: Kirigami.Units.smallSpacing spacing: Kirigami.Units.smallSpacing
clip: true reuseItems: true
verticalLayoutDirection: ListView.BottomToTop verticalLayoutDirection: ListView.BottomToTop
highlightMoveDuration: 500 highlightMoveDuration: 500
@@ -232,51 +284,62 @@ Kirigami.ScrollablePage {
sourceModel: messageEventModel sourceModel: messageEventModel
} }
// populate: Transition { populate: Transition {
// NumberAnimation { NumberAnimation {
// property: "opacity"; from: 0; to: 1 property: "opacity"; from: 0; to: 1
// duration: 200 duration: Kirigami.Units.shortDuration
// } }
// } }
// add: Transition { add: Transition {
// NumberAnimation { NumberAnimation {
// property: "opacity"; from: 0; to: 1 property: "opacity"; from: 0; to: 1
// duration: 200 duration: Kirigami.Units.shortDuration
// } }
// } }
// move: Transition { move: Transition {
// NumberAnimation { NumberAnimation {
// property: "y"; duration: 200 property: "y"
// } duration: Kirigami.Units.shortDuration
// NumberAnimation { }
// property: "opacity"; to: 1 NumberAnimation {
// } property: "opacity"; to: 1
// } }
}
// displaced: Transition { displaced: Transition {
// NumberAnimation { NumberAnimation {
// property: "y"; duration: 200 property: "y"
// easing.type: Easing.OutQuad duration: Kirigami.Units.shortDuration
// } easing.type: Easing.OutQuad
// NumberAnimation { }
// property: "opacity"; to: 1 NumberAnimation {
// } property: "opacity"; to: 1
// } }
}
delegate: DelegateChooser { delegate: DelegateChooser {
id: timelineDelegateChooser id: timelineDelegateChooser
role: "eventType" role: "eventType"
property bool delegateLoaded: true
ListView.onPooled: delegateLoaded = false
ListView.onReused: delegateLoaded = true
DelegateChoice { DelegateChoice {
roleValue: "state" roleValue: "state"
delegate: TimelineContainer { delegate: TimelineContainer {
width: messageListView.width id: container
width: messageListView.width - Kirigami.Units.largeSpacing
isLoaded: timelineDelegateChooser.delegateLoaded
cardBackground: false
hoverComponent: hoverActions
innerObject: StateDelegate { innerObject: StateDelegate {
Layout.maximumWidth: parent.width Layout.maximumWidth: container.width
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignLeft
} }
} }
} }
@@ -284,30 +347,24 @@ Kirigami.ScrollablePage {
DelegateChoice { DelegateChoice {
roleValue: "emote" roleValue: "emote"
delegate: TimelineContainer { delegate: TimelineContainer {
width: messageListView.width width: messageListView.width - Kirigami.Units.largeSpacing
innerObject: MessageDelegate { isLoaded: timelineDelegateChooser.delegateLoaded
Layout.fillWidth: true isEmote: true
Layout.maximumWidth: messageListView.width mouseArea: MouseArea {
acceptedButtons: Qt.RightButton
anchors.fill: parent
onClicked: openMessageContext(author, display, eventId, toolTip);
}
onReplyClicked: goToEvent(eventID)
onReplyToMessageClicked: replyToMessage(replyUser, replyContent, eventId);
hoverComponent: hoverActions
innerObject: TextDelegate {
isEmote: true isEmote: true
mouseArea: MouseArea { Layout.fillWidth: true
acceptedButtons: Qt.RightButton Layout.rightMargin: Kirigami.Units.largeSpacing
anchors.fill: parent Layout.leftMargin: Kirigami.Units.largeSpacing
onClicked: openMessageContext(author, display, eventId, toolTip);
}
onReplyClicked: goToEvent(eventID)
onReplyToMessageClicked: replyToMessage(replyUser, replyContent, eventId);
innerObject: [
TextDelegate {
isEmote: true
Layout.fillWidth: true
Layout.rightMargin: Kirigami.Units.largeSpacing
},
ReactionDelegate {
Layout.fillWidth: true
Layout.topMargin: 0
Layout.bottomMargin: Kirigami.Units.largeSpacing * 2
}
]
} }
} }
} }
@@ -315,31 +372,27 @@ Kirigami.ScrollablePage {
DelegateChoice { DelegateChoice {
roleValue: "message" roleValue: "message"
delegate: TimelineContainer { delegate: TimelineContainer {
width: messageListView.width id: timeline
innerObject: MessageDelegate { width: messageListView.width - Kirigami.Units.largeSpacing
isLoaded: timelineDelegateChooser.delegateLoaded
onReplyClicked: goToEvent(eventID)
onReplyToMessageClicked: replyToMessage(replyUser, replyContent, eventId);
hoverComponent: hoverActions
innerObject: TextDelegate {
Layout.fillWidth: true Layout.fillWidth: true
Layout.maximumWidth: messageListView.width Layout.rightMargin: Kirigami.Units.largeSpacing
onReplyClicked: goToEvent(eventID) Layout.leftMargin: Kirigami.Units.largeSpacing
onReplyToMessageClicked: replyToMessage(replyUser, replyContent, eventId); TapHandler {
innerObject: [ acceptedButtons: Qt.RightButton
TextDelegate { onTapped: openMessageContext(author, display, eventId, toolTip)
Layout.fillWidth: true }
Layout.rightMargin: Kirigami.Units.largeSpacing TapHandler {
TapHandler { acceptedButtons: Qt.LeftButton
acceptedButtons: Qt.RightButton onLongPressed: openMessageContext(author, display, eventId, toolTip)
onTapped: openMessageContext(author, display, eventId, toolTip) }
}
TapHandler {
acceptedButtons: Qt.LeftButton
onLongPressed: openMessageContext(author, display, eventId, toolTip)
}
},
ReactionDelegate {
Layout.fillWidth: true
Layout.topMargin: 0
Layout.bottomMargin: Kirigami.Units.largeSpacing
}
]
} }
} }
} }
@@ -347,16 +400,16 @@ Kirigami.ScrollablePage {
DelegateChoice { DelegateChoice {
roleValue: "notice" roleValue: "notice"
delegate: TimelineContainer { delegate: TimelineContainer {
width: messageListView.width width: messageListView.width - Kirigami.Units.largeSpacing
isLoaded: timelineDelegateChooser.delegateLoaded
onReplyClicked: goToEvent(eventID)
onReplyToMessageClicked: replyToMessage(replyUser, replyContent, eventId);
innerObject: MessageDelegate { hoverComponent: hoverActions
innerObject: TextDelegate {
Layout.fillWidth: true Layout.fillWidth: true
onReplyClicked: goToEvent(eventID) Layout.rightMargin: Kirigami.Units.largeSpacing
onReplyToMessageClicked: replyToMessage(replyUser, replyContent, eventId); Layout.leftMargin: Kirigami.Units.largeSpacing
innerObject: TextDelegate {
Layout.fillWidth: true
}
} }
} }
} }
@@ -364,26 +417,20 @@ Kirigami.ScrollablePage {
DelegateChoice { DelegateChoice {
roleValue: "image" roleValue: "image"
delegate: TimelineContainer { delegate: TimelineContainer {
width: messageListView.width width: messageListView.width - Kirigami.Units.largeSpacing
innerObject: MessageDelegate { isLoaded: timelineDelegateChooser.delegateLoaded
onReplyClicked: goToEvent(eventID)
onReplyToMessageClicked: replyToMessage(replyUser, replyContent, eventId);
hoverComponent: hoverActions
innerObject: ImageDelegate {
Layout.minimumWidth: Kirigami.Units.gridUnit * 10
Layout.fillWidth: true Layout.fillWidth: true
onReplyClicked: goToEvent(eventID) Layout.preferredHeight: info.h / info.w * width
onReplyToMessageClicked: replyToMessage(replyUser, replyContent, eventId); Layout.maximumHeight: Kirigami.Units.gridUnit * 15
Layout.maximumWidth: Kirigami.Units.gridUnit * 30
innerObject: [
ImageDelegate {
Layout.maximumWidth: parent.width
Layout.minimumWidth: 320
Layout.preferredHeight: info.h / info.w * width
},
ReactionDelegate {
Layout.fillWidth: true
Layout.topMargin: 0
Layout.maximumHeight: 320
Layout.bottomMargin: Kirigami.Units.largeSpacing
}
]
} }
} }
} }
@@ -391,27 +438,20 @@ Kirigami.ScrollablePage {
DelegateChoice { DelegateChoice {
roleValue: "sticker" roleValue: "sticker"
delegate: TimelineContainer { delegate: TimelineContainer {
width: messageListView.width width: messageListView.width - Kirigami.Units.largeSpacing
innerObject: MessageDelegate { isLoaded: timelineDelegateChooser.delegateLoaded
Layout.fillWidth: true onReplyClicked: goToEvent(eventID)
onReplyClicked: goToEvent(eventID) onReplyToMessageClicked: replyToMessage(replyUser, replyContent, eventId);
onReplyToMessageClicked: replyToMessage(replyUser, replyContent, eventId);
innerObject: [ hoverComponent: hoverActions
ImageDelegate { cardBackground: false
readonly: true
Layout.maximumWidth: parent.width / 2 innerObject: ImageDelegate {
Layout.minimumWidth: 320 readonly: true
Layout.preferredHeight: info.h / info.w * width Layout.maximumWidth: Kirigami.Units.gridUnit * 10
}, Layout.minimumWidth: Kirigami.Units.gridUnit * 10
ReactionDelegate { Layout.preferredHeight: info.h / info.w * width
Layout.fillWidth: true
Layout.topMargin: 0
Layout.maximumHeight: 320
Layout.bottomMargin: Kirigami.Units.largeSpacing
}
]
} }
} }
} }
@@ -419,25 +459,23 @@ Kirigami.ScrollablePage {
DelegateChoice { DelegateChoice {
roleValue: "audio" roleValue: "audio"
delegate: TimelineContainer { delegate: TimelineContainer {
width: messageListView.width width: messageListView.width - Kirigami.Units.largeSpacing
innerObject: MessageDelegate { hoverComponent: hoverActions
isLoaded: timelineDelegateChooser.delegateLoaded
onReplyClicked: goToEvent(eventID)
onReplyToMessageClicked: replyToMessage(replyUser, replyContent, eventId);
innerObject: AudioDelegate {
Layout.fillWidth: true Layout.fillWidth: true
onReplyClicked: goToEvent(eventID) TapHandler {
mouseArea: MouseArea { acceptedButtons: Qt.RightButton
acceptedButtons: (Kirigami.Settings.isMobile ? Qt.LeftButton : 0) | Qt.RightButton onTapped: openFileContext(author, display, eventId, toolTip, progressInfo, parent)
anchors.fill: parent
onClicked: {
if (mouse.button == Qt.RightButton) {
openMessageContext(author, display, eventId, toolTip);
}
}
onPressAndHold: openMessageContext(author, display, eventId, toolTip);
} }
onReplyToMessageClicked: replyToMessage(replyUser, replyContent, eventId); TapHandler {
acceptedButtons: Qt.LeftButton
innerObject: AudioDelegate { onLongPressed: openFileContext(author, display, eventId, toolTip, progressInfo, parent)
Layout.fillWidth: true
} }
} }
} }
@@ -446,28 +484,29 @@ Kirigami.ScrollablePage {
DelegateChoice { DelegateChoice {
roleValue: "video" roleValue: "video"
delegate: TimelineContainer { delegate: TimelineContainer {
width: messageListView.width width: messageListView.width - Kirigami.Units.largeSpacing
innerObject: MessageDelegate { hoverComponent: hoverActions
isLoaded: timelineDelegateChooser.delegateLoaded
onReplyClicked: goToEvent(eventID)
onReplyToMessageClicked: replyToMessage(replyUser, replyContent, eventId);
innerObject: VideoDelegate {
Layout.fillWidth: true Layout.fillWidth: true
onReplyClicked: goToEvent(eventID) Layout.minimumWidth: Kirigami.Units.gridUnit * 10
mouseArea: MouseArea { Layout.maximumWidth: Kirigami.Units.gridUnit * 30
acceptedButtons: (Kirigami.Settings.isMobile ? Qt.LeftButton : 0) | Qt.RightButton Layout.preferredHeight: content.info.h / content.info.w * width
anchors.fill: parent Layout.maximumHeight: Kirigami.Units.gridUnit * 15
onClicked: { Layout.minimumHeight: Kirigami.Units.gridUnit * 5
if (mouse.button == Qt.RightButton) {
openMessageContext(author, display, eventId, toolTip);
}
}
onPressAndHold: openMessageContext(author, display, eventId, toolTip);
}
onReplyToMessageClicked: replyToMessage(replyUser, replyContent, eventId);
innerObject: VideoDelegate { TapHandler {
Layout.maximumWidth: parent.width acceptedButtons: Qt.RightButton
Layout.minimumWidth: 320 onTapped: openFileContext(author, display, eventId, toolTip, progressInfo, parent)
Layout.maximumHeight: 320 }
Layout.preferredHeight: content.info.h / content.info.w * width TapHandler {
acceptedButtons: Qt.LeftButton
onLongPressed: openFileContext(author, display, eventId, toolTip, progressInfo, parent)
} }
} }
} }
@@ -476,15 +515,23 @@ Kirigami.ScrollablePage {
DelegateChoice { DelegateChoice {
roleValue: "file" roleValue: "file"
delegate: TimelineContainer { delegate: TimelineContainer {
width: messageListView.width width: messageListView.width - Kirigami.Units.largeSpacing
innerObject: MessageDelegate { hoverComponent: hoverActions
isLoaded: timelineDelegateChooser.delegateLoaded
onReplyClicked: goToEvent(eventID)
onReplyToMessageClicked: replyToMessage(replyUser, replyContent, eventId);
innerObject: FileDelegate {
Layout.fillWidth: true Layout.fillWidth: true
onReplyClicked: goToEvent(eventID) TapHandler {
onReplyToMessageClicked: replyToMessage(replyUser, replyContent, eventId); acceptedButtons: Qt.RightButton
onTapped: openFileContext(author, display, eventId, toolTip, progressInfo, parent)
innerObject: FileDelegate { }
Layout.fillWidth: true TapHandler {
acceptedButtons: Qt.LeftButton
onLongPressed: openFileContext(author, display, eventId, toolTip, progressInfo, parent)
} }
} }
} }
@@ -583,6 +630,18 @@ Kirigami.ScrollablePage {
MessageSourceSheet {} MessageSourceSheet {}
} }
Component {
id: openFolderDialog
OpenFolderDialog {}
}
Component {
id: fileDelegateContextMenu
FileDelegateContextMenu {}
}
} }
footer: ChatTextInput { footer: ChatTextInput {
@@ -626,8 +685,6 @@ Kirigami.ScrollablePage {
} }
} }
Kirigami.Theme.colorSet: Kirigami.Theme.View
function goToEvent(eventID) { function goToEvent(eventID) {
messageListView.positionViewAtIndex(eventToIndex(eventID), ListView.Contain) messageListView.positionViewAtIndex(eventToIndex(eventID), ListView.Contain)
} }
@@ -661,7 +718,56 @@ Kirigami.ScrollablePage {
return index; return index;
} }
function openMessageContext(author, message, eventId, toolTip, model) { /// Open message context dialog for file and videos
function openFileContext(author, message, eventId, toolTip, progressInfo, file) {
const contextMenu = fileDelegateContextMenu.createObject(page, {
'author': author,
'message': message,
'eventId': eventId,
});
contextMenu.downloadAndOpen.connect(function() {
if (file.downloaded) {
if (!Qt.openUrlExternally(progressInfo.localPath)) {
Qt.openUrlExternally(progressInfo.localDir);
}
} else {
file.onDownloadedChanged.connect(function() {
if (!Qt.openUrlExternally(progressInfo.localPath)) {
Qt.openUrlExternally(progressInfo.localDir);
}
});
currentRoom.downloadFile(eventId, Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId))
}
});
contextMenu.saveFileAs.connect(function() {
const folderDialog = openFolderDialog.createObject(ApplicationWindow.overlay)
folderDialog.chosen.connect(function(path) {
if (!path) {
return;
}
currentRoom.downloadFile(eventId, path + "/" + currentRoom.fileNameToDownload(eventId))
})
folderDialog.open()
})
contextMenu.viewSource.connect(function() {
messageSourceSheet.createObject(page, {
'sourceText': toolTip,
}).open();
});
contextMenu.reply.connect(function(replyUser, replyContent) {
replyToMessage(replyUser, replyContent, eventId);
})
contextMenu.remove.connect(function() {
currentRoom.redactEvent(eventId);
})
contextMenu.open();
}
/// Open context menu for normal message
function openMessageContext(author, message, eventId, toolTip) {
const contextMenu = messageDelegateContextMenu.createObject(page, { const contextMenu = messageDelegateContextMenu.createObject(page, {
'author': author, 'author': author,
'message': message, 'message': message,
@@ -671,17 +777,14 @@ Kirigami.ScrollablePage {
messageSourceSheet.createObject(page, { messageSourceSheet.createObject(page, {
'sourceText': toolTip, 'sourceText': toolTip,
}).open(); }).open();
contextMenu.close();
}); });
contextMenu.reply.connect(function(replyUser, replyContent) { contextMenu.reply.connect(function(replyUser, replyContent) {
replyToMessage(replyUser, replyContent, eventId); replyToMessage(replyUser, replyContent, eventId);
contextMenu.close();
}) })
contextMenu.remove.connect(function() { contextMenu.remove.connect(function() {
currentRoom.redactEvent(eventId); currentRoom.redactEvent(eventId);
contextMenu.close();
}) })
contextMenu.open() contextMenu.open();
} }
function replyToMessage(replyUser, replyContent, eventId) { function replyToMessage(replyUser, replyContent, eventId) {

View File

@@ -162,6 +162,8 @@ Kirigami.ApplicationWindow {
handleVisible: enabled && pageStack.layers.depth < 2 && pageStack.depth < 3 handleVisible: enabled && pageStack.layers.depth < 2 && pageStack.depth < 3
} }
pageStack.columnView.columnWidth: Kirigami.Units.gridUnit * 17
globalDrawer: Kirigami.GlobalDrawer { globalDrawer: Kirigami.GlobalDrawer {
property bool hasLayer property bool hasLayer
contentItem.implicitWidth: columnWidth contentItem.implicitWidth: columnWidth

View File

@@ -22,7 +22,6 @@
<file>imports/NeoChat/Component/Emoji/EmojiPicker.qml</file> <file>imports/NeoChat/Component/Emoji/EmojiPicker.qml</file>
<file>imports/NeoChat/Component/Emoji/qmldir</file> <file>imports/NeoChat/Component/Emoji/qmldir</file>
<file>imports/NeoChat/Component/Timeline/qmldir</file> <file>imports/NeoChat/Component/Timeline/qmldir</file>
<file>imports/NeoChat/Component/Timeline/MessageDelegate.qml</file>
<file>imports/NeoChat/Component/Timeline/ReplyComponent.qml</file> <file>imports/NeoChat/Component/Timeline/ReplyComponent.qml</file>
<file>imports/NeoChat/Component/Timeline/StateDelegate.qml</file> <file>imports/NeoChat/Component/Timeline/StateDelegate.qml</file>
<file>imports/NeoChat/Component/Timeline/TextDelegate.qml</file> <file>imports/NeoChat/Component/Timeline/TextDelegate.qml</file>