Skip to content
Snippets Groups Projects
MessageInput.qml 18.9 KiB
Newer Older
  • Learn to ignore specific revisions
  • // SPDX-FileCopyrightText: Nheko Contributors
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    // SPDX-License-Identifier: GPL-3.0-or-later
    
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    import "./emoji"
    
    trilene's avatar
    trilene committed
    import "./voip"
    
    import "./ui"
    
    import QtQuick 2.12
    
    import QtQuick.Controls 2.3
    
    import QtQuick.Layouts 1.2
    
    import QtQuick.Window 2.13
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    import im.nheko 1.0
    
    
        id: inputBar
    
    
        readonly property string text: messageInput.text
    
    
        color: Nheko.colors.window
    
        Layout.fillWidth: true
    
        Layout.preferredHeight: row.implicitHeight
    
        Layout.minimumHeight: 40
    
    Malte E's avatar
    Malte E committed
        property bool showAllButtons: width > 450 || (messageInput.length == 0 && !messageInput.inputMethodComposing)
    
    
    trilene's avatar
    trilene committed
        Component {
            id: placeCallDialog
    
            PlaceCall {
            }
    
        Component {
            id: screenShareDialog
    
            ScreenShare {
            }
    
        }
    
    
            visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false
    
            anchors.fill: parent
    
    Malte E's avatar
    Malte E committed
                visible: CallManager.callsSupported && showAllButtons
    
                opacity: (CallManager.haveCallInvite || CallManager.isOnCallOnOtherDevice) ? 0.3 : 1
    
                Layout.alignment: Qt.AlignBottom
                hoverEnabled: true
                width: 22
                height: 22
    
                image: CallManager.isOnCall ? ":/icons/icons/ui/end-call.svg" : ":/icons/icons/ui/place-call.svg"
    
                ToolTip.visible: hovered
    
                ToolTip.text: CallManager.isOnCall ? qsTr("Hang up") : (CallManager.isOnCallOnOtherDevice ? qsTr("Already on a call") : qsTr("Place a call"))
    
                Layout.margins: 8
    
    trilene's avatar
    trilene committed
                onClicked: {
    
    trilene's avatar
    trilene committed
                        if (CallManager.haveCallInvite) {
    
                            return ;
                        } else if (CallManager.isOnCall) {
    
    trilene's avatar
    trilene committed
                            CallManager.hangUp();
    
                        } 
                        else if(CallManager.isOnCallOnOtherDevice) {
                            return;
                        }
                        else {
    
    trilene's avatar
    trilene committed
                            var dialog = placeCallDialog.createObject(timelineRoot);
    
    trilene's avatar
    trilene committed
                            dialog.open();
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                            timelineRoot.destroyOnClose(dialog);
    
    Malte E's avatar
    Malte E committed
                visible: showAllButtons
    
                Layout.alignment: Qt.AlignBottom
                hoverEnabled: true
                width: 22
                height: 22
    
                image: ":/icons/icons/ui/attach.svg"
    
                Layout.margins: 8
    
                onClicked: room.input.openFileSelection()
    
                ToolTip.visible: hovered
                ToolTip.text: qsTr("Send a file")
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    
                Rectangle {
                    anchors.fill: parent
    
                    color: Nheko.colors.window
    
                    visible: room && room.input.uploading
    
                    Spinner {
                        anchors.centerIn: parent
                        height: parent.height / 2
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                        running: parent.visible
                    }
    
                }
    
    
            ScrollView {
    
                Layout.alignment: Qt.AlignVCenter
    
                Layout.maximumHeight: Window.height / 4
    
                Layout.minimumHeight: fontMetrics.lineSpacing
                Layout.preferredHeight: contentHeight
                Layout.fillWidth: true
    
                ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
    
                contentWidth: availableWidth
    
                    property int completerTriggeredAt: 0
    
                    function insertCompletion(completion) {
    
                        messageInput.remove(completerTriggeredAt, cursorPosition);
                        messageInput.insert(cursorPosition, completion);
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                    function openCompleter(pos, type) {
    
                        if (popup.opened) return;
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                        completerTriggeredAt = pos;
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                        completer.completerName = type;
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                        popup.open();
    
                        completer.completer.setSearchString(messageInput.getText(completerTriggeredAt, cursorPosition)+messageInput.preeditText);
    
                    function positionCursorAtEnd() {
                        cursorPosition = messageInput.length;
                    }
    
                    function positionCursorAtStart() {
                        cursorPosition = 0;
                    }
    
    
                    selectByMouse: true
    
                    placeholderText: qsTr("Write a message...")
    
                    placeholderTextColor: Nheko.colors.buttonText
                    color: Nheko.colors.text
    
                    width: textInput.width 
    
                    verticalAlignment: TextEdit.AlignVCenter
    
                    wrapMode: TextEdit.Wrap
    
                    padding: 0
                    topPadding: 8
                    bottomPadding: 8
    
    Malte E's avatar
    Malte E committed
                    leftPadding: inputBar.showAllButtons? 0 : 8
    
                    property string lastChar
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                    onTextChanged: {
    
                        if (room)
                            room.input.updateState(selectionStart, selectionEnd, cursorPosition, text);
    
                        forceActiveFocus();
    
                        if (cursorPosition > 0)
                            lastChar = text.charAt(cursorPosition-1)
                        else
                            lastChar = ''
                        if (lastChar == '@') {
    
                            messageInput.openCompleter(selectionStart-1, "user");
    
                        } else if (lastChar == ':') {
    
                            messageInput.openCompleter(selectionStart-1, "emoji");
    
                        } else if (lastChar == '#') {
    
                            messageInput.openCompleter(selectionStart-1, "roomAliases");
    
                        } else if (lastChar == "~") {
    
                            messageInput.openCompleter(selectionStart-1, "customEmoji");
    
                        } else if (lastChar == "/" && cursorPosition == 1) {
                            messageInput.openCompleter(selectionStart-1, "command");
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                    }
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                    onCursorPositionChanged: {
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                            return ;
    
    
                        room.input.updateState(selectionStart, selectionEnd, cursorPosition, text);
    
                        if (popup.opened && cursorPosition <= completerTriggeredAt)
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                            popup.close();
    
    Malte E's avatar
    Malte E committed
                            completer.completer.setSearchString(messageInput.getText(completerTriggeredAt, cursorPosition)+messageInput.preeditText);
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                    }
    
    Malte E's avatar
    Malte E committed
                    onPreeditTextChanged: {
                        if (popup.opened)
                            completer.completer.setSearchString(messageInput.getText(completerTriggeredAt, cursorPosition)+messageInput.preeditText);
                    }
    
                    onSelectionStartChanged: room.input.updateState(selectionStart, selectionEnd, cursorPosition, text)
                    onSelectionEndChanged: room.input.updateState(selectionStart, selectionEnd, cursorPosition, text)
    
                    // Ensure that we get escape key press events first.
    
                    Keys.onShortcutOverride: event.accepted = (popup.opened && (event.key === Qt.Key_Escape || event.key === Qt.Key_Tab || event.key === Qt.Key_Enter || event.key === Qt.Key_Space))
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                    Keys.onPressed: {
                        if (event.matches(StandardKey.Paste)) {
    
                            event.accepted = room.input.tryPasteAttachment(false);
    
                        } else if (event.key == Qt.Key_Space) {
    
                            // close popup if user enters space after colon
    
                            if (cursorPosition == completerTriggeredAt + 1)
    
                            if (popup.opened && completer.count <= 0)
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                        } else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_U) {
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                        } else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_P) {
    
                            messageInput.text = room.input.previousText();
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                        } else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_N) {
    
                            messageInput.text = room.input.nextText();
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                        } else if (event.key == Qt.Key_Escape && popup.opened) {
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                            completer.completerName = "";
    
                            popup.close();
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                            event.accepted = true;
    
                        } else if (event.matches(StandardKey.SelectAll) && popup.opened) {
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                            completer.completerName = "";
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                            popup.close();
    
                        } else if (event.matches(StandardKey.InsertLineSeparator)) {
                            if (popup.opened) popup.close();
    
                            if (Settings.invertEnterKey && (!Qt.inputMethod.visible || Qt.platform.os === "windows")) {
    
    LordMZTE's avatar
    LordMZTE committed
                                room.input.send();
                                event.accepted = true;
                            }
    
                        } else if (event.matches(StandardKey.InsertParagraphSeparator)) {
                            if (popup.opened) {
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                                var currentCompletion = completer.currentCompletion();
                                completer.completerName = "";
    
                                popup.close();
                                if (currentCompletion) {
    
                                    messageInput.insertCompletion(currentCompletion);
    
                                    event.accepted = true;
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                            }
    
                            if (!Settings.invertEnterKey && (!Qt.inputMethod.visible || Qt.platform.os === "windows")) {
    
                                room.input.send();
                                event.accepted = true;
                            }
    
                        } else if (event.key == Qt.Key_Tab && (event.modifiers == Qt.NoModifier || event.modifiers == Qt.ShiftModifier)) {
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                            event.accepted = true;
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                            if (popup.opened) {
    
                                if (event.modifiers & Qt.ShiftModifier)
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                                    completer.down();
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                                    completer.up();
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                            } else {
                                var pos = cursorPosition - 1;
                                while (pos > -1) {
    
                                    var t = messageInput.getText(pos, pos + 1);
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                                    console.log('"' + t + '"');
    
                                        messageInput.openCompleter(pos, "user");
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                                        return ;
    
                                    } else if (t == ' ' || t == '\t') {
                                        messageInput.openCompleter(pos + 1, "user");
                                        return ;
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                                    } else if (t == ':') {
    
                                        messageInput.openCompleter(pos, "emoji");
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                                        return ;
    
                                    } else if (t == '~') {
                                        messageInput.openCompleter(pos, "customEmoji");
                                        return ;
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                                    }
                                    pos = pos - 1;
                                }
                                // At start of input
    
                                messageInput.openCompleter(0, "user");
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                        } else if (event.key == Qt.Key_Up && popup.opened) {
                            event.accepted = true;
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                            completer.up();
    
                        } else if ((event.key == Qt.Key_Down || event.key == Qt.Key_Backtab) && popup.opened) {
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                            event.accepted = true;
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                            completer.down();
    
                        } else if (event.key == Qt.Key_Up && event.modifiers == Qt.NoModifier) {
    
                            if (cursorPosition == 0) {
                                event.accepted = true;
    
                                var idx = room.edit ? room.idToIndex(room.edit) + 1 : 0;
    
                                while (true) {
    
                                    var id = room.indexToId(idx);
                                    if (!id || room.getDump(id, "").isEditable) {
                                        room.edit = id;
    
                                        cursorPosition = 0;
    
                                        Qt.callLater(positionCursorAtEnd);
    
                            } else if (positionAt(0, cursorRectangle.y + cursorRectangle.height / 2) === 0) {
    
                                event.accepted = true;
    
                                positionCursorAtStart();
    
                        } else if (event.key == Qt.Key_Down && event.modifiers == Qt.NoModifier) {
    
                            if (cursorPosition == messageInput.length && room.edit) {
    
                                event.accepted = true;
    
                                var idx = room.idToIndex(room.edit) - 1;
    
                                while (true) {
    
                                    var id = room.indexToId(idx);
                                    if (!id || room.getDump(id, "").isEditable) {
                                        room.edit = id;
    
                                        Qt.callLater(positionCursorAtStart);
    
                            } else if (positionAt(width, cursorRectangle.y + cursorRectangle.height / 2) === messageInput.length) {
    
                                event.accepted = true;
                                positionCursorAtEnd();
    
                    background: null
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                    Connections {
    
                        function onRoomChanged() {
    
                            if (room)
    
                                messageInput.append(room.input.text);
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                            completer.completerName = "";
    
                            messageInput.forceActiveFocus();
    
                        target: timelineView
    
    
                    Connections {
    
                        function onCompletionClicked(completion) {
                            messageInput.insertCompletion(completion);
                        }
    
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                        target: completer
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                    Popup {
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                        id: popup
    
    
                        x: messageInput.positionToRectangle(messageInput.completerTriggeredAt).x
                        y: messageInput.positionToRectangle(messageInput.completerTriggeredAt).y - height
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                        background: null
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    
                        Completer {
                            anchors.fill: parent
                            id: completer
    
                            rowMargin: 2
                            rowSpacing: 0
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                        }
    
                        enter: Transition {
                            NumberAnimation {
                                property: "opacity"
                                from: 0
                                to: 1
                                duration: 100
                            }
    
                        }
    
                        exit: Transition {
                            NumberAnimation {
                                property: "opacity"
                                from: 1
                                to: 0
                                duration: 100
                            }
                        }
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                    }
    
                    Connections {
    
                        function onTextChanged(newText) {
    
                            messageInput.text = newText;
                            messageInput.cursorPosition = newText.length;
                        }
    
    
                        ignoreUnknownSignals: true
    
                        target: room ? room.input : null
    
                        function onReplyChanged() {
                            messageInput.forceActiveFocus();
                        }
    
                        function onEditChanged() {
                            messageInput.forceActiveFocus();
                        }
    
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                        function onThreadChanged() {
                            messageInput.forceActiveFocus();
                        }
    
    
                        ignoreUnknownSignals: true
    
                        function onFocusInput() {
                            messageInput.forceActiveFocus();
                        }
    
    
                    MouseArea {
                        // workaround for wrong cursor shape on some platforms
                        anchors.fill: parent
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                        acceptedButtons: Qt.MiddleButton
    
                        cursorShape: Qt.IBeamCursor
    
                        onPressed: (mouse) => mouse.accepted = room.input.tryPasteAttachment(true)
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            ImageButton {
                id: stickerButton
    
    Malte E's avatar
    Malte E committed
                visible: showAllButtons
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    
                Layout.alignment: Qt.AlignRight | Qt.AlignBottom
                Layout.margins: 8
                hoverEnabled: true
                width: 22
                height: 22
                image: ":/icons/icons/ui/sticky-note-solid.svg"
                ToolTip.visible: hovered
                ToolTip.text: qsTr("Stickers")
    
    Loren Burkholder's avatar
    Loren Burkholder committed
                onClicked: stickerPopup.visible ? stickerPopup.close() : stickerPopup.show(stickerButton, room.roomId, function(row) {
    
                    room.input.sticker(row);
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                    TimelineManager.focusMessageInput();
                })
    
                StickerPicker {
                    id: stickerPopup
    
                    colors: Nheko.colors
                }
    
            }
    
    
                id: emojiButton
    
    
                Layout.alignment: Qt.AlignRight | Qt.AlignBottom
    
                Layout.margins: 8
    
                hoverEnabled: true
                width: 22
                height: 22
    
                image: ":/icons/icons/ui/smile.svg"
    
                ToolTip.visible: hovered
                ToolTip.text: qsTr("Emoji")
    
                onClicked: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(emojiButton, function(emoji) {
    
                    messageInput.insert(messageInput.cursorPosition, emoji);
    
                    TimelineManager.focusMessageInput();
    
            }
    
            ImageButton {
                Layout.alignment: Qt.AlignRight | Qt.AlignBottom
    
                Layout.margins: 8
    
                hoverEnabled: true
                width: 22
                height: 22
    
                image: ":/icons/icons/ui/send.svg"
    
    Malte E's avatar
    Malte E committed
                Layout.rightMargin: 8
    
                ToolTip.visible: hovered
                ToolTip.text: qsTr("Send")
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                onClicked: {
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                }
    
        Text {
            anchors.centerIn: parent
    
            visible: room ? (!room.permissions.canSend(MtxEvent.TextMessage)) : false
    
            text: qsTr("You don't have permission to send messages in this room")
    
            color: Nheko.colors.text