From f05ffbce4442392d3c4dda12335138f86bc4c031 Mon Sep 17 00:00:00 2001
From: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
Date: Thu, 29 Jul 2021 16:59:18 -0400
Subject: [PATCH] swarm: prevent interacting with read-only conversations

Read-only conversation has:
- a lock icon in the smartlist item
- no message send bar in the chat view
- no call actions available from the chat view header
- no call actions available from the smartlist context menu
- no double click to call

Change-Id: I0497b94038b450f0a0a22fc007de1281b7b2214d
Gitlab: #475
---
 resources/icons/lock_black_24dp.svg           |  1 +
 src/conversationlistmodelbase.cpp             |  2 +
 src/conversationlistmodelbase.h               |  3 +-
 src/conversationsadapter.cpp                  | 13 +++++--
 src/conversationsadapter.h                    |  2 +-
 .../ConversationSmartListContextMenu.qml      |  4 +-
 src/mainview/components/MessageWebView.qml    | 17 +++++++--
 .../components/MessageWebViewFooter.qml       |  9 ++++-
 .../components/MessageWebViewHeader.qml       |  8 ++--
 .../components/SmartListItemDelegate.qml      | 17 +++++++--
 src/messagesadapter.cpp                       | 37 +++++++++++--------
 src/messagesadapter.h                         | 11 +++---
 12 files changed, 82 insertions(+), 42 deletions(-)
 create mode 100644 resources/icons/lock_black_24dp.svg

diff --git a/resources/icons/lock_black_24dp.svg b/resources/icons/lock_black_24dp.svg
new file mode 100644
index 000000000..1a38f2f68
--- /dev/null
+++ b/resources/icons/lock_black_24dp.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><g fill="none"><path d="M0 0h24v24H0V0z"/><path d="M0 0h24v24H0V0z" opacity=".87"/></g><path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zM9 6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9V6zm9 14H6V10h12v10zm-6-3c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z"/></svg>
\ No newline at end of file
diff --git a/src/conversationlistmodelbase.cpp b/src/conversationlistmodelbase.cpp
index 4e92d8698..25eb8b821 100644
--- a/src/conversationlistmodelbase.cpp
+++ b/src/conversationlistmodelbase.cpp
@@ -149,6 +149,8 @@ ConversationListModelBase::dataForItem(item_t item, int role) const
             }
         return ret;
     }
+    case Role::ReadOnly:
+        return QVariant(item.readOnly);
     default:
         break;
     }
diff --git a/src/conversationlistmodelbase.h b/src/conversationlistmodelbase.h
index 2359d15ca..36979d3fc 100644
--- a/src/conversationlistmodelbase.h
+++ b/src/conversationlistmodelbase.h
@@ -48,7 +48,8 @@
     X(IsRequest) \
     X(Mode) \
     X(Uris) \
-    X(Monikers)
+    X(Monikers) \
+    X(ReadOnly)
 
 namespace ConversationList {
 Q_NAMESPACE
diff --git a/src/conversationsadapter.cpp b/src/conversationsadapter.cpp
index 497f347bd..74e2567f1 100644
--- a/src/conversationsadapter.cpp
+++ b/src/conversationsadapter.cpp
@@ -274,8 +274,14 @@ ConversationsAdapter::onProfileUpdated(const QString& contactUri)
 }
 
 void
-ConversationsAdapter::onConversationUpdated(const QString&)
+ConversationsAdapter::onConversationUpdated(const QString& convId)
 {
+    // this could be the result of a member event
+    auto& convInfo = lrcInstance_->getConversationFromConvUid(convId);
+    if (convInfo.uid.isEmpty())
+        return;
+    set_currentConvIsReadOnly(convInfo.readOnly);
+
     updateConversationFilterData();
 }
 
@@ -283,8 +289,6 @@ void
 ConversationsAdapter::onFilterChanged()
 {
     updateConversationFilterData();
-    if (!lrcInstance_->get_selectedConvUid().isEmpty())
-        Q_EMIT indexRepositionRequested();
 }
 
 void
@@ -437,7 +441,8 @@ ConversationsAdapter::getConvInfoMap(const QString& convId)
             {"needsSyncing", convInfo.needsSyncing},
             {"isAudioOnly", isAudioOnly},
             {"callState", static_cast<int>(callState)},
-            {"callStackViewShouldShow", callStackViewShouldShow}};
+            {"callStackViewShouldShow", callStackViewShouldShow},
+            {"readOnly", convInfo.readOnly}};
 }
 
 bool
diff --git a/src/conversationsadapter.h b/src/conversationsadapter.h
index 60a066100..cae66565b 100644
--- a/src/conversationsadapter.h
+++ b/src/conversationsadapter.h
@@ -36,6 +36,7 @@ class ConversationsAdapter final : public QmlAdapterBase
     QML_PROPERTY(bool, filterRequests)
     QML_PROPERTY(int, totalUnreadMessageCount)
     QML_PROPERTY(int, pendingRequestCount)
+    QML_PROPERTY(bool, currentConvIsReadOnly)
 
 public:
     explicit ConversationsAdapter(SystemTray* systemTray,
@@ -56,7 +57,6 @@ Q_SIGNALS:
     void showSearchStatus(const QString& status);
 
     void navigateToWelcomePageRequested();
-    void indexRepositionRequested();
     void conversationReady(const QString& convId);
 
 private Q_SLOTS:
diff --git a/src/mainview/components/ConversationSmartListContextMenu.qml b/src/mainview/components/ConversationSmartListContextMenu.qml
index a23d486b8..dd9fe7ff6 100644
--- a/src/mainview/components/ConversationSmartListContextMenu.qml
+++ b/src/mainview/components/ConversationSmartListContextMenu.qml
@@ -45,7 +45,7 @@ ContextMenuAutoLoader {
         GeneralMenuItem {
             id: startVideoCallItem
 
-            canTrigger: !hasCall
+            canTrigger: !hasCall && !ConversationsAdapter.currentConvIsReadOnly
             itemName: JamiStrings.startVideoCall
             iconSource: JamiResources.videocam_24dp_svg
             onClicked: {
@@ -59,7 +59,7 @@ ContextMenuAutoLoader {
         GeneralMenuItem {
             id: startAudioCall
 
-            canTrigger: !hasCall
+            canTrigger: !hasCall && !ConversationsAdapter.currentConvIsReadOnly
             itemName: JamiStrings.startAudioCall
             iconSource: JamiResources.place_audiocall_24dp_svg
             onClicked: {
diff --git a/src/mainview/components/MessageWebView.qml b/src/mainview/components/MessageWebView.qml
index 35707086a..84aac50b8 100644
--- a/src/mainview/components/MessageWebView.qml
+++ b/src/mainview/components/MessageWebView.qml
@@ -114,9 +114,10 @@ Rectangle {
     Connections {
         target: MessagesAdapter
 
-        function onChangeInvitationViewRequest(show, isSwarm, needsSyncing,
-                                               title, convId) {
-            if (show)
+        function onSetChatViewMode(showInvitationPage,
+                                   isSwarm, needsSyncing,
+                                   title, convId) {
+            if (showInvitationPage)
                 root.mode = MessageWebView.Mode.Invitation
             else {
                 root.mode = MessageWebView.Mode.Chat
@@ -129,6 +130,16 @@ Rectangle {
         }
     }
 
+    Connections {
+        target: ConversationsAdapter
+
+        function onCurrentConvIsReadOnlyChanged() {
+            var isVisible = !ConversationsAdapter.currentConvIsReadOnly
+            setMessagingHeaderButtonsVisible(isVisible)
+            messageWebViewFooter.visible = isVisible
+        }
+    }
+
     QtObject {
         id: jsBridgeObject
 
diff --git a/src/mainview/components/MessageWebViewFooter.qml b/src/mainview/components/MessageWebViewFooter.qml
index ac60be724..48821c80f 100644
--- a/src/mainview/components/MessageWebViewFooter.qml
+++ b/src/mainview/components/MessageWebViewFooter.qml
@@ -83,8 +83,13 @@ Rectangle {
             messageBar.textAreaObj.pasteText()
         }
 
-        function onChangeInvitationViewRequest(show, isSwarm) {
-            var footerVisibility = show ? !isSwarm : !show
+        function onSetChatViewMode(showInvitationPage,
+                                   isSwarm, needsSyncing,
+                                   title, convId, readOnly) {
+            var footerVisibility = showInvitationPage ?
+                        !isSwarm :
+                        !showInvitationPage
+            footerVisibility &= !readOnly
             messageBar.visible = footerVisibility
             dataTransferSendContainer.visible = footerVisibility
             root.visible = footerVisibility
diff --git a/src/mainview/components/MessageWebViewHeader.qml b/src/mainview/components/MessageWebViewHeader.qml
index 7d39aeda7..bc53aac53 100644
--- a/src/mainview/components/MessageWebViewHeader.qml
+++ b/src/mainview/components/MessageWebViewHeader.qml
@@ -27,7 +27,7 @@ import net.jami.Adapters 1.0
 import "../../commoncomponents"
 
 Rectangle {
-    id: messagingHeaderRect
+    id: root
 
     property string userAliasLabelText
     property string userUserNameLabelText
@@ -72,9 +72,9 @@ Rectangle {
 
             onClicked: {
                 if (backToWelcomeViewButtonSource === JamiResources.back_24dp_svg)
-                    messagingHeaderRect.backClicked()
+                    root.backClicked()
                 else
-                    messagingHeaderRect.needToHideConversationInCall()
+                    root.needToHideConversationInCall()
             }
         }
 
@@ -84,7 +84,7 @@ Rectangle {
             Layout.alignment: Qt.AlignLeft | Qt.AlignTop
 
             // Width + margin.
-            Layout.preferredWidth: messagingHeaderRect.width
+            Layout.preferredWidth: root.width
                                    - backToWelcomeViewButton.width - buttonGroup.width - 45
             Layout.fillHeight: true
             Layout.topMargin: 7
diff --git a/src/mainview/components/SmartListItemDelegate.qml b/src/mainview/components/SmartListItemDelegate.qml
index 52a6ff309..9aae5880b 100644
--- a/src/mainview/components/SmartListItemDelegate.qml
+++ b/src/mainview/components/SmartListItemDelegate.qml
@@ -107,8 +107,15 @@ ItemDelegate {
             }
         }
 
+        // read-only conversation indicator
+        ResponsiveImage {
+            visible: ReadOnly
+            source: JamiResources.lock_black_24dp_svg
+            color: JamiTheme.primaryForegroundColor
+        }
+
         ColumnLayout {
-            visible: InCall || UnreadMessagesCount
+            visible: (InCall || UnreadMessagesCount) && !ReadOnly
             Layout.preferredWidth: childrenRect.width
             Layout.fillHeight: true
             spacing: 2
@@ -153,9 +160,11 @@ ItemDelegate {
         ListView.view.model.select(index)
         if (LRCInstance.currentAccountType === Profile.Type.SIP)
             CallAdapter.placeAudioOnlyCall()
-        else
-            CallAdapter.placeCall()
-
+        else {
+            if (!ConversationsAdapter.currentConvIsReadOnly) {
+                CallAdapter.placeCall()
+            }
+        }
         // TODO: factor this out (visible should be observing)
         communicationPageMessageWebView.setSendContactRequestButtonVisible(false)
     }
diff --git a/src/messagesadapter.cpp b/src/messagesadapter.cpp
index 9782c9b83..9e9843fa2 100644
--- a/src/messagesadapter.cpp
+++ b/src/messagesadapter.cpp
@@ -62,21 +62,24 @@ MessagesAdapter::setupChatView(const QVariantMap& convInfo)
     bool isLegacy = !convInfo["isSwarm"].toBool();
     bool isRequest = convInfo["isRequest"].toBool();
     bool needsSyncing = convInfo["needsSyncing"].toBool();
+    bool readOnly = convInfo["readOnly"].toBool();
 
     QMetaObject::invokeMethod(qmlObj_,
                               "setSendContactRequestButtonVisible",
                               Q_ARG(QVariant, isLegacy && isRequest));
     QMetaObject::invokeMethod(qmlObj_,
                               "setMessagingHeaderButtonsVisible",
-                              Q_ARG(QVariant, !(isLegacy && (isRequest || needsSyncing))));
+                              Q_ARG(QVariant,
+                                    !readOnly && !(isLegacy && (isRequest || needsSyncing))));
 
     setMessagesVisibility(false);
     setIsSwarm(!isLegacy);
-    Q_EMIT changeInvitationViewRequest(isRequest || needsSyncing,
-                                       isLegacy,
-                                       needsSyncing,
-                                       convInfo["title"].toString(),
-                                       convInfo["convId"].toString());
+    Q_EMIT setChatViewMode(isRequest || needsSyncing,
+                           isLegacy,
+                           needsSyncing,
+                           convInfo["title"].toString(),
+                           convInfo["convId"].toString(),
+                           readOnly);
 
     Utils::oneShotConnect(qmlObj_, SIGNAL(messagesCleared()), this, SLOT(slotMessagesCleared()));
     clearChatView();
@@ -369,14 +372,16 @@ MessagesAdapter::setConversationProfileData(const lrc::api::conversation::Info&
         QMetaObject::invokeMethod(qmlObj_,
                                   "setMessagingHeaderButtonsVisible",
                                   Q_ARG(QVariant,
-                                        !(convInfo.isSwarm()
-                                          && (convInfo.isRequest || convInfo.needsSyncing))));
-
-        Q_EMIT changeInvitationViewRequest(convInfo.isRequest or convInfo.needsSyncing,
-                                           convInfo.isSwarm(),
-                                           convInfo.needsSyncing,
-                                           title,
-                                           convInfo.uid);
+                                        !convInfo.readOnly
+                                            && !(convInfo.isSwarm()
+                                                 && (convInfo.isRequest || convInfo.needsSyncing))));
+
+        Q_EMIT setChatViewMode(convInfo.isRequest || convInfo.needsSyncing,
+                               convInfo.isSwarm(),
+                               convInfo.needsSyncing,
+                               title,
+                               convInfo.uid,
+                               convInfo.readOnly);
         if (convInfo.isSwarm())
             return;
 
@@ -593,7 +598,7 @@ MessagesAdapter::refuseInvitation(const QString& convUid)
 {
     const auto currentConvUid = convUid.isEmpty() ? lrcInstance_->get_selectedConvUid() : convUid;
     lrcInstance_->getCurrentConversationModel()->removeConversation(currentConvUid, false);
-    changeInvitationViewRequest(false);
+    Q_EMIT setChatViewMode(false);
 }
 
 void
@@ -601,7 +606,7 @@ MessagesAdapter::blockConversation(const QString& convUid)
 {
     const auto currentConvUid = convUid.isEmpty() ? lrcInstance_->get_selectedConvUid() : convUid;
     lrcInstance_->getCurrentConversationModel()->removeConversation(currentConvUid, true);
-    changeInvitationViewRequest(false);
+    Q_EMIT setChatViewMode(false);
 }
 
 void
diff --git a/src/messagesadapter.h b/src/messagesadapter.h
index 69f5d0103..f8651c468 100644
--- a/src/messagesadapter.h
+++ b/src/messagesadapter.h
@@ -93,11 +93,12 @@ Q_SIGNALS:
     void newMessageBarPlaceholderText(QString placeholderText);
     void newFilePasted(QString filePath);
     void newTextPasted();
-    void changeInvitationViewRequest(bool show,
-                                     bool isSwarm = false,
-                                     bool needsSyncing = false,
-                                     QString contactTitle = {},
-                                     QString contactUri = {});
+    void setChatViewMode(bool showInvitationPage,
+                         bool isSwarm = false,
+                         bool needsSyncing = false,
+                         QString contactTitle = {},
+                         QString contactUri = {},
+                         bool readOnly = false);
 
 private Q_SLOTS:
     void slotMessagesCleared();
-- 
GitLab