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
color: Kirigami.Theme.backgroundColor
Kirigami.Theme.colorSet: Kirigami.Theme.View
Kirigami.Theme.inherit: true
Kirigami.Theme.colorSet: Kirigami.Theme.Window
Kirigami.Theme.inherit: false
// Confetti

View File

@@ -17,6 +17,7 @@ import NeoChat.Setting 1.0
import NeoChat.Component 1.0
import NeoChat.Dialog 1.0
import NeoChat.Menu.Timeline 1.0
import org.kde.kcoreaddons 1.0 as KCA
Control {
id: root
@@ -29,27 +30,6 @@ Control {
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 {
RowLayout {
ToolButton {
@@ -69,6 +49,8 @@ Control {
}
RowLayout {
visible: audio.hasAudio
Layout.leftMargin: Kirigami.Units.largeSpacing
Layout.rightMargin: Kirigami.Units.largeSpacing
// Server doesn't support seeking, so use ProgressBar instead of Slider :(
ProgressBar {
from: 0
@@ -77,72 +59,8 @@ Control {
}
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 Qt.labs.platform 1.0
import org.kde.kirigami 2.13 as Kirigami
import org.kde.kcoreaddons 1.0 as KCA
import org.kde.neochat 1.0
import NeoChat.Setting 1.0
@@ -18,88 +19,51 @@ import NeoChat.Component 1.0
import NeoChat.Dialog 1.0
import NeoChat.Menu.Timeline 1.0
RowLayout {
id: root
property bool openOnFinished: false
readonly property bool downloaded: progressInfo && progressInfo.completed
id: root
spacing: 4
onDownloadedChanged: if (downloaded && openOnFinished) openSavedFile()
onDownloadedChanged: if (downloaded && openOnFinished) {
openSavedFile();
}
z: -5
Control {
contentItem: RowLayout {
ToolButton {
icon.name: progressInfo.completed ? "document-open" : "document-save"
onClicked: progressInfo.completed ? openSavedFile() : saveFileAs()
}
ToolButton {
icon.name: progressInfo.completed ? "document-open" : "document-save"
onClicked: progressInfo.completed ? openSavedFile() : saveFileAs()
}
ColumnLayout {
Kirigami.Heading {
Layout.fillWidth: true
level: 4
text: display
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
}
}
ColumnLayout {
Kirigami.Heading {
Layout.fillWidth: true
level: 4
text: display
wrapMode: Label.Wrap
}
background: Item {
MouseArea {
id: messageMouseArea
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: {
var contextMenu = fileDelegateContextMenu.createObject(root, {'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()
}
Label {
Layout.fillWidth: true
text: !progressInfo.completed && progressInfo.active ? (KCA.Format.formatByteSize(progressInfo.progress) + "/" + KCA.Format.formatByteSize(progressInfo.total)) : KCA.Format.formatByteSize(content.info ? content.info.size : 0)
color: Kirigami.Theme.disabledTextColor
wrapMode: Label.Wrap
}
Layout.rightMargin: Kirigami.Units.largeSpacing
}
Component {
id: messageSourceSheet
Component {
id: fileDialog
MessageSourceSheet {}
}
Component {
id: fileDialog
FileDialog {
fileMode: FileDialog.SaveFile
folder: StandardPaths.writableLocation(StandardPaths.DownloadLocation)
onAccepted: {
currentRoom.downloadFile(eventId, file)
}
}
}
Component {
id: fileDelegateContextMenu
FileDelegateContextMenu {}
}
FileDialog {
fileMode: FileDialog.SaveFile
folder: StandardPaths.writableLocation(StandardPaths.DownloadLocation)
onAccepted: {
currentRoom.downloadFile(eventId, file)
}
}
}
@@ -110,34 +74,18 @@ RowLayout {
dialog.currentFile = dialog.folder + "/" + currentRoom.fileNameToDownload(eventId)
}
function downloadAndOpen()
{
if (downloaded) openSavedFile()
else
{
openOnFinished = true
currentRoom.downloadFile(eventId, StandardPaths.writableLocation(StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId))
function downloadAndOpen() {
if (downloaded) {
openSavedFile();
} else {
openOnFinished = true;
currentRoom.downloadFile(eventId, StandardPaths.writableLocation(StandardPaths.CacheLocation) + "/"
+ eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId));
}
}
function openSavedFile()
{
function openSavedFile() {
if (Qt.openUrlExternally(progressInfo.localPath)) 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
Flow {
visible: (reaction && reaction.length > 0) ?? false
spacing: Kirigami.Units.largeSpacing
Repeater {
model: reaction
model: reaction ?? null
delegate: AbstractButton {
width: Math.max(implicitWidth, height)
@@ -31,8 +28,8 @@ Flow {
radius: height / 2
Kirigami.Theme.colorSet: Kirigami.Theme.Button
color: checked ? Kirigami.Theme.positiveBackgroundColor : Kirigami.Theme.backgroundColor
border.color: checked ? Kirigami.Theme.positiveTextColor : Kirigami.Theme.textColor
border.width: 1
border.color: checked ? Kirigami.Theme.positiveTextColor : Kirigami.Theme.textColor
border.width: 1
}
checkable: true

View File

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

View File

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

View File

@@ -5,12 +5,16 @@
*/
import QtQuick 2.12
import QtQuick.Controls 2.12 as QQC2
import QtQuick.Layouts 1.12
import org.kde.kirigami 2.4 as Kirigami
TextEdit {
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])+$/
property bool isEmote: false

View File

@@ -3,34 +3,240 @@
*
* SPDX-License-Identifier: GPL-3.0-only
*/
import QtQuick 2.12
import QtQuick.Controls 2.12 as Controls
import QtQuick 2.15
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 {
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 {
id: column
id: mainColumn
width: parent.width
spacing: 0
SectionDelegate {
id: sectionDelegate
Layout.maximumWidth: parent.width
Layout.alignment: Qt.AlignHCenter
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 {
width: parent.width * 0.9
x: parent.width * 0.05
height: Kirigami.Units.smallSpacing
anchors.top: column.bottom
height: Kirigami.Units.smallSpacing / 2
anchors.top: mainColumn.bottom
anchors.topMargin: Kirigami.Units.smallSpacing
visible: readMarker
color: Kirigami.Theme.positiveTextColor

View File

@@ -21,7 +21,6 @@ import NeoChat.Menu.Timeline 1.0
Video {
id: vid
property bool openOnFinished: false
property bool playOnFinished: false
readonly property bool downloaded: progressInfo && progressInfo.completed
@@ -32,10 +31,6 @@ Video {
vid.source = progressInfo.localPath
}
if (downloaded && openOnFinished) {
openSavedFile()
openOnFinished = false
}
if (downloaded && playOnFinished) {
playSavedFile()
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 {
anchors.fill: parent
@@ -153,100 +118,29 @@ Video {
}
}
AutoMouseArea {
anchors.fill: parent
id: messageMouseArea
onPrimaryClicked: {
if (supportStreaming || progressInfo.completed) {
if (vid.playbackState == MediaPlayer.PlayingState) {
vid.pause()
} else {
vid.play()
}
TapHandler {
acceptedButtons: Qt.LeftButton
onTapped: if (supportStreaming || progressInfo.completed) {
if (vid.playbackState == MediaPlayer.PlayingState) {
vid.pause()
} else {
downloadAndPlay()
vid.play()
}
}
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 {}
} else {
downloadAndPlay()
}
}
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 downloadAndPlay()
{
if (downloaded) playSavedFile()
else
{
function downloadAndPlay() {
if (downloaded) {
playSavedFile()
} else {
playOnFinished = 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 playSavedFile()
{
function playSavedFile() {
vid.stop()
vid.play()
}

View File

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

View File

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

View File

@@ -4,16 +4,16 @@
*
* SPDX-License-Identifier: GPL-3.0-only
*/
import QtQuick 2.12
import QtQuick.Controls 2.12 as QQC2
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
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 org.kde.neochat 1.0
Kirigami.OverlaySheet {
id: root
Loader {
id: loadRoot
required property var author
required property string message
@@ -23,99 +23,235 @@ Kirigami.OverlaySheet {
signal reply(var author, string message)
signal remove()
parent: applicationWindow().overlay
leftPadding: 0
rightPadding: 0
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
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 {
property list<Kirigami.Action> actions: [
Kirigami.Action {
text: i18n("Reply")
icon.name: "mail-replied-symbolic"
onTriggered: reply(author, message)
},
Kirigami.Action {
visible: author.id === currentRoom.localUser.id || currentRoom.canSendState("redact")
action: Kirigami.Action {
text: i18n("Remove")
icon.name: "edit-delete-remove"
icon.color: "red"
onTriggered: remove()
}
text: i18n("Remove")
icon.name: "edit-delete-remove"
icon.color: "red"
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")
icon.name: "edit-copy"
onTriggered: Clipboard.saveText(message)
}
}
Kirigami.BasicListItem {
action: Kirigami.Action {
text: i18n("View Source")
icon.name: "code-context"
onTriggered: viewSource()
]
Component {
id: regularMenu
Kirigami.OverlaySheet {
id: root
parent: applicationWindow().overlay
leftPadding: 0
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 {
id: messageListView
@@ -137,7 +189,7 @@ Kirigami.ScrollablePage {
readonly property bool isLoaded: page.width * page.height > 10
spacing: Kirigami.Units.smallSpacing
clip: true
reuseItems: true
verticalLayoutDirection: ListView.BottomToTop
highlightMoveDuration: 500
@@ -232,51 +284,62 @@ Kirigami.ScrollablePage {
sourceModel: messageEventModel
}
// populate: Transition {
// NumberAnimation {
// property: "opacity"; from: 0; to: 1
// duration: 200
// }
// }
populate: Transition {
NumberAnimation {
property: "opacity"; from: 0; to: 1
duration: Kirigami.Units.shortDuration
}
}
// add: Transition {
// NumberAnimation {
// property: "opacity"; from: 0; to: 1
// duration: 200
// }
// }
add: Transition {
NumberAnimation {
property: "opacity"; from: 0; to: 1
duration: Kirigami.Units.shortDuration
}
}
// move: Transition {
// NumberAnimation {
// property: "y"; duration: 200
// }
// NumberAnimation {
// property: "opacity"; to: 1
// }
// }
move: Transition {
NumberAnimation {
property: "y"
duration: Kirigami.Units.shortDuration
}
NumberAnimation {
property: "opacity"; to: 1
}
}
// displaced: Transition {
// NumberAnimation {
// property: "y"; duration: 200
// easing.type: Easing.OutQuad
// }
// NumberAnimation {
// property: "opacity"; to: 1
// }
// }
displaced: Transition {
NumberAnimation {
property: "y"
duration: Kirigami.Units.shortDuration
easing.type: Easing.OutQuad
}
NumberAnimation {
property: "opacity"; to: 1
}
}
delegate: DelegateChooser {
id: timelineDelegateChooser
role: "eventType"
property bool delegateLoaded: true
ListView.onPooled: delegateLoaded = false
ListView.onReused: delegateLoaded = true
DelegateChoice {
roleValue: "state"
delegate: TimelineContainer {
width: messageListView.width
id: container
width: messageListView.width - Kirigami.Units.largeSpacing
isLoaded: timelineDelegateChooser.delegateLoaded
cardBackground: false
hoverComponent: hoverActions
innerObject: StateDelegate {
Layout.maximumWidth: parent.width
Layout.alignment: Qt.AlignHCenter
Layout.maximumWidth: container.width
Layout.alignment: Qt.AlignLeft
}
}
}
@@ -284,30 +347,24 @@ Kirigami.ScrollablePage {
DelegateChoice {
roleValue: "emote"
delegate: TimelineContainer {
width: messageListView.width
innerObject: MessageDelegate {
Layout.fillWidth: true
Layout.maximumWidth: messageListView.width
width: messageListView.width - Kirigami.Units.largeSpacing
isLoaded: timelineDelegateChooser.delegateLoaded
isEmote: true
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
mouseArea: MouseArea {
acceptedButtons: Qt.RightButton
anchors.fill: parent
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
}
]
Layout.fillWidth: true
Layout.rightMargin: Kirigami.Units.largeSpacing
Layout.leftMargin: Kirigami.Units.largeSpacing
}
}
}
@@ -315,31 +372,27 @@ Kirigami.ScrollablePage {
DelegateChoice {
roleValue: "message"
delegate: TimelineContainer {
width: messageListView.width
innerObject: MessageDelegate {
id: timeline
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.maximumWidth: messageListView.width
onReplyClicked: goToEvent(eventID)
onReplyToMessageClicked: replyToMessage(replyUser, replyContent, eventId);
innerObject: [
TextDelegate {
Layout.fillWidth: true
Layout.rightMargin: Kirigami.Units.largeSpacing
TapHandler {
acceptedButtons: Qt.RightButton
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
}
]
Layout.rightMargin: Kirigami.Units.largeSpacing
Layout.leftMargin: Kirigami.Units.largeSpacing
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: openMessageContext(author, display, eventId, toolTip)
}
TapHandler {
acceptedButtons: Qt.LeftButton
onLongPressed: openMessageContext(author, display, eventId, toolTip)
}
}
}
}
@@ -347,16 +400,16 @@ Kirigami.ScrollablePage {
DelegateChoice {
roleValue: "notice"
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
onReplyClicked: goToEvent(eventID)
onReplyToMessageClicked: replyToMessage(replyUser, replyContent, eventId);
innerObject: TextDelegate {
Layout.fillWidth: true
}
Layout.rightMargin: Kirigami.Units.largeSpacing
Layout.leftMargin: Kirigami.Units.largeSpacing
}
}
}
@@ -364,26 +417,20 @@ Kirigami.ScrollablePage {
DelegateChoice {
roleValue: "image"
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
onReplyClicked: goToEvent(eventID)
onReplyToMessageClicked: replyToMessage(replyUser, replyContent, eventId);
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
}
]
Layout.preferredHeight: info.h / info.w * width
Layout.maximumHeight: Kirigami.Units.gridUnit * 15
Layout.maximumWidth: Kirigami.Units.gridUnit * 30
}
}
}
@@ -391,27 +438,20 @@ Kirigami.ScrollablePage {
DelegateChoice {
roleValue: "sticker"
delegate: TimelineContainer {
width: messageListView.width
width: messageListView.width - Kirigami.Units.largeSpacing
innerObject: MessageDelegate {
Layout.fillWidth: true
onReplyClicked: goToEvent(eventID)
onReplyToMessageClicked: replyToMessage(replyUser, replyContent, eventId);
isLoaded: timelineDelegateChooser.delegateLoaded
onReplyClicked: goToEvent(eventID)
onReplyToMessageClicked: replyToMessage(replyUser, replyContent, eventId);
innerObject: [
ImageDelegate {
readonly: true
Layout.maximumWidth: parent.width / 2
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
}
]
hoverComponent: hoverActions
cardBackground: false
innerObject: ImageDelegate {
readonly: true
Layout.maximumWidth: Kirigami.Units.gridUnit * 10
Layout.minimumWidth: Kirigami.Units.gridUnit * 10
Layout.preferredHeight: info.h / info.w * width
}
}
}
@@ -419,25 +459,23 @@ Kirigami.ScrollablePage {
DelegateChoice {
roleValue: "audio"
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
onReplyClicked: goToEvent(eventID)
mouseArea: MouseArea {
acceptedButtons: (Kirigami.Settings.isMobile ? Qt.LeftButton : 0) | Qt.RightButton
anchors.fill: parent
onClicked: {
if (mouse.button == Qt.RightButton) {
openMessageContext(author, display, eventId, toolTip);
}
}
onPressAndHold: openMessageContext(author, display, eventId, toolTip);
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: openFileContext(author, display, eventId, toolTip, progressInfo, parent)
}
onReplyToMessageClicked: replyToMessage(replyUser, replyContent, eventId);
innerObject: AudioDelegate {
Layout.fillWidth: true
TapHandler {
acceptedButtons: Qt.LeftButton
onLongPressed: openFileContext(author, display, eventId, toolTip, progressInfo, parent)
}
}
}
@@ -446,28 +484,29 @@ Kirigami.ScrollablePage {
DelegateChoice {
roleValue: "video"
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
onReplyClicked: goToEvent(eventID)
mouseArea: MouseArea {
acceptedButtons: (Kirigami.Settings.isMobile ? Qt.LeftButton : 0) | Qt.RightButton
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);
Layout.minimumWidth: Kirigami.Units.gridUnit * 10
Layout.maximumWidth: Kirigami.Units.gridUnit * 30
Layout.preferredHeight: content.info.h / content.info.w * width
Layout.maximumHeight: Kirigami.Units.gridUnit * 15
Layout.minimumHeight: Kirigami.Units.gridUnit * 5
innerObject: VideoDelegate {
Layout.maximumWidth: parent.width
Layout.minimumWidth: 320
Layout.maximumHeight: 320
Layout.preferredHeight: content.info.h / content.info.w * width
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: openFileContext(author, display, eventId, toolTip, progressInfo, parent)
}
TapHandler {
acceptedButtons: Qt.LeftButton
onLongPressed: openFileContext(author, display, eventId, toolTip, progressInfo, parent)
}
}
}
@@ -476,15 +515,23 @@ Kirigami.ScrollablePage {
DelegateChoice {
roleValue: "file"
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
onReplyClicked: goToEvent(eventID)
onReplyToMessageClicked: replyToMessage(replyUser, replyContent, eventId);
innerObject: FileDelegate {
Layout.fillWidth: true
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: openFileContext(author, display, eventId, toolTip, progressInfo, parent)
}
TapHandler {
acceptedButtons: Qt.LeftButton
onLongPressed: openFileContext(author, display, eventId, toolTip, progressInfo, parent)
}
}
}
@@ -583,6 +630,18 @@ Kirigami.ScrollablePage {
MessageSourceSheet {}
}
Component {
id: openFolderDialog
OpenFolderDialog {}
}
Component {
id: fileDelegateContextMenu
FileDelegateContextMenu {}
}
}
footer: ChatTextInput {
@@ -626,8 +685,6 @@ Kirigami.ScrollablePage {
}
}
Kirigami.Theme.colorSet: Kirigami.Theme.View
function goToEvent(eventID) {
messageListView.positionViewAtIndex(eventToIndex(eventID), ListView.Contain)
}
@@ -661,7 +718,56 @@ Kirigami.ScrollablePage {
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, {
'author': author,
'message': message,
@@ -671,17 +777,14 @@ Kirigami.ScrollablePage {
messageSourceSheet.createObject(page, {
'sourceText': toolTip,
}).open();
contextMenu.close();
});
contextMenu.reply.connect(function(replyUser, replyContent) {
replyToMessage(replyUser, replyContent, eventId);
contextMenu.close();
})
contextMenu.remove.connect(function() {
currentRoom.redactEvent(eventId);
contextMenu.close();
})
contextMenu.open()
contextMenu.open();
}
function replyToMessage(replyUser, replyContent, eventId) {

View File

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

View File

@@ -22,7 +22,6 @@
<file>imports/NeoChat/Component/Emoji/EmojiPicker.qml</file>
<file>imports/NeoChat/Component/Emoji/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/StateDelegate.qml</file>
<file>imports/NeoChat/Component/Timeline/TextDelegate.qml</file>