Make it possible to send images in threads

Title
This commit is contained in:
James Graham
2025-03-22 18:56:40 +00:00
parent 3b2a6800a0
commit 72edfe1112

View File

@@ -1,6 +1,7 @@
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com> // SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL // SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtCore
import QtQuick import QtQuick
import QtQuick.Controls as QQC2 import QtQuick.Controls as QQC2
import QtQuick.Layouts import QtQuick.Layouts
@@ -13,7 +14,7 @@ import org.kde.neochat.chatbar
/** /**
* @brief A component to show a chat bar in a message bubble. * @brief A component to show a chat bar in a message bubble.
*/ */
QQC2.TextArea { QQC2.Control {
id: root id: root
/** /**
@@ -22,126 +23,234 @@ QQC2.TextArea {
required property ChatBarCache chatBarCache required property ChatBarCache chatBarCache
onChatBarCacheChanged: documentHandler.chatBarCache = chatBarCache onChatBarCacheChanged: documentHandler.chatBarCache = chatBarCache
readonly property bool isBusy: root.Message.room && root.Message.room.hasFileUploading
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredWidth: textMetrics.advanceWidth + rightPadding + Kirigami.Units.smallSpacing + Kirigami.Units.gridUnit
Layout.maximumWidth: Message.maxContentWidth Layout.maximumWidth: Message.maxContentWidth
Layout.minimumHeight: chatButtons.height + topPadding + bottomPadding
Component.onCompleted: _private.updateText() contentItem: ColumnLayout {
Loader {
id: paneLoader
topPadding: Kirigami.Units.smallSpacing Layout.fillWidth: true
bottomPadding: Kirigami.Units.smallSpacing Layout.margins: Kirigami.Units.largeSpacing
rightPadding: chatButtons.width + chatButtons.anchors.rightMargin * 2 Layout.preferredHeight: active ? item.implicitHeight : 0
color: Kirigami.Theme.textColor active: visible
verticalAlignment: TextEdit.AlignVCenter visible: root.chatBarCache.replyId.length > 0 || root.chatBarCache.attachmentPath.length > 0
wrapMode: TextEdit.Wrap sourceComponent: root.chatBarCache.replyId.length > 0 ? replyPane : attachmentPane
onTextChanged: {
root.chatBarCache.text = text;
}
Keys.onEnterPressed: {
if (completionMenu.visible) {
completionMenu.complete();
} else if (event.modifiers & Qt.ShiftModifier) {
root.insert(cursorPosition, "\n");
} else {
_private.post();
} }
} RowLayout {
Keys.onReturnPressed: { Layout.fillWidth: true
if (completionMenu.visible) { spacing: 0
completionMenu.complete();
} else if (event.modifiers & Qt.ShiftModifier) {
root.insert(cursorPosition, "\n");
} else {
_private.post();
}
}
Keys.onTabPressed: {
if (completionMenu.visible) {
completionMenu.complete();
}
}
Keys.onPressed: event => {
if (event.key === Qt.Key_Up && completionMenu.visible) {
completionMenu.decrementIndex();
} else if (event.key === Qt.Key_Down && completionMenu.visible) {
completionMenu.incrementIndex();
}
}
/** QQC2.ScrollView {
* This is anchored like this so that control expands properly as the id: chatBarScrollView
* text grows in length. Layout.topMargin: Kirigami.Units.smallSpacing
*/ Layout.bottomMargin: Kirigami.Units.smallSpacing
RowLayout { Layout.leftMargin: Kirigami.Units.largeSpacing
id: chatButtons Layout.rightMargin: Kirigami.Units.largeSpacing
anchors.verticalCenter: root.verticalCenter
anchors.right: root.right Layout.fillWidth: true
anchors.rightMargin: Kirigami.Units.smallSpacing Layout.maximumHeight: Kirigami.Units.gridUnit * 8
spacing: 0
QQC2.ToolButton { QQC2.TextArea {
display: QQC2.AbstractButton.IconOnly id: textArea
action: Kirigami.Action { Component.onCompleted: _private.updateText()
text: root.chatBarCache.isEditing ? i18nc("@action:button", "Confirm edit") : i18nc("@action:button", "Post message in thread")
icon.name: "document-send" Layout.fillWidth: true
onTriggered: {
_private.post(); color: Kirigami.Theme.textColor
verticalAlignment: TextEdit.AlignVCenter
wrapMode: TextEdit.Wrap
onTextChanged: {
root.chatBarCache.text = text;
}
Keys.onEnterPressed: {
if (completionMenu.visible) {
completionMenu.complete();
} else if (event.modifiers & Qt.ShiftModifier) {
textArea.insert(cursorPosition, "\n");
} else {
_private.post();
}
}
Keys.onReturnPressed: {
if (completionMenu.visible) {
completionMenu.complete();
} else if (event.modifiers & Qt.ShiftModifier) {
textArea.insert(cursorPosition, "\n");
} else {
_private.post();
}
}
Keys.onTabPressed: {
if (completionMenu.visible) {
completionMenu.complete();
}
}
Keys.onPressed: event => {
if (event.key === Qt.Key_Up && completionMenu.visible) {
completionMenu.decrementIndex();
} else if (event.key === Qt.Key_Down && completionMenu.visible) {
completionMenu.incrementIndex();
}
}
CompletionMenu {
id: completionMenu
height: implicitHeight
y: -height - 5
z: 10
connection: root.Message.room.connection
chatDocumentHandler: documentHandler
Behavior on height {
NumberAnimation {
property: "height"
duration: Kirigami.Units.shortDuration
easing.type: Easing.OutCubic
}
}
}
// opt-out of whatever spell checker a styled TextArea might come with
Kirigami.SpellCheck.enabled: false
ChatDocumentHandler {
id: documentHandler
document: textArea.textDocument
cursorPosition: textArea.cursorPosition
selectionStart: textArea.selectionStart
selectionEnd: textArea.selectionEnd
room: root.Message.room // We don't care about saving for edits so this is OK.
mentionColor: Kirigami.Theme.linkColor
errorColor: Kirigami.Theme.negativeTextColor
}
TextMetrics {
id: textMetrics
text: textArea.text
}
Component {
id: openFileDialog
OpenFileDialog {
parentWindow: Window.window
currentFolder: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
}
}
Component {
id: attachDialog
AttachDialog {
anchors.centerIn: parent
}
}
background: null
} }
} }
QQC2.ToolTip.text: text PieProgressBar {
QQC2.ToolTip.visible: hovered visible: root.isBusy
progress: root.Message.room.fileUploadingProgress
}
QQC2.ToolButton {
visible: !root.isBusy
display: QQC2.AbstractButton.IconOnly
action: Kirigami.Action {
text: i18nc("@action:button", "Attach an image or file")
icon.name: "mail-attachment"
onTriggered: {
let dialog = (Clipboard.hasImage ? attachDialog : openFileDialog).createObject(QQC2.Overlay.overlay);
dialog.chosen.connect(path => root.chatBarCache.attachmentPath = path);
dialog.open();
}
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
}
QQC2.ToolButton {
display: QQC2.AbstractButton.IconOnly
action: Kirigami.Action {
text: root.chatBarCache.isEditing ? i18nc("@action:button", "Confirm edit") : i18nc("@action:button", "Post message in thread")
icon.name: "document-send"
onTriggered: {
_private.post();
}
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
}
QQC2.ToolButton {
display: QQC2.AbstractButton.IconOnly
action: Kirigami.Action {
text: i18nc("@action:button", "Cancel")
icon.name: "dialog-close"
onTriggered: {
root.chatBarCache.clearRelations();
}
shortcut: "Escape"
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
}
} }
QQC2.ToolButton { }
display: QQC2.AbstractButton.IconOnly
action: Kirigami.Action { background: Rectangle {
text: i18nc("@action:button", "Cancel") color: Kirigami.Theme.backgroundColor
radius: Kirigami.Units.cornerRadius
border {
width: 1
color: Kirigami.ColorUtils.linearInterpolation(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, Kirigami.Theme.frameContrast)
}
}
Component {
id: replyPane
Item {
implicitWidth: replyComponent.implicitWidth
implicitHeight: replyComponent.implicitHeight
ReplyComponent {
id: replyComponent
replyEventId: root.chatBarCache.replyId
replyAuthor: root.chatBarCache.relationAuthor
replyContentModel: root.chatBarCache.relationEventContentModel
Message.maxContentWidth: paneLoader.item.width
}
QQC2.Button {
id: cancelButton
anchors.top: parent.top
anchors.right: parent.right
display: QQC2.AbstractButton.IconOnly
text: i18nc("@action:button", "Cancel reply")
icon.name: "dialog-close" icon.name: "dialog-close"
onTriggered: { onClicked: {
root.chatBarCache.clearRelations(); root.chatBarCache.replyId = "";
root.chatBarCache.attachmentPath = "";
} }
shortcut: "Escape" QQC2.ToolTip.text: text
} QQC2.ToolTip.visible: hovered
QQC2.ToolTip.text: text QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
QQC2.ToolTip.visible: hovered
}
}
CompletionMenu {
id: completionMenu
height: implicitHeight
y: -height - 5
z: 10
connection: root.Message.room.connection
chatDocumentHandler: documentHandler
Behavior on height {
NumberAnimation {
property: "height"
duration: Kirigami.Units.shortDuration
easing.type: Easing.OutCubic
} }
} }
} }
Component {
id: attachmentPane
AttachmentPane {
attachmentPath: root.chatBarCache.attachmentPath
// opt-out of whatever spell checker a styled TextArea might come with onAttachmentCancelled: {
Kirigami.SpellCheck.enabled: false root.chatBarCache.attachmentPath = "";
root.forceActiveFocus();
ChatDocumentHandler { }
id: documentHandler }
document: root.textDocument
cursorPosition: root.cursorPosition
selectionStart: root.selectionStart
selectionEnd: root.selectionEnd
room: root.Message.room // We don't care about saving for edits so this is OK.
mentionColor: Kirigami.Theme.linkColor
errorColor: Kirigami.Theme.negativeTextColor
}
TextMetrics {
id: textMetrics
text: root.text
} }
QtObject { QtObject {
@@ -151,16 +260,16 @@ QQC2.TextArea {
// This could possibly be undefined due to some esoteric QtQuick issue. Referencing it somewhere in JS is enough. // This could possibly be undefined due to some esoteric QtQuick issue. Referencing it somewhere in JS is enough.
documentHandler.document; documentHandler.document;
if (chatBarCache?.isEditing && chatBarCache.relationMessage.length > 0) { if (chatBarCache?.isEditing && chatBarCache.relationMessage.length > 0) {
root.text = chatBarCache.relationMessage; textArea.text = chatBarCache.relationMessage;
root.chatBarCache.updateMentions(root.textDocument, documentHandler); root.chatBarCache.updateMentions(root.textDocument, documentHandler);
root.forceActiveFocus(); textArea.forceActiveFocus();
root.cursorPosition = root.text.length; textArea.cursorPosition = textArea.text.length;
} }
} }
function post() { function post() {
root.chatBarCache.postMessage(); root.chatBarCache.postMessage();
root.clear(); textArea.clear();
root.chatBarCache.clearRelations(); root.chatBarCache.clearRelations();
} }
} }