diff --git a/src/app/boot/app_controller.nim b/src/app/boot/app_controller.nim index af8b427735..02f4d1afc7 100644 --- a/src/app/boot/app_controller.nim +++ b/src/app/boot/app_controller.nim @@ -362,6 +362,7 @@ proc load(self: AppController) = # load main module self.mainModule.load( self.statusFoundation.status.events, + self.contactsService, self.chatService, self.communityService, self.messageService diff --git a/src/app/chat/signal_handling.nim b/src/app/chat/signal_handling.nim index 888cec322c..54024ca48c 100644 --- a/src/app/chat/signal_handling.nim +++ b/src/app/chat/signal_handling.nim @@ -4,7 +4,7 @@ import proc handleSignals(self: ChatController) = self.status.events.on(SignalType.Message.event) do(e:Args): var data = MessageSignal(e) - self.status.chat.update(data.chats, data.messages, data.emojiReactions, data.communities, data.membershipRequests, data.pinnedMessages, data.activityCenterNotification, data.statusUpdates, data.deletedMessages) + #self.status.chat.update(data.chats, data.messages, data.emojiReactions, data.communities, data.membershipRequests, data.pinnedMessages, data.activityCenterNotification, data.statusUpdates, data.deletedMessages) self.status.events.on(SignalType.DiscoverySummary.event) do(e:Args): ## Handle mailserver peers being added and removed @@ -39,8 +39,9 @@ proc handleSignals(self: ChatController) = self.view.messageView.messageList[chatId].checkTimeout(messageId) self.status.events.on(SignalType.CommunityFound.event) do(e: Args): - var data = CommunitySignal(e) - self.view.communities.addCommunityToList(data.community) + discard + # var data = CommunitySignal(e) + # self.view.communities.addCommunityToList(data.community) self.status.events.on(SignalType.MailserverRequestCompleted.event) do(e:Args): # TODO: if the signal contains a cursor, request additional messages diff --git a/src/app/core/signals/remote_signals/community.nim b/src/app/core/signals/remote_signals/community.nim index ac7f4991de..625e743724 100644 --- a/src/app/core/signals/remote_signals/community.nim +++ b/src/app/core/signals/remote_signals/community.nim @@ -2,13 +2,13 @@ import json import base -import status/types/community +import ../../../../app_service/service/community/dto/[community] import signal_type type CommunitySignal* = ref object of Signal - community*: Community + community*: CommunityDto proc fromEvent*(T: type CommunitySignal, event: JsonNode): CommunitySignal = result = CommunitySignal() result.signalType = SignalType.CommunityFound - result.community = event["event"].toCommunity() + result.community = event["event"].toCommunityDto() diff --git a/src/app/core/signals/remote_signals/messages.nim b/src/app/core/signals/remote_signals/messages.nim index 63631215d1..4fbb979c81 100644 --- a/src/app/core/signals/remote_signals/messages.nim +++ b/src/app/core/signals/remote_signals/messages.nim @@ -2,20 +2,26 @@ import json import base -import status/types/[message, chat, community, profile, installation, - activity_center_notification, status_update, removed_message] +# Step by step we should remove all these types from `status-lib` +import status/types/[installation, activity_center_notification, removed_message] +import status/types/community as old_community + +import ../../../../app_service/service/message/dto/[message, pinned_message, reaction] +import ../../../../app_service/service/chat/dto/[chat] +import ../../../../app_service/service/community/dto/[community] +import ../../../../app_service/service/contacts/dto/[contacts, status_update] type MessageSignal* = ref object of Signal - messages*: seq[Message] - pinnedMessages*: seq[Message] - chats*: seq[Chat] - contacts*: seq[Profile] + messages*: seq[MessageDto] + pinnedMessages*: seq[PinnedMessageDto] + chats*: seq[ChatDto] + contacts*: seq[ContactsDto] installations*: seq[Installation] - emojiReactions*: seq[Reaction] - communities*: seq[Community] - membershipRequests*: seq[CommunityMembershipRequest] + emojiReactions*: seq[ReactionDto] + communities*: seq[CommunityDto] + membershipRequests*: seq[old_community.CommunityMembershipRequest] activityCenterNotification*: seq[ActivityCenterNotification] - statusUpdates*: seq[StatusUpdate] + statusUpdates*: seq[StatusUpdateDto] deletedMessages*: seq[RemovedMessage] proc fromEvent*(T: type MessageSignal, event: JsonNode): MessageSignal = @@ -25,27 +31,21 @@ proc fromEvent*(T: type MessageSignal, event: JsonNode): MessageSignal = if event["event"]{"contacts"} != nil: for jsonContact in event["event"]["contacts"]: - signal.contacts.add(jsonContact.toProfile()) - - var chatsWithMentions: seq[string] = @[] + signal.contacts.add(jsonContact.toContactsDto()) if event["event"]{"messages"} != nil: for jsonMsg in event["event"]["messages"]: - var message = jsonMsg.toMessage() - if message.hasMention: - chatsWithMentions.add(message.chatId) + var message = jsonMsg.toMessageDto() signal.messages.add(message) if event["event"]{"chats"} != nil: for jsonChat in event["event"]["chats"]: - var chat = jsonChat.toChat - if chatsWithMentions.contains(chat.id): - chat.mentionsCount.inc + var chat = jsonChat.toChatDto() signal.chats.add(chat) if event["event"]{"statusUpdates"} != nil: for jsonStatusUpdate in event["event"]["statusUpdates"]: - var statusUpdate = jsonStatusUpdate.toStatusUpdate + var statusUpdate = jsonStatusUpdate.toStatusUpdateDto() signal.statusUpdates.add(statusUpdate) if event["event"]{"installations"} != nil: @@ -54,11 +54,11 @@ proc fromEvent*(T: type MessageSignal, event: JsonNode): MessageSignal = if event["event"]{"emojiReactions"} != nil: for jsonReaction in event["event"]["emojiReactions"]: - signal.emojiReactions.add(jsonReaction.toReaction) + signal.emojiReactions.add(jsonReaction.toReactionDto()) if event["event"]{"communities"} != nil: for jsonCommunity in event["event"]["communities"]: - signal.communities.add(jsonCommunity.toCommunity) + signal.communities.add(jsonCommunity.toCommunityDto()) if event["event"]{"requestsToJoinCommunity"} != nil: for jsonCommunity in event["event"]["requestsToJoinCommunity"]: @@ -73,23 +73,25 @@ proc fromEvent*(T: type MessageSignal, event: JsonNode): MessageSignal = signal.activityCenterNotification.add(jsonNotification.toActivityCenterNotification()) if event["event"]{"pinMessages"} != nil: - for jsonPinnedMessage in event["event"]["pinMessages"]: - var contentType: ContentType - try: - contentType = ContentType(jsonPinnedMessage{"contentType"}.getInt) - except: - contentType = ContentType.Message - signal.pinnedMessages.add(Message( - id: jsonPinnedMessage{"message_id"}.getStr, - chatId: jsonPinnedMessage{"chat_id"}.getStr, - localChatId: jsonPinnedMessage{"localChatId"}.getStr, - pinnedBy: jsonPinnedMessage{"from"}.getStr, - identicon: jsonPinnedMessage{"identicon"}.getStr, - alias: jsonPinnedMessage{"alias"}.getStr, - clock: jsonPinnedMessage{"clock"}.getInt, - isPinned: jsonPinnedMessage{"pinned"}.getBool, - contentType: contentType - )) + discard + # Need to refactor this + # for jsonPinnedMessage in event["event"]["pinMessages"]: + # var contentType: ContentType + # try: + # contentType = ContentType(jsonPinnedMessage{"contentType"}.getInt) + # except: + # contentType = ContentType.Message + # signal.pinnedMessages.add(Message( + # id: jsonPinnedMessage{"message_id"}.getStr, + # chatId: jsonPinnedMessage{"chat_id"}.getStr, + # localChatId: jsonPinnedMessage{"localChatId"}.getStr, + # pinnedBy: jsonPinnedMessage{"from"}.getStr, + # identicon: jsonPinnedMessage{"identicon"}.getStr, + # alias: jsonPinnedMessage{"alias"}.getStr, + # clock: jsonPinnedMessage{"clock"}.getInt, + # isPinned: jsonPinnedMessage{"pinned"}.getBool, + # contentType: contentType + # )) result = signal diff --git a/src/app/modules/main/chat_section/active_item.nim b/src/app/modules/main/chat_section/active_item.nim index 963e5388d1..aa274ddf11 100644 --- a/src/app/modules/main/chat_section/active_item.nim +++ b/src/app/modules/main/chat_section/active_item.nim @@ -33,6 +33,8 @@ QtObject: self.activeSubItemChanged() proc getId(self: ActiveItem): string {.slot.} = + if(self.item.isNil): + return "" return self.item.id QtProperty[string] id: @@ -48,54 +50,72 @@ QtObject: read = getIsSubItemActive proc getName(self: ActiveItem): string {.slot.} = + if(self.item.isNil): + return "" return self.item.name QtProperty[string] name: read = getName proc getIcon(self: ActiveItem): string {.slot.} = + if(self.item.isNil): + return "" return self.item.icon QtProperty[string] icon: read = getIcon proc getColor(self: ActiveItem): string {.slot.} = + if(self.item.isNil): + return "" return self.item.color QtProperty[string] color: read = getColor proc getDescription(self: ActiveItem): string {.slot.} = + if(self.item.isNil): + return "" return self.item.description QtProperty[string] description: read = getDescription proc getType(self: ActiveItem): int {.slot.} = + if(self.item.isNil): + return 0 return self.item.`type` QtProperty[int] type: read = getType proc getHasUnreadMessages(self: ActiveItem): bool {.slot.} = + if(self.item.isNil): + return false return self.item.hasUnreadMessages QtProperty[bool] hasUnreadMessages: read = getHasUnreadMessages proc getNotificationCount(self: ActiveItem): int {.slot.} = + if(self.item.isNil): + return 0 return self.item.notificationsCount QtProperty[int] notificationCount: read = getNotificationCount proc getMuted(self: ActiveItem): bool {.slot.} = + if(self.item.isNil): + return false return self.item.muted QtProperty[bool] muted: read = getMuted proc getPosition(self: ActiveItem): int {.slot.} = + if(self.item.isNil): + return 0 return self.item.position QtProperty[int] position: 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 ef36e9a600..c18a52cdd1 100644 --- a/src/app/modules/main/chat_section/chat_content/controller.nim +++ b/src/app/modules/main/chat_section/chat_content/controller.nim @@ -18,18 +18,20 @@ type events: EventEmitter chatId: string belongsToCommunity: bool + isUsersListAvailable: bool #users list is not available for 1:1 chat chatService: chat_service.ServiceInterface communityService: community_service.ServiceInterface messageService: message_service.Service proc newController*(delegate: io_interface.AccessInterface, events: EventEmitter, chatId: string, - belongsToCommunity: bool, chatService: chat_service.ServiceInterface, + belongsToCommunity: bool, isUsersListAvailable: bool, chatService: chat_service.ServiceInterface, communityService: community_service.ServiceInterface, messageService: message_service.Service): Controller = result = Controller() result.delegate = delegate result.events = events result.chatId = chatId result.belongsToCommunity = belongsToCommunity + result.isUsersListAvailable = isUsersListAvailable result.chatService = chatService result.communityService = communityService result.messageService = messageService @@ -67,4 +69,7 @@ method unpinMessage*(self: Controller, messageId: string) = method getMessageDetails*(self: Controller, messageId: string): tuple[message: MessageDto, reactions: seq[ReactionDto], error: string] = - return self.messageService.getDetailsForMessage(self.chatId, messageId) \ No newline at end of file + return self.messageService.getDetailsForMessage(self.chatId, messageId) + +method isUsersListAvailable*(self: Controller): bool = + return self.isUsersListAvailable \ No newline at end of file diff --git a/src/app/modules/main/chat_section/chat_content/controller_interface.nim b/src/app/modules/main/chat_section/chat_content/controller_interface.nim index c9f32099e5..229c5990d1 100644 --- a/src/app/modules/main/chat_section/chat_content/controller_interface.nim +++ b/src/app/modules/main/chat_section/chat_content/controller_interface.nim @@ -21,4 +21,7 @@ method unpinMessage*(self: AccessInterface, messageId: string) {.base.} = method getMessageDetails*(self: AccessInterface, messageId: string): tuple[message: MessageDto, reactions: seq[ReactionDto], error: string] {.base.} = + raise newException(ValueError, "No implementation available") + +method isUsersListAvailable*(self: AccessInterface): bool {.base.} = raise newException(ValueError, "No implementation available") \ No newline at end of file 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 7f077b089a..6983467af3 100644 --- a/src/app/modules/main/chat_section/chat_content/module.nim +++ b/src/app/modules/main/chat_section/chat_content/module.nim @@ -10,6 +10,7 @@ import input_area/module as input_area_module import messages/module as messages_module import users/module as users_module +import ../../../../../app_service/service/contacts/service as contact_service import ../../../../../app_service/service/chat/service as chat_service import ../../../../../app_service/service/community/service as community_service import ../../../../../app_service/service/message/service as message_service @@ -32,7 +33,8 @@ type usersModule: users_module.AccessInterface moduleLoaded: bool -proc newModule*(delegate: delegate_interface.AccessInterface, events: EventEmitter, chatId: string, belongsToCommunity: bool, +proc newModule*(delegate: delegate_interface.AccessInterface, events: EventEmitter, sectionId: string, chatId: string, + belongsToCommunity: bool, isUsersListAvailable: bool, contactService: contact_service.Service, chatService: chat_service.ServiceInterface, communityService: community_service.ServiceInterface, messageService: message_service.Service): Module = @@ -40,14 +42,15 @@ proc newModule*(delegate: delegate_interface.AccessInterface, events: EventEmitt result.delegate = delegate result.view = view.newView(result) result.viewVariant = newQVariant(result.view) - result.controller = controller.newController(result, events, chatId, belongsToCommunity, chatService, communityService, - messageService) + result.controller = controller.newController(result, events, chatId, belongsToCommunity, isUsersListAvailable, + chatService, communityService, messageService) result.moduleLoaded = false result.inputAreaModule = input_area_module.newModule(result, chatId, belongsToCommunity, chatService, communityService) result.messagesModule = messages_module.newModule(result, events, chatId, belongsToCommunity, chatService, communityService, messageService) - result.usersModule = users_module.newModule(result, chatId, belongsToCommunity, chatService, communityService) + result.usersModule = users_module.newModule(result, events, sectionId, chatId, belongsToCommunity, isUsersListAvailable, + contactService, communityService, messageService) method delete*(self: Module) = self.inputAreaModule.delete @@ -147,4 +150,6 @@ method onPinMessage*(self: Module, messageId: string) = return self.view.model.appendItem(item) - \ No newline at end of file + +method isUsersListAvailable*(self: Module): bool = + self.controller.isUsersListAvailable() \ No newline at end of file diff --git a/src/app/modules/main/chat_section/chat_content/private_interfaces/module_view_delegate_interface.nim b/src/app/modules/main/chat_section/chat_content/private_interfaces/module_view_delegate_interface.nim index 079d82dbf1..be43116c30 100644 --- a/src/app/modules/main/chat_section/chat_content/private_interfaces/module_view_delegate_interface.nim +++ b/src/app/modules/main/chat_section/chat_content/private_interfaces/module_view_delegate_interface.nim @@ -13,4 +13,7 @@ method getUsersModule*(self: AccessInterface): QVariant {.base.} = raise newException(ValueError, "No implementation available") method unpinMessage*(self: AccessInterface, messageId: string) {.base.} = + raise newException(ValueError, "No implementation available") + +method isUsersListAvailable*(self: AccessInterface): bool {.base.} = raise newException(ValueError, "No implementation available") \ No newline at end of file diff --git a/src/app/modules/main/chat_section/chat_content/users/controller.nim b/src/app/modules/main/chat_section/chat_content/users/controller.nim index 51279d4c80..04a6f682be 100644 --- a/src/app/modules/main/chat_section/chat_content/users/controller.nim +++ b/src/app/modules/main/chat_section/chat_content/users/controller.nim @@ -1,33 +1,80 @@ +import sequtils, sugar import controller_interface import io_interface +import ../../../../../../app_service/service/contacts/service as contact_service import ../../../../../../app_service/service/community/service_interface as community_service +import ../../../../../../app_service/service/message/service as message_service + +import eventemitter export controller_interface type Controller* = ref object of controller_interface.AccessInterface delegate: io_interface.AccessInterface + events: EventEmitter + sectionId: string chatId: string belongsToCommunity: bool + isUsersListAvailable: bool #users list is not available for 1:1 chat + contactService: contact_service.Service communityService: community_service.ServiceInterface + messageService: message_service.Service -proc newController*(delegate: io_interface.AccessInterface, chatId: string, belongsToCommunity: bool, - communityService: community_service.ServiceInterface): Controller = +proc newController*(delegate: io_interface.AccessInterface, events: EventEmitter, sectionId: string, chatId: string, + belongsToCommunity: bool, isUsersListAvailable: bool, contactService: contact_service.Service, + communityService: community_service.ServiceInterface, messageService: message_service.Service): + Controller = result = Controller() result.delegate = delegate + result.events = events + result.sectionId = sectionId result.chatId = chatId result.belongsToCommunity = belongsToCommunity + result.isUsersListAvailable = isUsersListAvailable + result.contactService = contactService result.communityService = communityService + result.messageService = messageService method delete*(self: Controller) = discard method init*(self: Controller) = - discard + if(self.isUsersListAvailable): + self.events.on(SIGNAL_MESSAGES_LOADED) do(e:Args): + let args = MessagesLoadedArgs(e) + if(self.chatId != args.chatId): + return -method getChatId*(self: Controller): string = - return self.chatId + self.delegate.newMessagesLoaded(args.messages) -method belongsToCommunity*(self: Controller): bool = - return self.belongsToCommunity \ No newline at end of file + self.events.on(SIGNAL_CONTACT_NICKNAME_CHANGED) do(e: Args): + var args = ContactNicknameUpdatedArgs(e) + self.delegate.contactNicknameChanged(args.contactId, args.nickname) + + self.events.on(SIGNAL_CONTACTS_STATUS_UPDATED) do(e: Args): + var args = ContactsStatusUpdatedArgs(e) + self.delegate.contactsStatusUpdated(args.statusUpdates) + + self.events.on(SIGNAL_CONTACT_UPDATED) do(e: Args): + var args = ContactUpdatedArgs(e) + self.delegate.contactUpdated(args.contact) + + self.events.on(SIGNAL_LOGGEDIN_USER_IMAGE_CHANGED) do(e: Args): + self.delegate.loggedInUserImageChanged() + +method getMembersPublicKeys*(self: Controller): seq[string] = + # in case of chat section, there is no a members list + if(not self.belongsToCommunity): + return + + let communityDto = self.communityService.getCommunityById(self.sectionId) + result = communityDto.members.map(x => x.id) + +method getContactNameAndImage*(self: Controller, contactId: string): + tuple[name: string, image: string, isIdenticon: bool] = + return self.contactService.getContactNameAndImage(contactId) + +method getStatusForContact*(self: Controller, contactId: string): StatusUpdateDto = + return self.contactService.getStatusForContactWithId(contactId) \ No newline at end of file diff --git a/src/app/modules/main/chat_section/chat_content/users/controller_interface.nim b/src/app/modules/main/chat_section/chat_content/users/controller_interface.nim index 615d65e361..f61df0f87e 100644 --- a/src/app/modules/main/chat_section/chat_content/users/controller_interface.nim +++ b/src/app/modules/main/chat_section/chat_content/users/controller_interface.nim @@ -1,4 +1,4 @@ -import ../../../../../../app_service/service/community/service_interface as community_service +import ../../../../../../app_service/service/contacts/service as contacts_service type AccessInterface* {.pure inheritable.} = ref object of RootObj @@ -13,7 +13,12 @@ method init*(self: AccessInterface) {.base.} = method getChatId*(self: AccessInterface): string {.base.} = raise newException(ValueError, "No implementation available") -method belongsToCommunity*(self: AccessInterface): bool {.base.} = +method getMembersPublicKeys*(self: AccessInterface): seq[string] {.base.} = raise newException(ValueError, "No implementation available") - \ No newline at end of file +method getContactNameAndImage*(self: AccessInterface, contactId: string): + tuple[name: string, image: string, isIdenticon: bool] {.base.} = + raise newException(ValueError, "No implementation available") + +method getStatusForContact*(self: AccessInterface, contactId: string): StatusUpdateDto {.base.} = + raise newException(ValueError, "No implementation available") \ No newline at end of file diff --git a/src/app/modules/main/chat_section/chat_content/users/item.nim b/src/app/modules/main/chat_section/chat_content/users/item.nim index e3f4e882ea..f9d411040b 100644 --- a/src/app/modules/main/chat_section/chat_content/users/item.nim +++ b/src/app/modules/main/chat_section/chat_content/users/item.nim @@ -1,24 +1,26 @@ type OnlineStatus* {.pure.} = enum - Online = 0 - Idle + Offline = 0 + Online DoNotDisturb + Idle Invisible - Offline - + type Item* = ref object id: string name: string onlineStatus: OnlineStatus - identicon: string + icon: string + isIdenticon: bool -proc initItem*(id: string, name: string, onlineStatus: OnlineStatus, identicon: string): Item = +proc initItem*(id: string, name: string, onlineStatus: OnlineStatus, icon: string, isidenticon: bool): Item = result = Item() result.id = id result.name = name result.onlineStatus = onlineStatus - result.identicon = identicon + result.icon = icon + result.isIdenticon = isidenticon proc id*(self: Item): string {.inline.} = self.id @@ -35,5 +37,14 @@ proc onlineStatus*(self: Item): OnlineStatus {.inline.} = proc `onlineStatus=`*(self: Item, value: OnlineStatus) {.inline.} = self.onlineStatus = value -proc identicon*(self: Item): string {.inline.} = - self.identicon \ No newline at end of file +proc icon*(self: Item): string {.inline.} = + self.icon + +proc `icon=`*(self: Item, value: string) {.inline.} = + self.icon = value + +proc isIdenticon*(self: Item): bool {.inline.} = + self.isIdenticon + +proc `isIdenticon=`*(self: Item, value: bool) {.inline.} = + self.isIdenticon = value \ No newline at end of file diff --git a/src/app/modules/main/chat_section/chat_content/users/model.nim b/src/app/modules/main/chat_section/chat_content/users/model.nim index a1cd6af126..7d3c902d98 100644 --- a/src/app/modules/main/chat_section/chat_content/users/model.nim +++ b/src/app/modules/main/chat_section/chat_content/users/model.nim @@ -7,7 +7,8 @@ type Id = UserRole + 1 Name OnlineStatus - Identicon + Icon + IsIdenticon QtObject: type @@ -33,7 +34,8 @@ QtObject: ModelRole.Id.int:"id", ModelRole.Name.int:"name", ModelRole.OnlineStatus.int:"onlineStatus", - ModelRole.Identicon.int:"identicon", + ModelRole.Icon.int:"icon", + ModelRole.IsIdenticon.int:"isIdenticon", }.toTable method data(self: Model, index: QModelIndex, role: int): QVariant = @@ -53,13 +55,30 @@ QtObject: result = newQVariant(item.name) of ModelRole.OnlineStatus: result = newQVariant(item.onlineStatus.int) - of ModelRole.Identicon: - result = newQVariant(item.identicon) + of ModelRole.Icon: + result = newQVariant(item.icon) + of ModelRole.IsIdenticon: + result = newQVariant(item.isIdenticon) - proc setItems*(self: Model, items: seq[Item]) = - self.beginResetModel() - self.items = items - self.endResetModel() + proc addItem*(self: Model, item: Item) = + # we need to maintain online contact on top, that means + # if we add an item online status we add it as the last online item (before the first offline item) + # if we add an item with offline status we add it as the first offline item (after the last online item) + var position = -1 + for i in 0 ..< self.items.len: + if(self.items[i].onlineStatus == OnlineStatus.Offline): + position = i + break + + if(position == -1): + position = self.items.len + + let parentModelIndex = newQModelIndex() + defer: parentModelIndex.delete + + self.beginInsertRows(parentModelIndex, position, position) + self.items.insert(item, position) + self.endInsertRows() proc findIndexForMessageId(self: Model, id: string): int = for i in 0 ..< self.items.len: @@ -68,6 +87,17 @@ QtObject: return -1 + proc removeItemWithIndex(self: Model, index: int) = + let parentModelIndex = newQModelIndex() + defer: parentModelIndex.delete + + self.beginRemoveRows(parentModelIndex, index, index) + self.items.delete(index) + self.endRemoveRows() + + proc isContactWithIdAdded*(self: Model, id: string): bool = + return self.findIndexForMessageId(id) != -1 + proc setName*(self: Model, id: string, name: string) = let ind = self.findIndexForMessageId(id) if(ind == -1): @@ -78,12 +108,38 @@ QtObject: let index = self.createIndex(ind, 0, nil) self.dataChanged(index, index, @[ModelRole.Name.int]) + proc setIcon*(self: Model, id: string, icon: string, isIdenticon: bool) = + let ind = self.findIndexForMessageId(id) + if(ind == -1): + return + + self.items[ind].icon = icon + self.items[ind].isIdenticon = isIdenticon + + let index = self.createIndex(ind, 0, nil) + self.dataChanged(index, index, @[ModelRole.Icon.int, ModelRole.IsIdenticon.int]) + + proc updateItem*(self: Model, id: string, name: string, icon: string, isIdenticon: bool) = + let ind = self.findIndexForMessageId(id) + if(ind == -1): + return + + self.items[ind].name = name + self.items[ind].icon = icon + self.items[ind].isIdenticon = isIdenticon + + let index = self.createIndex(ind, 0, nil) + self.dataChanged(index, index, @[ModelRole.Name.int, ModelRole.Icon.int, ModelRole.IsIdenticon.int]) + proc setOnlineStatus*(self: Model, id: string, onlineStatus: OnlineStatus) = let ind = self.findIndexForMessageId(id) if(ind == -1): return - self.items[ind].onlineStatus = onlineStatus - - let index = self.createIndex(ind, 0, nil) - self.dataChanged(index, index, @[ModelRole.OnlineStatus.int]) \ No newline at end of file + if(self.items[ind].onlineStatus == onlineStatus): + return + + var item = self.items[ind] + item.onlineStatus = onlineStatus + self.removeItemWithIndex(ind) + self.addItem(item) \ No newline at end of file diff --git a/src/app/modules/main/chat_section/chat_content/users/module.nim b/src/app/modules/main/chat_section/chat_content/users/module.nim index 4df22a0358..fa12c188ae 100644 --- a/src/app/modules/main/chat_section/chat_content/users/module.nim +++ b/src/app/modules/main/chat_section/chat_content/users/module.nim @@ -1,11 +1,14 @@ import NimQml import io_interface import ../io_interface as delegate_interface -import view, controller +import view, item, model, controller import ../../../../../global/global_singleton -import ../../../../../../app_service/service/chat/service_interface as chat_service +import ../../../../../../app_service/service/contacts/service as contact_service import ../../../../../../app_service/service/community/service_interface as community_service +import ../../../../../../app_service/service/message/service as message_service + +import eventemitter export io_interface @@ -17,14 +20,16 @@ type controller: controller.AccessInterface moduleLoaded: bool -proc newModule*(delegate: delegate_interface.AccessInterface, chatId: string, belongsToCommunity: bool, - chatService: chat_service.ServiceInterface, communityService: community_service.ServiceInterface): +proc newModule*(delegate: delegate_interface.AccessInterface, events: EventEmitter, sectionId: string, chatId: string, + belongsToCommunity: bool, isUsersListAvailable: bool, contactService: contact_service.Service, + communityService: community_service.ServiceInterface, messageService: message_service.Service): Module = result = Module() result.delegate = delegate result.view = view.newView(result) result.viewVariant = newQVariant(result.view) - result.controller = controller.newController(result, chatId, belongsToCommunity, communityService) + result.controller = controller.newController(result, events, sectionId, chatId, belongsToCommunity, isUsersListAvailable, + contactService, communityService, messageService) result.moduleLoaded = false method delete*(self: Module) = @@ -33,8 +38,6 @@ method delete*(self: Module) = self.controller.delete method load*(self: Module) = - singletonInstance.engine.setRootContextProperty("usersModule", self.viewVariant) - self.controller.init() self.view.load() @@ -42,8 +45,56 @@ method isLoaded*(self: Module): bool = return self.moduleLoaded method viewDidLoad*(self: Module) = + # add me as the first user to the list + let loggedInUserDisplayName = singletonInstance.userProfile.getName() & "(You)" + self.view.model().addItem(initItem(singletonInstance.userProfile.getPubKey(), loggedInUserDisplayName, + OnlineStatus.Online, singletonInstance.userProfile.getIcon(), singletonInstance.userProfile.getIsIdenticon())) + + # add other memebers + let usersKeys = self.controller.getMembersPublicKeys() + for k in usersKeys: + let (name, image, isIdenticon) = self.controller.getContactNameAndImage(k) + let statusUpdateDto = self.controller.getStatusForContact(k) + let status = statusUpdateDto.statusType.int.OnlineStatus + self.view.model().addItem(initItem(k, name, status, image, isidenticon)) + self.moduleLoaded = true self.delegate.usersDidLoad() method getModuleAsVariant*(self: Module): QVariant = - return self.viewVariant \ No newline at end of file + return self.viewVariant + +method newMessagesLoaded*(self: Module, messages: seq[MessageDto]) = + for m in messages: + if(self.view.model().isContactWithIdAdded(m.`from`)): + continue + + let (name, image, isIdenticon) = self.controller.getContactNameAndImage(m.`from`) + let statusUpdateDto = self.controller.getStatusForContact(m.`from`) + let status = statusUpdateDto.statusType.int.OnlineStatus + self.view.model().addItem(initItem(m.`from`, name, status, image, isidenticon)) + +method contactNicknameChanged*(self: Module, publicKey: string, nickname: string) = + if(nickname.len == 0): + let (name, _, _) = self.controller.getContactNameAndImage(publicKey) + self.view.model().setName(publicKey, name) + else: + self.view.model().setName(publicKey, nickname) + +method contactsStatusUpdated*(self: Module, statusUpdates: seq[StatusUpdateDto]) = + for s in statusUpdates: + let status = s.statusType.int.OnlineStatus + self.view.model().setOnlineStatus(s.publicKey, status) + +method contactUpdated*(self: Module, contact: ContactsDto) = + var icon = contact.identicon + var isIdenticon = contact.identicon.len > 0 + if(contact.image.thumbnail.len > 0): + icon = contact.image.thumbnail + isIdenticon = false + + self.view.model().updateItem(contact.id, contact.userNameOrAlias(), icon, isIdenticon) + +method loggedInUserImageChanged*(self: Module) = + self.view.model().setIcon(singletonInstance.userProfile.getPubKey(), singletonInstance.userProfile.getThumbnailImage(), + singletonInstance.userProfile.getIsIdenticon()) \ No newline at end of file diff --git a/src/app/modules/main/chat_section/chat_content/users/private_interfaces/module_controller_delegate_interface.nim b/src/app/modules/main/chat_section/chat_content/users/private_interfaces/module_controller_delegate_interface.nim index e69de29bb2..f4d5c95449 100644 --- a/src/app/modules/main/chat_section/chat_content/users/private_interfaces/module_controller_delegate_interface.nim +++ b/src/app/modules/main/chat_section/chat_content/users/private_interfaces/module_controller_delegate_interface.nim @@ -0,0 +1,17 @@ +import ../../../../../../../app_service/service/message/dto/[message] +import ../../../../../../../app_service/service/contacts/dto/[contacts, status_update] + +method newMessagesLoaded*(self: AccessInterface, messages: seq[MessageDto]) {.base.} = + raise newException(ValueError, "No implementation available") + +method contactNicknameChanged*(self: AccessInterface, publicKey: string, nickname: string) {.base.} = + raise newException(ValueError, "No implementation available") + +method contactsStatusUpdated*(self: AccessInterface, statusUpdates: seq[StatusUpdateDto]) {.base.} = + raise newException(ValueError, "No implementation available") + +method contactUpdated*(self: AccessInterface, contact: ContactsDto) {.base.} = + raise newException(ValueError, "No implementation available") + +method loggedInUserImageChanged*(self: AccessInterface) {.base.} = + raise newException(ValueError, "No implementation available") \ No newline at end of file diff --git a/src/app/modules/main/chat_section/chat_content/users/view.nim b/src/app/modules/main/chat_section/chat_content/users/view.nim index 481558237c..3d6b3e9658 100644 --- a/src/app/modules/main/chat_section/chat_content/users/view.nim +++ b/src/app/modules/main/chat_section/chat_content/users/view.nim @@ -7,9 +7,11 @@ QtObject: View* = ref object of QObject delegate: io_interface.AccessInterface model: Model + modelVariant: QVariant proc delete*(self: View) = self.model.delete + self.modelVariant.delete self.QObject.delete proc newView*(delegate: io_interface.AccessInterface): View = @@ -17,6 +19,19 @@ QtObject: result.QObject.setup result.delegate = delegate result.model = newModel() + result.modelVariant = newQVariant(result.model) + proc model*(self: View): Model = + return self.model + proc load*(self: View) = - self.delegate.viewDidLoad() \ No newline at end of file + self.delegate.viewDidLoad() + + proc modelChanged*(self: View) {.signal.} + + proc getModel(self: View): QVariant {.slot.} = + return self.modelVariant + + QtProperty[QVariant] model: + read = getModel + notify = modelChanged \ No newline at end of file diff --git a/src/app/modules/main/chat_section/chat_content/view.nim b/src/app/modules/main/chat_section/chat_content/view.nim index 9b0d7cdd3c..d37bb6ecd9 100644 --- a/src/app/modules/main/chat_section/chat_content/view.nim +++ b/src/app/modules/main/chat_section/chat_content/view.nim @@ -52,4 +52,7 @@ QtObject: read = getModel proc unpinMessage*(self: View, messageId: string) {.slot.} = - self.delegate.unpinMessage(messageId) \ No newline at end of file + self.delegate.unpinMessage(messageId) + + proc isUsersListAvailable*(self: View): bool {.slot.} = + return self.delegate.isUsersListAvailable() \ No newline at end of file diff --git a/src/app/modules/main/chat_section/controller.nim b/src/app/modules/main/chat_section/controller.nim index 2c89ac86f0..f41d28268d 100644 --- a/src/app/modules/main/chat_section/controller.nim +++ b/src/app/modules/main/chat_section/controller.nim @@ -3,6 +3,7 @@ import Tables import controller_interface import io_interface +import ../../../../app_service/service/contacts/service as contact_service import ../../../../app_service/service/chat/service_interface as chat_service import ../../../../app_service/service/community/service_interface as community_service import ../../../../app_service/service/message/service as message_service @@ -19,19 +20,20 @@ type activeItemId: string activeSubItemId: string events: EventEmitter + contactService: contact_service.Service chatService: chat_service.ServiceInterface communityService: community_service.ServiceInterface messageService: message_service.Service proc newController*(delegate: io_interface.AccessInterface, sectionId: string, isCommunity: bool, events: EventEmitter, - chatService: chat_service.ServiceInterface, - communityService: community_service.ServiceInterface, - messageService: message_service.Service): Controller = + contactService: contact_service.Service, chatService: chat_service.ServiceInterface, + communityService: community_service.ServiceInterface, messageService: message_service.Service): Controller = result = Controller() result.delegate = delegate result.sectionId = sectionId result.isCommunitySection = isCommunity result.events = events + result.contactService = contactService result.chatService = chatService result.communityService = communityService result.messageService = messageService @@ -87,5 +89,5 @@ method getOneToOneChatNameAndImage*(self: Controller, chatId: string): method createPublicChat*(self: Controller, chatId: string) = let response = self.chatService.createPublicChat(chatId) if(response.success): - self.delegate.addNewPublicChat(response.chatDto, self.events, self.chatService, self.communityService, - self.messageService) \ No newline at end of file + self.delegate.addNewPublicChat(response.chatDto, self.events, self.contactService, self.chatService, + self.communityService, self.messageService) \ No newline at end of file diff --git a/src/app/modules/main/chat_section/module.nim b/src/app/modules/main/chat_section/module.nim index 236a77f94c..1aae7ad9fc 100644 --- a/src/app/modules/main/chat_section/module.nim +++ b/src/app/modules/main/chat_section/module.nim @@ -5,6 +5,7 @@ import view, controller, item, sub_item, model, sub_model import chat_content/module as chat_content_module +import ../../../../app_service/service/contacts/service as contact_service import ../../../../app_service/service/chat/service_interface as chat_service import ../../../../app_service/service/community/service_interface as community_service import ../../../../app_service/service/message/service as message_service @@ -31,6 +32,7 @@ proc newModule*( events: EventEmitter, sectionId: string, isCommunity: bool, + contactService: contact_service.Service, chatService: chat_service.ServiceInterface, communityService: community_service.ServiceInterface, messageService: message_service.Service @@ -39,8 +41,8 @@ proc newModule*( result.delegate = delegate result.view = view.newView(result) result.viewVariant = newQVariant(result.view) - result.controller = controller.newController(result, sectionId, isCommunity, events, chatService, communityService, - messageService) + result.controller = controller.newController(result, sectionId, isCommunity, events, contactService, chatService, + communityService, messageService) result.moduleLoaded = false result.chatContentModule = initOrderedTable[string, chat_content_module.AccessInterface]() @@ -56,14 +58,15 @@ method delete*(self: Module) = method isCommunity*(self: Module): bool = return self.controller.isCommunity() -proc addSubmodule(self: Module, chatId: string, belongToCommunity: bool, events: EventEmitter, +proc addSubmodule(self: Module, chatId: string, belongToCommunity: bool, isUsersListAvailable: bool, events: EventEmitter, + contactService: contact_service.Service, chatService: chat_service.ServiceInterface, + communityService: community_service.ServiceInterface, messageService: message_service.Service) = + self.chatContentModule[chatId] = chat_content_module.newModule(self, events, self.controller.getMySectionId(), chatId, + belongToCommunity, isUsersListAvailable, contactService, chatService, communityService, messageService) + +proc buildChatUI(self: Module, events: EventEmitter, contactService: contact_service.Service, chatService: chat_service.ServiceInterface, communityService: community_service.ServiceInterface, messageService: message_service.Service) = - self.chatContentModule[chatId] = chat_content_module.newModule(self, events, chatId, belongToCommunity, chatService, - communityService, messageService) - -proc buildChatUI(self: Module, events: EventEmitter, chatService: chat_service.ServiceInterface, - communityService: community_service.ServiceInterface, messageService: message_service.Service) = let types = @[ChatType.OneToOne, ChatType.Public, ChatType.PrivateGroupChat] let chats = self.controller.getChatDetailsForChatTypes(types) @@ -74,13 +77,15 @@ proc buildChatUI(self: Module, events: EventEmitter, chatService: chat_service.S var chatName = c.name var chatImage = c.identicon var isIdenticon = false + var isUsersListAvailable = true if(c.chatType == ChatType.OneToOne): + isUsersListAvailable = false (chatName, chatImage, isIdenticon) = self.controller.getOneToOneChatNameAndImage(c.id) let item = initItem(c.id, chatName, chatImage, isIdenticon, c.color, c.description, c.chatType.int, hasNotification, notificationsCount, c.muted, false, 0) self.view.appendItem(item) - self.addSubmodule(c.id, false, events, chatService, communityService, messageService) + self.addSubmodule(c.id, false, isUsersListAvailable, events, contactService, chatService, communityService, messageService) # make the first Public chat active when load the app if(selectedItemId.len == 0 and c.chatType == ChatType.Public): @@ -88,8 +93,9 @@ proc buildChatUI(self: Module, events: EventEmitter, chatService: chat_service.S self.setActiveItemSubItem(selectedItemId, "") -proc buildCommunityUI(self: Module, events: EventEmitter, chatService: chat_service.ServiceInterface, - communityService: community_service.ServiceInterface, messageService: message_service.Service) = +proc buildCommunityUI(self: Module, events: EventEmitter, contactService: contact_service.Service, + chatService: chat_service.ServiceInterface, communityService: community_service.ServiceInterface, + messageService: message_service.Service) = var selectedItemId = "" var selectedSubItemId = "" let communityIds = self.controller.getCommunityIds() @@ -107,7 +113,7 @@ proc buildCommunityUI(self: Module, events: EventEmitter, chatService: chat_serv let channelItem = initItem(chatDto.id, chatDto.name, chatDto.identicon, false, chatDto.color, chatDto.description, chatDto.chatType.int, hasNotification, notificationsCount, chatDto.muted, false, c.position) self.view.appendItem(channelItem) - self.addSubmodule(chatDto.id, true, events, chatService, communityService, messageService) + self.addSubmodule(chatDto.id, true, false, events, contactService, chatService, communityService, messageService) # make the first channel which doesn't belong to any category active when load the app if(selectedItemId.len == 0): @@ -133,7 +139,7 @@ proc buildCommunityUI(self: Module, events: EventEmitter, chatService: chat_serv let channelItem = initSubItem(chatDto.id, cat.id, chatDto.name, chatDto.identicon, false, chatDto.color, chatDto.description, hasNotification, notificationsCount, chatDto.muted, false, c.position) categoryChannels.add(channelItem) - self.addSubmodule(chatDto.id, true, events, chatService, communityService, messageService) + self.addSubmodule(chatDto.id, true, false, events, contactService, chatService, communityService, messageService) # in case there is no channels beyond categories, # make the first channel of the first category active when load the app @@ -148,15 +154,16 @@ proc buildCommunityUI(self: Module, events: EventEmitter, chatService: chat_serv self.setActiveItemSubItem(selectedItemId, selectedSubItemId) -method load*(self: Module, events: EventEmitter, chatService: chat_service.ServiceInterface, - communityService: community_service.ServiceInterface, messageService: message_service.Service) = +method load*(self: Module, events: EventEmitter, contactService: contact_service.Service, + chatService: chat_service.ServiceInterface, communityService: community_service.ServiceInterface, + messageService: message_service.Service) = self.controller.init() self.view.load() if(self.controller.isCommunity()): - self.buildCommunityUI(events, chatService, communityService, messageService) + self.buildCommunityUI(events, contactService, chatService, communityService, messageService) else: - self.buildChatUI(events, chatService, communityService, messageService) + self.buildChatUI(events, contactService, chatService, communityService, messageService) for cModule in self.chatContentModule.values: cModule.load() @@ -230,14 +237,15 @@ method createPublicChat*(self: Module, chatId: string) = self.controller.createPublicChat(chatId) -method addNewPublicChat*(self: Module, chatDto: ChatDto, events: EventEmitter, chatService: chat_service.ServiceInterface, - communityService: community_service.ServiceInterface, messageService: message_service.Service) = +method addNewPublicChat*(self: Module, chatDto: ChatDto, events: EventEmitter, contactService: contact_service.Service, + chatService: chat_service.ServiceInterface, communityService: community_service.ServiceInterface, + messageService: message_service.Service) = let hasNotification = chatDto.unviewedMessagesCount > 0 or chatDto.unviewedMentionsCount > 0 let notificationsCount = chatDto.unviewedMentionsCount let item = initItem(chatDto.id, chatDto.name, chatDto.identicon, true, chatDto.color, chatDto.description, chatDto.chatType.int, hasNotification, notificationsCount, chatDto.muted, false, 0) self.view.appendItem(item) - self.addSubmodule(chatDto.id, false, events, chatService, communityService, messageService) + self.addSubmodule(chatDto.id, false, true, events, contactService, chatService, communityService, messageService) # make new added chat active one self.setActiveItemSubItem(item.id, "") \ No newline at end of file diff --git a/src/app/modules/main/chat_section/private_interfaces/module_access_interface.nim b/src/app/modules/main/chat_section/private_interfaces/module_access_interface.nim index a2c097486f..4109c9acd6 100644 --- a/src/app/modules/main/chat_section/private_interfaces/module_access_interface.nim +++ b/src/app/modules/main/chat_section/private_interfaces/module_access_interface.nim @@ -1,5 +1,6 @@ import NimQml +import ../../../../../app_service/service/contacts/service as contact_service import ../../../../../app_service/service/chat/service_interface as chat_service import ../../../../../app_service/service/community/service_interface as community_service import ../../../../../app_service/service/message/service as message_service @@ -9,8 +10,9 @@ import eventemitter method delete*(self: AccessInterface) {.base.} = raise newException(ValueError, "No implementation available") -method load*(self: AccessInterface, events: EventEmitter, chatService: chat_service.ServiceInterface, - communityService: community_service.ServiceInterface, messageService: message_service.Service) {.base.} = +method load*(self: AccessInterface, events: EventEmitter, contactService: contact_service.Service, + chatService: chat_service.ServiceInterface, communityService: community_service.ServiceInterface, + messageService: message_service.Service) {.base.} = raise newException(ValueError, "No implementation available") method isLoaded*(self: AccessInterface): bool {.base.} = diff --git a/src/app/modules/main/chat_section/private_interfaces/module_controller_delegate_interface.nim b/src/app/modules/main/chat_section/private_interfaces/module_controller_delegate_interface.nim index cb62e99e67..d63bcbddb5 100644 --- a/src/app/modules/main/chat_section/private_interfaces/module_controller_delegate_interface.nim +++ b/src/app/modules/main/chat_section/private_interfaces/module_controller_delegate_interface.nim @@ -2,6 +2,6 @@ method activeItemSubItemSet*(self: AccessInterface, itemId: string, subItemId: s raise newException(ValueError, "No implementation available") method addNewPublicChat*(self: AccessInterface, chatDto: ChatDto, events: EventEmitter, - chatService: chat_service.ServiceInterface, communityService: community_service.ServiceInterface, - messageService: message_service.Service) {.base.} = + contactService: contact_service.Service, chatService: chat_service.ServiceInterface, + communityService: community_service.ServiceInterface, messageService: message_service.Service) {.base.} = raise newException(ValueError, "No implementation available") \ No newline at end of file diff --git a/src/app/modules/main/module.nim b/src/app/modules/main/module.nim index b729c3985a..d67b34237d 100644 --- a/src/app/modules/main/module.nim +++ b/src/app/modules/main/module.nim @@ -84,8 +84,8 @@ proc newModule*[T]( result.moduleLoaded = false # Submodules - result.chatSectionModule = chat_section_module.newModule(result, events, conf.CHAT_SECTION_ID, false, chatService, - communityService, messageService) + result.chatSectionModule = chat_section_module.newModule(result, events, conf.CHAT_SECTION_ID, false, contactsService, + chatService, communityService, messageService) result.communitySectionsModule = initOrderedTable[string, chat_section_module.AccessInterface]() result.walletSectionModule = wallet_section_module.newModule[Module[T]](result, events, tokenService, transactionService, collectible_service, walletAccountService, settingsService) @@ -114,6 +114,7 @@ method delete*[T](self: Module[T]) = method load*[T]( self: Module[T], events: EventEmitter, + contactsService: contacts_service.Service, chatService: chat_service.Service, communityService: community_service.Service, messageService: message_service.Service @@ -131,6 +132,7 @@ method load*[T]( events, c.id, true, + contactsService, chatService, communityService, messageService @@ -209,9 +211,9 @@ method load*[T]( activeSection = profileSettingsSectionItem # Load all sections - self.chatSectionModule.load(events, chatService, communityService, messageService) + self.chatSectionModule.load(events, contactsService, chatService, communityService, messageService) for cModule in self.communitySectionsModule.values: - cModule.load(events, chatService, communityService, messageService) + cModule.load(events, contactsService, chatService, communityService, messageService) self.walletSectionModule.load() # self.walletV2SectionModule.load() self.browserSectionModule.load() diff --git a/src/app/modules/main/private_interfaces/module_access_interface.nim b/src/app/modules/main/private_interfaces/module_access_interface.nim index 12458a7ec5..aaa11409be 100644 --- a/src/app/modules/main/private_interfaces/module_access_interface.nim +++ b/src/app/modules/main/private_interfaces/module_access_interface.nim @@ -1,3 +1,4 @@ +import ../../../../app_service/service/contacts/service as contacts_service import ../../../../app_service/service/chat/service as chat_service import ../../../../app_service/service/community/service as community_service import ../../../../app_service/service/message/service as message_service @@ -10,6 +11,7 @@ method delete*(self: AccessInterface) {.base.} = method load*( self: AccessInterface, events: EventEmitter, + contactsService: contacts_service.Service, chatService: chat_service.Service, communityService: community_service.Service, messageService: message_service.Service diff --git a/src/app_service/common/json_utils.nim b/src/app_service/common/json_utils.nim index 8fafb94a65..dc23940a29 100644 --- a/src/app_service/common/json_utils.nim +++ b/src/app_service/common/json_utils.nim @@ -17,6 +17,14 @@ template getProp(obj: JsonNode, prop: string, value: var typedesc[int64]): bool success +template getProp(obj: JsonNode, prop: string, value: var typedesc[uint64]): bool = + var success = false + if (obj.kind == JObject and obj.contains(prop)): + value = uint64(obj[prop].getBiggestInt) + success = true + + success + template getProp(obj: JsonNode, prop: string, value: var typedesc[string]): bool = var success = false if (obj.kind == JObject and obj.contains(prop)): diff --git a/src/app_service/service/contacts/async_tasks.nim b/src/app_service/service/contacts/async_tasks.nim index a361989435..3797cb2018 100644 --- a/src/app_service/service/contacts/async_tasks.nim +++ b/src/app_service/service/contacts/async_tasks.nim @@ -1,3 +1,4 @@ +import os import status/ens as status_ens include ../../common/json_utils @@ -17,3 +18,16 @@ const lookupContactTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.} = if not id.startsWith("0x"): id = status_ens.pubkey(id) arg.finish(id) + +################################################# +# Async timer +################################################# + +type + TimerTaskArg = ref object of QObjectTaskArg + timeoutInMilliseconds: int + +const timerTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.} = + let arg = decode[TimerTaskArg](argEncoded) + sleep(arg.timeoutInMilliseconds) + arg.finish("done") diff --git a/src/app_service/service/contacts/dto/contacts.nim b/src/app_service/service/contacts/dto/contacts.nim index 7413a87508..58ba0503e3 100644 --- a/src/app_service/service/contacts/dto/contacts.nim +++ b/src/app_service/service/contacts/dto/contacts.nim @@ -17,6 +17,7 @@ type ContactsDto* = object alias*: string identicon*: string lastUpdated*: int64 + lastUpdatedLocally*: int64 localNickname*: string image*: Images added*: bool @@ -36,9 +37,10 @@ proc `$`*(self: ContactsDto): string = id: {self.id}, name: {self.name}, ensVerified: {self.ensVerified}, - alias: {self.alias}, - identicon: {self.identicon}, - lastUpdated: {self.lastUpdated}, + alias: {self.alias}, + identicon: {self.identicon}, + lastUpdated: {self.lastUpdated}, + lastUpdatedLocally: {self.lastUpdatedLocally}, localNickname: {self.localNickname}, image:[ {$self.image} @@ -69,6 +71,7 @@ proc toContactsDto*(jsonObj: JsonNode): ContactsDto = discard jsonObj.getProp("alias", result.alias) discard jsonObj.getProp("identicon", result.identicon) discard jsonObj.getProp("lastUpdated", result.lastUpdated) + discard jsonObj.getProp("lastUpdatedLocally", result.lastUpdatedLocally) discard jsonObj.getProp("localNickname", result.localNickname) var imageObj: JsonNode diff --git a/src/app_service/service/contacts/dto/status_update.nim b/src/app_service/service/contacts/dto/status_update.nim new file mode 100644 index 0000000000..9e0fa12f4b --- /dev/null +++ b/src/app_service/service/contacts/dto/status_update.nim @@ -0,0 +1,26 @@ +import json +include ../../../common/json_utils + +type StatusType* {.pure.}= enum + Offline = 0, + Online + DoNotDisturb + Idle + Invisible + +type StatusUpdateDto* = object + publicKey*: string + statusType*: StatusType + clock*: uint64 + text*: string + +proc toStatusUpdateDto*(jsonObj: JsonNode): StatusUpdateDto = + discard jsonObj.getProp("publicKey", result.publicKey) + discard jsonObj.getProp("clock", result.clock) + discard jsonObj.getProp("text", result.text) + + result.statusType = StatusType.Offline + var statusTypeInt: int + if (jsonObj.getProp("statusType", statusTypeInt) and + (statusTypeInt >= ord(low(StatusType)) or statusTypeInt <= ord(high(StatusType)))): + result.statusType = StatusType(statusTypeInt) \ No newline at end of file diff --git a/src/app_service/service/contacts/service.nim b/src/app_service/service/contacts/service.nim index c56e98f6d1..b690b8e648 100644 --- a/src/app_service/service/contacts/service.nim +++ b/src/app_service/service/contacts/service.nim @@ -1,15 +1,18 @@ -import NimQml, Tables, json, sequtils, strformat, chronicles, strutils +import NimQml, Tables, json, sequtils, strformat, chronicles, strutils, times import eventemitter +import ../../../app/global/global_singleton +import ../../../app/core/signals/types import ../../../app/core/tasks/[qt, threadpool] import ./dto/contacts as contacts_dto +import ./dto/status_update as status_update_dto import status/statusgo_backend_new/contacts as status_contacts import status/statusgo_backend_new/accounts as status_accounts import status/statusgo_backend_new/chat as status_chat import status/statusgo_backend_new/utils as status_utils -export contacts_dto +export contacts_dto, status_update_dto include async_tasks @@ -26,6 +29,17 @@ type ContactAddedArgs* = ref object of Args contact*: ContactsDto + ContactUpdatedArgs* = ref object of Args + contact*: ContactsDto + + ContactsStatusUpdatedArgs* = ref object of Args + statusUpdates*: seq[StatusUpdateDto] + +# Local Constants: +const CheckStatusIntervalInMilliseconds = 5000 # 5 seconds, this is timeout how often do we check for user status. +const OnlineLimitInSeconds = int(5.5 * 60) # 5.5 minutes +const IdleLimitInSeconds = int(7 * 60) # 7 minutes + # Signals which may be emitted by this service: const SIGNAL_CONTACT_LOOKED_UP* = "SIGNAL_CONTACT_LOOKED_UP" # Remove new when old code is removed @@ -34,23 +48,43 @@ const SIGNAL_CONTACT_BLOCKED* = "new-contactBlocked" const SIGNAL_CONTACT_UNBLOCKED* = "new-contactUnblocked" const SIGNAL_CONTACT_REMOVED* = "new-contactRemoved" const SIGNAL_CONTACT_NICKNAME_CHANGED* = "new-contactNicknameChanged" +const SIGNAL_CONTACTS_STATUS_UPDATED* = "new-contactsStatusUpdated" +const SIGNAL_CONTACT_UPDATED* = "new-contactUpdated" +const SIGNAL_LOGGEDIN_USER_IMAGE_CHANGED* = "new-loggedInUserImageChanged" QtObject: type Service* = ref object of QObject threadpool: ThreadPool contacts: Table[string, ContactsDto] # [contact_id, ContactsDto] + contactsStatus: Table[string, StatusUpdateDto] # [contact_id, StatusUpdateDto] events: EventEmitter + closingApp: bool + + # Forward declaration + proc getContactById*(self: Service, id: string): ContactsDto + proc saveContact(self: Service, contact: ContactsDto) + proc startCheckingContactStatuses(self: Service) proc delete*(self: Service) = + self.closingApp = true + self.contacts.clear + self.contactsStatus.clear self.QObject.delete proc newService*(events: EventEmitter, threadpool: ThreadPool): Service = new(result, delete) result.QObject.setup + result.closingApp = false result.events = events result.threadpool = threadpool result.contacts = initTable[string, ContactsDto]() + signalConnect(singletonInstance.userProfile, "imageChanged()", result, "onLoggedInUserImageChange()", 2) + + proc addContact(self: Service, contact: ContactsDto) = + # Private proc, used for adding contacts only. + self.contacts[contact.id] = contact + self.contactsStatus[contact.id] = StatusUpdateDto(publicKey: contact.id, statusType: StatusType.Offline) proc fetchContacts*(self: Service) = try: @@ -59,15 +93,45 @@ QtObject: let contacts = map(response.result.getElems(), proc(x: JsonNode): ContactsDto = x.toContactsDto()) for contact in contacts: - self.contacts[contact.id] = contact + self.addContact(contact) except Exception as e: let errDesription = e.msg error "error: ", errDesription return + proc doConnect(self: Service) = + self.events.on(SignalType.Message.event) do(e:Args): + var receivedData = MessageSignal(e) + if(receivedData.statusUpdates.len > 0): + for s in receivedData.statusUpdates: + if(not self.contactsStatus.hasKey(s.publicKey)): + # we shouldn't be here ever, but the following line ensures we have added a contact before setting status for it + discard self.getContactById(s.publicKey) + + self.contactsStatus[s.publicKey] = s + + let data = ContactsStatusUpdatedArgs(statusUpdates: receivedData.statusUpdates) + self.events.emit(SIGNAL_CONTACTS_STATUS_UPDATED, data) + + if(receivedData.contacts.len > 0): + for c in receivedData.contacts: + let localContact = self.getContactById(c.id) + var receivedContact = c + receivedContact.localNickname = localContact.localNickname + self.saveContact(receivedContact) + + let data = ContactUpdatedArgs(contact: receivedContact) + self.events.emit(SIGNAL_CONTACT_UPDATED, data) + proc init*(self: Service) = self.fetchContacts() + self.doConnect() + self.startCheckingContactStatuses() + + proc onLoggedInUserImageChange*(self: Service) {.slot.} = + let data = Args() + self.events.emit(SIGNAL_LOGGEDIN_USER_IMAGE_CHANGED, data) proc getContacts*(self: Service): seq[ContactsDto] = return toSeq(self.contacts.values) @@ -77,7 +141,10 @@ QtObject: let response = status_contacts.getContactByID(id) result = response.result.toContactsDto() - self.contacts[result.id] = result + if result.id.len == 0: + return + + self.addContact(result) except Exception as e: let errDesription = e.msg @@ -109,6 +176,15 @@ QtObject: blocked: false, hasAddedUs: false ) + self.addContact(result) + + proc getStatusForContactWithId*(self: Service, publicKey: string): StatusUpdateDto = + # This method will fetch current accurate status from `status-go` once we add an api point there for it. + if(not self.contactsStatus.hasKey(publicKey)): + # following line ensures that we have added a contact before setting status for it + discard self.getContactById(publicKey) + + return self.contactsStatus[publicKey] proc getContactNameAndImage*(self: Service, publicKey: string): tuple[name: string, image: string, isIdenticon: bool] = ## This proc should be used accross the app in order to have for the same contact @@ -207,6 +283,8 @@ QtObject: self.events.emit(SIGNAL_CONTACT_LOOKED_UP, data) proc lookupContact*(self: Service, value: string) = + if(self.closingApp): + return let arg = LookupContactTaskArg( tptr: cast[ByteAddress](lookupContactTask), vptr: cast[ByteAddress](self.vptr), @@ -214,3 +292,42 @@ QtObject: value: value ) self.threadpool.start(arg) + + proc checkContactsStatus*(self: Service, response: string) {.slot.} = + let nowInMyLocalZone = now() + let timestampNow = uint64(nowInMyLocalZone.toTime().toUnix()) + var updatedStatuses: seq[StatusUpdateDto] + for status in self.contactsStatus.mvalues: + if(timestampNow - status.clock < uint64(OnlineLimitInSeconds)): + if(status.statusType == StatusType.Online): + continue + else: + status.statusType = StatusType.Online + updatedStatuses.add(status) + elif(timestampNow - status.clock < uint64(IdleLimitInSeconds)): + if(status.statusType == StatusType.Idle): + continue + else: + status.statusType = StatusType.Idle + updatedStatuses.add(status) + elif(status.statusType != StatusType.Offline): + status.statusType = StatusType.Offline + updatedStatuses.add(status) + + if(updatedStatuses.len > 0): + let data = ContactsStatusUpdatedArgs(statusUpdates: updatedStatuses) + self.events.emit(SIGNAL_CONTACTS_STATUS_UPDATED, data) + + self.startCheckingContactStatuses() + + proc startCheckingContactStatuses(self: Service) = + if(self.closingApp): + return + + let arg = TimerTaskArg( + tptr: cast[ByteAddress](timerTask), + vptr: cast[ByteAddress](self.vptr), + slot: "checkContactsStatus", + timeoutInMilliseconds: CheckStatusIntervalInMilliseconds + ) + self.threadpool.start(arg) diff --git a/ui/app/AppLayouts/Chat/ChatLayout.qml b/ui/app/AppLayouts/Chat/ChatLayout.qml index c79e38f2f3..274cd865e8 100644 --- a/ui/app/AppLayouts/Chat/ChatLayout.qml +++ b/ui/app/AppLayouts/Chat/ChatLayout.qml @@ -29,6 +29,18 @@ StatusAppThreePanelLayout { // Each `ChatLayout` has its own chatCommunitySectionModule // (on the backend chat and community sections share the same module since they are actually the same) property var chatCommunitySectionModule + // Since qml component doesn't follow encaptulation from the backend side, we're introducing + // a method which will return appropriate chat content module for selected chat/channel + function currentChatContentModule(){ + // When we decide to have the same struct as it's on the backend we will remove this function. + // So far this is a way to deal with refactord backend from the current qml structure. + if(chatCommunitySectionModule.activeItem.isSubItemActive) + chatCommunitySectionModule.prepareChatContentModuleForChatId(chatCommunitySectionModule.activeItem.activeSubItem.id) + else + chatCommunitySectionModule.prepareChatContentModuleForChatId(chatCommunitySectionModule.activeItem.id) + + return chatCommunitySectionModule.getChatContentModule() + } // Not Refactored property var messageStore @@ -72,35 +84,37 @@ StatusAppThreePanelLayout { } } - showRightPanel: (localAccountSensitiveSettings.expandUsersList && (localAccountSensitiveSettings.showOnlineUsers || chatsModel.communities.activeCommunity.active) - && (chatsModel.channelView.activeChannel.chatType !== Constants.chatTypeOneToOne)) + showRightPanel: { + // Check if user list is available as an option for particular chat content module. + let usersListAvailable = currentChatContentModule().isUsersListAvailable() + return localAccountSensitiveSettings.showOnlineUsers && usersListAvailable && localAccountSensitiveSettings.expandUsersList + } + rightPanel: localAccountSensitiveSettings.communitiesEnabled && chatCommunitySectionModule.isCommunity()? communityUserListComponent : userListComponent Component { id: communityUserListComponent CommunityUserListPanel { - //Not Refactored Yet - //currentTime: chatColumn.currentTime messageContextMenu: quickActionMessageOptionsMenu -// profilePubKey: userProfile.pubKey -// community: root.rootStore.chatsModelInst.communities.activeCommunity -// currentUserName: Utils.removeStatusEns(root.rootStore.profileModelInst.ens.preferredUsername -// || root.rootStore.profileModelInst.profile.username) -// currentUserOnline: root.store.userProfileInst.userStatus -// contactsList: root.rootStore.allContacts + usersModule: { + if(chatCommunitySectionModule.activeItem.isSubItemActive) + chatCommunitySectionModule.prepareChatContentModuleForChatId(chatCommunitySectionModule.activeItem.activeSubItem.id) + else + chatCommunitySectionModule.prepareChatContentModuleForChatId(chatCommunitySectionModule.activeItem.id) + + return chatCommunitySectionModule.getChatContentModule().usersModule + } } } Component { id: userListComponent UserListPanel { - //Not Refactored Yet - //currentTime: chatColumn.currentTime - //userList: chatColumn.userList messageContextMenu: quickActionMessageOptionsMenu -// profilePubKey: userProfile.pubKey -// contactsList: root.rootStore.allContacts -// isOnline: root.rootStore.chatsModelInst.isOnline + usersModule: { + chatCommunitySectionModule.prepareChatContentModuleForChatId(chatCommunitySectionModule.activeItem.id) + return chatCommunitySectionModule.getChatContentModule().usersModule + } } } diff --git a/ui/app/AppLayouts/Chat/controls/UserDelegate.qml b/ui/app/AppLayouts/Chat/controls/UserDelegate.qml index dfeccc9a65..a1ca16031e 100644 --- a/ui/app/AppLayouts/Chat/controls/UserDelegate.qml +++ b/ui/app/AppLayouts/Chat/controls/UserDelegate.qml @@ -9,95 +9,113 @@ import StatusQ.Components 0.1 import StatusQ.Core.Theme 0.1 import StatusQ.Core 0.1 -StatusListItem { - id: root +Item { + id: wrapper anchors.right: parent.right - anchors.rightMargin: 8 anchors.left: parent.left - anchors.leftMargin: 8 - implicitHeight: 44 - leftPadding: 8 - rightPadding: 8 + height: rectangle.height + 4 - property var contactsList - property int statusType: -1 - property string name: "" property string publicKey: "" - property string profilePubKey: "" + property string name: "" property string identicon: "" - property bool isCurrentUser: (publicKey === profilePubKey) - property string profileImage: appMain.getProfileImage(publicKey) || "" - property bool highlighted: false - property string lastSeen: "" - property bool isOnline: false - property var currentTime + property bool isIdenticon: true + property int userStatus: Constants.userStatus.offline property var messageContextMenu - - - title: isCurrentUser ? qsTr("You") : Emoji.parse(Utils.removeStatusEns(Utils.filterXSS(root.name))) - image.source: profileImage || identicon - image.isIdenticon: !profileImage - image.width: 24 - image.height: 24 - statusListItemIcon.anchors.topMargin: 10 - statusListItemTitle.elide: Text.ElideRight - statusListItemTitle.wrapMode: Text.NoWrap - color: sensor.containsMouse ? - Theme.palette.statusChatListItem.hoverBackgroundColor : - Theme.palette.baseColor4 - - sensor.onClicked: { - if (mouse.button === Qt.LeftButton) { - //TODO remove dynamic scoping - openProfilePopup(root.name, root.publicKey, (root.profileImage || root.identicon), "", appMain.getUserNickname(root.publicKey)); - } - else if (mouse.button === Qt.RightButton && !!messageContextMenu) { - // Set parent, X & Y positions for the messageContextMenu - messageContextMenu.parent = root - messageContextMenu.setXPosition = function() { return 0} - messageContextMenu.setYPosition = function() { return root.height} - messageContextMenu.isProfile = true; - messageContextMenu.show(root.name, root.publicKey, (root.profileImage || root.identicon), "", appMain.getUserNickname(root.publicKey)) + property bool enableMouseArea: true + property color color: { + if (wrapper.hovered) { + return Style.current.menuBackgroundHover } + return Style.current.transparent } - Connections { - enabled: !!root.contactsList - target: root.contactsList - onContactChanged: { - if (pubkey === root.publicKey) { - root.profileImage = !!appMain.getProfileImage(root.publicKey) ? - appMain.getProfileImage(root.publicKey) : "" + Rectangle { + id: rectangle + width: parent.width + height: 40 + radius: 8 + color: wrapper.color + + StatusSmartIdenticon { + id: contactImage + anchors.left: parent.left + anchors.leftMargin: Style.current.padding + anchors.verticalCenter: parent.verticalCenter + image: StatusImageSettings { + width: 28 + height: 28 + source: wrapper.identicon + isIdenticon: wrapper.isIdenticon + } + icon: StatusIconSettings { + width: 28 + height: 28 + letterSize: 15 + } + name: wrapper.name + } + + StyledText { + id: contactInfo + text: wrapper.name + anchors.right: parent.right + anchors.rightMargin: Style.current.smallPadding + elide: Text.ElideRight + color: Style.current.textColor + font.weight: Font.Medium + font.pixelSize: 15 + anchors.left: contactImage.right + anchors.leftMargin: Style.current.halfPadding + anchors.verticalCenter: parent.verticalCenter + } + + StatusBadge { + id: statusBadge + width: 15 + height: 15 + anchors.left: contactImage.right + anchors.leftMargin: -Style.current.smallPadding + anchors.bottom: contactImage.bottom + visible: wrapper.userStatus !== Constants.userStatus.offline + border.width: 3 + border.color: Theme.palette.statusAppNavBar.backgroundColor + color: { + if(wrapper.userStatus === Constants.userStatus.online) + return Style.current.green + else if(wrapper.userStatus === Constants.userStatus.idle) + return Style.current.orange + else if(wrapper.userStatus === Constants.userStatus.doNotDisturb) + return Style.current.red + + return "transparent" } } - } - StatusBadge { - id: statusBadge - width: 15 - height: 15 - anchors.left: parent.left - anchors.leftMargin: 22 - anchors.bottom: parent.bottom - anchors.bottomMargin: 6 - visible: root.isOnline && !((root.statusType === -1) && (lastSeenMinutesAgo > 7)) - border.width: 3 - border.color: root.sensor.containsMouse ? Theme.palette.baseColor2 : Theme.palette.baseColor4 - property real lastSeenMinutesAgo: ((currentTime/1000 - parseInt(lastSeen)) / 60); - color: { - if (visible) { - if (statusType === Constants.statusType_DoNotDisturb) { - return Style.current.red; - } else if (isCurrentUser || (lastSeenMinutesAgo < 5.5)) { - return Style.current.green; - } else if (((statusType !== -1) && (lastSeenMinutesAgo > 5.5)) || - ((statusType === -1) && (lastSeenMinutesAgo < 7))) { - return Style.current.orange; - } else if ((statusType === -1) && (lastSeenMinutesAgo > 7)) { - return "transparent"; + MouseArea { + enabled: enableMouseArea + cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton + anchors.fill: parent + hoverEnabled: true + onEntered: { + wrapper.hovered = true + } + onExited: { + wrapper.hovered = false + } + onClicked: { + if (mouse.button === Qt.LeftButton) { + //TODO remove dynamic scoping + openProfilePopup(wrapper.name, wrapper.publicKey, wrapper.identicon, "", wrapper.name); + } + else if (mouse.button === Qt.RightButton && !!messageContextMenu) { + // Set parent, X & Y positions for the messageContextMenu + messageContextMenu.parent = rectangle + messageContextMenu.setXPosition = function() { return 0} + messageContextMenu.setYPosition = function() { return rectangle.height} + messageContextMenu.isProfile = true; + messageContextMenu.show(wrapper.name, wrapper.publicKey, wrapper.identicon, "", wrapper.name) } - } else { - return "transparent"; } } } diff --git a/ui/app/AppLayouts/Chat/panels/UserListPanel.qml b/ui/app/AppLayouts/Chat/panels/UserListPanel.qml index 9f8dc1fc8b..92ede3b98f 100644 --- a/ui/app/AppLayouts/Chat/panels/UserListPanel.qml +++ b/ui/app/AppLayouts/Chat/panels/UserListPanel.qml @@ -1,11 +1,5 @@ import QtQuick 2.13 -import Qt.labs.platform 1.1 import QtQuick.Controls 2.13 -import QtQuick.Window 2.13 -import QtQuick.Layouts 1.13 -import QtQml.Models 2.13 -import QtGraphicalEffects 1.13 -import QtQuick.Dialogs 1.3 import shared 1.0 import shared.panels 1.0 import shared.status 1.0 @@ -17,11 +11,11 @@ import utils 1.0 Item { id: root anchors.fill: parent - property var userList - property var currentTime - property bool isOnline - property var contactsList - property string profilePubKey + + // Important: + // Each chat/community has its own ChatContentModule and each ChatContentModule has a single usersModule + // usersModule on the backend contains everything needed for this component + property var usersModule property var messageContextMenu StyledText { @@ -52,26 +46,14 @@ Item { bottomMargin: Style.current.bigPadding } boundsBehavior: Flickable.StopAtBounds - model: userListDelegate - } - - DelegateModelGeneralized { - id: userListDelegate - lessThan: [ - function (left, right) { - return (left.lastSeen > right.lastSeen); - } - ] - model: root.userList + model: usersModule.model delegate: UserDelegate { - name: model.userName - publicKey: model.publicKey - profilePubKey: root.profilePubKey - identicon: model.identicon - contactsList: root.contactsList - lastSeen: model.lastSeen / 1000 - currentTime: root.currentTime - isOnline: root.isOnline + publicKey: model.id + name: model.name + identicon: model.icon + isIdenticon: model.isIdenticon + userStatus: model.onlineStatus + messageContextMenu: root.messageContextMenu } } } diff --git a/ui/app/AppLayouts/Chat/panels/communities/CommunityUserListPanel.qml b/ui/app/AppLayouts/Chat/panels/communities/CommunityUserListPanel.qml index 9f5a7f7b2b..af398ffe64 100644 --- a/ui/app/AppLayouts/Chat/panels/communities/CommunityUserListPanel.qml +++ b/ui/app/AppLayouts/Chat/panels/communities/CommunityUserListPanel.qml @@ -1,11 +1,5 @@ import QtQuick 2.13 -import Qt.labs.platform 1.1 import QtQuick.Controls 2.13 -import QtQuick.Window 2.13 -import QtQuick.Layouts 1.13 -import QtQml.Models 2.13 -import QtGraphicalEffects 1.13 -import QtQuick.Dialogs 1.3 import shared 1.0 import shared.panels 1.0 import shared.status 1.0 @@ -20,14 +14,12 @@ import StatusQ.Core.Theme 0.1 Item { id: root anchors.fill: parent - property var userList - property var currentTime - property var contactsList - property string profilePubKey + + // Important: + // Each chat/community has its own ChatContentModule and each ChatContentModule has a single usersModule + // usersModule on the backend contains everything needed for this component + property var usersModule property var messageContextMenu - property var community - property string currentUserName: "" - property bool currentUserOnline: true StatusBaseText { id: titleText @@ -59,46 +51,32 @@ Item { bottomMargin: Style.current.bigPadding } boundsBehavior: Flickable.StopAtBounds - model: userListDelegate - section.property: "online" - section.delegate: (root.width > 58) ? sectionDelegateComponent : null - Component { - id: sectionDelegateComponent - Item { - width: parent.width - height: 24 - StatusBaseText { - anchors.fill: parent - anchors.leftMargin: Style.current.padding - verticalAlignment: Text.AlignVCenter - font.pixelSize: Style.current.additionalTextSize - color: Theme.palette.baseColor1 - text: section === 'true' ? qsTr("Online") : qsTr("Offline") - } - } - } - } - - DelegateModelGeneralized { - id: userListDelegate - lessThan: [ - function(left, right) { - return left.sortKey.localeCompare(right.sortKey) < 0 - } - ] - model: community.members + model: usersModule.model delegate: UserDelegate { - name: model.userName - publicKey: model.pubKey - profilePubKey: root.profilePubKey - identicon: model.identicon - contactsList: root.contactsList - lastSeen: model.lastSeen - statusType: model.statusType - currentTime: root.currentTime - isOnline: (model.userName === root.currentUserName) ? - root.currentUserOnline : model.online + publicKey: model.id + name: model.name + identicon: model.icon + isIdenticon: model.isIdenticon + userStatus: model.onlineStatus messageContextMenu: root.messageContextMenu } + section.property: "onlineStatus" + section.delegate: (root.width > 58) ? sectionDelegateComponent : null + } + + Component { + id: sectionDelegateComponent + Item { + width: parent.width + height: 24 + StyledText { + anchors.fill: parent + anchors.leftMargin: Style.current.padding + verticalAlignment: Text.AlignVCenter + font.pixelSize: Style.current.additionalTextSize + color: Theme.palette.baseColor1 + text: model.onlineStatus === Constants.userStatus.online? qsTr("Online") : qsTr("Offline") + } + } } } diff --git a/ui/app/AppLayouts/Chat/views/ChatColumnView.qml b/ui/app/AppLayouts/Chat/views/ChatColumnView.qml index 71683c90d4..233f4f1a6c 100644 --- a/ui/app/AppLayouts/Chat/views/ChatColumnView.qml +++ b/ui/app/AppLayouts/Chat/views/ChatColumnView.qml @@ -28,6 +28,18 @@ Item { // Important: we have parent module in this context only cause qml components // don't follow struct from we have on the backend. property var parentModule + // Since qml component doesn't follow encaptulation from the backend side, we're introducing + // a method which will return appropriate chat content module for selected chat/channel + function currentChatContentModule(){ + // When we decide to have the same struct as it's on the backend we will remove this function. + // So far this is a way to deal with refactord backend from the current qml structure. + if(parentModule.activeItem.isSubItemActive) + parentModule.prepareChatContentModuleForChatId(chatCommunitySectionModule.activeItem.activeSubItem.id) + else + parentModule.prepareChatContentModuleForChatId(chatCommunitySectionModule.activeItem.id) + + return parentModule.getChatContentModule() + } property var rootStore property alias pinnedMessagesPopupComponent: pinnedMessagesPopupComponent @@ -210,8 +222,11 @@ Item { } } - membersButton.visible: (localAccountSensitiveSettings.showOnlineUsers || root.rootStore.chatsModelInst.communities.activeCommunity.active) - && root.rootStore.chatsModelInst.channelView.activeChannel.chatType !== Constants.chatTypeOneToOne + membersButton.visible: { + // Check if user list is available as an option for particular chat content module. + let usersListAvailable = currentChatContentModule().isUsersListAvailable() + return localAccountSensitiveSettings.showOnlineUsers && usersListAvailable + } membersButton.highlighted: localAccountSensitiveSettings.expandUsersList notificationButton.visible: localAccountSensitiveSettings.isActivityCenterEnabled notificationButton.tooltip.offset: localAccountSensitiveSettings.expandUsersList ? 0 : 14 diff --git a/ui/app/AppMain.qml b/ui/app/AppMain.qml index 6402d4c753..595ecddee6 100644 --- a/ui/app/AppMain.qml +++ b/ui/app/AppMain.qml @@ -320,19 +320,18 @@ Item { badge.implicitHeight: 15 badge.implicitWidth: 15 badge.border.color: hovered ? Theme.palette.statusBadge.hoverBorderColor : Theme.palette.statusAppNavBar.backgroundColor - badge.color: { - return userProfile.sendUserStatus ? Style.current.green : Style.current.midGrey - /* - // Use this code once support for custom user status is added - switch(userProfile.currentUserStatus){ - case Constants.statusType_Online: - return Style.current.green; - case Constants.statusType_DoNotDisturb: - return Style.current.red; - default: - return Style.current.midGrey; - }*/ - } + /* + //This is still not in use. Read a comment for `currentUserStatus` in UserProfile on the nim side. + // Use this code once support for custom user status is added + switch(userProfile.currentUserStatus){ + case Constants.userStatus.online: + return Style.current.green; + case Constants.userStatus.doNotDisturb: + return Style.current.red; + default: + return Style.current.midGrey; + }*/ + badge.color: appMain.rootStore.userProfileInst.userStatus ? Style.current.green : Style.current.midGrey badge.border.width: 3 onClicked: { userStatusContextMenu.opened ? diff --git a/ui/imports/utils/Constants.qml b/ui/imports/utils/Constants.qml index 226ba5061a..19ab934f34 100644 --- a/ui/imports/utils/Constants.qml +++ b/ui/imports/utils/Constants.qml @@ -40,6 +40,13 @@ QtObject { readonly property int newMessage: 6 } + readonly property QtObject userStatus: QtObject{ + readonly property int offline: 0 + readonly property int online: 1 + readonly property int doNotDisturb: 2 + readonly property int idle: 3 + } + readonly property int communityImported: 0 readonly property int communityImportingInProgress: 1 readonly property int communityImportingError: 2 @@ -113,10 +120,6 @@ QtObject { readonly property string linux: "linux" readonly property string mac: "mac" - readonly property int statusType_Unknown: 0 - readonly property int statusType_Online: 1 - readonly property int statusType_DoNotDisturb: 2 - // Transaction states readonly property int addressRequested: 1 readonly property int declined: 2