/** * SPDX-FileCopyrightText: 2020 Carl Schwan * * SPDX-License-Identifier: GPL-2.0-or-later */ import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import org.kde.kirigami 2.13 as Kirigami import NeoChat.Component 1.0 import NeoChat.Component.Emoji 1.0 import NeoChat.Dialog 1.0 import NeoChat.Effect 1.0 import org.kde.neochat 1.0 ToolBar { id: root property alias isReply: replyItem.visible property bool isReaction: false property var replyUser property string replyEventID property string replyContent property alias isAutoCompleting: autoCompleteListView.visible property var autoCompleteModel property int autoCompleteBeginPosition property int autoCompleteEndPosition property bool hasAttachment: false property url attachmentPath position: ToolBar.Footer contentItem: ColumnLayout { id: layout spacing: 0 EmojiPicker { id: emojiPicker Layout.fillWidth: true visible: false textArea: inputField emojiModel: EmojiModel { id: emojiModel } onChosen: { textArea.insert(textArea.cursorPosition, emoji); textArea.forceActiveFocus(); } } RowLayout { Layout.fillWidth: true Layout.margins: 8 id: replyItem visible: false spacing: 8 Control { Layout.alignment: Qt.AlignTop padding: 4 contentItem: RowLayout { Kirigami.Avatar { Layout.preferredWidth: Kirigami.Units.gridUnit Layout.preferredHeight: Kirigami.Units.gridUnit source: replyUser ? "image://mxc/" + replyUser.avatarMediaId: "" name: replyUser ? replyUser.displayName : i18n("No name") } Label { Layout.alignment: Qt.AlignVCenter text: replyUser ? replyUser.displayName : i18n("No name") rightPadding: 8 } } } TextEdit { Layout.fillWidth: true text: "" + replyContent color: Kirigami.Theme.textColor selectByMouse: true readOnly: true wrapMode: Label.Wrap selectedTextColor: "white" textFormat: Text.RichText } } ListView { Layout.fillWidth: true Layout.preferredHeight: 36 Layout.margins: 8 id: autoCompleteListView visible: false model: autoCompleteModel clip: true spacing: 4 orientation: ListView.Horizontal highlightFollowsCurrentItem: true keyNavigationWraps: true delegate: Control { property string autoCompleteText: modelData.displayName ?? modelData.unicode property bool isEmoji: modelData.unicode != null readonly property bool highlighted: autoCompleteListView.currentIndex == index padding: Kirigami.Units.smallSpacing contentItem: RowLayout { spacing: Kirigami.Units.largeSpacing Label { width: Kirigami.Units.gridUnit height: Kirigami.Units.gridUnit visible: isEmoji text: autoCompleteText font.family: "Emoji" font.pointSize: 20 verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter } Kirigami.Avatar { Layout.preferredWidth: Kirigami.Units.gridUnit Layout.preferredHeight: Kirigami.Units.gridUnit source: modelData.avatarMediaId ? "image://mxc/" + modelData.avatarMediaId : "" color: modelData.color ? Qt.darker(modelData.color, 1.1) : null visible: !isEmoji } Label { Layout.fillHeight: true visible: !isEmoji text: autoCompleteText color: highlighted ? Kirigami.Theme.highlightTextColor : Kirigami.Theme.textColor font.underline: highlighted verticalAlignment: Text.AlignVCenter rightPadding: Kirigami.Units.largeSpacing } } MouseArea { anchors.fill: parent onClicked: { autoCompleteListView.currentIndex = index inputField.replaceAutoComplete(autoCompleteText) } } } } Kirigami.Separator { Layout.fillWidth: true Layout.preferredHeight: 1 visible: emojiPicker.visible || replyItem.visible || autoCompleteListView.visible } RowLayout { Layout.fillWidth: true spacing: 0 //Kirigami.Units.smallSpacing Button { id: cancelReplyButton visible: isReply icon.name: "dialog-cancel" onClicked: clearReply() } Control { Layout.margins: 6 Layout.preferredHeight: 36 Layout.alignment: Qt.AlignVCenter visible: hasAttachment rightPadding: 8 contentItem: RowLayout { spacing: 0 ToolButton { Layout.preferredWidth: height Layout.fillHeight: true id: cancelAttachmentButton icon.name: "dialog-cancel" onClicked: { hasAttachment = false; attachmentPath = ""; } } Label { Layout.alignment: Qt.AlignVCenter text: attachmentPath !== "" ? attachmentPath.toString().substring(attachmentPath.toString().lastIndexOf('/') + 1, attachmentPath.length) : "" } } } TextArea { property real progress: 0 property bool autoAppeared: false; Layout.fillWidth: true id: inputField wrapMode: Text.Wrap placeholderText: i18n("Write your message...") topPadding: 0 bottomPadding: 0 leftPadding: Kirigami.Units.smallSpacing selectByMouse: true verticalAlignment: TextEdit.AlignVCenter text: currentRoom != null ? currentRoom.cachedInput : "" background: Item {} Rectangle { width: currentRoom && currentRoom.hasFileUploading ? parent.width * currentRoom.fileUploadingProgress / 100 : 0 height: parent.height opacity: 0.2 } Timer { id: timeoutTimer repeat: false interval: 2000 onTriggered: { repeatTimer.stop() currentRoom.sendTypingNotification(false) } } Timer { id: repeatTimer repeat: true interval: 5000 triggeredOnStart: true onTriggered: currentRoom.sendTypingNotification(true) } Keys.onReturnPressed: { if (event.modifiers & Qt.ShiftModifier) { insert(cursorPosition, "\n") } else { postMessage(text) text = "" clearReply() closeAll() } } Keys.onEscapePressed: closeAll() Keys.onBacktabPressed: if (isAutoCompleting) autoCompleteListView.decrementCurrentIndex() Keys.onTabPressed: { if (isAutoCompleting && autoAppeared === false) { autoAppeared = false; autoCompleteListView.incrementCurrentIndex() } else { autoAppeared = false; autoCompleteBeginPosition = text.substring(0, cursorPosition).lastIndexOf(" ") + 1 let autoCompletePrefix = text.substring(0, cursorPosition).split(" ").pop() if (!autoCompletePrefix) return if (autoCompletePrefix.startsWith(":")) { autoCompleteBeginPosition = text.substring(0, cursorPosition).lastIndexOf(" ") + 1 autoCompleteModel = emojiModel.filterModel(autoCompletePrefix) } else { autoCompleteModel = currentRoom.getUsers(autoCompletePrefix) } if (autoCompleteModel.length === 0) return isAutoCompleting = true autoCompleteEndPosition = cursorPosition } replaceAutoComplete(autoCompleteListView.currentItem.autoCompleteText) } onTextChanged: { timeoutTimer.restart() repeatTimer.start() currentRoom.cachedInput = text autoAppeared = false; if (cursorPosition !== autoCompleteBeginPosition && cursorPosition !== autoCompleteEndPosition) { isAutoCompleting = false; autoCompleteListView.currentIndex = 0; } let autoCompletePrefix = text.substring(0, cursorPosition).split(" ").pop(); if (!autoCompletePrefix) { return; } if (autoCompletePrefix.startsWith("@") || autoCompletePrefix.startsWith(":")) { if (autoCompletePrefix.startsWith("@")) { autoCompletePrefix = autoCompletePrefix.substring(1); autoCompleteBeginPosition = text.substring(0, cursorPosition).lastIndexOf(" ") + 1 // 1 = space autoCompleteModel = currentRoom.getUsers(autoCompletePrefix); } else { autoCompleteModel = emojiModel.filterModel(autoCompletePrefix); } if (autoCompleteModel.length === 0) { return; } isAutoCompleting = true autoAppeared = true; autoCompleteEndPosition = cursorPosition } } function replaceAutoComplete(word) { remove(autoCompleteBeginPosition, autoCompleteEndPosition) autoCompleteEndPosition = autoCompleteBeginPosition + word.length insert(cursorPosition, word) } function postMessage(text) { if(!currentRoom) { return } if (hasAttachment) { currentRoom.uploadFile(attachmentPath, text) clearAttachment() return } if (text.trim().length === 0) { return } var PREFIX_ME = '/me ' var PREFIX_NOTICE = '/notice ' var PREFIX_RAINBOW = '/rainbow ' var messageEventType = RoomMessageEvent.Text if (text.indexOf(PREFIX_RAINBOW) === 0) { text = text.substr(PREFIX_RAINBOW.length) var parsedText = "" var rainbowColor = ["#ff2b00", "#ff5500", "#ff8000", "#ffaa00", "#ffd500", "#ffff00", "#d4ff00", "#aaff00", "#80ff00", "#55ff00", "#2bff00", "#00ff00", "#00ff2b", "#00ff55", "#00ff80", "#00ffaa", "#00ffd5", "#00ffff", "#00d4ff", "#00aaff", "#007fff", "#0055ff", "#002bff", "#0000ff", "#2a00ff", "#5500ff", "#7f00ff", "#aa00ff", "#d400ff", "#ff00ff", "#ff00d4", "#ff00aa", "#ff0080", "#ff0055", "#ff002b", "#ff0000"] for (var i = 0; i < text.length; i++) { parsedText = parsedText + "" + text.charAt(i) + "" } currentRoom.postHtmlMessage(text, parsedText, RoomMessageEvent.Text, replyEventID) return } if (text.indexOf(PREFIX_ME) === 0) { text = text.substr(PREFIX_ME.length) messageEventType = RoomMessageEvent.Emote } else if (text.indexOf(PREFIX_NOTICE) === 0) { text = text.substr(PREFIX_NOTICE.length) messageEventType = RoomMessageEvent.Notice } currentRoom.postArbitaryMessage(text, messageEventType, replyEventID) } } ToolButton { id: emojiButton icon.name: "preferences-desktop-emoticons" icon.color: "transparent" checkable: true checked: emojiPicker.visible onToggled: emojiPicker.visible = !emojiPicker.visible ToolTip { text: i18n("Add an Emoji") } } ToolButton { id: uploadButton visible: !isReply && !hasAttachment icon.name: "mail-attachment" onClicked: { if (Clipboard.hasImage) { attachDialog.open() } else { var fileDialog = openFileDialog.createObject(ApplicationWindow.overlay) fileDialog.chosen.connect(function(path) { if (!path) return root.attach(path) }) fileDialog.open() } } ToolTip { text: i18n("Attach an image or file") } BusyIndicator { anchors.fill: parent running: currentRoom && currentRoom.hasFileUploading } } ToolButton { icon.name: "document-send" icon.color: "transparent" onClicked: { inputField.postMessage(inputField.text) inputField.text = "" root.clearReply() root.closeAll() } ToolTip { text: i18n("Send message") } } } } background: Rectangle { implicitHeight: 40 color: Kirigami.Theme.backgroundColor Kirigami.Separator { anchors { left: parent.left right: parent.right top: parent.top } } } function insert(str) { inputField.insert(inputField.cursorPosition, str) } function clear() { inputField.clear() } function clearReply() { isReply = false replyUser = null; replyContent = ""; replyEventID = "" } function focus() { inputField.forceActiveFocus() } function closeAll() { replyItem.visible = false autoCompleteListView.visible = false emojiPicker.visible = false } function attach(localPath) { hasAttachment = true attachmentPath = localPath } function clearAttachment() { hasAttachment = false attachmentPath = "" } }