From 3570b23d8ab4bad995c53acf4ae386c34978892e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Blin?= <sebastien.blin@savoirfairelinux.com> Date: Thu, 5 Jan 2023 16:08:44 -0500 Subject: [PATCH] swarmdetailspanel: show kicked contacts if administrator This patch avoid for non-admins to try to re-add kicked members as filtered out from the list. However kicked members are only visible for administrators. Change-Id: Ie01b7071c072d147bbc0f39e477cc24d7fd58b1a --- CMakeLists.txt | 2 + daemon | 2 +- src/app/constant/JamiStrings.qml | 1 + src/app/currentconversation.cpp | 25 +++++- src/app/currentconversation.h | 4 +- src/app/currentconversationmembers.cpp | 76 +++++++++++++++++++ src/app/currentconversationmembers.h | 59 ++++++++++++++ .../mainview/components/AddMemberPanel.qml | 5 +- src/app/mainview/components/ChatView.qml | 18 +++-- .../mainview/components/ChatViewHeader.qml | 2 +- .../components/PluginHandlerPicker.qml | 6 +- .../mainview/components/SwarmDetailsPanel.qml | 42 +++++----- .../SwarmParticipantContextMenu.qml | 9 ++- src/app/messagesadapter.cpp | 7 ++ src/app/messagesadapter.h | 1 + src/app/qmlregister.cpp | 1 + 16 files changed, 216 insertions(+), 44 deletions(-) create mode 100644 src/app/currentconversationmembers.cpp create mode 100644 src/app/currentconversationmembers.h diff --git a/CMakeLists.txt b/CMakeLists.txt index d4e24da0d..d0fdd65be 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -215,6 +215,7 @@ set(COMMON_SOURCES ${APP_SRC_DIR}/wizardviewstepmodel.cpp ${APP_SRC_DIR}/avatarregistry.cpp ${APP_SRC_DIR}/currentconversation.cpp + ${APP_SRC_DIR}/currentconversationmembers.cpp ${APP_SRC_DIR}/currentaccount.cpp ${APP_SRC_DIR}/videodevices.cpp ${APP_SRC_DIR}/videoprovider.cpp @@ -275,6 +276,7 @@ set(COMMON_HEADERS ${APP_SRC_DIR}/wizardviewstepmodel.h ${APP_SRC_DIR}/avatarregistry.h ${APP_SRC_DIR}/currentconversation.h + ${APP_SRC_DIR}/currentconversationmembers.h ${APP_SRC_DIR}/currentaccount.h ${APP_SRC_DIR}/videodevices.h ${APP_SRC_DIR}/videoprovider.h diff --git a/daemon b/daemon index 47f3fd14a..3481da56c 160000 --- a/daemon +++ b/daemon @@ -1 +1 @@ -Subproject commit 47f3fd14ab0532ff94fd5317f885689928d9293b +Subproject commit 3481da56c29e378bf3003d067c06e409d5c52c32 diff --git a/src/app/constant/JamiStrings.qml b/src/app/constant/JamiStrings.qml index 4872ddc00..59920b894 100644 --- a/src/app/constant/JamiStrings.qml +++ b/src/app/constant/JamiStrings.qml @@ -828,6 +828,7 @@ Item { property string goToConversation: qsTr("Go to conversation") property string promoteAdministrator: qsTr("Promote to administrator") property string kickMember: qsTr("Kick member") + property string reinstateMember: qsTr("Reinstate member") property string administrator: qsTr("Administrator") property string invited: qsTr("Invited") property string removeMember: qsTr("Remove member") diff --git a/src/app/currentconversation.cpp b/src/app/currentconversation.cpp index b4e4bb45b..a0f940dbe 100644 --- a/src/app/currentconversation.cpp +++ b/src/app/currentconversation.cpp @@ -24,6 +24,7 @@ CurrentConversation::CurrentConversation(LRCInstance* lrcInstance, QObject* pare : QObject(parent) , lrcInstance_(lrcInstance) { + uris_ = new CurrentConversationMembers(lrcInstance, this); // whenever the account changes, reconnect the new conversation model // for updates to the conversation and call state/id connect(lrcInstance_, @@ -59,7 +60,23 @@ CurrentConversation::updateData() if (auto optConv = accInfo.conversationModel->getConversationForUid(convId)) { auto& convInfo = optConv->get(); set_lastSelfMessageId(convInfo.lastSelfMessageId); - set_uris(convInfo.participantsUris()); + QStringList uris, bannedUris; + auto isAdmin = false; + for (const auto& p : convInfo.participants) { + if (p.uri == accInfo.profileInfo.uri) { + isAdmin = p.role == member::Role::ADMIN; + } + if (p.role == member::Role::BANNED) { + bannedUris.push_back(p.uri); + } else { + uris.push_back(p.uri); + } + } + if (isAdmin) { + for (const auto& banned : bannedUris) + uris.push_back(banned); + } + uris_->setMembers(accountId, convId, uris); set_isSwarm(convInfo.isSwarm()); set_isLegacy(convInfo.isLegacy()); set_isCoreDialog(convInfo.isCoreDialog()); @@ -170,6 +187,12 @@ CurrentConversation::setInfo(const QString& key, const QString& value) accInfo.conversationModel->updateConversationInfos(convId, infos); } +CurrentConversationMembers* +CurrentConversation::uris() const +{ + return uris_; +} + void CurrentConversation::onConversationUpdated(const QString& convId) { diff --git a/src/app/currentconversation.h b/src/app/currentconversation.h index 84f640f64..a6245f7a2 100644 --- a/src/app/currentconversation.h +++ b/src/app/currentconversation.h @@ -19,6 +19,7 @@ #pragma once #include "lrcinstance.h" +#include "currentconversationmembers.h" #include <QObject> #include <QString> @@ -32,7 +33,6 @@ class CurrentConversation final : public QObject QML_PROPERTY(QString, id) QML_PROPERTY(QString, title) QML_PROPERTY(QString, description) - QML_PROPERTY(QStringList, uris) QML_PROPERTY(bool, isSwarm) QML_PROPERTY(bool, isLegacy) QML_PROPERTY(bool, isCoreDialog) @@ -66,6 +66,7 @@ public: Q_INVOKABLE QString getPreference(const QString& key) const; Q_INVOKABLE MapStringString getPreferences() const; Q_INVOKABLE void setInfo(const QString& key, const QString& value); + CurrentConversationMembers* uris() const; Q_SIGNALS: void scrollTo(const QString& msgId); @@ -87,6 +88,7 @@ Q_SIGNALS: private: LRCInstance* lrcInstance_; + CurrentConversationMembers* uris_; void connectModel(); }; diff --git a/src/app/currentconversationmembers.cpp b/src/app/currentconversationmembers.cpp new file mode 100644 index 000000000..076ea5aa0 --- /dev/null +++ b/src/app/currentconversationmembers.cpp @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2023 Savoir-faire Linux Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "currentconversationmembers.h" + +#include <algorithm> +#include <random> + +CurrentConversationMembers::CurrentConversationMembers(LRCInstance* lrcInstance, QObject* parent) + : QAbstractListModel(parent) + , lrcInstance_(lrcInstance) +{} + +int +CurrentConversationMembers::rowCount(const QModelIndex& parent) const +{ + if (parent.isValid()) + return 0; + return members_.size(); +} + +void +CurrentConversationMembers::setMembers(const QString& accountId, + const QString& convId, + const QStringList& members) +{ + beginResetModel(); + accountId_ = accountId; + convId_ = convId; + members_ = members; + set_count(members.size()); + endResetModel(); +} + +QVariant +CurrentConversationMembers::data(const QModelIndex& index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + auto member = members_.at(index.row()); + + switch (role) { + case Members::Role::MemberUri: + return QVariant::fromValue(member); + case Members::Role::MemberRole: + return QVariant::fromValue( + lrcInstance_->getAccountInfo(accountId_).conversationModel->memberRole(convId_, member)); + } + return QVariant(); +} + +QHash<int, QByteArray> +CurrentConversationMembers::roleNames() const +{ + using namespace Members; + QHash<int, QByteArray> roles; +#define X(role) roles[role] = #role; + MEMBERS_ROLES +#undef X + return roles; +} diff --git a/src/app/currentconversationmembers.h b/src/app/currentconversationmembers.h new file mode 100644 index 000000000..bd082c470 --- /dev/null +++ b/src/app/currentconversationmembers.h @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2023 Savoir-faire Linux Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +#pragma once + +#include "lrcinstance.h" +#include "appsettingsmanager.h" +#include "qtutils.h" + +#include <QAbstractListModel> +#include <QObject> + +#define MEMBERS_ROLES \ + X(MemberUri) \ + X(MemberRole) + +namespace Members { +Q_NAMESPACE +enum Role { + DummyRole = Qt::UserRole + 1, +#define X(role) role, + MEMBERS_ROLES +#undef X +}; +Q_ENUM_NS(Role) +} // namespace Members + +class CurrentConversationMembers : public QAbstractListModel +{ + Q_OBJECT + QML_PROPERTY(int, count) + +public: + explicit CurrentConversationMembers(LRCInstance* lrcInstance, QObject* parent = nullptr); + void setMembers(const QString& accountId, const QString& convId, const QStringList& members); + + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + QHash<int, QByteArray> roleNames() const override; + +private: + LRCInstance* lrcInstance_; + QString accountId_; + QString convId_; + QStringList members_; +}; \ No newline at end of file diff --git a/src/app/mainview/components/AddMemberPanel.qml b/src/app/mainview/components/AddMemberPanel.qml index dc8c02557..5c91db5d9 100644 --- a/src/app/mainview/components/AddMemberPanel.qml +++ b/src/app/mainview/components/AddMemberPanel.qml @@ -63,10 +63,9 @@ Rectangle { model: ContactAdapter.getContactSelectableModel(type) Connections { - enabled: visible - target: CurrentConversation + target: CurrentConversationMembers - function onUrisChanged(uris) { + function onCountChanged() { contactPickerListView.model = ContactAdapter.getContactSelectableModel(type) } } diff --git a/src/app/mainview/components/ChatView.qml b/src/app/mainview/components/ChatView.qml index 77b64b11c..38faae52b 100644 --- a/src/app/mainview/components/ChatView.qml +++ b/src/app/mainview/components/ChatView.qml @@ -157,13 +157,6 @@ Rectangle { Connections { target: CurrentConversation - function onUrisChanged(uris) { - if (CurrentConversation.uris.length >= 8 && addMemberPanel.visible) { - swarmDetailsPanel.visible = false - addMemberPanel.visible = !addMemberPanel.visible - } - } - function onNeedsHost() { viewCoordinator.presentDialog( appWindow, @@ -171,6 +164,17 @@ Rectangle { } } + Connections { + target: CurrentConversationMembers + + function onCountChanged() { + if (CurrentConversationMembers.count >= 8 && addMemberPanel.visible) { + swarmDetailsPanel.visible = false + addMemberPanel.visible = !addMemberPanel.visible + } + } + } + onAddToConversationClicked: { swarmDetailsPanel.visible = false if (addMemberPanel.visible) { diff --git a/src/app/mainview/components/ChatViewHeader.qml b/src/app/mainview/components/ChatViewHeader.qml index 599fef401..468f3da14 100644 --- a/src/app/mainview/components/ChatViewHeader.qml +++ b/src/app/mainview/components/ChatViewHeader.qml @@ -208,7 +208,7 @@ Rectangle { normalColor: JamiTheme.chatviewBgColor imageColor: JamiTheme.chatviewButtonColor - visible: CurrentConversation.uris.length < 8 && addMemberVisibility + visible: CurrentConversationMembers.count < 8 && addMemberVisibility onClicked: addToConversationClicked() } diff --git a/src/app/mainview/components/PluginHandlerPicker.qml b/src/app/mainview/components/PluginHandlerPicker.qml index 936cef4f8..59c5413ba 100644 --- a/src/app/mainview/components/PluginHandlerPicker.qml +++ b/src/app/mainview/components/PluginHandlerPicker.qml @@ -60,7 +60,7 @@ Popup { if (isCall) { pluginhandlerPickerListView.model = PluginAdapter.getMediaHandlerSelectableModel(CurrentCall.id) } else { - var peerId = CurrentConversation.isSwarm ? CurrentConversation.id : CurrentConversation.uris[0] + var peerId = CurrentConversation.isSwarm ? CurrentConversation.id : CurrentConversationMembers[0] pluginhandlerPickerListView.model = PluginAdapter.getChatHandlerSelectableModel(LRCInstance.currentAccountId, peerId) } } @@ -72,7 +72,7 @@ Popup { pluginhandlerPickerListView.model = PluginAdapter.getMediaHandlerSelectableModel(CurrentCall.id) } else { var accountId = LRCInstance.currentAccountId - var peerId = CurrentConversation.isSwarm ? CurrentConversation.id : CurrentConversation.uris[0] + var peerId = CurrentConversation.isSwarm ? CurrentConversation.id : CurrentConversationMembers[0] PluginModel.toggleChatHandler(handlerId, accountId, peerId, !isLoaded) pluginhandlerPickerListView.model = PluginAdapter.getChatHandlerSelectableModel(accountId, peerId) } @@ -127,7 +127,7 @@ Popup { if (isCall) { return PluginAdapter.getMediaHandlerSelectableModel(CurrentCall.id) } else { - var peerId = CurrentConversation.isSwarm ? CurrentConversation.id : CurrentConversation.uris[0] + var peerId = CurrentConversation.isSwarm ? CurrentConversation.id : CurrentConversationMembers[0] return PluginAdapter.getChatHandlerSelectableModel(LRCInstance.currentAccountId, peerId) } } diff --git a/src/app/mainview/components/SwarmDetailsPanel.qml b/src/app/mainview/components/SwarmDetailsPanel.qml index 50db6f315..6f85610dc 100644 --- a/src/app/mainview/components/SwarmDetailsPanel.qml +++ b/src/app/mainview/components/SwarmDetailsPanel.qml @@ -238,7 +238,7 @@ Rectangle { down: tabBar.currentIndex === 1 labelText: { - var membersNb = CurrentConversation.uris.length; + var membersNb = CurrentConversationMembers.count; if (membersNb > 1) return JamiStrings.members.arg(membersNb) return JamiStrings.member @@ -605,7 +605,7 @@ Rectangle { } } - model: CurrentConversation.uris + model: CurrentConversationMembers delegate: ItemDelegate { id: member @@ -626,11 +626,11 @@ Rectangle { id: memberMouseArea anchors.fill: parent - enabled: modelData !== CurrentAccount.uri + enabled: MemberUri !== CurrentAccount.uri acceptedButtons: Qt.RightButton onClicked: function (mouse) { var position = mapToItem(members, mouse.x, mouse.y) - contextMenu.openMenuAt(position.x, position.y, modelData) + contextMenu.openMenuAt(position.x, position.y, MemberUri) } } @@ -640,19 +640,17 @@ Rectangle { anchors.rightMargin: JamiTheme.preferredMarginSize Avatar { + id: avatar width: JamiTheme.smartListAvatarSize height: JamiTheme.smartListAvatarSize Layout.leftMargin: JamiTheme.preferredMarginSize Layout.topMargin: JamiTheme.preferredMarginSize / 2 z: -index - opacity: { - var role = UtilsAdapter.getParticipantRole(CurrentAccount.id, CurrentConversation.id, modelData) - return role === Member.Role.INVITED ? 0.5 : 1 - } + opacity: (MemberRole === Member.Role.INVITED || MemberRole === Member.Role.BANNED)? 0.5 : 1 - imageId: CurrentAccount.uri == modelData ? CurrentAccount.id : modelData - showPresenceIndicator: UtilsAdapter.getContactPresence(CurrentAccount.id, modelData) - mode: CurrentAccount.uri == modelData ? Avatar.Mode.Account : Avatar.Mode.Contact + imageId: CurrentAccount.uri == MemberUri ? CurrentAccount.id : MemberUri + showPresenceIndicator: UtilsAdapter.getContactPresence(CurrentAccount.id, MemberUri) + mode: CurrentAccount.uri == MemberUri ? Avatar.Mode.Account : Avatar.Mode.Contact } ElidedTextLabel { @@ -662,16 +660,12 @@ Rectangle { Layout.topMargin: JamiTheme.preferredMarginSize / 2 Layout.fillWidth: true - eText: UtilsAdapter.getContactBestName(CurrentAccount.id, modelData) + eText: UtilsAdapter.getContactBestName(CurrentAccount.id, MemberUri) maxWidth: width font.pointSize: JamiTheme.participantFontSize color: JamiTheme.primaryForegroundColor - opacity: { - var role = UtilsAdapter.getParticipantRole(CurrentAccount.id, CurrentConversation.id, modelData) - return role === Member.Role.INVITED ? 0.5 : 1 - } - + opacity: (MemberRole === Member.Role.INVITED || MemberRole === Member.Role.BANNED)? 0.5 : 1 font.kerning: true verticalAlignment: Text.AlignVCenter @@ -682,27 +676,25 @@ Rectangle { } ElidedTextLabel { - id: role + id: roleLabel Layout.preferredHeight: JamiTheme.preferredFieldHeight Layout.topMargin: JamiTheme.preferredMarginSize / 2 eText: { - var role = UtilsAdapter.getParticipantRole(CurrentAccount.id, CurrentConversation.id, modelData) - if (role === Member.Role.ADMIN) + if (MemberRole === Member.Role.ADMIN) return JamiStrings.administrator - if (role === Member.Role.INVITED) + if (MemberRole === Member.Role.INVITED) return JamiStrings.invited + if (MemberRole === Member.Role.BANNED) + return JamiStrings.banned return "" } maxWidth: JamiTheme.preferredFieldWidth font.pointSize: JamiTheme.participantFontSize color: JamiTheme.textColorHovered - opacity: { - var role = UtilsAdapter.getParticipantRole(CurrentAccount.id, CurrentConversation.id, modelData) - return role === Member.Role.INVITED ? 0.5 : 1 - } + opacity: (MemberRole === Member.Role.INVITED || MemberRole === Member.Role.BANNED)? 0.5 : 1 font.kerning: true horizontalAlignment: Text.AlignRight diff --git a/src/app/mainview/components/SwarmParticipantContextMenu.qml b/src/app/mainview/components/SwarmParticipantContextMenu.qml index fbaff5551..f30526af0 100644 --- a/src/app/mainview/components/SwarmParticipantContextMenu.qml +++ b/src/app/mainview/components/SwarmParticipantContextMenu.qml @@ -74,12 +74,17 @@ ContextMenuAutoLoader { }, GeneralMenuItem { id: kickMember - itemName: JamiStrings.kickMember + property var memberRole: UtilsAdapter.getParticipantRole(CurrentAccount.id, conversationId, participantUri) + itemName: memberRole === Member.Role.BANNED ? JamiStrings.reinstateMember : JamiStrings.kickMember iconSource: JamiResources.kick_member_svg canTrigger: role === Member.Role.ADMIN onClicked: { - MessagesAdapter.removeConversationMember(conversationId, participantUri) + if (memberRole === Member.Role.BANNED) { + MessagesAdapter.addConversationMember(conversationId, participantUri) + } else { + MessagesAdapter.removeConversationMember(conversationId, participantUri) + } } } ] diff --git a/src/app/messagesadapter.cpp b/src/app/messagesadapter.cpp index b7bd9d093..b96f68e06 100644 --- a/src/app/messagesadapter.cpp +++ b/src/app/messagesadapter.cpp @@ -516,6 +516,13 @@ MessagesAdapter::removeConversationMember(const QString& convUid, const QString& accInfo.conversationModel->removeConversationMember(convUid, memberUri); } +void +MessagesAdapter::addConversationMember(const QString& convUid, const QString& memberUri) +{ + auto& accInfo = lrcInstance_->getCurrentAccountInfo(); + accInfo.conversationModel->addConversationMember(convUid, memberUri); +} + void MessagesAdapter::removeContact(const QString& convUid, bool banContact) { diff --git a/src/app/messagesadapter.h b/src/app/messagesadapter.h index bf5644d1e..afa1389d5 100644 --- a/src/app/messagesadapter.h +++ b/src/app/messagesadapter.h @@ -85,6 +85,7 @@ protected: Q_INVOKABLE void connectConversationModel(); Q_INVOKABLE void sendConversationRequest(); Q_INVOKABLE void removeConversation(const QString& convUid); + Q_INVOKABLE void addConversationMember(const QString& convUid, const QString& participantUri); Q_INVOKABLE void removeConversationMember(const QString& convUid, const QString& participantUri); Q_INVOKABLE void removeContact(const QString& convUid, bool banContact = false); Q_INVOKABLE void clearConversationHistory(const QString& accountId, const QString& convUid); diff --git a/src/app/qmlregister.cpp b/src/app/qmlregister.cpp index 80faf8f8f..bc7a88dd4 100644 --- a/src/app/qmlregister.cpp +++ b/src/app/qmlregister.cpp @@ -141,6 +141,7 @@ registerTypes(QQmlEngine* engine, QML_REGISTERSINGLETONTYPE_POBJECT(NS_ADAPTERS, pluginAdapter, "PluginAdapter"); QML_REGISTERSINGLETONTYPE_POBJECT(NS_ADAPTERS, currentCall, "CurrentCall"); QML_REGISTERSINGLETONTYPE_POBJECT(NS_ADAPTERS, currentConversation, "CurrentConversation"); + QML_REGISTERSINGLETONTYPE_POBJECT(NS_ADAPTERS, currentConversation->uris(), "CurrentConversationMembers"); QML_REGISTERSINGLETONTYPE_POBJECT(NS_ADAPTERS, currentAccount, "CurrentAccount"); QML_REGISTERSINGLETONTYPE_POBJECT(NS_ADAPTERS, videoDevices, "VideoDevices"); QML_REGISTERSINGLETONTYPE_POBJECT(NS_ADAPTERS, currentAccountToMigrate, "CurrentAccountToMigrate") -- GitLab