import QtGraphicalEffects 1.0 import QtQuick 2.7 import QtQuick.Controls 2.4 import QtQuick.Controls.Material 2.0 import QtQuick.Layouts 1.3 import "../widgets" import "../widgets/controls" as Awesome import "../fonts/Twemoji.js" as T import "../utils.js" as Utils ColumnLayout { Layout.fillWidth: true Flickable { // THE MESSAGE LIST ITSELF id: sv clip: true Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillHeight: true Layout.fillWidth: true contentWidth: colMessages.width contentHeight: colMessages.height boundsBehavior: Flickable.StopAtBounds maximumFlickVelocity: 800 Connections { target: gcd onClearMessages: function() { messagesModel.clear() txtMessage.text = "" } onAppendMessage: function(handle, from, displayName, message, image, mid, fromMe, ts, ackd) { var msg try { msg = JSON.parse(message) } catch (e) { msg = {"o": 1, "d": "(legacy message type) " + message} } if (msg.o != 1) return messagesModel.append({ "_handle": handle, "_from": from, "_displayName": displayName, "_message":parse(msg.d, 12), "_image": image, "_mid": mid, "_fromMe": fromMe, "_ts": ts, "_ackd": ackd, }) // If the window is out of focus, alert the user (makes taskbar light up) windowItem.alert(0) if (sv.contentY + sv.height >= sv.contentHeight - colMessages.height && sv.contentHeight > sv.height) { sv.contentY = sv.contentHeight - sv.height } } } ScrollBar.vertical: ScrollBar{ policy: ScrollBar.AlwaysOn } ColumnLayout { id: colMessages width: sv.width ListModel { // MESSAGE OBJECTS ARE STORED HERE ... id: messagesModel } Item { height: 6 } Repeater { // ... AND DISPLAYED HERE model: messagesModel delegate: Message { handle: _handle from: _from displayName: _displayName message: _message image: _image messageID: _mid fromMe: _fromMe timestamp: _ts ackd: _ackd } } } } RowLayout { // THE BOTTOM DRAWER Rectangle { // MESSAGE ENTRY TEXTFIELD id: rectMessage Layout.fillWidth: true Layout.minimumHeight: 40 * gcd.themeScale Layout.maximumHeight: 40 * gcd.themeScale color: "#EDEDED" border.color: "#AAAAAA" radius: 10 Flickable { id: flkMessage anchors.fill: parent//this does nothing! bug in qt Layout.minimumWidth: parent.width Layout.maximumWidth: parent.width Layout.minimumHeight: rectMessage.height Layout.maximumHeight: rectMessage.height contentWidth: txtMessage.width contentHeight: txtMessage.height boundsBehavior: Flickable.StopAtBounds clip:true maximumFlickVelocity: 300 ScrollBar.vertical: ScrollBar{} TextEdit { id: txtMessage font.pixelSize: 10 text: "" padding: 6 wrapMode: TextEdit.Wrap textFormat: Text.RichText width: rectMessage.width property bool skipOneUpdate: false Keys.onReturnPressed: { // CTRL+ENTER = LINEBREAK if (event.modifiers & Qt.ControlModifier) { txtMessage.insert(txtMessage.cursorPosition, "
") } else if (event.modifiers == Qt.NoModifier) { btnSend.clicked() } } // welcome to the emoji parser! it is horrifying code that needs to leave in // while also stripping any other tag, including other images. // TODO: this probably breaks if people actually do want to paste html onTextChanged: { //console.log("onTextChanged()") // we're taking advantage of TextEdit.getText()'s parsing capability, which means occasionally // passing text into it to be filtered. this prevents recursive calls putting us into an // infinite loop if (skipOneUpdate) { skipOneUpdate = false return } //console.log("1: " + txtMessage.getText(0, txtMessage.text.length)) // convert tags back to their emoji form // Then parse out the rest of the HTML var nt = restoreEmoji(txtMessage.text) if (nt != txtMessage.text) { skipOneUpdate = true txtMessage.text = nt } //console.log("2: " + txtMessage.getText(0, txtMessage.text.length)) // strip all HTML tags var theText = Utils.htmlEscaped(txtMessage.getText(0, txtMessage.text.length)) //console.log("3: " + theText) // convert emoji back to tags nt = parse(theText, 10) //console.log("4: " + nt) // first we need to update the cursor position to be the same distance from the end var oldcursor = txtMessage.cursorPosition var oldlen = txtMessage.getText(0, txtMessage.text.length).length // then we actually put the updated text in skipOneUpdate = true txtMessage.text = nt // and then restore the cursor var newlen = txtMessage.getText(0, txtMessage.text.length).length txtMessage.cursorPosition = newlen - (oldlen - oldcursor) // autoscroll down only when the scrollbar is already all the way down if (flkMessage.contentY + flkMessage.height >= flkMessage.contentHeight - txtMessage.height && flkMessage.contentHeight > flkMessage.height) { flkMessage.contentY = flkMessage.contentHeight - flkMessage.height } } } } MouseArea { anchors.fill: parent onClicked: txtMessage.focus = true } } ColumnLayout { id: colRight spacing: 1 SimpleButton { // SEND MESSAGE BUTTON id: btnSend icon: "regular/paper-plane" text: "send" Layout.minimumWidth: btnEmoji.width + btnAttach.width + 2 Layout.maximumWidth: btnEmoji.width + btnAttach.width + 2 anchors.right: parent.right anchors.rightMargin: 2 property int nextMessageID: 1 TextEdit { id: txtHidden visible: false textFormat: Text.RichText } onClicked: { if (txtMessage.text != "") { txtHidden.text = restoreEmoji(txtMessage.text) var txt = txtHidden.getText(0, txtHidden.text.length).trim() if (txt.length > 0) { var msg = JSON.stringify({"o":1, "d":txtHidden.getText(0, txtHidden.text.length)}) gcd.sendMessage(msg, nextMessageID++) } } txtMessage.text = "" } } RowLayout { spacing: 1 SimpleButton { // EMOJI DRAWER BUTTON id: btnEmoji icon: "regular/smile" anchors.right: btnAttach.left anchors.rightMargin: 2 onClicked: gcd.popup("emoji not yet implemented, sorry") } SimpleButton { id: btnAttach icon: "solid/paperclip" anchors.right: parent.right anchors.rightMargin: 2 onClicked: { gcd.popup("attachments not yet implemented, sorry") } } } } } }