refactor(@desktop/chat-communities): messages added

Messages model added, not completely done.
Also convenient methods for add/remove reactions are added.
This commit is contained in:
Sale Djenic 2021-11-04 09:30:44 +01:00
parent 12f9282a59
commit 9777191501
12 changed files with 270 additions and 23 deletions

View File

@ -7,23 +7,27 @@ import ../../../../../app_service/service/chat/service as chat_service
import ../../../../../app_service/service/community/service as community_service import ../../../../../app_service/service/community/service as community_service
import ../../../../../app_service/service/message/service as message_service import ../../../../../app_service/service/message/service as message_service
import eventemitter
import status/[signals]
export controller_interface export controller_interface
type type
Controller* = ref object of controller_interface.AccessInterface Controller* = ref object of controller_interface.AccessInterface
delegate: io_interface.AccessInterface delegate: io_interface.AccessInterface
events: EventEmitter
chatId: string chatId: string
belongsToCommunity: bool belongsToCommunity: bool
chatService: chat_service.ServiceInterface chatService: chat_service.ServiceInterface
communityService: community_service.ServiceInterface communityService: community_service.ServiceInterface
messageService: message_service.Service messageService: message_service.Service
proc newController*(delegate: io_interface.AccessInterface, chatId: string, belongsToCommunity: bool, proc newController*(delegate: io_interface.AccessInterface, events: EventEmitter, chatId: string,
chatService: chat_service.ServiceInterface, belongsToCommunity: bool, chatService: chat_service.ServiceInterface,
communityService: community_service.ServiceInterface, communityService: community_service.ServiceInterface, messageService: message_service.Service): Controller =
messageService: message_service.Service): Controller =
result = Controller() result = Controller()
result.delegate = delegate result.delegate = delegate
result.events = events
result.chatId = chatId result.chatId = chatId
result.belongsToCommunity = belongsToCommunity result.belongsToCommunity = belongsToCommunity
result.chatService = chatService result.chatService = chatService
@ -34,7 +38,13 @@ method delete*(self: Controller) =
discard discard
method init*(self: Controller) = method init*(self: Controller) =
discard self.events.on(SIGNAL_MESSAGES_LOADED) do(e:Args):
let args = MessagesLoadedArgs(e)
if(self.chatId != args.chatId):
return
# We should handle only pinned messages within this module.
echo "ChatContent...RECEIVED MESSAGES: ", repr(args)
method getChatId*(self: Controller): string = method getChatId*(self: Controller): string =
return self.chatId return self.chatId

View File

@ -1,3 +1,4 @@
import chronicles
import controller_interface import controller_interface
import io_interface import io_interface
@ -9,6 +10,9 @@ import status/[signals]
export controller_interface export controller_interface
logScope:
topics = "messages-controller"
type type
Controller* = ref object of controller_interface.AccessInterface Controller* = ref object of controller_interface.AccessInterface
delegate: io_interface.AccessInterface delegate: io_interface.AccessInterface
@ -34,10 +38,29 @@ method delete*(self: Controller) =
method init*(self: Controller) = method init*(self: Controller) =
self.events.on(SIGNAL_MESSAGES_LOADED) do(e:Args): self.events.on(SIGNAL_MESSAGES_LOADED) do(e:Args):
let args = MessagesLoadedArgs(e) let args = MessagesLoadedArgs(e)
echo "RECEIVED MESSAGES ASYNC: ", repr(args) if(self.chatId != args.chatId):
return
self.delegate.newMessagesLoaded(args.messages, args.reactions)
method getChatId*(self: Controller): string = method getChatId*(self: Controller): string =
return self.chatId return self.chatId
method belongsToCommunity*(self: Controller): bool = method belongsToCommunity*(self: Controller): bool =
return self.belongsToCommunity 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.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)

View File

@ -16,4 +16,8 @@ method getChatId*(self: AccessInterface): string {.base.} =
method belongsToCommunity*(self: AccessInterface): bool {.base.} = method belongsToCommunity*(self: AccessInterface): bool {.base.} =
raise newException(ValueError, "No implementation available") raise newException(ValueError, "No implementation available")
method addReaction*(self: AccessInterface, messageId: string, emojiId: int) {.base.} =
raise newException(ValueError, "No implementation available")
method removeReaction*(self: AccessInterface, messageId: string, reactionId: string) {.base.} =
raise newException(ValueError, "No implementation available")

View File

@ -1,3 +1,5 @@
import Tables, json
type type
ContentType* {.pure.} = enum ContentType* {.pure.} = enum
FetchMoreMessagesButton = -2 FetchMoreMessagesButton = -2
@ -16,7 +18,7 @@ type
Edit = 11 Edit = 11
type type
Item* = object Item* = ref object
id: string id: string
`from`: string `from`: string
alias: string alias: string
@ -32,9 +34,12 @@ type
timestamp: int64 timestamp: int64
contentType: ContentType contentType: ContentType
messageType: int messageType: int
reactions: OrderedTable[int, seq[tuple[name: string, reactionId: string]]] # [emojiId, list of [names reacted with the emojiId, reaction id]]
reactionIds: seq[string]
proc initItem*(id, `from`, alias, identicon, outgoingStatus, text: string, seen: bool, timestamp: int64, proc initItem*(id, `from`, alias, identicon, outgoingStatus, text: string, seen: bool, timestamp: int64,
contentType: ContentType, messageType: int): Item = contentType: ContentType, messageType: int): Item =
result = Item()
result.id = id result.id = id
result.`from` = `from` result.`from` = `from`
result.alias = alias result.alias = alias
@ -74,4 +79,62 @@ proc contentType*(self: Item): ContentType {.inline.} =
self.contentType self.contentType
proc messageType*(self: Item): int {.inline.} = proc messageType*(self: Item): int {.inline.} =
self.messageType self.messageType
proc shouldAddReaction*(self: Item, emojiId: int, name: string): bool =
for k, values in self.reactions:
if(k != emojiId):
continue
for t in values:
if(t.name == name):
return false
return true
proc getReactionId*(self: Item, emojiId: int, name: string): string =
for k, values in self.reactions:
if(k != emojiId):
continue
for t in values:
if(t.name == name):
return t.reactionId
# we should never be here, since this is a controlled call
return ""
proc addReaction*(self: Item, emojiId: int, name: string, reactionId: string) =
if(not self.reactions.contains(emojiId)):
self.reactions[emojiId] = @[]
self.reactions[emojiId].add((name, reactionId))
proc removeReaction*(self: Item, reactionId: string) =
var key: int
var index: int
for k, values in self.reactions:
var i = -1
for t in values:
i += 1
if(t.reactionId == reactionId):
key = k
index = i
self.reactions[key].del(index)
if(self.reactions[key].len == 0):
self.reactions.del(key)
proc getNamesForReactions*(self: Item, emojiId: int): seq[string] =
if(not self.reactions.contains(emojiId)):
return
for v in self.reactions[emojiId]:
result.add(v.name)
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})

View File

@ -1,4 +1,4 @@
import NimQml, Tables, strutils, strformat import NimQml, Tables, json, strutils, strformat
import item import item
@ -19,6 +19,7 @@ type
# Image # Image
# GapFrom # GapFrom
# GapTo # GapTo
CountsForReactions
QtObject: QtObject:
type type
@ -55,7 +56,8 @@ QtObject:
# ModelRole.StickerPack.int:"stickerPack", # ModelRole.StickerPack.int:"stickerPack",
# ModelRole.Image.int:"image", # ModelRole.Image.int:"image",
# ModelRole.GapFrom.int:"gapFrom", # ModelRole.GapFrom.int:"gapFrom",
# ModelRole.GapTo.int:"gapTo" # ModelRole.GapTo.int:"gapTo",
ModelRole.CountsForReactions.int:"countsForReactions",
}.toTable }.toTable
method data(self: Model, index: QModelIndex, role: int): QVariant = method data(self: Model, index: QModelIndex, role: int): QVariant =
@ -98,4 +100,55 @@ QtObject:
# of ModelRole.GapFrom: # of ModelRole.GapFrom:
# result = newQVariant(item.gapFrom) # result = newQVariant(item.gapFrom)
# of ModelRole.GapTo: # of ModelRole.GapTo:
# result = newQVariant(item.gapTo) # result = newQVariant(item.gapTo)
of ModelRole.CountsForReactions:
result = newQVariant($(%* item.getCountsForReactions))
proc prependItems*(self: Model, items: seq[Item]) =
let parentModelIndex = newQModelIndex()
defer: parentModelIndex.delete
let first = 0
let last = items.len - 1
self.beginInsertRows(parentModelIndex, first, last)
self.items = items & self.items
self.endInsertRows()
proc findIndexForMessageId(self: Model, messageId: string): int =
for i in 0 ..< self.items.len:
if(self.items[i].id == messageId):
return i
return -1
proc getItemWithMessageId*(self: Model, messageId: string): Item =
let ind = self.findIndexForMessageId(messageId)
if(ind == -1):
return
return self.items[ind]
proc addReaction*(self: Model, messageId: string, emojiId: int, name: string, reactionId: string) =
let ind = self.findIndexForMessageId(messageId)
if(ind == -1):
return
self.items[ind].addReaction(emojiId, name, reactionId)
let index = self.createIndex(ind, 0, nil)
self.dataChanged(index, index, @[ModelRole.CountsForReactions.int])
proc removeReaction*(self: Model, messageId: string, reactionId: string) =
let ind = self.findIndexForMessageId(messageId)
if(ind == -1):
return
self.items[ind].removeReaction(reactionId)
let index = self.createIndex(ind, 0, nil)
self.dataChanged(index, index, @[ModelRole.CountsForReactions.int])
proc getNamesForReaction*(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].getNamesForReactions(emojiId)

View File

@ -1,7 +1,7 @@
import NimQml import NimQml
import io_interface import io_interface
import ../io_interface as delegate_interface import ../io_interface as delegate_interface
import view, controller import view, controller, model, item
import ../../../../../core/global_singleton import ../../../../../core/global_singleton
import ../../../../../../app_service/service/chat/service as chat_service import ../../../../../../app_service/service/chat/service as chat_service
@ -51,4 +51,36 @@ method viewDidLoad*(self: Module) =
self.delegate.messagesDidLoad() self.delegate.messagesDidLoad()
method getModuleAsVariant*(self: Module): QVariant = method getModuleAsVariant*(self: Module): QVariant =
return self.viewVariant return self.viewVariant
method newMessagesLoaded*(self: Module, messages: seq[MessageDto], reactions: seq[ReactionDto]) =
var viewItems: seq[Item]
for m in messages:
var item = initItem(m.id, m.`from`, m.alias, m.identicon, m.outgoingStatus, m.text, m.seen, m.timestamp,
m.contentType.ContentType, m.messageType)
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)
# messages are sorted from the most recent to the least recent one
viewItems = item & viewItems
self.view.model.prependItems(viewItems)
method toggleReaction*(self: Module, messageId: string, emojiId: int) =
let item = self.view.model.getItemWithMessageId(messageId)
let myName = "MY_NAME" #once we have "userProfile" merged, we will request alias/ens name from there
if(item.shouldAddReaction(emojiId, myName)):
self.controller.addReaction(messageId, emojiId)
else:
let reactionId = item.getReactionId(emojiId, myName)
self.controller.removeReaction(messageId, reactionId)
method onReactionAdded*(self: Module, messageId: string, emojiId: int, reactionId: string) =
let myName = "MY_NAME" #once we have "userProfile" merged, we will request alias/ens name from there
self.view.model.addReaction(messageId, emojiId, myName, reactionId)
method onReactionRemoved*(self: Module, messageId: string, reactionId: string) =
self.view.model.removeReaction(messageId, reactionId)

View File

@ -0,0 +1,10 @@
import ../../../../../../../app_service/service/message/dto/[message, reaction]
method newMessagesLoaded*(self: AccessInterface, messages: seq[MessageDto], reactions: seq[ReactionDto]) {.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, reactionId: string) {.base.} =
raise newException(ValueError, "No implementation available")

View File

@ -1,2 +1,5 @@
method viewDidLoad*(self: AccessInterface) {.base.} = method viewDidLoad*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available") raise newException(ValueError, "No implementation available")
method toggleReaction*(self: AccessInterface, messageId: string, emojiId: int) {.base.} =
raise newException(ValueError, "No implementation available")

View File

@ -1,4 +1,4 @@
import NimQml import NimQml, json
import model import model
import io_interface import io_interface
@ -6,10 +6,12 @@ QtObject:
type type
View* = ref object of QObject View* = ref object of QObject
delegate: io_interface.AccessInterface delegate: io_interface.AccessInterface
model: Model model*: Model
modelVariant: QVariant
proc delete*(self: View) = proc delete*(self: View) =
self.model.delete self.model.delete
self.modelVariant.delete
self.QObject.delete self.QObject.delete
proc newView*(delegate: io_interface.AccessInterface): View = proc newView*(delegate: io_interface.AccessInterface): View =
@ -17,6 +19,20 @@ QtObject:
result.QObject.setup result.QObject.setup
result.delegate = delegate result.delegate = delegate
result.model = newModel() result.model = newModel()
result.modelVariant = newQVariant(result.model)
proc load*(self: View) = proc load*(self: View) =
self.delegate.viewDidLoad() self.delegate.viewDidLoad()
proc getModel(self: View): QVariant {.slot.} =
return self.modelVariant
QtProperty[QVariant] model:
read = getModel
notify = modelChanged
proc toggleReaction*(self: View, messageId: string, emojiId: int) {.slot.} =
self.delegate.toggleReaction(messageId, emojiId)
proc getNamesForReaction*(self: View, messageId: string, emojiId: int): string {.slot.} =
return $(%* self.model.getNamesForReaction(messageId, emojiId))

View File

@ -38,7 +38,7 @@ proc newModule*(delegate: delegate_interface.AccessInterface, events: EventEmitt
result.delegate = delegate result.delegate = delegate
result.view = view.newView(result) result.view = view.newView(result)
result.viewVariant = newQVariant(result.view) result.viewVariant = newQVariant(result.view)
result.controller = controller.newController(result, chatId, belongsToCommunity, chatService, communityService, result.controller = controller.newController(result, events, chatId, belongsToCommunity, chatService, communityService,
messageService) messageService)
result.moduleLoaded = false result.moduleLoaded = false

View File

@ -67,8 +67,8 @@ proc buildChatUI(self: Module, events: EventEmitter, chatService: chat_service.S
self.view.appendItem(item) self.view.appendItem(item)
self.addSubmodule(c.id, false, events, chatService, communityService, messageService) self.addSubmodule(c.id, false, events, chatService, communityService, messageService)
# make the first chat active when load the app # make the first Public chat active when load the app
if(selectedItemId.len == 0): if(selectedItemId.len == 0 and c.chatType == ChatType.Public):
selectedItemId = item.id selectedItemId = item.id
self.setActiveItemSubItem(selectedItemId, "") self.setActiveItemSubItem(selectedItemId, "")

View File

@ -130,4 +130,37 @@ QtObject:
# we're here if initial messages are not loaded yet # we're here if initial messages are not loaded yet
self.loadMoreMessagesForChat(chatId) self.loadMoreMessagesForChat(chatId)
proc addReaction*(self: Service, chatId: string, messageId: string, emojiId: int):
tuple[result: string, error: string] =
try:
let response = status_go.addReaction(chatId, messageId, emojiId)
result.error = "response doesn't contain \"error\""
if(response.result.contains("error")):
result.error = response.result["error"].getStr
return
var reactionsArr: JsonNode
var reactions: seq[ReactionDto]
if(response.result.getProp("emojiReactions", reactionsArr)):
reactions = map(reactionsArr.getElems(), proc(x: JsonNode): ReactionDto = x.toReactionDto())
if(reactions.len > 0):
result.result = reactions[0].id
except Exception as e:
error "error: ", methodName="addReaction", errName = e.name, errDesription = e.msg
proc removeReaction*(self: Service, reactionId: string):
tuple[result: string, error: string] =
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
return
except Exception as e:
error "error: ", methodName="removeReaction", errName = e.name, errDesription = e.msg