From e1309536343d8e4c6716fe3ae19d5ea4bfc39207 Mon Sep 17 00:00:00 2001 From: Patryk Osmaczko Date: Wed, 9 Mar 2022 11:27:32 +0100 Subject: [PATCH] feat(@desktop): use emoji hash and identicon ring Closes: #4782 --- src/app/boot/app_controller.nim | 14 +- .../main/chat_section/chat_content/module.nim | 5 +- .../chat_content/users/controller.nim | 12 +- .../users/controller_interface.nim | 7 + .../chat_content/users/module.nim | 17 +- src/app/modules/main/chat_section/module.nim | 40 ++-- .../module_access_interface.nim | 4 +- .../modules/main/communities/controller.nim | 14 +- .../main/communities/controller_interface.nim | 7 + src/app/modules/main/communities/module.nim | 9 +- src/app/modules/main/controller.nim | 31 ++- src/app/modules/main/controller_interface.nim | 9 +- src/app/modules/main/module.nim | 36 ++- .../module_access_interface.nim | 4 +- .../module_controller_delegate_interface.nim | 7 +- .../module_view_delegate_interface.nim | 6 + src/app/modules/main/view.nim | 10 +- .../modules/shared_models/color_hash_item.nim | 23 ++ .../shared_models/color_hash_model.nim | 53 +++++ .../modules/shared_models/emojis_model.nim | 42 ++++ src/app/modules/shared_models/user_item.nim | 29 ++- src/app/modules/shared_models/user_model.nim | 8 + .../service/visual_identity/dto.nim | 17 ++ .../service/visual_identity/service.nim | 42 ++++ .../visual_identity/service_interface.nim | 15 ++ src/backend/visual_identity.nim | 15 ++ ui/StatusQ | 2 +- ui/app/AppLayouts/Chat/ChatLayout.qml | 1 + .../AppLayouts/Chat/controls/UserDelegate.qml | 31 ++- .../Chat/panels/ContactListPanel.qml | 12 +- .../AppLayouts/Chat/panels/UserListPanel.qml | 3 + ui/app/AppLayouts/Chat/stores/RootStore.qml | 8 + .../AppLayouts/Profile/views/EnsListView.qml | 5 +- .../shared/controls/chat/ProfileHeader.qml | 115 ++++++++++ ui/imports/shared/controls/chat/UserImage.qml | 70 +++--- ui/imports/shared/controls/chat/qmldir | 1 + .../shared/panels/chat/ChatReplyPanel.qml | 17 +- ui/imports/shared/popups/ProfilePopup.qml | 58 ++++- .../shared/popups/UserStatusContextMenu.qml | 92 +++----- .../shared/views/chat/CompactMessageView.qml | 14 +- .../views/chat/MessageContextMenuView.qml | 54 +---- ui/imports/shared/views/chat/MessageView.qml | 41 +--- .../shared/views/chat/StatusUpdateView.qml | 211 ------------------ ui/imports/shared/views/chat/qmldir | 1 - ui/imports/utils/Utils.qml | 14 ++ 45 files changed, 736 insertions(+), 490 deletions(-) create mode 100644 src/app/modules/shared_models/color_hash_item.nim create mode 100644 src/app/modules/shared_models/color_hash_model.nim create mode 100644 src/app/modules/shared_models/emojis_model.nim create mode 100644 src/app_service/service/visual_identity/dto.nim create mode 100644 src/app_service/service/visual_identity/service.nim create mode 100644 src/app_service/service/visual_identity/service_interface.nim create mode 100644 src/backend/visual_identity.nim create mode 100644 ui/imports/shared/controls/chat/ProfileHeader.qml delete mode 100644 ui/imports/shared/views/chat/StatusUpdateView.qml diff --git a/src/app/boot/app_controller.nim b/src/app/boot/app_controller.nim index 8cf895189e..8553e9ffcd 100644 --- a/src/app/boot/app_controller.nim +++ b/src/app/boot/app_controller.nim @@ -30,6 +30,7 @@ import ../../app_service/service/devices/service as devices_service import ../../app_service/service/mailservers/service as mailservers_service import ../../app_service/service/gif/service as gif_service import ../../app_service/service/ens/service as ens_service +import ../../app_service/service/visual_identity/service as visual_identity_service import ../modules/startup/module as startup_module import ../modules/main/module as main_module @@ -83,6 +84,7 @@ type nodeService: node_service.Service gifService: gif_service.Service ensService: ens_service.Service + visualIdentityService: visual_identity_service.Service # Modules startupModule: startup_module.AccessInterface @@ -146,7 +148,7 @@ proc newAppController*(statusFoundation: StatusFoundation): AppController = ) result.messageService = message_service.newService(statusFoundation.events, statusFoundation.threadpool, result.contactsService, result.ethService, result.tokenService, result.walletAccountService) - result.transactionService = transaction_service.newService(statusFoundation.events, statusFoundation.threadpool, + result.transactionService = transaction_service.newService(statusFoundation.events, statusFoundation.threadpool, result.walletAccountService, result.ethService, result.networkService, result.settingsService) result.bookmarkService = bookmark_service.newService() result.profileService = profile_service.newService() @@ -175,10 +177,11 @@ proc newAppController*(statusFoundation: StatusFoundation): AppController = result.nodeService = node_service.newService(statusFoundation.events, statusFoundation.threadpool, result.settingsService) result.gifService = gif_service.newService(result.settingsService) - result.ensService = ens_service.newService(statusFoundation.events, statusFoundation.threadpool, - result.settingsService, result.walletAccountService, result.transactionService, result.ethService, + result.ensService = ens_service.newService(statusFoundation.events, statusFoundation.threadpool, + result.settingsService, result.walletAccountService, result.transactionService, result.ethService, result.networkService, result.tokenService) result.providerService = provider_service.newService(result.ensService) + result.visualIdentityService = visual_identity_service.newService() # Modules result.startupModule = startup_module.newModule[AppController]( @@ -219,6 +222,7 @@ proc newAppController*(statusFoundation: StatusFoundation): AppController = result.gifService, result.ensService, result.networkService, + result.visualIdentityService ) # Do connections @@ -267,6 +271,7 @@ proc delete*(self: AppController) = self.generalService.delete self.ensService.delete self.gifService.delete + self.visualIdentityService.delete proc startupDidLoad*(self: AppController) = singletonInstance.engine.setRootContextProperty("localAppSettings", self.localAppSettingsVariant) @@ -336,7 +341,8 @@ proc load(self: AppController) = self.communityService, self.messageService, self.gifService, - self.mailserversService + self.mailserversService, + self.visualIdentityService, ) proc userLoggedIn*(self: AppController) = 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 62611015ea..8f9f0023fc 100644 --- a/src/app/modules/main/chat_section/chat_content/module.nim +++ b/src/app/modules/main/chat_section/chat_content/module.nim @@ -20,6 +20,7 @@ import ../../../../../app_service/service/community/service as community_service import ../../../../../app_service/service/gif/service as gif_service import ../../../../../app_service/service/message/service as message_service import ../../../../../app_service/service/mailservers/service as mailservers_service +import ../../../../../app_service/service/visual_identity/service as visual_identity_service export io_interface @@ -41,7 +42,7 @@ proc newModule*(delegate: delegate_interface.AccessInterface, events: EventEmitt belongsToCommunity: bool, isUsersListAvailable: bool, settingsService: settings_service.ServiceInterface, contactService: contact_service.Service, chatService: chat_service.Service, communityService: community_service.Service, messageService: message_service.Service, gifService: gif_service.Service, - mailserversService: mailservers_service.Service): + mailserversService: mailservers_service.Service, visualIdentityService: visual_identity_service.Service): Module = result = Module() result.delegate = delegate @@ -56,7 +57,7 @@ proc newModule*(delegate: delegate_interface.AccessInterface, events: EventEmitt contactService, communityService, chatService, messageService, mailserversService) result.usersModule = users_module.newModule( result, events, sectionId, chatId, belongsToCommunity, isUsersListAvailable, - contactService, chat_service, communityService, messageService, + contactService, chat_service, communityService, messageService, visualIdentityService ) method delete*(self: Module) = 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 b7f0998f4f..db6afdc917 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 @@ -6,7 +6,7 @@ import ../../../../../../app_service/service/contacts/service as contact_service import ../../../../../../app_service/service/community/service as community_service import ../../../../../../app_service/service/message/service as message_service import ../../../../../../app_service/service/chat/service as chat_service - +import ../../../../../../app_service/service/visual_identity/service as visual_identity_service import ../../../../../core/eventemitter @@ -24,12 +24,13 @@ type chatService: chat_service.Service communityService: community_service.Service messageService: message_service.Service + visualIdentityService: visual_identity_service.Service proc newController*( delegate: io_interface.AccessInterface, events: EventEmitter, sectionId: string, chatId: string, belongsToCommunity: bool, isUsersListAvailable: bool, contactService: contact_service.Service, chatService: chat_service.Service, communityService: community_service.Service, - messageService: message_service.Service + messageService: message_service.Service, visualIdentityService: visual_identity_service.Service ): Controller = result = Controller() result.delegate = delegate @@ -43,6 +44,7 @@ proc newController*( result.communityService = communityService result.messageService = messageService result.chatService = chatService + result.visualIdentityService = visualIdentityService method delete*(self: Controller) = discard @@ -153,3 +155,9 @@ method getContactDetails*(self: Controller, contactId: string): ContactDetails = method getStatusForContact*(self: Controller, contactId: string): StatusUpdateDto = return self.contactService.getStatusForContactWithId(contactId) + +method getEmojiHash*(self: Controller, pubkey: string): EmojiHashDto = + return self.visual_identity_service.emojiHashOf(pubkey) + +method getColorHash*(self: Controller, pubkey: string): ColorHashDto = + return self.visual_identity_service.colorHashOf(pubkey) 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 e3eb656d46..0bc7593537 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,5 +1,6 @@ import ../../../../../../app_service/service/contacts/service as contacts_service import ../../../../../../app_service/service/chat/service as chat_service +import ../../../../../../app_service/service/visual_identity/service type AccessInterface* {.pure inheritable.} = ref object of RootObj @@ -32,3 +33,9 @@ method getChat*(self: AccessInterface): ChatDto {.base.} = method getChatMemberInfo*(self: AccessInterface, id: string): (bool, bool) = raise newException(ValueError, "No implementation available") + +method getEmojiHash*(self: AccessInterface, pubkey: string): EmojiHashDto {.base.} = + raise newException(ValueError, "No implementation available") + +method getColorHash*(self: AccessInterface, pubkey: string): ColorHashDto {.base.} = + raise newException(ValueError, "No implementation available") 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 5db11ea79d..eea8a99b9d 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 @@ -9,6 +9,7 @@ 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 +import ../../../../../../app_service/service/visual_identity/service as visual_identity_service export io_interface @@ -24,7 +25,7 @@ proc newModule*( delegate: delegate_interface.AccessInterface, events: EventEmitter, sectionId: string, chatId: string, belongsToCommunity: bool, isUsersListAvailable: bool, contactService: contact_service.Service, chatService: chat_service.Service, communityService: community_service.Service, - messageService: message_service.Service, + messageService: message_service.Service, visualIdentityService: visual_identity_service.Service ): Module = result = Module() result.delegate = delegate @@ -32,7 +33,7 @@ proc newModule*( result.viewVariant = newQVariant(result.view) result.controller = controller.newController( result, events, sectionId, chatId, belongsToCommunity, isUsersListAvailable, - contactService, chatService, communityService, messageService, + contactService, chatService, communityService, messageService, visualIdentityService ) result.moduleLoaded = false @@ -62,6 +63,8 @@ method viewDidLoad*(self: Module) = singletonInstance.userProfile.getIcon(), singletonInstance.userProfile.getIdenticon(), singletonInstance.userProfile.getIsIdenticon(), + self.controller.getEmojiHash(singletonInstance.userProfile.getPubKey()), + self.controller.getColorHash(singletonInstance.userProfile.getPubKey()), isAdded = true, admin, joined, @@ -87,8 +90,10 @@ method viewDidLoad*(self: Module) = contactDetails.icon, contactDetails.details.identicon, contactDetails.isidenticon, + self.controller.getEmojiHash(publicKey), + self.controller.getColorHash(publicKey), contactDetails.details.added, - admin, + admin, joined )) @@ -120,6 +125,8 @@ method newMessagesLoaded*(self: Module, messages: seq[MessageDto]) = contactDetails.icon, contactDetails.details.identicon, contactDetails.isidenticon, + self.controller.getEmojiHash(m.`from`), + self.controller.getColorHash(m.`from`), contactDetails.details.added, )) @@ -173,8 +180,10 @@ method onChatMembersAdded*(self: Module, ids: seq[string]) = contactDetails.icon, contactDetails.details.identicon, contactDetails.isidenticon, + self.controller.getEmojiHash(id), + self.controller.getColorHash(id), contactDetails.details.added, - admin, + admin, joined )) diff --git a/src/app/modules/main/chat_section/module.nim b/src/app/modules/main/chat_section/module.nim index 52bbdf1e84..1bb5472432 100644 --- a/src/app/modules/main/chat_section/module.nim +++ b/src/app/modules/main/chat_section/module.nim @@ -19,6 +19,7 @@ import ../../../../app_service/service/community/service as community_service import ../../../../app_service/service/message/service as message_service import ../../../../app_service/service/mailservers/service as mailservers_service import ../../../../app_service/service/gif/service as gif_service +import ../../../../app_service/service/visual_identity/service as visual_identity_service export io_interface @@ -85,10 +86,11 @@ proc addSubmodule(self: Module, chatId: string, belongToCommunity: bool, isUsers communityService: community_service.Service, messageService: message_service.Service, gifService: gif_service.Service, - mailserversService: mailservers_service.Service) = + mailserversService: mailservers_service.Service, + visualIdentityService: visual_identity_service.Service) = self.chatContentModules[chatId] = chat_content_module.newModule(self, events, self.controller.getMySectionId(), chatId, belongToCommunity, isUsersListAvailable, settingsService, contactService, chatService, communityService, - messageService, gifService, mailserversService) + messageService, gifService, mailserversService, visualIdentityService) proc removeSubmodule(self: Module, chatId: string) = if(not self.chatContentModules.contains(chatId)): @@ -102,7 +104,8 @@ proc buildChatUI(self: Module, events: EventEmitter, communityService: community_service.Service, messageService: message_service.Service, gifService: gif_service.Service, - mailserversService: mailservers_service.Service) = + mailserversService: mailservers_service.Service, + visualIdentityService: visual_identity_service.Service) = let types = @[ChatType.OneToOne, ChatType.Public, ChatType.PrivateGroupChat] let chats = self.controller.getChatDetailsForChatTypes(types) @@ -130,7 +133,7 @@ proc buildChatUI(self: Module, events: EventEmitter, active=false, c.position, c.categoryId) self.view.chatsModel().appendItem(item) self.addSubmodule(c.id, false, isUsersListAvailable, events, settingsService, contactService, chatService, - communityService, messageService, gifService, mailserversService) + communityService, messageService, gifService, mailserversService, visualIdentityService) # make the first Public chat active when load the app if(selectedItemId.len == 0 and c.chatType == ChatType.Public): @@ -145,7 +148,8 @@ proc buildCommunityUI(self: Module, events: EventEmitter, communityService: community_service.Service, messageService: message_service.Service, gifService: gif_service.Service, - mailserversService: mailservers_service.Service) = + mailserversService: mailservers_service.Service, + visualIdentityService: visual_identity_service.Service) = var selectedItemId = "" var selectedSubItemId = "" let communities = self.controller.getJoinedCommunities() @@ -166,7 +170,7 @@ proc buildCommunityUI(self: Module, events: EventEmitter, notificationsCount, chatDto.muted, blocked=false, active = false, c.position, c.categoryId) self.view.chatsModel().appendItem(channelItem) self.addSubmodule(chatDto.id, true, true, events, settingsService, contactService, chatService, communityService, - messageService, gifService, mailserversService) + messageService, gifService, mailserversService, visualIdentityService) # make the first channel which doesn't belong to any category active when load the app if(selectedItemId.len == 0): @@ -197,7 +201,7 @@ proc buildCommunityUI(self: Module, events: EventEmitter, active=false, c.position) categoryChannels.add(channelItem) self.addSubmodule(chatDto.id, true, true, events, settingsService, contactService, chatService, communityService, - messageService, gifService, mailserversService) + messageService, gifService, mailserversService, visualIdentityService) # in case there is no channels beyond categories, # make the first channel of the first category active when load the app @@ -262,14 +266,15 @@ method load*(self: Module, events: EventEmitter, communityService: community_service.Service, messageService: message_service.Service, gifService: gif_service.Service, - mailserversService: mailservers_service.Service) = + mailserversService: mailservers_service.Service, + visualIdentityService: visual_identity_service.Service) = self.controller.init() self.view.load() if(self.controller.isCommunity()): - self.buildCommunityUI(events, settingsService, contactService, chatService, communityService, messageService, gifService, mailserversService) + self.buildCommunityUI(events, settingsService, contactService, chatService, communityService, messageService, gifService, mailserversService, visualIdentityService) else: - self.buildChatUI(events, settingsService, contactService, chatService, communityService, messageService, gifService, mailserversService) + self.buildChatUI(events, settingsService, contactService, chatService, communityService, messageService, gifService, mailserversService, visualIdentityService) self.initContactRequestsModel() # we do this only in case of chat section (not in case of communities) for cModule in self.chatContentModules.values: @@ -400,6 +405,7 @@ method addNewChat*( messageService: message_service.Service, gifService: gif_service.Service, mailserversService: mailservers_service.Service, + visualIdentityService: visual_identity_service.Service, setChatAsActive: bool = true) = let hasNotification = chatDto.unviewedMessagesCount > 0 or chatDto.unviewedMentionsCount > 0 let notificationsCount = chatDto.unviewedMentionsCount @@ -421,7 +427,7 @@ method addNewChat*( chatDto.description, chatDto.chatType.int, amIChatAdmin, hasNotification, notificationsCount, chatDto.muted, blocked=false, active=false, position = 0, chatDto.categoryId, chatDto.highlight) self.addSubmodule(chatDto.id, belongsToCommunity, isUsersListAvailable, events, settingsService, contactService, chatService, - communityService, messageService, gifService, mailserversService) + communityService, messageService, gifService, mailserversService, visualIdentityService) self.chatContentModules[chatDto.id].load() self.view.chatsModel().appendItem(item) if setChatAsActive: @@ -437,7 +443,7 @@ method addNewChat*( amIChatAdmin, hasNotification, notificationsCount, chatDto.muted, blocked=false, active=false, chatDto.position) self.addSubmodule(chatDto.id, belongsToCommunity, isUsersListAvailable, events, settingsService, contactService, chatService, - communityService, messageService, gifService, mailserversService) + communityService, messageService, gifService, mailserversService, visualIdentityService) self.chatContentModules[chatDto.id].load() categoryItem.appendSubItem(channelItem) if setChatAsActive: @@ -638,8 +644,8 @@ method onContactDetailsUpdated*(self: Module, publicKey: string) = let item = self.createItemFromPublicKey(publicKey) self.view.contactRequestsModel().addItem(item) self.updateParentBadgeNotifications() - singletonInstance.globalEvents.showNewContactRequestNotification("New Contact Request", - fmt "{contactDetails.displayName} added you as contact", conf.CHAT_SECTION_ID) + singletonInstance.globalEvents.showNewContactRequestNotification("New Contact Request", + fmt "{contactDetails.displayName} added you as contact", conf.CHAT_SECTION_ID) let chatName = contactDetails.displayName let chatImage = contactDetails.icon @@ -659,10 +665,10 @@ method onNewMessagesReceived*(self: Module, chatId: string, unviewedMessagesCoun let renderedMessageText = self.controller.getRenderedText(m.parsedText) let plainText = singletonInstance.utils.plainText(renderedMessageText) if(m.isUserWithPkMentioned(myPK)): - singletonInstance.globalEvents.showMentionMessageNotification(contactDetails.displayName, plainText, + singletonInstance.globalEvents.showMentionMessageNotification(contactDetails.displayName, plainText, self.controller.getMySectionId(), chatId, m.id) else: - singletonInstance.globalEvents.showNormalMessageNotification(contactDetails.displayName, plainText, + singletonInstance.globalEvents.showNormalMessageNotification(contactDetails.displayName, plainText, self.controller.getMySectionId(), chatId, m.id) method addGroupMembers*(self: Module, communityID: string, chatId: string, pubKeys: string) = @@ -757,7 +763,7 @@ method reorderCommunityCategories*(self: Module, categoryId: string, position: i method reorderCommunityChat*(self: Module, categoryId: string, chatId: string, position: int): string = self.controller.reorderCommunityChat(categoryId, chatId, position) - + method setLoadingHistoryMessagesInProgress*(self: Module, isLoading: bool) = self.view.setLoadingHistoryMessagesInProgress(isLoading) 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 4912fb68d1..d62eaf37e9 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 @@ -7,6 +7,7 @@ import ../../../../../app_service/service/community/service as community_service import ../../../../../app_service/service/message/service as message_service import ../../../../../app_service/service/gif/service as gif_service import ../../../../../app_service/service/mailservers/service as mailservers_service +import ../../../../../app_service/service/visual_identity/service as visual_identity_service import ../model as chats_model @@ -22,7 +23,8 @@ method load*(self: AccessInterface, events: EventEmitter, communityService: community_service.Service, messageService: message_service.Service, gifService: gif_service.Service, - mailserversService: mailservers_service.Service) {.base.} = + mailserversService: mailservers_service.Service, + visualIdentityService: visual_identity_service.Service) {.base.} = raise newException(ValueError, "No implementation available") method isLoaded*(self: AccessInterface): bool {.base.} = diff --git a/src/app/modules/main/communities/controller.nim b/src/app/modules/main/communities/controller.nim index f258fd7732..e19bf46ce5 100644 --- a/src/app/modules/main/communities/controller.nim +++ b/src/app/modules/main/communities/controller.nim @@ -6,6 +6,7 @@ import ../../../core/signals/types import ../../../core/eventemitter import ../../../../app_service/service/community/service as community_service import ../../../../app_service/service/contacts/service as contacts_service +import ../../../../app_service/service/visual_identity/service as visual_identity_service export controller_interface @@ -15,18 +16,21 @@ type events: EventEmitter communityService: community_service.Service contactsService: contacts_service.Service + visualIdentityService: visual_identity_service.Service proc newController*( delegate: io_interface.AccessInterface, events: EventEmitter, communityService: community_service.Service, - contactsService: contacts_service.Service + contactsService: contacts_service.Service, + visualIdentityService: visual_identity_service.Service ): Controller = result = Controller() result.delegate = delegate result.events = events result.communityService = communityService result.contactsService = contactsService + result.visualIdentityService = visualIdentityService method delete*(self: Controller) = discard @@ -133,4 +137,10 @@ method userCanJoin*(self: Controller, communityId: string): bool = return self.communityService.userCanJoin(communityId) method isCommunityRequestPending*(self: Controller, communityId: string): bool = - return self.communityService.isCommunityRequestPending(communityId) \ No newline at end of file + return self.communityService.isCommunityRequestPending(communityId) + +method getEmojiHash*(self: Controller, pubkey: string): EmojiHashDto = + return self.visualIdentityService.emojiHashOf(pubkey) + +method getColorHash*(self: Controller, pubkey: string): ColorHashDto = + return self.visualIdentityService.colorHashOf(pubkey) diff --git a/src/app/modules/main/communities/controller_interface.nim b/src/app/modules/main/communities/controller_interface.nim index be7d6ce89e..a7c3cc845c 100644 --- a/src/app/modules/main/communities/controller_interface.nim +++ b/src/app/modules/main/communities/controller_interface.nim @@ -1,5 +1,6 @@ import ../../../../app_service/service/community/service as community_service import ../../../../app_service/service/contacts/service as contacts_service +import ../../../../app_service/service/visual_identity/service as visual_identity_service type AccessInterface* {.pure inheritable.} = ref object of RootObj @@ -63,6 +64,12 @@ method getContactNameAndImage*(self: AccessInterface, contactId: string): method getContactDetails*(self: AccessInterface, contactId: string): ContactDetails {.base.} = raise newException(ValueError, "No implementation available") +method getEmojiHash*(self: AccessInterface, pubkey: string): EmojiHashDto {.base.} = + raise newException(ValueError, "No implementation available") + +method getColorHash*(self: AccessInterface, pubkey: string): ColorHashDto {.base.} = + raise newException(ValueError, "No implementation available") + type ## Abstract class (concept) which must be implemented by object/s used in this ## module. diff --git a/src/app/modules/main/communities/module.nim b/src/app/modules/main/communities/module.nim index 4b6914e592..42458fbe3b 100644 --- a/src/app/modules/main/communities/module.nim +++ b/src/app/modules/main/communities/module.nim @@ -9,6 +9,7 @@ import ../../../global/global_singleton import ../../../core/eventemitter import ../../../../app_service/service/community/service as community_service import ../../../../app_service/service/contacts/service as contacts_service +import ../../../../app_service/service/visual_identity/service as visual_identity_service export io_interface @@ -33,7 +34,8 @@ proc newModule*( delegate: delegate_interface.AccessInterface, events: EventEmitter, communityService: community_service.Service, - contactsService: contacts_service.Service + contactsService: contacts_service.Service, + visualIdentityService: visual_identity_service.Service ): Module = result = Module() result.delegate = delegate @@ -43,7 +45,8 @@ proc newModule*( result, events, communityService, - contactsService + contactsService, + visualIdentityService ) result.moduleLoaded = false @@ -100,6 +103,8 @@ method getCommunityItem(self: Module, c: CommunityDto): SectionItem = contactDetails.icon, contactDetails.details.identicon, contactDetails.isidenticon, + self.controller.getEmojiHash(member.id), + self.controller.getColorHash(member.id), contactDetails.details.added, )) ) diff --git a/src/app/modules/main/controller.nim b/src/app/modules/main/controller.nim index 4be5f18233..f3af7f3312 100644 --- a/src/app/modules/main/controller.nim +++ b/src/app/modules/main/controller.nim @@ -16,6 +16,7 @@ import ../../../app_service/service/gif/service as gif_service import ../../../app_service/service/mailservers/service as mailservers_service import ../../../app_service/service/privacy/service as privacy_service import ../../../app_service/service/node/service as node_service +import ../../../app_service/service/visual_identity/service as visual_identity_service export controller_interface @@ -37,6 +38,7 @@ type privacyService: privacy_service.Service mailserversService: mailservers_service.Service nodeService: node_service.Service + visualIdentityService: visual_identity_service.Service activeSectionId: string proc newController*(delegate: io_interface.AccessInterface, @@ -51,7 +53,8 @@ proc newController*(delegate: io_interface.AccessInterface, gifService: gif_service.Service, privacyService: privacy_service.Service, mailserversService: mailservers_service.Service, - nodeService: node_service.Service + nodeService: node_service.Service, + visualIdentityService: visual_identity_service.Service ): Controller = result = Controller() @@ -67,6 +70,7 @@ proc newController*(delegate: io_interface.AccessInterface, result.gifService = gifService result.privacyService = privacyService result.nodeService = nodeService + result.visualIdentityService = visualIdentityService method delete*(self: Controller) = discard @@ -103,7 +107,8 @@ method init*(self: Controller) = self.communityService, self.messageService, self.gifService, - self.mailserversService + self.mailserversService, + self.visualIdentityService ) self.events.on(TOGGLE_SECTION) do(e:Args): @@ -121,7 +126,8 @@ method init*(self: Controller) = self.communityService, self.messageService, self.gifService, - self.mailserversService + self.mailserversService, + self.visualIdentityService ) self.events.on(SIGNAL_COMMUNITY_IMPORTED) do(e:Args): @@ -137,7 +143,8 @@ method init*(self: Controller) = self.communityService, self.messageService, self.gifService, - self.mailserversService + self.mailserversService, + self.visualIdentityService ) self.events.on(SIGNAL_COMMUNITY_LEFT) do(e:Args): @@ -172,20 +179,20 @@ method init*(self: Controller) = var args = ActiveSectionChatArgs(e) let sectionType = if args.sectionId == conf.CHAT_SECTION_ID: SectionType.Chat else: SectionType.Community self.setActiveSection(args.sectionId, sectionType) - + self.events.on(SIGNAL_OS_NOTIFICATION_CLICKED) do(e: Args): var args = ClickedNotificationArgs(e) self.delegate.osNotificationClicked(args.details) self.events.on(SIGNAL_NEW_REQUEST_TO_JOIN_COMMUNITY) do(e: Args): var args = CommunityRequestArgs(e) - self.delegate.newCommunityMembershipRequestReceived(args.communityRequest) + self.delegate.newCommunityMembershipRequestReceived(args.communityRequest) self.events.on(SIGNAL_NETWORK_CONNECTED) do(e: Args): - self.delegate.onNetworkConnected() + self.delegate.onNetworkConnected() self.events.on(SIGNAL_NETWORK_DISCONNECTED) do(e: Args): - self.delegate.onNetworkDisconnected() + self.delegate.onNetworkDisconnected() method isConnected*(self: Controller): bool = return self.nodeService.isConnected() @@ -285,4 +292,10 @@ method switchTo*(self: Controller, sectionId, chatId, messageId: string) = self.events.emit(SIGNAL_MAKE_SECTION_CHAT_ACTIVE, data) method getCommunityById*(self: Controller, communityId: string): CommunityDto = - return self.communityService.getCommunityById(communityId) \ No newline at end of file + return self.communityService.getCommunityById(communityId) + +method getEmojiHash*(self: Controller, pubkey: string): EmojiHashDto = + return self.visualIdentityService.emojiHashOf(pubkey) + +method getColorHash*(self: Controller, pubkey: string): ColorHashDto = + return self.visualIdentityService.colorHashOf(pubkey) diff --git a/src/app/modules/main/controller_interface.nim b/src/app/modules/main/controller_interface.nim index e664be134e..6d4391d8b0 100644 --- a/src/app/modules/main/controller_interface.nim +++ b/src/app/modules/main/controller_interface.nim @@ -2,6 +2,7 @@ import ../shared_models/section_item import ../../../app_service/service/contacts/dto/contact_details as contact_details import ../../../app_service/service/contacts/dto/contacts as contacts_dto import ../../../app_service/service/community/service as community_service +import ../../../app_service/service/visual_identity/service as visual_identity_service type AccessInterface* {.pure inheritable.} = ref object of RootObj @@ -64,4 +65,10 @@ method getCommunityById*(self: AccessInterface, communityId: string): CommunityD raise newException(ValueError, "No implementation available") method isConnected*(self: AccessInterface): bool {.base.} = - raise newException(ValueError, "No implementation available") \ No newline at end of file + raise newException(ValueError, "No implementation available") + +method getEmojiHash*(self: AccessInterface, pubkey: string): EmojiHashDto {.base.} = + raise newException(ValueError, "No implementation available") + +method getColorHash*(self: AccessInterface, pubkey: string): ColorHashDto {.base.} = + raise newException(ValueError, "No implementation available") diff --git a/src/app/modules/main/module.nim b/src/app/modules/main/module.nim index be3a29451a..decb277108 100644 --- a/src/app/modules/main/module.nim +++ b/src/app/modules/main/module.nim @@ -1,4 +1,4 @@ -import NimQml, tables, json, sugar, sequtils, strformat +import NimQml, tables, json, sugar, sequtils, strformat, marshal import io_interface, view, controller, chat_search_item, chat_search_model import ./communities/models/[pending_request_item, pending_request_model] @@ -49,6 +49,7 @@ import ../../../app_service/service/mailservers/service as mailservers_service import ../../../app_service/service/gif/service as gif_service import ../../../app_service/service/ens/service as ens_service import ../../../app_service/service/network/service as network_service +import ../../../app_service/service/visual_identity/service as visual_identity_service import ../../core/notifications/details import ../../core/eventemitter @@ -108,6 +109,7 @@ proc newModule*[T]( gifService: gif_service.Service, ensService: ens_service.Service, networkService: network_service.Service, + visualIdentityService: visual_identity_service.Service ): Module[T] = result = Module[T]() result.delegate = delegate @@ -126,7 +128,8 @@ proc newModule*[T]( gifService, privacyService, mailserversService, - nodeService + nodeService, + visualIdentityService ) result.moduleLoaded = false @@ -149,7 +152,7 @@ proc newModule*[T]( result.stickersModule = stickers_module.newModule(result, events, stickersService, settingsService, walletAccountService) result.activityCenterModule = activity_center_module.newModule(result, events, activityCenterService, contactsService, messageService, chatService) - result.communitiesModule = communities_module.newModule(result, events, communityService, contactsService) + result.communitiesModule = communities_module.newModule(result, events, communityService, contactsService, visualIdentityService) result.appSearchModule = app_search_module.newModule(result, events, contactsService, chatService, communityService, messageService) result.nodeSectionModule = node_section_module.newModule(result, events, settingsService, nodeService, nodeConfigurationService) @@ -210,6 +213,8 @@ proc createCommunityItem[T](self: Module[T], c: CommunityDto): SectionItem = contactDetails.icon, contactDetails.details.identicon, contactDetails.isidenticon, + self.controller.getEmojiHash(member.id), + self.controller.getColorHash(member.id), contactDetails.details.added )), c.pendingRequestsToJoin.map(x => pending_request_item.initItem( @@ -231,7 +236,8 @@ method load*[T]( communityService: community_service.Service, messageService: message_service.Service, gifService: gif_service.Service, - mailserversService: mailservers_service.Service + mailserversService: mailservers_service.Service, + visualIdentityService: visual_identity_service.Service ) = singletonInstance.engine.setRootContextProperty("mainModule", self.viewVariant) self.controller.init() @@ -347,9 +353,9 @@ method load*[T]( activeSection = profileSettingsSectionItem # Load all sections - self.chatSectionModule.load(events, settingsService, contactsService, chatService, communityService, messageService, gifService, mailserversService) + self.chatSectionModule.load(events, settingsService, contactsService, chatService, communityService, messageService, gifService, mailserversService, visualIdentityService) for cModule in self.communitySectionsModule.values: - cModule.load(events, settingsService, contactsService, chatService, communityService, messageService, gifService, mailserversService) + cModule.load(events, settingsService, contactsService, chatService, communityService, messageService, gifService, mailserversService, visualIdentityService) self.browserSectionModule.load() # self.nodeManagementSectionModule.load() @@ -575,7 +581,8 @@ method communityJoined*[T]( communityService: community_service.Service, messageService: message_service.Service, gifService: gif_service.Service, - mailserversService: mailservers_service.Service + mailserversService: mailservers_service.Service, + visualIdentityService: visual_identity_service.Service ) = var firstCommunityJoined = false if (self.communitySectionsModule.len == 0): @@ -593,7 +600,7 @@ method communityJoined*[T]( gifService, mailserversService ) - self.communitySectionsModule[community.id].load(events, settingsService, contactsService, chatService, communityService, messageService, gifService, mailserversService) + self.communitySectionsModule[community.id].load(events, settingsService, contactsService, chatService, communityService, messageService, gifService, mailserversService, visualIdentityService) let communitySectionItem = self.createCommunityItem(community) if (firstCommunityJoined): @@ -646,6 +653,17 @@ method getContactDetailsAsJson*[T](self: Module[T], publicKey: string): string = } return $jsonObj +method getEmojiHashAsJson*[T](self: Module[T], publicKey: string): string = + let emojiHash = self.controller.getEmojiHash(publicKey) + return $$emojiHash + +method getColorHashAsJson*[T](self: Module[T], publicKey: string): string = + let colorHash = self.controller.getColorHash(publicKey) + let json = newJArray() + for segment in colorHash: + json.add(%* {"segmentLength": segment.len, "colorId": segment.colorIdx}) + return $json + method resolveENS*[T](self: Module[T], ensName: string, uuid: string) = if ensName.len == 0: error "error: cannot do a lookup for empty ens name" @@ -692,4 +710,4 @@ method newCommunityMembershipRequestReceived*[T](self: Module[T], membershipRequ let (contactName, _, _) = self.controller.getContactNameAndImage(membershipRequest.publicKey) let community = self.controller.getCommunityById(membershipRequest.communityId) singletonInstance.globalEvents.newCommunityMembershipRequestNotification("New membership request", - fmt "{contactName} asks to join {community.name}", community.id) \ No newline at end of file + fmt "{contactName} asks to join {community.name}", community.id) 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 ca6698ef5b..9c8d6b8b42 100644 --- a/src/app/modules/main/private_interfaces/module_access_interface.nim +++ b/src/app/modules/main/private_interfaces/module_access_interface.nim @@ -5,6 +5,7 @@ import ../../../../app_service/service/community/service as community_service import ../../../../app_service/service/message/service as message_service import ../../../../app_service/service/gif/service as gif_service import ../../../../app_service/service/mailservers/service as mailservers_service +import ../../../../app_service/service/visual_identity/service as visual_identity_service import ../../../core/eventemitter @@ -20,7 +21,8 @@ method load*( communityService: community_service.Service, messageService: message_service.Service, gifService: gif_service.Service, - mailserversService: mailservers_service.Service + mailserversService: mailservers_service.Service, + visualIdentityService: visual_identity_service.Service ) {.base.} = raise newException(ValueError, "No implementation available") diff --git a/src/app/modules/main/private_interfaces/module_controller_delegate_interface.nim b/src/app/modules/main/private_interfaces/module_controller_delegate_interface.nim index d77b372441..20afecc656 100644 --- a/src/app/modules/main/private_interfaces/module_controller_delegate_interface.nim +++ b/src/app/modules/main/private_interfaces/module_controller_delegate_interface.nim @@ -23,7 +23,8 @@ method communityJoined*(self: AccessInterface, community: CommunityDto, events: communityService: community_service.Service, messageService: message_service.Service, gifService: gif_service.Service, - mailserversService: mailservers_service.Service) {.base.} = + mailserversService: mailservers_service.Service, + visualIdentityService: visual_identity_service.Service) {.base.} = raise newException(ValueError, "No implementation available") method communityEdited*(self: AccessInterface, community: CommunityDto) {.base.} = @@ -44,7 +45,7 @@ method mnemonicBackedUp*(self: AccessInterface) {.base.} = method osNotificationClicked*(self: AccessInterface, details: NotificationDetails) {.base.} = raise newException(ValueError, "No implementation available") -method newCommunityMembershipRequestReceived*(self: AccessInterface, membershipRequest: CommunityMembershipRequestDto) +method newCommunityMembershipRequestReceived*(self: AccessInterface, membershipRequest: CommunityMembershipRequestDto) {.base.} = raise newException(ValueError, "No implementation available") @@ -52,4 +53,4 @@ method onNetworkConnected*(self: AccessInterface) {.base.} = raise newException(ValueError, "No implementation available") method onNetworkDisconnected*(self: AccessInterface) {.base.} = - raise newException(ValueError, "No implementation available") \ No newline at end of file + raise newException(ValueError, "No implementation available") diff --git a/src/app/modules/main/private_interfaces/module_view_delegate_interface.nim b/src/app/modules/main/private_interfaces/module_view_delegate_interface.nim index 3e4225e984..423a412808 100644 --- a/src/app/modules/main/private_interfaces/module_view_delegate_interface.nim +++ b/src/app/modules/main/private_interfaces/module_view_delegate_interface.nim @@ -26,6 +26,12 @@ method getAppSearchModule*(self: AccessInterface): QVariant {.base.} = method getContactDetailsAsJson*(self: AccessInterface, publicKey: string): string {.base.} = raise newException(ValueError, "No implementation available") +method getEmojiHashAsJson*(self: AccessInterface, publicKey: string): string {.base.} = + raise newException(ValueError, "No implementation available") + +method getColorHashAsJson*(self: AccessInterface, publicKey: string): string {.base.} = + raise newException(ValueError, "No implementation available") + method resolveENS*(self: AccessInterface, ensName: string, uuid: string) {.base.} = raise newException(ValueError, "No implementation available") diff --git a/src/app/modules/main/view.nim b/src/app/modules/main/view.nim index 983e4702ae..2978c725f1 100644 --- a/src/app/modules/main/view.nim +++ b/src/app/modules/main/view.nim @@ -151,6 +151,14 @@ QtObject: proc getContactDetailsAsJson(self: View, publicKey: string): string {.slot.} = return self.delegate.getContactDetailsAsJson(publicKey) + # serialized return - nimqml does not allow QVariant return type in slots + proc getEmojiHashAsJson(self: View, publicKey: string): string {.slot.} = + return self.delegate.getEmojiHashAsJson(publicKey) + + # serialized return - nimqml does not allow QVariant return type in slots + proc getColorHashAsJson(self: View, publicKey: string): string {.slot.} = + return self.delegate.getColorHashAsJson(publicKey) + proc resolveENS*(self: View, ensName: string, uuid: string) {.slot.} = self.delegate.resolveENS(ensName, uuid) @@ -176,4 +184,4 @@ QtObject: QtProperty[bool] isOnline: read = isConnected - notify = onlineStatusChanged \ No newline at end of file + notify = onlineStatusChanged diff --git a/src/app/modules/shared_models/color_hash_item.nim b/src/app/modules/shared_models/color_hash_item.nim new file mode 100644 index 0000000000..931284312b --- /dev/null +++ b/src/app/modules/shared_models/color_hash_item.nim @@ -0,0 +1,23 @@ +import strformat + +type + Item* = ref object + length: int + colorIdx: int + +proc initItem*(length: int, colorIdx: int): Item = + result = Item() + result.length = length + result.colorIdx = colorIdx + +proc `$`*(self: Item): string = + result = fmt"""ColorHashItem( + length: {$self.length}, + colorIdx: {$self.colorIdx}, + ]""" + +proc length*(self: Item): int {.inline.} = + self.length + +proc colorIdx*(self: Item): int {.inline.} = + self.colorIdx diff --git a/src/app/modules/shared_models/color_hash_model.nim b/src/app/modules/shared_models/color_hash_model.nim new file mode 100644 index 0000000000..e8ce9243ad --- /dev/null +++ b/src/app/modules/shared_models/color_hash_model.nim @@ -0,0 +1,53 @@ +import NimQml, Tables + +import color_hash_item + +type + ModelRole {.pure.} = enum + Length = UserRole + 1 + ColorIdx + +QtObject: + type + Model* = ref object of QAbstractListModel + items*: seq[Item] + + proc delete(self: Model) = + self.items = @[] + self.QAbstractListModel.delete + + proc setup(self: Model) = + self.QAbstractListModel.setup + + proc newModel*(): Model = + new(result, delete) + result.setup + + proc setItems*(self: Model, items: seq[Item]) = + self.beginResetModel() + self.items = items + self.endResetModel() + + method rowCount(self: Model, index: QModelIndex = nil): int = + return self.items.len + + method data(self: Model, index: QModelIndex, role: int): QVariant = + if not index.isValid: + return + if index.row < 0 or index.row >= self.items.len: + return + + let item = self.items[index.row] + let enumRole = role.ModelRole + + case enumRole: + of ModelRole.Length: + result = newQVariant(item.length) + of ModelRole.ColorIdx: + result = newQVariant(item.colorIdx) + + method roleNames(self: Model): Table[int, string] = + { + ModelRole.Length.int:"length", + ModelRole.ColorIdx.int:"colorIdx", + }.toTable diff --git a/src/app/modules/shared_models/emojis_model.nim b/src/app/modules/shared_models/emojis_model.nim new file mode 100644 index 0000000000..34aeb472d1 --- /dev/null +++ b/src/app/modules/shared_models/emojis_model.nim @@ -0,0 +1,42 @@ +import NimQml, Tables + +type + RoleNames {.pure.} = enum + Emoji = UserRole + 1, + +QtObject: + type + Model* = ref object of QAbstractListModel + items*: seq[string] + + proc delete(self: Model) = + self.items = @[] + self.QAbstractListModel.delete + + proc setup(self: Model) = + self.QAbstractListModel.setup + + proc newModel*(): Model = + new(result, delete) + result.setup + + proc setItems*(self: Model, items: seq[string]) = + self.beginResetModel() + self.items = items + self.endResetModel() + + proc items*(self: Model): seq[string] = + return self.items + + method rowCount(self: Model, index: QModelIndex = nil): int = + return self.items.len + + method data(self: Model, index: QModelIndex, role: int): QVariant = + if not index.isValid: + return + if index.row < 0 or index.row >= self.items.len: + return + return newQVariant(self.items[index.row]) + + method roleNames(self: Model): Table[int, string] = + { RoleNames.Emoji.int:"emoji" }.toTable diff --git a/src/app/modules/shared_models/user_item.nim b/src/app/modules/shared_models/user_item.nim index c7ca32e1e2..825b46af05 100644 --- a/src/app/modules/shared_models/user_item.nim +++ b/src/app/modules/shared_models/user_item.nim @@ -1,4 +1,6 @@ -import strformat +import strformat, sequtils, sugar + +import ./emojis_model, ./color_hash_model, ./color_hash_item type OnlineStatus* {.pure.} = enum @@ -8,6 +10,9 @@ type Idle Invisible +type + ColorHashSegment* = tuple[len, colorIdx: int] + # TODO add role when it is needed type Item* = ref object @@ -20,6 +25,8 @@ type icon: string identicon: string isIdenticon: bool + emojiHashModel: emojis_model.Model + colorHashModel: color_hash_model.Model isAdded: bool isAdmin: bool joined: bool @@ -34,6 +41,8 @@ proc initItem*( icon: string, identicon: string, isidenticon: bool, + emojiHash: seq[string], + colorHash: seq[ColorHashSegment], isAdded: bool = false, isAdmin: bool = false, joined: bool = false, @@ -48,6 +57,10 @@ proc initItem*( result.icon = icon result.identicon = identicon result.isIdenticon = isidenticon + result.emojiHashModel = emojis_model.newModel() + result.emojiHashModel.setItems(emojiHash) + result.colorHashModel = color_hash_model.newModel() + result.colorHashModel.setItems(map(colorHash, x => color_hash_item.initItem(x.len, x.colorIdx))) result.isAdded = isAdded result.isAdmin = isAdmin result.joined = joined @@ -61,10 +74,10 @@ proc `$`*(self: Item): string = onlineStatus: {$self.onlineStatus.int}, icon: {self.icon}, identicon: {self.identicon}, - isIdenticon: {$self.isIdenticon} - isAdded: {$self.isAdded} - isAdmin: {$self.isAdmin} - joined: {$self.joined} + isIdenticon: {$self.isIdenticon}, + isAdded: {$self.isAdded}, + isAdmin: {$self.isAdmin}, + joined: {$self.joined}, ]""" proc id*(self: Item): string {.inline.} = @@ -132,3 +145,9 @@ proc joined*(self: Item): bool {.inline.} = proc `joined=`*(self: Item, value: bool) {.inline.} = self.joined = value + +proc emojiHashModel*(self: Item): emojis_model.Model {.inline.} = + self.emojiHashModel + +proc colorHashModel*(self: Item): color_hash_model.Model {.inline.} = + self.colorHashModel diff --git a/src/app/modules/shared_models/user_model.nim b/src/app/modules/shared_models/user_model.nim index 2501017bc1..a0c75246fd 100644 --- a/src/app/modules/shared_models/user_model.nim +++ b/src/app/modules/shared_models/user_model.nim @@ -13,6 +13,8 @@ type Icon Identicon IsIdenticon + EmojiHashModel + ColorHashModel IsAdded IsAdmin Joined @@ -66,6 +68,8 @@ QtObject: ModelRole.Icon.int:"icon", ModelRole.Identicon.int:"identicon", ModelRole.IsIdenticon.int:"isIdenticon", + ModelRole.EmojiHashModel.int:"emojiHashModel", + ModelRole.ColorHashModel.int:"colorHashModel", ModelRole.IsAdded.int:"isAdded", ModelRole.IsAdmin.int:"isAdmin", ModelRole.Joined.int:"joined", @@ -100,6 +104,10 @@ QtObject: result = newQVariant(item.identicon) of ModelRole.IsIdenticon: result = newQVariant(item.isIdenticon) + of ModelRole.EmojiHashModel: + result = newQVariant(item.emojiHashModel) + of ModelRole.ColorHashModel: + result = newQVariant(item.colorHashModel) of ModelRole.IsAdded: result = newQVariant(item.isAdded) of ModelRole.IsAdmin: diff --git a/src/app_service/service/visual_identity/dto.nim b/src/app_service/service/visual_identity/dto.nim new file mode 100644 index 0000000000..7db370cda4 --- /dev/null +++ b/src/app_service/service/visual_identity/dto.nim @@ -0,0 +1,17 @@ +import json, sequtils, sugar + +type + EmojiHashDto* = seq[string] + ColorHashSegmentDto* = tuple[len, colorIdx: int] + ColorHashDto* = seq[ColorHashSegmentDto] + +proc toEmojiHashDto*(jsonObj: JsonNode): EmojiHashDto = + result = map(jsonObj.getElems(), node => node.getStr()) + return + +proc toColorHashDto*(jsonObj: JsonNode): ColorHashDto = + result = map(jsonObj.getElems(), + node => (len: node.getElems()[0].getInt(), + colorIdx: node.getElems()[1].getInt()) + ) + return diff --git a/src/app_service/service/visual_identity/service.nim b/src/app_service/service/visual_identity/service.nim new file mode 100644 index 0000000000..5eacac056d --- /dev/null +++ b/src/app_service/service/visual_identity/service.nim @@ -0,0 +1,42 @@ +import chronicles + +import ./dto as dto +import ./service_interface +import ../../../backend/visual_identity as status_visual_identity + +export dto + +type + Service* = ref object of service_interface.ServiceInterface + +proc newService*(): Service = + result = Service() + +method delete*(self: Service) = + discard + +proc emojiHashOf*(self: Service, pubkey: string): EmojiHashDto = + try: + let response = status_visual_identity.emojiHashOf(pubkey) + + if(not response.error.isNil): + error "error emojiHashOf: ", errDescription = response.error.message + + result = toEmojiHashDto(response.result) + + except Exception as e: + error "error: ", methodName = "emojiHashOf", errName = e.name, + errDesription = e.msg + +proc colorHashOf*(self: Service, pubkey: string): ColorHashDto = + try: + let response = status_visual_identity.colorHashOf(pubkey) + + if(not response.error.isNil): + error "error colorHashOf: ", errDescription = response.error.message + + result = toColorHashDto(response.result) + + except Exception as e: + error "error: ", methodName = "colorHashOf", errName = e.name, + errDesription = e.msg diff --git a/src/app_service/service/visual_identity/service_interface.nim b/src/app_service/service/visual_identity/service_interface.nim new file mode 100644 index 0000000000..1a4ed98783 --- /dev/null +++ b/src/app_service/service/visual_identity/service_interface.nim @@ -0,0 +1,15 @@ +import ./dto as dto +export dto + +type + ServiceInterface* {.pure inheritable.} = ref object of RootObj + ## Abstract class for this service access. + +method delete*(self: ServiceInterface) {.base.} = + raise newException(ValueError, "No implementation available") + +method emojiHashOf*(self: ServiceInterface, pubkey: string): EmojiHashDto {.base.} = + raise newException(ValueError, "No implementation available") + +method colorHashOf*(self: ServiceInterface, pubkey: string): ColorHashDto {.base.} = + raise newException(ValueError, "No implementation available") diff --git a/src/backend/visual_identity.nim b/src/backend/visual_identity.nim new file mode 100644 index 0000000000..5cc3046a3d --- /dev/null +++ b/src/backend/visual_identity.nim @@ -0,0 +1,15 @@ +import json, chronicles, core +import response_type + +export response_type + +logScope: + topics = "rpc-visual-identity" + +proc emojiHashOf*(key: string): RpcResponse[JsonNode] {.raises: [Exception].} = + let payload = %* [key] + result = core.callPrivateRPC("visualIdentity_emojiHashOf", payload) + +proc colorHashOf*(key: string): RpcResponse[JsonNode] {.raises: [Exception].} = + let payload = %* [key] + result = core.callPrivateRPC("visualIdentity_colorHashOf", payload) diff --git a/ui/StatusQ b/ui/StatusQ index 428b165198..082bb8ef45 160000 --- a/ui/StatusQ +++ b/ui/StatusQ @@ -1 +1 @@ -Subproject commit 428b165198e53caa5a55ef605f241cc4b6f7e02d +Subproject commit 082bb8ef45b64986be1bf67f4d7c95b1a8ea1440 diff --git a/ui/app/AppLayouts/Chat/ChatLayout.qml b/ui/app/AppLayouts/Chat/ChatLayout.qml index 690d6e7d51..9a45d89fc9 100644 --- a/ui/app/AppLayouts/Chat/ChatLayout.qml +++ b/ui/app/AppLayouts/Chat/ChatLayout.qml @@ -98,6 +98,7 @@ StatusAppThreePanelLayout { Component { id: userListComponent UserListPanel { + rootStore: root.rootStore label: localAccountSensitiveSettings.communitiesEnabled && root.rootStore.chatCommunitySectionModule.isCommunity() ? //% "Members" diff --git a/ui/app/AppLayouts/Chat/controls/UserDelegate.qml b/ui/app/AppLayouts/Chat/controls/UserDelegate.qml index 6c6111e108..856a6af9ff 100644 --- a/ui/app/AppLayouts/Chat/controls/UserDelegate.qml +++ b/ui/app/AppLayouts/Chat/controls/UserDelegate.qml @@ -1,10 +1,12 @@ -import QtQuick 2.13 -import QtQuick.Controls 2.13 -import shared 1.0 -import shared.panels 1.0 +import QtQuick 2.14 +import QtQuick.Controls 2.14 import utils 1.0 +import shared 1.0 +import shared.panels 1.0 +import shared.controls.chat 1.0 + import StatusQ.Components 0.1 import StatusQ.Core.Theme 0.1 import StatusQ.Core 0.1 @@ -20,6 +22,7 @@ Item { property string icon: "" property string identicon: "" property bool isIdenticon: true + property bool isCurrentUser: false property bool isAdded: false property string iconToShow: { if (isIdenticon || (!isAdded && @@ -48,23 +51,19 @@ Item { radius: 8 color: wrapper.color - StatusSmartIdenticon { + UserImage { id: contactImage anchors.left: parent.left anchors.leftMargin: Style.current.padding anchors.verticalCenter: parent.verticalCenter - image: StatusImageSettings { - width: 28 - height: 28 - source: wrapper.iconToShow - isIdenticon: wrapper.isIdenticon - } - icon: StatusIconSettings { - width: 28 - height: 28 - letterSize: 15 - } + + imageWidth: 28 + imageHeight: 28 name: wrapper.name + pubkey: wrapper.publicKey + icon: wrapper.iconToShow + isIdenticon: wrapper.isIdenticon + showRing: !(wrapper.isCurrentUser || wrapper.isAdded) } StyledText { diff --git a/ui/app/AppLayouts/Chat/panels/ContactListPanel.qml b/ui/app/AppLayouts/Chat/panels/ContactListPanel.qml index 2d3686707c..136010e1e1 100644 --- a/ui/app/AppLayouts/Chat/panels/ContactListPanel.qml +++ b/ui/app/AppLayouts/Chat/panels/ContactListPanel.qml @@ -27,11 +27,9 @@ ScrollView { clip: true delegate: StatusListItem { id: contactDelegate + property bool isChecked: selectedPubKeys.indexOf(model.pubKey) !== -1 - title: !model.name.endsWith(".eth") && !!model.localNickname ? - model.localNickname : Utils.removeStatusEns(model.name) - image.source: Global.getProfileImage(model.pubKey) || model.identicon - image.isIdenticon: !!model.identicon + visible: { if (selectMode) { return !searchString || model.name.toLowerCase().includes(searchString) @@ -39,6 +37,12 @@ ScrollView { return checkbox.checked } + title: !model.name.endsWith(".eth") && !!model.localNickname ? + model.localNickname : Utils.removeStatusEns(model.name) + image.source: Global.getProfileImage(model.pubKey) || model.identicon + image.isIdenticon: !!model.identicon + ringSettings.ringSpecModel: Utils.getColorHashAsJson(model.pubKey) + height: visible ? implicitHeight : 0 function contactToggled(pubKey) { diff --git a/ui/app/AppLayouts/Chat/panels/UserListPanel.qml b/ui/app/AppLayouts/Chat/panels/UserListPanel.qml index 528119f514..935a2bcd82 100644 --- a/ui/app/AppLayouts/Chat/panels/UserListPanel.qml +++ b/ui/app/AppLayouts/Chat/panels/UserListPanel.qml @@ -22,6 +22,8 @@ Item { property var messageContextMenu property string label + property var rootStore + StatusBaseText { id: titleText anchors.top: parent.top @@ -61,6 +63,7 @@ Item { isAdded: model.isAdded userStatus: model.onlineStatus messageContextMenu: root.messageContextMenu + isCurrentUser: rootStore.isCurrentUser(model.id) } section.property: "onlineStatus" section.delegate: (root.width > 58) ? sectionDelegateComponent : null diff --git a/ui/app/AppLayouts/Chat/stores/RootStore.qml b/ui/app/AppLayouts/Chat/stores/RootStore.qml index b4a7d98770..c1abd6aea2 100644 --- a/ui/app/AppLayouts/Chat/stores/RootStore.qml +++ b/ui/app/AppLayouts/Chat/stores/RootStore.qml @@ -134,6 +134,14 @@ QtObject { globalUtilsInst.downloadImage(content, path) } + function isCurrentUser(pubkey) { + return userProfileInst.pubKey === pubkey + } + + function displayName(name, pubkey) { + return isCurrentUser(pubkey) ? qsTr("You") : name + } + function getCommunity(communityId) { // Not Refactored Yet // try { diff --git a/ui/app/AppLayouts/Profile/views/EnsListView.qml b/ui/app/AppLayouts/Profile/views/EnsListView.qml index 2e24088415..10e45f29f2 100644 --- a/ui/app/AppLayouts/Profile/views/EnsListView.qml +++ b/ui/app/AppLayouts/Profile/views/EnsListView.qml @@ -289,10 +289,9 @@ Item { icon: root.ensUsernamesStore.icon isIdenticon: root.ensUsernamesStore.isIdenticon + showRing: true - onClickMessage: { - root.parent.clickMessage(isProfileClick, isSticker, isImage, image, emojiOnly, hideEmojiPicker, isReply); - } + onClicked: root.parent.clickMessage(true, false, false, null, false, false, false) } UsernameLabel { diff --git a/ui/imports/shared/controls/chat/ProfileHeader.qml b/ui/imports/shared/controls/chat/ProfileHeader.qml new file mode 100644 index 0000000000..a6ed328e1a --- /dev/null +++ b/ui/imports/shared/controls/chat/ProfileHeader.qml @@ -0,0 +1,115 @@ +import QtQuick 2.14 +import QtQuick.Layouts 1.14 + +import utils 1.0 +import shared.panels 1.0 + +import StatusQ.Components 0.1 +import StatusQ.Core.Utils 0.1 as StatusQUtils + +Item { + id: root + + property string displayName + property string pubkey + property string icon + property bool isIdenticon: false + + property bool displayNameVisible: true + property bool pubkeyVisible: true + + property alias imageWidth: userImage.imageWidth + property alias imageHeight: userImage.imageHeight + property alias emojiSize: emojihash.size + + property alias imageOverlay: imageOverlay.sourceComponent + + signal clicked() + + height: visible ? contentContainer.height : 0 + + ColumnLayout { + id: contentContainer + + anchors { + left: parent.left + right: parent.right + leftMargin: Style.current.smallPadding + rightMargin: Style.current.smallPadding + } + + UserImage { + id: userImage + + Layout.alignment: Qt.AlignHCenter + + name: root.displayName + pubkey: root.pubkey + icon: root.icon + isIdenticon: root.isIdenticon + showRing: true + interactive: false + + Loader { + id: imageOverlay + anchors.fill: parent + } + } + + StyledText { + Layout.fillWidth: true + + visible: root.displayNameVisible + + text: root.displayName + + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + maximumLineCount: 3 + wrapMode: Text.Wrap + font { + weight: Font.Medium + pixelSize: Style.current.primaryTextFontSize + } + } + + StyledText { + Layout.fillWidth: true + + visible: root.pubkeyVisible + + text: pubkey.substring(0, 10) + "..." + pubkey.substring( + pubkey.length - 4) + + horizontalAlignment: Text.AlignHCenter + font.pixelSize: Style.current.asideTextFontSize + color: Style.current.secondaryText + } + + Text { + id: emojihash + + property string size: "14x14" + + Layout.fillWidth: true + + text: { + const emojiHash = Utils.getEmojiHashAsJson(root.pubkey) + var emojiHashFirstLine = "" + var emojiHashSecondLine = "" + for (var i = 0; i < 7; i++) { + emojiHashFirstLine += emojiHash[i] + } + for (var i = 7; i < emojiHash.length; i++) { + emojiHashSecondLine += emojiHash[i] + } + + return StatusQUtils.Emoji.parse(emojiHashFirstLine, size) + "
" + + StatusQUtils.Emoji.parse(emojiHashSecondLine, size) + } + + horizontalAlignment: Text.AlignHCenter + font.pointSize: 1 // make sure there is no padding for emojis due to 'style: "vertical-align: top"' + } + } +} diff --git a/ui/imports/shared/controls/chat/UserImage.qml b/ui/imports/shared/controls/chat/UserImage.qml index dfa6b9f914..f3297d6836 100644 --- a/ui/imports/shared/controls/chat/UserImage.qml +++ b/ui/imports/shared/controls/chat/UserImage.qml @@ -4,46 +4,56 @@ import shared.panels 1.0 import utils 1.0 +import StatusQ.Components 0.1 + Loader { id: root - height: active ? item.height : 0 + property int imageHeight: 36 property int imageWidth: 36 + + property string name + property string pubkey property string icon: "" property bool isIdenticon: false + property bool showRing: false - signal clickMessage(bool isProfileClick, bool isSticker, bool isImage, var image, bool emojiOnly, bool hideEmojiPicker, bool isReply) + property bool interactive: true - sourceComponent: Component { - Item { - id: chatImage - width: identiconImage.width - height: identiconImage.height + signal clicked() - RoundedImage { - id: identiconImage - width: root.imageWidth - height: root.imageHeight - border.width: root.isIdenticon ? 1 : 0 - border.color: Style.current.border - source: root.icon - smooth: false - antialiasing: true + height: active ? item.height : 0 - MouseArea { - cursorShape: Qt.PointingHandCursor - acceptedButtons: Qt.LeftButton | Qt.RightButton - anchors.fill: parent - onClicked: { - if (!!messageContextMenu) { - // Set parent, X & Y positions for the messageContextMenu - messageContextMenu.parent = root - messageContextMenu.setXPosition = function() { return root.width + 4} - messageContextMenu.setYPosition = function() { return root.height/2 + 4} - } - root.clickMessage(true, false, false, null, false, false, isReplyImage) - } - } + sourceComponent: StatusSmartIdenticon { + name: root.name + image { + width: root.imageWidth + height: root.imageHeight + source: root.isIdenticon ? "" : root.icon + isIdenticon: false + } + icon { + width: root.imageWidth + height: root.imageHeight + color: Style.current.background + textColor: Style.current.secondaryText + letterSize: Math.max(4, root.imageWidth / 2.4) + charactersLen: 2 + } + ringSettings { + ringSpecModel: root.showRing ? Utils.getColorHashAsJson(root.pubkey) : undefined + ringPxSize: Math.max(root.imageWidth / 24.0) + } + + Loader { + anchors.fill: parent + + active: root.interactive + + sourceComponent: MouseArea { + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onClicked: root.clicked() } } } diff --git a/ui/imports/shared/controls/chat/qmldir b/ui/imports/shared/controls/chat/qmldir index 695fe4dc00..08a4866415 100644 --- a/ui/imports/shared/controls/chat/qmldir +++ b/ui/imports/shared/controls/chat/qmldir @@ -12,3 +12,4 @@ StateBubble 1.0 StateBubble.qml GasSelectorButton 1.0 GasSelectorButton.qml MessageBorder 1.0 MessageBorder.qml EmojiReaction 1.0 EmojiReaction.qml +ProfileHeader 1.0 ProfileHeader.qml diff --git a/ui/imports/shared/panels/chat/ChatReplyPanel.qml b/ui/imports/shared/panels/chat/ChatReplyPanel.qml index f69bce37c3..6c6d5451e3 100644 --- a/ui/imports/shared/panels/chat/ChatReplyPanel.qml +++ b/ui/imports/shared/panels/chat/ChatReplyPanel.qml @@ -13,12 +13,15 @@ import StatusQ.Core.Utils 0.1 as StatusQUtils Loader { id: root + property bool amISenderOfTheRepliedMessage property int repliedMessageContentType property string repliedMessageSenderIcon property bool repliedMessageSenderIconIsIdenticon property bool repliedMessageIsEdited property string repliedMessageSender + property string repliedMessageSenderPubkey + property bool repliedMessageSenderIsAdded property string repliedMessageContent property string repliedMessageImage property bool isCurrentUser: false @@ -97,16 +100,20 @@ Loader { UserImage { id: userImage + anchors.left: replyCorner.right + anchors.leftMargin: Style.current.halfPadding + imageHeight: 20 imageWidth: 20 active: true - anchors.left: replyCorner.right - anchors.leftMargin: Style.current.halfPadding + + name: repliedMessageSender + pubkey: repliedMessageSenderPubkey icon: repliedMessageSenderIcon isIdenticon: repliedMessageSenderIconIsIdenticon - onClickMessage: { - root.clickMessage(true, false, false, null, false, false, true) - } + showRing: !(amISenderOfTheRepliedMessage || repliedMessageSenderIsAdded) + + onClicked: root.clickMessage(true, false, false, null, false, false, true) } StyledTextEdit { diff --git a/ui/imports/shared/popups/ProfilePopup.qml b/ui/imports/shared/popups/ProfilePopup.qml index ace3232e00..0f3df30a70 100644 --- a/ui/imports/shared/popups/ProfilePopup.qml +++ b/ui/imports/shared/popups/ProfilePopup.qml @@ -3,12 +3,13 @@ import QtQuick.Controls 2.13 import QtQuick.Layouts 1.13 import QtGraphicalEffects 1.13 - import utils 1.0 import shared 1.0 import shared.popups 1.0 import shared.stores 1.0 +import shared.controls.chat 1.0 +import StatusQ.Core 0.1 import StatusQ.Core.Theme 0.1 import StatusQ.Components 0.1 import StatusQ.Controls 0.1 @@ -29,9 +30,9 @@ StatusModal { property string userNickname: "" property string userEnsName: "" property string userIcon: "" + property bool isUserIconIdenticon: true property string text: "" - readonly property int innerMargin: 20 property bool userIsEnsVerified: false @@ -62,6 +63,7 @@ StatusModal { } else { userIcon = contactDetails.displayIcon } + isUserIconIdenticon = contactDetails.isDisplayIconIdenticon userIsEnsVerified = contactDetails.ensVerified userIsBlocked = contactDetails.isBlocked isAddedContact = contactDetails.isContact @@ -72,17 +74,9 @@ StatusModal { popup.open() } - onHeaderImageClicked: { - Global.openChangeProfilePicPopup() - } - header.title: userDisplayName header.subTitle: userIsEnsVerified ? userName : userPublicKey header.subTitleElide: Text.ElideMiddle - // In the following line we need to use `icon` property as that's the easiest way - // to update image on change (in case of logged in user) - header.image.source: isCurrentUser? popup.profileStore.icon : userIcon - header.headerImageEditable: isCurrentUser headerActionButton: StatusFlatRoundButton { type: StatusFlatRoundButton.Type.Secondary @@ -109,6 +103,50 @@ StatusModal { anchors.top: parent.top width: parent.width + Item { + height: 16 + width: parent.width + } + + ProfileHeader { + width: parent.width + + displayName: popup.userDisplayName + pubkey: popup.userPublicKey + icon: popup.isCurrentUser ? popup.profileStore.icon : popup.userIcon + isIdenticon: popup.isCurrentUser ? popup.profileStore.isIdenticon : popup.isUserIconIdenticon + + displayNameVisible: false + pubkeyVisible: false + + emojiSize: "20x20" + imageWidth: 80 + imageHeight: 80 + + imageOverlay: Item { + visible: popup.isCurrentUser + + StatusFlatRoundButton { + width: 24 + height: 24 + + anchors { + right: parent.right + bottom: parent.bottom + rightMargin: -8 + } + + type: StatusFlatRoundButton.Type.Secondary + icon.name: "pencil" + icon.color: Theme.palette.directColor1 + icon.width: 12.5 + icon.height: 12.5 + + onClicked: Global.openChangeProfilePicPopup() + } + } + } + StatusBanner { width: parent.width visible: popup.userIsBlocked diff --git a/ui/imports/shared/popups/UserStatusContextMenu.qml b/ui/imports/shared/popups/UserStatusContextMenu.qml index 1c210c8374..5a5f3b4c61 100644 --- a/ui/imports/shared/popups/UserStatusContextMenu.qml +++ b/ui/imports/shared/popups/UserStatusContextMenu.qml @@ -4,85 +4,54 @@ import QtQuick.Layouts 1.3 import QtQml.Models 2.3 import utils 1.0 -import "../panels" -import "." +import shared.controls.chat 1.0 +import shared.panels 1.0 import StatusQ.Components 0.1 // TODO: replace with StatusPopupMenu PopupMenu { id: root + property var store - width: profileHeader.width + + width: 200 closePolicy: Popup.CloseOnReleaseOutsideParent | Popup.CloseOnEscape + overrideTextColor: Style.current.textColor + + ProfileHeader { + width: parent.width + + displayName: root.store.userProfileInst.name + pubkey: root.store.userProfileInst.pubKey + icon: root.store.userProfileInst.icon + isIdenticon: root.store.userProfileInst.isIdenticon + } + Item { - id: profileHeader - width: 200 - height: visible ? profileImage.height + username.height + viewProfileBtn.height + Style.current.padding * 2 : 0 - Rectangle { - anchors.fill: parent - visible: mouseArea.containsMouse - color: Style.current.backgroundHover - } - StatusSmartIdenticon { - id: profileImage - anchors.top: parent.top - anchors.topMargin: 4 - anchors.horizontalCenter: parent.horizontalCenter - image.source: root.store.userProfileInst.icon - image.isIdenticon: root.store.userProfileInst.isIdenticon - } - StyledText { - id: username - text: root.store.userProfileInst.name - elide: Text.ElideRight - maximumLineCount: 3 - horizontalAlignment: Text.AlignHCenter - wrapMode: Text.Wrap - anchors.top: profileImage.bottom - anchors.topMargin: 4 - anchors.left: parent.left - anchors.leftMargin: Style.current.smallPadding - anchors.right: parent.right - anchors.rightMargin: Style.current.smallPadding - font.weight: Font.Medium - font.pixelSize: 13 - } + height: root.topPadding + } - StyledText { - id: viewProfileBtn - text: qsTr("My profile →") - horizontalAlignment: Text.AlignHCenter - anchors.top: username.bottom - anchors.topMargin: 4 - anchors.left: parent.left - anchors.leftMargin: Style.current.smallPadding - anchors.right: parent.right - anchors.rightMargin: Style.current.smallPadding - font.weight: Font.Medium - font.pixelSize: Style.current.tertiaryTextFontSize - color: Style.current.secondaryText - } + Separator { + } - MouseArea { - id: mouseArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - Global.openProfilePopup(root.store.userProfileInst.pubKey) - root.close() - } + Action { + text: qsTr("View My Profile") + + icon.source: Style.svg("profile") + icon.width: 16 + icon.height: 16 + + onTriggered: { + Global.openProfilePopup(root.store.userProfileInst.pubKey) + root.close() } } Separator { - anchors.bottom: profileHeader.bottom } - overrideTextColor: Style.current.textColor - Action { text: qsTr("Online") onTriggered: { @@ -113,5 +82,4 @@ PopupMenu { icon.width: 16 icon.height: 16 } - } diff --git a/ui/imports/shared/views/chat/CompactMessageView.qml b/ui/imports/shared/views/chat/CompactMessageView.qml index 4e5da7f995..28c38edadf 100644 --- a/ui/imports/shared/views/chat/CompactMessageView.qml +++ b/ui/imports/shared/views/chat/CompactMessageView.qml @@ -11,6 +11,7 @@ import shared.controls.chat 1.0 import StatusQ.Controls 0.1 as StatusQControls import StatusQ.Core.Utils 0.1 as StatusQUtils +import StatusQ.Components 0.1 Item { id: root @@ -306,6 +307,8 @@ Item { // TODO: not sure about is edited at the moment repliedMessageIsEdited = false repliedMessageSender = obj.senderDisplayName + repliedMessageSenderPubkey = obj.senderId + repliedMessageSenderIsAdded = obj.senderIsAdded repliedMessageContent = obj.messageText repliedMessageImage = obj.messageImage } @@ -327,17 +330,22 @@ Item { UserImage { id: chatImage + active: isMessage && headerRepeatCondition + anchors.left: parent.left anchors.leftMargin: Style.current.padding anchors.top: chatReply.active ? chatReply.bottom : pinnedRectangleLoader.active ? pinnedRectangleLoader.bottom : parent.top anchors.topMargin: chatReply.active || pinnedRectangleLoader.active ? 4 : Style.current.smallPadding + icon: root.senderIcon isIdenticon: root.isSenderIconIdenticon - onClickMessage: { - root.clickMessage(isProfileClick, isSticker, isImage, image, emojiOnly, hideEmojiPicker, isReply, false, "") - } + pubkey: senderId + name: senderDisplayName + showRing: !(root.amISender || senderIsAdded) + + onClicked: root.clickMessage(true, false, false, null, false, false, false, false, "") } UsernameLabel { diff --git a/ui/imports/shared/views/chat/MessageContextMenuView.qml b/ui/imports/shared/views/chat/MessageContextMenuView.qml index 537cc95c08..e6e8a42df6 100644 --- a/ui/imports/shared/views/chat/MessageContextMenuView.qml +++ b/ui/imports/shared/views/chat/MessageContextMenuView.qml @@ -116,53 +116,19 @@ StatusPopupMenu { } } - Item { - id: profileHeader - visible: root.isProfile + ProfileHeader { width: parent.width - height: visible ? profileImage.height + username.height + Style.current.padding : 0 - Rectangle { - anchors.fill: parent - visible: mouseArea.containsMouse - color: Style.current.backgroundHover - } + visible: root.isProfile - StatusSmartIdenticon { - id: profileImage - anchors.top: parent.top - anchors.topMargin: 4 - anchors.horizontalCenter: parent.horizontalCenter - image.source: root.selectedUserIcon - image.isIdenticon: root.isSelectedUserIconIdenticon - } + displayName: root.selectedUserDisplayName + pubkey: root.selectedUserPublicKey + icon: root.selectedUserIcon + isIdenticon: root.isSelectedUserIconIdenticon + } - StyledText { - id: username - text: selectedUserDisplayName - elide: Text.ElideRight - maximumLineCount: 3 - horizontalAlignment: Text.AlignHCenter - wrapMode: Text.Wrap - anchors.top: profileImage.bottom - anchors.topMargin: 4 - anchors.left: parent.left - anchors.leftMargin: Style.current.smallPadding - anchors.right: parent.right - anchors.rightMargin: Style.current.smallPadding - font.weight: Font.Medium - font.pixelSize: 15 - } - - MouseArea { - id: mouseArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - root.openProfileClicked(root.selectedUserPublicKey) - root.close() - } - } + Item { + visible: root.isProfile + height: root.topPadding } Separator { diff --git a/ui/imports/shared/views/chat/MessageView.qml b/ui/imports/shared/views/chat/MessageView.qml index 74357f93a7..0ebf582226 100644 --- a/ui/imports/shared/views/chat/MessageView.qml +++ b/ui/imports/shared/views/chat/MessageView.qml @@ -103,7 +103,7 @@ Column { // The system message for private groups appear as created by the group host, but it shouldn't prevMessageAsJsonObj.contentType === Constants.messageContentType.systemMessagePrivateGroupType) { return "" - } + } return prevMessageAsJsonObj.senderId } @@ -149,7 +149,6 @@ Column { || contentType === Constants.messageContentType.communityInviteType || contentType === Constants.messageContentType.transactionType property bool isExpired: (outgoingStatus === "sending" && (Math.floor(timestamp) + 180000) < Date.now()) - property bool isStatusUpdate: false property int statusAgeEpoch: 0 signal imageClicked(var image) @@ -196,8 +195,8 @@ Column { if(!obj) return - messageContextMenu.messageSenderId = obj.id - messageContextMenu.selectedUserPublicKey = obj.id + messageContextMenu.messageSenderId = obj.senderId + messageContextMenu.selectedUserPublicKey = obj.senderId messageContextMenu.selectedUserDisplayName = obj.senderDisplayName messageContextMenu.selectedUserIcon = obj.senderIcon messageContextMenu.isSelectedUserIconIdenticon = obj.isSenderIconIdenticon @@ -258,7 +257,7 @@ Column { case Constants.messageContentType.gapType: return gapComponent default: - return isStatusUpdate ? statusUpdateComponent : compactMessageComponent + return compactMessageComponent } } @@ -335,38 +334,6 @@ Column { } } - Component { - id: statusUpdateComponent - StatusUpdateView { - messageStore: root.messageStore - statusAgeEpoch: root.statusAgeEpoch - container: root - // Not Refactored Yet -// store: root.rootStore - messageContextMenu: root.messageContextMenu - onAddEmoji: { - root.clickMessage(isProfileClick, isSticker, isImage , image, emojiOnly, hideEmojiPicker); - } - onChatImageClicked: root.imageClicked(image) - - onUserNameClicked: { - // Not Refactored Yet - Should do it via messageStore -// root.parent.clickMessage(isProfileClick); - } - onEmojiBtnClicked: { - // Not Refactored Yet - Should do it via messageStore -// root.parent.clickMessage(isProfileClick, isSticker, isImage, image, emojiOnly); - } - onClickMessage: { - // Not Refactored Yet - Should do it via messageStore -// root.parent.clickMessage(isProfileClick, isSticker, isImage, image, emojiOnly, hideEmojiPicker, isReply); - } - onSetMessageActive: { - root.setMessageActive(messageId, active); - } - } - } - Component { id: compactMessageComponent diff --git a/ui/imports/shared/views/chat/StatusUpdateView.qml b/ui/imports/shared/views/chat/StatusUpdateView.qml deleted file mode 100644 index a5c8bdc358..0000000000 --- a/ui/imports/shared/views/chat/StatusUpdateView.qml +++ /dev/null @@ -1,211 +0,0 @@ -import QtQuick 2.3 -import QtGraphicalEffects 1.13 - -import utils 1.0 -import shared 1.0 -import shared.panels 1.0 -import shared.status 1.0 -import shared.panels.chat 1.0 -import shared.views.chat 1.0 -import shared.controls.chat 1.0 - -import StatusQ.Controls 0.1 - -MouseArea { - id: root -// property var store - property var messageStore - property bool hovered: containsMouse - property var container - property int statusAgeEpoch: 0 - property var messageContextMenu - - signal userNameClicked(bool isProfileClick) - signal setMessageActive(string messageId, bool active) - signal emojiBtnClicked(bool isProfileClick, bool isSticker, bool isImage, var image, bool emojiOnly) - signal clickMessage(bool isProfileClick, bool isSticker, bool isImage, var image, bool emojiOnly, bool hideEmojiPicker, bool isReply) - -// TODO bring those back and remove dynamic scoping -// property var emojiReactionsModel -// property string timestamp: "" -// property bool isCurrentUser: false -// property bool isMessageActive: false -// property string userName: "" -// property string localName: "" -// property string displayUserName: "" -// property bool isImage: false -// property bool isMessage: false -// property string profileImageSource: "" -// property string userIdenticon: "" - - anchors.top: parent.top - anchors.topMargin: 0 - height: (isImage ? chatImageContent.height : chatText.height) + chatName.height + 2* Style.current.padding + (emojiReactionsModel.length ? 20 : 0) - width: parent.width - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - propagateComposedEvents: true - - signal chatImageClicked(string image) - signal addEmoji(bool isProfileClick, bool isSticker, bool isImage , var image, bool emojiOnly, bool hideEmojiPicker) - - onClicked: { - mouse.accepted = false - } - - Rectangle { - id: rootRect - anchors.fill: parent - radius: Style.current.radius - color: root.hovered ? Style.current.border : Style.current.background - - UserImage { - id: chatImage - active: isMessage || isImage - anchors.left: parent.left - anchors.leftMargin: Style.current.padding - anchors.top: parent.top - anchors.topMargin: Style.current.halfPadding -// messageContextMenu: root.messageContextMenu -// profileImage: root.profileImageSource -// isMessage: root.isMessage -// identiconImageSource: root.userIdenticon - onClickMessage: { - root.clickMessage(true, false, false, null, false, false, isReplyImage) - } - } - - UsernameLabel { - id: chatName - z: 51 - visible: chatImage.visible - anchors.leftMargin: Style.current.halfPadding - anchors.top: chatImage.top - anchors.left: chatImage.right - label.font.pixelSize: Style.current.primaryTextFontSize -// messageContextMenu: root.messageContextMenu -// isCurrentUser: root.isCurrentUser -// userName: root.userName -// localName: root.localName -// displayUserName: root.displayUserName - onClickMessage: { - root.userNameClicked(true); - } - } - - ChatTimePanel { - id: chatTime - // statusAgeEpoch is used to trigger Qt property update - // since the returned string will be the same in 99% cases, this should not trigger ChatTime re-rendering - text: Utils.formatAgeFromTime(timestamp, statusAgeEpoch) - visible: chatName.visible - anchors.verticalCenter: chatName.verticalCenter - anchors.left: chatName.right - anchors.leftMargin: Style.current.halfPadding - //timestamp: timestamp - } - - ChatTextView { - id: chatText - anchors.top: chatName.visible ? chatName.bottom : chatImage.top - anchors.topMargin: chatName.visible ? 6 : 0 - anchors.left: chatImage.right - anchors.leftMargin: Style.current.halfPadding - anchors.right: parent.right - anchors.rightMargin: Style.current.padding -// store: root.store - } - - Loader { - id: chatImageContent - active: isImage - anchors.left: chatImage.right - anchors.leftMargin: Style.current.halfPadding - anchors.top: chatText.bottom - z: 51 - - sourceComponent: Component { - StatusChatImage { - playing: root.messageStore.playAnimation - imageSource: image - imageWidth: 200 - container: root.container - onClicked: { - root.chatImageClicked(image); - } - } - } - } - - StatusFlatRoundButton { - id: emojiBtn - width: 32 - height: 32 - anchors.top: rootRect.top - anchors.topMargin: -height / 4 - anchors.right: rootRect.right - anchors.rightMargin: Style.current.halfPadding - visible: root.hovered - icon.name: "reaction-b" - icon.width: 20 - icon.height: 20 - type: StatusFlatRoundButton.Type.Tertiary - backgroundHoverColor: Style.current.background - onClicked: { - // Set parent, X & Y positions for the messageContextMenu - messageContextMenu.parent = emojiBtn - messageContextMenu.setXPosition = function() { return -messageContextMenu.width + emojiBtn.width} - messageContextMenu.setYPosition = function() { return -messageContextMenu.height - 4} - root.emojiBtnClicked(false, false, false, null, true) - } - } - - DropShadow { - anchors.fill: emojiBtn - horizontalOffset: 0 - verticalOffset: 2 - radius: 10 - samples: 12 - color: "#22000000" - source: emojiBtn - } - - Loader { - id: emojiReactionLoader - active: emojiReactionsModel.length - sourceComponent: emojiReactionsComponent - anchors.left: chatImage.right - anchors.leftMargin: Style.current.halfPadding - anchors.top: isImage ? chatImageContent.bottom : chatText.bottom - anchors.topMargin: Style.current.halfPadding - } - - Component { - id: emojiReactionsComponent - EmojiReactionsPanel { - store: messageStore - emojiReactionsModel: reactionsModel - isMessageActive: isMessageActive - isCurrentUser: isCurrentUser - onAddEmojiClicked: { - root.addEmoji(false, false, false, null, true, false); - // Set parent, X & Y positions for the messageContextMenu - messageContextMenu.parent = emojiReactionLoader - messageContextMenu.setXPosition = function() { return (messageContextMenu.parent.x + 4)} - messageContextMenu.setYPosition = function() { return (-messageContextMenu.height - 4)} - } - - onToggleReaction: messageStore.toggleReaction(messageId, emojiID) - - onSetMessageActive: { - root.setMessageActive(messageId, active); - } - } - } - - Separator { - anchors.bottom: parent.bottom - visible: !root.hovered - } - } -} diff --git a/ui/imports/shared/views/chat/qmldir b/ui/imports/shared/views/chat/qmldir index 56998db150..5f7b6d157d 100644 --- a/ui/imports/shared/views/chat/qmldir +++ b/ui/imports/shared/views/chat/qmldir @@ -1,7 +1,6 @@ ChannelIdentifierView 1.0 ChannelIdentifierView.qml ChatTextView 1.0 ChatTextView.qml MessageView 1.0 MessageView.qml -StatusUpdateView 1.0 StatusUpdateView.qml TransactionBubbleView 1.0 TransactionBubbleView.qml LinksMessageView 1.0 LinksMessageView.qml InvitationBubbleView 1.0 InvitationBubbleView.qml diff --git a/ui/imports/utils/Utils.qml b/ui/imports/utils/Utils.qml index 1d3e794d9a..cc144351dc 100644 --- a/ui/imports/utils/Utils.qml +++ b/ui/imports/utils/Utils.qml @@ -604,7 +604,21 @@ QtObject { } } + function getEmojiHashAsJson(publicKey) { + if (publicKey === "") { + return "" + } + let jsonObj = mainModule.getEmojiHashAsJson(publicKey) + return JSON.parse(jsonObj) + } + function getColorHashAsJson(publicKey) { + if (publicKey === "") { + return "" + } + let jsonObj = mainModule.getColorHashAsJson(publicKey) + return JSON.parse(jsonObj) + } // Leave this function at the bottom of the file as QT Creator messes up the code color after this function isPunct(c) {