From 0bb2bc0e03ba9b9701d04af0542c1cfca8059285 Mon Sep 17 00:00:00 2001 From: Jonathan Rainville Date: Thu, 10 Oct 2024 13:01:47 -0400 Subject: [PATCH] Fix some of the freezes experienced by the admins when the community updates (#16384) * fix: force focus on search inputs in permissions and sort members * fix(admin): fix freezes when community gets updated * fix(airdrop): use FastExpressionFilter to speed up Iterates #16043 When the community gets updated by any means, we reconstruct the section item, which is already bad, but we also re-fetch all tokens and all shared addresses, which in turn re-updates models for no reason. Instead, I make sure to only fetch those on first section build, then, I get the new shared addresses when members join using the request to join response that comes from status-go --- src/app/modules/main/controller.nim | 7 +++ src/app/modules/main/io_interface.nim | 3 + src/app/modules/main/module.nim | 57 +++++++++++++++---- .../modules/shared_models/member_model.nim | 17 ++++++ .../modules/shared_models/section_model.nim | 4 +- .../service/community/dto/community.nim | 55 ++++++++++-------- src/app_service/service/community/service.nim | 47 ++++++++------- .../Communities/popups/InDropdown.qml | 5 +- .../Communities/popups/MembersDropdown.qml | 1 + .../Communities/views/EditAirdropView.qml | 25 ++++---- 10 files changed, 149 insertions(+), 72 deletions(-) diff --git a/src/app/modules/main/controller.nim b/src/app/modules/main/controller.nim index 3f32ea0201..3cf34e2e5a 100644 --- a/src/app/modules/main/controller.nim +++ b/src/app/modules/main/controller.nim @@ -309,6 +309,10 @@ proc init*(self: Controller) = var args = CommunityRequestArgs(e) self.delegate.newCommunityMembershipRequestReceived(args.communityRequest) + self.events.on(SIGNAL_NEW_REQUEST_TO_JOIN_COMMUNITY_ACCEPTED) do(e: Args): + var args = CommunityRequestArgs(e) + self.delegate.communityMemberRevealedAccountsAdded(args.communityRequest) + self.events.on(SIGNAL_NETWORK_CONNECTED) do(e: Args): self.delegate.onNetworkConnected() @@ -595,6 +599,9 @@ proc getColorId*(self: Controller, pubkey: string): int = proc asyncGetRevealedAccountsForAllMembers*(self: Controller, communityId: string) = self.communityService.asyncGetRevealedAccountsForAllMembers(communityId) +proc asyncGetRevealedAccountsForMember*(self: Controller, communityId, memberPubkey: string) = + self.communityService.asyncGetRevealedAccountsForMember(communityId, memberPubkey) + proc asyncReevaluateCommunityMembersPermissions*(self: Controller, communityId: string) = self.communityService.asyncReevaluateCommunityMembersPermissions(communityId) diff --git a/src/app/modules/main/io_interface.nim b/src/app/modules/main/io_interface.nim index e36cd2c807..70b3d3684b 100644 --- a/src/app/modules/main/io_interface.nim +++ b/src/app/modules/main/io_interface.nim @@ -391,6 +391,9 @@ method windowDeactivated*(self: AccessInterface) {.base.} = method communityMembersRevealedAccountsLoaded*(self: AccessInterface, communityId: string, membersRevealedAccounts: MembersRevealedAccounts) {.base.} = raise newException(ValueError, "No implementation available") +method communityMemberRevealedAccountsAdded*(self: AccessInterface, request: CommunityMembershipRequestDto) {.base.} = + raise newException(ValueError, "No implementation available") + ## Used in test env only, for testing keycard flows method registerMockedKeycard*(self: AccessInterface, cardIndex: int, readerState: int, keycardState: int, mockedKeycard: string, mockedKeycardHelper: string) {.base.} = diff --git a/src/app/modules/main/module.nim b/src/app/modules/main/module.nim index a8292c81f2..7f681c93c1 100644 --- a/src/app/modules/main/module.nim +++ b/src/app/modules/main/module.nim @@ -315,21 +315,24 @@ method onCommunityTokensDetailsLoaded[T](self: Module[T], communityId: string, ) self.view.model().setTokenItems(communityId, communityTokensItems) -proc createCommunitySectionItem[T](self: Module[T], communityDetails: CommunityDto): SectionItem = +proc createCommunitySectionItem[T](self: Module[T], communityDetails: CommunityDto, isEdit: bool = false): SectionItem = var communityTokensItems: seq[TokenItem] + var communityMembersAirdropAddress: Table[string, string] + let existingCommunity = self.view.model().getItemById(communityDetails.id) if communityDetails.memberRole == MemberRole.Owner or communityDetails.memberRole == MemberRole.TokenMaster: - self.controller.getCommunityTokensDetailsAsync(communityDetails.id) + if not isEdit: + # When first creating the section, we load the community tokens and the members revealed accounts + self.controller.getCommunityTokensDetailsAsync(communityDetails.id) - # Get community members' revealed accounts - # We will update the model later when we finish loading the accounts - self.controller.asyncGetRevealedAccountsForAllMembers(communityDetails.id) + # Get community members' revealed accounts + # We will update the model later when we finish loading the accounts + self.controller.asyncGetRevealedAccountsForAllMembers(communityDetails.id) # If there are tokens already in the model, we should keep the existing community tokens, until # getCommunityTokensDetailsAsync will trigger onCommunityTokensDetailsLoaded - let existingCommunity = self.view.model().getItemById(communityDetails.id) if not existingCommunity.isEmpty() and not existingCommunity.communityTokens.isNil: - communityTokensItems = existingCommunity.communityTokens.items + communityTokensItems = existingCommunity.communityTokens.items let (unviewedCount, notificationsCount) = self.controller.sectionUnreadMessagesAndMentionsCount( communityDetails.id, @@ -394,8 +397,16 @@ proc createCommunitySectionItem[T](self: Module[T], communityDetails: CommunityD state = memberState elif not member.joined: state = MembershipRequestState.AwaitingAddress - - result = self.createMemberItem(member.id, "", state, member.role) + var airdropAddress = "" + if not existingCommunity.isEmpty() and not existingCommunity.communityTokens.isNil: + airdropAddress = existingCommunity.members.getAirdropAddressForMember(member.id) + result = self.createMemberItem( + member.id, + requestId = "", + state, + member.role, + airdropAddress, + ) ), # pendingRequestsToJoin communityDetails.pendingRequestsToJoin.map(x => pending_request_item.initItem( @@ -1093,7 +1104,7 @@ method communityEdited*[T]( community: CommunityDto) = if(not self.chatSectionModules.contains(community.id)): return - var communitySectionItem = self.createCommunitySectionItem(community) + var communitySectionItem = self.createCommunitySectionItem(community, isEdit = true) # We need to calculate the unread counts because the community update doesn't come with it let (unviewedMessagesCount, unviewedMentionsCount) = self.controller.sectionUnreadMessagesAndMentionsCount( communitySectionItem.id, @@ -1667,6 +1678,20 @@ method communityMembersRevealedAccountsLoaded*[T](self: Module[T], communityId: self.view.model.setMembersAirdropAddress(communityId, communityMembersAirdropAddress) +method communityMemberRevealedAccountsAdded*[T](self: Module[T], request: CommunityMembershipRequestDto) = + var airdropAddress = "" + for revealedAccount in request.revealedAccounts: + if revealedAccount.isAirdropAddress: + airdropAddress = revealedAccount.address + discard + + if airdropAddress == "": + warn "Request to join doesn't have an airdrop address", requestId = request.id, communityId = request.communityId + return + + let communityMembersAirdropAddress = {request.publicKey: airdropAddress}.toTable + self.view.model.setMembersAirdropAddress(request.communityId, communityMembersAirdropAddress) + ## Used in test env only, for testing keycard flows method registerMockedKeycard*[T](self: Module[T], cardIndex: int, readerState: int, keycardState: int, mockedKeycard: string, mockedKeycardHelper: string) = @@ -1700,7 +1725,14 @@ method updateRequestToJoinState*[T](self: Module[T], sectionId: string, requestT if sectionId in self.chatSectionModules: self.chatSectionModules[sectionId].updateRequestToJoinState(requestToJoinState) -proc createMemberItem*[T](self: Module[T], memberId: string, requestId: string, state: MembershipRequestState, role: MemberRole): MemberItem = +proc createMemberItem*[T]( + self: Module[T], + memberId: string, + requestId: string, + state: MembershipRequestState, + role: MemberRole, + airdropAddress: string = "", + ): MemberItem = let contactDetails = self.controller.getContactDetails(memberId) let status = self.controller.getStatusForContactWithId(memberId) return initMemberItem( @@ -1718,7 +1750,8 @@ proc createMemberItem*[T](self: Module[T], memberId: string, requestId: string, isVerified = contactDetails.dto.isContactVerified(), memberRole = role, membershipRequestState = state, - requestToJoinId = requestId + requestToJoinId = requestId, + airdropAddress = airdropAddress, ) {.pop.} diff --git a/src/app/modules/shared_models/member_model.nim b/src/app/modules/shared_models/member_model.nim index 61d9dcaaa6..4aa0990c04 100644 --- a/src/app/modules/shared_models/member_model.nim +++ b/src/app/modules/shared_models/member_model.nim @@ -396,6 +396,13 @@ QtObject: ModelRole.AirdropAddress.int ]) + proc getAirdropAddressForMember*(self: Model, pubKey: string): string = + let idx = self.findIndexForMember(pubKey) + if idx == -1: + return "" + + return self.items[idx].airdropAddress + # TODO: rename me to removeItemByPubkey proc removeItemById*(self: Model, pubKey: string) = let ind = self.findIndexForMember(pubKey) @@ -437,3 +444,13 @@ QtObject: self.dataChanged(index, index, @[ ModelRole.MembershipRequestState.int ]) + + proc getNewMembers*(self: Model, pubkeys: seq[string]): seq[string] = + for pubkey in pubkeys: + var found = false + for item in self.items: + if item.pubKey == pubkey: + found = true + break + if not found: + result.add(pubkey) diff --git a/src/app/modules/shared_models/section_model.nim b/src/app/modules/shared_models/section_model.nim index 355584add0..2d85c82f46 100644 --- a/src/app/modules/shared_models/section_model.nim +++ b/src/app/modules/shared_models/section_model.nim @@ -473,8 +473,8 @@ QtObject: if (index == -1): return - for pubkey, revealedAccounts in communityMembersAirdropAddress.pairs: - self.items[index].members.setAirdropAddress(pubkey, revealedAccounts) + for pubkey, airdropAddress in communityMembersAirdropAddress.pairs: + self.items[index].members.setAirdropAddress(pubkey, airdropAddress) proc setTokenItems*(self: SectionModel, id: string, communityTokensItems: seq[TokenItem]) = let index = self.getItemIndex(id) diff --git a/src/app_service/service/community/dto/community.nim b/src/app_service/service/community/dto/community.nim index 211108f318..69b5b8b410 100644 --- a/src/app_service/service/community/dto/community.nim +++ b/src/app_service/service/community/dto/community.nim @@ -48,6 +48,13 @@ type proc isBanned*(state: CommunityMemberPendingBanOrKick): bool = return state == CommunityMemberPendingBanOrKick.Banned or state == CommunityMemberPendingBanOrKick.BannedWithAllMessagesDelete +type RevealedAccount* = object + address*: string + chainIds*: seq[int] + isAirdropAddress*: bool + +type MembersRevealedAccounts* = Table[string, seq[RevealedAccount]] + type CommunityMembershipRequestDto* = object id*: string publicKey*: string @@ -55,6 +62,7 @@ type CommunityMembershipRequestDto* = object communityId*: string state*: int our*: string #FIXME: should be bool + revealedAccounts*: seq[RevealedAccount] type CommunitySettingsDto* = object id*: string @@ -126,13 +134,6 @@ type CommunityMetricsDto* = object metricsType*: CommunityMetricsType intervals*: seq[MetricsIntervalDto] -type RevealedAccount* = object - address*: string - chainIds*: seq[int] - isAirdropAddress*: bool - -type MembersRevealedAccounts* = Table[string, seq[RevealedAccount]] - type CommunityDto* = object id*: string memberRole*: MemberRole @@ -388,6 +389,24 @@ proc toCommunityMetricsDto*(jsonObj: JsonNode): CommunityMetricsDto = for interval in intervalsObj: result.intervals.add(interval.toMetricsIntervalDto) +proc toRevealedAccount*(revealedAccountObj: JsonNode): RevealedAccount = + var chainIdsObj: JsonNode + var chainIds: seq[int] = @[] + if revealedAccountObj.getProp("chain_ids", chainIdsObj): + for chainIdObj in chainIdsObj: + chainIds.add(chainIdObj.getInt) + + result = RevealedAccount( + address: revealedAccountObj["address"].getStr, + chainIds: chainIds, + isAirdropAddress: revealedAccountObj{"isAirdropAddress"}.getBool, + ) + +proc toRevealedAccounts*(revealedAccountsObj: JsonNode): seq[RevealedAccount] = + result = @[] + for revealedAccountObj in revealedAccountsObj: + result.add(revealedAccountObj.toRevealedAccount()) + proc toCommunityMembershipRequestDto*(jsonObj: JsonNode): CommunityMembershipRequestDto = result = CommunityMembershipRequestDto() discard jsonObj.getProp("id", result.id) @@ -397,6 +416,10 @@ proc toCommunityMembershipRequestDto*(jsonObj: JsonNode): CommunityMembershipReq discard jsonObj.getProp("communityId", result.communityId) discard jsonObj.getProp("our", result.our) + var revealedAccountObj: JsonNode + if(jsonObj.getProp("revealedAccounts", revealedAccountObj)): + result.revealedAccounts = toRevealedAccounts(revealedAccountObj) + proc toCollapsedCategoryDto*(jsonObj: JsonNode, isCollapsed: bool = false): Category = result = Category() discard jsonObj.getProp("categoryId", result.id) @@ -579,24 +602,6 @@ proc parseDiscordChannels*(response: JsonNode): seq[DiscordChannelDto] = for channel in response["discordChannels"].items(): result.add(channel.toDiscordChannelDto()) -proc toRevealedAccount*(revealedAccountObj: JsonNode): RevealedAccount = - var chainIdsObj: JsonNode - var chainIds: seq[int] = @[] - if revealedAccountObj.getProp("chain_ids", chainIdsObj): - for chainIdObj in chainIdsObj: - chainIds.add(chainIdObj.getInt) - - result = RevealedAccount( - address: revealedAccountObj["address"].getStr, - chainIds: chainIds, - isAirdropAddress: revealedAccountObj{"isAirdropAddress"}.getBool, - ) - -proc toRevealedAccounts*(revealedAccountsObj: JsonNode): seq[RevealedAccount] = - result = @[] - for revealedAccountObj in revealedAccountsObj: - result.add(revealedAccountObj.toRevealedAccount()) - proc toMembersRevealedAccounts*(membersRevealedAccountsObj: JsonNode): MembersRevealedAccounts = result = initTable[string, seq[RevealedAccount]]() for (pubkey, revealedAccountsObj) in membersRevealedAccountsObj.pairs: diff --git a/src/app_service/service/community/service.nim b/src/app_service/service/community/service.nim index e67bd4d96a..ea8caa206a 100644 --- a/src/app_service/service/community/service.nim +++ b/src/app_service/service/community/service.nim @@ -205,6 +205,7 @@ const SIGNAL_COMMUNITY_MEMBER_STATUS_CHANGED* = "communityMemberStatusChanged" const SIGNAL_COMMUNITY_MEMBERS_CHANGED* = "communityMembersChanged" const SIGNAL_COMMUNITY_KICKED* = "communityKicked" const SIGNAL_NEW_REQUEST_TO_JOIN_COMMUNITY* = "newRequestToJoinCommunity" +const SIGNAL_NEW_REQUEST_TO_JOIN_COMMUNITY_ACCEPTED* = "requestToJoinCommunityAccepted" const SIGNAL_REQUEST_TO_JOIN_COMMUNITY_CANCELED* = "requestToJoinCommunityCanceled" const SIGNAL_WAITING_ON_NEW_COMMUNITY_OWNER_TO_CONFIRM_REQUEST_TO_REJOIN* = "waitingOnNewCommunityOwnerToConfirmRequestToRejoin" const SIGNAL_CURATED_COMMUNITY_FOUND* = "curatedCommunityFound" @@ -769,29 +770,31 @@ QtObject: proc handleCommunitiesRequestsToJoin(self: Service, membershipRequests: seq[CommunityMembershipRequestDto]) = for membershipRequest in membershipRequests: - if (not self.communities.contains(membershipRequest.communityId)): - error "Received a membership request for an unknown community", communityId=membershipRequest.communityId - continue + if (not self.communities.contains(membershipRequest.communityId)): + error "Received a membership request for an unknown community", communityId=membershipRequest.communityId + continue - let requestToJoinState = RequestToJoinType(membershipRequest.state) - let noAwaitingIndex = self.getWaitingForSharedAddressesRequestIndex(membershipRequest.communityId, membershipRequest.id) == -1 - if requestToJoinState == RequestToJoinType.AwaitingAddress and noAwaitingIndex: - self.communities[membershipRequest.communityId].waitingForSharedAddressesRequestsToJoin.add(membershipRequest) - let myPublicKey = singletonInstance.userProfile.getPubKey() - if myPublicKey == membershipRequest.publicKey: - self.events.emit(SIGNAL_WAITING_ON_NEW_COMMUNITY_OWNER_TO_CONFIRM_REQUEST_TO_REJOIN, CommunityIdArgs(communityId: membershipRequest.communityId)) - elif RequestToJoinType.Pending == requestToJoinState and self.getPendingRequestIndex(membershipRequest.communityId, membershipRequest.id) == -1: - self.communities[membershipRequest.communityId].pendingRequestsToJoin.add(membershipRequest) - self.events.emit(SIGNAL_NEW_REQUEST_TO_JOIN_COMMUNITY, - CommunityRequestArgs(communityRequest: membershipRequest)) - else: - try: - self.updateMembershipRequestToNewState(membershipRequest.communityId, membershipRequest.id, self.communities[membershipRequest.communityId], - requestToJoinState) - except Exception as e: - error "Unknown request", msg = e.msg - - self.events.emit(SIGNAL_COMMUNITY_EDITED, CommunityArgs(community: self.communities[membershipRequest.communityId])) + let requestToJoinState = RequestToJoinType(membershipRequest.state) + let noAwaitingIndex = self.getWaitingForSharedAddressesRequestIndex(membershipRequest.communityId, membershipRequest.id) == -1 + if requestToJoinState == RequestToJoinType.AwaitingAddress and noAwaitingIndex: + self.communities[membershipRequest.communityId].waitingForSharedAddressesRequestsToJoin.add(membershipRequest) + let myPublicKey = singletonInstance.userProfile.getPubKey() + if myPublicKey == membershipRequest.publicKey: + self.events.emit(SIGNAL_WAITING_ON_NEW_COMMUNITY_OWNER_TO_CONFIRM_REQUEST_TO_REJOIN, CommunityIdArgs(communityId: membershipRequest.communityId)) + elif requestToJoinState == RequestToJoinType.Pending and self.getPendingRequestIndex(membershipRequest.communityId, membershipRequest.id) == -1: + self.communities[membershipRequest.communityId].pendingRequestsToJoin.add(membershipRequest) + self.events.emit(SIGNAL_NEW_REQUEST_TO_JOIN_COMMUNITY, + CommunityRequestArgs(communityRequest: membershipRequest)) + elif requestToJoinState == RequestToJoinType.Accepted: + # Request was accepted, update the member's airdrop address + self.events.emit(SIGNAL_NEW_REQUEST_TO_JOIN_COMMUNITY_ACCEPTED, + CommunityRequestArgs(communityRequest: membershipRequest)) + else: + try: + self.updateMembershipRequestToNewState(membershipRequest.communityId, membershipRequest.id, self.communities[membershipRequest.communityId], + requestToJoinState) + except Exception as e: + error "Unknown request", msg = e.msg proc init*(self: Service) = self.doConnect() diff --git a/ui/app/AppLayouts/Communities/popups/InDropdown.qml b/ui/app/AppLayouts/Communities/popups/InDropdown.qml index 44beaefdaf..a84f68d190 100644 --- a/ui/app/AppLayouts/Communities/popups/InDropdown.qml +++ b/ui/app/AppLayouts/Communities/popups/InDropdown.qml @@ -53,8 +53,11 @@ StatusDropdown { listView.positionViewAtBeginning() } - onAboutToShow: listView.Layout.preferredHeight = Math.min( + onAboutToShow: { + searcher.input.edit.forceActiveFocus() + listView.Layout.preferredHeight = Math.min( listView.implicitHeight, 420) + } // only channels (no entries representing categories), sorted according to // category position and position diff --git a/ui/app/AppLayouts/Communities/popups/MembersDropdown.qml b/ui/app/AppLayouts/Communities/popups/MembersDropdown.qml index 2e759433ef..bf04cb7787 100644 --- a/ui/app/AppLayouts/Communities/popups/MembersDropdown.qml +++ b/ui/app/AppLayouts/Communities/popups/MembersDropdown.qml @@ -67,6 +67,7 @@ StatusDropdown { onOpened: { listView.positionViewAtBeginning() filterInput.text = "" + filterInput.input.edit.forceActiveFocus() } QtObject { diff --git a/ui/app/AppLayouts/Communities/views/EditAirdropView.qml b/ui/app/AppLayouts/Communities/views/EditAirdropView.qml index 6392e213fc..4f728923bb 100644 --- a/ui/app/AppLayouts/Communities/views/EditAirdropView.qml +++ b/ui/app/AppLayouts/Communities/views/EditAirdropView.qml @@ -2,6 +2,7 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 +import StatusQ 0.1 import StatusQ.Components 0.1 import StatusQ.Controls 0.1 import StatusQ.Core 0.1 @@ -512,27 +513,31 @@ StatusScrollView { model: SortFilterProxyModel { sourceModel: membersModel + sorters : [ + StringSorter { + roleName: "preferredDisplayName" + caseSensitivity: Qt.CaseInsensitive + } + ] + filters: [ - ExpressionFilter { + FastExpressionFilter { enabled: membersDropdown.searchText !== "" - function matchesAlias(name, filter) { - return name.split(" ").some(p => p.startsWith(filter)) - } - expression: { - membersDropdown.searchText - const filter = membersDropdown.searchText.toLowerCase() - return matchesAlias(model.alias.toLowerCase(), filter) + return model.alias.toLowerCase().includes(filter) || model.displayName.toLowerCase().includes(filter) || model.ensName.toLowerCase().includes(filter) || model.localNickname.toLowerCase().includes(filter) || model.pubKey.toLowerCase().includes(filter) } + expectedRoles: ["alias", "displayName", "ensName", "localNickname", "pubKey"] }, - ExpressionFilter { - expression: !!model.airdropAddress + ValueFilter { + roleName: "airdropAddress" + value: "" + inverted: true } ] }