From 21d2c00b40b7c03052b4c35245735c3304d5f020 Mon Sep 17 00:00:00 2001 From: mprakhov Date: Thu, 23 Mar 2023 10:38:09 +0100 Subject: [PATCH] feat(@desktop/chats): Keep only last 5 chats/channels in the memory --- src/app/global/global_singleton.nim | 9 ++++ src/app/global/loader_deactivator.nim | 45 +++++++++++++++++++ .../main/chat_section/io_interface.nim | 3 ++ src/app/modules/main/chat_section/item.nim | 15 ++++++- src/app/modules/main/chat_section/model.nim | 20 ++++++++- src/app/modules/main/chat_section/module.nim | 18 ++++++-- src/app/modules/main/chat_section/view.nim | 2 +- src/app/modules/main/io_interface.nim | 3 ++ src/app/modules/main/module.nim | 13 ++++-- .../modules/shared_models/section_item.nim | 10 +++++ .../modules/shared_models/section_model.nim | 21 +++++++-- .../AppLayouts/Chat/views/ChatColumnView.qml | 12 +---- ui/app/mainui/AppMain.qml | 9 +--- 13 files changed, 148 insertions(+), 32 deletions(-) create mode 100644 src/app/global/loader_deactivator.nim diff --git a/src/app/global/global_singleton.nim b/src/app/global/global_singleton.nim index 78a5010d53..10cf0e5c4c 100644 --- a/src/app/global/global_singleton.nim +++ b/src/app/global/global_singleton.nim @@ -6,6 +6,7 @@ import local_app_settings import user_profile import utils import global_events +import loader_deactivator export local_account_settings export local_account_sensitive_settings @@ -13,6 +14,7 @@ export local_app_settings export user_profile export utils export global_events +export loader_deactivator type GlobalSingleton = object @@ -63,9 +65,16 @@ proc globalEvents*(self: GlobalSingleton): GlobalEvents = globalEvents = newGlobalEvents() return globalEvents +proc loaderDeactivator*(self: GlobalSingleton): LoaderDeactivator = + var loaderDeactivator {.global.}: LoaderDeactivator + if (loaderDeactivator.isNil): + loaderDeactivator = newLoaderDeactivator() + return loaderDeactivator + proc delete*(self: GlobalSingleton) = self.engine.delete() self.localAccountSettings.delete() self.localAccountSensitiveSettings.delete() self.localAppSettings.delete() self.userProfile.delete() + self.loaderDeactivator.delete() diff --git a/src/app/global/loader_deactivator.nim b/src/app/global/loader_deactivator.nim new file mode 100644 index 0000000000..77dd01ca1a --- /dev/null +++ b/src/app/global/loader_deactivator.nim @@ -0,0 +1,45 @@ +import NimQml +import std/deques + +const MAX_CHATS_IN_MEMORY = 5 + +type + ChatInMemory = tuple + sectionId: string + chatId: string + +QtObject: + type LoaderDeactivator* = ref object of QObject + keepInMemory: Deque[ChatInMemory] + + proc setup(self: LoaderDeactivator) = + self.QObject.setup + self.keepInMemory = initDeque[ChatInMemory]() + + proc delete*(self: LoaderDeactivator) = + self.QObject.delete + + proc newLoaderDeactivator*(): + LoaderDeactivator = + new(result, delete) + result.setup + + proc newChatInMemory(sectionId, chatId: string): ChatInMemory = + (sectionId, chatId) + + proc unloadSection*(self: LoaderDeactivator, searchSectionId: string): bool = + if searchSectionId.len == 0: + return false + for (sectionId, _) in self.keepInMemory.items: + if sectionId == searchSectionId: + return false + return true + + proc addChatInMemory*(self: LoaderDeactivator, sectionId, chatId: string): ChatInMemory = + if self.keepInMemory.contains(newChatInMemory(sectionId, chatId)): + return + + self.keepInMemory.addFirst(newChatInMemory(sectionId, chatId)) + + if self.keepInMemory.len > MAX_CHATS_IN_MEMORY: + result = self.keepInMemory.popLast() \ No newline at end of file diff --git a/src/app/modules/main/chat_section/io_interface.nim b/src/app/modules/main/chat_section/io_interface.nim index 49faf5edb7..8d884a3a24 100644 --- a/src/app/modules/main/chat_section/io_interface.nim +++ b/src/app/modules/main/chat_section/io_interface.nim @@ -374,3 +374,6 @@ method onUserAuthenticated*(self: AccessInterface, pin: string, password: string method requestToJoinCommunity*(self: AccessInterface, communityId: string, ensName: string) = raise newException(ValueError, "No implementation available") + +method onDeactivateChatLoader*(self: AccessInterface, chatId: string) = + raise newException(ValueError, "No implementation available") diff --git a/src/app/modules/main/chat_section/item.nim b/src/app/modules/main/chat_section/item.nim index baf73ca089..81bd3ae3a1 100644 --- a/src/app/modules/main/chat_section/item.nim +++ b/src/app/modules/main/chat_section/item.nim @@ -32,6 +32,7 @@ type highlight: bool trustStatus: TrustStatus onlineStatus: OnlineStatus + loaderActive: bool proc initItem*( id, @@ -56,7 +57,8 @@ proc initItem*( highlight: bool = false, categoryOpened: bool = true, trustStatus: TrustStatus = TrustStatus.Unknown, - onlineStatus = OnlineStatus.Inactive + onlineStatus = OnlineStatus.Inactive, + loaderActive = false ): Item = result = Item() result.id = id @@ -83,6 +85,7 @@ proc initItem*( result.categoryOpened = categoryOpened result.trustStatus = trustStatus result.onlineStatus = onlineStatus + result.loaderActive = loaderActive proc `$`*(self: Item): string = result = fmt"""chat_section/Item( @@ -108,6 +111,7 @@ proc `$`*(self: Item): string = categoryOpened: {$self.categoryOpened}, trustStatus: {$self.trustStatus}, onlineStatus: {$self.onlineStatus}, + loaderActive: {$self.loaderActive}, ]""" proc toJsonNode*(self: Item): JsonNode = @@ -132,7 +136,8 @@ proc toJsonNode*(self: Item): JsonNode = "highlight": self.highlight, "categoryOpened": self.categoryOpened, "trustStatus": self.trustStatus, - "onlineStatus": self.onlineStatus + "onlineStatus": self.onlineStatus, + "loaderActive": self.loaderActive } proc delete*(self: Item) = @@ -263,3 +268,9 @@ proc `onlineStatus=`*(self: var Item, value: OnlineStatus) = proc setHasUnreadMessages*(self: Item, value: bool) = self.hasUnreadMessages = value + +proc loaderActive*(self: Item): bool = + self.loaderActive + +proc `loaderActive=`*(self: var Item, value: bool) = + self.loaderActive = value \ No newline at end of file diff --git a/src/app/modules/main/chat_section/model.nim b/src/app/modules/main/chat_section/model.nim index 79d8289211..bdec58c615 100644 --- a/src/app/modules/main/chat_section/model.nim +++ b/src/app/modules/main/chat_section/model.nim @@ -30,6 +30,7 @@ type TrustStatus OnlineStatus IsCategory + LoaderActive QtObject: type @@ -97,6 +98,7 @@ QtObject: ModelRole.TrustStatus.int:"trustStatus", ModelRole.OnlineStatus.int:"onlineStatus", ModelRole.IsCategory.int:"isCategory", + ModelRole.LoaderActive.int:"loaderActive", }.toTable method data(self: Model, index: QModelIndex, role: int): QVariant = @@ -158,6 +160,8 @@ QtObject: result = newQVariant(item.onlineStatus.int) of ModelRole.IsCategory: result = newQVariant(item.`type` == CATEGORY_TYPE) + of ModelRole.LoaderActive: + result = newQVariant(item.loaderActive) proc appendItem*(self: Model, item: Item) = let parentModelIndex = newQModelIndex() @@ -236,7 +240,11 @@ QtObject: let index = self.createIndex(i, 0, nil) # Set active channel to true and others to false self.items[i].active = isChannelToSetActive - self.dataChanged(index, index, @[ModelRole.Active.int]) + if (isChannelToSetActive): + self.items[i].loaderActive = true + self.dataChanged(index, index, @[ModelRole.Active.int, ModelRole.LoaderActive.int]) + else: + self.dataChanged(index, index, @[ModelRole.Active.int]) proc changeMutedOnItemById*(self: Model, id: string, muted: bool) = let index = self.getItemIdxById(id) @@ -501,3 +509,13 @@ QtObject: return return self.items[index].toJsonNode() + + proc disableChatLoader*(self: Model, chatId: string) = + let index = self.getItemIdxById(chatId) + if index == -1: + return + + self.items[index].loaderActive = false + self.items[index].active = false + let modelIndex = self.createIndex(index, 0, nil) + self.dataChanged(modelIndex, modelIndex, @[ModelRole.Active.int, ModelRole.LoaderActive.int]) \ No newline at end of file diff --git a/src/app/modules/main/chat_section/module.nim b/src/app/modules/main/chat_section/module.nim index d2511ebc8a..c21415f0c0 100644 --- a/src/app/modules/main/chat_section/module.nim +++ b/src/app/modules/main/chat_section/module.nim @@ -226,7 +226,7 @@ proc buildChatSectionUI( var isActive = false # restore on a startup last open channel for the section or # make the first channel which doesn't belong to any category active - if selectedItemId.len == 0 or chatDto.id == sectionLastOpenChat: + if (selectedItemId.len == 0 and sectionLastOpenChat.len == 0) or chatDto.id == sectionLastOpenChat: selectedItemId = chatDto.id isActive = true @@ -258,6 +258,7 @@ proc buildChatSectionUI( colorId, colorHash, onlineStatus = onlineStatus, + loaderActive = isActive ) self.view.chatsModel().appendItem(newChatItem) @@ -477,9 +478,12 @@ method activeItemSet*(self: Module, itemId: string) = # save last open chat in settings for restore on the next app launch singletonInstance.localAccountSensitiveSettings.setSectionLastOpenChat(mySectionId, activeChatId) + + let (deactivateSectionId, deactivateChatId) = singletonInstance.loaderDeactivator.addChatInMemory(mySectionId, activeChatId) # notify parent module about active chat/channel self.delegate.onActiveChatChange(mySectionId, activeChatId) + self.delegate.onDeactivateSectionAndChatLoader(deactivateSectionId, deactivateChatId) method getModuleAsVariant*(self: Module): QVariant = return self.viewVariant @@ -591,6 +595,7 @@ method addNewChat*( colorHash, chatDto.highlight, onlineStatus = onlineStatus, + loaderActive = setChatAsActive ) self.addSubmodule( chatDto.id, @@ -667,8 +672,12 @@ method setFirstChannelAsActive*(self: Module) = if(self.view.chatsModel().getCount() == 0): self.setActiveItem("") return - let chat_item = self.view.chatsModel().getItemAtIndex(0) - self.setActiveItem(chat_item.id) + + let chat_items = self.view.chatsModel().items() + for chat_item in chat_items: + if chat_item.`type` != CATEGORY_TYPE: + self.setActiveItem(chat_item.id) + break method onReorderChat*(self: Module, chatId: string, position: int, newCategoryIdForChat: string, prevCategoryId: string, prevCategoryDeleted: bool) = var newCategoryName = "" @@ -1222,3 +1231,6 @@ proc buildTokenPermissionItem*(self: Module, tokenPermission: CommunityTokenPerm return tokenPermissionItem +method onDeactivateChatLoader*(self: Module, chatId: string) = + self.view.chatsModel().disableChatLoader(chatId) + diff --git a/src/app/modules/main/chat_section/view.nim b/src/app/modules/main/chat_section/view.nim index 39ea2802a4..39bfd514d6 100644 --- a/src/app/modules/main/chat_section/view.nim +++ b/src/app/modules/main/chat_section/view.nim @@ -435,4 +435,4 @@ QtObject: QtProperty[bool] allTokenRequirementsMet: read = getAllTokenRequirementsMet - notify = allTokenRequirementsMetChanged + notify = allTokenRequirementsMetChanged \ No newline at end of file diff --git a/src/app/modules/main/io_interface.nim b/src/app/modules/main/io_interface.nim index faa8599094..c87bcbe95f 100644 --- a/src/app/modules/main/io_interface.nim +++ b/src/app/modules/main/io_interface.nim @@ -298,6 +298,9 @@ method onAcceptRequestToJoinLoading*(self: AccessInterface, communityId: string, method onAcceptRequestToJoinSuccess*(self: AccessInterface, communityId: string, memberKey: string, requestId: string) {.base.} = raise newException(ValueError, "No implementation available") +method onDeactivateSectionAndChatLoader*(self: AccessInterface, sectionId: string, chatId: string) {.base.} = + raise newException(ValueError, "No implementation available") + # This way (using concepts) is used only for the modules managed by AppController type DelegateInterface* = concept c diff --git a/src/app/modules/main/module.nim b/src/app/modules/main/module.nim index d4bfc8eddc..1bfa1b551e 100644 --- a/src/app/modules/main/module.nim +++ b/src/app/modules/main/module.nim @@ -369,7 +369,8 @@ proc createChannelGroupItem[T](self: Module[T], c: ChannelGroupDto): SectionItem ) ) else: @[], c.encrypted, - communityTokensItems + communityTokensItems, + loaderActive = active ) method load*[T]( @@ -733,8 +734,8 @@ method setActiveSection*[T](self: Module[T], item: SectionItem, skipSavingInSett self.controller.setActiveSection(item.id, skipSavingInSettings) method setActiveSectionById*[T](self: Module[T], id: string) = - let item = self.view.model().getItemById(id) - self.setActiveSection(item) + let item = self.view.model().getItemById(id) + self.setActiveSection(item) proc notifySubModulesAboutChange[T](self: Module[T], sectionId: string) = for cModule in self.channelGroupModules.values: @@ -1187,3 +1188,9 @@ method onDisplayKeycardSharedModuleFlow*[T](self: Module[T]) = method activateStatusDeepLink*[T](self: Module[T], statusDeepLink: string) = let linkToActivate = self.urlsManager.convertExternalLinkToInternal(statusDeepLink) self.urlsManager.onUrlActivated(linkToActivate) + +method onDeactivateSectionAndChatLoader*[T](self: Module[T], sectionId: string, chatId: string) = + if (sectionId.len > 0 and self.channelGroupModules.contains(sectionId)): + self.channelGroupModules[sectionId].onDeactivateChatLoader(chatId) + if (singletonInstance.loaderDeactivator.unloadSection(sectionId)): + self.view.model().disableSectionLoader(sectionId) \ No newline at end of file diff --git a/src/app/modules/shared_models/section_item.nim b/src/app/modules/shared_models/section_item.nim index effd95a3f7..a002da2932 100644 --- a/src/app/modules/shared_models/section_item.nim +++ b/src/app/modules/shared_models/section_item.nim @@ -55,6 +55,7 @@ type declinedMemberRequestsModel: member_model.Model encrypted: bool communityTokensModel: community_tokens_model.TokenModel + loaderActive: bool proc initItem*( id: string, @@ -91,6 +92,7 @@ proc initItem*( declinedMemberRequests: seq[MemberItem] = @[], encrypted: bool = false, communityTokens: seq[TokenItem] = @[], + loaderActive = false, ): SectionItem = result.id = id result.sectionType = sectionType @@ -132,6 +134,7 @@ proc initItem*( result.encrypted = encrypted result.communityTokensModel = newTokenModel() result.communityTokensModel.setItems(communityTokens) + result.loaderActive = loaderActive proc isEmpty*(self: SectionItem): bool = return self.id.len == 0 @@ -171,6 +174,7 @@ proc `$`*(self: SectionItem): string = declinedMemberRequests:{self.declinedMemberRequestsModel}, encrypted:{self.encrypted}, communityTokensModel:{self.communityTokensModel}, + loaderActive:{self.loaderActive}, ]""" proc id*(self: SectionItem): string {.inline.} = @@ -322,3 +326,9 @@ proc communityTokens*(self: SectionItem): community_tokens_model.TokenModel {.in proc updatePendingRequestLoadingState*(self: SectionItem, memberKey: string, loading: bool) {.inline.} = self.pendingMemberRequestsModel.updateLoadingState(memberKey, loading) + +proc loaderActive*(self: SectionItem): bool {.inline.} = + self.loaderActive + +proc `loaderActive=`*(self: var SectionItem, value: bool) {.inline.} = + self.loaderActive = value diff --git a/src/app/modules/shared_models/section_model.nim b/src/app/modules/shared_models/section_model.nim index d8051d2be3..a9cff66ad5 100644 --- a/src/app/modules/shared_models/section_model.nim +++ b/src/app/modules/shared_models/section_model.nim @@ -43,6 +43,7 @@ type PendingMemberRequestsModel DeclinedMemberRequestsModel AmIBanned + LoaderActive QtObject: type @@ -114,7 +115,8 @@ QtObject: ModelRole.CommunityTokensModel.int:"communityTokens", ModelRole.PendingMemberRequestsModel.int:"pendingMemberRequests", ModelRole.DeclinedMemberRequestsModel.int:"declinedMemberRequests", - ModelRole.AmIBanned.int:"amIBanned" + ModelRole.AmIBanned.int:"amIBanned", + ModelRole.LoaderActive.int:"loaderActive" }.toTable method data(self: SectionModel, index: QModelIndex, role: int): QVariant = @@ -198,6 +200,8 @@ QtObject: result = newQVariant(item.declinedMemberRequests) of ModelRole.AmIBanned: result = newQVariant(item.amIBanned) + of ModelRole.LoaderActive: + result = newQVariant(item.loaderActive) proc isItemExist(self: SectionModel, id: string): bool = for it in self.items: @@ -293,7 +297,8 @@ QtObject: ModelRole.CommunityTokensModel.int, ModelRole.PendingMemberRequestsModel.int, ModelRole.DeclinedMemberRequestsModel.int, - ModelRole.AmIBanned.int + ModelRole.AmIBanned.int, + ModelRole.LoaderActive.int ]) proc getNthEnabledItem*(self: SectionModel, nth: int): SectionItem = @@ -331,7 +336,9 @@ QtObject: if(self.items[i].id == id): let index = self.createIndex(i, 0, nil) self.items[i].active = true - self.dataChanged(index, index, @[ModelRole.Active.int]) + self.items[i].loaderActive = true + + self.dataChanged(index, index, @[ModelRole.Active.int, ModelRole.LoaderActive.int]) proc sectionVisibilityUpdated*(self: SectionModel) {.signal.} @@ -428,5 +435,13 @@ QtObject: "ensOnly": item.ensOnly, "nbMembers": item.members.getCount(), "encrypted": item.encrypted, + "loaderActive": item.loaderActive, } return $jsonObj + + proc disableSectionLoader*(self: SectionModel, sectionId: string) = + for i in 0 ..< self.items.len: + if(self.items[i].id == sectionId): + let index = self.createIndex(i, 0, nil) + self.items[i].loaderActive = false + self.dataChanged(index, index, @[ModelRole.LoaderActive.int]) \ No newline at end of file diff --git a/ui/app/AppLayouts/Chat/views/ChatColumnView.qml b/ui/app/AppLayouts/Chat/views/ChatColumnView.qml index dee64d0359..13f5061a0f 100644 --- a/ui/app/AppLayouts/Chat/views/ChatColumnView.qml +++ b/ui/app/AppLayouts/Chat/views/ChatColumnView.qml @@ -124,21 +124,11 @@ Item { id: chatLoader // Channels/chats are not loaded by default and only load when first put active - active: false + active: model.loaderActive width: parent.width height: parent.height visible: model.active - // Removing the binding in order not to unload the content: - // It is done for keeping: - // - the last channel/chat scroll position - // - the last typed but not sent text - Binding on active { - when: !chatLoader.active - restoreMode: Binding.RestoreNone - value: model.itemId && root.isSectionActive && (model.itemId === root.activeChatId || model.itemId === root.activeSubItemId) - } - sourceComponent: ChatContentView { visible: !root.rootStore.openCreateChat && isActiveChannel rootStore: root.rootStore diff --git a/ui/app/mainui/AppMain.qml b/ui/app/mainui/AppMain.qml index d60c1ffa77..93d2943340 100644 --- a/ui/app/mainui/AppMain.qml +++ b/ui/app/mainui/AppMain.qml @@ -977,7 +977,7 @@ Item { readonly property string sectionId: model.id asynchronous: true - active: sectionId === appMain.rootStore.mainModuleInst.activeSection.id + active: model.loaderActive Layout.fillWidth: true Layout.alignment: Qt.AlignLeft | Qt.AlignTop @@ -1015,13 +1015,6 @@ Item { onOpenAppSearch: { appSearch.openSearchPopup() } - - Component.onCompleted: { - // Do not unload section data from the memory in order not - // to reset scroll, not send text input and etc during the - // sections switching - communityLoader.active = true - } } } }