import QtQuick 2.9 import QtQuick.Controls 2.2 import QtQuick.Layouts 1.3 import QtQuick.Controls.Material 2.2 import Spectral.Component 2.0 import Spectral.Component.Emoji 2.0 import Spectral.Effect 2.0 import Spectral.Setting 0.1 import Spectral 0.1 import "qrc:/js/md.js" as Markdown Rectangle { property bool isReply property string replyUserID property string replyEventID property string replyContent property bool isEmojiAutoCompleting property bool isUserAutoCompleting property var userAutoCompleteModel property var emojiAutoCompleteModel property int autoCompleteBeginPosition property int autoCompleteEndPosition color: MSettings.darkTheme ? "#303030" : "#fafafa" layer.enabled: true layer.effect: ElevationEffect { elevation: 2 } Popup { x: 0 y: -height - 10 width: Math.min(userAutoCompleteListView.contentWidth, parent.width) height: 36 padding: 0 Material.elevation: 2 id: userAutoComplete visible: isUserAutoCompleting && userAutoCompleteModel.length !== 0 contentItem: ListView { id: userAutoCompleteListView model: userAutoCompleteModel clip: true orientation: ListView.Horizontal highlightFollowsCurrentItem: true highlight: Rectangle { color: Material.accent opacity: 0.4 } delegate: ItemDelegate { property string displayName: modelData.displayName height: parent.height padding: 4 contentItem: Row { spacing: 8 ImageItem { width: parent.height height: parent.height image: modelData.avatar } Label { height: parent.height text: modelData.displayName verticalAlignment: Text.AlignVCenter } } text: modelData.displayName onClicked: { userAutoCompleteListView.currentIndex = index inputField.replaceAutoComplete(displayName) } } } } Popup { x: 0 y: -height - 10 width: Math.min(emojiAutoCompleteListView.contentWidth, parent.width) height: 36 padding: 0 Material.elevation: 2 id: emojiAutoComplete visible: isEmojiAutoCompleting && emojiAutoCompleteModel.length !== 0 contentItem: ListView { id: emojiAutoCompleteListView model: emojiAutoCompleteModel clip: true orientation: ListView.Horizontal highlightFollowsCurrentItem: true highlight: Rectangle { color: Material.accent opacity: 0.4 } delegate: ItemDelegate { property string unicode: modelData.unicode width: 36 height: 36 contentItem: Text { horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter font.pointSize: 20 font.family: "Emoji" text: modelData.unicode } onClicked: { emojiAutoCompleteListView.currentIndex = index inputField.replaceAutoComplete(modelData.unicode) } } } } Rectangle { width: currentRoom && currentRoom.hasFileUploading ? parent.width * currentRoom.fileUploadingProgress / 100 : 0 height: parent.height opacity: 0.2 color: Material.accent } RowLayout { anchors.fill: parent spacing: 0 ItemDelegate { Layout.preferredWidth: 48 Layout.preferredHeight: 48 id: uploadButton visible: !isReply contentItem: MaterialIcon { icon: "\ue226" } onClicked: currentRoom.chooseAndUploadFile() BusyIndicator { anchors.fill: parent running: currentRoom && currentRoom.hasFileUploading } } ItemDelegate { Layout.preferredWidth: 48 Layout.preferredHeight: 48 id: cancelReplyButton visible: isReply contentItem: MaterialIcon { icon: "\ue5cd" } onClicked: clearReply() } ScrollView { Layout.fillWidth: true Layout.preferredHeight: 48 ScrollBar.horizontal.policy: ScrollBar.AlwaysOff clip: true TextArea { property real progress: 0 id: inputField wrapMode: Text.Wrap placeholderText: isReply ? "Reply to " + replyUserID : "Send a Message" leftPadding: 16 topPadding: 0 bottomPadding: 0 selectByMouse: true verticalAlignment: TextEdit.AlignVCenter text: currentRoom ? currentRoom.cachedInput : "" background: Item { } 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) } ToolTip.visible: currentRoom && currentRoom.hasUsersTyping ToolTip.text: currentRoom ? currentRoom.usersTyping : "" Keys.onReturnPressed: { if (event.modifiers & Qt.ShiftModifier) { insert(cursorPosition, "\n") } else { postMessage(text) text = "" } } Keys.onBacktabPressed: { if (isUserAutoCompleting) { if (userAutoCompleteListView.currentIndex == 0) userAutoCompleteListView.currentIndex = userAutoCompleteListView.count - 1 else userAutoCompleteListView.currentIndex-- } if (isEmojiAutoCompleting) { if (emojiAutoCompleteListView.currentIndex == 0) emojiAutoCompleteListView.currentIndex = emojiAutoCompleteListView.count - 1 else emojiAutoCompleteListView.currentIndex-- } } Keys.onTabPressed: { if (isEmojiAutoCompleting) { if (emojiAutoCompleteListView.currentIndex + 1 == emojiAutoCompleteListView.count) emojiAutoCompleteListView.currentIndex = 0 else emojiAutoCompleteListView.currentIndex++ replaceAutoComplete(emojiAutoCompleteListView.currentItem.unicode) } else { if (isUserAutoCompleting) { if (userAutoCompleteListView.currentIndex + 1 == userAutoCompleteListView.count) userAutoCompleteListView.currentIndex = 0 else userAutoCompleteListView.currentIndex++ } else { autoCompleteBeginPosition = text.substring(0, cursorPosition).lastIndexOf(" ") + 1 var autoCompletePrefix = text.substring(0, cursorPosition).split(" ").pop() if (!autoCompletePrefix) return userAutoCompleteModel = currentRoom.getUsers(autoCompletePrefix) if (userAutoCompleteModel.length === 0) return isUserAutoCompleting = true autoCompleteEndPosition = cursorPosition } replaceAutoComplete(userAutoCompleteListView.currentItem.displayName) } } onTextChanged: { timeoutTimer.restart() repeatTimer.start() currentRoom.cachedInput = text if (cursorPosition !== autoCompleteBeginPosition && cursorPosition !== autoCompleteEndPosition) { isUserAutoCompleting = false isEmojiAutoCompleting = false userAutoCompleteListView.currentIndex = 0 emojiAutoCompleteListView.currentIndex = 0 } var autoCompletePrefix = text.substring(0, cursorPosition).split(" ").pop() if (autoCompletePrefix.startsWith(":")) { if (autoCompletePrefix.length < 3) return autoCompleteBeginPosition = text.substring(0, cursorPosition).lastIndexOf(" ") + 1 emojiAutoCompleteModel = emojiModel.filterModel(autoCompletePrefix) if (emojiAutoCompleteModel.length === 0) return console.log("Auto completing emoji.") isEmojiAutoCompleting = true autoCompleteEndPosition = cursorPosition } } function replaceAutoComplete(word) { remove(autoCompleteBeginPosition, autoCompleteEndPosition) autoCompleteEndPosition = autoCompleteBeginPosition + word.length insert(cursorPosition, word) } function postMessage(text) { if (text.trim().length === 0) { return } if(!currentRoom) { return } var PREFIX_ME = '/me ' var PREFIX_NOTICE = '/notice ' var PREFIX_RAINBOW = '/rainbow ' var PREFIX_HTML = '/html ' var PREFIX_MARKDOWN = '/md ' if (isReply) { currentRoom.sendReply(replyUserID, replyEventID, replyContent, text) clearReply() return } if (text.indexOf(PREFIX_ME) === 0) { text = text.substr(PREFIX_ME.length) currentRoom.postMessage(text, RoomMessageEvent.Emote) return } if (text.indexOf(PREFIX_NOTICE) === 0) { text = text.substr(PREFIX_NOTICE.length) currentRoom.postMessage(text, RoomMessageEvent.Notice) return } 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) return } if (text.indexOf(PREFIX_HTML) === 0) { text = text.substr(PREFIX_HTML.length) var re = new RegExp("<.*?>") var plainText = text.replace(re, "") currentRoom.postHtmlMessage(plainText, text, RoomMessageEvent.Text) return } if (text.indexOf(PREFIX_MARKDOWN) === 0) { text = text.substr(PREFIX_MARKDOWN.length) var parsedText = Markdown.markdown_parser(text) currentRoom.postHtmlMessage(text, parsedText, RoomMessageEvent.Text) return } currentRoom.postPlainText(text) } } } ItemDelegate { Layout.preferredWidth: 48 Layout.preferredHeight: 48 id: emojiButton contentItem: MaterialIcon { icon: "\ue24e" } onClicked: emojiPicker.visible ? emojiPicker.close() : emojiPicker.open() EmojiPicker { x: -width + parent.width y: -height - 16 width: 360 height: 320 id: emojiPicker emojiModel: EmojiModel { id: emojiModel } Material.elevation: 2 textArea: inputField } } } function insert(str) { inputField.insert(inputField.cursorPosition, str) } function clear() { inputField.clear() } function clearReply() { isReply = false replyUserID = "" replyEventID = "" replyContent = "" } }