diff --git a/src/app/chat/view.nim b/src/app/chat/view.nim index c690af362c..7f5a36bec2 100644 --- a/src/app/chat/view.nim +++ b/src/app/chat/view.nim @@ -945,3 +945,50 @@ QtObject: fromValue: fromValue ) mailserverWorker.start(task) + + proc formatInputStuff(self: ChatsView, regex: Regex, inputText: string): string = + var matches: seq[tuple[first, last: int]] = @[(-1, 0)] + + var resultTuple: tuple[first, last: int] + var start = 0 + var results: seq[tuple[first, last: int]] = @[] + + while true: + resultTuple = inputText.findBounds(regex, matches, start) + if (resultTuple[0] == -1): + break + start = resultTuple[1] + 1 + results.add(matches[0]) + + if (results.len == 0): + return "" + + var jsonString = "[" + var first = true + + for result in results: + if (not first): + jsonString = jsonString & "," + first = false + jsonString = jsonString & "[" & $result[0] & "," & $result[1] & "]" + + jsonString = jsonString & "]" + + return jsonString + + + proc formatInputItalic(self: ChatsView, inputText: string): string {.slot.} = + let italicRegex = re"""(?)(?)([^*]+)(?!<\/span>)\*""" + self.formatInputStuff(italicRegex, inputText) + + proc formatInputBold(self: ChatsView, inputText: string): string {.slot.} = + let boldRegex = re"""(?)\*\*(?!)([^*]+)(?!<\/span>)\*\*""" + self.formatInputStuff(boldRegex, inputText) + + proc formatInputStrikeThrough(self: ChatsView, inputText: string): string {.slot.} = + let strikeThroughRegex = re"""(?)~~(?!)([^*]+)(?!<\/span>)~~""" + self.formatInputStuff(strikeThroughRegex, inputText) + + proc formatInputCode(self: ChatsView, inputText: string): string {.slot.} = + let strikeThroughRegex = re"""(?)`(?!)([^*]+)(?!<\/span>)`""" + self.formatInputStuff(strikeThroughRegex, inputText) diff --git a/ui/imports/Emoji.qml b/ui/imports/Emoji.qml index 99471a476a..7c0291b4b5 100644 --- a/ui/imports/Emoji.qml +++ b/ui/imports/Emoji.qml @@ -34,17 +34,21 @@ QtObject { return Twemoji.twemoji.convert.fromCodePoint(value) } function deparse(value) { - return value.replace(/\"(.+?)\"/g, "$1"); + return value.replace(/\"(.+?)\"/g, "$1"); } function deparseFromParse(value) { - return value.replace(/\"(.+?)\"/g, "$1"); + return value.replace(/\"(.+?)\"/g, "$1"); } function hasEmoji(value) { - let match = value.match(/\"(.+?)\"/g) + let match = value.match(/\"(.+?)\"/g) return match && match.length > 0 } + function nbEmojis(value) { + let match = value.match(/\"(.+?)\"/g) + return match ? match.length : 0 + } function getEmojis(value) { - return value.match(/\"(.+?)\"/g, "$1"); + return value.match(/\"(.+?)\"/g, "$1"); } function getEmojiUnicode(shortname) { var _emoji; diff --git a/ui/shared/status/StatusChatInput.qml b/ui/shared/status/StatusChatInput.qml index 5f0e5c7399..40eaae0065 100644 --- a/ui/shared/status/StatusChatInput.qml +++ b/ui/shared/status/StatusChatInput.qml @@ -105,15 +105,6 @@ Rectangle { return false } - function parseMarkdown(markdownText) { - const htmlText = markdownText - .replace(/\~\~([^*]+)\~\~/gim, '~~$1~~') - .replace(/\*\*([^*]+)\*\*/gim, ':asterisk::asterisk:$1:asterisk::asterisk:') - .replace(/\`([^*]+)\`/gim, '`$1`') - .replace(/\*([^*]+)\*/gim, ':asterisk:$1:asterisk:') - return htmlText.replace(/\:asterisk\:/gim, "*") - } - function onKeyPress(event){ if (event.modifiers === Qt.NoModifier && (event.key === Qt.Key_Enter || event.key === Qt.Key_Return)) { if (checkTextInsert()) { @@ -194,7 +185,7 @@ Rectangle { function wrapSelection(wrapWith) { if (messageInputField.selectionStart - messageInputField.selectionEnd === 0) return - + insertInTextInput(messageInputField.selectionStart, wrapWith); insertInTextInput(messageInputField.selectionEnd, wrapWith); @@ -230,19 +221,89 @@ Rectangle { return currentText.replace(/\[\[mention\]\]/g, '') } + function parseMarkdown(markdownText) { + const htmlText = markdownText + .replace(/\~\~([^*]+)\~\~/gim, '~~$1~~') + .replace(/\*\*([^*]+)\*\*/gim, ':asterisk::asterisk:$1:asterisk::asterisk:') + .replace(/\`([^*]+)\`/gim, '`$1`') + .replace(/\*([^*]+)\*/gim, ':asterisk:$1:asterisk:') + return htmlText.replace(/\:asterisk\:/gim, "*") + } + function parseBackText(plainText) { - plainText = plainText.replace(/\[\[mention\]\](@([a-z\.]+(\ ?[a-z]+\ ?[a-z]+)?))\[\[mention\]\]/gi, `${Constants.mentionSpanTag}$1`) - - return parseMarkdown(Emoji.parse(plainText.replace(/\n/g, "
"))) } + function getFormattedText(start, end) { + start = start || 0 + end = end || messageInputField.length + + const oldFormattedText = messageInputField.getFormattedText(start, end) + + const found = oldFormattedText.match(/([\w\W\s]*)/m); + + return found[1] + } + + function setFormatInInput(formationFunction, startTag, endTag, formationChar, numFormationChars) { + const inputText = getFormattedText() + const plainInputText = messageInputField.getText(0, messageInputField.length) + + let lengthDifference + + try { + const result = formationFunction(inputText) + + if (!result) { + return + } + const parsed = JSON.parse(result) + + let substring + let nbEmojis + parsed.forEach(function (match) { + match[1] += 1 + const truncatedInputText = inputText.substring(0, match[1] + numFormationChars) + const truncatedPlainText = plainInputText.substring(0, messageInputField.cursorPosition) + + const lengthDifference = truncatedInputText.length - truncatedPlainText.length + + nbEmojis = Emoji.nbEmojis(truncatedInputText) + + + match[1] += (nbEmojis * -2) + match[0] += (nbEmojis * -2) + substring = inputText.substring(match[0], match[1]) + + if (plainInputText.charAt(match[0] - 1) !== formationChar) { + match[0] -= lengthDifference + match[1] -= lengthDifference + } else { + match[1] -= lengthDifference + } + + messageInputField.remove(match[0], match[1]) + insertInTextInput(match[0], `${startTag}${substring}${endTag}`) + }) + } catch (e) { + // + } + } + function formatInputMessage() { const posBeforeEnd = messageInputField.length - messageInputField.cursorPosition; - const plainText = getPlainText() - const formatted = parseBackText(plainText) - messageInputField.text = formatted.replace(/ /g, '  ') - messageInputField.cursorPosition = messageInputField.length - posBeforeEnd; + + // Remove align center spans + // TODO fix Those spans are added automatically by QT when you press space after an emoji. They break the code formation process + + // strikethrough + setFormatInInput(chatsModel.formatInputStrikeThrough, '', '', '~', 2) + // bold + setFormatInInput(chatsModel.formatInputBold, '', '', '*', 2) + // code + setFormatInInput(chatsModel.formatInputCode, '', '', '`', 1) + // italic + setFormatInInput(chatsModel.formatInputItalic, '', '', '*', 1) } function onRelease(event) { @@ -254,13 +315,17 @@ Rectangle { // we can only get it in the `released` event if (paste) { paste = false; - formatInputMessage() interrogateMessage(); + // TODO use the new formatInputMessage function instead + const plainText = getPlainText() + const newText = parseBackText(plainText) + messageInputField.remove(0, messageInputField.length) + insertInTextInput(0, newText) } else { if (event.key === Qt.Key_Asterisk || - event.key === Qt.Key_QuoteLeft || - event.key === Qt.Key_Space || - event.key === Qt.Key_AsciiTilde) { + event.key === Qt.Key_QuoteLeft || + event.key === Qt.Key_Space || + event.key === Qt.Key_AsciiTilde) { formatInputMessage() } } @@ -274,8 +339,9 @@ Rectangle { } function interrogateMessage() { + // TODO change this function to use remove and insert instead of replcing the whole text const text = chatsModel.plainText(Emoji.deparse(messageInputField.text)); - + var words = text.split(' '); let madeChanges = false @@ -304,31 +370,30 @@ Rectangle { } // since emoji length is not 1 we need to match that position that TextArea returns - // to the actual position in the string. + // to the actual position in the string. function extrapolateCursorPosition() { // we need only the message part to be html const text = getPlainText() const completelyPlainText = removeMentions(text) const plainText = Emoji.parse(text); - var bracketEvent = false; var almostMention = false; var mentionEvent = false; var length = 0; - - // This loop calculates the cursor position inside the plain text which contains the image tags () and the mention tags ([[mention]]) const cursorPos = messageInputField.cursorPosition - for (var i = 0; i < plainText.length;) { + let character = "" + for (var i = 0; i < plainText.length; i++) { if (length >= cursorPos) break; - if (!bracketEvent && plainText.charAt(i) !== '<' && !mentionEvent && plainText.charAt(i) !== '[') { + character = plainText.charAt(i) + if (!bracketEvent && character !== '<' && !mentionEvent && character !== '[') { length++; - } else if (!bracketEvent && plainText.charAt(i) === '<') { + } else if (!bracketEvent && character === '<') { bracketEvent = true; - } else if (bracketEvent && plainText.charAt(i) === '>') { + } else if (bracketEvent && character === '>') { bracketEvent = false; length++; } else if (!mentionEvent && almostMention && plainText.charAt(i) === '[') { @@ -344,7 +409,6 @@ Rectangle { almostMention = false mentionEvent = false } - i++ } let textBeforeCursor = Emoji.deparseFromParse(plainText.substr(0, i)); @@ -410,11 +474,8 @@ Rectangle { function replaceWithEmoji(message, shortname, codePoint) { const encodedCodePoint = Emoji.getEmojiCodepoint(codePoint) - const newMessage = message.data - .replace(shortname, encodedCodePoint) - .replace(/ /g, " "); - messageInputField.remove(0, messageInputField.cursorPosition); - insertInTextInput(0, parseBackText(newMessage)); + messageInputField.remove(messageInputField.cursorPosition - shortname.length, messageInputField.cursorPosition); + insertInTextInput(messageInputField.cursorPosition, Emoji.parse(encodedCodePoint) + " "); emojiSuggestions.close() emojiEvent = false } @@ -438,7 +499,7 @@ Rectangle { function isKeyValid(key) { if (key === Qt.Key_Space || key === Qt.Key_Tab || - (key >= Qt.Key_Exclam && key <= Qt.Key_Slash) || + (key >= Qt.Key_Exclam && key <= Qt.Key_Slash) || (key >= Qt.Key_Semicolon && key <= Qt.Key_Question) || (key >= Qt.Key_BracketLeft && key <= Qt.Key_hyphen)) return false; @@ -550,41 +611,22 @@ Rectangle { property: "ensName, localNickname, alias" onItemSelected: function (item, lastAtPosition, lastCursorPosition) { const hasEmoji = Emoji.hasEmoji(messageInputField.text) - const currentText = getPlainText() - - const completelyPlainText = removeMentions(currentText) - - lastAtPosition += currentText.length - completelyPlainText.length - lastCursorPosition += currentText.length - completelyPlainText.length const properties = "ensName, alias"; // Ignore localNickname let aliasName = item[properties.split(",").map(p => p.trim()).find(p => !!item[p])] aliasName = aliasName.replace(/(\.stateofus)?\.eth/, "") - let nameLen = aliasName.length + 2 // We're doing a +2 here because of the `@` and the trailing whitespace - let position = 0; - let text = "" - let cursorPositionMention = 0 + const spanPlusAlias = `${Constants.mentionSpanTag}@${aliasName}
` - if (currentText === "@") { - position = nameLen - text = spanPlusAlias - } else { - let left = currentText.substring(0, lastAtPosition) - // If there is an odd number of mentions, it means we are rewritting an old mention - let matches = left.match(/[[mention]]/g) - if (!!matches && matches.length % 2 === 1) { - let index = left.lastIndexOf('[[mention]]') - left = left.substring(0, index) - cursorPositionMention = lastAtPosition - index - } - let right = currentText.substring(hasEmoji ? lastCursorPosition + 2 : lastCursorPosition) - text = `${left} ${spanPlusAlias}${right}` - } + let rightIndex = hasEmoji ? lastCursorPosition + 2 : lastCursorPosition - messageInputField.text = parseBackText(text) - messageInputField.cursorPosition = lastAtPosition + aliasName.length + 2 - cursorPositionMention + messageInputField.remove(lastAtPosition, rightIndex) + + messageInputField.insert(lastAtPosition, spanPlusAlias) + + + messageInputField.cursorPosition = lastAtPosition + aliasName.length + 2 if (messageInputField.cursorPosition === 0) { // It reset to 0 for some reason, go back to the end messageInputField.cursorPosition = messageInputField.length