fix(@desktop/chat): support to mentions in message edit mode

This commit is contained in:
Andrei Smirnov 2021-08-02 16:38:03 +03:00 committed by Iuri Matias
parent 0ef1dee5ce
commit 544b0aafc7
3 changed files with 326 additions and 302 deletions

View File

@ -15,7 +15,7 @@ import "../ContactsColumn"
import "../CommunityComponents" import "../CommunityComponents"
Item { Item {
id: svRoot id: root
anchors.fill: parent anchors.fill: parent
property alias chatLogView: chatLogView property alias chatLogView: chatLogView
@ -26,317 +26,307 @@ Item {
property bool loadingMessages: false property bool loadingMessages: false
property real scrollY: chatLogView.visibleArea.yPosition * chatLogView.contentHeight property real scrollY: chatLogView.visibleArea.yPosition * chatLogView.contentHeight
property int newMessages: 0 property int newMessages: 0
property int countOnStartUp: 0
ScrollView { ListView {
id: root id: chatLogView
anchors.fill: parent anchors.fill: parent
contentHeight: childrenRect.height anchors.bottomMargin: Style.current.bigPadding
contentItem: chatLogView spacing: appSettings.useCompactMode ? 0 : 4
ScrollBar.vertical.policy: chatLogView.contentHeight > chatLogView.height ? ScrollBar.AlwaysOn : ScrollBar.AlwaysOff boundsBehavior: Flickable.StopAtBounds
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff clip: true
flickDeceleration: {
property int countOnStartUp: 0 if (utilsModel.getOs() === Constants.windows) {
return 5000
ListView {
id: chatLogView
anchors.fill: parent
anchors.bottomMargin: Style.current.bigPadding
spacing: appSettings.useCompactMode ? 0 : 4
boundsBehavior: Flickable.StopAtBounds
flickDeceleration: {
if (utilsModel.getOs() === Constants.windows) {
return 5000
}
return 10000
} }
verticalLayoutDirection: ListView.BottomToTop return 10000
}
verticalLayoutDirection: ListView.BottomToTop
// This header and Connections is to create an invisible padding so that the chat identifier is at the top // This header and Connections is to create an invisible padding so that the chat identifier is at the top
// The Connections is necessary, because doing the check inside the header created a binding loop (the contentHeight includes the header height // The Connections is necessary, because doing the check inside the header created a binding loop (the contentHeight includes the header height
// If the content height is smaller than the full height, we "show" the padding so that the chat identifier is at the top, otherwise we disable the Connections // If the content height is smaller than the full height, we "show" the padding so that the chat identifier is at the top, otherwise we disable the Connections
header: Item { header: Item {
height: 0 height: 0
width: chatLogView.width width: chatLogView.width
}
function checkHeaderHeight() {
if (!chatLogView.headerItem) {
return
}
if (chatLogView.contentItem.height - chatLogView.headerItem.height < chatLogView.height) {
chatLogView.headerItem.height = chatLogView.height - (chatLogView.contentItem.height - chatLogView.headerItem.height) - 36
} else {
chatLogView.headerItem.height = 0
}
}
property var scrollToMessage: function (messageId) {
let item
for (let i = 0; i < messageListDelegate.count; i++) {
item = messageListDelegate.items.get(i)
if (item.model.messageId === messageId) {
chatLogView.positionViewAtIndex(i, ListView.Center)
return
}
}
}
Connections {
id: contentHeightConnection
enabled: true
target: chatLogView
onContentHeightChanged: {
chatLogView.checkHeaderHeight()
}
onHeightChanged: {
chatLogView.checkHeaderHeight()
}
}
Timer {
id: timer
}
Button {
readonly property int buttonPadding: 5
id: scrollDownButton
visible: false
height: 32
width: nbMessages.width + arrowImage.width + 2 * Style.current.halfPadding + (nbMessages.visible ? scrollDownButton.buttonPadding : 0)
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.rightMargin: Style.current.padding
background: Rectangle {
color: Style.current.buttonSecondaryColor
border.width: 0
radius: 16
}
onClicked: {
newMessages = 0
scrollDownButton.visible = false
chatLogView.scrollToBottom(true)
}
StyledText {
id: nbMessages
visible: newMessages > 0
width: visible ? implicitWidth : 0
text: newMessages
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
color: Style.current.pillButtonTextColor
font.pixelSize: 15
anchors.leftMargin: Style.current.halfPadding
}
SVGImage {
id: arrowImage
width: 24
height: 24
anchors.verticalCenter: parent.verticalCenter
anchors.left: nbMessages.right
source: "../../../img/leave_chat.svg"
anchors.leftMargin: nbMessages.visible ? scrollDownButton.buttonPadding : 0
rotation: -90
ColorOverlay {
anchors.fill: parent
source: parent
color: Style.current.pillButtonTextColor
}
}
MouseArea {
cursorShape: Qt.PointingHandCursor
anchors.fill: parent
onPressed: mouse.accepted = false
}
}
function scrollToBottom(force, caller) {
if (!force && !chatLogView.atYEnd) {
// User has scrolled up, we don't want to scroll back
return false
}
if (caller && caller !== chatLogView.itemAtIndex(chatLogView.count - 1)) {
// If we have a caller, only accept its request if it's the last message
return false
}
// Call this twice and with a timer since the first scroll to bottom might have happened before some stuff loads
// meaning that the scroll will not actually be at the bottom on switch
// Add a small delay because images, even though they say they say they are loaed, they aren't shown yet
Qt.callLater(chatLogView.positionViewAtBeginning)
timer.setTimeout(function() {
Qt.callLater(chatLogView.positionViewAtBeginning)
}, 100);
return true
}
Connections {
target: chatsModel
onAppReady: {
chatLogView.scrollToBottom(true)
}
}
Connections {
target: chatsModel.messageView
onMessagesLoaded: {
loadingMessages = false;
}
onSendingMessage: {
chatLogView.scrollToBottom(true)
}
onSendingMessageFailed: {
sendingMsgFailedPopup.open();
}
onNewMessagePushed: {
if (!chatLogView.scrollToBottom()) {
newMessages++
}
}
}
Connections {
target: chatsModel.communities
onMembershipRequestChanged: function (communityId, communityName, accepted) {
chatColumnLayout.currentNotificationChatId = null
chatColumnLayout.currentNotificationCommunityId = communityId
systemTray.showMessage("Status",
//% "You have been accepted into the %1 community"
accepted ? qsTrId("you-have-been-accepted-into-the---1--community").arg(communityName) :
//% "Your request to join the %1 community was declined"
qsTrId("your-request-to-join-the---1--community-was-declined").arg(communityName),
SystemTrayIcon.NoIcon,
Constants.notificationPopupTTL)
}
onMembershipRequestPushed: function (communityId, communityName, pubKey) {
chatColumnLayout.currentNotificationChatId = null
chatColumnLayout.currentNotificationCommunityId = communityId
//% "New membership request"
systemTray.showMessage(qsTrId("new-membership-request"),
//% "%1 asks to join %2"
qsTrId("-1-asks-to-join---2-").arg(Utils.getDisplayName(pubKey)).arg(communityName),
SystemTrayIcon.NoIcon,
Constants.notificationPopupTTL)
}
}
property var loadMsgs : Backpressure.oneInTime(chatLogView, 500, function() {
if(loadingMessages) return;
loadingMessages = true;
chatsModel.messageView.loadMoreMessages();
});
onContentYChanged: {
scrollDownButton.visible = (contentHeight - (scrollY + height) > 400)
if(scrollY < 500){
loadMsgs();
}
}
model: messageListDelegate
section.property: "sectionIdentifier"
section.criteria: ViewSection.FullString
} }
MessageDialog { function checkHeaderHeight() {
id: sendingMsgFailedPopup if (!chatLogView.headerItem) {
standardButtons: StandardButton.Ok return
//% "Failed to send message." }
text: qsTrId("failed-to-send-message-")
icon: StandardIcon.Critical if (chatLogView.contentItem.height - chatLogView.headerItem.height < chatLogView.height) {
chatLogView.headerItem.height = chatLogView.height - (chatLogView.contentItem.height - chatLogView.headerItem.height) - 36
} else {
chatLogView.headerItem.height = 0
}
}
property var scrollToMessage: function (messageId) {
let item
for (let i = 0; i < messageListDelegate.count; i++) {
item = messageListDelegate.items.get(i)
if (item.model.messageId === messageId) {
chatLogView.positionViewAtIndex(i, ListView.Center)
return
}
}
}
ScrollBar.vertical: ScrollBar {
visible: chatLogView.visibleArea.heightRatio < 1
}
Connections {
id: contentHeightConnection
enabled: true
target: chatLogView
onContentHeightChanged: {
chatLogView.checkHeaderHeight()
}
onHeightChanged: {
chatLogView.checkHeaderHeight()
}
} }
Timer { Timer {
id: modelLoadingDelayTimer id: timer
interval: 1000 }
onTriggered: {
root.countOnStartUp = messageListDelegate.count; Button {
readonly property int buttonPadding: 5
id: scrollDownButton
visible: false
height: 32
width: nbMessages.width + arrowImage.width + 2 * Style.current.halfPadding + (nbMessages.visible ? scrollDownButton.buttonPadding : 0)
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.rightMargin: Style.current.padding
background: Rectangle {
color: Style.current.buttonSecondaryColor
border.width: 0
radius: 16
}
onClicked: {
newMessages = 0
scrollDownButton.visible = false
chatLogView.scrollToBottom(true)
}
StyledText {
id: nbMessages
visible: newMessages > 0
width: visible ? implicitWidth : 0
text: newMessages
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
color: Style.current.pillButtonTextColor
font.pixelSize: 15
anchors.leftMargin: Style.current.halfPadding
}
SVGImage {
id: arrowImage
width: 24
height: 24
anchors.verticalCenter: parent.verticalCenter
anchors.left: nbMessages.right
source: "../../../img/leave_chat.svg"
anchors.leftMargin: nbMessages.visible ? scrollDownButton.buttonPadding : 0
rotation: -90
ColorOverlay {
anchors.fill: parent
source: parent
color: Style.current.pillButtonTextColor
}
}
MouseArea {
cursorShape: Qt.PointingHandCursor
anchors.fill: parent
onPressed: mouse.accepted = false
} }
} }
DelegateModelGeneralized { function scrollToBottom(force, caller) {
id: messageListDelegate if (!force && !chatLogView.atYEnd) {
lessThan: [ // User has scrolled up, we don't want to scroll back
function(left, right) { return left.clock > right.clock } return false
]
model: messageList
delegate: Message {
id: msgDelegate
fromAuthor: model.fromAuthor
chatId: model.chatId
userName: model.userName
alias: model.alias
localName: model.localName
message: model.message
plainText: model.plainText
identicon: model.identicon
isCurrentUser: model.isCurrentUser
timestamp: model.timestamp
sticker: model.sticker
contentType: model.contentType
replaces: model.replaces
isEdited: model.isEdited
outgoingStatus: model.outgoingStatus
responseTo: model.responseTo
authorCurrentMsg: msgDelegate.ListView.section
// The previous message is actually the nextSection since we reversed the list order
authorPrevMsg: msgDelegate.ListView.nextSection
imageClick: imagePopup.openPopup.bind(imagePopup)
messageId: model.messageId
emojiReactions: model.emojiReactions
linkUrls: model.linkUrls
communityId: model.communityId
hasMention: model.hasMention
stickerPackId: model.stickerPackId
pinnedMessage: model.isPinned
pinnedBy: model.pinnedBy
gapFrom: model.gapFrom
gapTo: model.gapTo
Component.onCompleted: {
if ((root.countOnStartUp > 0) && (root.countOnStartUp - 1) < index) {
//new message, increment z order
z = index;
}
}
messageContextMenu: svRoot.messageContextMenuInst
prevMessageIndex: {
// This is used in order to have access to the previous message and determine the timestamp
// we can't rely on the index because the sequence of messages is not ordered on the nim side
if (msgDelegate.DelegateModel.itemsIndex < messageListDelegate.items.count - 1) {
return messageListDelegate.items.get(msgDelegate.DelegateModel.itemsIndex + 1).model.index
}
return -1;
}
nextMessageIndex: {
if (msgDelegate.DelegateModel.itemsIndex < 1) {
return -1
}
return messageListDelegate.items.get(msgDelegate.DelegateModel.itemsIndex - 1).model.index
}
scrollToBottom: chatLogView.scrollToBottom
timeout: model.timeout
} }
if (caller && caller !== chatLogView.itemAtIndex(chatLogView.count - 1)) {
// If we have a caller, only accept its request if it's the last message
return false
}
// Call this twice and with a timer since the first scroll to bottom might have happened before some stuff loads
// meaning that the scroll will not actually be at the bottom on switch
// Add a small delay because images, even though they say they say they are loaed, they aren't shown yet
Qt.callLater(chatLogView.positionViewAtBeginning)
timer.setTimeout(function() {
Qt.callLater(chatLogView.positionViewAtBeginning)
}, 100);
return true
}
Connections {
target: chatsModel
onAppReady: {
chatLogView.scrollToBottom(true)
}
}
Connections {
target: chatsModel.messageView
onMessagesLoaded: {
loadingMessages = false;
}
onSendingMessage: {
chatLogView.scrollToBottom(true)
}
onSendingMessageFailed: {
sendingMsgFailedPopup.open();
}
onNewMessagePushed: {
if (!chatLogView.scrollToBottom()) {
newMessages++
}
}
}
Connections {
target: chatsModel.communities
onMembershipRequestChanged: function (communityId, communityName, accepted) {
chatColumnLayout.currentNotificationChatId = null
chatColumnLayout.currentNotificationCommunityId = communityId
systemTray.showMessage("Status",
//% "You have been accepted into the %1 community"
accepted ? qsTrId("you-have-been-accepted-into-the---1--community").arg(communityName) :
//% "Your request to join the %1 community was declined"
qsTrId("your-request-to-join-the---1--community-was-declined").arg(communityName),
SystemTrayIcon.NoIcon,
Constants.notificationPopupTTL)
}
onMembershipRequestPushed: function (communityId, communityName, pubKey) {
chatColumnLayout.currentNotificationChatId = null
chatColumnLayout.currentNotificationCommunityId = communityId
//% "New membership request"
systemTray.showMessage(qsTrId("new-membership-request"),
//% "%1 asks to join %2"
qsTrId("-1-asks-to-join---2-").arg(Utils.getDisplayName(pubKey)).arg(communityName),
SystemTrayIcon.NoIcon,
Constants.notificationPopupTTL)
}
}
property var loadMsgs : Backpressure.oneInTime(chatLogView, 500, function() {
if(loadingMessages) return;
loadingMessages = true;
chatsModel.messageView.loadMoreMessages();
});
onContentYChanged: {
scrollDownButton.visible = (contentHeight - (scrollY + height) > 400)
if(scrollY < 500){
loadMsgs();
}
}
model: messageListDelegate
section.property: "sectionIdentifier"
section.criteria: ViewSection.FullString
}
MessageDialog {
id: sendingMsgFailedPopup
standardButtons: StandardButton.Ok
//% "Failed to send message."
text: qsTrId("failed-to-send-message-")
icon: StandardIcon.Critical
}
Timer {
id: modelLoadingDelayTimer
interval: 1000
onTriggered: {
root.countOnStartUp = messageListDelegate.count;
}
}
DelegateModelGeneralized {
id: messageListDelegate
lessThan: [
function(left, right) { return left.clock > right.clock }
]
model: messageList
delegate: Message {
id: msgDelegate
fromAuthor: model.fromAuthor
chatId: model.chatId
userName: model.userName
alias: model.alias
localName: model.localName
message: model.message
plainText: model.plainText
identicon: model.identicon
isCurrentUser: model.isCurrentUser
timestamp: model.timestamp
sticker: model.sticker
contentType: model.contentType
replaces: model.replaces
isEdited: model.isEdited
outgoingStatus: model.outgoingStatus
responseTo: model.responseTo
authorCurrentMsg: msgDelegate.ListView.section
// The previous message is actually the nextSection since we reversed the list order
authorPrevMsg: msgDelegate.ListView.nextSection
imageClick: imagePopup.openPopup.bind(imagePopup)
messageId: model.messageId
emojiReactions: model.emojiReactions
linkUrls: model.linkUrls
communityId: model.communityId
hasMention: model.hasMention
stickerPackId: model.stickerPackId
pinnedMessage: model.isPinned
pinnedBy: model.pinnedBy
gapFrom: model.gapFrom
gapTo: model.gapTo
Component.onCompleted: { Component.onCompleted: {
modelLoadingDelayTimer.start(); if ((root.countOnStartUp > 0) && (root.countOnStartUp - 1) < index) {
//new message, increment z order
z = index;
}
} }
messageContextMenu: root.messageContextMenuInst
prevMessageIndex: {
// This is used in order to have access to the previous message and determine the timestamp
// we can't rely on the index because the sequence of messages is not ordered on the nim side
if (msgDelegate.DelegateModel.itemsIndex < messageListDelegate.items.count - 1) {
return messageListDelegate.items.get(msgDelegate.DelegateModel.itemsIndex + 1).model.index
}
return -1;
}
nextMessageIndex: {
if (msgDelegate.DelegateModel.itemsIndex < 1) {
return -1
}
return messageListDelegate.items.get(msgDelegate.DelegateModel.itemsIndex - 1).model.index
}
scrollToBottom: chatLogView.scrollToBottom
timeout: model.timeout
}
Component.onCompleted: {
modelLoadingDelayTimer.start();
} }
} }
} }
/*##^##
Designer {
D{i:0;autoSize:true;height:480;width:640}
}
##^##*/

View File

@ -24,6 +24,14 @@ Item {
height: messageContainer.height + messageContainer.anchors.topMargin height: messageContainer.height + messageContainer.anchors.topMargin
+ (dateGroupLbl.visible ? dateGroupLbl.height + dateGroupLbl.anchors.topMargin : 0) + (dateGroupLbl.visible ? dateGroupLbl.height + dateGroupLbl.anchors.topMargin : 0)
Timer {
id: ensureMessageFullyVisibleTimer
interval: 1
onTriggered: {
chatLogView.positionViewAtIndex(ListView.currentIndex, ListView.Contain)
}
}
MouseArea { MouseArea {
enabled: !placeholderMessage enabled: !placeholderMessage
anchors.fill: messageContainer anchors.fill: messageContainer
@ -195,16 +203,38 @@ Item {
sourceComponent: Item { sourceComponent: Item {
id: editText id: editText
height: childrenRect.height height: childrenRect.height
property bool suggestionsOpened: false
Keys.onEscapePressed: {
if (!suggestionsOpened) {
cancelBtn.clicked()
}
suggestionsOpened = false
}
StatusChatInput { StatusChatInput {
id: editTextInput
readonly property string originalText: Utils.getMessageWithStyle(Emoji.parse(message))
Component.onCompleted: { Component.onCompleted: {
textInput.forceActiveFocus(); suggestionsList.clear()
for (let i = 0; i < chatInput.suggestionsList.count; i++) {
suggestionsList.append(chatInput.suggestionsList.get(i))
}
textInput.forceActiveFocus()
textInput.cursorPosition = textInput.length textInput.cursorPosition = textInput.length
} }
id: editTextInput
chatInputPlaceholder: qsTrId("type-a-message-") chatInputPlaceholder: qsTrId("type-a-message-")
chatType: chatsModel.channelView.activeChannel.chatType chatType: chatsModel.channelView.activeChannel.chatType
isEdit: true isEdit: true
textInput.text: Utils.getMessageWithStyle(Emoji.parse(message.replace(/(<a href="\/\/0x[0-9A-Fa-f]+" class="mention">)/g, "$1@"))) textInput.text: originalText
onSendMessage: {
saveBtn.clicked()
}
suggestions.onVisibleChanged: {
if (suggestions.visible) {
editText.suggestionsOpened = true
}
}
} }
StatusButton { StatusButton {
@ -218,6 +248,7 @@ Item {
onClicked: { onClicked: {
isEdit = false isEdit = false
editTextInput.textInput.text = Emoji.parse(message) editTextInput.textInput.text = Emoji.parse(message)
ensureMessageFullyVisibleTimer.start()
} }
} }
@ -228,6 +259,7 @@ Item {
anchors.top: editTextInput.bottom anchors.top: editTextInput.bottom
//% "Save" //% "Save"
text: qsTrId("save") text: qsTrId("save")
enabled: editTextInput.textInput.text.trim().length > 0
onClicked: { onClicked: {
let msg = chatsModel.plainText(Emoji.deparse(editTextInput.textInput.text)) let msg = chatsModel.plainText(Emoji.deparse(editTextInput.textInput.text))
if (msg.length > 0){ if (msg.length > 0){
@ -251,7 +283,7 @@ Item {
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: root.chatHorizontalPadding anchors.rightMargin: root.chatHorizontalPadding
visible: !isEdit visible: !isEdit
ChatText { ChatText {
readonly property int leftPadding: chatImage.anchors.leftMargin + chatImage.width + root.chatHorizontalPadding readonly property int leftPadding: chatImage.anchors.leftMargin + chatImage.width + root.chatHorizontalPadding
id: chatText id: chatText

View File

@ -143,7 +143,9 @@ ModalPopup {
reactionModel: EmojiReactions { } reactionModel: EmojiReactions { }
onCloseParentPopup: { onCloseParentPopup: {
messageItem.view.closePopup() if (messageItem.view) {
messageItem.view.closePopup()
}
} }
} }
} }