refactor(members): use a single members list for public community chats (#16301)

Fixes #16288

Introduces a new instance of the users module, but managed by the section module.
This user module is managing the "public" community members list. Meaning that everytime we have a public channel in a community, we use that module instead.

The channel's user module is empty for public channels to reduce the amount of processing and memory used.

If the channel becomes private, we update the member list and populate it.
This commit is contained in:
Jonathan Rainville 2024-10-11 12:35:35 -04:00 committed by GitHub
parent 3e4e3591cd
commit d6031f8126
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 164 additions and 68 deletions

View File

@ -354,7 +354,7 @@ QtObject:
self.missingEncryptionKeyChanged()
proc requiresPermissionsChanged(self: ChatDetails) {.signal.}
proc getRequiresPermissions(self: ChatDetails): bool {.slot.} =
proc getRequiresPermissions*(self: ChatDetails): bool {.slot.} =
return self.requiresPermissions
QtProperty[bool] requiresPermissions:
read = getRequiresPermissions

View File

@ -401,7 +401,11 @@ method onCommunityChannelEdited*(self: Module, chatDto: ChatDto) =
self.view.chatDetails.setName(chatDto.name)
self.view.chatDetails.setIcon(chatDto.icon)
self.view.chatDetails.setMissingEncryptionKey(chatDto.missingEncryptionKey)
if self.view.chatDetails.getRequiresPermissions() != chatDto.tokenGated:
# The channel permission status changed. Update the member list
self.view.chatDetails.setRequiresPermissions(chatDto.tokenGated)
self.usersModule.updateMembersList()
self.messagesModule.updateChatFetchMoreMessages()
self.messagesModule.updateChatIdentifier()

View File

@ -21,9 +21,6 @@ type
communityService: community_service.Service
messageService: message_service.Service
# Forward declaration
proc getChat*(self: Controller): ChatDto
proc newController*(
delegate: io_interface.AccessInterface, events: EventEmitter, sectionId: string, chatId: string,
belongsToCommunity: bool, isUsersListAvailable: bool, contactService: contact_service.Service,
@ -63,7 +60,7 @@ proc init*(self: Controller) =
self.delegate.loggedInUserImageChanged()
# Events only for the user list, so not needed in one to one chats
if(self.isUsersListAvailable):
if self.isUsersListAvailable:
self.events.on(SIGNAL_CONTACT_UNTRUSTWORTHY) do(e: Args):
var args = TrustArgs(e)
self.delegate.contactUpdated(args.publicKey)
@ -118,19 +115,11 @@ proc belongsToCommunity*(self: Controller): bool =
proc getMyCommunity*(self: Controller): CommunityDto =
return self.communityService.getCommunityById(self.sectionId)
proc getChat*(self: Controller): ChatDto =
return self.chatService.getChatById(self.chatId)
proc getMyChatId*(self: Controller): string =
return self.chatId
proc getChatMembers*(self: Controller): seq[ChatMember] =
if self.belongsToCommunity:
let myCommunity = self.getMyCommunity()
# TODO: when a new channel is added, chat may arrive earlier and we have no up to date community yet
# see log here: https://github.com/status-im/status-desktop/issues/14442#issuecomment-2120756598
# should be resolved in https://github.com/status-im/status-desktop/issues/14219
let members = myCommunity.getCommunityChat(self.chatId).members
if members.len > 0:
return members
return self.chatService.getChatById(self.chatId).members
proc getMyChat*(self: Controller): ChatDto =
return self.chatService.getChatById(self.chatId)
proc getContactNameAndImage*(self: Controller, contactId: string):
tuple[name: string, image: string, largeImage: string] =

View File

@ -20,6 +20,9 @@ method isLoaded*(self: AccessInterface): bool {.base.} =
method getModuleAsVariant*(self: AccessInterface): QVariant {.base.} =
raise newException(ValueError, "No implementation available")
method getUsersListVariant*(self: AccessInterface): QVariant {.base.} =
raise newException(ValueError, "No implementation available")
method onNewMessagesLoaded*(self: AccessInterface, messages: seq[MessageDto]) {.base.} =
raise newException(ValueError, "No implementation available")
@ -59,5 +62,5 @@ method addGroupMembers*(self: AccessInterface, pubKeys: seq[string]) {.base.} =
method removeGroupMembers*(self: AccessInterface, pubKeys: seq[string]) {.base.} =
raise newException(ValueError, "No implementation available")
method updateMembersList*(self: AccessInterface) {.base.} =
method updateMembersList*(self: AccessInterface, membersToReset: seq[ChatMember] = @[]) {.base.} =
raise newException(ValueError, "No implementation available")

View File

@ -4,13 +4,13 @@ import view, controller
import ../../../../shared_models/[member_model, member_item]
import ../../../../../global/global_singleton
import ../../../../../core/eventemitter
import ../../../../../../app_service/common/conversion
import ../../../../../../app_service/common/types
import ../../../../../../app_service/service/contacts/dto/contacts
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
from ../../../../../../app_service/common/conversion import intToEnum
export io_interface
@ -20,15 +20,17 @@ type
viewVariant: QVariant
controller: Controller
moduleLoaded: bool
isPublicCommunityChannel: bool
isSectionMemberList: bool
# Forward declaration
proc processChatMember(self: Module, member: ChatMember): MemberItem
proc processChatMember(self: Module, member: ChatMember, reset: bool = false): tuple[doAdd: bool, memberItem: MemberItem]
proc newModule*(
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, isSectionMemberList: bool = false,
): Module =
result = Module()
result.view = view.newView(result)
@ -38,6 +40,8 @@ proc newModule*(
contactService, chatService, communityService, messageService,
)
result.moduleLoaded = false
result.isPublicCommunityChannel = false
result.isSectionMemberList = isSectionMemberList
method delete*(self: Module) =
self.controller.delete
@ -59,7 +63,12 @@ method viewDidLoad*(self: Module) =
method getModuleAsVariant*(self: Module): QVariant =
return self.viewVariant
method getUsersListVariant*(self: Module): QVariant =
self.view.getModel()
method contactNicknameChanged*(self: Module, publicKey: string) =
if self.isPublicCommunityChannel:
return
let contactDetails = self.controller.getContactDetails(publicKey)
self.view.model().setName(
publicKey,
@ -69,11 +78,15 @@ method contactNicknameChanged*(self: Module, publicKey: string) =
)
method contactsStatusUpdated*(self: Module, statusUpdates: seq[StatusUpdateDto]) =
if self.isPublicCommunityChannel:
return
for s in statusUpdates:
var status = toOnlineStatus(s.statusType)
self.view.model().setOnlineStatus(s.publicKey, status)
method contactUpdated*(self: Module, publicKey: string) =
if self.isPublicCommunityChannel:
return
let contactDetails = self.controller.getContactDetails(publicKey)
let isMe = publicKey == singletonInstance.userProfile.getPubKey()
self.view.model().updateItem(
@ -90,37 +103,43 @@ method contactUpdated*(self: Module, publicKey: string) =
)
method userProfileUpdated*(self: Module) =
if self.isPublicCommunityChannel:
return
self.contactUpdated(singletonInstance.userProfile.getPubKey())
method loggedInUserImageChanged*(self: Module) =
if self.isPublicCommunityChannel:
return
self.view.model().setIcon(singletonInstance.userProfile.getPubKey(), singletonInstance.userProfile.getIcon())
# This function either removes the member if it is no longer part of the community,
# does nothing if the member is already in the model or creates the MemberItem
proc processChatMember(self: Module, member: ChatMember): MemberItem =
proc processChatMember(self: Module, member: ChatMember, reset: bool = false): tuple[doAdd: bool, memberItem: MemberItem] =
result.doAdd = false
if member.id == "":
return
if not self.controller.belongsToCommunity() and not member.joined:
if not reset and not self.controller.belongsToCommunity() and not member.joined:
if self.view.model().isContactWithIdAdded(member.id):
# Member is no longer joined
self.view.model().removeItemById(member.id)
return
if self.view.model().isContactWithIdAdded(member.id):
if not reset and self.view.model().isContactWithIdAdded(member.id):
return
let isMe = member.id == singletonInstance.userProfile.getPubKey()
let contactDetails = self.controller.getContactDetails(member.id)
var status = OnlineStatus.Online
if (isMe):
if isMe:
let currentUserStatus = intToEnum(singletonInstance.userProfile.getCurrentUserStatus(), StatusType.Unknown)
status = toOnlineStatus(currentUserStatus)
else:
let statusUpdateDto = self.controller.getStatusForContact(member.id)
status = toOnlineStatus(statusUpdateDto.statusType)
return initMemberItem(
result.doAdd = true
result.memberItem = initMemberItem(
pubKey = member.id,
displayName = contactDetails.dto.displayName,
ensName = contactDetails.dto.name,
@ -139,35 +158,42 @@ proc processChatMember(self: Module, member: ChatMember): MemberItem =
)
method onChatMembersAdded*(self: Module, ids: seq[string]) =
if self.isPublicCommunityChannel:
return
var memberItems: seq[MemberItem] = @[]
for memberId in ids:
let item = self.processChatMember(ChatMember(id: memberId, role: MemberRole.None, joined: true))
if item.pubKey != "":
let (doAdd, item) = self.processChatMember(ChatMember(id: memberId, role: MemberRole.None, joined: true))
if doAdd:
memberItems.add(item)
self.view.model().addItems(memberItems)
method onChatMemberRemoved*(self: Module, id: string) =
if self.isPublicCommunityChannel:
return
self.view.model().removeItemById(id)
method onMembersChanged*(self: Module, members: seq[ChatMember]) =
if self.isPublicCommunityChannel:
return
let modelIDs = self.view.model().getItemIds()
let membersAdded = filter(members, member => not modelIDs.contains(member.id))
let membersRemoved = filter(modelIDs, id => not members.any(member => member.id == id))
var memberItems: seq[MemberItem] = @[]
for member in membersAdded:
let item = self.processChatMember(member)
if item.pubKey != "":
let (doAdd, item) = self.processChatMember(member)
if doAdd:
memberItems.add(item)
self.view.model().addItems(memberItems)
for id in membersRemoved:
self.onChatMemberRemoved(id)
method onChatMemberUpdated*(self: Module, publicKey: string, memberRole: MemberRole, joined: bool) =
if self.isPublicCommunityChannel:
return
let contactDetails = self.controller.getContactDetails(publicKey)
let isMe = publicKey == singletonInstance.userProfile.getPubKey()
self.view.model().updateItem(
@ -191,13 +217,45 @@ method addGroupMembers*(self: Module, pubKeys: seq[string]) =
method removeGroupMembers*(self: Module, pubKeys: seq[string]) =
self.controller.removeGroupMembers(pubKeys)
method updateMembersList*(self: Module) =
let members = self.controller.getChatMembers()
method updateMembersList*(self: Module, membersToReset: seq[ChatMember] = @[]) =
let reset = membersToReset.len > 0
var members: seq[ChatMember]
if reset:
members = membersToReset
else:
if self.controller.belongsToCommunity():
let myCommunity = self.controller.getMyCommunity()
if self.isSectionMemberList:
members = myCommunity.members
else:
# TODO: when a new channel is added, chat may arrive earlier and we have no up to date community yet
# see log here: https://github.com/status-im/status-desktop/issues/14442#issuecomment-2120756598
# should be resolved in https://github.com/status-im/status-desktop/issues/11694
let myChatId = self.controller.getMyChatId()
let chat = myCommunity.getCommunityChat(myChatId)
if not chat.tokenGated:
# No need to get the members, this channel is not encrypted and can use the section member list
self.isPublicCommunityChannel = true
return
self.isPublicCommunityChannel = false
if chat.members.len > 0:
members = chat.members
else:
# The channel now has a permisison, but the re-eval wasn't performed yet. Show all members for now
members = myCommunity.members
if members.len == 0:
members = self.controller.getMyChat().members
var memberItems: seq[MemberItem] = @[]
for member in members:
let item = self.processChatMember(member)
if item.pubKey != "":
let (doAdd, item) = self.processChatMember(member, reset)
if doAdd:
memberItems.add(item)
if reset:
self.view.model().setItems(memberItems)
else:
self.view.model().addItems(memberItems)

View File

@ -33,7 +33,7 @@ QtObject:
proc modelChanged*(self: View) {.signal.}
proc getModel(self: View): QVariant {.slot.} =
proc getModel*(self: View): QVariant {.slot.} =
return self.modelVariant
QtProperty[QVariant] model:

View File

@ -342,6 +342,11 @@ proc init*(self: Controller) =
if args.communityId == self.sectionId:
self.delegate.onSectionMutedChanged()
self.events.on(SIGNAL_COMMUNITY_MEMBERS_CHANGED) do(e: Args):
let args = CommunityMembersArgs(e)
if args.communityId == self.sectionId:
self.delegate.updateCommunityMemberList(args.members)
self.events.on(SIGNAL_CONTACT_NICKNAME_CHANGED) do(e: Args):
var args = ContactArgs(e)
self.delegate.onContactDetailsUpdated(args.contactId)

View File

@ -46,6 +46,12 @@ method isLoaded*(self: AccessInterface): bool {.base.} =
method getModuleAsVariant*(self: AccessInterface): QVariant {.base.} =
raise newException(ValueError, "No implementation available")
method updateCommunityMemberList*(self: AccessInterface, members: seq[ChatMember]) {.base.} =
raise newException(ValueError, "No implementation available")
method getSectionMemberList*(self: AccessInterface): QVariant {.base.} =
raise newException(ValueError, "No implementation available")
method onActiveSectionChange*(self: AccessInterface, sectionId: string) {.base.} =
raise newException(ValueError, "No implementation available")

View File

@ -17,6 +17,7 @@ import ../../shared_models/token_criteria_model
import ../../shared_models/token_permission_chat_list_model
import chat_content/module as chat_content_module
import chat_content/users/module as users_module
import ../../../global/global_singleton
import ../../../core/eventemitter
@ -53,6 +54,7 @@ type
chatContentModules: OrderedTable[string, chat_content_module.AccessInterface]
moduleLoaded: bool
chatsLoaded: bool
membersListModule: users_module.AccessInterface
# Forward declaration
proc buildChatSectionUI(
@ -121,6 +123,11 @@ proc newModule*(
result.chatsLoaded = false
result.chatContentModules = initOrderedTable[string, chat_content_module.AccessInterface]()
if isCommunity:
result.membersListModule = users_module.newModule(events, sectionId, chatId = "", isCommunity,
isUsersListAvailable = true, contactService, chatService, communityService, messageService, isSectionMemberList = true)
else:
result.membersListModule = nil
proc currentUserWalletContainsAddress(self: Module, address: string): bool =
if (address.len == 0):
@ -218,6 +225,8 @@ method delete*(self: Module) =
for cModule in self.chatContentModules.values:
cModule.delete
self.chatContentModules.clear
if self.membersListModule != nil:
self.membersListModule.delete
method isCommunity*(self: Module): bool =
return self.controller.isCommunity()
@ -440,6 +449,10 @@ method onChatsLoaded*(
self.buildChatSectionUI(community, chats, events, settingsService, nodeConfigurationService,
contactService, chatService, communityService, messageService, mailserversService, sharedUrlsService)
# Generate members list
if self.membersListModule != nil:
self.membersListModule.load()
if(not self.controller.isCommunity()):
# we do this only in case of chat section (not in case of communities)
self.initContactRequestsModel()
@ -480,6 +493,12 @@ proc checkIfModuleDidLoad(self: Module) =
method isLoaded*(self: Module): bool =
return self.moduleLoaded
method getSectionMemberList*(self: Module): QVariant =
return self.membersListModule.getUsersListVariant()
method updateCommunityMemberList*(self: Module, members: seq[ChatMember]) =
self.membersListModule.updateMembersList(members)
method viewDidLoad*(self: Module) =
self.checkIfModuleDidLoad()

View File

@ -573,3 +573,11 @@ QtObject:
return
self.communityMemberReevaluationStatus = value
self.communityMemberReevaluationStatusChanged()
proc membersModelChanged*(self: View) {.signal.}
proc getMembersModel(self: View): QVariant {.slot.} =
return self.delegate.getSectionMemberList()
QtProperty[QVariant] membersModel:
read = getMembersModel
notify = membersModelChanged

View File

@ -509,10 +509,7 @@ QtObject:
self.events.emit(SIGNAL_COMMUNITY_CATEGORY_NAME_EDITED,
CommunityCategoryArgs(communityId: community.id, category: category))
self.events.emit(SIGNAL_COMMUNITY_MEMBERS_CHANGED,
CommunityMembersArgs(communityId: community.id, members: community.members))
proc communityTokensChanged(self: Service, community: CommunityDto, prev_community: CommunityDto): bool =
proc communityTokensChanged(community: CommunityDto, prev_community: CommunityDto): bool =
let communityTokens = community.communityTokensMetadata
let prevCommunityTokens = prev_community.communityTokensMetadata
# checking length is sufficient - communityTokensMetadata list can only extend
@ -536,6 +533,17 @@ QtObject:
let prevCommunity = self.communities[community.id]
# If there's settings without `id` it means the original
# signal didn't include actual communitySettings, hence we
# assign the settings we already have, otherwise we risk our
# settings to be overridden with wrong defaults.
if community.settings.id == "":
community.settings = prevCommunity.settings
# Save the updated community before calling events, because some events triggers look ups on the new community
# We will save again at the end if other properties were updated
self.saveUpdatedCommunity(community)
try:
let currOwner = community.findOwner()
let prevOwner = prevCommunity.findOwner()
@ -551,23 +559,15 @@ QtObject:
let response = tokens_backend.registerLostOwnershipNotification(community.id)
checkAndEmitACNotificationsFromResponse(self.events, response.result{"activityCenterNotifications"})
if self.communityTokensChanged(community, prevCommunity):
if communityTokensChanged(community, prevCommunity):
self.events.emit(SIGNAL_COMMUNITY_TOKENS_CHANGED, nil)
# If there's settings without `id` it means the original
# signal didn't include actual communitySettings, hence we
# assign the settings we already have, otherwise we risk our
# settings to be overridden with wrong defaults.
if community.settings.id == "":
community.settings = prevCommunity.settings
var deletedCategories: seq[string] = @[]
# category was added
if(community.categories.len > prevCommunity.categories.len):
for category in community.categories:
if findIndexById(category.id, prevCommunity.categories) == -1:
self.communities[community.id].categories.add(category)
let chats = self.getChatsInCategory(community, category.id)
self.events.emit(SIGNAL_COMMUNITY_CATEGORY_CREATED,
@ -671,7 +671,6 @@ QtObject:
if community.communityTokensMetadata.len > prevCommunity.communityTokensMetadata.len:
for tokenMetadata in community.communityTokensMetadata:
if findIndexBySymbol(tokenMetadata.symbol, prevCommunity.communityTokensMetadata) == -1:
self.communities[community.id].communityTokensMetadata.add(tokenMetadata)
self.events.emit(SIGNAL_COMMUNITY_TOKEN_METADATA_ADDED,
CommunityTokenMetadataArgs(communityId: community.id,
tokenMetadata: tokenMetadata))
@ -680,14 +679,12 @@ QtObject:
if community.tokenPermissions.len > prevCommunity.tokenPermissions.len:
for id, tokenPermission in community.tokenPermissions:
if not prevCommunity.tokenPermissions.hasKey(id):
self.communities[community.id].tokenPermissions[id] = tokenPermission
self.events.emit(SIGNAL_COMMUNITY_TOKEN_PERMISSION_CREATED,
CommunityTokenPermissionArgs(communityId: community.id, tokenPermission: tokenPermission))
elif community.tokenPermissions.len < prevCommunity.tokenPermissions.len:
for id, prvTokenPermission in prevCommunity.tokenPermissions:
if not community.tokenPermissions.hasKey(id):
self.communities[community.id].tokenPermissions.del(id)
self.events.emit(SIGNAL_COMMUNITY_TOKEN_PERMISSION_DELETED,
CommunityTokenPermissionArgs(communityId: community.id, tokenPermission: prvTokenPermission))
else:
@ -725,16 +722,13 @@ QtObject:
break
if permissionUpdated:
self.communities[community.id].tokenPermissions[id] = tokenPermission
self.events.emit(SIGNAL_COMMUNITY_TOKEN_PERMISSION_UPDATED,
CommunityTokenPermissionArgs(communityId: community.id, tokenPermission: tokenPermission))
let wasJoined = self.communities[community.id].joined
self.saveUpdatedCommunity(community)
let wasJoined = prevCommunity.joined
# If the community was not joined before but is now, we signal it
if(not wasJoined and community.joined and community.isMember):
if not wasJoined and community.joined and community.isMember:
self.events.emit(SIGNAL_COMMUNITY_JOINED, CommunityArgs(community: community, fromUserAction: false))
self.events.emit(SIGNAL_COMMUNITIES_UPDATE, CommunitiesArgs(communities: @[community]))

View File

@ -68,7 +68,7 @@ proc `$`(self: Images): string =
]"""
proc `$`*(self: ContactsDto): string =
result = fmt"""ContactDto(
result = fmt"""ContactsDto(
id: {self.id},
name: {self.name},
ensVerified: {self.ensVerified},

View File

@ -15,7 +15,7 @@ proc `==`*(l, r: EnsUsernameDto): bool =
return l.chainId == r.chainid and l.username == r.username
proc `$`*(self: EnsUsernameDto): string =
result = fmt"""ContactDto(
result = fmt"""EnsUsernameDto(
chainId: {self.chainId},
username: {self.username},
txType: {self.txType},

View File

@ -179,7 +179,17 @@ StatusSectionLayout {
store: root.rootStore
label: qsTr("Members")
communityMemberReevaluationStatus: root.rootStore.communityMemberReevaluationStatus
usersModel: root.chatContentModule && root.chatContentModule.usersModule ? root.chatContentModule.usersModule.model : null
usersModel: {
if (!root.chatContentModule || !root.chatContentModule.chatDetails) {
return null
}
let isFullCommunityList = !root.chatContentModule.chatDetails.requiresPermissions
if (root.chatContentModule.chatDetails.belongsToCommunity && isFullCommunityList) {
// Community channel with no permisisons. We can use the section's membersModel
return root.rootStore.chatCommunitySectionModule.membersModel
}
return root.chatContentModule.usersModule ? root.chatContentModule.usersModule.model : null
}
}
}