From 7bb5ad0ee0bcd29d3ce8492353eb5ed897b8c6b2 Mon Sep 17 00:00:00 2001 From: Nicolas Vengeon <nicolas.vengeon@savoirfairelinux.com> Date: Wed, 23 Nov 2022 09:50:53 -0500 Subject: [PATCH] Emoji: Implement emoji-reactions Change-Id: I83f27e86a6a7ee2140dc3028a4e183dcb8de8a27 GitLab: #875 --- resources/icons/add_reaction.svg | 1 + resources/icons/copy.svg | 1 + resources/icons/delete.svg | 1 + resources/icons/edit.svg | 1 + resources/icons/reply.svg | 1 + resources/icons/save_file.svg | 1 + .../ChatviewMessageOptions.qml | 205 ++++++++++++++++++ .../commoncomponents/EmojiReactionPopup.qml | 174 +++++++++++++++ src/app/commoncomponents/EmojiReactions.qml | 111 ++++++++++ .../commoncomponents/MessageOptionButton.qml | 63 ++++++ src/app/commoncomponents/SBSMessageBase.qml | 160 ++++++++++++-- .../commoncomponents/TextMessageDelegate.qml | 13 +- src/app/constant/JamiStrings.qml | 5 + src/app/constant/JamiTheme.qml | 14 +- src/app/mainview/components/ChatView.qml | 8 +- .../mainview/components/MessageListView.qml | 6 +- src/app/messagesadapter.cpp | 31 ++- src/app/messagesadapter.h | 6 + src/libclient/api/conversationmodel.h | 7 + src/libclient/api/interaction.h | 30 ++- src/libclient/conversationmodel.cpp | 43 +++- src/libclient/messagelistmodel.cpp | 92 +++++++- src/libclient/messagelistmodel.h | 11 + src/libclient/qtwrapper/conversions_wrap.hpp | 10 + src/libclient/typedefs.h | 6 + 25 files changed, 962 insertions(+), 39 deletions(-) create mode 100644 resources/icons/add_reaction.svg create mode 100644 resources/icons/copy.svg create mode 100644 resources/icons/delete.svg create mode 100644 resources/icons/edit.svg create mode 100644 resources/icons/reply.svg create mode 100644 resources/icons/save_file.svg create mode 100644 src/app/commoncomponents/ChatviewMessageOptions.qml create mode 100644 src/app/commoncomponents/EmojiReactionPopup.qml create mode 100644 src/app/commoncomponents/EmojiReactions.qml create mode 100644 src/app/commoncomponents/MessageOptionButton.qml diff --git a/resources/icons/add_reaction.svg b/resources/icons/add_reaction.svg new file mode 100644 index 000000000..10778a520 --- /dev/null +++ b/resources/icons/add_reaction.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="M24 24Zm0 20q-4.1 0-7.75-1.575-3.65-1.575-6.375-4.3-2.725-2.725-4.3-6.375Q4 28.1 4 24q0-4.15 1.575-7.8 1.575-3.65 4.3-6.35 2.725-2.7 6.375-4.275Q19.9 4 24 4q2.3 0 4.425.5T32.5 5.9v3.35q-1.9-1.1-4.025-1.675Q26.35 7 24 7q-7.05 0-12.025 4.95Q7 16.9 7 24q0 7.05 4.975 12.025Q16.95 41 24 41q7.1 0 12.05-4.975Q41 31.05 41 24q0-1.75-.325-3.375T39.7 17.5h3.2q.55 1.55.825 3.15Q44 22.25 44 24q0 4.1-1.575 7.75-1.575 3.65-4.275 6.375t-6.35 4.3Q28.15 44 24 44Zm16.5-30V9.5H36v-3h4.5V2h3v4.5H48v3h-4.5V14Zm-9.2 7.35q1.15 0 1.925-.8.775-.8.775-1.9 0-1.15-.775-1.925-.775-.775-1.925-.775-1.1 0-1.9.775-.8.775-.8 1.925 0 1.1.8 1.9.8.8 1.9.8Zm-14.6 0q1.15 0 1.925-.8.775-.8.775-1.9 0-1.15-.775-1.925-.775-.775-1.925-.775-1.1 0-1.9.775-.8.775-.8 1.925 0 1.1.8 1.9.8.8 1.9.8Zm7.3 13.6q3.3 0 6.075-1.775Q32.85 31.4 34.1 28.35H13.9q1.3 3.05 4.05 4.825Q20.7 34.95 24 34.95Z"/></svg> \ No newline at end of file diff --git a/resources/icons/copy.svg b/resources/icons/copy.svg new file mode 100644 index 000000000..c5f4a8d92 --- /dev/null +++ b/resources/icons/copy.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="M9 43.95q-1.2 0-2.1-.9-.9-.9-.9-2.1V10.8h3v30.15h23.7v3Zm6-6q-1.2 0-2.1-.9-.9-.9-.9-2.1v-28q0-1.2.9-2.1.9-.9 2.1-.9h22q1.2 0 2.1.9.9.9.9 2.1v28q0 1.2-.9 2.1-.9.9-2.1.9Zm0-3h22v-28H15v28Zm0 0v-28 28Z"/></svg> \ No newline at end of file diff --git a/resources/icons/delete.svg b/resources/icons/delete.svg new file mode 100644 index 000000000..a8565b38b --- /dev/null +++ b/resources/icons/delete.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="M13.05 42q-1.25 0-2.125-.875T10.05 39V10.5H8v-3h9.4V6h13.2v1.5H40v3h-2.05V39q0 1.2-.9 2.1-.9.9-2.1.9Zm21.9-31.5h-21.9V39h21.9Zm-16.6 24.2h3V14.75h-3Zm8.3 0h3V14.75h-3Zm-13.6-24.2V39Z"/></svg> \ No newline at end of file diff --git a/resources/icons/edit.svg b/resources/icons/edit.svg new file mode 100644 index 000000000..69222a042 --- /dev/null +++ b/resources/icons/edit.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="M24 42v-3.55l10.8-10.8 3.55 3.55L27.55 42ZM6 31.5v-3h15v3Zm34.5-2.45-3.55-3.55 1.45-1.45q.4-.4 1.05-.4t1.05.4l1.45 1.45q.4.4.4 1.05t-.4 1.05ZM6 23.25v-3h23.5v3ZM6 15v-3h23.5v3Z"/></svg> \ No newline at end of file diff --git a/resources/icons/reply.svg b/resources/icons/reply.svg new file mode 100644 index 000000000..59f3c8927 --- /dev/null +++ b/resources/icons/reply.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="M39 38v-8.7q0-2.7-1.9-4.6-1.9-1.9-4.6-1.9H11.7l7.7 7.7-2.1 2.1L6 21.3 17.3 10l2.1 2.1-7.7 7.7h20.8q3.9 0 6.7 2.775Q42 25.35 42 29.3V38Z"/></svg> \ No newline at end of file diff --git a/resources/icons/save_file.svg b/resources/icons/save_file.svg new file mode 100644 index 000000000..9c84d19ae --- /dev/null +++ b/resources/icons/save_file.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="M42 13.85V39q0 1.2-.9 2.1-.9.9-2.1.9H9q-1.2 0-2.1-.9Q6 40.2 6 39V9q0-1.2.9-2.1Q7.8 6 9 6h25.15Zm-3 1.35L32.8 9H9v30h30ZM24 35.75q2.15 0 3.675-1.525T29.2 30.55q0-2.15-1.525-3.675T24 25.35q-2.15 0-3.675 1.525T18.8 30.55q0 2.15 1.525 3.675T24 35.75ZM11.65 18.8h17.9v-7.15h-17.9ZM9 15.2V39 9Z"/></svg> \ No newline at end of file diff --git a/src/app/commoncomponents/ChatviewMessageOptions.qml b/src/app/commoncomponents/ChatviewMessageOptions.qml new file mode 100644 index 000000000..d32e3af2b --- /dev/null +++ b/src/app/commoncomponents/ChatviewMessageOptions.qml @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2022 Savoir-faire Linux Inc. + * Author: Nicolas Vengeon <nicolas.vengeon@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +import QtQuick +import QtQuick.Controls +import Qt5Compat.GraphicalEffects +import QtQuick.Layouts + +import net.jami.Models 1.1 +import net.jami.Adapters 1.1 +import net.jami.Constants 1.1 + +Popup { + id: root + + width: emojiColumn.width + JamiTheme.emojiMargins + height: emojiColumn.height + JamiTheme.emojiMargins + padding: 0 + background.visible: false + + property string msgId + property string msg + property string emojiReplied + property bool out + property int type + property string transferId: msgId + property string location: Body + property string transferName + + Rectangle { + id: bubble + + color: JamiTheme.chatviewBgColor + anchors.fill: parent + radius: JamiTheme.modalPopupRadius + + ColumnLayout { + id: emojiColumn + + anchors.centerIn: parent + + RowLayout { + id: emojiRow + Layout.alignment: Qt.AlignCenter + + Repeater { + model: ["ðŸ‘", "😂", "😠" ] + + delegate: Button { + id: emojiButton + + height: 50 + width: 50 + text: modelData + font.pointSize: JamiTheme.emojiBubbleSize + + Text { + visible: emojiButton.hovered + anchors.centerIn: parent + text: modelData + font.pointSize: JamiTheme.emojiBubbleSizeBig + z: 1 + } + + background: Rectangle { + anchors.fill: parent + opacity: emojiReplied.includes(modelData) ? 1 : 0 + color: JamiTheme.emojiReactPushButtonColor + radius: 10 + } + + onClicked: { + if (emojiReplied.includes(modelData)) + MessagesAdapter.removeEmojiReaction(CurrentConversation.id,text,msgId) + else + MessagesAdapter.addEmojiReaction(CurrentConversation.id,text,msgId) + close() + } + } + } + } + + Rectangle { + Layout.margins: 5 + color: JamiTheme.timestampColor + Layout.fillWidth: true + Layout.preferredHeight: 1 + radius: width * 0.5 + opacity: 0.6 + } + + MessageOptionButton { + textButton: JamiStrings.copy + iconSource: JamiResources.copy_svg + Layout.fillWidth: true + Layout.margins: 5 + onClicked: { + UtilsAdapter.setClipboardText(msg) + close() + } + } + + MessageOptionButton { + visible: type === Interaction.Type.DATA_TRANSFER + textButton: JamiStrings.saveFile + iconSource: JamiResources.save_file_svg + Layout.fillWidth: true + Layout.margins: 5 + onClicked: { + MessagesAdapter.copyToDownloads(root.transferId, root.transferName) + close() + } + } + + MessageOptionButton { + visible: type === Interaction.Type.DATA_TRANSFER + textButton: JamiStrings.openLocation + iconSource: JamiResources.round_folder_24dp_svg + Layout.fillWidth: true + Layout.margins: 5 + onClicked: { + MessagesAdapter.openDirectory(root.location) + close() + } + } + + MessageOptionButton { + id: buttonEdit + + visible: root.out && type === Interaction.Type.TEXT + textButton: JamiStrings.editMessage + iconSource: JamiResources.edit_svg + Layout.fillWidth: true + Layout.margins: 5 + onClicked: { + MessagesAdapter.replyToId = "" + MessagesAdapter.editId = root.msgId + close() + } + } + + MessageOptionButton { + visible: root.out && type === Interaction.Type.TEXT + textButton: JamiStrings.deleteMessage + iconSource: JamiResources.delete_svg + Layout.fillWidth: true + Layout.margins: 5 + onClicked: { + MessagesAdapter.editMessage(CurrentConversation.id, "", root.msgId) + close() + } + } + } + } + + Overlay.modal: Rectangle { + color: JamiTheme.transparentColor + // Color animation for overlay when pop up is shown. + ColorAnimation on color { + to: JamiTheme.popupOverlayColor + duration: 500 + } + } + + DropShadow { + z: -1 + + width: bubble.width + height: bubble.height + horizontalOffset: 3.0 + verticalOffset: 3.0 + radius: bubble.radius * 4 + color: JamiTheme.shadowColor + source: bubble + transparentBorder: true + } + + enter: Transition { + NumberAnimation { + properties: "opacity"; from: 0.0; to: 1.0 + duration: JamiTheme.shortFadeDuration + } + } + + exit: Transition { + NumberAnimation { + properties: "opacity"; from: 1.0; to: 0.0 + duration: JamiTheme.shortFadeDuration + } + } +} diff --git a/src/app/commoncomponents/EmojiReactionPopup.qml b/src/app/commoncomponents/EmojiReactionPopup.qml new file mode 100644 index 000000000..78cded7d9 --- /dev/null +++ b/src/app/commoncomponents/EmojiReactionPopup.qml @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2022 Savoir-faire Linux Inc. + * Author: Nicolas Vengeon <nicolas.vengeon@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls + +import net.jami.Models 1.1 +import net.jami.Adapters 1.1 +import net.jami.Constants 1.1 +import Qt5Compat.GraphicalEffects + + +Popup { + id: root + + width: popupContent.width + height: popupContent.height + background.visible: false + parent: Overlay.overlay + + property var emojiReaction + + // center in parent + x: Math.round((parent.width - width) / 2) + y: Math.round((parent.height - height) / 2) + + modal: true + padding: 0 + + visible: false + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + + Rectangle { + id: container + + anchors.fill: parent + radius: JamiTheme.modalPopupRadius + color: JamiTheme.secondaryBackgroundColor + + ColumnLayout { + id: popupContent + + Layout.alignment: Qt.AlignCenter + + PushButton { + id: btnClose + + Layout.alignment: Qt.AlignRight + width: 30 + height: 30 + imageContainerWidth: 30 + imageContainerHeight : 30 + Layout.margins: 8 + radius : 5 + imageColor: "grey" + normalColor: JamiTheme.transparentColor + source: JamiResources.round_close_24dp_svg + onClicked: { root.close() } + } + + RowLayout { + Layout.leftMargin: JamiTheme.popupButtonsMargin + Layout.rightMargin: JamiTheme.popupButtonsMargin + Layout.alignment: Qt.AlignCenter + + ListView { + id: listViewReaction + + Layout.preferredWidth: 400 + Layout.preferredHeight: modelCount < 5 + ? 50 + (JamiTheme.avatarSize * modelCount) + : 300 + model: Object.entries(emojiReaction) + clip: true + property int modelCount: Object.entries(emojiReaction).length + delegate: RowLayout { + + width: parent.width + property string authorUri: modelData[0] + property var emojiArray: modelData[1] + property bool isMe: authorUri === CurrentAccount.uri + + Avatar { + imageId: isMe ? CurrentAccount.id : authorUri + showPresenceIndicator: false + mode: isMe ? Avatar.Mode.Account : Avatar.Mode.Contact + width: JamiTheme.avatarSize + height: JamiTheme.avatarSize + } + + Text { + id: authorName + + Layout.maximumWidth: 180 + elide: Text.ElideRight + font.pointSize: JamiTheme.namePopupFontsize + color: JamiTheme.chatviewTextColor + text: isMe + ? " " + CurrentAccount.bestName + + " " + : " " + UtilsAdapter.getBestNameForUri(CurrentAccount.id, authorUri) + + " " + } + + Text { + Layout.fillWidth: true + text: { + var cur = ""; + for (const emojiIndex in emojiArray) { + cur = cur + emojiArray[emojiIndex] + } + return cur + } + horizontalAlignment: Text.AlignRight + font.pointSize: JamiTheme.emojiPopupFontsize + elide: Text.ElideRight + } + } + } + } + } + } + + Overlay.modal: Rectangle { + color: JamiTheme.transparentColor + // Color animation for overlay when pop up is shown. + ColorAnimation on color { + to: JamiTheme.popupOverlayColor + duration: 500 + } + } + + DropShadow { + z: -1 + width: root.width + height: root.height + horizontalOffset: 3.0 + verticalOffset: 3.0 + radius: container.radius * 4 + color: JamiTheme.shadowColor + source: container + transparentBorder: true + } + + enter: Transition { + NumberAnimation { + properties: "opacity"; from: 0.0; to: 1.0 + duration: JamiTheme.shortFadeDuration + } + } + + exit: Transition { + NumberAnimation { + properties: "opacity"; from: 1.0; to: 0.0 + duration: JamiTheme.shortFadeDuration + } + } +} diff --git a/src/app/commoncomponents/EmojiReactions.qml b/src/app/commoncomponents/EmojiReactions.qml new file mode 100644 index 000000000..db93e912e --- /dev/null +++ b/src/app/commoncomponents/EmojiReactions.qml @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2022 Savoir-faire Linux Inc. + * Author: Nicolas Vengeon <nicolas.vengeon@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import QtQuick +import Qt5Compat.GraphicalEffects + +import net.jami.Models 1.1 +import net.jami.Adapters 1.1 +import net.jami.Constants 1.1 + +Item { + id: root + + property var emojiReaction + property real contentHeight: bubble.height + property real contentWidth: bubble.width + property string emojiTexts: ownEmojiList + + visible: emojis ? emojis.length : false + + property string emojis: { + var space = "" + var emojiList = [] + var emojiNumberList = [] + for (const reactions of Object.entries(emojiReaction)) { + var authorEmojiList = reactions[1] + for (var emojiIndex in authorEmojiList) { + var emoji = authorEmojiList[emojiIndex] + if (emojiList.includes(emoji)) { + var findIndex = emojiList.indexOf(emoji) + if (findIndex != -1) + emojiNumberList[findIndex] += 1 + } else { + emojiList.push(emoji) + emojiNumberList.push(1) + } + } + } + var cur = "" + for (var i in emojiList) { + if (emojiNumberList[i] !== 1) + cur = cur + space + emojiList[i] + emojiNumberList[i] + "" + else + cur = cur + space + emojiList[i] + "" + space = " " + } + return cur + } + + property string ownEmojiList: { + var list = "" + for (const reactions of Object.entries(emojiReaction)) { + var authorUri = reactions[0] + var authorEmojiList = reactions[1] + if (CurrentAccount.uri === authorUri) { + for (var emojiIndex in authorEmojiList) { + list = list + authorEmojiList[emojiIndex] + } + return list + } + } + return "" + } + + Rectangle { + id: bubble + + color: JamiTheme.emojiReactBubbleBgColor + width: textEmojis.width + 6 + height: textEmojis.height + 6 + radius: 10 + + Text { + id: textEmojis + + anchors.margins: 10 + anchors.centerIn: bubble + font.pointSize: JamiTheme.emojiReactSize + color: JamiTheme.chatviewTextColor + text: root.emojis + } + } + + DropShadow { + z: -1 + + width: bubble.width + height: bubble.height + horizontalOffset: 3.0 + verticalOffset: 3.0 + radius: bubble.radius * 4 + color: JamiTheme.shadowColor + source: bubble + transparentBorder: true + } +} diff --git a/src/app/commoncomponents/MessageOptionButton.qml b/src/app/commoncomponents/MessageOptionButton.qml new file mode 100644 index 000000000..c0176804e --- /dev/null +++ b/src/app/commoncomponents/MessageOptionButton.qml @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2022 Savoir-faire Linux Inc. + * Author: Nicolas Vengeon <nicolas.vengeon@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Qt5Compat.GraphicalEffects + +import net.jami.Models 1.1 +import net.jami.Adapters 1.1 +import net.jami.Constants 1.1 + +Button { + id: buttonA + + icon.color: JamiTheme.emojiReactPushButtonColor + font.pixelSize:JamiTheme.messageOptionTextFontSize + height: 20 + + property string textButton + property string iconSource + + contentItem: RowLayout { + ResponsiveImage { + id: icon + + source: iconSource + width: 25 + height: 25 + color: JamiTheme.emojiReactPushButtonColor + Layout.rightMargin: 10 + } + + Text { + text: textButton + Layout.fillWidth: true + horizontalAlignment: Text.AlignLeft + font.pixelSize: JamiTheme.messageOptionTextFontSize + color: JamiTheme.chatviewTextColor + } + } + + background: Rectangle { + visible: parent.hovered + radius: 10 + color: parent.down ? JamiTheme.pressedButtonColor : JamiTheme.hoveredButtonColor + } +} diff --git a/src/app/commoncomponents/SBSMessageBase.qml b/src/app/commoncomponents/SBSMessageBase.qml index 50847d957..459988f47 100644 --- a/src/app/commoncomponents/SBSMessageBase.qml +++ b/src/app/commoncomponents/SBSMessageBase.qml @@ -54,6 +54,7 @@ Control { readonly property real avatarSize: 20 readonly property real msgRadius: 20 readonly property real hPadding: JamiTheme.sbsMessageBasePreferredPadding + property bool textHovered: false width: ListView.view ? ListView.view.width : 0 height: mainColumnLayout.implicitHeight @@ -80,7 +81,6 @@ Control { } Item { - id: usernameblock Layout.preferredHeight: (seq === MsgSeq.first || seq === MsgSeq.single) ? 10 : 0 @@ -98,11 +98,14 @@ Control { RowLayout { + id: msgRowlayout + Layout.preferredHeight: innerContent.height + root.extraHeight Layout.topMargin: (seq === MsgSeq.first || seq === MsgSeq.single) ? 6 : 0 Item { id: avatarBlock + Layout.preferredWidth: isOutgoing ? 0 : avatar.width + hPadding/3 Layout.preferredHeight: isOutgoing ? 0 : bubble.height Avatar { @@ -117,32 +120,132 @@ Control { } } - MouseArea { - id: itemMouseArea + Item { + id: itemRowMessage - Layout.fillWidth: true Layout.fillHeight: true - acceptedButtons: Qt.LeftButton | Qt.RightButton - onClicked: function (mouse) { - if (mouse.button === Qt.RightButton - && (transferId !== "" || Type === Interaction.Type.TEXT)) { - // Context Menu for Transfers - ctxMenu.x = mouse.x - ctxMenu.y = mouse.y - ctxMenu.openMenu() - } else if (root.hoveredLink) { - MessagesAdapter.openUrl(root.hoveredLink) + Layout.fillWidth: true + + MouseArea { + id: bubbleArea + + anchors.fill: bubble + hoverEnabled: true + onClicked: function (mouse) { + if (root.hoveredLink) { + MessagesAdapter.openUrl(root.hoveredLink) + } } + property bool bubbleHovered: containsMouse || textHovered } Column { id: innerContent + width: parent.width + visible: true // place actual content here ReplyToRow {} } + Item { + id: optionButtonItem + + anchors.right: isOutgoing ? bubble.left : undefined + anchors.left: !isOutgoing ? bubble.right : undefined + width: JamiTheme.emojiPushButtonSize * 2 + height: JamiTheme.emojiPushButtonSize + anchors.verticalCenter: bubble.verticalCenter + + HoverHandler { + id: bgHandler + } + + PushButton { + id: more + + anchors.rightMargin: isOutgoing ? 10 : 0 + anchors.leftMargin: !isOutgoing ? 10 : 0 + + imageColor: JamiTheme.emojiReactPushButtonColor + normalColor: JamiTheme.transparentColor + toolTipText: JamiStrings.moreOptions + anchors.verticalCenter: parent.verticalCenter + anchors.right: isOutgoing ? optionButtonItem.right : undefined + anchors.left: !isOutgoing ? optionButtonItem.left : undefined + visible: bubbleArea.bubbleHovered + || hovered + || reply.hovered + || bgHandler.hovered + source: JamiResources.more_vert_24dp_svg + width: optionButtonItem.width / 2 + height: optionButtonItem.height + + onClicked: { + messageOptionPopup.setPosition() + messageOptionPopup.open() + } + } + + PushButton { + id: reply + + imageColor: JamiTheme.emojiReactPushButtonColor + normalColor: JamiTheme.transparentColor + toolTipText: JamiStrings.reply + source: JamiResources.reply_svg + width: optionButtonItem.width / 2 + height: optionButtonItem.height + anchors.verticalCenter: parent.verticalCenter + anchors.right: isOutgoing ? more.left : undefined + anchors.left: !isOutgoing ? more.right : undefined + visible: bubbleArea.bubbleHovered + || hovered + || more.hovered + || bgHandler.hovered + + onClicked: { + MessagesAdapter.editId = "" + MessagesAdapter.replyToId = Id + } + } + } + + + ChatviewMessageOptions { + id: messageOptionPopup + + emojiReplied: emojiReaction.emojiTexts + out: isOutgoing + msgId: Id + msg: Body + type: Type + transferName: TransferName + + function setPosition() { + var mappedCoord = bubble.mapToItem(appWindow.contentItem,0, 0) + var distBottomScreen = appWindow.height - mappedCoord.y - height + if (distBottomScreen < 0) { + y = distBottomScreen + } else { + y = 0 + } + var distBorders = root.width - bubble.width - width + if (isOutgoing) { + if (distBorders > 0) + x = bubble.x - width + else + x = bubble.x + } else { + if (distBorders > 0) + x = bubble.x + bubble.width + else + x = bubble.x + bubble.width - width + } + } + } + MessageBubble { id: bubble @@ -253,6 +356,25 @@ Control { } } + EmojiReactions { + id: emojiReaction + + property bool isOutgoing: Author === "" + Layout.alignment: isOutgoing ? Qt.AlignRight : Qt.AlignLeft + Layout.rightMargin: isOutgoing ? status.width : undefined + Layout.leftMargin: !isOutgoing ? avatarBlock.width : undefined + Layout.topMargin: - contentHeight/4 + Layout.preferredHeight: contentHeight + 5 + Layout.preferredWidth: contentWidth + emojiReaction: Reactions + + TapHandler { + onTapped: { + reactionPopup.open() + } + } + } + ListView { id: infoCell @@ -285,13 +407,9 @@ Control { } } - SBSContextMenu { - id: ctxMenu + EmojiReactionPopup { + id: reactionPopup - msgId: Id - location: root.location - isOutgoing: root.isOutgoing - transferId: root.transferId - transferName: root.transferName + emojiReaction: Reactions } } diff --git a/src/app/commoncomponents/TextMessageDelegate.qml b/src/app/commoncomponents/TextMessageDelegate.qml index 943f08455..7016ccf9e 100644 --- a/src/app/commoncomponents/TextMessageDelegate.qml +++ b/src/app/commoncomponents/TextMessageDelegate.qml @@ -44,7 +44,7 @@ SBSMessageBase { formattedTime: MessagesAdapter.getFormattedTime(Timestamp) formattedDay: MessagesAdapter.getFormattedDay(Timestamp) extraHeight: extraContent.active && !isRemoteImage ? msgRadius : -isRemoteImage - + textHovered: textHoverhandler.hovered EditedPopup { id: editedPopup @@ -58,11 +58,13 @@ SBSMessageBase { padding: isEmojiOnly ? 0 : JamiTheme.preferredMarginSize anchors.right: isOutgoing ? parent.right : undefined - text: Body - horizontalAlignment: Text.AlignLeft + HoverHandler { + id: textHoverhandler + } + width: { if (extraContent.active) Math.max(extraContent.width, @@ -77,7 +79,6 @@ SBSMessageBase { wrapMode: Label.WrapAtWordBoundaryOrAnywhere selectByMouse: true font.pixelSize: isEmojiOnly? JamiTheme.chatviewEmojiSize : JamiTheme.chatviewFontSize - font.hintingPreference: Font.PreferNoHinting renderType: Text.NativeRendering textFormat: Text.MarkdownText @@ -119,6 +120,7 @@ SBSMessageBase { }, RowLayout { id: editedRow + anchors.right: isOutgoing ? parent.right : undefined visible: PreviousBodies.length !== 0 @@ -127,7 +129,6 @@ SBSMessageBase { Layout.leftMargin: JamiTheme.preferredMarginSize Layout.bottomMargin: JamiTheme.preferredMarginSize - source: JamiResources.round_edit_24dp_svg width: JamiTheme.editedFontSize height: JamiTheme.editedFontSize @@ -161,12 +162,14 @@ SBSMessageBase { }, Loader { id: extraContent + anchors.right: isOutgoing ? parent.right : undefined property real minSize: 192 property real maxSize: 320 active: LinkPreviewInfo.url !== undefined sourceComponent: ColumnLayout { id: previewContent + spacing: 12 Component.onCompleted: { isRemoteImage = MessagesAdapter.isRemoteImage(LinkPreviewInfo.url) diff --git a/src/app/constant/JamiStrings.qml b/src/app/constant/JamiStrings.qml index 39091b93b..9426b79d5 100644 --- a/src/app/constant/JamiStrings.qml +++ b/src/app/constant/JamiStrings.qml @@ -806,4 +806,9 @@ Item { property string customizationDescription: qsTr("This profile is only shared with this account's contacts") property string customizationDescription2: qsTr("Your profile is only shared with your contacts") property string whySaveAccount: qsTr("Why should I save my account?") + + //message options + property string deleteMessage: qsTr("Delete message") + property string editMessage: qsTr("Edit message") + } diff --git a/src/app/constant/JamiTheme.qml b/src/app/constant/JamiTheme.qml index 34686dc94..afea00299 100644 --- a/src/app/constant/JamiTheme.qml +++ b/src/app/constant/JamiTheme.qml @@ -222,6 +222,19 @@ Item { property color sharePositionIndicatorColor: red_ property color sharedPositionIndicatorColor: urgentOrange_ + //EmojiReact + property real emojiBubbleSize: calcSize(17) + property real emojiBubbleSizeBig: calcSize(21) + property real emojiReactSize: calcSize(12) + property real emojiPopupFontsize: calcSize(25) + property real namePopupFontsize: calcSize(15) + property real avatarSize: 30 + property int emojiPushButtonSize: 30 + property int emojiMargins: 20 + property color emojiReactBubbleBgColor: darkTheme ? darkGreyColor : whiteColor + property color emojiReactPushButtonColor: darkTheme ? "#bbb" : "#003b4e" + property real messageOptionTextFontSize: calcSize(15) + // Files To Send Container property color removeFileButtonColor: Qt.rgba(96, 95, 97, 0.5) @@ -233,7 +246,6 @@ Item { property color typingDotsEnlargeColor: darkTheme ? "white" : Qt.darker("lightgrey", 3.0) // Font. - property color faddedFontColor: darkTheme? "#c0c0c0" : "#a0a0a0" property color faddedLastInteractionFontColor: darkTheme ? "#c0c0c0" : "#505050" diff --git a/src/app/mainview/components/ChatView.qml b/src/app/mainview/components/ChatView.qml index 5b40d8712..6bbe66745 100644 --- a/src/app/mainview/components/ChatView.qml +++ b/src/app/mainview/components/ChatView.qml @@ -39,6 +39,12 @@ Rectangle { signal messagesCleared signal messagesLoaded + onVisibleChanged: { + if (visible) + return + UtilsAdapter.clearInteractionsCache(CurrentAccount.id, CurrentConversation.id) + } + function focusChatView() { chatViewFooter.textInput.forceActiveFocus() swarmDetailsPanel.visible = false @@ -213,7 +219,7 @@ Rectangle { Layout.rightMargin: JamiTheme.chatviewMargin currentIndex: CurrentConversation.isRequest || - CurrentConversation.needsSyncing + CurrentConversation.needsSyncing Loader { active: CurrentConversation.id !== "" diff --git a/src/app/mainview/components/MessageListView.qml b/src/app/mainview/components/MessageListView.qml index 2ccc8098b..ef1a559c5 100644 --- a/src/app/mainview/components/MessageListView.qml +++ b/src/app/mainview/components/MessageListView.qml @@ -214,7 +214,11 @@ JamiListView { filters: ExpressionFilter { readonly property int mergeType: Interaction.Type.MERGE readonly property int editedType: Interaction.Type.EDITED - expression: Body !== "" && Type !== mergeType && Type !== editedType + readonly property int reactionType: Interaction.Type.REACTION + expression: Body !== "" + && Type !== mergeType + && Type !== editedType + && Type !== reactionType } sorters: ExpressionSorter { expression: modelLeft.index > modelRight.index diff --git a/src/app/messagesadapter.cpp b/src/app/messagesadapter.cpp index f09348cc0..837abb947 100644 --- a/src/app/messagesadapter.cpp +++ b/src/app/messagesadapter.cpp @@ -170,16 +170,43 @@ MessagesAdapter::editMessage(const QString& convId, const QString& newBody, cons const auto convUid = lrcInstance_->get_selectedConvUid(); auto editId = !messageId.isEmpty() ? messageId : editId_; if (editId.isEmpty()) { - qWarning("No message to edit"); return; } lrcInstance_->getCurrentConversationModel()->editMessage(convId, newBody, editId); - set_editId(""); } catch (...) { qDebug() << "Exception during message edition:" << messageId; } } +void +MessagesAdapter::removeEmojiReaction(const QString& convId, + const QString& emoji, + const QString& messageId) +{ + try { + const auto convUid = lrcInstance_->get_selectedConvUid(); + const auto authorUri = lrcInstance_->getCurrentAccountInfo().profileInfo.uri; + // check if this emoji has already been added by this author + auto emojiId = lrcInstance_->getConversationFromConvUid(convId) + .interactions->findEmojiReaction(emoji, authorUri, messageId); + editMessage(convId, "", emojiId); + } catch (...) { + qDebug() << "Exception during removeEmojiReaction():" << messageId; + } +} + +void +MessagesAdapter::addEmojiReaction(const QString& convId, + const QString& emoji, + const QString& messageId) +{ + try { + lrcInstance_->getCurrentConversationModel()->reactMessage(convId, emoji, messageId); + } catch (...) { + qDebug() << "Exception during addEmojiReaction():" << messageId; + } +} + void MessagesAdapter::sendFile(const QString& message) { diff --git a/src/app/messagesadapter.h b/src/app/messagesadapter.h index f3362cf3a..b19193e50 100644 --- a/src/app/messagesadapter.h +++ b/src/app/messagesadapter.h @@ -72,6 +72,12 @@ protected: Q_INVOKABLE void editMessage(const QString& convId, const QString& newBody, const QString& messageId = ""); + Q_INVOKABLE void addEmojiReaction(const QString& convId, + const QString& emoji, + const QString& messageId = ""); + Q_INVOKABLE void removeEmojiReaction(const QString& convId, + const QString& emoji, + const QString& messageId); Q_INVOKABLE void sendFile(const QString& message); Q_INVOKABLE void acceptFile(const QString& arg); Q_INVOKABLE void cancelFile(const QString& arg); diff --git a/src/libclient/api/conversationmodel.h b/src/libclient/api/conversationmodel.h index cd5849d18..18caa2d6b 100644 --- a/src/libclient/api/conversationmodel.h +++ b/src/libclient/api/conversationmodel.h @@ -229,6 +229,13 @@ public: * @param messageId The id of the message (MUST be by the same author & plain/text) */ void editMessage(const QString& convId, const QString& newBody, const QString& messageId); + /** + * React to a message with an emoji + * @param convId The conversation id + * @param emoji The emoji + * @param messageId The id of the message + */ + void reactMessage(const QString& convId, const QString& emoji, const QString& messageId); /** * Modify the current filter (will change the result of getFilteredConversations) * @param filter the new filter diff --git a/src/libclient/api/interaction.h b/src/libclient/api/interaction.h index 4cebc79ff..84e58df56 100644 --- a/src/libclient/api/interaction.h +++ b/src/libclient/api/interaction.h @@ -32,7 +32,18 @@ namespace interaction { Q_NAMESPACE Q_CLASSINFO("RegisterEnumClassesUnscoped", "false") -enum class Type { INVALID, INITIAL, TEXT, CALL, CONTACT, DATA_TRANSFER, MERGE, EDITED, COUNT__ }; +enum class Type { + INVALID, + INITIAL, + TEXT, + CALL, + CONTACT, + DATA_TRANSFER, + MERGE, + EDITED, + REACTION, + COUNT__ +}; Q_ENUM_NS(Type) static inline const QString @@ -53,6 +64,8 @@ to_string(const Type& type) return "MERGE"; case Type::EDITED: return "EDITED"; + case Type::REACTION: + return "REACTION"; case Type::INVALID: case Type::COUNT__: default: @@ -67,6 +80,8 @@ to_type(const QString& type) return interaction::Type::INITIAL; else if (type == "TEXT" || type == TEXT_PLAIN) return interaction::Type::TEXT; + else if (type == "REACTION") + return interaction::Type::REACTION; else if (type == "CALL" || type == "application/call-history+json") return interaction::Type::CALL; else if (type == "CONTACT" || type == "member") @@ -281,6 +296,8 @@ struct Info MapStringString commit; QVariantMap linkPreviewInfo = {}; bool linkified = false; + QVariantMap reactions; + QString react_to; QVector<Body> previousBodies; Info() {} @@ -305,10 +322,17 @@ struct Info Info(const MapStringString& message, const QString& accountURI) { type = to_type(message["type"]); - if (type == Type::TEXT || type == Type::EDITED) { + if (message.contains("react-to") && type == Type::TEXT) { + type = to_type("REACTION"); + react_to = message["react-to"]; + authorUri = message["author"]; + } + + if (type == Type::TEXT || type == Type::EDITED || type == Type::REACTION) { body = message["body"]; } - authorUri = accountURI == message["author"] ? "" : message["author"]; + if (type != Type::REACTION) + authorUri = accountURI == message["author"] ? "" : message["author"]; timestamp = message["timestamp"].toInt(); status = Status::SUCCESS; parentId = message["linearizedParent"]; diff --git a/src/libclient/conversationmodel.cpp b/src/libclient/conversationmodel.cpp index 45d777fa9..b4c295e45 100644 --- a/src/libclient/conversationmodel.cpp +++ b/src/libclient/conversationmodel.cpp @@ -1223,7 +1223,11 @@ ConversationModel::sendMessage(const QString& uid, const QString& body, const QS try { auto& conversation = pimpl_->getConversationForUid(uid, true).get(); if (!conversation.isLegacy()) { - ConfigurationManager::instance().sendMessage(owner.id, uid, body, parentId, 0); + ConfigurationManager::instance().sendMessage(owner.id, + uid, + body, + parentId, + static_cast<int>(MessageFlag::Text)); return; } @@ -1251,7 +1255,7 @@ ConversationModel::sendMessage(const QString& uid, const QString& body, const QS conversationId, body, parentId, - 0); + static_cast<int>(MessageFlag::Text)); return; } auto& peers = pimpl_->peersForConversation(newConv); @@ -1351,7 +1355,27 @@ ConversationModel::editMessage(const QString& convId, if (!conversationOpt.has_value()) { return; } - ConfigurationManager::instance().sendMessage(owner.id, convId, newBody, messageId, 1); + ConfigurationManager::instance().sendMessage(owner.id, + convId, + newBody, + messageId, + static_cast<int>(MessageFlag::Reply)); +} + +void +ConversationModel::reactMessage(const QString& convId, + const QString& emoji, + const QString& messageId) +{ + auto conversationOpt = getConversationForUid(convId); + if (!conversationOpt.has_value()) { + return; + } + ConfigurationManager::instance().sendMessage(owner.id, + convId, + emoji, + messageId, + static_cast<int>(MessageFlag::Reaction)); } void @@ -2430,6 +2454,7 @@ ConversationModelPimpl::slotConversationLoaded(uint32_t requestId, auto msgId = message["id"]; auto msg = interaction::Info(message, linked.owner.profileInfo.uri); conversation.interactions->editMessage(msgId, msg); + conversation.interactions->reactToMessage(msgId, msg); auto downloadFile = false; if (msg.type == interaction::Type::INITIAL) { allLoaded = true; @@ -2465,6 +2490,8 @@ ConversationModelPimpl::slotConversationLoaded(uint32_t requestId, message["action"])); } else if (msg.type == interaction::Type::EDITED) { conversation.interactions->addEdition(msgId, msg, false); + } else if (msg.type == interaction::Type::REACTION) { + conversation.interactions->addReaction(msg.react_to, msgId); } insertSwarmInteraction(msgId, msg, conversation, true); if (downloadFile) { @@ -2617,6 +2644,8 @@ ConversationModelPimpl::slotMessageReceived(const QString& accountId, if (msg.authorUri != linked.owner.profileInfo.uri) { updateUnread = true; } + } else if (msg.type == interaction::Type::REACTION) { + conversation.interactions->addReaction(msg.react_to, msgId); } else if (msg.type == interaction::Type::EDITED) { conversation.interactions->addEdition(msgId, msg, true); } @@ -2625,6 +2654,14 @@ ConversationModelPimpl::slotMessageReceived(const QString& accountId, // message already exists return; } + // once the reaction is added to interactions, we can update the reacted + // message + if (msg.type == interaction::Type::REACTION) { + auto reactInteraction = conversation.interactions->find(msg.react_to); + if (reactInteraction != conversation.interactions->end()) { + conversation.interactions->reactToMessage(msg.react_to, reactInteraction->second); + } + } if (updateUnread) { conversation.unreadMessages++; } diff --git a/src/libclient/messagelistmodel.cpp b/src/libclient/messagelistmodel.cpp index e6913a508..b657ceaa4 100644 --- a/src/libclient/messagelistmodel.cpp +++ b/src/libclient/messagelistmodel.cpp @@ -23,6 +23,7 @@ #include "api/conversationmodel.h" #include "api/interaction.h" +#include "qtwrapper/conversions_wrap.hpp" #include <QAbstractListModel> @@ -191,6 +192,7 @@ MessageListModel::clear() interactions_.clear(); replyTo_.clear(); editedBodies_.clear(); + reactedMessages_.clear(); Q_EMIT endResetModel(); } @@ -466,6 +468,8 @@ MessageListModel::dataForItem(item_t item, int, int role) const return QVariant(messageToReaders_[item.first]); case Role::IsEmojiOnly: return QVariant(isOnlyEmoji(item.second.body)); + case Role::Reactions: + return QVariant(item.second.reactions); default: return {}; } @@ -588,6 +592,11 @@ MessageListModel::addEdition(const QString& msgId, const interaction::Info& info if (editedId.isEmpty()) return; auto& edited = editedBodies_[editedId]; + auto editedMsgIt = std::find_if(edited.begin(), edited.end(), [&](const auto& v) { + return msgId == v.commitId; + }); + if (editedMsgIt != edited.end()) + return; // Already added auto value = interaction::Body {msgId, info.body, info.timestamp}; if (end) edited.push_back(value); @@ -597,7 +606,58 @@ MessageListModel::addEdition(const QString& msgId, const interaction::Info& info if (editedIt != interactions_.end()) { // If already there, we can update the content editMessage(editedId, editedIt->second); + if (!editedIt->second.react_to.isEmpty()) { + auto reactToIt = find(editedIt->second.react_to); + if (reactToIt != interactions_.end()) + reactToMessage(editedIt->second.react_to, reactToIt->second); + } + } +} + +void +MessageListModel::addReaction(const QString& messageId, const QString& reactionId) +{ + auto itReacted = reactedMessages_.find(messageId); + if (itReacted != reactedMessages_.end()) { + itReacted->insert(reactionId); + } else { + QSet<QString> emojiList; + emojiList.insert(reactionId); + reactedMessages_.insert(messageId, emojiList); + } + auto interaction = find(reactionId); + if (interaction != interactions_.end()) { + // Edit reaction if needed + editMessage(reactionId, interaction->second); + } +} + +QVariantMap +MessageListModel::convertReactMessagetoQVariant(const QSet<QString>& emojiIdList) +{ + QVariantMap convertedMap; + QMap<QString, QStringList> mapStringEmoji; + for (auto emojiId = emojiIdList.begin(); emojiId != emojiIdList.end(); emojiId++) { + auto interaction = find(*emojiId); + if (interaction != interactions_.end()) { + auto author = interaction->second.authorUri; + auto body = interaction->second.body; + if (!body.isEmpty()) { + auto itAuthor = mapStringEmoji.find(author); + if (itAuthor != mapStringEmoji.end()) { + mapStringEmoji[author].append(body); + } else { + QStringList emojiList; + emojiList.append(body); + mapStringEmoji.insert(author, emojiList); + } + } + } + } + for (auto i = mapStringEmoji.begin(); i != mapStringEmoji.end(); i++) { + convertedMap.insert(i.key(), i.value()); } + return convertedMap; } void @@ -635,6 +695,19 @@ MessageListModel::editMessage(const QString& msgId, interaction::Info& info) } } +void +MessageListModel::reactToMessage(const QString& msgId, interaction::Info& info) +{ + // If already there, we can update the content + auto itReact = reactedMessages_.find(msgId); + + if (itReact != reactedMessages_.end()) { + auto convertedMap = convertReactMessagetoQVariant(reactedMessages_[msgId]); + info.reactions = convertedMap; + emitDataChanged(find(msgId), {Role::Reactions}); + } +} + QString MessageListModel::lastMessageUid() const { @@ -653,12 +726,27 @@ MessageListModel::lastSelfMessageId() const { for (auto it = interactions_.rbegin(); it != interactions_.rend(); ++it) { auto lastType = it->second.type; - if (lastType == interaction::Type::TEXT - and !it->second.body.isEmpty() and it->second.authorUri.isEmpty()) { + if (lastType == interaction::Type::TEXT and !it->second.body.isEmpty() + and it->second.authorUri.isEmpty()) { return it->first; } } return {}; } +QString +MessageListModel::findEmojiReaction(const QString& emoji, + const QString& authorURI, + const QString& messageId) +{ + auto& messageReactions = reactedMessages_[messageId]; + for (auto it = messageReactions.begin(); it != messageReactions.end(); it++) { + auto interaction = find(*it); + if (interaction != interactions_.end() && interaction->second.body == emoji + && interaction->second.authorUri == authorURI) { + return *it; + } + } + return {}; +} } // namespace lrc diff --git a/src/libclient/messagelistmodel.h b/src/libclient/messagelistmodel.h index 6c367bf77..2a86affde 100644 --- a/src/libclient/messagelistmodel.h +++ b/src/libclient/messagelistmodel.h @@ -48,6 +48,7 @@ struct Info; X(LinkPreviewInfo) \ X(Linkified) \ X(PreviousBodies) \ + X(Reactions) \ X(ReplyTo) \ X(ReplyToBody) \ X(ReplyToAuthor) \ @@ -136,10 +137,17 @@ public: Q_SIGNAL void timestampUpdate(); void addEdition(const QString& msgId, const interaction::Info& info, bool end); + void addReaction(const QString& messageId, const QString& reactionId); void editMessage(const QString& msgId, interaction::Info& info); + void reactToMessage(const QString& msgId, interaction::Info& info); + QVariantMap convertReactMessagetoQVariant(const QSet<QString>&); QString lastMessageUid() const; QString lastSelfMessageId() const; + QString findEmojiReaction(const QString& emoji, + const QString& authorURI, + const QString& messageId); + protected: using Role = MessageList::Role; @@ -156,6 +164,9 @@ private: void updateReplies(item_t& message); QMap<QString, QVector<interaction::Body>> editedBodies_; + // key = messageId and values = QSet of reactionIds + QMap<QString, QSet<QString>> reactedMessages_; + void moveMessage(const QString& msgId, const QString& parentId); void insertMessage(int index, item_t& message); iterator insertMessage(iterator it, item_t& message); diff --git a/src/libclient/qtwrapper/conversions_wrap.hpp b/src/libclient/qtwrapper/conversions_wrap.hpp index 7389dc596..9e9dd1fb3 100644 --- a/src/libclient/qtwrapper/conversions_wrap.hpp +++ b/src/libclient/qtwrapper/conversions_wrap.hpp @@ -53,6 +53,16 @@ mapStringStringToQVariantMap(const MapStringString& map) return convertedMap; } +inline QVariantMap +mapStringIntToQVariantMap(const MapStringInt& map) +{ + QVariantMap convertedMap; + for (auto i = map.begin(); i != map.end(); i++) { + convertedMap.insert(i.key(), i.value()); + } + return convertedMap; +} + inline MapStringString convertMap(const std::map<std::string, std::string>& m) { diff --git a/src/libclient/typedefs.h b/src/libclient/typedefs.h index 45fd32c84..b84c47eda 100644 --- a/src/libclient/typedefs.h +++ b/src/libclient/typedefs.h @@ -47,6 +47,12 @@ constexpr static const char* TEXT_PLAIN = "text/plain"; constexpr static const char* APPLICATION_GEO = "application/geo"; constexpr static const char* FALSE_STR = "false"; +enum class MessageFlag : int { + Text = 0, + Reply = 1, + Reaction = 2, +}; + // Adapted from libring libjami::DataTransferInfo struct DataTransferInfo { -- GitLab