From 179b0f5a36d6db23a543bb6f5c7589dbb3e8bcdd Mon Sep 17 00:00:00 2001 From: Sale Djenic Date: Mon, 20 Dec 2021 15:21:35 +0100 Subject: [PATCH] refactor(@desktop/chat-messages): add/remove reactions --- .../chat_section/chat_content/controller.nim | 12 ++ .../chat_content/messages/controller.nim | 28 ++-- .../messages/controller_interface.nim | 2 +- .../chat_content/messages/module.nim | 54 ++++--- .../module_controller_delegate_interface.nim | 2 +- .../module_view_delegate_interface.nim | 4 - .../chat_content/messages/view.nim | 3 - .../main/chat_section/chat_content/module.nim | 29 +++- .../module_controller_delegate_interface.nim | 6 + .../modules/shared_models/message_item.nim | 79 +++------- .../modules/shared_models/message_model.nim | 28 ++-- .../shared_models/message_reaction_item.nim | 88 +++++++++++ .../shared_models/message_reaction_model.nim | 146 ++++++++++++++++++ src/app_service/service/message/service.nim | 34 ++-- .../Chat/popups/PinnedMessagesPopup.qml | 6 + .../AppLayouts/Chat/stores/MessageStore.qml | 53 +++++++ ui/app/AppLayouts/Chat/stores/RootStore.qml | 40 ----- .../AppLayouts/Chat/views/ChatContentView.qml | 4 + .../Chat/views/ChatMessagesView.qml | 1 + .../panels/chat/EmojiReactionsPanel.qml | 15 +- .../shared/views/chat/CompactMessageView.qml | 9 +- .../views/chat/MessageContextMenuView.qml | 4 +- ui/imports/shared/views/chat/MessageView.qml | 38 +---- .../shared/views/chat/StatusUpdateView.qml | 16 +- 24 files changed, 475 insertions(+), 226 deletions(-) create mode 100644 src/app/modules/shared_models/message_reaction_item.nim create mode 100644 src/app/modules/shared_models/message_reaction_model.nim diff --git a/src/app/modules/main/chat_section/chat_content/controller.nim b/src/app/modules/main/chat_section/chat_content/controller.nim index c49e3b0bb3..3d15934bc1 100644 --- a/src/app/modules/main/chat_section/chat_content/controller.nim +++ b/src/app/modules/main/chat_section/chat_content/controller.nim @@ -74,6 +74,18 @@ method init*(self: Controller) = return self.delegate.onChatUnmuted() + self.events.on(SIGNAL_MESSAGE_REACTION_ADDED) do(e:Args): + let args = MessageAddRemoveReactionArgs(e) + if(self.chatId != args.chatId): + return + self.delegate.onReactionAdded(args.messageId, args.emojiId, args.reactionId) + + self.events.on(SIGNAL_MESSAGE_REACTION_REMOVED) do(e:Args): + let args = MessageAddRemoveReactionArgs(e) + if(self.chatId != args.chatId): + return + self.delegate.onReactionRemoved(args.messageId, args.emojiId, args.reactionId) + method getMyChatId*(self: Controller): string = return self.chatId diff --git a/src/app/modules/main/chat_section/chat_content/messages/controller.nim b/src/app/modules/main/chat_section/chat_content/messages/controller.nim index 8abf09a0ce..925e3e4fe9 100644 --- a/src/app/modules/main/chat_section/chat_content/messages/controller.nim +++ b/src/app/modules/main/chat_section/chat_content/messages/controller.nim @@ -58,6 +58,18 @@ method init*(self: Controller) = return self.delegate.onPinUnpinMessage(args.messageId, false) + self.events.on(SIGNAL_MESSAGE_REACTION_ADDED) do(e:Args): + let args = MessageAddRemoveReactionArgs(e) + if(self.chatId != args.chatId): + return + self.delegate.onReactionAdded(args.messageId, args.emojiId, args.reactionId) + + self.events.on(SIGNAL_MESSAGE_REACTION_REMOVED) do(e:Args): + let args = MessageAddRemoveReactionArgs(e) + if(self.chatId != args.chatId): + return + self.delegate.onReactionRemoved(args.messageId, args.emojiId, args.reactionId) + method getChatId*(self: Controller): string = return self.chatId @@ -71,20 +83,10 @@ method belongsToCommunity*(self: Controller): bool = return self.belongsToCommunity method addReaction*(self: Controller, messageId: string, emojiId: int) = - let (res, err) = self.messageService.addReaction(self.chatId, messageId, emojiId) - if(err.len != 0): - error "an error has occurred while saving reaction: ", err - return + self.messageService.addReaction(self.chatId, messageId, emojiId) - self.delegate.onReactionAdded(messageId, emojiId, res) - -method removeReaction*(self: Controller, messageId: string, reactionId: string) = - let (res, err) = self.messageService.removeReaction(reactionId) - if(err.len != 0): - error "an error has occurred while removing reaction: ", err - return - - self.delegate.onReactionRemoved(messageId, reactionId) +method removeReaction*(self: Controller, messageId: string, emojiId: int, reactionId: string) = + self.messageService.removeReaction(reactionId, self.chatId, messageId, emojiId) method pinUnpinMessage*(self: Controller, messageId: string, pin: bool) = self.messageService.pinUnpinMessage(self.chatId, messageId, pin) diff --git a/src/app/modules/main/chat_section/chat_content/messages/controller_interface.nim b/src/app/modules/main/chat_section/chat_content/messages/controller_interface.nim index b71c3b3830..fa742c87c8 100644 --- a/src/app/modules/main/chat_section/chat_content/messages/controller_interface.nim +++ b/src/app/modules/main/chat_section/chat_content/messages/controller_interface.nim @@ -27,7 +27,7 @@ method belongsToCommunity*(self: AccessInterface): bool {.base.} = method addReaction*(self: AccessInterface, messageId: string, emojiId: int) {.base.} = raise newException(ValueError, "No implementation available") -method removeReaction*(self: AccessInterface, messageId: string, reactionId: string) {.base.} = +method removeReaction*(self: AccessInterface, messageId: string, emojiId: int, reactionId: string) {.base.} = raise newException(ValueError, "No implementation available") method pinUnpinMessage*(self: AccessInterface, messageId: string, pin: bool) {.base.} = diff --git a/src/app/modules/main/chat_section/chat_content/messages/module.nim b/src/app/modules/main/chat_section/chat_content/messages/module.nim index dbbffe8dcd..aa4626af55 100644 --- a/src/app/modules/main/chat_section/chat_content/messages/module.nim +++ b/src/app/modules/main/chat_section/chat_content/messages/module.nim @@ -1,9 +1,10 @@ -import NimQml +import NimQml, chronicles import io_interface import ../io_interface as delegate_interface import view, controller import ../../../../shared_models/message_model import ../../../../shared_models/message_item +import ../../../../shared_models/message_reaction_item import ../../../../../global/global_singleton import ../../../../../../app_service/service/contacts/service as contact_service @@ -14,6 +15,9 @@ import eventemitter export io_interface +logScope: + topics = "messages-module" + const CHAT_IDENTIFIER_MESSAGE_ID = "chat-identifier-message-id" type @@ -92,7 +96,14 @@ method newMessagesLoaded*(self: Module, messages: seq[MessageDto], reactions: se for r in reactions: if(r.messageId == m.id): - item.addReaction(r.emojiId, m.`from`, r.id) + var emojiIdAsEnum: EmojiId + if(message_reaction_item.toEmojiIdAsEnum(r.emojiId, emojiIdAsEnum)): + let userWhoAddedThisReaction = self.controller.getContactById(r.`from`) + let didIReactWithThisEmoji = userWhoAddedThisReaction.id == singletonInstance.userProfile.getPubKey() + item.addReaction(emojiIdAsEnum, didIReactWithThisEmoji, userWhoAddedThisReaction.id, + userWhoAddedThisReaction.userNameOrAlias(), r.id) + else: + error "wrong emoji id found when loading messages" for p in pinnedMessages: if(p.message.id == m.id): @@ -109,26 +120,33 @@ method newMessagesLoaded*(self: Module, messages: seq[MessageDto], reactions: se self.view.model().prependItems(viewItems) method toggleReaction*(self: Module, messageId: string, emojiId: int) = - let item = self.view.model().getItemWithMessageId(messageId) - let myName = singletonInstance.userProfile.getName() - if(item.shouldAddReaction(emojiId, myName)): - self.controller.addReaction(messageId, emojiId) + var emojiIdAsEnum: EmojiId + if(message_reaction_item.toEmojiIdAsEnum(emojiId, emojiIdAsEnum)): + let item = self.view.model().getItemWithMessageId(messageId) + let myPublicKey = singletonInstance.userProfile.getPubKey() + if(item.shouldAddReaction(emojiIdAsEnum, myPublicKey)): + self.controller.addReaction(messageId, emojiId) + else: + let reactionId = item.getReactionId(emojiIdAsEnum, myPublicKey) + self.controller.removeReaction(messageId, emojiId, reactionId) else: - let reactionId = item.getReactionId(emojiId, myName) - self.controller.removeReaction(messageId, reactionId) + error "wrong emoji id found on reaction added response", emojiId method onReactionAdded*(self: Module, messageId: string, emojiId: int, reactionId: string) = - let myName = singletonInstance.userProfile.getName() - self.view.model().addReaction(messageId, emojiId, myName, reactionId) + var emojiIdAsEnum: EmojiId + if(message_reaction_item.toEmojiIdAsEnum(emojiId, emojiIdAsEnum)): + let myPublicKey = singletonInstance.userProfile.getPubKey() + let myName = singletonInstance.userProfile.getName() + self.view.model().addReaction(messageId, emojiIdAsEnum, true, myPublicKey, myName, reactionId) + else: + error "wrong emoji id found on reaction added response", emojiId -method onReactionRemoved*(self: Module, messageId: string, reactionId: string) = - self.view.model().removeReaction(messageId, reactionId) - -method getNamesReactedWithEmojiIdForMessageId*(self: Module, messageId: string, emojiId: int): seq[string] = - let pubKeysForEmojiId = self.view.model().getPubKeysReactedWithEmojiIdForMessageId(messageId, emojiId) - for pk in pubKeysForEmojiId: - let (name, _, _) = self.controller.getContactNameAndImage(pk) - result.add(name) +method onReactionRemoved*(self: Module, messageId: string, emojiId: int, reactionId: string) = + var emojiIdAsEnum: EmojiId + if(message_reaction_item.toEmojiIdAsEnum(emojiId, emojiIdAsEnum)): + self.view.model().removeReaction(messageId, emojiIdAsEnum, reactionId) + else: + error "wrong emoji id found on reaction remove response", emojiId method pinUnpinMessage*(self: Module, messageId: string, pin: bool) = self.controller.pinUnpinMessage(messageId, pin) diff --git a/src/app/modules/main/chat_section/chat_content/messages/private_interfaces/module_controller_delegate_interface.nim b/src/app/modules/main/chat_section/chat_content/messages/private_interfaces/module_controller_delegate_interface.nim index d37f9acf90..dcf526b5cf 100644 --- a/src/app/modules/main/chat_section/chat_content/messages/private_interfaces/module_controller_delegate_interface.nim +++ b/src/app/modules/main/chat_section/chat_content/messages/private_interfaces/module_controller_delegate_interface.nim @@ -7,7 +7,7 @@ method newMessagesLoaded*(self: AccessInterface, messages: seq[MessageDto], reac method onReactionAdded*(self: AccessInterface, messageId: string, emojiId: int, reactionId: string) {.base.} = raise newException(ValueError, "No implementation available") -method onReactionRemoved*(self: AccessInterface, messageId: string, reactionId: string) {.base.} = +method onReactionRemoved*(self: AccessInterface, messageId: string, emojiId: int, reactionId: string) {.base.} = raise newException(ValueError, "No implementation available") method onPinUnpinMessage*(self: AccessInterface, messageId: string, pin: bool) {.base.} = diff --git a/src/app/modules/main/chat_section/chat_content/messages/private_interfaces/module_view_delegate_interface.nim b/src/app/modules/main/chat_section/chat_content/messages/private_interfaces/module_view_delegate_interface.nim index dcd8eeb224..48b60d135a 100644 --- a/src/app/modules/main/chat_section/chat_content/messages/private_interfaces/module_view_delegate_interface.nim +++ b/src/app/modules/main/chat_section/chat_content/messages/private_interfaces/module_view_delegate_interface.nim @@ -7,10 +7,6 @@ method toggleReaction*(self: AccessInterface, messageId: string, emojiId: int) { method pinUnpinMessage*(self: AccessInterface, messageId: string, pin: bool) {.base.} = raise newException(ValueError, "No implementation available") -method getNamesReactedWithEmojiIdForMessageId*(self: AccessInterface, messageId: string, emojiId: int): seq[string] - {.base.} = - raise newException(ValueError, "No implementation available") - method getChatType*(self: AccessInterface): int {.base.} = raise newException(ValueError, "No implementation available") diff --git a/src/app/modules/main/chat_section/chat_content/messages/view.nim b/src/app/modules/main/chat_section/chat_content/messages/view.nim index 69256c3fd5..cb640e2b3f 100644 --- a/src/app/modules/main/chat_section/chat_content/messages/view.nim +++ b/src/app/modules/main/chat_section/chat_content/messages/view.nim @@ -36,9 +36,6 @@ QtObject: proc toggleReaction*(self: View, messageId: string, emojiId: int) {.slot.} = self.delegate.toggleReaction(messageId, emojiId) - proc getNamesReactedWithEmojiIdForMessageId*(self: View, messageId: string, emojiId: int): string {.slot.} = - return $(%* self.delegate.getNamesReactedWithEmojiIdForMessageId(messageId, emojiId)) - proc pinMessage*(self: View, messageId: string) {.slot.} = self.delegate.pinUnpinMessage(messageId, true) diff --git a/src/app/modules/main/chat_section/chat_content/module.nim b/src/app/modules/main/chat_section/chat_content/module.nim index ee49421b42..0b11074c87 100644 --- a/src/app/modules/main/chat_section/chat_content/module.nim +++ b/src/app/modules/main/chat_section/chat_content/module.nim @@ -4,6 +4,7 @@ import ../io_interface as delegate_interface import view, controller import ../../../shared_models/message_model as pinned_msg_model import ../../../shared_models/message_item as pinned_msg_item +import ../../../shared_models/message_reaction_item as pinned_msg_reaction_item import ../../../../global/global_singleton import input_area/module as input_area_module @@ -151,8 +152,14 @@ proc buildPinnedMessageItem(self: Module, messageId: string, item: var pinned_ms for r in reactions: if(r.messageId == m.id): - # m.`from` should be replaced by appropriate ens/alias when we have that part refactored - item.addReaction(r.emojiId, m.`from`, r.id) + var emojiIdAsEnum: pinned_msg_reaction_item.EmojiId + if(pinned_msg_reaction_item.toEmojiIdAsEnum(r.emojiId, emojiIdAsEnum)): + let userWhoAddedThisReaction = self.controller.getContactById(r.`from`) + let didIReactWithThisEmoji = userWhoAddedThisReaction.id == singletonInstance.userProfile.getPubKey() + item.addReaction(emojiIdAsEnum, didIReactWithThisEmoji, userWhoAddedThisReaction.id, + userWhoAddedThisReaction.userNameOrAlias(), r.id) + else: + error "wrong emoji id found when loading messages" return true @@ -193,4 +200,20 @@ method onChatMuted*(self: Module) = self.view.setMuted(true) method onChatUnmuted*(self: Module) = - self.view.setMuted(false) \ No newline at end of file + self.view.setMuted(false) + +method onReactionAdded*(self: Module, messageId: string, emojiId: int, reactionId: string) = + var emojiIdAsEnum: EmojiId + if(pinned_msg_reaction_item.toEmojiIdAsEnum(emojiId, emojiIdAsEnum)): + let myPublicKey = singletonInstance.userProfile.getPubKey() + let myName = singletonInstance.userProfile.getName() + self.view.pinnedModel().addReaction(messageId, emojiIdAsEnum, true, myPublicKey, myName, reactionId) + else: + error "(pinned) wrong emoji id found on reaction added response", emojiId + +method onReactionRemoved*(self: Module, messageId: string, emojiId: int, reactionId: string) = + var emojiIdAsEnum: EmojiId + if(pinned_msg_reaction_item.toEmojiIdAsEnum(emojiId, emojiIdAsEnum)): + self.view.pinnedModel().removeReaction(messageId, emojiIdAsEnum, reactionId) + else: + error "(pinned) wrong emoji id found on reaction remove response", emojiId \ No newline at end of file diff --git a/src/app/modules/main/chat_section/chat_content/private_interfaces/module_controller_delegate_interface.nim b/src/app/modules/main/chat_section/chat_content/private_interfaces/module_controller_delegate_interface.nim index c62cb5567e..8125efb0d1 100644 --- a/src/app/modules/main/chat_section/chat_content/private_interfaces/module_controller_delegate_interface.nim +++ b/src/app/modules/main/chat_section/chat_content/private_interfaces/module_controller_delegate_interface.nim @@ -14,3 +14,9 @@ method onChatMuted*(self: AccessInterface) {.base.} = method onChatUnmuted*(self: AccessInterface) {.base.} = raise newException(ValueError, "No implementation available") + +method onReactionAdded*(self: AccessInterface, messageId: string, emojiId: int, reactionId: string) {.base.} = + raise newException(ValueError, "No implementation available") + +method onReactionRemoved*(self: AccessInterface, messageId: string, emojiId: int, reactionId: string) {.base.} = + raise newException(ValueError, "No implementation available") \ No newline at end of file diff --git a/src/app/modules/shared_models/message_item.nim b/src/app/modules/shared_models/message_item.nim index 20d56c7e29..9217d95658 100644 --- a/src/app/modules/shared_models/message_item.nim +++ b/src/app/modules/shared_models/message_item.nim @@ -1,4 +1,6 @@ -import Tables, json, strformat +import json, strformat + +import message_reaction_model, message_reaction_item type ContentType* {.pure.} = enum @@ -38,8 +40,7 @@ type timestamp: int64 contentType: ContentType messageType: int - reactions: OrderedTable[int, seq[tuple[publicKey: string, reactionId: string]]] # [emojiId, list of [user publicKey reacted with the emojiId, reaction id]] - reactionIds: seq[string] + reactionsModel: MessageReactionModel pinned: bool proc initItem*(id, responseToMessageWithId, senderId, senderDisplayName, senderLocalName, senderIcon: string, @@ -62,6 +63,7 @@ proc initItem*(id, responseToMessageWithId, senderId, senderDisplayName, senderL result.contentType = contentType result.messageType = messageType result.pinned = false + result.reactionsModel = newMessageReactionModel() proc `$`*(self: Item): string = result = fmt"""Item( @@ -79,7 +81,8 @@ proc `$`*(self: Item): string = timestamp:{$self.timestamp}, contentType:{$self.contentType.int}, messageType:{$self.messageType}, - pinned:{$self.pinned} + pinned:{$self.pinned}, + messageReactions: [{$self.reactionsModel}] )""" proc id*(self: Item): string {.inline.} = @@ -139,67 +142,21 @@ proc pinned*(self: Item): bool {.inline.} = proc `pinned=`*(self: Item, value: bool) {.inline.} = self.pinned = value -proc shouldAddReaction*(self: Item, emojiId: int, publicKey: string): bool = - for k, values in self.reactions: - if(k != emojiId): - continue +proc reactionsModel*(self: Item): MessageReactionModel {.inline.} = + self.reactionsModel - for t in values: - if(t.publicKey == publicKey): - return false +proc shouldAddReaction*(self: Item, emojiId: EmojiId, userPublicKey: string): bool = + return self.reactionsModel.shouldAddReaction(emojiId, userPublicKey) - return true +proc getReactionId*(self: Item, emojiId: EmojiId, userPublicKey: string): string = + return self.reactionsModel.getReactionId(emojiId, userPublicKey) -proc getReactionId*(self: Item, emojiId: int, publicKey: string): string = - for k, values in self.reactions: - if(k != emojiId): - continue +proc addReaction*(self: Item, emojiId: EmojiId, didIReactWithThisEmoji: bool, userPublicKey: string, + userDisplayName: string, reactionId: string) = + self.reactionsModel.addReaction(emojiId, didIReactWithThisEmoji, userPublicKey, userDisplayName, reactionId) - for t in values: - if(t.publicKey == publicKey): - return t.reactionId - - # we should never be here, since this is a controlled call - return "" - -proc addReaction*(self: Item, emojiId: int, publicKey: string, reactionId: string) = - if(not self.reactions.contains(emojiId)): - self.reactions[emojiId] = @[] - - self.reactions[emojiId].add((publicKey, reactionId)) - -proc removeReaction*(self: Item, reactionId: string) = - var key = -1 - var index = -1 - for k, values in self.reactions: - var i = -1 - for t in values: - i += 1 - if(t.reactionId == reactionId): - key = k - index = i - break - - if(key == -1 or index == -1): - return - - self.reactions[key].del(index) - if(self.reactions[key].len == 0): - self.reactions.del(key) - -proc getPubKeysReactedWithEmojiId*(self: Item, emojiId: int): seq[string] = - if(not self.reactions.contains(emojiId)): - return - - for v in self.reactions[emojiId]: - result.add(v.publicKey) - -proc getCountsForReactions*(self: Item): seq[JsonNode] = - for k, v in self.reactions: - if(self.reactions[k].len == 0): - continue - - result.add(%* {"emojiId": k, "counts": v.len}) +proc removeReaction*(self: Item, emojiId: EmojiId, reactionId: string) = + self.reactionsModel.removeReaction(emojiId, reactionId) proc toJsonNode*(self: Item): JsonNode = result = %* { diff --git a/src/app/modules/shared_models/message_model.nim b/src/app/modules/shared_models/message_model.nim index 9a15274a28..ec94e56e16 100644 --- a/src/app/modules/shared_models/message_model.nim +++ b/src/app/modules/shared_models/message_model.nim @@ -1,6 +1,6 @@ import NimQml, Tables, json, strutils, strformat -import message_item +import message_item, message_reaction_item type ModelRole {.pure.} = enum @@ -24,7 +24,7 @@ type # GapFrom # GapTo Pinned - CountsForReactions + Reactions QtObject: type @@ -80,7 +80,7 @@ QtObject: # ModelRole.GapFrom.int:"gapFrom", # ModelRole.GapTo.int:"gapTo", ModelRole.Pinned.int:"pinned", - ModelRole.CountsForReactions.int:"countsForReactions", + ModelRole.Reactions.int:"reactions", }.toTable method data(self: Model, index: QModelIndex, role: int): QVariant = @@ -134,8 +134,8 @@ QtObject: # result = newQVariant(item.gapTo) of ModelRole.Pinned: result = newQVariant(item.pinned) - of ModelRole.CountsForReactions: - result = newQVariant($(%* item.getCountsForReactions)) + of ModelRole.Reactions: + result = newQVariant(item.reactionsModel) proc findIndexForMessageId(self: Model, messageId: string): int = for i in 0 ..< self.items.len: @@ -187,30 +187,26 @@ QtObject: return self.items[ind] - proc addReaction*(self: Model, messageId: string, emojiId: int, name: string, reactionId: string) = + proc addReaction*(self: Model, messageId: string, emojiId: EmojiId, didIReactWithThisEmoji: bool, + userPublicKey: string, userDisplayName: string, reactionId: string) = let ind = self.findIndexForMessageId(messageId) if(ind == -1): return - self.items[ind].addReaction(emojiId, name, reactionId) + self.items[ind].addReaction(emojiId, didIReactWithThisEmoji, userPublicKey, userDisplayName, reactionId) let index = self.createIndex(ind, 0, nil) - self.dataChanged(index, index, @[ModelRole.CountsForReactions.int]) + self.dataChanged(index, index, @[ModelRole.Reactions.int]) - proc removeReaction*(self: Model, messageId: string, reactionId: string) = + proc removeReaction*(self: Model, messageId: string, emojiId: EmojiId, reactionId: string) = let ind = self.findIndexForMessageId(messageId) if(ind == -1): return - self.items[ind].removeReaction(reactionId) + self.items[ind].removeReaction(emojiId, reactionId) let index = self.createIndex(ind, 0, nil) - self.dataChanged(index, index, @[ModelRole.CountsForReactions.int]) - - proc getPubKeysReactedWithEmojiIdForMessageId*(self: Model, messageId: string, emojiId: int): seq[string] = - for i in 0 ..< self.items.len: - if(self.items[i].id == messageId): - return self.items[i].getPubKeysReactedWithEmojiId(emojiId) + self.dataChanged(index, index, @[ModelRole.Reactions.int]) proc pinUnpinMessage*(self: Model, messageId: string, pin: bool) = let ind = self.findIndexForMessageId(messageId) diff --git a/src/app/modules/shared_models/message_reaction_item.nim b/src/app/modules/shared_models/message_reaction_item.nim new file mode 100644 index 0000000000..1d362d890f --- /dev/null +++ b/src/app/modules/shared_models/message_reaction_item.nim @@ -0,0 +1,88 @@ +import json, strformat + +type + EmojiId* {.pure.} = enum + Heart = 1, + Thumbsup, + Thumbsdown, + Laughing, + Cry, + Angry + +type + ReactionDetails* = object + publicKey: string + displayName: string + reactionId: string + +type + MessageReactionItem* = object + emojiId: EmojiId + didIReactWithThisEmoji: bool + reactions: seq[ReactionDetails] + +proc toEmojiIdAsEnum*(emojiId: int, emojiIdAsEnum: var EmojiId): bool = + if(emojiId >= ord(low(EmojiId)) or emojiId <= ord(high(EmojiId))): + emojiIdAsEnum = EmojiId(emojiId) + return true + return false + +proc initMessageReactionItem*(emojiId: EmojiId): MessageReactionItem = + result.emojiId = emojiId + +proc `$`*(self: MessageReactionItem): string = + var reactions = "" + for r in self.reactions: + reactions = reactions & "displayName: " & r.displayName & " publicKey: " & r.publicKey & " reactionId: " & + r.reactionId & "\n" + + result = fmt"""MessageReactionItem( + emojiId: {self.emojiId}, + didIReactWithThisEmoji: {self.didIReactWithThisEmoji}, + reactionsCount: {self.reactions.len}, + reactions: {reactions} + ]""" + +proc emojiId*(self: MessageReactionItem): EmojiId {.inline.} = + self.emojiId + +proc didIReactWithThisEmoji*(self: MessageReactionItem): bool {.inline.} = + self.didIReactWithThisEmoji + +proc numberOfReactions*(self: MessageReactionItem): int {.inline.} = + self.reactions.len + +proc jsonArrayOfUsersReactedWithThisEmoji*(self: MessageReactionItem): JsonNode {.inline.} = + var users: seq[string] + for r in self.reactions: + users.add(r.displayName) + return %* users + +proc shouldAddReaction*(self: MessageReactionItem, userPublicKey: string): bool = + for r in self.reactions: + if (r.publicKey == userPublicKey): + return false + return true + +proc getReactionId*(self: MessageReactionItem, userPublicKey: string): string = + for r in self.reactions: + if (r.publicKey == userPublicKey): + return r.reactionId + return "" + +proc addReaction*(self: var MessageReactionItem, didIReactWithThisEmoji: bool, userPublicKey: string, + userDisplayName: string, reactionId: string) = + self.didIReactWithThisEmoji = didIReactWithThisEmoji + self.reactions.add(ReactionDetails(publicKey: userPublicKey, displayName: userDisplayName, reactionId: reactionId)) + +proc removeReaction*(self: var MessageReactionItem, reactionId: string) = + var index = -1 + for i in 0..= self.items.len): + return + + let item = self.items[index.row] + let enumRole = role.ModelRole + + case enumRole: + of ModelRole.EmojiId: + result = newQVariant(item.emojiId.int) + of ModelRole.DidIReactWithThisEmoji: + result = newQVariant(item.didIReactWithThisEmoji) + of ModelRole.NumberOfReactions: + result = newQVariant(item.numberOfReactions) + of ModelRole.JsonArrayOfUsersReactedWithThisEmoji: + # Would be good if we could return QVariant of array (seq) here, but it's not supported in our NimQml, + # because of that we're returning json array as a string. + result = newQVariant($item.jsonArrayOfUsersReactedWithThisEmoji) + + proc reactionItemWithEmojiIdExists(self: MessageReactionModel, emojiId: EmojiId): bool = + for it in self.items: + if(it.emojiId == emojiId): + return true + return false + + proc getIndexOfTheItemWithEmojiId(self: MessageReactionModel, emojiId: EmojiId): int = + for i in 0.. 0): - result.result = reactions[0].id + reactionId = reactions[0].id + + let data = MessageAddRemoveReactionArgs(chatId: chatId, messageId: messageId, emojiId: emojiId, + reactionId: reactionId) + self.events.emit(SIGNAL_MESSAGE_REACTION_ADDED, data) except Exception as e: - result.error = e.msg error "error: ", methodName="addReaction", errName = e.name, errDesription = e.msg - proc removeReaction*(self: Service, reactionId: string): tuple[result: string, error: string] = + proc removeReaction*(self: Service, reactionId: string, chatId: string, messageId: string, emojiId: int) = try: let response = status_go.removeReaction(reactionId) - result.error = "response doesn't contain \"error\"" if(response.result.contains("error")): - result.error = response.result["error"].getStr + let errMsg = response.result["error"].getStr + error "error: ", methodName="removeReaction", errDesription = errMsg return + let data = MessageAddRemoveReactionArgs(chatId: chatId, messageId: messageId, emojiId: emojiId, + reactionId: reactionId) + self.events.emit(SIGNAL_MESSAGE_REACTION_REMOVED, data) + except Exception as e: - result.error = e.msg error "error: ", methodName="removeReaction", errName = e.name, errDesription = e.msg proc pinUnpinMessage*(self: Service, chatId: string, messageId: string, pin: bool) = diff --git a/ui/app/AppLayouts/Chat/popups/PinnedMessagesPopup.qml b/ui/app/AppLayouts/Chat/popups/PinnedMessagesPopup.qml index 71facec3ea..a539c097a7 100644 --- a/ui/app/AppLayouts/Chat/popups/PinnedMessagesPopup.qml +++ b/ui/app/AppLayouts/Chat/popups/PinnedMessagesPopup.qml @@ -125,6 +125,7 @@ ModalPopup { messageOutgoingStatus: model.outgoingStatus messageContentType: model.contentType pinnedMessage: model.pinned + reactionsModel: model.reactions // This is possible since we have all data loaded before we load qml. // When we fetch messages to fulfill a gap we have to set them at once. @@ -163,6 +164,7 @@ ModalPopup { } MessageContextMenuView { id: msgContextMenu + reactionModel: root.rootStore.emojiReactionsModel pinnedPopup: true pinnedMessage: true onShouldCloseParentPopup: { @@ -176,6 +178,10 @@ ModalPopup { onUnpinMessage: { popup.messageStore.unpinMessage(messageId) } + + onToggleReaction: { + popup.messageStore.toggleReaction(messageId, emojiId) + } } } diff --git a/ui/app/AppLayouts/Chat/stores/MessageStore.qml b/ui/app/AppLayouts/Chat/stores/MessageStore.qml index 9604f27f9c..c63623f13d 100644 --- a/ui/app/AppLayouts/Chat/stores/MessageStore.qml +++ b/ui/app/AppLayouts/Chat/stores/MessageStore.qml @@ -77,4 +77,57 @@ QtObject { return messageModule.unpinMessage(messageId) } + + function toggleReaction(messageId, emojiId) { + if(!messageModule) + return + + return messageModule.toggleReaction(messageId, emojiId) + } + + function lastTwoItems(nodes) { + //% " and " + return nodes.join(qsTrId("-and-")); + } + + function showReactionAuthors(jsonArrayOfUsersReactedWithThisEmoji, emojiId) { + let listOfUsers = JSON.parse(jsonArrayOfUsersReactedWithThisEmoji) + if (listOfUsers.error) { + console.error("error parsing users who reacted to a message, error: ", obj.error) + return + } + + let tooltip + if (listOfUsers.length === 1) { + tooltip = listOfUsers[0] + } else if (listOfUsers.length === 2) { + tooltip = lastTwoItems(listOfUsers); + } else { + var leftNode = []; + var rightNode = []; + const maxReactions = 12 + let maximum = Math.min(maxReactions, listOfUsers.length) + + if (listOfUsers.length > maxReactions) { + leftNode = listOfUsers.slice(0, maxReactions); + rightNode = listOfUsers.slice(maxReactions, listOfUsers.length); + return (rightNode.length === 1) ? + lastTwoItems([leftNode.join(", "), rightNode[0]]) : + //% "%1 more" + lastTwoItems([leftNode.join(", "), qsTrId("-1-more").arg(rightNode.length)]); + } + + leftNode = listOfUsers.slice(0, maximum - 1); + rightNode = listOfUsers.slice(maximum - 1, listOfUsers.length); + tooltip = lastTwoItems([leftNode.join(", "), rightNode[0]]) + } + + //% " reacted with " + tooltip += qsTrId("-reacted-with-"); + let emojiHtml = Emoji.getEmojiFromId(emojiId); + if (emojiHtml) { + tooltip += emojiHtml; + } + return tooltip + } } diff --git a/ui/app/AppLayouts/Chat/stores/RootStore.qml b/ui/app/AppLayouts/Chat/stores/RootStore.qml index 7f569b1d6d..d17db70e02 100644 --- a/ui/app/AppLayouts/Chat/stores/RootStore.qml +++ b/ui/app/AppLayouts/Chat/stores/RootStore.qml @@ -67,46 +67,6 @@ QtObject { // chatsModelInst.messageView.deleteMessage(messageId); } - function lastTwoItems(nodes) { - //% " and " - return nodes.join(qsTrId("-and-")); - } - - function showReactionAuthors(fromAccounts, emojiId) { - let tooltip - if (fromAccounts.length === 1) { - tooltip = fromAccounts[0] - } else if (fromAccounts.length === 2) { - tooltip = lastTwoItems(fromAccounts); - } else { - var leftNode = []; - var rightNode = []; - const maxReactions = 12 - let maximum = Math.min(maxReactions, fromAccounts.length) - - if (fromAccounts.length > maxReactions) { - leftNode = fromAccounts.slice(0, maxReactions); - rightNode = fromAccounts.slice(maxReactions, fromAccounts.length); - return (rightNode.length === 1) ? - lastTwoItems([leftNode.join(", "), rightNode[0]]) : - //% "%1 more" - lastTwoItems([leftNode.join(", "), qsTrId("-1-more").arg(rightNode.length)]); - } - - leftNode = fromAccounts.slice(0, maximum - 1); - rightNode = fromAccounts.slice(maximum - 1, fromAccounts.length); - tooltip = lastTwoItems([leftNode.join(", "), rightNode[0]]) - } - - //% " reacted with " - tooltip += qsTrId("-reacted-with-"); - let emojiHtml = Emoji.getEmojiFromId(emojiId); - if (emojiHtml) { - tooltip += emojiHtml; - } - return tooltip - } - function getCommunity(communityId) { // Not Refactored Yet // try { diff --git a/ui/app/AppLayouts/Chat/views/ChatContentView.qml b/ui/app/AppLayouts/Chat/views/ChatContentView.qml index 8575689d23..52809207d9 100644 --- a/ui/app/AppLayouts/Chat/views/ChatContentView.qml +++ b/ui/app/AppLayouts/Chat/views/ChatContentView.qml @@ -207,6 +207,10 @@ ColumnLayout { messageToPin: messageId }) } + + onToggleReaction: { + messageStore.toggleReaction(messageId, emojiId) + } } StatusImageModal { diff --git a/ui/app/AppLayouts/Chat/views/ChatMessagesView.qml b/ui/app/AppLayouts/Chat/views/ChatMessagesView.qml index af83a6c288..2d50b47624 100644 --- a/ui/app/AppLayouts/Chat/views/ChatMessagesView.qml +++ b/ui/app/AppLayouts/Chat/views/ChatMessagesView.qml @@ -312,6 +312,7 @@ Item { messageOutgoingStatus: model.outgoingStatus messageContentType: model.contentType pinnedMessage: model.pinned + reactionsModel: model.reactions // This is possible since we have all data loaded before we load qml. // When we fetch messages to fulfill a gap we have to set them at once. diff --git a/ui/imports/shared/panels/chat/EmojiReactionsPanel.qml b/ui/imports/shared/panels/chat/EmojiReactionsPanel.qml index b4a61671aa..a1bbfe9361 100644 --- a/ui/imports/shared/panels/chat/EmojiReactionsPanel.qml +++ b/ui/imports/shared/panels/chat/EmojiReactionsPanel.qml @@ -19,6 +19,7 @@ Item { signal toggleReaction(int emojiID) signal setMessageActive(string messageId, bool active) + property var store property bool isCurrentUser property var emojiReactionsModel property bool isMessageActive @@ -38,14 +39,14 @@ Item { width: emojiImage.width + emojiCount.width + (root.imageMargin * 2) + + 8 height: 20 radius: 10 - color: modelData.currentUserReacted ? + color: model.didIReactWithThisEmoji ? (isHovered ? Style.current.emojiReactionActiveBackgroundHovered : Style.current.secondaryBackground) : (isHovered ? Style.current.emojiReactionBackgroundHovered : Style.current.emojiReactionBackground) StatusQ.StatusToolTip { visible: mouseArea.containsMouse maxWidth: 400 - text: showReactionAuthors(modelData.fromAccounts, modelData.emojiId) + text: root.store.showReactionAuthors(model.jsonArrayOfUsersReactedWithThisEmoji, model.emojiId) } // Rounded corner to cover one corner @@ -64,7 +65,7 @@ Item { // This is a workaround to get a "border" around the rectangle including the weird rectangle Loader { - active: modelData.currentUserReacted + active: model.didIReactWithThisEmoji anchors.top: parent.top anchors.topMargin: -1 anchors.left: parent.left @@ -100,7 +101,7 @@ Item { height: 15 fillMode: Image.PreserveAspectFit source: { - switch (modelData.emojiId) { + switch (model.emojiId) { case 1: return Style.svg("emojiReactions/heart") case 2: return Style.svg("emojiReactions/thumbsUp") case 3: return Style.svg("emojiReactions/thumbsDown") @@ -117,12 +118,12 @@ Item { StyledText { id: emojiCount - text: modelData.count + text: model.numberOfReactions anchors.verticalCenter: parent.verticalCenter anchors.left: emojiImage.right anchors.leftMargin: root.imageMargin font.pixelSize: 12 - color: modelData.currentUserReacted ? Style.current.textColorTertiary : Style.current.textColor + color: model.didIReactWithThisEmoji ? Style.current.textColorTertiary : Style.current.textColor } MouseArea { @@ -139,7 +140,7 @@ Item { emojiContainer.isHovered = false } onClicked: { - toggleReaction(modelData.emojiId) + toggleReaction(model.emojiId) } } } diff --git a/ui/imports/shared/views/chat/CompactMessageView.qml b/ui/imports/shared/views/chat/CompactMessageView.qml index 461eb6adcf..54c90b6db1 100644 --- a/ui/imports/shared/views/chat/CompactMessageView.qml +++ b/ui/imports/shared/views/chat/CompactMessageView.qml @@ -648,7 +648,7 @@ Item { Loader { id: emojiReactionLoader - active: emojiReactionsModel.length + active: reactionsModel.count > 0 anchors.bottom: messageContainer.bottom anchors.bottomMargin: Style.current.halfPadding anchors.left: messageContainer.left @@ -657,7 +657,8 @@ Item { sourceComponent: Component { EmojiReactionsPanel { id: emojiRect - emojiReactionsModel: emojiReactionsModel + store: messageStore + emojiReactionsModel: reactionsModel onHoverChanged: { setHovered(messageId, hovered) } @@ -670,8 +671,8 @@ Item { root.messageContextMenu.setXPosition = function() { return (root.messageContextMenu.parent.x + 4)} root.messageContextMenu.setYPosition = function() { return (-root.messageContextMenu.height - 4)} } - // Not Refactored Yet -// onToggleReaction: rootStore.chatsModelInst.toggleReaction(messageId, emojiID) + + onToggleReaction: messageStore.toggleReaction(messageId, emojiID) onSetMessageActive: { setMessageActive(messageId, active);; diff --git a/ui/imports/shared/views/chat/MessageContextMenuView.qml b/ui/imports/shared/views/chat/MessageContextMenuView.qml index 71c152aef6..9d35ba40c5 100644 --- a/ui/imports/shared/views/chat/MessageContextMenuView.qml +++ b/ui/imports/shared/views/chat/MessageContextMenuView.qml @@ -57,6 +57,7 @@ StatusPopupMenu { signal shouldCloseParentPopup() signal createOneToOneChat(string chatId, string ensName) signal showReplyArea() + signal toggleReaction(string messageId, int emojiId) onHeightChanged: { root.y = setYPosition() @@ -85,8 +86,7 @@ StatusPopupMenu { emojiId: model.emojiId reactedByUser: !!root.emojiReactionsReactedByUser[model.emojiId] onCloseModal: { - // Not Refactored Yet -// chatsModel.toggleReaction(SelectedMessage.messageId, emojiId) + root.toggleReaction(root.messageId, emojiId) root.close() } } diff --git a/ui/imports/shared/views/chat/MessageView.qml b/ui/imports/shared/views/chat/MessageView.qml index 41634c2cc2..7381e9f785 100644 --- a/ui/imports/shared/views/chat/MessageView.qml +++ b/ui/imports/shared/views/chat/MessageView.qml @@ -33,6 +33,7 @@ Column { property string messageOutgoingStatus: "" property int messageContentType: 1 property bool pinnedMessage: false + property var reactionsModel property int prevMessageIndex: -1 property var prevMessageAsJsonObj @@ -129,40 +130,6 @@ Column { property var imageClick: function () {} property var scrollToBottom: function () {} - property var emojiReactionsModel: { - // Not Refactored Yet - return [] -// if (!emojiReactions) { -// return [] -// } - -// try { -// // group by id -// var allReactions = Object.values(JSON.parse(emojiReactions)) -// var byEmoji = {} -// allReactions.forEach(function (reaction) { -// if (!byEmoji[reaction.emojiId]) { -// byEmoji[reaction.emojiId] = { -// emojiId: reaction.emojiId, -// fromAccounts: [], -// count: 0, -// currentUserReacted: false -// } -// } -// byEmoji[reaction.emojiId].count++; -// byEmoji[reaction.emojiId].fromAccounts.push(root.chatsModel.userNameOrAlias(reaction.from)); -// if (!byEmoji[reaction.emojiId].currentUserReacted && reaction.from === userProfile.pubKey) { -// byEmoji[reaction.emojiId].currentUserReacted = true -// } - -// }) -// return Object.values(byEmoji) -// } catch (e) { -// console.error('Error parsing emoji reactions', e) -// return [] -// } - } - property var clickMessage: function(isProfileClick, isSticker = false, isImage = false, @@ -378,8 +345,7 @@ Column { // root.parent.clickMessage(isProfileClick, isSticker, isImage, image, emojiOnly, hideEmojiPicker, isReply); } onSetMessageActive: { - // Not Refactored Yet - Should do it via messageStore -// root.messageStore.setMessageActive(messageId, active);; + root.setMessageActive(messageId, active); } } } diff --git a/ui/imports/shared/views/chat/StatusUpdateView.qml b/ui/imports/shared/views/chat/StatusUpdateView.qml index 4e460413c0..f24cca7607 100644 --- a/ui/imports/shared/views/chat/StatusUpdateView.qml +++ b/ui/imports/shared/views/chat/StatusUpdateView.qml @@ -180,8 +180,10 @@ MouseArea { Component { id: emojiReactionsComponent EmojiReactionsPanel { -// isMessageActive: root.isMessageActive -// emojiReactionsModel: root.emojiReactionsModel + store: messageStore + emojiReactionsModel: reactionsModel + isMessageActive: isMessageActive + isCurrentUser: isCurrentUser onAddEmojiClicked: { root.addEmoji(false, false, false, null, true, false); // Set parent, X & Y positions for the messageContextMenu @@ -189,12 +191,12 @@ MouseArea { messageContextMenu.setXPosition = function() { return (messageContextMenu.parent.x + 4)} messageContextMenu.setYPosition = function() { return (-messageContextMenu.height - 4)} } - // Not Refactored Yet -// onToggleReaction: chatsModel.toggleReaction(messageId, emojiID) -// onSetMessageActive: { -// root.setMessageActive(messageId, active);; -// } + onToggleReaction: messageStore.toggleReaction(messageId, emojiID) + + onSetMessageActive: { + root.setMessageActive(messageId, active); + } } }