feat(@desktop/chat): Add requests sections to members tab of the community management

Add tabs: "Pending requests", "Rejected"
Add getting declined requests from status-go

Issue #6279
This commit is contained in:
Michal Iskierko 2022-08-04 09:49:41 +02:00 committed by Michał Iskierko
parent 7ef4a2d257
commit 346af7c245
12 changed files with 266 additions and 62 deletions

View File

@ -73,6 +73,19 @@ method viewDidLoad*(self: Module) =
self.delegate.communitiesModuleDidLoad()
proc createMemberItem(self: Module, memberId, requestId: string): MemberItem =
let contactDetails = self.controller.getContactDetails(memberId)
result = initMemberItem(
pubKey = memberId,
displayName = contactDetails.displayName,
ensName = contactDetails.details.name,
localNickname = contactDetails.details.localNickname,
alias = contactDetails.details.alias,
icon = contactDetails.icon,
onlineStatus = toOnlineStatus(self.controller.getStatusForContactWithId(memberId).statusType),
isContact = contactDetails.details.isContact,
requestToJoinId = requestId)
method getCommunityItem(self: Module, c: CommunityDto): SectionItem =
return initItem(
c.id,
@ -100,31 +113,14 @@ method getCommunityItem(self: Module, c: CommunityDto): SectionItem =
c.permissions.ensOnly,
c.muted,
c.members.map(proc(member: Member): MemberItem =
let contactDetails = self.controller.getContactDetails(member.id)
result = initMemberItem(
pubKey = member.id,
displayName = contactDetails.displayName,
ensName = contactDetails.details.name,
localNickname = contactDetails.details.localNickname,
alias = contactDetails.details.alias,
icon = contactDetails.icon,
onlineStatus = toOnlineStatus(self.controller.getStatusForContactWithId(member.id).statusType),
isContact = contactDetails.details.isContact,
)),
result = self.createMemberItem(member.id, "")),
historyArchiveSupportEnabled = c.settings.historyArchiveSupportEnabled,
bannedMembers = c.bannedMembersIds.map(proc(bannedMemberId: string): MemberItem =
let contactDetails = self.controller.getContactDetails(bannedMemberId)
result = initMemberItem(
pubKey = bannedMemberId,
displayName = contactDetails.displayName,
ensName = contactDetails.details.name,
localNickname = contactDetails.details.localNickname,
alias = contactDetails.details.alias,
icon = contactDetails.icon,
onlineStatus = toOnlineStatus(self.controller.getStatusForContactWithId(bannedMemberId).statusType),
isContact = contactDetails.details.added, # FIXME
)
),
result = self.createMemberItem(bannedMemberId, "")),
pendingMemberRequests = c.pendingRequestsToJoin.map(proc(requestDto: CommunityMembershipRequestDto): MemberItem =
result = self.createMemberItem(requestDto.publicKey, requestDto.id)),
declinedMemberRequests = c.declinedRequestsToJoin.map(proc(requestDto: CommunityMembershipRequestDto): MemberItem =
result = self.createMemberItem(requestDto.publicKey, requestDto.id)),
)
method getCuratedCommunityItem(self: Module, c: CuratedCommunity): CuratedCommunityItem =

View File

@ -259,13 +259,40 @@ proc createChannelGroupItem[T](self: Module[T], c: ChannelGroupDto): SectionItem
alias = contactDetails.details.alias,
icon = contactDetails.icon,
onlineStatus = toOnlineStatus(self.controller.getStatusForContactWithId(bannedMemberId).statusType),
isContact = contactDetails.details.added # FIXME
isContact = contactDetails.details.isContact
)
)
),
if (isCommunity): communityDetails.pendingRequestsToJoin.map(proc(requestDto: CommunityMembershipRequestDto): MemberItem =
let contactDetails = self.controller.getContactDetails(requestDto.publicKey)
result = initMemberItem(
pubKey = requestDto.publicKey,
displayName = contactDetails.displayName,
ensName = contactDetails.details.name,
localNickname = contactDetails.details.localNickname,
alias = contactDetails.details.alias,
icon = contactDetails.icon,
onlineStatus = toOnlineStatus(self.controller.getStatusForContactWithId(requestDto.publicKey).statusType),
isContact = contactDetails.details.isContact,
requestToJoinId = requestDto.id
)
) else: @[],
if (isCommunity): communityDetails.declinedRequestsToJoin.map(proc(requestDto: CommunityMembershipRequestDto): MemberItem =
let contactDetails = self.controller.getContactDetails(requestDto.publicKey)
result = initMemberItem(
pubKey = requestDto.publicKey,
displayName = contactDetails.displayName,
ensName = contactDetails.details.name,
localNickname = contactDetails.details.localNickname,
alias = contactDetails.details.alias,
icon = contactDetails.icon,
onlineStatus = toOnlineStatus(self.controller.getStatusForContactWithId(requestDto.publicKey).statusType),
isContact = contactDetails.details.isContact,
requestToJoinId = requestDto.id
)
) else: @[]
)
method load*[T](
self: Module[T],
events: EventEmitter,

View File

@ -19,11 +19,15 @@ QtObject:
proc membersChanged*(self: ActiveSection) {.signal.}
proc bannedMembersChanged*(self: ActiveSection) {.signal.}
proc pendingRequestsToJoinChanged*(self: ActiveSection) {.signal.}
proc pendingMemberRequestsChanged*(self: ActiveSection) {.signal.}
proc declinedMemberRequestsChanged*(self: ActiveSection) {.signal.}
proc setActiveSectionData*(self: ActiveSection, item: SectionItem) =
self.item = item
self.membersChanged()
self.bannedMembersChanged()
self.pendingMemberRequestsChanged()
self.declinedMemberRequestsChanged()
self.pendingRequestsToJoinChanged()
proc getId*(self: ActiveSection): string {.slot.} =
@ -185,6 +189,27 @@ QtObject:
read = bannedMembers
notify = bannedMembersChanged
proc pendingMemberRequests(self: ActiveSection): QVariant {.slot.} =
if (self.item.id == ""):
# FIXME (Jo) I don't know why but the Item is sometimes empty and doing anything here crashes the app
return newQVariant("")
return newQVariant(self.item.pendingMemberRequests)
QtProperty[QVariant] pendingMemberRequests:
read = pendingMemberRequests
notify = pendingMemberRequestsChanged
proc declinedMemberRequests(self: ActiveSection): QVariant {.slot.} =
if (self.item.id == ""):
# FIXME (Jo) I don't know why but the Item is sometimes empty and doing anything here crashes the app
return newQVariant("")
return newQVariant(self.item.declinedMemberRequests)
QtProperty[QVariant] declinedMemberRequests:
read = declinedMemberRequests
notify = declinedMemberRequestsChanged
proc hasMember(self: ActiveSection, pubkey: string): bool {.slot.} =
return self.item.hasMember(pubkey)

View File

@ -7,6 +7,7 @@ type
MemberItem* = ref object of UserItem
isAdmin: bool
joined: bool
requestToJoinId: string
# FIXME: remove defaults
proc initMemberItem*(
@ -28,10 +29,12 @@ proc initMemberItem*(
outgoingVerificationStatus: VerificationRequestStatus = VerificationRequestStatus.None,
isAdmin: bool = false,
joined: bool = false,
requestToJoinId: string = "",
): MemberItem =
result = MemberItem()
result.isAdmin = isAdmin
result.joined = joined
result.requestToJoinId = requestToJoinId
result.UserItem.setup(
pubKey = pubKey,
displayName = displayName,
@ -70,7 +73,8 @@ proc `$`*(self: MemberItem): string =
incomingVerificationStatus: {$self.incomingVerificationStatus.int},
outgoingVerificationStatus: {$self.outgoingVerificationStatus.int},
isAdmin: {self.isAdmin},
joined: {self.joined}
joined: {self.joined},
requestToJoinId: {self.requestToJoinId}
]"""
proc isAdmin*(self: MemberItem): bool {.inline.} =
@ -84,3 +88,9 @@ proc joined*(self: MemberItem): bool {.inline.} =
proc `joined=`*(self: MemberItem, value: bool) {.inline.} =
self.joined = value
proc requestToJoinId*(self: MemberItem): string {.inline.} =
self.requestToJoinId
proc `requestToJoinId=`*(self: MemberItem, value: string) {.inline.} =
self.requestToJoinId = value

View File

@ -25,6 +25,7 @@ type
OutgoingVerificationStatus
IsAdmin
Joined
RequestToJoinId
QtObject:
type
@ -86,6 +87,7 @@ QtObject:
ModelRole.OutgoingVerificationStatus.int: "outgoingVerificationStatus",
ModelRole.IsAdmin.int: "isAdmin",
ModelRole.Joined.int: "joined",
ModelRole.RequestToJoinId.int: "requestToJoinId",
}.toTable
method data(self: Model, index: QModelIndex, role: int): QVariant =
@ -135,6 +137,8 @@ QtObject:
result = newQVariant(item.isAdmin)
of ModelRole.Joined:
result = newQVariant(item.joined)
of ModelRole.RequestToJoinId:
result = newQVariant(item.requestToJoinId)
proc addItem*(self: Model, item: MemberItem) =
# we need to maintain online contact on top, that means

View File

@ -43,6 +43,8 @@ type
historyArchiveSupportEnabled: bool
pinMessageAllMembersEnabled: bool
bannedMembersModel: member_model.Model
pendingMemberRequestsModel: member_model.Model
declinedMemberRequestsModel: member_model.Model
proc initItem*(
id: string,
@ -74,6 +76,8 @@ proc initItem*(
historyArchiveSupportEnabled = false,
pinMessageAllMembersEnabled = false,
bannedMembers: seq[MemberItem] = @[],
pendingMemberRequests: seq[MemberItem] = @[],
declinedMemberRequests: seq[MemberItem] = @[],
): SectionItem =
result.id = id
result.sectionType = sectionType
@ -107,6 +111,10 @@ proc initItem*(
result.pinMessageAllMembersEnabled = pinMessageAllMembersEnabled
result.bannedMembersModel = newModel()
result.bannedMembersModel.setItems(bannedMembers)
result.pendingMemberRequestsModel = newModel()
result.pendingMemberRequestsModel.setItems(pendingMemberRequests)
result.declinedMemberRequestsModel = newModel()
result.declinedMemberRequestsModel.setItems(declinedMemberRequests)
proc isEmpty*(self: SectionItem): bool =
return self.id.len == 0
@ -141,6 +149,8 @@ proc `$`*(self: SectionItem): string =
historyArchiveSupportEnabled:{self.historyArchiveSupportEnabled},
pinMessageAllMembersEnabled:{self.pinMessageAllMembersEnabled},
bannedMembers:{self.bannedMembersModel},
pendingMemberRequests:{self.pendingMemberRequestsModel},
declinedMemberRequests:{self.declinedMemberRequestsModel},
]"""
proc id*(self: SectionItem): string {.inline.} =
@ -256,6 +266,12 @@ proc updateMember*(
proc bannedMembers*(self: SectionItem): member_model.Model {.inline.} =
self.bannedMembersModel
proc pendingMemberRequests*(self: SectionItem): member_model.Model {.inline.} =
self.pendingMemberRequestsModel
proc declinedMemberRequests*(self: SectionItem): member_model.Model {.inline.} =
self.declinedMemberRequestsModel
proc pendingRequestsToJoin*(self: SectionItem): PendingRequestModel {.inline.} =
self.pendingRequestsToJoinModel

View File

@ -7,6 +7,11 @@ include ../../../common/json_utils
import ../../chat/dto/chat
type RequestToJoinType* {.pure.}= enum
Pending = 0,
Declined = 1,
Accepted = 2
type Member* = object
id*: string
roles*: seq[int]
@ -64,6 +69,7 @@ type CommunityDto* = object
settings*: CommunitySettingsDto
adminSettings*: CommunityAdminSettingsDto
bannedMembersIds*: seq[string]
declinedRequestsToJoin*: seq[CommunityMembershipRequestDto]
type CuratedCommunity* = object
available*: bool

View File

@ -122,6 +122,7 @@ QtObject:
proc handleCommunityUpdates(self: Service, communities: seq[CommunityDto], updatedChats: seq[ChatDto])
proc handleCommunitiesSettingsUpdates(self: Service, communitiesSettings: seq[CommunitySettingsDto])
proc pendingRequestsToJoinForCommunity*(self: Service, communityId: string): seq[CommunityMembershipRequestDto]
proc declinedRequestsToJoinForCommunity*(self: Service, communityId: string): seq[CommunityMembershipRequestDto]
proc delete*(self: Service) =
discard
@ -210,6 +211,7 @@ QtObject:
# Community data we get from the signals and responses don't contgain the pending requests
# therefore, we must keep the old one
community.pendingRequestsToJoin = self.joinedCommunities[community.id].pendingRequestsToJoin
community.declinedRequestsToJoin = self.joinedCommunities[community.id].declinedRequestsToJoin
# Update the joinded community list with the new data
self.joinedCommunities[community.id] = community
@ -343,6 +345,7 @@ QtObject:
self.joinedCommunities[community.id] = community
if (community.admin):
self.joinedCommunities[community.id].pendingRequestsToJoin = self.pendingRequestsToJoinForCommunity(community.id)
self.joinedCommunities[community.id].declinedRequestsToJoin = self.declinedRequestsToJoinForCommunity(community.id)
let allCommunities = self.loadAllCommunities()
for community in allCommunities:
@ -579,6 +582,17 @@ QtObject:
except Exception as e:
error "Error fetching community requests", msg = e.msg
proc declinedRequestsToJoinForCommunity*(self: Service, communityId: string): seq[CommunityMembershipRequestDto] =
try:
let response = status_go.declinedRequestsToJoinForCommunity(communityId)
result = @[]
if response.result.kind != JNull:
for jsonCommunityReqest in response.result:
result.add(jsonCommunityReqest.toCommunityMembershipRequestDto())
except Exception as e:
error "Error fetching community declined requests", msg = e.msg
proc leaveCommunity*(self: Service, communityId: string) =
try:
let response = status_go.leaveCommunity(communityId)
@ -1026,7 +1040,7 @@ QtObject:
except Exception as e:
error "Error exporting community", msg = e.msg
proc getPendingRequestIndex*(self: Service, communityId: string, requestId: string): int =
proc getPendingRequestIndex(self: Service, communityId: string, requestId: string): int =
let community = self.joinedCommunities[communityId]
var i = 0
for pendingRequest in community.pendingRequestsToJoin:
@ -1035,15 +1049,43 @@ QtObject:
i.inc()
return -1
proc removeMembershipRequestFromCommunityAndGetMemberPubkey*(self: Service, communityId: string, requestId: string): string =
let index = self.getPendingRequestIndex(communityId, requestId)
proc getDeclinedRequestIndex(self: Service, communityId: string, requestId: string): int =
let community = self.joinedCommunities[communityId]
var i = 0
for declinedRequest in community.declinedRequestsToJoin:
if (declinedRequest.id == requestId):
return i
i.inc()
return -1
if (index == -1):
proc removeMembershipRequestFromCommunityAndGetMemberPubkey*(self: Service, communityId: string, requestId: string): string =
let indexPending = self.getPendingRequestIndex(communityId, requestId)
let indexDeclined = self.getDeclinedRequestIndex(communityId, requestId)
if (indexPending == -1 and indexDeclined == -1):
raise newException(RpcException, fmt"Community request not found: {requestId}")
var community = self.joinedCommunities[communityId]
result = community.pendingRequestsToJoin[index].publicKey
community.pendingRequestsToJoin.delete(index)
if (indexPending != -1):
result = community.pendingRequestsToJoin[indexPending].publicKey
community.pendingRequestsToJoin.delete(indexPending)
elif (indexDeclined != -1):
result = community.declinedRequestsToJoin[indexDeclined].publicKey
community.declinedRequestsToJoin.delete(indexDeclined)
self.joinedCommunities[communityId] = community
proc moveRequestToDeclined*(self: Service, communityId: string, requestId: string) =
let indexPending = self.getPendingRequestIndex(communityId, requestId)
if (indexPending == -1):
raise newException(RpcException, fmt"Community request not found: {requestId}")
var community = self.joinedCommunities[communityId]
if (indexPending != -1):
let itemToMove = community.pendingRequestsToJoin[indexPending]
community.declinedRequestsToJoin.add(itemToMove)
community.pendingRequestsToJoin.delete(indexPending)
self.joinedCommunities[communityId] = community
@ -1065,7 +1107,7 @@ QtObject:
try:
discard status_go.declineRequestToJoinCommunity(requestId)
discard self.removeMembershipRequestFromCommunityAndGetMemberPubkey(communityId, requestId)
self.moveRequestToDeclined(communityId, requestId)
self.events.emit(SIGNAL_COMMUNITY_EDITED, CommunityArgs(community: self.joinedCommunities[communityId]))
except Exception as e:

View File

@ -43,6 +43,9 @@ proc myPendingRequestsToJoin*(): RpcResponse[JsonNode] {.raises: [Exception].} =
proc pendingRequestsToJoinForCommunity*(communityId: string): RpcResponse[JsonNode] {.raises: [Exception].} =
result = callPrivateRPC("pendingRequestsToJoinForCommunity".prefix, %*[communityId])
proc declinedRequestsToJoinForCommunity*(communityId: string): RpcResponse[JsonNode] {.raises: [Exception].} =
result = callPrivateRPC("declinedRequestsToJoinForCommunity".prefix, %*[communityId])
proc leaveCommunity*(communityId: string): RpcResponse[JsonNode] {.raises: [Exception].} =
result = callPrivateRPC("leaveCommunity".prefix, %*[communityId])

View File

@ -18,16 +18,19 @@ SettingsPageLayout {
property var membersModel
property var bannedMembersModel
property var pendingMemberRequestsModel
property var declinedMemberRequestsModel
property string communityName
property bool editable: true
property int pendingRequests
signal membershipRequestsClicked()
signal userProfileClicked(string id)
signal kickUserClicked(string id)
signal banUserClicked(string id)
signal unbanUserClicked(string id)
signal acceptRequestToJoin(string id)
signal declineRequestToJoin(string id)
title: qsTr("Members")
@ -45,21 +48,19 @@ SettingsPageLayout {
text: qsTr("All Members")
}
// TODO will be added in next phase
// StatusTabButton {
// id: pendingRequestsBtn
// width: implicitWidth
// text: qsTr("Pending Requests")
// enabled: false
// }
// Temporary commented until we provide appropriate flags on the `status-go` side to cover all sections.
// StatusTabButton {
// id: rejectedRequestsBtn
// width: implicitWidth
// enabled: root.contactsStore.receivedButRejectedContactRequestsModel.count > 0 ||
// root.contactsStore.sentButRejectedContactRequestsModel.count > 0
// btnText: qsTr("Rejected Requests")
// }
StatusTabButton {
id: pendingRequestsBtn
width: implicitWidth
text: qsTr("Pending Requests")
enabled: pendingMemberRequestsModel.count > 0
}
StatusTabButton {
id: declinedRequestsBtn
width: implicitWidth
text: qsTr("Rejected")
enabled: declinedMemberRequestsModel.count > 0
}
StatusTabButton {
id: bannedBtn
@ -105,6 +106,45 @@ SettingsPageLayout {
}
}
CommunityMembersTabPanel {
model: root.pendingMemberRequestsModel
placeholderText: {
if (root.pendingMemberRequestsModel.count === 0) {
return qsTr("No pending requests to search")
} else {
return qsTr("Search %1's %2 pending request%3").arg(root.communityName)
.arg(root.pendingMemberRequestsModel.count)
.arg(root.pendingMemberRequestsModel.count > 1 ? "s" : "")
}
}
panelType: CommunityMembersTabPanel.TabType.PendingRequests
Layout.fillWidth: true
Layout.fillHeight: true
onAcceptRequestToJoin: root.acceptRequestToJoin(id)
onDeclineRequestToJoin: root.declineRequestToJoin(id)
}
CommunityMembersTabPanel {
model: root.declinedMemberRequestsModel
placeholderText: {
if (root.declinedMemberRequestsModel.count === 0) {
return qsTr("No rejected members to search")
} else {
return qsTr("Search %1's %2 rejected member%3").arg(root.communityName)
.arg(root.declinedMemberRequestsModel.count)
.arg(root.declinedMemberRequestsModel.count > 1 ? "s" : "")
}
}
panelType: CommunityMembersTabPanel.TabType.DeclinedRequests
Layout.fillWidth: true
Layout.fillHeight: true
onAcceptRequestToJoin: root.acceptRequestToJoin(id)
}
CommunityMembersTabPanel {
model: root.bannedMembersModel
placeholderText: {

View File

@ -24,9 +24,14 @@ Item {
signal banUserClicked(string id, string name)
signal unbanUserClicked(string id)
signal acceptRequestToJoin(string id)
signal declineRequestToJoin(string id)
enum TabType {
AllMembers,
BannedMembers
BannedMembers,
PendingRequests,
DeclinedRequests
}
property int panelType: CommunityMembersTabPanel.TabType.AllMembers
@ -62,16 +67,18 @@ Item {
id: memberItem
readonly property bool itsMe: model.pubKey.toLowerCase() === userProfile.pubKey.toLowerCase()
readonly property bool showButton: !memberItem.itsMe && !model.isAdmin
&& memberItem.sensor.containsMouse
readonly property bool isHovered: memberItem.sensor.containsMouse
readonly property bool canBeBanned: !memberItem.itsMe && !model.isAdmin
statusListItemComponentsSlot.spacing: 16
rightPadding: 80
statusListItemTitleArea.anchors.rightMargin: 0
statusListItemSubTitle.elide: Text.ElideRight
rightPadding: 75
leftPadding: 12
components: [
StatusButton {
visible: (root.panelType === CommunityMembersTabPanel.TabType.AllMembers) && showButton
visible: (root.panelType === CommunityMembersTabPanel.TabType.AllMembers) && isHovered && canBeBanned
text: qsTr("Kick")
type: StatusBaseButton.Type.Danger
size: StatusBaseButton.Size.Small
@ -79,7 +86,7 @@ Item {
},
StatusButton {
visible: (root.panelType === CommunityMembersTabPanel.TabType.AllMembers) && showButton
visible: (root.panelType === CommunityMembersTabPanel.TabType.AllMembers) && isHovered && canBeBanned
text: qsTr("Ban")
type: StatusBaseButton.Type.Danger
size: StatusBaseButton.Size.Small
@ -87,12 +94,40 @@ Item {
},
StatusButton {
visible: (root.panelType === CommunityMembersTabPanel.TabType.BannedMembers) && showButton
visible: (root.panelType === CommunityMembersTabPanel.TabType.BannedMembers) && isHovered && canBeBanned
text: qsTr("Unban")
type: StatusBaseButton.Type.Normal
size: StatusBaseButton.Size.Small
size: StatusBaseButton.Size.Large
onClicked: root.unbanUserClicked(model.pubKey)
width: 95
height: 44
},
StatusButton {
visible: (root.panelType === CommunityMembersTabPanel.TabType.PendingRequests ||
root.panelType === CommunityMembersTabPanel.TabType.DeclinedRequests) && isHovered
text: qsTr("Accept")
type: StatusBaseButton.Type.Normal
size: StatusBaseButton.Size.Large
icon.name: "checkmark-circle"
icon.color: Theme.palette.successColor1
normalColor: Theme.palette.successColor2
hoverColor: Theme.palette.successColor3
textColor: Theme.palette.successColor1
onClicked: root.acceptRequestToJoin(model.requestToJoinId)
width: 124
height: 44
},
StatusButton {
visible: (root.panelType === CommunityMembersTabPanel.TabType.PendingRequests) && isHovered
text: qsTr("Reject")
type: StatusBaseButton.Type.Danger
icon.name: "close-circle"
icon.color: Style.current.danger
onClicked: root.declineRequestToJoin(model.requestToJoinId)
width: 118
height: 44
}
]

View File

@ -185,17 +185,17 @@ StatusAppTwoPanelLayout {
CommunityMembersSettingsPanel {
membersModel: root.community.members
bannedMembersModel: root.community.bannedMembers
pendingMemberRequestsModel: root.community.pendingMemberRequests
declinedMemberRequestsModel: root.community.declinedMemberRequests
editable: root.community.amISectionAdmin
pendingRequests: root.community.pendingRequestsToJoin ? root.community.pendingRequestsToJoin.count : 0
communityName: root.community.name
onUserProfileClicked: Global.openProfilePopup(id)
onKickUserClicked: root.rootStore.removeUserFromCommunity(id)
onBanUserClicked: root.rootStore.banUserFromCommunity(id)
onUnbanUserClicked: root.rootStore.unbanUserFromCommunity(id)
onMembershipRequestsClicked: Global.openPopup(root.membershipRequestPopup, {
communitySectionModule: root.chatCommunitySectionModule
})
onAcceptRequestToJoin: root.rootStore.acceptRequestToJoinCommunity(id)
onDeclineRequestToJoin: root.rootStore.declineRequestToJoinCommunity(id)
}
CommunityPermissionsSettingsPanel {}