diff --git a/src/timeline/ChatBarComponent.qml b/src/timeline/ChatBarComponent.qml index 62aca5a88..a05929fa3 100644 --- a/src/timeline/ChatBarComponent.qml +++ b/src/timeline/ChatBarComponent.qml @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2023 James Graham // SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +import QtCore import QtQuick import QtQuick.Controls as QQC2 import QtQuick.Layouts @@ -13,7 +14,7 @@ import org.kde.neochat.chatbar /** * @brief A component to show a chat bar in a message bubble. */ -QQC2.TextArea { +QQC2.Control { id: root /** @@ -22,126 +23,234 @@ QQC2.TextArea { required property ChatBarCache chatBarCache onChatBarCacheChanged: documentHandler.chatBarCache = chatBarCache + readonly property bool isBusy: root.Message.room && root.Message.room.hasFileUploading + Layout.fillWidth: true - Layout.preferredWidth: textMetrics.advanceWidth + rightPadding + Kirigami.Units.smallSpacing + Kirigami.Units.gridUnit Layout.maximumWidth: Message.maxContentWidth - Layout.minimumHeight: chatButtons.height + topPadding + bottomPadding - Component.onCompleted: _private.updateText() + contentItem: ColumnLayout { + Loader { + id: paneLoader - topPadding: Kirigami.Units.smallSpacing - bottomPadding: Kirigami.Units.smallSpacing - rightPadding: chatButtons.width + chatButtons.anchors.rightMargin * 2 + Layout.fillWidth: true + Layout.margins: Kirigami.Units.largeSpacing + Layout.preferredHeight: active ? item.implicitHeight : 0 - 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) { - root.insert(cursorPosition, "\n"); - } else { - _private.post(); + active: visible + visible: root.chatBarCache.replyId.length > 0 || root.chatBarCache.attachmentPath.length > 0 + sourceComponent: root.chatBarCache.replyId.length > 0 ? replyPane : attachmentPane } - } - Keys.onReturnPressed: { - if (completionMenu.visible) { - 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(); - } - } + RowLayout { + Layout.fillWidth: true + spacing: 0 - /** - * This is anchored like this so that control expands properly as the - * text grows in length. - */ - RowLayout { - id: chatButtons - anchors.verticalCenter: root.verticalCenter - anchors.right: root.right - anchors.rightMargin: Kirigami.Units.smallSpacing - spacing: 0 - 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.ScrollView { + id: chatBarScrollView + Layout.topMargin: Kirigami.Units.smallSpacing + Layout.bottomMargin: Kirigami.Units.smallSpacing + Layout.leftMargin: Kirigami.Units.largeSpacing + Layout.rightMargin: Kirigami.Units.largeSpacing + + Layout.fillWidth: true + Layout.maximumHeight: Kirigami.Units.gridUnit * 8 + + QQC2.TextArea { + id: textArea + Component.onCompleted: _private.updateText() + + Layout.fillWidth: true + + 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 - QQC2.ToolTip.visible: hovered + PieProgressBar { + 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 { - text: i18nc("@action:button", "Cancel") + } + + background: Rectangle { + 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" - onTriggered: { - root.chatBarCache.clearRelations(); + onClicked: { + root.chatBarCache.replyId = ""; + root.chatBarCache.attachmentPath = ""; } - shortcut: "Escape" - } - QQC2.ToolTip.text: text - 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 + QQC2.ToolTip.text: text + QQC2.ToolTip.visible: hovered + QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay } } } + Component { + id: attachmentPane + AttachmentPane { + attachmentPath: root.chatBarCache.attachmentPath - // opt-out of whatever spell checker a styled TextArea might come with - Kirigami.SpellCheck.enabled: false - - 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 + onAttachmentCancelled: { + root.chatBarCache.attachmentPath = ""; + root.forceActiveFocus(); + } + } } 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. documentHandler.document; if (chatBarCache?.isEditing && chatBarCache.relationMessage.length > 0) { - root.text = chatBarCache.relationMessage; + textArea.text = chatBarCache.relationMessage; root.chatBarCache.updateMentions(root.textDocument, documentHandler); - root.forceActiveFocus(); - root.cursorPosition = root.text.length; + textArea.forceActiveFocus(); + textArea.cursorPosition = textArea.text.length; } } function post() { root.chatBarCache.postMessage(); - root.clear(); + textArea.clear(); root.chatBarCache.clearRelations(); } }