1
0
Fork 0
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:
Nicolas Vengeon 2022-11-23 09:50:53 -05:00 committed by Adrien Béraud
parent 719e3af445
commit 7bb5ad0ee0
25 changed files with 962 additions and 39 deletions

View 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
View 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

View 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
View 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

View 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

View 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

View 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
}
}
}

View 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
}
}
}

View 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
}
}

View 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
}
}

View file

@ -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
}
}

View file

@ -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)

View file

@ -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")
}

View file

@ -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"

View file

@ -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 !== ""

View file

@ -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

View file

@ -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)
{

View file

@ -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);

View file

@ -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

View file

@ -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"];

View file

@ -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++;
}

View file

@ -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

View file

@ -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);

View file

@ -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)
{

View file

@ -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
{