mirror of
https://git.jami.net/savoirfairelinux/jami-client-qt.git
synced 2025-03-28 14:56:19 +01:00
Emoji: Implement emoji-reactions
Change-Id: I83f27e86a6a7ee2140dc3028a4e183dcb8de8a27 GitLab: #875
This commit is contained in:
parent
719e3af445
commit
7bb5ad0ee0
25 changed files with 962 additions and 39 deletions
1
resources/icons/add_reaction.svg
Normal file
1
resources/icons/add_reaction.svg
Normal file
|
@ -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>
|
After Width: | Height: | Size: 933 B |
1
resources/icons/copy.svg
Normal file
1
resources/icons/copy.svg
Normal file
|
@ -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>
|
After Width: | Height: | Size: 279 B |
1
resources/icons/delete.svg
Normal file
1
resources/icons/delete.svg
Normal file
|
@ -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>
|
After Width: | Height: | Size: 263 B |
1
resources/icons/edit.svg
Normal file
1
resources/icons/edit.svg
Normal file
|
@ -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>
|
After Width: | Height: | Size: 257 B |
1
resources/icons/reply.svg
Normal file
1
resources/icons/reply.svg
Normal file
|
@ -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>
|
After Width: | Height: | Size: 216 B |
1
resources/icons/save_file.svg
Normal file
1
resources/icons/save_file.svg
Normal file
|
@ -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>
|
After Width: | Height: | Size: 369 B |
205
src/app/commoncomponents/ChatviewMessageOptions.qml
Normal file
205
src/app/commoncomponents/ChatviewMessageOptions.qml
Normal file
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
174
src/app/commoncomponents/EmojiReactionPopup.qml
Normal file
174
src/app/commoncomponents/EmojiReactionPopup.qml
Normal file
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
111
src/app/commoncomponents/EmojiReactions.qml
Normal file
111
src/app/commoncomponents/EmojiReactions.qml
Normal file
|
@ -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
|
||||
}
|
||||
}
|
63
src/app/commoncomponents/MessageOptionButton.qml
Normal file
63
src/app/commoncomponents/MessageOptionButton.qml
Normal file
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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 !== ""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"];
|
||||
|
|
|
@ -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++;
|
||||
}
|
||||
|
|
|
@ -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,9 +606,60 @@ 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
|
||||
MessageListModel::editMessage(const QString& msgId, interaction::Info& info)
|
||||
{
|
||||
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
Loading…
Add table
Reference in a new issue