From 18b16b5da67cfe19cb1a0f0d6ffb547ed20608c7 Mon Sep 17 00:00:00 2001 From: Alexandra Betouni Date: Sat, 3 Sep 2022 16:33:38 +0300 Subject: [PATCH] feat(GroupChats): Implemented group admin permissions Closes #5941 --- ui/StatusQ | 2 +- .../AppLayouts/Chat/panels/GroupChatPanel.qml | 46 +++-- .../Chat/views/ChatContextMenuView.qml | 4 +- .../Chat/views/ChatHeaderContentView.qml | 192 ++++++++---------- ui/app/AppLayouts/Chat/views/ChatView.qml | 27 ++- .../AppLayouts/Chat/views/CreateChatView.qml | 20 +- 6 files changed, 162 insertions(+), 129 deletions(-) diff --git a/ui/StatusQ b/ui/StatusQ index f6424328d8..1bbd9dc54f 160000 --- a/ui/StatusQ +++ b/ui/StatusQ @@ -1 +1 @@ -Subproject commit f6424328d87a4cac571f3b1c424f2dc125ced7b3 +Subproject commit 1bbd9dc54f8e311637e9a28ea0e0cec381f973aa diff --git a/ui/app/AppLayouts/Chat/panels/GroupChatPanel.qml b/ui/app/AppLayouts/Chat/panels/GroupChatPanel.qml index 558ed78a85..2b3d4e9a3a 100644 --- a/ui/app/AppLayouts/Chat/panels/GroupChatPanel.qml +++ b/ui/app/AppLayouts/Chat/panels/GroupChatPanel.qml @@ -25,6 +25,7 @@ RowLayout { property ListModel contactsModel: ListModel { } property var addedMembersIds: [] property var removedMembersIds: [] + property bool isAdminMode: false function initialize () { groupUsersModel.clear() @@ -32,6 +33,12 @@ RowLayout { addedMembersIds = [] removedMembersIds = [] 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) { @@ -51,7 +58,6 @@ RowLayout { delegate: Item { property string pubKey: model.pubKey property string name: model.displayName - property string icon: model.icon property bool isAdmin: model.isAdmin } } @@ -62,8 +68,13 @@ RowLayout { model: root.rootStore.contactsModel delegate: Item { 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 bool onlineStatus: model.onlineStatus } } @@ -76,24 +87,26 @@ RowLayout { for (var i = 0; i < groupUsersModelListView.count; i ++) { var entry = groupUsersModelListView.itemAtIndex(i) - // Add all group users different than me - if(!entry.isAdmin) { - d.groupUsersModel.insert(d.groupUsersModel.count, - {"pubKey": entry.pubKey, - "name": entry.name, - "icon": entry.icon}) - } + // Add all group users + d.groupUsersModel.append({pubKey: entry.pubKey, + name: entry.name, + tagIcon: entry.isAdmin ? "crown" : "", + isReadonly: d.isAdminMode ? entry.isAdmin : !rootStore.isCurrentUser(entry.pubKey) + }) } // Build contactsModel type from model type (to fit with expected StatusTagSelector format for (var j = 0; j < contactsModelListView.count; j ++) { var entry2 = contactsModelListView.itemAtIndex(j) - d.contactsModel.insert(d.contactsModel.count, - {"pubKey": entry2.pubKey, - "displayName": entry2.name, - "icon": entry2.icon, - "isIdenticon": false, - "onlineStatus": false}) + d.contactsModel.append({pubKey: entry2.pubKey, + displayName: entry2.displayName, + localNickname: entry2.localNickname, + isVerified: entry2.isVerified, + isUntrustworthy: entry2.isUntrustworthy, + isContact: entry2.isContact, + icon: entry2.icon, + isImage: true, + onlineStatus: entry2.onlineStatus}) } // Update contacts list used by StatusTagSelector @@ -160,6 +173,9 @@ RowLayout { compressedKeyGetter: function(pubKey) { return Utils.getCompressedPk(pubKey); } + colorIdForPubkeyGetter: function (pubKey) { + return Utils.colorIdForPubkey(pubKey); + } } StatusButton { diff --git a/ui/app/AppLayouts/Chat/views/ChatContextMenuView.qml b/ui/app/AppLayouts/Chat/views/ChatContextMenuView.qml index 22adf1bb6c..cd759daebd 100644 --- a/ui/app/AppLayouts/Chat/views/ChatContextMenuView.qml +++ b/ui/app/AppLayouts/Chat/views/ChatContextMenuView.qml @@ -62,9 +62,9 @@ StatusPopupMenu { } StatusMenuItem { - text: qsTr("Add / remove from group") + text: root.amIChatAdmin ? qsTr("Add / remove from group") : qsTr("Add to group") icon.name: "add-to-dm" - enabled: root.amIChatAdmin && (root.chatType === Constants.chatType.privateGroupChat) + enabled: (root.chatType === Constants.chatType.privateGroupChat) onTriggered: { root.addRemoveGroupMember() } } diff --git a/ui/app/AppLayouts/Chat/views/ChatHeaderContentView.qml b/ui/app/AppLayouts/Chat/views/ChatHeaderContentView.qml index ee89606b73..96e6a4d7db 100644 --- a/ui/app/AppLayouts/Chat/views/ChatHeaderContentView.qml +++ b/ui/app/AppLayouts/Chat/views/ChatHeaderContentView.qml @@ -19,18 +19,96 @@ RowLayout { property alias searchButton: searchButton property var rootStore - property var chatContentModule: root.rootStore.currentChatContentModule() + property var chatContentModule property int padding: 8 signal searchButtonClicked() + signal addRemoveGroupMemberClicked() - Loader { - id: loader - sourceComponent: statusChatInfoButton - Layout.fillWidth: true + StatusChatInfoButton { + objectName: "chatInfoBtnInHeader" + Layout.preferredWidth: Math.min(implicitWidth, parent.width) Layout.fillHeight: true Layout.alignment: Qt.AlignLeft 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 { @@ -207,7 +285,7 @@ RowLayout { ) } onAddRemoveGroupMember: { - loader.sourceComponent = contactsSelector + root.addRemoveGroupMemberClicked(); } onFetchMoreMessages: { root.rootStore.messageStore.requestMoreMessages(); @@ -234,106 +312,4 @@ RowLayout { 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 - } - } } diff --git a/ui/app/AppLayouts/Chat/views/ChatView.qml b/ui/app/AppLayouts/Chat/views/ChatView.qml index 0929e49b59..708f64d636 100644 --- a/ui/app/AppLayouts/Chat/views/ChatView.qml +++ b/ui/app/AppLayouts/Chat/views/ChatView.qml @@ -28,6 +28,7 @@ StatusSectionLayout { property bool hasAddedContacts: root.contactsStore.myContactsModel.count > 0 property RootStore rootStore + property var chatContentModule: root.rootStore.currentChatContentModule() property Component pinnedMessagesListPopupComponent property Component membershipRequestPopup @@ -55,6 +56,7 @@ StatusSectionLayout { function onActiveItemChanged() { Global.closeCreateChatView() + groupChatLoader.active = false; } } @@ -65,9 +67,11 @@ StatusSectionLayout { headerContent: ChatHeaderContentView { id: headerContent - visible: !!root.rootStore.currentChatContentModule() + visible: (!!root.rootStore.currentChatContentModule() && !groupChatLoader.active) rootStore: root.rootStore + chatContentModule: root.chatContentModule onSearchButtonClicked: root.openAppSearch() + onAddRemoveGroupMemberClicked: groupChatLoader.active = true; } leftPanel: Loader { @@ -92,6 +96,27 @@ StatusSectionLayout { onOpenAppSearch: { 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: { diff --git a/ui/app/AppLayouts/Chat/views/CreateChatView.qml b/ui/app/AppLayouts/Chat/views/CreateChatView.qml index 536408e96e..a1a787cc2d 100644 --- a/ui/app/AppLayouts/Chat/views/CreateChatView.qml +++ b/ui/app/AppLayouts/Chat/views/CreateChatView.qml @@ -30,7 +30,14 @@ Page { delegate: Item { property string pubKey: model.pubKey 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 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 ++) { var entry = contactsModelListView.itemAtIndex(i); contactsModel.insert(contactsModel.count, - {"pubKey": entry.pubKey, "displayName": entry.displayName, - "icon": entry.icon}); + {"pubKey": entry.pubKey, + "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); } else {