From f8ecd9dbce98bde836b219ae4e6125f2ab782c79 Mon Sep 17 00:00:00 2001 From: Jonathan Rainville Date: Mon, 30 Jan 2023 16:05:34 -0500 Subject: [PATCH] refactor(chat): make getChats async to speed up start time Fixes #9340 --- src/app/boot/app_controller.nim | 2 +- src/app/modules/main/controller.nim | 18 +++ src/app/modules/main/io_interface.nim | 18 +++ src/app/modules/main/module.nim | 75 ++++++++---- src/app/modules/main/view.nim | 28 +++++ src/app_service/service/chat/async_tasks.nim | 15 +++ src/app_service/service/chat/service.nim | 62 +++++++--- .../Chat/views/ContactsColumnView.qml | 2 +- ui/app/mainui/AppMain.qml | 112 +++++++++++++----- ui/imports/utils/Constants.qml | 2 + 10 files changed, 257 insertions(+), 77 deletions(-) create mode 100644 src/app_service/service/chat/async_tasks.nim diff --git a/src/app/boot/app_controller.nim b/src/app/boot/app_controller.nim index e1aef99f2a..f16d64cf69 100644 --- a/src/app/boot/app_controller.nim +++ b/src/app/boot/app_controller.nim @@ -166,7 +166,7 @@ proc newAppController*(statusFoundation: StatusFoundation): AppController = statusFoundation.events, statusFoundation.threadpool, result.networkService, result.settingsService, result.activityCenterService ) - result.chatService = chat_service.newService(statusFoundation.events, result.contactsService) + result.chatService = chat_service.newService(statusFoundation.events, statusFoundation.threadpool, result.contactsService) result.tokenService = token_service.newService( statusFoundation.events, statusFoundation.threadpool, result.networkService ) diff --git a/src/app/modules/main/controller.nim b/src/app/modules/main/controller.nim index 3925019021..5dfa3167c8 100644 --- a/src/app/modules/main/controller.nim +++ b/src/app/modules/main/controller.nim @@ -86,6 +86,24 @@ proc init*(self: Controller) = let d9 = 9*86400 # 9 days discard self.settingsService.setDefaultSyncPeriod(d9) + self.events.on(SIGNAL_CHATS_LOADED) do(e:Args): + let args = ChannelGroupsArgs(e) + self.delegate.onChatsLoaded( + args.channelGroups, + self.events, + self.settingsService, + self.nodeConfigurationService, + self.contactsService, + self.chatService, + self.communityService, + self.messageService, + self.gifService, + self.mailserversService, + ) + + self.events.on(SIGNAL_CHATS_LOADING_FAILED) do(e:Args): + self.delegate.onChatsLoadingFailed() + self.events.on(SIGNAL_ACTIVE_MAILSERVER_CHANGED) do(e:Args): let args = ActiveMailserverChangedArgs(e) if args.nodeAddress == "": diff --git a/src/app/modules/main/io_interface.nim b/src/app/modules/main/io_interface.nim index 781763d33a..f065b781cb 100644 --- a/src/app/modules/main/io_interface.nim +++ b/src/app/modules/main/io_interface.nim @@ -68,6 +68,24 @@ method chatSectionDidLoad*(self: AccessInterface) {.base.} = method communitySectionDidLoad*(self: AccessInterface) {.base.} = raise newException(ValueError, "No implementation available") +method onChatsLoaded*( + self: AccessInterface, + channelGroups: seq[ChannelGroupDto], + events: EventEmitter, + settingsService: settings_service.Service, + nodeConfigurationService: node_configuration_service.Service, + contactsService: contacts_service.Service, + chatService: chat_service.Service, + communityService: community_service.Service, + messageService: message_service.Service, + gifService: gif_service.Service, + mailserversService: mailservers_service.Service) + {.base.} = + raise newException(ValueError, "No implementation available") + +method onChatsLoadingFailed*(self: AccessInterface) {.base.} = + raise newException(ValueError, "No implementation available") + method onActiveChatChange*(self: AccessInterface, sectionId: string, chatId: string) {.base.} = raise newException(ValueError, "No implementation available") diff --git a/src/app/modules/main/module.nim b/src/app/modules/main/module.nim index 80bb65e996..8f36639310 100644 --- a/src/app/modules/main/module.nim +++ b/src/app/modules/main/module.nim @@ -334,7 +334,6 @@ proc createChannelGroupItem[T](self: Module[T], c: ChannelGroupDto): SectionItem c.encrypted ) - method load*[T]( self: Module[T], events: EventEmitter, @@ -356,30 +355,6 @@ method load*[T]( if (activeSectionId == ""): activeSectionId = singletonInstance.userProfile.getPubKey() - let channelGroups = self.controller.getChannelGroups() - for channelGroup in channelGroups: - self.channelGroupModules[channelGroup.id] = chat_section_module.newModule( - self, - events, - channelGroup.id, - isCommunity = channelGroup.channelGroupType == ChannelGroupType.Community, - settingsService, - nodeConfigurationService, - contactsService, - chatService, - communityService, - messageService, - gifService, - mailserversService - ) - let channelGroupItem = self.createChannelGroupItem(channelGroup) - self.view.model().addItem(channelGroupItem) - if(activeSectionId == channelGroupItem.id): - activeSection = channelGroupItem - - self.channelGroupModules[channelGroup.id].load(channelGroup, events, settingsService, nodeConfigurationService, - contactsService, chatService, communityService, messageService, gifService, mailserversService) - # Communities Portal Section let communitiesPortalSectionItem = initItem(conf.COMMUNITIESPORTAL_SECTION_ID, SectionType.CommunitiesPortal, conf.COMMUNITIESPORTAL_SECTION_NAME, amISectionAdmin = false, @@ -485,6 +460,56 @@ method load*[T]( else: self.setActiveSection(activeSection) +method onChatsLoaded*[T]( + self: Module[T], + channelGroups: seq[ChannelGroupDto], + events: EventEmitter, + settingsService: settings_service.Service, + nodeConfigurationService: node_configuration_service.Service, + contactsService: contacts_service.Service, + chatService: chat_service.Service, + communityService: community_service.Service, + messageService: message_service.Service, + gifService: gif_service.Service, + mailserversService: mailservers_service.Service, +) = + var activeSection: SectionItem + var activeSectionId = singletonInstance.localAccountSensitiveSettings.getActiveSection() + if activeSectionId == "": + activeSectionId = singletonInstance.userProfile.getPubKey() + + for channelGroup in channelGroups: + self.channelGroupModules[channelGroup.id] = chat_section_module.newModule( + self, + events, + channelGroup.id, + isCommunity = channelGroup.channelGroupType == ChannelGroupType.Community, + settingsService, + nodeConfigurationService, + contactsService, + chatService, + communityService, + messageService, + gifService, + mailserversService + ) + let channelGroupItem = self.createChannelGroupItem(channelGroup) + self.view.model().addItem(channelGroupItem) + if activeSectionId == channelGroupItem.id: + activeSection = channelGroupItem + + self.channelGroupModules[channelGroup.id].load(channelGroup, events, settingsService, nodeConfigurationService, + contactsService, chatService, communityService, messageService, gifService, mailserversService) + + # Set active section if it is one of the channel sections + if not activeSection.isEmpty(): + self.setActiveSection(activeSection) + + self.view.chatsLoaded() + +method onChatsLoadingFailed*[T](self: Module[T]) = + self.view.chatsLoadingFailed() + proc checkIfModuleDidLoad [T](self: Module[T]) = if self.moduleLoaded: return diff --git a/src/app/modules/main/view.nim b/src/app/modules/main/view.nim index 91a9364007..0ca2e18bd5 100644 --- a/src/app/modules/main/view.nim +++ b/src/app/modules/main/view.nim @@ -14,6 +14,8 @@ QtObject: delegate: io_interface.AccessInterface model: section_model.SectionModel modelVariant: QVariant + chatsLoaded: bool + chatsLoadingFailed: bool activeSection: ActiveSection activeSectionVariant: QVariant chatSearchModel: chat_search_model.Model @@ -40,6 +42,8 @@ QtObject: result.QObject.setup result.delegate = delegate result.model = section_model.newModel() + result.chatsLoaded = false + result.chatsLoadingFailed = false result.modelVariant = newQVariant(result.model) result.activeSection = newActiveSection() result.activeSectionVariant = newQVariant(result.activeSection) @@ -155,6 +159,30 @@ QtObject: proc setCurrentUserStatus*(self: View, status: int) {.slot.} = self.delegate.setCurrentUserStatus(intToEnum(status, StatusType.Unknown)) + proc chatsLoadedChanged(self: View) {.signal.} + + proc chatsLoaded*(self: View) = + self.chatsLoaded = true + self.chatsLoadedChanged() + + proc getChatsLoaded(self: View): bool {.slot.} = + return self.chatsLoaded + QtProperty[bool] chatsLoaded: + read = getChatsLoaded + notify = chatsLoadedChanged + + proc chatsLoadingFailedChanged(self: View) {.signal.} + + proc chatsLoadingFailed*(self: View) = + self.chatsLoadingFailed = true + self.chatsLoadingFailedChanged() + + proc getChatsLoadingFailed(self: View): bool {.slot.} = + return self.chatsLoadingFailed + QtProperty[bool] chatsLoadingFailed: + read = getChatsLoadingFailed + notify = chatsLoadingFailedChanged + # Since we cannot return QVariant from the proc which has arguments, so cannot have proc like this: # prepareCommunitySectionModuleForCommunityId(self: View, communityId: string): QVariant {.slot.} # we're using combination of diff --git a/src/app_service/service/chat/async_tasks.nim b/src/app_service/service/chat/async_tasks.nim new file mode 100644 index 0000000000..392579f948 --- /dev/null +++ b/src/app_service/service/chat/async_tasks.nim @@ -0,0 +1,15 @@ +################################################# +# Async get chats (channel groups) +################################################# +type + AsyncGetChatsTaskArg = ref object of QObjectTaskArg + +const asyncGetChatsTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.} = + let arg = decode[AsyncGetChatsTaskArg](argEncoded) + + let response = status_chat.getChats() + + let responseJson = %*{ + "channelGroups": response.result + } + arg.finish(responseJson) diff --git a/src/app_service/service/chat/service.nim b/src/app_service/service/chat/service.nim index ee89f16cf6..5cef1d79e2 100644 --- a/src/app_service/service/chat/service.nim +++ b/src/app_service/service/chat/service.nim @@ -1,5 +1,7 @@ import NimQml, Tables, json, sequtils, strformat, chronicles, os, std/algorithm, strutils, uuids, base64 +import std/[times, os] +import ../../../app/core/tasks/[qt, threadpool] import ./dto/chat as chat_dto import ../message/dto/message as message_dto import ../activity_center/dto/notification as notification_dto @@ -18,24 +20,20 @@ from ../../common/account_constants import ZERO_ADDRESS export chat_dto - logScope: topics = "chat-service" include ../../common/json_utils +include ../../../app/core/tasks/common +include async_tasks type + ChannelGroupsArgs* = ref object of Args + channelGroups*: seq[ChannelGroupDto] + ChatUpdateArgs* = ref object of Args chats*: seq[ChatDto] messages*: seq[MessageDto] - # TODO refactor that part - # pinnedMessages*: seq[MessageDto] - # emojiReactions*: seq[Reaction] - # communities*: seq[Community] - # communityMembershipRequests*: seq[CommunityMembershipRequest] - activityCenterNotifications*: seq[ActivityCenterNotificationDto] - # statusUpdates*: seq[StatusUpdate] - # deletedMessages*: seq[RemovedMessage] CreatedChatArgs* = ref object of Args chat*: ChatDto @@ -85,6 +83,8 @@ type # Signals which may be emitted by this service: +const SIGNAL_CHATS_LOADED* = "chatsLoaded" +const SIGNAL_CHATS_LOADING_FAILED* = "chatsLoadingFailed" const SIGNAL_CHAT_UPDATE* = "chatUpdate" const SIGNAL_CHAT_LEFT* = "channelLeft" const SIGNAL_SENDING_FAILED* = "messageSendingFailed" @@ -105,19 +105,27 @@ const SIGNAL_CHAT_CREATED* = "chatCreated" QtObject: type Service* = ref object of QObject + threadpool: ThreadPool events: EventEmitter chats: Table[string, ChatDto] # [chat_id, ChatDto] channelGroups: OrderedTable[string, ChannelGroupDto] # [chatGroup_id, ChannelGroupDto] contactService: contact_service.Service proc delete*(self: Service) = - discard + self.QObject.delete - proc newService*(events: EventEmitter, contactService: contact_service.Service): Service = + proc newService*( + events: EventEmitter, + threadpool: ThreadPool, + contactService: contact_service.Service + ): Service = new(result, delete) + result.QObject.setup result.events = events + result.threadpool = threadpool result.contactService = contactService result.chats = initTable[string, ChatDto]() + result.channelGroups = initOrderedTable[string, ChannelGroupDto]() # Forward declarations proc updateOrAddChat*(self: Service, chat: ChatDto) @@ -152,19 +160,31 @@ QtObject: if (community.joined): self.updateOrAddChannelGroup(community.toChannelGroupDto()) + proc getChannelGroups*(self: Service): seq[ChannelGroupDto] = + return toSeq(self.channelGroups.values) + + proc asyncGetChats*(self: Service) = + let arg = AsyncGetChatsTaskArg( + tptr: cast[ByteAddress](asyncGetChatsTask), + vptr: cast[ByteAddress](self.vptr), + slot: "onAsyncGetChatsResponse", + ) + self.threadpool.start(arg) + proc sortPersonnalChatAsFirst[T, D](x, y: (T, D)): int = if (x[1].channelGroupType == Personal): return -1 if (y[1].channelGroupType == Personal): return 1 return 0 - proc init*(self: Service) = - self.doConnect() - + proc onAsyncGetChatsResponse*(self: Service, response: string) {.slot.} = try: - let response = status_chat.getChats() + let rpcResponseObj = response.parseJson + + if(rpcResponseObj["channelGroups"].kind == JNull): + raise newException(RpcException, "No channel groups returned") var chats: seq[ChatDto] = @[] - for (sectionId, section) in response.result.pairs: + for (sectionId, section) in rpcResponseObj["channelGroups"].pairs: var channelGroup = section.toChannelGroupDto() channelGroup.id = sectionId self.channelGroups[sectionId] = channelGroup @@ -181,13 +201,17 @@ QtObject: discard status_chat.deactivateChat(chat.id) else: self.chats[chat.id] = chat + + self.events.emit(SIGNAL_CHATS_LOADED, ChannelGroupsArgs(channelGroups: self.getChannelGroups())) except Exception as e: let errDesription = e.msg error "error: ", errDesription - return + self.events.emit(SIGNAL_CHATS_LOADING_FAILED, Args()) - proc getChannelGroups*(self: Service): seq[ChannelGroupDto] = - return toSeq(self.channelGroups.values) + proc init*(self: Service) = + self.doConnect() + + self.asyncGetChats() proc hasChannel*(self: Service, chatId: string): bool = self.chats.hasKey(chatId) diff --git a/ui/app/AppLayouts/Chat/views/ContactsColumnView.qml b/ui/app/AppLayouts/Chat/views/ContactsColumnView.qml index cabb8ecccc..ac1b088c4a 100644 --- a/ui/app/AppLayouts/Chat/views/ContactsColumnView.qml +++ b/ui/app/AppLayouts/Chat/views/ContactsColumnView.qml @@ -20,7 +20,7 @@ import "../popups/community" Item { id: root - width: 304 + width: Constants.chatSectionLeftColumnWidth height: parent.height // Important: diff --git a/ui/app/mainui/AppMain.qml b/ui/app/mainui/AppMain.qml index 36baac6062..bf0288d3e9 100644 --- a/ui/app/mainui/AppMain.qml +++ b/ui/app/mainui/AppMain.qml @@ -374,9 +374,13 @@ Item { height: 440 } - StatusStickersPopup { - id: statusStickersPopup - store: chatLayoutContainer.rootStore + Loader { + id: statusStickersPopupLoader + active: appMain.rootStore.mainModuleInst.chatsLoaded + sourceComponent: StatusStickersPopup { + id: statusStickersPopup + store: personalChatLayoutLoader.item.rootStore + } } StatusMainLayout { @@ -874,34 +878,78 @@ Item { // If we ever change stack layout component order we need to updade // Constants.appViewStackIndex accordingly - ChatLayout { - id: chatLayoutContainer + Loader { + id: personalChatLayoutLoader + sourceComponent: { + if (appMain.rootStore.mainModuleInst.chatsLoadingFailed) { + return errorStateComponent + } + if (appMain.rootStore.mainModuleInst.chatsLoaded) { + return personalChatLayoutComponent + } + return loadingStateComponent + } + + Component { + id: loadingStateComponent + Item { + anchors.fill: parent - chatView.emojiPopup: statusEmojiPopup - chatView.stickersPopup: statusStickersPopup - - contactsStore: appMain.rootStore.contactStore - rootStore.emojiReactionsModel: appMain.rootStore.emojiReactionsModel - rootStore.openCreateChat: createChatView.opened - - chatView.onProfileButtonClicked: { - Global.changeAppSectionBySectionType(Constants.appSection.profile); + Row { + anchors.centerIn: parent + spacing: 6 + StatusBaseText { + text: qsTr("Loading...") + } + LoadingAnimation {} + } + } + } + + Component { + id: errorStateComponent + Item { + anchors.fill: parent + StatusBaseText { + text: qsTr("Error loading chats, try closing the app and restarting") + anchors.centerIn: parent + } + } } - chatView.onOpenAppSearch: { - appSearch.openSearchPopup() - } + Component { + id: personalChatLayoutComponent - onImportCommunityClicked: { - Global.openPopup(communitiesPortalLayoutContainer.importCommunitiesPopup); - } + ChatLayout { + id: chatLayoutContainer - onCreateCommunityClicked: { - Global.openPopup(communitiesPortalLayoutContainer.createCommunitiesPopup); - } + chatView.emojiPopup: statusEmojiPopup + chatView.stickersPopup: statusStickersPopupLoader.item - Component.onCompleted: { - rootStore.chatCommunitySectionModule = appMain.rootStore.mainModuleInst.getChatSectionModule() + contactsStore: appMain.rootStore.contactStore + rootStore.emojiReactionsModel: appMain.rootStore.emojiReactionsModel + rootStore.openCreateChat: createChatView.opened + + chatView.onProfileButtonClicked: { + Global.changeAppSectionBySectionType(Constants.appSection.profile); + } + + chatView.onOpenAppSearch: { + appSearch.openSearchPopup() + } + + onImportCommunityClicked: { + Global.openPopup(communitiesPortalLayoutContainer.importCommunitiesPopup); + } + + onCreateCommunityClicked: { + Global.openPopup(communitiesPortalLayoutContainer.createCommunitiesPopup); + } + + Component.onCompleted: { + rootStore.chatCommunitySectionModule = appMain.rootStore.mainModuleInst.getChatSectionModule() + } + } } } @@ -975,7 +1023,7 @@ Item { sourceComponent: ChatLayout { chatView.emojiPopup: statusEmojiPopup - chatView.stickersPopup: statusStickersPopup + chatView.stickersPopup: statusStickersPopupLoader.item contactsStore: appMain.rootStore.contactStore rootStore.emojiReactionsModel: appMain.rootStore.emojiReactionsModel @@ -1006,7 +1054,7 @@ Item { id: createChatView property bool opened: false - active: opened + active: appMain.rootStore.mainModuleInst.chatsLoaded && opened asynchronous: true anchors.top: parent.top @@ -1014,12 +1062,14 @@ Item { anchors.rightMargin: 8 anchors.bottom: parent.bottom anchors.right: parent.right - width: parent.width - chatLayoutContainer.chatView.leftPanel.width - anchors.rightMargin - anchors.leftMargin + width: active ? + parent.width - Constants.chatSectionLeftColumnWidth - + anchors.rightMargin - anchors.leftMargin : 0 sourceComponent: CreateChatView { - rootStore: chatLayoutContainer.rootStore + rootStore: personalChatLayoutLoader.item.rootStore emojiPopup: statusEmojiPopup - stickersPopup: statusStickersPopup + stickersPopup: statusStickersPopupLoader.item } } } @@ -1076,7 +1126,7 @@ Item { x: parent.width - width - Style.current.smallPadding y: parent.y + _buttonSize height: appView.height - _buttonSize * 2 - store: chatLayoutContainer.rootStore + store: personalChatLayoutLoader.item.rootStore activityCenterStore: appMain.activityCenterStore } } diff --git a/ui/imports/utils/Constants.qml b/ui/imports/utils/Constants.qml index 55b10a930c..8dd3b1d6f6 100644 --- a/ui/imports/utils/Constants.qml +++ b/ui/imports/utils/Constants.qml @@ -291,6 +291,8 @@ QtObject { } } + readonly property int chatSectionLeftColumnWidth: 304 + readonly property QtObject appSection: QtObject { readonly property int chat: 0 readonly property int community: 1