From 406a251c855a63f623230449b66a93466150a759 Mon Sep 17 00:00:00 2001
From: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
Date: Fri, 7 Jun 2024 14:44:02 -0400
Subject: [PATCH] accountmodel: improve handling for account list reordering

This update fixes an issue where a stale account list might be used
in AccountListModel by synchronizing list invalidation with a newly
introduced signal (accountsReordered). This change prevents a data race
that could occur due to the asynchronous nature of setting the account
order over D-Bus, or when queuing the signal handling for
accountsChanged, which is emitted only once the reordering is saved.

As a result, QAbstractItemModel mutations are now performed within AccountListModel instead of in the UI.

Gitlab: #1638 (Account list in popup is incorrect after selection)
Change-Id: I7ed6eeb45eb319f21e40554f3d023ad24e139a6f
---
 src/app/accountlistmodel.cpp                  | 30 +++++++++++++++++++
 src/app/lrcinstance.cpp                       | 10 +++++--
 .../mainview/components/AccountComboBox.qml   | 18 -----------
 .../components/AccountComboBoxPopup.qml       | 22 --------------
 src/libclient/accountmodel.cpp                |  3 +-
 src/libclient/api/accountmodel.h              |  4 +++
 6 files changed, 43 insertions(+), 44 deletions(-)

diff --git a/src/app/accountlistmodel.cpp b/src/app/accountlistmodel.cpp
index ac01baccc..2b70f7834 100644
--- a/src/app/accountlistmodel.cpp
+++ b/src/app/accountlistmodel.cpp
@@ -29,6 +29,35 @@ AccountListModel::AccountListModel(LRCInstance* instance, QObject* parent)
     : AbstractListModelBase(parent)
 {
     lrcInstance_ = instance;
+
+    // Avoid resetting/redrawing the model when the account status changes.
+    QObject::connect(&lrcInstance_->accountModel(),
+                     &AccountModel::accountStatusChanged,
+                     this,
+                     [&](const QString& accountId) {
+                         auto accountList = lrcInstance_->accountModel().getAccountList();
+                         auto index = accountList.indexOf(accountId);
+                         if (index != -1) {
+                             QModelIndex modelIndex = QAbstractListModel::index(index, 0);
+                             Q_EMIT dataChanged(modelIndex, modelIndex /*, ALL ROLES */);
+                         }
+                     });
+    // If there's a reorder, it's reasonable to reset the model for simplicity, instead
+    // of computing the difference. The same goes for accounts being added and removed.
+    // These operations will only occur when the list is hidden, unless dbus is used while
+    // the list is visible.
+    QObject::connect(&lrcInstance_->accountModel(),
+                     &AccountModel::accountsReordered,
+                     this,
+                     &AccountListModel::reset);
+    QObject::connect(&lrcInstance_->accountModel(),
+                     &AccountModel::accountAdded,
+                     this,
+                     &AccountListModel::reset);
+    QObject::connect(&lrcInstance_->accountModel(),
+                     &AccountModel::accountRemoved,
+                     this,
+                     &AccountListModel::reset);
 }
 
 int
@@ -91,6 +120,7 @@ AccountListModel::roleNames() const
 void
 AccountListModel::reset()
 {
+    // Used to invalidate proxy models.
     beginResetModel();
     endResetModel();
 }
diff --git a/src/app/lrcinstance.cpp b/src/app/lrcinstance.cpp
index af6247934..e7c381ad5 100644
--- a/src/app/lrcinstance.cpp
+++ b/src/app/lrcinstance.cpp
@@ -43,9 +43,8 @@ LRCInstance::LRCInstance(const QString& updateUrl,
     muteDaemon_ = muteDaemon;
     threadPool_->setMaxThreadCount(1);
 
-    connect(this, &LRCInstance::currentAccountIdChanged, [this] {
-        // save to config, editing the accountlistmodel's underlying data
-        accountModel().setTopAccount(currentAccountId_);
+    // Update the current account when the account list changes.
+    connect(&accountModel(), &AccountModel::accountsReordered, this, [this] {
         Q_EMIT accountListChanged();
 
         profile::Info profileInfo;
@@ -62,6 +61,11 @@ LRCInstance::LRCInstance(const QString& updateUrl,
         set_currentAccountAvatarSet(!profileInfo.avatar.isEmpty());
     });
 
+    connect(this, &LRCInstance::currentAccountIdChanged, [this] {
+        // This will trigger `AccountModel::accountsReordered`.
+        accountModel().setTopAccount(currentAccountId_);
+    });
+
     connect(&accountModel(), &AccountModel::profileUpdated, this, [this](const QString& id) {
         if (id != currentAccountId_)
             return;
diff --git a/src/app/mainview/components/AccountComboBox.qml b/src/app/mainview/components/AccountComboBox.qml
index ea149f614..7173cc80d 100644
--- a/src/app/mainview/components/AccountComboBox.qml
+++ b/src/app/mainview/components/AccountComboBox.qml
@@ -34,24 +34,6 @@ Label {
 
     property bool inSettings: viewCoordinator.currentViewName === "SettingsView"
 
-    // TODO: remove these refresh hacks use QAbstractItemModels correctly
-    Connections {
-        target: AccountAdapter
-
-        function onAccountStatusChanged(accountId) {
-            AccountListModel.reset();
-        }
-    }
-
-    Connections {
-        target: LRCInstance
-
-        function onAccountListChanged() {
-            root.update();
-            AccountListModel.reset();
-        }
-    }
-
     function togglePopup() {
         if (root.popup.opened) {
             root.popup.close();
diff --git a/src/app/mainview/components/AccountComboBoxPopup.qml b/src/app/mainview/components/AccountComboBoxPopup.qml
index 9bfac5c91..35118d233 100644
--- a/src/app/mainview/components/AccountComboBoxPopup.qml
+++ b/src/app/mainview/components/AccountComboBoxPopup.qml
@@ -60,23 +60,6 @@ Popup {
 
             property bool inSettings: viewCoordinator.currentViewName === "SettingsView"
 
-            // TODO: remove these refresh hacks use QAbstractItemModels correctly
-            Connections {
-                target: AccountAdapter
-
-                function onAccountStatusChanged(accountId) {
-                    AccountListModel.reset();
-                }
-            }
-
-            Connections {
-                target: LRCInstance
-
-                function onAccountListChanged() {
-                    AccountListModel.reset();
-                }
-            }
-
             RowLayout {
                 id: mainLayout
                 anchors.fill: parent
@@ -257,11 +240,6 @@ Popup {
             color: JamiTheme.smartListHoveredColor
         }
 
-        // fake footer item as workaround for Qt 5.15 bug
-        // https://bugreports.qt.io/browse/QTBUG-85302
-        // don't use the clip trick and footer item overlay
-        // explained here https://stackoverflow.com/a/64625149
-        // as it causes other complexities in handling the drop shadow
         ItemDelegate {
             id: addAccountItem
 
diff --git a/src/libclient/accountmodel.cpp b/src/libclient/accountmodel.cpp
index 45983b970..07af96f4c 100644
--- a/src/libclient/accountmodel.cpp
+++ b/src/libclient/accountmodel.cpp
@@ -428,8 +428,9 @@ AccountModelPimpl::updateAccounts()
     const auto previousAccountIdListSize = accountIdList.size();
     accountIdList = configurationManager.getAccountList();
 
-    // If this is just a reordering of the accounts, don't do anything
+    // If this is just a reordering of the accounts, just notify the view.
     if (accountIdList.size() == previousAccountIdListSize) {
+        Q_EMIT linked.accountsReordered();
         return;
     }
 
diff --git a/src/libclient/api/accountmodel.h b/src/libclient/api/accountmodel.h
index 6f199811f..ba58eddf2 100644
--- a/src/libclient/api/accountmodel.h
+++ b/src/libclient/api/accountmodel.h
@@ -288,6 +288,10 @@ Q_SIGNALS:
      * @param accountID
      */
     void accountRemoved(const QString& accountID);
+    /**
+     * Emitted when the account list order has changed.
+     */
+    void accountsReordered();
     /**
      * Connect this signal to know when an account was updated.
      * @param accountID
-- 
GitLab