fix(StatusChatInput): New handler method for large messages
Instead of silently cutting off user text beyond the 2000 char limit, handle it more gracefully within certain limits. Closes #11767
This commit is contained in:
parent
57cfc425fc
commit
e532abb448
|
@ -44,8 +44,9 @@ Rectangle {
|
||||||
property bool isImage: false
|
property bool isImage: false
|
||||||
property bool isEdit: false
|
property bool isEdit: false
|
||||||
|
|
||||||
property int messageLimit: 2000
|
readonly property int messageLimit: 2000 // actual message limit, we don't allow sending more than that
|
||||||
property int messageLimitVisible: 200
|
readonly property int messageLimitSoft: 200 // we start showing a char counter when this no. of chars left in the message
|
||||||
|
readonly property int messageLimitHard: 20000 // still cut-off attempts to paste beyond this limit, for app usability reasons
|
||||||
|
|
||||||
property int chatType
|
property int chatType
|
||||||
|
|
||||||
|
@ -55,7 +56,7 @@ Rectangle {
|
||||||
|
|
||||||
property var fileUrlsAndSources: []
|
property var fileUrlsAndSources: []
|
||||||
|
|
||||||
property var imageErrorMessageLocation: StatusChatInput.ImageErrorMessageLocation.Top // TODO: Remove this proeprty?
|
property var imageErrorMessageLocation: StatusChatInput.ImageErrorMessageLocation.Top // TODO: Remove this property?
|
||||||
|
|
||||||
property alias suggestions: suggestionsBox
|
property alias suggestions: suggestionsBox
|
||||||
|
|
||||||
|
@ -274,7 +275,7 @@ Rectangle {
|
||||||
}
|
}
|
||||||
|
|
||||||
function insertInTextInput(start, text) {
|
function insertInTextInput(start, text) {
|
||||||
// Repace new lines with entities because `insert` gets rid of them
|
// Replace new lines with entities because `insert` gets rid of them
|
||||||
messageInputField.insert(start, text.replace(/\n/g, "<br/>"));
|
messageInputField.insert(start, text.replace(/\n/g, "<br/>"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -325,26 +326,33 @@ Rectangle {
|
||||||
}
|
}
|
||||||
|
|
||||||
function onKeyPress(event) {
|
function onKeyPress(event) {
|
||||||
|
// get text without HTML formatting
|
||||||
|
const messageLength = messageInputField.getText(0, messageInputField.length).length;
|
||||||
|
|
||||||
if (event.modifiers === Qt.NoModifier && (event.key === Qt.Key_Enter || event.key === Qt.Key_Return)) {
|
if (event.modifiers === Qt.NoModifier && (event.key === Qt.Key_Enter || event.key === Qt.Key_Return)) {
|
||||||
if (checkTextInsert()) {
|
if (checkTextInsert()) {
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (messageLength <= messageLimit) {
|
||||||
if (messageInputField.length <= messageLimit) {
|
|
||||||
checkForInlineEmojis(true);
|
checkForInlineEmojis(true);
|
||||||
control.sendMessage(event)
|
control.sendMessage(event);
|
||||||
control.hideExtendedArea();
|
control.hideExtendedArea();
|
||||||
event.accepted = true
|
event.accepted = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (event) {
|
else {
|
||||||
event.accepted = true
|
// pop-up a warning message when trying to send a message over the limit
|
||||||
console.error("Attempting to send a message exceeding length limit")
|
messageLengthLimitTooltip.open();
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
} else if (event.key === Qt.Key_Escape && control.isReply) {
|
}
|
||||||
control.isReply = false
|
|
||||||
event.accepted = true
|
if (event.key === Qt.Key_Escape && control.isReply) {
|
||||||
|
control.isReply = false;
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const symbolPressed = event.text.length > 0 &&
|
const symbolPressed = event.text.length > 0 &&
|
||||||
|
@ -427,6 +435,16 @@ Rectangle {
|
||||||
validateImagesAndShowImageArea([clipboardImage])
|
validateImagesAndShowImageArea([clipboardImage])
|
||||||
event.accepted = true
|
event.accepted = true
|
||||||
} else if (QClipboardProxy.hasText) {
|
} else if (QClipboardProxy.hasText) {
|
||||||
|
const clipboardText = Utils.plainText(QClipboardProxy.text)
|
||||||
|
// prevent repetitive & huge clipboard paste, where huge is total char count > than messageLimitHard
|
||||||
|
const selectionLength = messageInputField.selectionEnd - messageInputField.selectionStart;
|
||||||
|
if ((messageLength + clipboardText.length - selectionLength) > control.messageLimitHard)
|
||||||
|
{
|
||||||
|
messageLengthLimitTooltip.open();
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
messageInputField.remove(messageInputField.selectionStart, messageInputField.selectionEnd)
|
messageInputField.remove(messageInputField.selectionStart, messageInputField.selectionEnd)
|
||||||
|
|
||||||
// cursor position must be stored in a helper property because setting readonly to true causes change
|
// cursor position must be stored in a helper property because setting readonly to true causes change
|
||||||
|
@ -434,7 +452,6 @@ Rectangle {
|
||||||
d.copyTextStart = messageInputField.cursorPosition
|
d.copyTextStart = messageInputField.cursorPosition
|
||||||
messageInputField.readOnly = true
|
messageInputField.readOnly = true
|
||||||
|
|
||||||
const clipboardText = Utils.plainText(QClipboardProxy.text)
|
|
||||||
const copiedText = Utils.plainText(d.copiedTextPlain)
|
const copiedText = Utils.plainText(d.copiedTextPlain)
|
||||||
if (copiedText === clipboardText) {
|
if (copiedText === clipboardText) {
|
||||||
if (d.copiedTextPlain.includes("@")) {
|
if (d.copiedTextPlain.includes("@")) {
|
||||||
|
@ -467,6 +484,7 @@ Rectangle {
|
||||||
("<div style='white-space: pre-wrap'>" + StatusQUtils.StringUtils.escapeHtml(QClipboardProxy.text) + "</div>")
|
("<div style='white-space: pre-wrap'>" + StatusQUtils.StringUtils.escapeHtml(QClipboardProxy.text) + "</div>")
|
||||||
: StatusQUtils.Emoji.deparse(QClipboardProxy.html)));
|
: StatusQUtils.Emoji.deparse(QClipboardProxy.html)));
|
||||||
}
|
}
|
||||||
|
event.accepted = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -529,7 +547,7 @@ Rectangle {
|
||||||
function unwrapSelection(unwrapWith, selectedTextWithFormationChars) {
|
function unwrapSelection(unwrapWith, selectedTextWithFormationChars) {
|
||||||
if (messageInputField.selectionStart - messageInputField.selectionEnd === 0) return
|
if (messageInputField.selectionStart - messageInputField.selectionEnd === 0) return
|
||||||
|
|
||||||
// calulate the new selection start and end positions
|
// Calculate the new selection start and end positions
|
||||||
var newSelectionStart = messageInputField.selectionStart - unwrapWith.length
|
var newSelectionStart = messageInputField.selectionStart - unwrapWith.length
|
||||||
var newSelectionEnd = messageInputField.selectionEnd-messageInputField.selectionStart + newSelectionStart
|
var newSelectionEnd = messageInputField.selectionEnd-messageInputField.selectionStart + newSelectionStart
|
||||||
|
|
||||||
|
@ -601,51 +619,6 @@ Rectangle {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = StatusQUtils.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 onRelease(event) {
|
function onRelease(event) {
|
||||||
if ((event.modifiers & Qt.ControlModifier) || (event.modifiers & Qt.MetaModifier)) // these are likely shortcuts with no meaningful text
|
if ((event.modifiers & Qt.ControlModifier) || (event.modifiers & Qt.MetaModifier)) // these are likely shortcuts with no meaningful text
|
||||||
return
|
return
|
||||||
|
@ -910,7 +883,7 @@ Rectangle {
|
||||||
control.fileUrlsAndSources = imageArea.imageSource
|
control.fileUrlsAndSources = imageArea.imageSource
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use this to validate and show the images. The concatanation of previous selected images is done automatically
|
// Use this to validate and show the images. The concatenation of previous selected images is done automatically
|
||||||
// Returns true if the images were valid and added
|
// Returns true if the images were valid and added
|
||||||
function validateImagesAndShowImageArea(imagePaths) {
|
function validateImagesAndShowImageArea(imagePaths) {
|
||||||
const validImages = validateImages(imagePaths)
|
const validImages = validateImages(imagePaths)
|
||||||
|
@ -1068,16 +1041,21 @@ Rectangle {
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: messageInput
|
id: messageInput
|
||||||
|
|
||||||
readonly property int defaultInputFieldHeight: 40
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
implicitHeight: inputLayout.implicitHeight + inputLayout.anchors.topMargin + inputLayout.anchors.bottomMargin
|
implicitHeight: inputLayout.implicitHeight + inputLayout.anchors.topMargin + inputLayout.anchors.bottomMargin
|
||||||
implicitWidth: inputLayout.implicitWidth + inputLayout.anchors.leftMargin + inputLayout.anchors.rightMargin
|
implicitWidth: inputLayout.implicitWidth + inputLayout.anchors.leftMargin + inputLayout.anchors.rightMargin
|
||||||
|
|
||||||
color: isEdit ? Theme.palette.statusChatInput.secondaryBackgroundColor : Style.current.inputBackground
|
color: isEdit ? Theme.palette.statusChatInput.secondaryBackgroundColor : Style.current.inputBackground
|
||||||
radius: 20
|
radius: 20
|
||||||
|
|
||||||
|
StatusQ.StatusToolTip {
|
||||||
|
id: messageLengthLimitTooltip
|
||||||
|
text: messageInputField.length >= control.messageLimitHard ? qsTr("Please reduce the message length")
|
||||||
|
: qsTr("Maximum message character count is %n", "", control.messageLimit)
|
||||||
|
orientation: StatusQ.StatusToolTip.Orientation.Top
|
||||||
|
timeout: 3000 // show for 3 seconds
|
||||||
|
}
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
id: textFormatMenuComponent
|
id: textFormatMenuComponent
|
||||||
|
|
||||||
|
@ -1203,7 +1181,7 @@ Rectangle {
|
||||||
RowLayout {
|
RowLayout {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.minimumHeight: (messageInputField.contentHeight + messageInputField.topPadding + messageInputField.bottomPadding)
|
Layout.minimumHeight: (messageInputField.contentHeight + messageInputField.topPadding + messageInputField.bottomPadding)
|
||||||
Layout.maximumHeight: 112
|
Layout.maximumHeight: 200
|
||||||
spacing: Style.current.radius
|
spacing: Style.current.radius
|
||||||
StatusScrollView {
|
StatusScrollView {
|
||||||
id: inputScrollView
|
id: inputScrollView
|
||||||
|
@ -1294,9 +1272,10 @@ Rectangle {
|
||||||
} else {
|
} else {
|
||||||
checkForInlineEmojis()
|
checkForInlineEmojis()
|
||||||
}
|
}
|
||||||
} else {
|
} else if (length > control.messageLimitHard) {
|
||||||
const removeFrom = (cursorPosition < messageLimit) ? cursorWhenPressed : messageLimit;
|
const removeFrom = (cursorPosition < messageLimitHard) ? cursorWhenPressed : messageLimitHard;
|
||||||
remove(removeFrom, cursorPosition);
|
remove(removeFrom, cursorPosition);
|
||||||
|
messageLengthLimitTooltip.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
d.updateMentionsPositions()
|
d.updateMentionsPositions()
|
||||||
|
@ -1400,9 +1379,26 @@ Rectangle {
|
||||||
property int remainingChars: -1
|
property int remainingChars: -1
|
||||||
leftPadding: Style.current.halfPadding
|
leftPadding: Style.current.halfPadding
|
||||||
rightPadding: Style.current.halfPadding
|
rightPadding: Style.current.halfPadding
|
||||||
visible: ((messageInputField.length >= control.messageLimitVisible) && (messageInputField.length <= control.messageLimit))
|
visible: messageInputField.length >= control.messageLimit - control.messageLimitSoft
|
||||||
color: (remainingChars <= messageLimitVisible) ? Style.current.danger : Style.current.textColor
|
color: {
|
||||||
|
if (remainingChars >= 0)
|
||||||
|
return Style.current.textColor
|
||||||
|
else
|
||||||
|
return Style.current.danger
|
||||||
|
}
|
||||||
text: visible ? remainingChars.toString() : ""
|
text: visible ? remainingChars.toString() : ""
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: mouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
onEntered: {
|
||||||
|
messageLengthLimitTooltip.open()
|
||||||
|
}
|
||||||
|
onExited: {
|
||||||
|
messageLengthLimitTooltip.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
|
|
Loading…
Reference in New Issue