Feat/issue 11795 introduce KickedPending and BannedPending states (#12068)

* feat(Communities): Introduce pending states for kick, ban and unban actions

Close #11795

* feat(Communities): Show bannedMembers pending states on the UI

* feat(Communities:) make kick, ban and unban methods async

* feat(Communities): add signal about community membership status change

* fix(Communities): move membership managment to to the appropriate model

* chore: review fixes
This commit is contained in:
Mikhail Rogachev 2023-10-05 00:41:51 +03:00 committed by GitHub
parent ebc48c0072
commit cd4d92aef0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 211 additions and 42 deletions

View File

@ -59,11 +59,6 @@ proc handleCommunityOnlyConnections(self: Controller) =
self.delegate.onMembersChanged(args.members)
self.events.on(SIGNAL_COMMUNITY_MEMBER_REMOVED) do(e: Args):
let args = CommunityMemberArgs(e)
if (args.communityId == self.sectionId):
self.delegate.onChatMemberRemoved(args.pubKey)
proc init*(self: Controller) =
# Events that are needed for all chats because of mentions
self.events.on(SIGNAL_CONTACT_NICKNAME_CHANGED) do(e: Args):

View File

@ -576,13 +576,13 @@ proc leaveCommunity*(self: Controller) =
self.communityService.leaveCommunity(self.sectionId)
proc removeUserFromCommunity*(self: Controller, pubKey: string) =
self.communityService.removeUserFromCommunity(self.sectionId, pubKey)
self.communityService.asyncRemoveUserFromCommunity(self.sectionId, pubKey)
proc banUserFromCommunity*(self: Controller, pubKey: string) =
self.communityService.banUserFromCommunity(self.sectionId, pubKey)
self.communityService.asyncBanUserFromCommunity(self.sectionId, pubKey)
proc unbanUserFromCommunity*(self: Controller, pubKey: string) =
self.communityService.unbanUserFromCommunity(self.sectionId, pubKey)
self.communityService.asyncUnbanUserFromCommunity(self.sectionId, pubKey)
proc editCommunity*(
self: Controller,

View File

@ -145,6 +145,17 @@ proc createMemberItem(self: Module, memberId, requestId: string, status: Members
)
method getCommunityItem(self: Module, c: CommunityDto): SectionItem =
# TODO: unite bannedMembers, pendingMemberRequests and declinedMemberRequests
var members: seq[MemberItem] = @[]
for member in c.members:
if c.pendingAndBannedMembers.hasKey(member.id):
let communityMemberState = c.pendingAndBannedMembers[member.id]
members.add(self.createMemberItem(member.id, "", toMembershipRequestState(communityMemberState)))
var bannedMembers: seq[MemberItem] = @[]
for memberId, communityMemberState in c.pendingAndBannedMembers:
bannedMembers.add(self.createMemberItem(memberId, "", toMembershipRequestState(communityMemberState)))
return initItem(
c.id,
SectionType.Community,
@ -172,11 +183,9 @@ method getCommunityItem(self: Module, c: CommunityDto): SectionItem =
c.permissions.access,
c.permissions.ensOnly,
c.muted,
c.members.map(proc(member: ChatMember): MemberItem =
result = self.createMemberItem(member.id, "", MembershipRequestState.Accepted)),
members = members,
historyArchiveSupportEnabled = c.settings.historyArchiveSupportEnabled,
bannedMembers = c.bannedMembersIds.map(proc(bannedMemberId: string): MemberItem =
result = self.createMemberItem(bannedMemberId, "", MembershipRequestState.Banned)),
bannedMembers = bannedMembers,
pendingMemberRequests = c.pendingRequestsToJoin.map(proc(requestDto: CommunityMembershipRequestDto): MemberItem =
result = self.createMemberItem(requestDto.publicKey, requestDto.id, MembershipRequestState(requestDto.state))),
declinedMemberRequests = c.declinedRequestsToJoin.map(proc(requestDto: CommunityMembershipRequestDto): MemberItem =

View File

@ -381,6 +381,10 @@ proc init*(self: Controller) =
var args = CommunityMemberArgs(e)
self.delegate.onAcceptRequestToJoinSuccess(args.communityId, args.pubKey, args.requestId)
self.events.on(SIGNAL_COMMUNITY_MEMBER_STATUS_CHANGED) do(e: Args):
let args = CommunityMemberStatusUpdatedArgs(e)
self.delegate.onMembershipStatusUpdated(args.communityId, args.memberPubkey, args.status)
self.events.on(SIGNAL_COMMUNITY_MEMBERS_CHANGED) do(e: Args):
let args = CommunityMembersArgs(e)
self.communityTokensService.fetchCommunityTokenOwners(args.communityId)

View File

@ -13,7 +13,7 @@ import app_service/service/wallet_account/service as wallet_account_service
import app_service/service/token/service as token_service
import app_service/service/community_tokens/service as community_tokens_service
import app_service/service/community_tokens/community_collectible_owner
from app_service/common/types import StatusType, ContractTransactionStatus
from app_service/common/types import StatusType, ContractTransactionStatus, MembershipRequestState
import app/global/app_signals
import app/core/eventemitter
@ -341,6 +341,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 onMembershipStatusUpdated*(self: AccessInterface, communityId: string, memberPubkey: string, status: MembershipRequestState) {.base.} =
raise newException(ValueError, "No implementation available")
method onDeactivateChatLoader*(self: AccessInterface, sectionId: string, chatId: string) {.base.} =
raise newException(ValueError, "No implementation available")

View File

@ -1181,6 +1181,11 @@ method onAcceptRequestToJoinSuccess*[T](self: Module[T], communityId: string, me
if item.id != "":
item.updatePendingRequestLoadingState(memberKey, false)
method onMembershipStatusUpdated*[T](self: Module[T], communityId: string, memberPubkey: string, status: MembershipRequestState) =
let item = self.view.model().getItemById(communityId)
if item.id != "":
item.updateMembershipStatus(memberPubkey, status)
method calculateProfileSectionHasNotification*[T](self: Module[T]): bool =
return not self.controller.isMnemonicBackedUp()

View File

@ -12,7 +12,7 @@ type
requestToJoinId: string
requestToJoinLoading*: bool
airdropAddress*: string
membershipRequestState: MembershipRequestState
membershipRequestState*: MembershipRequestState
# FIXME: remove defaults
proc initMemberItem*(

View File

@ -358,3 +358,14 @@ QtObject:
ModelRole.RequestToJoinLoading.int
])
proc updateMembershipStatus*(self: Model, memberKey: string, status: MembershipRequestState) {.inline.} =
let idx = self.findIndexForMember(memberKey)
if(idx == -1):
return
self.items[idx].membershipRequestState = status
let index = self.createIndex(idx, 0, nil)
defer: index.delete
self.dataChanged(index, index, @[
ModelRole.MembershipRequestState.int
])

View File

@ -360,3 +360,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 updateMembershipStatus*(self: SectionItem, memberKey: string, status: MembershipRequestState) {.inline.} =
if status == MembershipRequestState.UnbannedPending or status == MembershipRequestState.Banned:
self.bannedMembersModel.updateMembershipStatus(memberKey, status)
else:
self.membersModel.updateMembershipStatus(memberKey, status)

View File

@ -55,6 +55,8 @@ type MemberRole* {.pure} = enum
Admin
TokenMaster
# TODO: consider refactor MembershipRequestState to MembershipState and use both for request to join and kick/ban actions
# Issue: https://github.com/status-im/status-desktop/issues/11842
type MembershipRequestState* {.pure} = enum
None = 0,
Pending = 1,
@ -65,7 +67,8 @@ type MembershipRequestState* {.pure} = enum
Banned = 6,
Kicked = 7,
BannedPending = 8,
KickedPending = 9
UnbannedPending = 9,
KickedPending = 10,
type
ContractTransactionStatus* {.pure.} = enum

View File

@ -103,6 +103,54 @@ const asyncAcceptRequestToJoinCommunityTask: Task = proc(argEncoded: string) {.g
"requestId": arg.requestId
})
type
AsyncCommunityMemberActionTaskArg = ref object of QObjectTaskArg
communityId: string
pubKey: string
const asyncRemoveUserFromCommunityTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.} =
let arg = decode[AsyncCommunityMemberActionTaskArg](argEncoded)
try:
let response = status_go.removeUserFromCommunity(arg.communityId, arg.pubKey)
arg.finish(%* {
"communityId": arg.communityId,
"pubKey": arg.pubKey,
"response": response,
"error": "",
})
except Exception as e:
arg.finish(%* {
"communityId": arg.communityId,
"pubKey": arg.pubKey,
"error": e.msg,
})
const asyncBanUserFromCommunityTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.} =
let arg = decode[AsyncCommunityMemberActionTaskArg](argEncoded)
try:
let response = status_go.banUserFromCommunity(arg.communityId, arg.pubKey)
let tpl: tuple[communityId: string, pubKey: string, response: RpcResponse[JsonNode], error: string] = (arg.communityId, arg.pubKey, response, "")
arg.finish(tpl)
except Exception as e:
arg.finish(%* {
"error": e.msg,
"communityId": arg.communityId,
"pubKey": arg.pubKey
})
const asyncUnbanUserFromCommunityTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.} =
let arg = decode[AsyncCommunityMemberActionTaskArg](argEncoded)
try:
let response = status_go.unbanUserFromCommunity(arg.communityId, arg.pubKey)
let tpl: tuple[communityId: string, pubKey: string, response: RpcResponse[JsonNode], error: string] = (arg.communityId, arg.pubKey, response, "")
arg.finish(tpl)
except Exception as e:
arg.finish(%* {
"error": e.msg,
"communityId": arg.communityId,
"pubKey": arg.pubKey
})
type
AsyncRequestToJoinCommunityTaskArg = ref object of QObjectTaskArg
communityId: string

View File

@ -34,6 +34,13 @@ type
Members,
ControlNodeUptime
type
CommunityMemberPendingBanOrKick* {.pure.} = enum
Banned = 0,
BanPending,
UnbanPending,
KickPending
type CommunityMembershipRequestDto* = object
id*: string
publicKey*: string
@ -154,7 +161,7 @@ type CommunityDto* = object
pendingRequestsToJoin*: seq[CommunityMembershipRequestDto]
settings*: CommunitySettingsDto
adminSettings*: CommunityAdminSettingsDto
bannedMembersIds*: seq[string]
pendingAndBannedMembers*: Table[string, CommunityMemberPendingBanOrKick]
declinedRequestsToJoin*: seq[CommunityMembershipRequestDto]
encrypted*: bool
canceledRequestsToJoin*: seq[CommunityMembershipRequestDto]
@ -427,10 +434,11 @@ proc toCommunityDto*(jsonObj: JsonNode): CommunityDto =
else:
result.tags = "[]"
var bannedMembersIdsObj: JsonNode
if(jsonObj.getProp("banList", bannedMembersIdsObj) and bannedMembersIdsObj.kind == JArray):
for bannedMemberId in bannedMembersIdsObj:
result.bannedMembersIds.add(bannedMemberId.getStr)
var pendingAndBannedMembersObj: JsonNode
if (jsonObj.getProp("pendingAndBannedMembers", pendingAndBannedMembersObj) and pendingAndBannedMembersObj.kind == JObject):
result.pendingAndBannedMembers = initTable[string, CommunityMemberPendingBanOrKick]()
for memberId, pendingKickOrBanMember in pendingAndBannedMembersObj:
result.pendingAndBannedMembers[memberId] = CommunityMemberPendingBanOrKick(pendingKickOrBanMember.getInt())
discard jsonObj.getProp("canRequestAccess", result.canRequestAccess)
discard jsonObj.getProp("canManageUsers", result.canManageUsers)
@ -446,6 +454,18 @@ proc toCommunityDto*(jsonObj: JsonNode): CommunityDto =
for tokenObj in communityTokensMetadataObj:
result.communityTokensMetadata.add(tokenObj.toCommunityTokensMetadataDto())
proc toMembershipRequestState*(state: CommunityMemberPendingBanOrKick): MembershipRequestState =
case state:
of CommunityMemberPendingBanOrKick.Banned:
return MembershipRequestState.Banned
of CommunityMemberPendingBanOrKick.BanPending:
return MembershipRequestState.BannedPending
of CommunityMemberPendingBanOrKick.UnbanPending:
return MembershipRequestState.UnbannedPending
of CommunityMemberPendingBanOrKick.KickPending:
return MembershipRequestState.KickedPending
return MembershipRequestState.None
proc toCommunityMembershipRequestDto*(jsonObj: JsonNode): CommunityMembershipRequestDto =
result = CommunityMembershipRequestDto()
discard jsonObj.getProp("id", result.id)
@ -494,6 +514,13 @@ proc contains(arrayToSearch: seq[int], searched: int): bool =
return true
return false
proc getBannedMembersIds*(self: CommunityDto): seq[string] =
var bannedIds: seq[string] = @[]
for memberId, state in self.pendingAndBannedMembers:
if state == CommunityMemberPendingBanOrKick.Banned:
bannedIds.add(memberId)
return bannedIds
proc toChannelGroupDto*(communityDto: CommunityDto): ChannelGroupDto =
ChannelGroupDto(
id: communityDto.id,
@ -520,7 +547,7 @@ proc toChannelGroupDto*(communityDto: CommunityDto): ChannelGroupDto =
canManageUsers: communityDto.canManageUsers,
muted: communityDto.muted,
historyArchiveSupportEnabled: communityDto.settings.historyArchiveSupportEnabled,
bannedMembersIds: communityDto.bannedMembersIds,
bannedMembersIds: communityDto.getBannedMembersIds(),
encrypted: communityDto.encrypted,
)

View File

@ -139,6 +139,11 @@ type
communityId*: string
membersRevealedAccounts*: MembersRevealedAccounts
CommunityMemberStatusUpdatedArgs* = ref object of Args
communityId*: string
memberPubkey*: string
status*: MembershipRequestState
# Signals which may be emitted by this service:
const SIGNAL_COMMUNITY_DATA_LOADED* = "communityDataLoaded"
const SIGNAL_COMMUNITY_JOINED* = "communityJoined"
@ -169,7 +174,7 @@ const SIGNAL_COMMUNITY_CATEGORY_DELETED* = "communityCategoryDeleted"
const SIGNAL_COMMUNITY_CATEGORY_REORDERED* = "communityCategoryReordered"
const SIGNAL_COMMUNITY_CHANNEL_CATEGORY_CHANGED* = "communityChannelCategoryChanged"
const SIGNAL_COMMUNITY_MEMBER_APPROVED* = "communityMemberApproved"
const SIGNAL_COMMUNITY_MEMBER_REMOVED* = "communityMemberRemoved"
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"
@ -466,6 +471,8 @@ 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 handleCommunityUpdates(self: Service, communities: seq[CommunityDto], updatedChats: seq[ChatDto], removedChats: seq[string]) =
try:
@ -1884,26 +1891,74 @@ QtObject:
except Exception as e:
error "Error unmuting category", msg = e.msg
proc removeUserFromCommunity*(self: Service, communityId: string, pubKey: string) =
try:
discard status_go.removeUserFromCommunity(communityId, pubKey)
proc asyncRemoveUserFromCommunity*(self: Service, communityId, pubKey: string) =
let arg = AsyncCommunityMemberActionTaskArg(
tptr: cast[ByteAddress](asyncRemoveUserFromCommunityTask),
vptr: cast[ByteAddress](self.vptr),
slot: "onAsyncCommunityMemberActionCompleted",
communityId: communityId,
pubKey: pubKey,
)
self.threadpool.start(arg)
self.events.emit(SIGNAL_COMMUNITY_MEMBER_REMOVED,
CommunityMemberArgs(communityId: communityId, pubKey: pubKey))
except Exception as e:
error "Error removing user from community", msg = e.msg
proc asyncBanUserFromCommunity*(self: Service, communityId, pubKey: string) =
let arg = AsyncCommunityMemberActionTaskArg(
tptr: cast[ByteAddress](asyncBanUserFromCommunityTask),
vptr: cast[ByteAddress](self.vptr),
slot: "onAsyncCommunityMemberActionCompleted",
communityId: communityId,
pubKey: pubKey,
)
self.threadpool.start(arg)
proc banUserFromCommunity*(self: Service, communityId: string, pubKey: string) =
try:
discard status_go.banUserFromCommunity(communityId, pubKey)
except Exception as e:
error "Error banning user from community", msg = e.msg
proc asyncUnbanUserFromCommunity*(self: Service, communityId, pubKey: string) =
let arg = AsyncCommunityMemberActionTaskArg(
tptr: cast[ByteAddress](asyncUnbanUserFromCommunityTask),
vptr: cast[ByteAddress](self.vptr),
slot: "onAsyncCommunityMemberActionCompleted",
communityId: communityId,
pubKey: pubKey,
)
self.threadpool.start(arg)
proc unbanUserFromCommunity*(self: Service, communityId: string, pubKey: string) =
proc onAsyncCommunityMemberActionCompleted*(self: Service, response: string) {.slot.} =
try:
discard status_go.unbanUserFromCommunity(communityId, pubKey)
let rpcResponseObj = response.parseJson
if rpcResponseObj{"error"}.kind != JNull and rpcResponseObj{"error"}.getStr != "":
raise newException(RpcException, rpcResponseObj["error"].getStr)
if rpcResponseObj["response"]{"error"}.kind != JNull:
let error = Json.decode(rpcResponseObj["response"]["error"].getStr, RpcError)
raise newException(RpcException, error.message)
let memberPubkey = rpcResponseObj{"pubKey"}.getStr()
var communityJArr: JsonNode
if not rpcResponseObj["response"]{"result"}.getProp("communities", communityJArr):
raise newException(RpcException, "there is no `communities` key in the response")
if communityJArr.len == 0:
raise newException(RpcException, "`communities` array is empty in the response")
var community = communityJArr[0].toCommunityDto()
var status: MembershipRequestState = MembershipRequestState.None
if community.pendingAndBannedMembers.hasKey(memberPubkey):
status = community.pendingAndBannedMembers[memberPubkey].toMembershipRequestState()
else:
for member in community.members:
if member.id == memberPubkey:
status = MembershipRequestState.Accepted
self.events.emit(SIGNAL_COMMUNITY_MEMBER_STATUS_CHANGED, CommunityMemberStatusUpdatedArgs(
communityId: community.id,
memberPubkey: memberPubkey,
status: status
))
except Exception as e:
error "Error banning user from community", msg = e.msg
error "error while getting the community members' revealed addressesses", msg = e.msg
proc setCommunityMuted*(self: Service, communityId: string, mutedType: int) =
try:

View File

@ -94,7 +94,7 @@ SplitView {
SortFilterProxyModel {
id: usersModelWithMembershipState
readonly property var membershipStatePerView: [
[Constants.CommunityMembershipRequestState.Accepted , Constants.CommunityMembershipRequestState.BannedPending, Constants.CommunityMembershipRequestState.KickedPending],
[Constants.CommunityMembershipRequestState.Accepted , Constants.CommunityMembershipRequestState.BannedPending, Constants.CommunityMembershipRequestState.UnbannedPending, Constants.CommunityMembershipRequestState.KickedPending],
[Constants.CommunityMembershipRequestState.Banned],
[Constants.CommunityMembershipRequestState.Pending, Constants.CommunityMembershipRequestState.AcceptedPending, Constants.CommunityMembershipRequestState.RejectedPending],
[Constants.CommunityMembershipRequestState.Rejected]

View File

@ -101,13 +101,14 @@ Item {
readonly property bool isRejectedPending: model.membershipRequestState === Constants.CommunityMembershipRequestState.RejectedPending
readonly property bool isAcceptedPending: model.membershipRequestState === Constants.CommunityMembershipRequestState.AcceptedPending
readonly property bool isBanPending: model.membershipRequestState === Constants.CommunityMembershipRequestState.BannedPending
readonly property bool isUnbanPending: model.membershipRequestState === Constants.CommunityMembershipRequestState.UnbannedPending
readonly property bool isKickPending: model.membershipRequestState === Constants.CommunityMembershipRequestState.KickedPending
readonly property bool isBanned: model.membershipRequestState === Constants.CommunityMembershipRequestState.Banned
readonly property bool isKicked: model.membershipRequestState === Constants.CommunityMembershipRequestState.Kicked
// TODO: Connect to backend when available
// The admin that initited the pending state can change the state. Actions are not visible for other admins
readonly property bool ctaAllowed: !isRejectedPending && !isAcceptedPending && !isBanPending && !isKickPending
readonly property bool ctaAllowed: !isRejectedPending && !isAcceptedPending && !isBanPending && !isUnbanPending && !isKickPending
readonly property bool itsMe: model.pubKey.toLowerCase() === Global.userProfile.pubKey.toLowerCase()
readonly property bool isHovered: memberItem.sensor.containsMouse
@ -140,10 +141,11 @@ Item {
readonly property bool unbanButtonVisible: tabIsShowingUnbanButton && isBanned && showOnHover
/// Pending states ///
readonly property bool isPendingState: isAcceptedPending || isRejectedPending || isBanPending || isKickPending
readonly property bool isPendingState: isAcceptedPending || isRejectedPending || isBanPending || isUnbanPending || isKickPending
readonly property string pendingStateText: isAcceptedPending ? qsTr("Accept pending...") :
isRejectedPending ? qsTr("Reject pending...") :
isBanPending ? qsTr("Ban pending...") :
isUnbanPending ? qsTr("Unban pending...") :
isKickPending ? qsTr("Kick pending...") : ""
statusListItemComponentsSlot.spacing: 16
@ -160,7 +162,7 @@ Item {
d.pendingTextMaxWidth = Math.max(implicitWidth, d.pendingTextMaxWidth)
}
visible: !!text && isPendingState
rightPadding: isKickPending || isBanPending ? 0 : Style.current.bigPadding
rightPadding: isKickPending || isBanPending || isUnbanPending ? 0 : Style.current.bigPadding
anchors.verticalCenter: parent.verticalCenter
text: pendingStateText
color: Theme.palette.baseColor1

View File

@ -1184,6 +1184,7 @@ QtObject {
Banned,
Kicked,
BannedPending,
UnbannedPending,
KickedPending
}

2
vendor/status-go vendored

@ -1 +1 @@
Subproject commit c85a110a3185e89e4565ef67fe4527c5708eb8c4
Subproject commit a17ee052fb28e4108be1b02b45aca9c764c845f2