status-desktop/ui/shared/status/StatusChatInput.qml
Eric Mastro f1e83f74bc feat: drag and drop images
Allow up to 5 images to be dragged and dropped in to one-on-one chats and in the timeline. Can be combined with the existing upload button. The upload file dialog has been changed to allow multiple selections. Drag and dropped images adhere to the following rules, with corresponding validations messages:
- Max 5 image
- Image size must be 0.5 MB or less
- File extension must be one of [".png", ".jpg", ".jpeg", ".heif", "tif", ".tiff"]

Drag and drop and uploaded images are now also deduplicated.
2021-03-16 13:51:37 -04:00

969 lines
36 KiB
QML

import QtQuick 2.13
import QtQuick.Controls 2.13
import QtGraphicalEffects 1.13
import QtQuick.Layouts 1.13
import QtMultimedia 5.13
import QtQuick.Dialogs 1.3
import "../../imports"
import "../../shared"
import "../../app/AppLayouts/Chat/ChatColumn/samples"
import "../../app/AppLayouts/Chat/ChatColumn"
import "./emojiList.js" as EmojiJSON
Rectangle {
id: control
signal sendTransactionCommandButtonClicked()
signal receiveTransactionCommandButtonClicked()
signal stickerSelected(string hashId, string packId)
signal sendMessage(var event)
property bool emojiEvent: false;
property bool paste: false;
property bool isColonPressed: false;
property bool isReply: false
property bool isImage: false
property var recentStickers
property var stickerPackList
property int extraHeightFactor: calculateExtraHeightFactor()
property int messageLimit: control.isStatusUpdateInput ? 300 : 2000
property int messageLimitVisible: control.isStatusUpdateInput ? 50 : 200
property int chatType
//% "Type a message."
property string chatInputPlaceholder: qsTrId("type-a-message-")
property alias textInput: messageInputField
property bool isStatusUpdateInput: chatType === Constants.chatTypeStatusUpdate
property var fileUrls: []
property alias suggestionsList: suggestions
property alias suggestions: suggestionsBox
property var imageErrorMessageLocation: StatusChatInput.ImageErrorMessageLocation.Top
enum ImageErrorMessageLocation {
Top,
Bottom
}
height: {
if (extendedArea.visible) {
return messageInput.height + extendedArea.height + (control.isStatusUpdateInput ? 0 : Style.current.bigPadding)
}
if (messageInput.height > messageInput.defaultInputFieldHeight) {
if (messageInput.height >= messageInput.maxInputFieldHeight) {
return messageInput.maxInputFieldHeight + (control.isStatusUpdateInput ? 0 : Style.current.bigPadding)
}
return messageInput.height + (control.isStatusUpdateInput ? 0 : Style.current.bigPadding)
}
return control.isStatusUpdateInput ? 56 : 64
}
anchors.left: parent.left
anchors.right: parent.right
color: Style.current.background
function calculateExtraHeightFactor() {
const factor = (messageInputField.length / 500) + 1;
return (factor > 5) ? 5 : factor;
}
function insertInTextInput(start, text) {
// Repace new lines with entities because `insert` gets rid of them
messageInputField.insert(start, text.replace(/\n/g, "<br/>"));
}
property var interpretMessage: function (msg) {
if (msg.startsWith("/shrug")) {
return msg.replace("/shrug", "") + " ¯\\\\\\_(ツ)\\_/¯"
}
if (msg.startsWith("/tableflip")) {
return msg.replace("/tableflip", "") + " (╯°□°)╯︵ ┻━┻"
}
return msg
}
function isUploadFilePressed(event) {
return (event.key === Qt.Key_U) && (event.modifiers & Qt.ControlModifier) && imageBtn.visible && !imageDialog.visible
}
function checkTextInsert() {
if (emojiSuggestions.visible) {
emojiSuggestions.addEmoji();
return true
}
if (suggestionsBox.visible) {
suggestionsBox.selectCurrentItem();
return true
}
return false
}
function parseMarkdown(markdownText) {
const htmlText = markdownText
.replace(/\~\~([^*]+)\~\~/gim, '~~<span style="text-decoration: line-through">$1</span>~~')
.replace(/\*\*([^*]+)\*\*/gim, ':asterisk::asterisk:<b>$1</b>:asterisk::asterisk:')
.replace(/\`([^*]+)\`/gim, '`<code>$1</code>`')
.replace(/\*([^*]+)\*/gim, ':asterisk:<i>$1</i>: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()) {
event.accepted = true;
return
}
if (control.isStatusUpdateInput) {
return // Status update require the send button to be clicked
}
if (messageInputField.length < messageLimit) {
control.sendMessage(event)
control.hideExtendedArea();
event.accepted = true
return;
}
if(event) event.accepted = true
messageTooLongDialog.open()
}
if (event.key === Qt.Key_Tab) {
if (checkTextInsert()) {
event.accepted = true;
return
}
}
const message = control.extrapolateCursorPosition();
// handle new line in blockquote
if ((event.key === Qt.Key_Enter || event.key === Qt.Key_Return) && (event.modifiers & Qt.ShiftModifier) && message.data.startsWith(">")) {
if(message.data.startsWith(">") && !message.data.endsWith("\n\n")) {
let newMessage = ""
if (message.data.endsWith("\n> ")) {
newMessage = message.data.substr(0, message.data.lastIndexOf("> ")) + "\n\n"
} else {
newMessage = message.data + "\n> ";
}
messageInputField.remove(0, messageInputField.cursorPosition);
insertInTextInput(0, Emoji.parse(newMessage));
}
event.accepted = true
}
// handle backspace when entering an existing blockquote
if ((event.key === Qt.Key_Backspace || event.key === Qt.Key_Delete)) {
if(message.data.startsWith(">") && message.data.endsWith("\n\n")) {
const newMessage = message.data.substr(0, message.data.lastIndexOf("\n")) + "> ";
messageInputField.remove(0, messageInputField.cursorPosition);
insertInTextInput(0, Emoji.parse(newMessage));
event.accepted = true
}
}
if ((event.key === Qt.Key_V) && (event.modifiers & Qt.ControlModifier)) {
paste = true;
}
// ⌘⇧U
if (isUploadFilePressed(event)) {
imageBtn.clicked()
event.accepted = true
}
if (event.key === Qt.Key_Down) {
suggestionsBox.listView.incrementCurrentIndex()
return emojiSuggestions.listView.incrementCurrentIndex()
}
if (event.key === Qt.Key_Up) {
suggestionsBox.listView.decrementCurrentIndex()
return emojiSuggestions.listView.decrementCurrentIndex()
}
if (event.key === Qt.Key_Escape) {
suggestionsBox.hide()
}
isColonPressed = (event.key === Qt.Key_Colon) && (event.modifiers & Qt.ShiftModifier);
}
function wrapSelection(wrapWith) {
if (messageInputField.selectionStart - messageInputField.selectionEnd === 0) return
insertInTextInput(messageInputField.selectionStart, wrapWith);
insertInTextInput(messageInputField.selectionEnd, wrapWith);
messageInputField.deselect()
formatInputMessage()
}
function formatInputMessage() {
const posBeforeEnd = messageInputField.length - messageInputField.cursorPosition;
const deparsedEmoji = Emoji.deparse(messageInputField.text);
const plainText = chatsModel.plainText(deparsedEmoji);
const formatted = parseMarkdown(Emoji.parse(plainText.replace(/\n/g, "<br />")))
messageInputField.text = formatted
messageInputField.cursorPosition = messageInputField.length - posBeforeEnd;
}
function onRelease(event) {
if (event.key === Qt.Key_Backspace && textFormatMenu.opened) {
textFormatMenu.close()
}
// the text doesn't get registered to the textarea fast enough
// we can only get it in the `released` event
if (paste) {
paste = false;
formatInputMessage()
interrogateMessage();
} else {
if (event.key === Qt.Key_Asterisk ||
event.key === Qt.Key_QuoteLeft ||
event.key === Qt.Key_Space ||
event.key === Qt.Key_AsciiTilde) {
formatInputMessage()
}
}
if (event.key !== Qt.Key_Escape) {
emojiEvent = emojiHandler(event);
if (!emojiEvent) {
emojiSuggestions.close()
}
}
}
function interrogateMessage() {
const text = chatsModel.plainText(Emoji.deparse(messageInputField.text));
var words = text.split(' ');
let madeChanges = false
let transform = true;
for (var i = 0; i < words.length; i++) {
transform = true;
if (words[i].charAt(0) === ':') {
for (var j = 0; j < words[i].length; j++) {
if (Utils.isSpace(words[i].charAt(j)) === true || Utils.isPunct(words[i].charAt(j)) === true) {
transform = false;
}
}
if (transform) {
madeChanges = true
const codePoint = Emoji.getEmojiUnicode(words[i]);
words[i] = words[i].replace(words[i], (codePoint !== undefined) ? Emoji.fromCodePoint(codePoint) : words[i]);
}
}
}
if (madeChanges) {
messageInputField.remove(0, messageInputField.length);
insertInTextInput(0, Emoji.parse(words.join('&nbsp;')));
}
}
// since emoji length is not 1 we need to match that position that TextArea returns
// to the actual position in the string.
function extrapolateCursorPosition() {
// we need only the message part to be html
const text = chatsModel.plainText(Emoji.deparse(messageInputField.text));
const plainText = Emoji.parse(text);
var bracketEvent = false;
var length = 0;
for (var i = 0; i < plainText.length;) {
if (length >= messageInputField.cursorPosition) break;
if (!bracketEvent && plainText.charAt(i) !== '<') {
i++;
length++;
} else if (!bracketEvent && plainText.charAt(i) === '<') {
bracketEvent = true;
i++;
} else if (bracketEvent && plainText.charAt(i) !== '>') {
i++;
} else if (bracketEvent && plainText.charAt(i) === '>') {
bracketEvent = false;
i++;
length++;
}
}
let textBeforeCursor = Emoji.deparseFromParse(plainText.substr(0, i));
return {
cursor: countEmojiLengths(plainText.substr(0, i)) + messageInputField.cursorPosition,
data: Emoji.deparseFromParse(textBeforeCursor),
};
}
function emojiHandler(event) {
let message = extrapolateCursorPosition();
pollEmojiEvent(message);
// state machine to handle different forms of the emoji event state
if (!emojiEvent && isColonPressed) {
return (message.data.length <= 1 || Utils.isSpace(message.data.charAt(message.cursor - 1))) ? true : false;
} else if (emojiEvent && isColonPressed) {
const index = message.data.lastIndexOf(':', message.cursor - 2);
if (index >= 0 && message.cursor > 0) {
const shortname = message.data.substr(index, message.cursor);
const codePoint = Emoji.getEmojiUnicode(shortname);
if (codePoint !== undefined) {
replaceWithEmoji(message, shortname, codePoint);
}
return false;
}
return true;
} else if (emojiEvent && isKeyValid(event.key) && !isColonPressed) {
// popup
const index2 = message.data.lastIndexOf(':', message.cursor - 1);
if (index2 >= 0 && message.cursor > 0) {
const emojiPart = message.data.substr(index2, message.cursor);
if (emojiPart.length > 2) {
const emojis = EmojiJSON.emoji_json.filter(function (emoji) {
return emoji.name.includes(emojiPart) ||
emoji.shortname.includes(emojiPart) ||
emoji.aliases.some(a => a.includes(emojiPart))
})
emojiSuggestions.openPopup(emojis, emojiPart)
}
return true;
}
} else if (emojiEvent && !isKeyValid(event.key) && !isColonPressed) {
return false;
}
return false;
}
function countEmojiLengths(value) {
const match = Emoji.getEmojis(value);
var length = 0;
if (match && match.length > 0) {
for (var i = 0; i < match.length; i++) {
length += Emoji.deparseFromParse(match[i]).length;
}
length = length - match.length;
}
return length;
}
function replaceWithEmoji(message, shortname, codePoint) {
const encodedCodePoint = Emoji.getEmojiCodepoint(codePoint)
const newMessage = message.data
.replace(shortname, encodedCodePoint)
.replace(/ /g, "&nbsp;");
messageInputField.remove(0, messageInputField.cursorPosition);
insertInTextInput(0, Emoji.parse(newMessage));
emojiSuggestions.close()
emojiEvent = false
}
// check if user has placed cursor near valid emoji colon token
function pollEmojiEvent(message) {
const index = message.data.lastIndexOf(':', message.cursor);
if (index >= 0) {
emojiEvent = validSubstr(message.data.substr(index, message.cursor - index));
}
}
function validSubstr(substr) {
for(var i = 0; i < substr.length; i++) {
var c = substr.charAt(i);
if (Utils.isSpace(c) === true || Utils.isPunct(c) === true)
return false;
}
return true;
}
function isKeyValid(key) {
if (key === Qt.Key_Space || key === Qt.Key_Tab ||
(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;
return true;
}
function hideExtendedArea() {
isImage = false;
isReply = false;
control.fileUrls = []
imageArea.imageSource = [];
replyArea.userName = ""
replyArea.identicon = ""
replyArea.message = ""
for (let i=0; i<validators.children.length; i++) {
const validator = validators.children[i]
validator.images = []
}
}
function validateImages(imagePaths) {
// needed because imageArea.imageSource is not a normal js array
const existing = (imageArea.imageSource || []).map(x => x.toString())
let validImages = Utils.deduplicate(existing.concat(imagePaths))
for (let i=0; i<validators.children.length; i++) {
const validator = validators.children[i]
validator.images = validImages
validImages = validImages.filter(validImage => validator.validImages.includes(validImage))
}
return validImages
}
function showImageArea(imagePaths) {
isImage = true;
isReply = false;
imageArea.imageSource = imagePaths
control.fileUrls = imageArea.imageSource
}
function showReplyArea(userName, message, identicon) {
isReply = true
replyArea.userName = userName
replyArea.message = message
replyArea.identicon = identicon
messageInputField.forceActiveFocus();
}
Connections {
target: applicationWindow.dragAndDrop
onDroppedOnValidScreen: (drop) => {
let validImages = validateImages(drop.urls)
if (validImages.length > 0) {
showImageArea(validImages)
drop.acceptProposedAction()
}
}
}
ListModel {
id: suggestions
}
FileDialog {
id: imageDialog
//% "Please choose an image"
title: qsTrId("please-choose-an-image")
folder: shortcuts.pictures
selectMultiple: true
nameFilters: [
qsTr("Image files (%1)").arg(Constants.acceptedDragNDropImageExtensions.map(img => "*" + img).join(" "))
]
onAccepted: {
imageBtn.highlighted = false
imageBtn2.highlighted = false
let validImages = validateImages(imageDialog.fileUrls)
if (validImages.length > 0) {
control.showImageArea(validImages)
}
messageInputField.forceActiveFocus();
}
onRejected: {
imageBtn.highlighted = false
imageBtn2.highlighted = false
}
}
MessageDialog {
id: messageTooLongDialog
//% "Your message is too long."
title: qsTrId("your-message-is-too-long.")
icon: StandardIcon.Critical
//% "Please make your message shorter. We have set the limit to 2000 characters to be courteous of others."
text: qsTrId("please-make-your-message-shorter.-we-have-set-the-limit-to-2000-characters-to-be-courteous-of-others.")
standardButtons: StandardButton.Ok
}
StatusEmojiSuggestionPopup {
id: emojiSuggestions
}
SuggestionBox {
id: suggestionsBox
model: suggestions
x : messageInput.x
y: -height - Style.current.smallPadding
width: messageInput.width
filter: messageInputField.text
cursorPosition: messageInputField.cursorPosition
property: "ensName, localNickname, alias"
onItemSelected: function (item, lastAtPosition, lastCursorPosition) {
let hasEmoji = Emoji.hasEmoji(messageInputField.text)
let currentText = hasEmoji ?
chatsModel.plainText(Emoji.deparse(messageInputField.text)) :
chatsModel.plainText(messageInputField.text);
var 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 = ""
if (currentText === "@") {
position = nameLen
text = "@" + aliasName + " "
} else {
let left = currentText.substring(0, lastAtPosition)
let right = currentText.substring(hasEmoji ? lastCursorPosition + 2 : lastCursorPosition)
text = `${left} @${aliasName} ${right}`
}
messageInputField.text = hasEmoji ? Emoji.parse(text) : text
messageInputField.cursorPosition = lastAtPosition + aliasName.length + 2
suggestionsBox.suggestionsModel.clear()
}
}
StatusChatCommandsPopup {
id: chatCommandsPopup
x: 8
y: -height
onSendTransactionCommandButtonClicked: {
control.sendTransactionCommandButtonClicked()
chatCommandsPopup.close()
}
onReceiveTransactionCommandButtonClicked: {
control.receiveTransactionCommandButtonClicked()
chatCommandsPopup.close()
}
onClosed: {
chatCommandsBtn.highlighted = false
}
}
StatusEmojiPopup {
id: emojiPopup
width: 360
height: 440
x: parent.width - width - Style.current.halfPadding
y: -height
emojiSelected: function (text, atCursor) {
insertInTextInput(atCursor ? messageInputField.cursorPosition : messageInputField.length, text)
emojiBtn.highlighted = false
messageInputField.forceActiveFocus();
}
onClosed: {
emojiBtn.highlighted = false
}
}
StatusStickersPopup {
id: stickersPopup
x: parent.width - width - Style.current.halfPadding
y: -height
enabled: !!control.recentStickers && !!control.stickerPackList
recentStickers: control.recentStickers
stickerPackList: control.stickerPackList
onStickerSelected: {
control.stickerSelected(hashId, packId)
messageInputField.forceActiveFocus();
}
onClosed: {
stickersBtn.highlighted = false
}
}
StatusIconButton {
id: chatCommandsBtn
icon.name: "chat-commands"
anchors.left: parent.left
anchors.leftMargin: 4
anchors.bottom: parent.bottom
anchors.bottomMargin: 16
visible: control.chatType === Constants.chatTypeOneToOne && !control.isStatusUpdateInput
onClicked: {
highlighted = true
chatCommandsPopup.open()
}
}
StatusIconButton {
id: imageBtn
icon.name: "images_icon"
icon.height: 18
icon.width: 20
anchors.left: chatCommandsBtn.visible ? chatCommandsBtn.right : parent.left
anchors.leftMargin: chatCommandsBtn.visible ? 2 : 4
anchors.bottom: parent.bottom
anchors.bottomMargin: 16
visible: control.chatType !== Constants.chatTypePublic && !control.isStatusUpdateInput
onClicked: {
highlighted = true
imageDialog.open()
}
}
Rectangle {
id: messageInput
property int maxInputFieldHeight: control.isStatusUpdateInput ? 123 : 112
property int defaultInputFieldHeight: control.isStatusUpdateInput ? 56 : 40
anchors.left: imageBtn.visible ? imageBtn.right : parent.left
anchors.leftMargin: imageBtn.visible ? 5 : Style.current.smallPadding
anchors.top: control.isStatusUpdateInput ? parent.top : undefined
anchors.bottom: !control.isStatusUpdateInput ? parent.bottom : undefined
anchors.bottomMargin: control.isStatusUpdateInput ? 0 : 12
anchors.right: parent.right
anchors.rightMargin: Style.current.smallPadding
height: scrollView.height
color: Style.current.inputBackground
radius: control.isStatusUpdateInput ? 36 :
height > defaultInputFieldHeight + 1 || extendedArea.visible ? 16 : 32
ColumnLayout {
id: validators
anchors.bottom: control.imageErrorMessageLocation === StatusChatInput.ImageErrorMessageLocation.Top ? extendedArea.top : undefined
anchors.bottomMargin: control.imageErrorMessageLocation === StatusChatInput.ImageErrorMessageLocation.Top ? -4 : undefined
anchors.top: control.imageErrorMessageLocation === StatusChatInput.ImageErrorMessageLocation.Bottom ? extendedArea.bottom : undefined
anchors.topMargin: control.imageErrorMessageLocation === StatusChatInput.ImageErrorMessageLocation.Bottom ? (isImage ? -4 : 4) : undefined
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width
z: 1
StatusChatImageExtensionValidator {
Layout.alignment: Qt.AlignHCenter
}
StatusChatImageSizeValidator {
Layout.alignment: Qt.AlignHCenter
}
StatusChatImageQtyValidator {
Layout.alignment: Qt.AlignHCenter
}
}
Rectangle {
id: extendedArea
visible: isImage || isReply
height: {
if (visible) {
if (isImage) {
return imageArea.height + Style.current.halfPadding
}
if (isReply) {
return replyArea.height + replyArea.anchors.topMargin
}
}
return 0
}
anchors.left: messageInput.left
anchors.right: messageInput.right
anchors.bottom: control.isStatusUpdateInput ? undefined : messageInput.top
anchors.top: control.isStatusUpdateInput ? messageInput.bottom : undefined
anchors.topMargin: control.isStatusUpdateInput ? -Style.current.halfPadding : 0
color: Style.current.inputBackground
radius: control.isStatusUpdateInput ? 36 : 16
Rectangle {
color: parent.color
anchors.right: parent.right
anchors.left: parent.left
height: control.isStatusUpdateInput ? 64 : 30
anchors.top: control.isStatusUpdateInput ? parent.top : undefined
anchors.topMargin: control.isStatusUpdateInput ? -24 : 0
anchors.bottom: control.isStatusUpdateInput ? undefined : parent.bottom
anchors.bottomMargin: control.isStatusUpdateInput ? 0 : -height/2
}
StatusChatInputImageArea {
id: imageArea
anchors.left: parent.left
anchors.leftMargin: control.isStatusUpdateInput ? profileImage.width + Style.current.padding : Style.current.halfPadding
anchors.right: parent.right
anchors.rightMargin: control.isStatusUpdateInput ? actions.width + 2* Style.current.padding : Style.current.halfPadding
anchors.top: parent.top
anchors.topMargin: control.isStatusUpdateInput ? 0 : Style.current.halfPadding
visible: isImage
width: messageInputField.width - actions.width
onImageRemoved: {
if (control.fileUrls.length > index && control.fileUrls[index]) {
control.fileUrls.splice(index, 1)
}
isImage = control.fileUrls.length > 0
validateImages(control.fileUrls)
}
}
StatusChatInputReplyArea {
id: replyArea
visible: isReply
anchors.left: parent.left
anchors.leftMargin: 2
anchors.right: parent.right
anchors.rightMargin: 2
anchors.top: parent.top
anchors.topMargin: 2
onCloseButtonClicked: {
isReply = false
}
}
}
StatusImageIdenticon {
id: profileImage
source: profileModel.profile.identicon
anchors.left: parent.left
anchors.leftMargin: Style.current.smallPadding
anchors.top: parent.top
anchors.topMargin: Style.current.halfPadding
visible: control.isStatusUpdateInput
}
ScrollView {
id: scrollView
anchors.bottom: parent.bottom
anchors.left: profileImage.visible ? profileImage.right : parent.left
anchors.leftMargin: Style.current.smallPadding
anchors.right: actions.left
anchors.rightMargin: Style.current.halfPadding
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
height: {
if (messageInputField.implicitHeight <= messageInput.defaultInputFieldHeight) {
return messageInput.defaultInputFieldHeight
}
if (messageInputField.implicitHeight >= messageInput.maxInputFieldHeight) {
return messageInput.maxInputFieldHeight
}
return messageInputField.implicitHeight
}
TextArea {
id: messageInputField
textFormat: Text.RichText
verticalAlignment: TextEdit.AlignVCenter
font.pixelSize: 15
font.family: Style.current.fontRegular.name
wrapMode: TextArea.Wrap
height: parent.height
//% "Type a message"
placeholderText: qsTrId("type-a-message")
placeholderTextColor: Style.current.secondaryText
selectByMouse: true
color: Style.current.textColor
topPadding: Style.current.smallPadding
bottomPadding: 12
Keys.onPressed: onKeyPress(event)
Keys.onReleased: onRelease(event) // gives much more up to date cursorPosition
Keys.onShortcutOverride: event.accepted = isUploadFilePressed(event)
leftPadding: 0
background: Rectangle {
color: "transparent"
}
selectionColor: Style.current.primarySelectionColor
persistentSelection: true
onReleased: function (event) {
if (messageInputField.selectedText.trim() !== "") {
let x = event.x - (textFormatMenu.width / 2)
textFormatMenu.popup(x, -messageInputField.height-2)
messageInputField.forceActiveFocus();
}
}
StatusTextFormatMenu {
id: textFormatMenu
function surroundedBy(chars) {
return messageInputField.selectedText.trim().startsWith(chars) &&
messageInputField.selectedText.trim().endsWith(chars)
}
Action {
icon.name: "format-text-bold"
icon.width: 12
icon.height: 16
onTriggered: wrapSelection("**")
//% "Bold"
text: qsTrId("bold")
checked: textFormatMenu.surroundedBy("**")
}
Action {
icon.name: "format-text-italic"
icon.width: 12
icon.height: 16
onTriggered: wrapSelection("*")
//% "Italic"
text: qsTrId("italic")
checked: textFormatMenu.surroundedBy("*") && !textFormatMenu.surroundedBy("**")
}
Action {
icon.name: "format-text-strike-through"
icon.width: 20
icon.height: 18
onTriggered: wrapSelection("~~")
//% "Strikethrough"
text: qsTrId("strikethrough")
checked: textFormatMenu.surroundedBy("~~")
}
Action {
icon.name: "format-text-code"
icon.width: 20
icon.height: 18
onTriggered: wrapSelection("`")
//% "Code"
text: qsTrId("code")
checked: textFormatMenu.surroundedBy("`")
}
}
}
Shortcut {
enabled: messageInputField.activeFocus
sequence: StandardKey.Bold
onActivated: wrapSelection("**")
}
Shortcut {
enabled: messageInputField.activeFocus
sequence: StandardKey.Italic
onActivated: wrapSelection("*")
}
Shortcut {
enabled: messageInputField.activeFocus
sequence: "Ctrl+Shift+Alt+C"
onActivated: wrapSelection("```")
}
Shortcut {
enabled: messageInputField.activeFocus
sequence: "Ctrl+Shift+C"
onActivated: wrapSelection("`")
}
Shortcut {
enabled: messageInputField.activeFocus
sequence: "Ctrl+Alt+-"
onActivated: wrapSelection("~~")
}
Shortcut {
enabled: messageInputField.activeFocus
sequence: "Ctrl+Shift+X"
onActivated: wrapSelection("~~")
}
Shortcut {
enabled: messageInputField.activeFocus
sequence: "Ctrl+Meta+Space"
onActivated: emojiBtn.clicked()
}
}
Rectangle {
color: parent.color
anchors.bottom: parent.bottom
anchors.right: parent.right
visible: !control.isStatusUpdateInput
height: parent.height / 2
width: 32
radius: Style.current.radius
}
StyledText {
id: messageLengthLimit
property int remainingChars: messageLimit - messageInputField.length
text: remainingChars.toString()
visible: remainingChars <= control.messageLimitVisible
color: (remainingChars <= 0) ? Style.current.danger : Style.current.textColor
anchors.right: parent.right
anchors.bottom: actions.top
anchors.rightMargin: control.isStatusUpdateInput ? Style.current.padding : Style.current.radius
leftPadding: Style.current.halfPadding
rightPadding: Style.current.halfPadding
}
Item {
id: actions
width: control.isStatusUpdateInput ?
imageBtn2.width + sendBtn.anchors.leftMargin + sendBtn.width :
emojiBtn.width + stickersBtn.anchors.leftMargin + stickersBtn.width
anchors.bottom: control.isStatusUpdateInput && extendedArea.visible ? extendedArea.bottom : parent.bottom
anchors.bottomMargin: control.isStatusUpdateInput ? Style.current.smallPadding+2: 4
anchors.right: parent.right
anchors.rightMargin: Style.current.radius
height: emojiBtn.height
StatusIconButton {
id: imageBtn2
icon.name: "images_icon"
icon.height: 18
icon.width: 20
anchors.right: sendBtn.left
anchors.rightMargin: 2
anchors.bottom: parent.bottom
visible: control.isStatusUpdateInput
onClicked: {
highlighted = true
imageDialog.open()
}
}
StatusButton {
id: sendBtn
icon.source: "../../app/img/send.svg"
icon.width: 16
icon.height: 18
borderRadius: 16
//% "Send"
text: qsTrId("command-button-send")
type: "secondary"
flat: true
showBorder: true
forceBgColorOnHover: true
anchors.right: parent.right
anchors.rightMargin: Style.current.halfPadding
anchors.bottom: parent.bottom
visible: imageBtn2.visible
highlighted: chatsModel.plainText(Emoji.deparse(messageInputField.text)).length > 0 || isImage
enabled: highlighted && messageInputField.length < messageLimit
onClicked: function (event) {
control.sendMessage(event)
control.hideExtendedArea();
}
}
StatusIconButton {
id: emojiBtn
visible: !imageBtn2.visible
anchors.left: parent.left
anchors.bottom: parent.bottom
icon.name: "emojiBtn"
type: "secondary"
onClicked: {
stickersPopup.close()
if (emojiPopup.opened) {
emojiPopup.close()
highlighted = false
} else {
emojiPopup.open()
highlighted = true
}
}
}
StatusIconButton {
id: stickersBtn
anchors.left: emojiBtn.right
anchors.leftMargin: 2
anchors.bottom: parent.bottom
icon.name: "stickers_icon"
visible: profileModel.network.current === Constants.networkMainnet && emojiBtn.visible
width: visible ? 32 : 0
type: "secondary"
onClicked: {
emojiPopup.close()
if (stickersPopup.opened) {
stickersPopup.close()
highlighted = false
} else {
stickersPopup.open()
highlighted = true
}
}
}
}
}
}