feat(GroupChats): Implemented group admin permissions

Closes #5941
This commit is contained in:
Alexandra Betouni 2022-09-03 16:33:38 +03:00
parent b2485603de
commit 18b16b5da6
6 changed files with 162 additions and 129 deletions

@ -1 +1 @@
Subproject commit f6424328d87a4cac571f3b1c424f2dc125ced7b3 Subproject commit 1bbd9dc54f8e311637e9a28ea0e0cec381f973aa

View File

@ -25,6 +25,7 @@ RowLayout {
property ListModel contactsModel: ListModel { } property ListModel contactsModel: ListModel { }
property var addedMembersIds: [] property var addedMembersIds: []
property var removedMembersIds: [] property var removedMembersIds: []
property bool isAdminMode: false
function initialize () { function initialize () {
groupUsersModel.clear() groupUsersModel.clear()
@ -32,6 +33,12 @@ RowLayout {
addedMembersIds = [] addedMembersIds = []
removedMembersIds = [] removedMembersIds = []
tagSelector.namesModel.clear() tagSelector.namesModel.clear()
for (var k = 0; k < groupUsersModelListView.count; k++) {
var groupEntry = groupUsersModelListView.itemAtIndex(k);
if (rootStore.isCurrentUser(groupEntry.pubKey) && groupEntry.isAdmin) {
d.isAdminMode = true;
}
}
} }
function find(val, array) { function find(val, array) {
@ -51,7 +58,6 @@ RowLayout {
delegate: Item { delegate: Item {
property string pubKey: model.pubKey property string pubKey: model.pubKey
property string name: model.displayName property string name: model.displayName
property string icon: model.icon
property bool isAdmin: model.isAdmin property bool isAdmin: model.isAdmin
} }
} }
@ -62,8 +68,13 @@ RowLayout {
model: root.rootStore.contactsModel model: root.rootStore.contactsModel
delegate: Item { delegate: Item {
property string pubKey: model.pubKey property string pubKey: model.pubKey
property string name: model.displayName property string displayName: model.displayName
property string localNickname: model.localNickname
property bool isVerified: model.isVerified
property bool isUntrustworthy: model.isUntrustworthy
property bool isContact: model.isContact
property string icon: model.icon property string icon: model.icon
property bool onlineStatus: model.onlineStatus
} }
} }
@ -76,24 +87,26 @@ RowLayout {
for (var i = 0; i < groupUsersModelListView.count; i ++) { for (var i = 0; i < groupUsersModelListView.count; i ++) {
var entry = groupUsersModelListView.itemAtIndex(i) var entry = groupUsersModelListView.itemAtIndex(i)
// Add all group users different than me // Add all group users
if(!entry.isAdmin) { d.groupUsersModel.append({pubKey: entry.pubKey,
d.groupUsersModel.insert(d.groupUsersModel.count, name: entry.name,
{"pubKey": entry.pubKey, tagIcon: entry.isAdmin ? "crown" : "",
"name": entry.name, isReadonly: d.isAdminMode ? entry.isAdmin : !rootStore.isCurrentUser(entry.pubKey)
"icon": entry.icon}) })
}
} }
// Build contactsModel type from model type (to fit with expected StatusTagSelector format // Build contactsModel type from model type (to fit with expected StatusTagSelector format
for (var j = 0; j < contactsModelListView.count; j ++) { for (var j = 0; j < contactsModelListView.count; j ++) {
var entry2 = contactsModelListView.itemAtIndex(j) var entry2 = contactsModelListView.itemAtIndex(j)
d.contactsModel.insert(d.contactsModel.count, d.contactsModel.append({pubKey: entry2.pubKey,
{"pubKey": entry2.pubKey, displayName: entry2.displayName,
"displayName": entry2.name, localNickname: entry2.localNickname,
"icon": entry2.icon, isVerified: entry2.isVerified,
"isIdenticon": false, isUntrustworthy: entry2.isUntrustworthy,
"onlineStatus": false}) isContact: entry2.isContact,
icon: entry2.icon,
isImage: true,
onlineStatus: entry2.onlineStatus})
} }
// Update contacts list used by StatusTagSelector // Update contacts list used by StatusTagSelector
@ -160,6 +173,9 @@ RowLayout {
compressedKeyGetter: function(pubKey) { compressedKeyGetter: function(pubKey) {
return Utils.getCompressedPk(pubKey); return Utils.getCompressedPk(pubKey);
} }
colorIdForPubkeyGetter: function (pubKey) {
return Utils.colorIdForPubkey(pubKey);
}
} }
StatusButton { StatusButton {

View File

@ -62,9 +62,9 @@ StatusPopupMenu {
} }
StatusMenuItem { StatusMenuItem {
text: qsTr("Add / remove from group") text: root.amIChatAdmin ? qsTr("Add / remove from group") : qsTr("Add to group")
icon.name: "add-to-dm" icon.name: "add-to-dm"
enabled: root.amIChatAdmin && (root.chatType === Constants.chatType.privateGroupChat) enabled: (root.chatType === Constants.chatType.privateGroupChat)
onTriggered: { root.addRemoveGroupMember() } onTriggered: { root.addRemoveGroupMember() }
} }

View File

@ -19,18 +19,96 @@ RowLayout {
property alias searchButton: searchButton property alias searchButton: searchButton
property var rootStore property var rootStore
property var chatContentModule: root.rootStore.currentChatContentModule() property var chatContentModule
property int padding: 8 property int padding: 8
signal searchButtonClicked() signal searchButtonClicked()
signal addRemoveGroupMemberClicked()
Loader { StatusChatInfoButton {
id: loader objectName: "chatInfoBtnInHeader"
sourceComponent: statusChatInfoButton Layout.preferredWidth: Math.min(implicitWidth, parent.width)
Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
Layout.alignment: Qt.AlignLeft Layout.alignment: Qt.AlignLeft
Layout.leftMargin: padding Layout.leftMargin: padding
title: chatContentModule? chatContentModule.chatDetails.name : ""
subTitle: {
if(!chatContentModule)
return ""
// In some moment in future this should be part of the backend logic.
// (once we add transaltion on the backend side)
switch (chatContentModule.chatDetails.type) {
case Constants.chatType.oneToOne:
return (chatContentModule.isMyContact(chatContentModule.chatDetails.id) ?
qsTr("Contact") :
qsTr("Not a contact"))
case Constants.chatType.publicChat:
return qsTr("Public chat")
case Constants.chatType.privateGroupChat:
let cnt = root.usersStore.usersModule.model.count
if(cnt > 1) return qsTr("%n member(s)", "", cnt);
return qsTr("1 member");
case Constants.chatType.communityChat:
return Utils.linkifyAndXSS(chatContentModule.chatDetails.description).trim()
default:
return ""
}
}
asset.name: chatContentModule? chatContentModule.chatDetails.icon : ""
ringSettings.ringSpecModel: chatContentModule && chatContentModule.chatDetails.type === Constants.chatType.oneToOne ?
Utils.getColorHashAsJson(chatContentModule.chatDetails.id) : ""
asset.color: chatContentModule?
chatContentModule.chatDetails.type === Constants.chatType.oneToOne ?
Utils.colorForPubkey(chatContentModule.chatDetails.id)
: chatContentModule.chatDetails.color
: ""
asset.emoji: chatContentModule? chatContentModule.chatDetails.emoji : ""
asset.emojiSize: "24x24"
type: chatContentModule? chatContentModule.chatDetails.type : Constants.chatType.unknown
pinnedMessagesCount: chatContentModule? chatContentModule.pinnedMessagesModel.count : 0
muted: chatContentModule? chatContentModule.chatDetails.muted : false
onPinnedMessagesCountClicked: {
if(!chatContentModule) {
console.debug("error on open pinned messages - chat content module is not set")
return
}
Global.openPopup(pinnedMessagesPopupComponent, {
store: rootStore,
messageStore: messageStore,
pinnedMessagesModel: chatContentModule.pinnedMessagesModel,
messageToPin: ""
})
}
onUnmute: {
if(!chatContentModule) {
console.debug("error on unmute chat - chat content module is not set")
return
}
chatContentModule.unmuteChat()
}
sensor.enabled: {
if(!chatContentModule)
return false
return chatContentModule.chatDetails.type !== Constants.chatType.publicChat &&
chatContentModule.chatDetails.type !== Constants.chatType.communityChat
}
onClicked: {
switch (chatContentModule.chatDetails.type) {
case Constants.chatType.privateGroupChat:
Global.openPopup(root.rootStore.groupInfoPopupComponent, {
chatContentModule: chatContentModule,
chatDetails: chatContentModule.chatDetails
})
break;
case Constants.chatType.oneToOne:
Global.openProfilePopup(chatContentModule.chatDetails.id)
break;
}
}
} }
RowLayout { RowLayout {
@ -207,7 +285,7 @@ RowLayout {
) )
} }
onAddRemoveGroupMember: { onAddRemoveGroupMember: {
loader.sourceComponent = contactsSelector root.addRemoveGroupMemberClicked();
} }
onFetchMoreMessages: { onFetchMoreMessages: {
root.rootStore.messageStore.requestMoreMessages(); root.rootStore.messageStore.requestMoreMessages();
@ -234,106 +312,4 @@ RowLayout {
visible: (menuButton.visible || membersButton.visible || searchButton.visible) visible: (menuButton.visible || membersButton.visible || searchButton.visible)
} }
} }
Keys.onEscapePressed: { loader.sourceComponent = statusChatInfoButton }
// Chat toolbar content option 1:
Component {
id: statusChatInfoButton
StatusChatInfoButton {
objectName: "chatInfoBtnInHeader"
width: Math.min(implicitWidth, parent.width)
title: chatContentModule? chatContentModule.chatDetails.name : ""
subTitle: {
if(!chatContentModule)
return ""
// In some moment in future this should be part of the backend logic.
// (once we add transaltion on the backend side)
switch (chatContentModule.chatDetails.type) {
case Constants.chatType.oneToOne:
return (chatContentModule.isMyContact(chatContentModule.chatDetails.id) ?
qsTr("Contact") :
qsTr("Not a contact"))
case Constants.chatType.publicChat:
return qsTr("Public chat")
case Constants.chatType.privateGroupChat:
let cnt = root.usersStore.usersModule.model.count
if(cnt > 1) return qsTr("%n member(s)", "", cnt);
return qsTr("1 member");
case Constants.chatType.communityChat:
return Utils.linkifyAndXSS(chatContentModule.chatDetails.description).trim()
default:
return ""
}
}
asset.name: chatContentModule? chatContentModule.chatDetails.icon : ""
ringSettings.ringSpecModel: chatContentModule && chatContentModule.chatDetails.type === Constants.chatType.oneToOne ?
Utils.getColorHashAsJson(chatContentModule.chatDetails.id) : ""
asset.color: chatContentModule?
chatContentModule.chatDetails.type === Constants.chatType.oneToOne ?
Utils.colorForPubkey(chatContentModule.chatDetails.id)
: chatContentModule.chatDetails.color
: ""
asset.emoji: chatContentModule? chatContentModule.chatDetails.emoji : ""
asset.emojiSize: "24x24"
type: chatContentModule? chatContentModule.chatDetails.type : Constants.chatType.unknown
pinnedMessagesCount: chatContentModule? chatContentModule.pinnedMessagesModel.count : 0
muted: chatContentModule? chatContentModule.chatDetails.muted : false
onPinnedMessagesCountClicked: {
if(!chatContentModule) {
console.debug("error on open pinned messages - chat content module is not set")
return
}
Global.openPopup(pinnedMessagesPopupComponent, {
store: rootStore,
messageStore: messageStore,
pinnedMessagesModel: chatContentModule.pinnedMessagesModel,
messageToPin: ""
})
}
onUnmute: {
if(!chatContentModule) {
console.debug("error on unmute chat - chat content module is not set")
return
}
chatContentModule.unmuteChat()
}
sensor.enabled: {
if(!chatContentModule)
return false
return chatContentModule.chatDetails.type !== Constants.chatType.publicChat &&
chatContentModule.chatDetails.type !== Constants.chatType.communityChat
}
onClicked: {
switch (chatContentModule.chatDetails.type) {
case Constants.chatType.privateGroupChat:
Global.openPopup(root.rootStore.groupInfoPopupComponent, {
chatContentModule: chatContentModule,
chatDetails: chatContentModule.chatDetails
})
break;
case Constants.chatType.oneToOne:
Global.openProfilePopup(chatContentModule.chatDetails.id)
break;
}
}
}
}
// Chat toolbar content option 2:
Component {
id: contactsSelector
GroupChatPanel {
sectionModule: root.chatSectionModule
chatContentModule: root.chatContentModule
rootStore: root.rootStore
maxHeight: root.height
onPanelClosed: loader.sourceComponent = statusChatInfoButton
}
}
} }

View File

@ -28,6 +28,7 @@ StatusSectionLayout {
property bool hasAddedContacts: root.contactsStore.myContactsModel.count > 0 property bool hasAddedContacts: root.contactsStore.myContactsModel.count > 0
property RootStore rootStore property RootStore rootStore
property var chatContentModule: root.rootStore.currentChatContentModule()
property Component pinnedMessagesListPopupComponent property Component pinnedMessagesListPopupComponent
property Component membershipRequestPopup property Component membershipRequestPopup
@ -55,6 +56,7 @@ StatusSectionLayout {
function onActiveItemChanged() { function onActiveItemChanged() {
Global.closeCreateChatView() Global.closeCreateChatView()
groupChatLoader.active = false;
} }
} }
@ -65,9 +67,11 @@ StatusSectionLayout {
headerContent: ChatHeaderContentView { headerContent: ChatHeaderContentView {
id: headerContent id: headerContent
visible: !!root.rootStore.currentChatContentModule() visible: (!!root.rootStore.currentChatContentModule() && !groupChatLoader.active)
rootStore: root.rootStore rootStore: root.rootStore
chatContentModule: root.chatContentModule
onSearchButtonClicked: root.openAppSearch() onSearchButtonClicked: root.openAppSearch()
onAddRemoveGroupMemberClicked: groupChatLoader.active = true;
} }
leftPanel: Loader { leftPanel: Loader {
@ -92,6 +96,27 @@ StatusSectionLayout {
onOpenAppSearch: { onOpenAppSearch: {
root.openAppSearch(); root.openAppSearch();
} }
Loader {
id: groupChatLoader
anchors.fill: parent
active: false
anchors {
leftMargin: Style.current.padding
rightMargin: (headerContent.height + Style.current.padding)
//move a bit up as we still need the activity button
topMargin: -(headerContent.height + Style.current.halfPadding)
bottomMargin: Style.current.halfPadding
}
sourceComponent: GroupChatPanel {
anchors.fill: parent
maxHeight: parent.height
rootStore: root.rootStore
chatContentModule: root.chatContentModule
sectionModule: root.rootStore.chatCommunitySectionModule
onPanelClosed: { groupChatLoader.active = false; }
}
}
Keys.onEscapePressed: { groupChatLoader.active = false; }
} }
showRightPanel: { showRightPanel: {

View File

@ -30,7 +30,14 @@ Page {
delegate: Item { delegate: Item {
property string pubKey: model.pubKey property string pubKey: model.pubKey
property string displayName: model.displayName property string displayName: model.displayName
property string localNickname: model.localNickname
property bool isVerified: model.isVerified
property bool isUntrustworthy: model.isUntrustworthy
property bool isContact: model.isContact
property string icon: model.icon property string icon: model.icon
property bool onlineStatus: model.onlineStatus
property string tagIcon: !!model.tagIcon ? model.tagIcon : ""
property bool isAdmin: model.isAdmin
} }
} }
@ -39,8 +46,17 @@ Page {
for (var i = 0; i < contactsModelListView.count; i ++) { for (var i = 0; i < contactsModelListView.count; i ++) {
var entry = contactsModelListView.itemAtIndex(i); var entry = contactsModelListView.itemAtIndex(i);
contactsModel.insert(contactsModel.count, contactsModel.insert(contactsModel.count,
{"pubKey": entry.pubKey, "displayName": entry.displayName, {"pubKey": entry.pubKey,
"icon": entry.icon}); "displayName": entry.displayName,
"localNickname": entry.localNickname,
"isVerified": entry.isVerified,
"isUntrustworthy": entry.isUntrustworthy,
"isContact": entry.isContact,
"icon": entry.icon,
"isImage": true,
"onlineStatus": entry.onlineStatus,
"tagIcon": entry.tagIcon ? entry.tagIcon : "",
"isReadonly": entry.isAdmin})
} }
tagSelector.sortModel(root.contactsModel); tagSelector.sortModel(root.contactsModel);
} else { } else {