From 46a955aa3db2603ff68ef2d41fe70ee1cd26c06b Mon Sep 17 00:00:00 2001
From: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
Date: Wed, 20 Dec 2023 17:16:07 -0500
Subject: [PATCH] misc: fix ups for conversationmodel/messagelistmodel

Removes some remaining excess complexity in the way interactions are managed by the client. Removes raw iterator access and provides thread-safe alternatives.

Change-Id: I482bf599de869245f96c4aab418127f30508ef41
---
 src/app/conversationlistmodelbase.cpp         |  41 +-
 .../mainview/components/MessageListView.qml   |   4 +-
 src/app/messagesadapter.cpp                   |  44 +-
 src/app/messagesadapter.h                     |  11 +-
 src/app/utils.cpp                             |   2 +-
 src/app/utils.h                               |   1 -
 src/libclient/CMakeLists.txt                  |   1 +
 src/libclient/api/conversation.h              |   3 +-
 src/libclient/api/interaction.h               |  21 +-
 src/libclient/api/messagelistmodel.h          | 157 ++++
 src/libclient/authority/storagehelper.cpp     |   2 +-
 src/libclient/conversationmodel.cpp           | 555 ++++++-------
 src/libclient/messagelistmodel.cpp            | 758 ++++++++----------
 src/libclient/messagelistmodel.h              | 167 ----
 14 files changed, 789 insertions(+), 978 deletions(-)
 create mode 100644 src/libclient/api/messagelistmodel.h
 delete mode 100644 src/libclient/messagelistmodel.h

diff --git a/src/app/conversationlistmodelbase.cpp b/src/app/conversationlistmodelbase.cpp
index 250e72464..ff81c1fc8 100644
--- a/src/app/conversationlistmodelbase.cpp
+++ b/src/app/conversationlistmodelbase.cpp
@@ -108,35 +108,38 @@ ConversationListModelBase::dataForItem(item_t item, int role) const
     case Role::UnreadMessagesCount:
         return QVariant(item.unreadMessages);
     case Role::LastInteractionTimeStamp: {
-        if (!item.interactions->empty()) {
-            auto ts = static_cast<qint32>(item.interactions->rbegin()->second.timestamp);
-            return QVariant(ts);
-        }
-        break;
+        qint32 ts = 0;
+        item.interactions->withLast([&ts](const QString&, const interaction::Info& interaction) {
+            ts = interaction.timestamp;
+        });
+        return QVariant(ts);
     }
     case Role::LastInteraction: {
-        if (!item.interactions->empty()) {
-            auto interaction = item.interactions->rbegin()->second;
+        QString lastInteractionBody;
+        item.interactions->withLast([&](const QString&, const interaction::Info& interaction) {
             auto& accInfo = lrcInstance_->getCurrentAccountInfo();
-            if (interaction.type == interaction::Type::DATA_TRANSFER) {
-                return QVariant(interaction.commit.value("displayName"));
+            if (interaction.type == interaction::Type::UPDATE_PROFILE) {
+                lastInteractionBody = interaction::getProfileUpdatedString();
+            } else if (interaction.type == interaction::Type::DATA_TRANSFER) {
+                lastInteractionBody = interaction.commit.value("displayName");
             } else if (interaction.type == lrc::api::interaction::Type::CALL) {
-                return QVariant(interaction::getCallInteractionString(interaction.authorUri
-                                                                          == accInfo.profileInfo.uri,
-                                                                      interaction));
+                const auto isOutgoing = interaction.authorUri == accInfo.profileInfo.uri;
+                lastInteractionBody = interaction::getCallInteractionString(isOutgoing, interaction);
             } else if (interaction.type == lrc::api::interaction::Type::CONTACT) {
                 auto bestName = interaction.authorUri == accInfo.profileInfo.uri
                                     ? accInfo.accountModel->bestNameForAccount(accInfo.id)
                                     : accInfo.contactModel->bestNameForContact(
                                         interaction.authorUri);
-                return QVariant(
-                    interaction::getContactInteractionString(bestName,
-                                                             interaction::to_action(
-                                                                 interaction.commit["action"])));
+                lastInteractionBody
+                    = interaction::getContactInteractionString(bestName,
+                                                               interaction::to_action(
+                                                                   interaction.commit["action"]));
+            } else {
+                lastInteractionBody = interaction.body.isEmpty() ? tr("(deleted message)")
+                                                                 : interaction.body;
             }
-            return QVariant(interaction.body);
-        }
-        break;
+        });
+        return QVariant(lastInteractionBody);
     }
     case Role::IsSwarm:
         return QVariant(item.isSwarm());
diff --git a/src/app/mainview/components/MessageListView.qml b/src/app/mainview/components/MessageListView.qml
index 3e6660fe8..92498f1e7 100644
--- a/src/app/mainview/components/MessageListView.qml
+++ b/src/app/mainview/components/MessageListView.qml
@@ -37,7 +37,6 @@ JamiListView {
 
     function loadMoreMsgsIfNeeded() {
         if (atYBeginning && !CurrentConversation.allMessagesLoaded) {
-            print("load more messages", atYBeginning, CurrentConversation.allMessagesLoaded)
             MessagesAdapter.loadMoreMessages()
         }
     }
@@ -175,7 +174,8 @@ JamiListView {
     Connections {
         target: CurrentConversation
         function onScrollTo(id) {
-            var idx = MessagesAdapter.getMessageIndexFromId(id)
+            // Get the filtered index from the interaction ID.
+            var idx = MessagesAdapter.messageListModel.getDisplayIndex(id)
             positionViewAtIndex(idx, ListView.Visible)
         }
     }
diff --git a/src/app/messagesadapter.cpp b/src/app/messagesadapter.cpp
index f39f0f127..4cd9d6f75 100644
--- a/src/app/messagesadapter.cpp
+++ b/src/app/messagesadapter.cpp
@@ -73,8 +73,6 @@ MessagesAdapter::MessagesAdapter(AppSettingsManager* settingsManager,
         filteredMsgListModel_->setSourceModel(conversation.interactions.get());
 
         set_currentConvComposingList(conversationTypersUrlToName(conversation.typers));
-        mediaInteractions_.reset(new MessageListModel(&lrcInstance_->getCurrentAccountInfo(), this));
-        set_mediaMessageListModel(QVariant::fromValue(mediaInteractions_.get()));
     });
 
     connect(messageParser_, &MessageParser::messageParsed, this, &MessagesAdapter::onMessageParsed);
@@ -83,9 +81,10 @@ MessagesAdapter::MessagesAdapter(AppSettingsManager* settingsManager,
     connect(timestampTimer_, &QTimer::timeout, this, &MessagesAdapter::timestampUpdated);
     timestampTimer_->start(timestampUpdateIntervalMs_);
 
-    connect(lrcInstance_, &LRCInstance::currentAccountIdChanged, this, [this]() {
-        connectConversationModel();
-    });
+    connect(lrcInstance_,
+            &LRCInstance::currentAccountIdChanged,
+            this,
+            &MessagesAdapter::connectConversationModel);
 
     connectConversationModel();
 }
@@ -142,6 +141,9 @@ MessagesAdapter::connectConversationModel()
                      this,
                      &MessagesAdapter::onMessagesFoundProcessed,
                      Qt::UniqueConnection);
+
+    mediaInteractions_.reset(new MessageListModel(&lrcInstance_->getCurrentAccountInfo(), this));
+    set_mediaMessageListModel(QVariant::fromValue(mediaInteractions_.get()));
 }
 
 void
@@ -379,18 +381,7 @@ QVariant
 MessagesAdapter::dataForInteraction(const QString& interactionId, int role) const
 {
     if (auto* model = getMsgListSourceModel()) {
-        auto idx = model->indexOfMessage(interactionId);
-        if (idx != -1)
-            return model->data(idx, role);
-    }
-    return {};
-}
-
-int
-MessagesAdapter::getIndexOfMessage(const QString& interactionId) const
-{
-    if (auto* model = getMsgListSourceModel()) {
-        return model->indexOfMessage(interactionId);
+        return model->data(interactionId, role);
     }
     return {};
 }
@@ -584,7 +575,7 @@ MessagesAdapter::onMessagesFoundProcessed(const QString& accountId,
     bool isSearchInProgress = messageInformation.size();
     if (isSearchInProgress) {
         for (auto it = messageInformation.begin(); it != messageInformation.end(); it++) {
-            mediaInteractions_->insert(qMakePair(it.key(), it.value()));
+            mediaInteractions_->append(it.key(), it.value());
         }
     } else {
         set_mediaMessageListModel(QVariant::fromValue(mediaInteractions_.get()));
@@ -745,23 +736,6 @@ MessagesAdapter::startSearch(const QString& text, bool isMedia)
     }
 }
 
-int
-MessagesAdapter::getMessageIndexFromId(const QString& id)
-{
-    const QString& convId = lrcInstance_->get_selectedConvUid();
-    const auto& conversation = lrcInstance_->getConversationFromConvUid(convId);
-    auto allInteractions = conversation.interactions.get();
-    int index = 0;
-    for (auto it = allInteractions->rbegin(); it != allInteractions->rend(); it++) {
-        if (interaction::isDisplayedInChatview(it->second.type)) {
-            if (it->first == id)
-                return index;
-            index++;
-        }
-    }
-    return -1;
-}
-
 MessageListModel*
 MessagesAdapter::getMsgListSourceModel() const
 {
diff --git a/src/app/messagesadapter.h b/src/app/messagesadapter.h
index 0ae1bdd48..7cd2d2aca 100644
--- a/src/app/messagesadapter.h
+++ b/src/app/messagesadapter.h
@@ -45,12 +45,19 @@ public:
         auto index = sourceModel()->index(sourceRow, 0, sourceParent);
         auto type = static_cast<interaction::Type>(
             sourceModel()->data(index, MessageList::Role::Type).toInt());
-        return interaction::isDisplayedInChatview(type);
+        return interaction::isTypeDisplayable(type);
     };
     bool lessThan(const QModelIndex& left, const QModelIndex& right) const override
     {
         return left.row() > right.row();
     };
+
+    Q_INVOKABLE int getDisplayIndex(const QString& id)
+    {
+        auto sourceRow = ((MessageListModel*) sourceModel())->indexOfMessage(id);
+        auto index = mapFromSource(sourceModel()->index(sourceRow, 0));
+        return index.row();
+    };
 };
 
 class MessagesAdapter final : public QmlAdapterBase
@@ -129,13 +136,11 @@ protected:
                                   const QColor& linkColor = QColor(0x06, 0x45, 0xad),
                                   const QColor& backgroundColor = QColor(0x0, 0x0, 0x0));
     Q_INVOKABLE void onPaste();
-    Q_INVOKABLE int getIndexOfMessage(const QString& messageId) const;
     Q_INVOKABLE QString getStatusString(int status);
     Q_INVOKABLE QVariantMap getTransferStats(const QString& messageId, int);
     Q_INVOKABLE QVariant dataForInteraction(const QString& interactionId,
                                             int role = Qt::DisplayRole) const;
     Q_INVOKABLE void startSearch(const QString& text, bool isMedia);
-    Q_INVOKABLE int getMessageIndexFromId(const QString& id);
 
     // Run corrsponding js functions, c++ to qml.
     void setMessagesImageContent(const QString& path, bool isBased64 = false);
diff --git a/src/app/utils.cpp b/src/app/utils.cpp
index 78f3c8af4..5cad2ce60 100644
--- a/src/app/utils.cpp
+++ b/src/app/utils.cpp
@@ -436,7 +436,7 @@ Utils::contactPhoto(LRCInstance* instance,
             auto avatarName = contactInfo.profileInfo.uri == bestName ? QString() : bestName;
             photo = Utils::fallbackAvatar("jami:" + contactInfo.profileInfo.uri, avatarName);
         }
-    } catch (const std::exception& e) {
+    } catch (const std::exception&) {
         photo = fallbackAvatar("jami:" + contactUri, QString(), size);
     }
     return Utils::scaleAndFrame(photo, size);
diff --git a/src/app/utils.h b/src/app/utils.h
index 886d28877..8f65d5c14 100644
--- a/src/app/utils.h
+++ b/src/app/utils.h
@@ -48,7 +48,6 @@
 #endif
 
 #include "api/account.h"
-#include "api/contact.h"
 #include "api/contactmodel.h"
 #include "api/conversationmodel.h"
 
diff --git a/src/libclient/CMakeLists.txt b/src/libclient/CMakeLists.txt
index 21ddca3ce..66aed1136 100644
--- a/src/libclient/CMakeLists.txt
+++ b/src/libclient/CMakeLists.txt
@@ -309,6 +309,7 @@ set(LIBCLIENT_HEADERS_API
   api/contactmodel.h
   api/conversationmodel.h
   api/datatransfermodel.h
+  api/messagelistmodel.h
   api/datatransfer.h
   api/interaction.h
   api/lrc.h
diff --git a/src/libclient/api/conversation.h b/src/libclient/api/conversation.h
index 580cf9d54..abb6cf207 100644
--- a/src/libclient/api/conversation.h
+++ b/src/libclient/api/conversation.h
@@ -20,12 +20,11 @@
 
 #include "interaction.h"
 #include "messagelistmodel.h"
+#include "account.h"
 #include "member.h"
 #include "typedefs.h"
 
-#include <map>
 #include <memory>
-#include <vector>
 
 namespace lrc {
 
diff --git a/src/libclient/api/interaction.h b/src/libclient/api/interaction.h
index 61dc57d08..f682f343e 100644
--- a/src/libclient/api/interaction.h
+++ b/src/libclient/api/interaction.h
@@ -47,12 +47,11 @@ enum class Type {
     COUNT__
 };
 Q_ENUM_NS(Type)
+
 static inline bool
-isDisplayedInChatview(const Type& type)
+isTypeDisplayable(const Type& type)
 {
-    return type != interaction::Type::MERGE && type != interaction::Type::EDITED
-           && type != interaction::Type::REACTION && type != interaction::Type::VOTE
-           && type != interaction::Type::UPDATE_PROFILE && type != interaction::Type::INVALID;
+    return type != interaction::Type::VOTE && type != interaction::Type::UPDATE_PROFILE;
 }
 
 static inline const QString
@@ -380,7 +379,7 @@ struct Info
     QString react_to;
     QVector<Body> previousBodies;
 
-    Info() {}
+    Info() = default;
 
     Info(QString authorUri,
          QString body,
@@ -399,6 +398,11 @@ struct Info
         this->isRead = isRead;
     }
 
+    Info(const Info& other) = default;
+    Info(Info&& other) = default;
+    Info& operator=(const Info& other) = delete;
+    Info& operator=(Info&& other) = default;
+
     void init(const MapStringString& message, const QString& accountURI)
     {
         type = to_type(message["type"]);
@@ -479,6 +483,13 @@ getCallInteractionString(bool isSelf, const Info& info)
     return getCallInteractionStringNonSwarm(isSelf, info.duration);
 }
 
+static inline QString
+getProfileUpdatedString()
+{
+    // Perhaps one day this will be more detailed.
+    return QObject::tr("(profile updated)");
+}
+
 } // namespace interaction
 } // namespace api
 } // namespace lrc
diff --git a/src/libclient/api/messagelistmodel.h b/src/libclient/api/messagelistmodel.h
new file mode 100644
index 000000000..18443e369
--- /dev/null
+++ b/src/libclient/api/messagelistmodel.h
@@ -0,0 +1,157 @@
+/*
+ *  Copyright (C) 2020-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, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ */
+
+#pragma once
+
+#include "api/interaction.h"
+
+#include <QAbstractListModel>
+
+#include <mutex>
+
+namespace lrc {
+namespace api {
+
+namespace account {
+struct Info;
+}
+
+#define MSG_ROLES \
+    X(Id) \
+    X(Author) \
+    X(Body) \
+    X(ParentId) \
+    X(Timestamp) \
+    X(Duration) \
+    X(Type) \
+    X(Status) \
+    X(IsRead) \
+    X(ContactAction) \
+    X(ActionUri) \
+    X(ConfId) \
+    X(DeviceId) \
+    X(LinkPreviewInfo) \
+    X(ParsedBody) \
+    X(PreviousBodies) \
+    X(Reactions) \
+    X(ReplyTo) \
+    X(ReplyToBody) \
+    X(ReplyToAuthor) \
+    X(TotalSize) \
+    X(TransferName) \
+    X(FileExtension) \
+    X(Readers) \
+    X(IsEmojiOnly) \
+    X(Index)
+
+namespace MessageList {
+Q_NAMESPACE
+enum Role {
+    DummyRole = Qt::UserRole + 1,
+#define X(role) role,
+    MSG_ROLES
+#undef X
+};
+Q_ENUM_NS(Role)
+} // namespace MessageList
+
+class MessageListModel : public QAbstractListModel
+{
+    Q_OBJECT
+
+public:
+    // A pair of message id and interaction info.
+    using item_t = QPair<QString, interaction::Info>;
+    using container_t = QList<item_t>;
+    using iterator = container_t::iterator;
+
+    explicit MessageListModel(const account::Info* account, QObject* parent = nullptr);
+    ~MessageListModel() = default;
+
+    // QAbstractListModel interface
+    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;
+
+    // QAbstractListModel helpers
+    Q_INVOKABLE QVariant data(const QString& id, int role = Qt::DisplayRole) const;
+
+    // Basic container + mutation/update methods.
+    bool empty() const;
+    int indexOfMessage(const QString& messageId) const;
+    void clear();
+    void reloadHistory();
+    bool insert(const QString& id, const interaction::Info& interaction, int index = -1);
+    bool append(const QString& id, const interaction::Info& interaction);
+    bool update(const QString& id, const interaction::Info& interaction);
+    bool updateStatus(const QString& id, interaction::Status newStatus, const QString& newBody = {});
+    QPair<bool, bool> addOrUpdate(const QString& id, const interaction::Info& interaction);
+
+    // Thread-safe access to interactions.
+    // Note: be careful when using these functions to modify interactions, as
+    //       the dataChanged() signal is not emitted. Use add/update/remove instead
+    //       if per-message UI updates are required. Also, DO NOT use these to
+    //       mutate the interactions_ container.
+    using InteractionCb = std::function<void(const QString&, interaction::Info&)>;
+    void forEach(const InteractionCb&);
+    // Operations on a single interaction. Returns true if the interaction is found.
+    // Note: if idHint is an empty string, the last interaction is used.
+    bool with(const QString& idHint, const InteractionCb&);
+    // A convenience function to access the last interaction.
+    bool withLast(const InteractionCb&);
+
+    // Used when sorting conversations by timestamp, where locking multiple
+    // interactions simultaneously is required.
+    std::recursive_mutex& getMutex();
+
+    // Methods to manage message metadata.
+    void addHyperlinkInfo(const QString& messageId, const QVariantMap& info);
+    void addReaction(const QString& messageId, const MapStringString& reaction);
+    void rmReaction(const QString& messageId, const QString& reactionId);
+    void setParsedMessage(const QString& messageId, const QString& parsed);
+    void setRead(const QString& peer, const QString& messageId);
+    QString getRead(const QString& peer);
+    QString lastSelfMessageId(const QString& id) const;
+    QPair<QString, time_t> getDisplayedInfoForPeer(const QString& peerId);
+
+private:
+    using Role = MessageList::Role;
+
+    container_t interactions_;
+    mutable std::recursive_mutex mutex_;
+    const account::Info* account_;
+
+    // Note: because read status are updated even if interaction is not loaded we need to
+    // keep track of these status outside the interaction::Info to allow quick access.
+    // lastDisplayedMessageUid_ is used to keep track of the last message displayed for each
+    // peer. This is used to update the far end read status of the interaction. messageToReaders_
+    // is used to keep track of the readers of each message. This is a different view of
+    // lastDisplayedMessageUid_, and is used to update the read status of the interaction.
+    QMap<QString, QString> lastDisplayedMessageUid_; // {"peerId": "messageId"}
+    QMap<QString, QStringList> messageToReaders_;    // {"messageId": ["peer1", "peer2"]}
+    QMap<QString, QSet<QString>> replyTo_;
+
+    iterator find(const QString& msgId);
+    int move(iterator it, const QString& newParentId);
+    QVariant data(int idx, int role = Qt::DisplayRole) const;
+    QVariant dataForItem(const item_t& item, int indexRow, int role = Qt::DisplayRole) const;
+    void updateReplies(const item_t& message);
+};
+} // namespace api
+} // namespace lrc
+Q_DECLARE_METATYPE(lrc::api::MessageListModel*)
diff --git a/src/libclient/authority/storagehelper.cpp b/src/libclient/authority/storagehelper.cpp
index 866ed07c1..d88cd2d4e 100644
--- a/src/libclient/authority/storagehelper.cpp
+++ b/src/libclient/authority/storagehelper.cpp
@@ -496,7 +496,7 @@ getHistory(Database& db, api::conversation::Info& conversation, const QString& l
                                                type,
                                                status,
                                                (payloads[i + 6] == "1" ? true : false)});
-            conversation.interactions->emplace(payloads[i], std::move(msg));
+            conversation.interactions->append(payloads[i], std::move(msg));
             if (status != api::interaction::Status::DISPLAYED || !payloads[i + 1].isEmpty()) {
                 continue;
             }
diff --git a/src/libclient/conversationmodel.cpp b/src/libclient/conversationmodel.cpp
index 87b4a602a..ccd06cd9a 100644
--- a/src/libclient/conversationmodel.cpp
+++ b/src/libclient/conversationmodel.cpp
@@ -51,7 +51,6 @@
 #include <algorithm>
 #include <mutex>
 #include <regex>
-#include <fstream>
 #include <sstream>
 
 namespace lrc {
@@ -172,11 +171,11 @@ public:
      * @param peerId, peer id
      * @param status, new status for this interaction
      */
-    void slotUpdateInteractionStatus(const QString& accountId,
-                                     const QString& conversationId,
-                                     const QString& peerId,
-                                     const QString& messageId,
-                                     int status);
+    void updateInteractionStatus(const QString& accountId,
+                                 const QString& conversationId,
+                                 const QString& peerId,
+                                 const QString& messageId,
+                                 int status);
 
     /**
      * place a call
@@ -236,7 +235,6 @@ public:
     FilterType typeFilter;
     FilterType customTypeFilter;
 
-    std::map<QString, std::mutex> interactionsLocks; ///< {convId, mutex}
     MapStringString transfIdToDbIntId;
     uint32_t mediaResearchRequestId;
     uint32_t msgResearchRequestId;
@@ -493,8 +491,8 @@ ConversationModel::getConferenceableConversations(const QString& convId, const Q
     // filter out calls from conference
     for (const auto& c : conferences) {
         for (const auto& subcall : owner.callModel->getConferenceSubcalls(c)) {
-            auto position = std::find(calls.begin(), calls.end(), subcall);
-            if (position != calls.end()) {
+            const auto position = std::find(calls.cbegin(), calls.cend(), subcall);
+            if (position != calls.cend()) {
                 calls.erase(position);
             }
         }
@@ -567,12 +565,12 @@ ConversationModel::getConferenceableConversations(const QString& convId, const Q
         } catch (...) {
         }
     }
-    for (auto it : tempConferences.toStdMap()) {
+    for (const auto& it : tempConferences.toStdMap()) {
         if (filter.isEmpty()) {
             callsVector.push_back(it.second);
             continue;
         }
-        for (AccountConversation accConv : it.second) {
+        for (const AccountConversation& accConv : it.second) {
             try {
                 auto& account = pimpl_->lrc.getAccountModel().getAccountInfo(accConv.accountId);
                 auto& conv = account.conversationModel->getConversationForUid(accConv.convId)->get();
@@ -889,8 +887,8 @@ ConversationModel::joinCall(const QString& uid,
                                                           isAudioOnly);
         // Update interaction status
         pimpl_->invalidateModel();
-        emit selectConversation(uid);
-        emit conversationUpdated(uid);
+        selectConversation(uid);
+        Q_EMIT conversationUpdated(uid);
     } catch (...) {
     }
 }
@@ -913,8 +911,8 @@ ConversationModelPimpl::placeCall(const QString& uid, bool isAudioOnly)
 
             // Update interaction status
             invalidateModel();
-            emit linked.selectConversation(conversation.uid);
-            emit linked.conversationUpdated(conversation.uid);
+            linked.selectConversation(conversation.uid);
+            Q_EMIT linked.conversationUpdated(conversation.uid);
             Q_EMIT linked.dataChanged(indexOf(conversation.uid));
             return;
         }
@@ -1158,11 +1156,13 @@ ConversationModel::notificationsCount() const
 void
 ConversationModel::reloadHistory() const
 {
-    std::for_each(pimpl_->conversations.begin(), pimpl_->conversations.end(), [&](const auto& c) {
-        c.interactions->reloadHistory();
-        Q_EMIT conversationUpdated(c.uid);
-        Q_EMIT dataChanged(pimpl_->indexOf(c.uid));
-    });
+    std::for_each(pimpl_->conversations.begin(),
+                  pimpl_->conversations.end(),
+                  [&](const conversation::Info& c) {
+                      c.interactions->reloadHistory();
+                      Q_EMIT conversationUpdated(c.uid);
+                      Q_EMIT dataChanged(pimpl_->indexOf(c.uid));
+                  });
 }
 
 QString
@@ -1345,19 +1345,8 @@ ConversationModel::sendMessage(const QString& uid, const QString& body, const QS
                 storage::addDaemonMsgId(pimpl_->db, msgId, toQString(daemonMsgId));
             }
 
-            bool ret = false;
-
-            {
-                std::lock_guard<std::mutex> lk(pimpl_->interactionsLocks[newConv.uid]);
-                ret = newConv.interactions->insert(std::pair<QString, interaction::Info>(msgId, msg))
-                          .second;
-            }
-
-            if (!ret) {
-                qDebug()
-                    << "ConversationModel::sendMessage failed to send message because an existing "
-                       "key was already present in the database key ="
-                    << msgId;
+            if (!newConv.interactions->append(msgId, msg)) {
+                qWarning() << Q_FUNC_INFO << "Append failed: duplicate ID";
                 return;
             }
 
@@ -1503,10 +1492,7 @@ ConversationModel::clearHistory(const QString& uid)
     // Remove all TEXT interactions from database
     storage::clearHistory(pimpl_->db, uid);
     // Update conversation
-    {
-        std::lock_guard<std::mutex> lk(pimpl_->interactionsLocks[conversation.uid]);
-        conversation.interactions->clear();
-    }
+    conversation.interactions->clear();
     storage::getHistory(pimpl_->db,
                         conversation,
                         pimpl_->linked.owner.profileInfo.uri); // will contain "Conversation started"
@@ -1541,7 +1527,6 @@ ConversationModel::clearAllHistory()
                 // WARNING: clear all history is not implemented for swarm
                 continue;
             }
-            std::lock_guard<std::mutex> lk(pimpl_->interactionsLocks[conversation.uid]);
             conversation.interactions->clear();
         }
         storage::getHistory(pimpl_->db, conversation, pimpl_->linked.owner.profileInfo.uri);
@@ -1558,37 +1543,30 @@ ConversationModel::clearUnreadInteractions(const QString& convId)
         return;
     }
     auto& conversation = conversationOpt->get();
-    bool emitUpdated = false;
-    QString lastDisplayed;
-    {
-        std::lock_guard<std::mutex> lk(pimpl_->interactionsLocks[convId]);
-        auto& interactions = conversation.interactions;
-        if (conversation.isSwarm()) {
-            emitUpdated = true;
-            if (!interactions->empty())
-                lastDisplayed = interactions->rbegin()->first;
-        } else {
-            std::for_each(interactions->begin(),
-                          interactions->end(),
-                          [&](decltype(*interactions->begin())& it) {
-                              if (!it.second.isRead) {
-                                  emitUpdated = true;
-                                  it.second.isRead = true;
-                                  if (owner.profileInfo.type != profile::Type::SIP)
-                                      lastDisplayed = storage::getDaemonIdByInteractionId(pimpl_->db,
-                                                                                          it.first);
-                                  storage::setInteractionRead(pimpl_->db, it.first);
-                              }
-                          });
-        }
+    bool updated = false;
+    QString lastDisplayedId;
+    if (conversation.isSwarm()) {
+        updated = true;
+        conversation.interactions->withLast(
+            [&](const QString& id, interaction::Info&) { lastDisplayedId = id; });
+    } else {
+        conversation.interactions->forEach([&](const QString& id, interaction::Info& interaction) {
+            if (interaction.isRead)
+                return;
+            updated = true;
+            interaction.isRead = true;
+            if (owner.profileInfo.type != profile::Type::SIP)
+                lastDisplayedId = storage::getDaemonIdByInteractionId(pimpl_->db, id);
+            storage::setInteractionRead(pimpl_->db, id);
+        });
     }
-    if (!lastDisplayed.isEmpty()) {
+    if (!lastDisplayedId.isEmpty()) {
         auto to = conversation.isSwarm()
                       ? "swarm:" + convId
                       : "jami:" + pimpl_->peersForConversation(conversation).front();
-        ConfigurationManager::instance().setMessageDisplayed(owner.id, to, lastDisplayed, 3);
+        ConfigurationManager::instance().setMessageDisplayed(owner.id, to, lastDisplayedId, 3);
     }
-    if (emitUpdated) {
+    if (updated) {
         conversation.unreadMessages = 0;
         pimpl_->invalidateModel();
         Q_EMIT conversationUpdated(convId);
@@ -1607,8 +1585,9 @@ ConversationModel::loadConversationMessages(const QString& conversationId, const
     if (conversation.allMessagesLoaded) {
         return -1;
     }
-    auto lastMsgId = conversation.interactions->empty() ? ""
-                                                        : conversation.interactions->front().first;
+    QString lastMsgId;
+    conversation.interactions->withLast(
+        [&lastMsgId](const QString& id, interaction::Info&) { lastMsgId = id; });
     return ConfigurationManager::instance().loadConversation(owner.id,
                                                              conversationId,
                                                              lastMsgId,
@@ -1719,7 +1698,7 @@ ConversationModelPimpl::ConversationModelPimpl(const ConversationModel& linked,
     connect(&callbacksHandler,
             &CallbacksHandler::accountMessageStatusChanged,
             this,
-            &ConversationModelPimpl::slotUpdateInteractionStatus);
+            &ConversationModelPimpl::updateInteractionStatus);
 
     // Call related
     connect(&*linked.owner.contactModel,
@@ -1886,7 +1865,7 @@ ConversationModelPimpl::~ConversationModelPimpl()
     disconnect(&callbacksHandler,
                &CallbacksHandler::accountMessageStatusChanged,
                this,
-               &ConversationModelPimpl::slotUpdateInteractionStatus);
+               &ConversationModelPimpl::updateInteractionStatus);
 
     // Call related
     disconnect(&*linked.owner.contactModel,
@@ -2061,23 +2040,22 @@ ConversationModelPimpl::initConversations()
 
         auto convIdx = indexOf(conv[0]);
 
-        // Check if file transfer interactions were left in an incorrect state
-        std::lock_guard<std::mutex> lk(interactionsLocks[conversations[convIdx].uid]);
-        for (auto& interaction : *(conversations[convIdx].interactions)) {
-            if (interaction.second.status == interaction::Status::TRANSFER_CREATED
-                || interaction.second.status == interaction::Status::TRANSFER_AWAITING_HOST
-                || interaction.second.status == interaction::Status::TRANSFER_AWAITING_PEER
-                || interaction.second.status == interaction::Status::TRANSFER_ONGOING
-                || interaction.second.status == interaction::Status::TRANSFER_ACCEPTED) {
+        // Resolve any file transfer interactions were left in an incorrect state
+        auto& interactions = conversations[convIdx].interactions;
+        interactions->forEach([&](const QString& id, interaction::Info& interaction) {
+            if (interaction.status == interaction::Status::TRANSFER_CREATED
+                || interaction.status == interaction::Status::TRANSFER_AWAITING_HOST
+                || interaction.status == interaction::Status::TRANSFER_AWAITING_PEER
+                || interaction.status == interaction::Status::TRANSFER_ONGOING
+                || interaction.status == interaction::Status::TRANSFER_ACCEPTED) {
                 // If a datatransfer was left in a non-terminal status in DB, we switch this status
                 // to ERROR
                 // TODO : Improve for DBus clients as daemon and transfer may still be ongoing
-                storage::updateInteractionStatus(db,
-                                                 interaction.first,
-                                                 interaction::Status::TRANSFER_ERROR);
-                interaction.second.status = interaction::Status::TRANSFER_ERROR;
+                storage::updateInteractionStatus(db, id, interaction::Status::TRANSFER_ERROR);
+
+                interaction.status = interaction::Status::TRANSFER_ERROR;
             }
-        }
+        });
     }
     invalidateModel();
 
@@ -2226,15 +2204,13 @@ ConversationModelPimpl::sort(const conversation::Info& convA, const conversation
     if (convA.uid == convB.uid)
         return false;
 
-    auto& mtxA = interactionsLocks[convA.uid];
-    auto& mtxB = interactionsLocks[convB.uid];
-    std::lock(mtxA, mtxB);
-    std::lock_guard<std::mutex> lockConvA(mtxA, std::adopt_lock);
-    std::lock_guard<std::mutex> lockConvB(mtxB, std::adopt_lock);
-
     auto& historyA = convA.interactions;
     auto& historyB = convB.interactions;
 
+    std::lock(historyA->getMutex(), historyB->getMutex());
+    std::lock_guard<std::recursive_mutex> lockConvA(historyA->getMutex(), std::adopt_lock);
+    std::lock_guard<std::recursive_mutex> lockConvB(historyB->getMutex(), std::adopt_lock);
+
     // A or B is a new conversation (without CONTACT interaction)
     if (convA.uid.isEmpty() || convB.uid.isEmpty())
         return convA.uid.isEmpty();
@@ -2256,14 +2232,14 @@ ConversationModelPimpl::sort(const conversation::Info& convA, const conversation
     if (historyB->empty())
         return true;
     // Sort by last Interaction
-    try {
-        auto lastMessageA = historyA->rbegin()->second;
-        auto lastMessageB = historyB->rbegin()->second;
-        return lastMessageA.timestamp > lastMessageB.timestamp;
-    } catch (const std::exception& e) {
-        qDebug() << "ConversationModel::sortConversations(), can't get lastMessage";
-        return false;
-    }
+    time_t timestampA, timestampB;
+    historyA->withLast([&](const QString&, const interaction::Info& interaction) {
+        timestampA = interaction.timestamp;
+    });
+    historyB->withLast([&](const QString&, const interaction::Info& interaction) {
+        timestampB = interaction.timestamp;
+    });
+    return timestampA > timestampB;
 }
 
 void
@@ -2319,18 +2295,10 @@ ConversationModelPimpl::slotSwarmLoaded(uint32_t requestId,
                 downloadFile = (bytesProgress == 0);
             }
 
-            {
-                // If message is loaded, insert message at beginning
-                std::lock_guard<std::mutex> lk(interactionsLocks[conversation.uid]);
-                auto itExists = conversation.interactions->find(msgId);
-                // If found, nothing to do.
-                if (itExists != conversation.interactions->end())
-                    continue;
-
-                auto result = conversation.interactions->insert(std::make_pair(msgId, msg), true);
-                if (!result.second) {
-                    continue;
-                }
+            // If message is loaded, insert message at beginning
+            if (!conversation.interactions->insert(msgId, msg, 0)) {
+                qDebug() << Q_FUNC_INFO << "Insert failed: duplicate ID";
+                continue;
             }
 
             if (downloadFile) {
@@ -2380,13 +2348,13 @@ ConversationModelPimpl::slotMessagesFound(uint32_t requestId,
                                                                  bytesProgress);
                 intInfo.body = path;
             }
-            messageDetailedInformation[msg["id"]] = intInfo;
+            messageDetailedInformation[msg["id"]] = std::move(intInfo);
         }
     } else if (requestId == msgResearchRequestId) {
         Q_FOREACH (const MapStringString& msg, messageIds) {
             auto intInfo = interaction::Info(msg, "");
             if (intInfo.type == interaction::Type::TEXT) {
-                messageDetailedInformation[msg["id"]] = intInfo;
+                messageDetailedInformation[msg["id"]] = std::move(intInfo);
             }
         }
     }
@@ -2437,23 +2405,11 @@ ConversationModelPimpl::slotMessageReceived(const QString& accountId,
             linked.owner.dataTransferModel->registerTransferId(fileId, msgId);
         }
 
-        {
-            // If message is received, insert message after its parent.
-            std::lock_guard<std::mutex> lk(interactionsLocks[conversation.uid]);
-            auto itExists = conversation.interactions->find(msgId);
-            // If found, nothing to do.
-            if (itExists != conversation.interactions->end())
-                return;
-            int index = conversation.interactions->indexOfMessage(msg.parentId);
-            if (index >= 0) {
-                auto result = conversation.interactions->insert(index + 1, qMakePair(msgId, msg));
-                if (!result.second) {
-                    return;
-                }
-            } else {
-                return;
-            }
+        if (!conversation.interactions->append(msgId, msg)) {
+            qDebug() << Q_FUNC_INFO << "Append failed: duplicate ID" << msgId;
+            return;
         }
+
         auto updateUnread = msg.authorUri != linked.owner.profileInfo.uri;
         if (updateUnread)
             conversation.unreadMessages++;
@@ -2491,26 +2447,11 @@ ConversationModelPimpl::slotMessageUpdated(const QString& accountId,
         QString msgId = message.id;
         auto msg = interaction::Info(message, linked.owner.profileInfo.uri);
 
-        {
-            std::lock_guard<std::mutex> lk(interactionsLocks[conversation.uid]);
-            auto itExists = conversation.interactions->find(msgId);
-            // If not found, nothing to do.
-            if (itExists == conversation.interactions->end())
-                return;
-            // Now there is two cases:
-            // ParentId changed, in this case, remove previous message and re-insert at new place
-            // Else, just update body
-            conversation.interactions->erase(itExists);
-            int index = conversation.interactions->indexOfMessage(msg.parentId);
-            if (index >= 0) {
-                auto result = conversation.interactions->insert(index + 1, qMakePair(msgId, msg));
-                if (!result.second) {
-                    return;
-                }
-            } else {
-                return;
-            }
+        if (!conversation.interactions->update(msgId, msg)) {
+            qDebug() << "message not found or could not be reparented";
+            return;
         }
+        // The conversation is updated, so we need to notify the view.
         invalidateModel();
         Q_EMIT linked.modelChanged();
         Q_EMIT linked.dataChanged(indexOf(conversationId));
@@ -2870,11 +2811,7 @@ ConversationModelPimpl::addConversationRequest(const MapStringString& convReques
         {"linearizedParent", ""},
     };
     auto msg = interaction::Info(messageMap, linked.owner.profileInfo.uri);
-
-    {
-        std::lock_guard<std::mutex> lk(interactionsLocks[convId]);
-        conversation.interactions->insert(std::make_pair(convId, msg), true);
-    }
+    conversation.interactions->insert(convId, msg);
 
     // add the author to the contact model's contact list as a PENDING
     // if they aren't already a contact
@@ -2935,8 +2872,7 @@ ConversationModelPimpl::slotPendingContactAccepted(const QString& uri)
                                                                     interaction::Status::SUCCESS);
             auto convIdx = indexOf(convs[0]);
             if (convIdx >= 0) {
-                std::lock_guard<std::mutex> lk(interactionsLocks[conversations[convIdx].uid]);
-                conversations[convIdx].interactions->emplace(msgId, interaction);
+                conversations[convIdx].interactions->append(msgId, interaction);
             }
             filteredConversations.invalidate();
             Q_EMIT linked.newInteraction(convs[0], msgId, interaction);
@@ -2958,7 +2894,7 @@ ConversationModelPimpl::slotContactRemoved(const QString& uri)
     }
 
     // actually remove them from the list
-    for (auto id : convIdsToRemove) {
+    for (const auto& id : convIdsToRemove) {
         eraseConversation(id);
         Q_EMIT linked.conversationRemoved(id);
     }
@@ -3092,11 +3028,7 @@ ConversationModelPimpl::addSwarmConversation(const QString& convId)
             {"linearizedParent", ""},
         };
         auto msg = interaction::Info(messageMap, linked.owner.profileInfo.uri);
-
-        {
-            std::lock_guard<std::mutex> lk(interactionsLocks[convId]);
-            conversation.interactions->insert(std::make_pair(convId, msg), true);
-        }
+        conversation.interactions->append(convId, msg);
         conversation.needsSyncing = true;
         Q_EMIT linked.conversationUpdated(conversation.uid);
         Q_EMIT linked.dataChanged(indexOf(conversation.uid));
@@ -3127,34 +3059,31 @@ ConversationModelPimpl::addConversationWith(const QString& convId,
         conversation.callId = "";
     }
     storage::getHistory(db, conversation, linked.owner.profileInfo.uri);
-    std::vector<std::function<void(void)>> updateSlots;
-    {
-        std::lock_guard<std::mutex> lk(interactionsLocks[conversation.uid]);
-        for (auto& interaction : (*(conversation.interactions))) {
-            if (interaction.second.status != interaction::Status::SENDING) {
-                continue;
-            }
-            // Get the message status from daemon, else unknown
-            auto id = storage::getDaemonIdByInteractionId(db, interaction.first);
-            int status = 0;
-            if (id.isEmpty()) {
-                continue;
-            }
-            try {
-                auto msgId = std::stoull(id.toStdString());
-                status = ConfigurationManager::instance().getMessageStatus(msgId);
-                updateSlots.emplace_back([this, convId, contactUri, id, status]() -> void {
-                    auto accId = linked.owner.id;
-                    slotUpdateInteractionStatus(accId, convId, contactUri, id, status);
-                });
-            } catch (const std::exception& e) {
-                qDebug() << "message id was invalid";
-            }
+
+    QList<std::function<void(void)>> toUpdate;
+    conversation.interactions->forEach([&](const QString& id, interaction::Info& interaction) {
+        if (interaction.status != interaction::Status::SENDING) {
+            return;
         }
-    }
-    for (const auto& s : updateSlots) {
-        s();
-    }
+        // Get the message status from daemon, else unknown
+        auto daemonId = storage::getDaemonIdByInteractionId(db, id);
+        int status = 0;
+        if (daemonId.isEmpty()) {
+            return;
+        }
+        try {
+            auto msgId = std::stoull(daemonId.toStdString());
+            status = ConfigurationManager::instance().getMessageStatus(msgId);
+            toUpdate.emplace_back([this, convId, contactUri, daemonId, status]() {
+                auto accId = linked.owner.id;
+                updateInteractionStatus(accId, convId, contactUri, daemonId, status);
+            });
+        } catch (const std::exception& e) {
+            qWarning() << Q_FUNC_INFO << "Failed: message id was invalid";
+        }
+    });
+    Q_FOREACH (const auto& func, toUpdate)
+        func();
 
     conversation.unreadMessages = getNumberOfUnreadMessagesFor(convId);
 
@@ -3296,7 +3225,7 @@ ConversationModelPimpl::slotCallStatusChanged(const QString& callId, int code)
         if (i != conversations.end()) {
             // Update interaction status
             invalidateModel();
-            Q_EMIT linked.selectConversation(i->uid);
+            linked.selectConversation(i->uid);
             Q_EMIT linked.conversationUpdated(i->uid);
             Q_EMIT linked.dataChanged(indexOf(i->uid));
         }
@@ -3378,7 +3307,6 @@ ConversationModelPimpl::addOrUpdateCallMessage(const QString& callId,
     // do not save call interaction for swarm conversation
     if (conv_it->isSwarm())
         return;
-    auto uid = conv_it->uid;
     auto uriString = incoming ? storage::prepareUri(from, linked.owner.profileInfo.type)
                               : linked.owner.profileInfo.uri;
     auto msg = interaction::Info {uriString,
@@ -3393,20 +3321,12 @@ ConversationModelPimpl::addOrUpdateCallMessage(const QString& callId,
     // now set the formatted call message string in memory only
     msg.body = interaction::getCallInteractionString(msg.authorUri == linked.owner.profileInfo.uri,
                                                      msg);
-    bool newInteraction = false;
-    {
-        std::lock_guard<std::mutex> lk(interactionsLocks[conv_it->uid]);
-        auto interactionIt = conv_it->interactions->find(msgId);
-        newInteraction = interactionIt == conv_it->interactions->end();
-        if (newInteraction) {
-            conv_it->interactions->emplace(msgId, msg);
-        } else {
-            interactionIt->second = msg;
-            conv_it->interactions->emitDataChanged(interactionIt);
-        }
+    auto [added, success] = conv_it->interactions->addOrUpdate(msgId, msg);
+    if (!success) {
+        qWarning() << Q_FUNC_INFO << QString("Failed: to %1 msg").arg(added ? "add" : "update");
+        return;
     }
-
-    if (newInteraction)
+    if (added)
         Q_EMIT linked.newInteraction(conv_it->uid, msgId, msg);
 
     invalidateModel();
@@ -3423,11 +3343,12 @@ ConversationModelPimpl::slotNewAccountMessage(const QString& accountId,
     if (accountId != linked.owner.id)
         return;
 
-    for (const auto& payload : payloads.keys()) {
+    for (auto it = payloads.constBegin(); it != payloads.constEnd(); ++it) {
+        const auto& payload = it.key();
         if (payload.contains(TEXT_PLAIN)) {
-            addIncomingMessage(peerId, payloads.value(payload), 0, msgId);
+            addIncomingMessage(peerId, it.value(), 0, msgId);
         } else {
-            qWarning() << payload;
+            qDebug() << payload;
         }
     }
 }
@@ -3504,10 +3425,8 @@ ConversationModelPimpl::addIncomingMessage(const QString& peerId,
         addConversationWith(convIds[0], peerId, isRequest);
         Q_EMIT linked.newConversation(convIds[0]);
     } else {
-        {
-            std::lock_guard<std::mutex> lk(interactionsLocks[conversations[conversationIdx].uid]);
-            conversations[conversationIdx].interactions->emplace(msgId, msg);
-        }
+        // Maybe check if this is failing?
+        conversations[conversationIdx].interactions->append(msgId, msg);
         conversations[conversationIdx].unreadMessages = getNumberOfUnreadMessagesFor(convIds[0]);
     }
 
@@ -3532,25 +3451,25 @@ ConversationModelPimpl::slotCallAddedToConference(const QString& callId, const Q
             MapStringString confDetails = CallManager::instance()
                                               .getConferenceDetails(linked.owner.id, confId);
             if (confDetails["STATE"] == "ACTIVE_ATTACHED")
-                Q_EMIT linked.selectConversation(conversation.uid);
+                linked.selectConversation(conversation.uid);
             return;
         }
     }
 }
 
 void
-ConversationModelPimpl::slotUpdateInteractionStatus(const QString& accountId,
-                                                    const QString& conversationId,
-                                                    const QString& peerId,
-                                                    const QString& messageId,
-                                                    int status)
+ConversationModelPimpl::updateInteractionStatus(const QString& accountId,
+                                                const QString& conversationId,
+                                                const QString& peerUri,
+                                                const QString& messageId,
+                                                int status)
 {
     if (accountId != linked.owner.id) {
         return;
     }
-    // it may be not swarm conversation check in db
+    // non-swarm conversation
     if (conversationId.isEmpty() || conversationId == linked.owner.profileInfo.uri) {
-        auto convIds = storage::getConversationsWithPeer(db, peerId);
+        auto convIds = storage::getConversationsWithPeer(db, peerUri);
         if (convIds.empty()) {
             return;
         }
@@ -3591,79 +3510,83 @@ ConversationModelPimpl::slotUpdateInteractionStatus(const QString& accountId,
             idString = QString::number(id);
         }
         // Update database
-        auto interactionId = storage::getInteractionIdByDaemonId(db, idString);
-        if (interactionId.isEmpty()) {
+        auto msgId = storage::getInteractionIdByDaemonId(db, idString);
+        if (msgId.isEmpty()) {
             return;
         }
-        auto msgId = interactionId;
         storage::updateInteractionStatus(db, msgId, newStatus);
         // Update conversations
-        bool emitUpdated = false;
+        bool updated = false;
         bool updateDisplayedUid = false;
         QString oldDisplayedUid = 0;
         {
-            std::lock_guard<std::mutex> lk(interactionsLocks[conversation.uid]);
             auto& interactions = conversation.interactions;
-            auto it = interactions->find(msgId);
-            auto messageId = conversation.interactions->getRead(peerId);
-            if (it != interactions->end()) {
-                it->second.status = newStatus;
-                interactions->emitDataChanged(it, {MessageList::Role::Status});
-                bool interactionDisplayed = newStatus == interaction::Status::DISPLAYED
-                                            && isOutgoing(it->second);
-                if (messageId != "") {
-                    auto lastDisplayedIt = interactions->find(messageId);
-                    bool interactionIsLast = lastDisplayedIt == interactions->end()
-                                             || lastDisplayedIt->second.timestamp
-                                                    < it->second.timestamp;
-                    updateDisplayedUid = interactionDisplayed && interactionIsLast;
-                    if (updateDisplayedUid) {
-                        oldDisplayedUid = messageId;
-                        if (peerId != linked.owner.profileInfo.uri)
-                            conversation.interactions->setRead(peerId, it->first);
+            // Try to update the status.
+            if (interactions->updateStatus(msgId, newStatus)) {
+                updated = true;
+                interactions->with(msgId, [&](const QString& id, interaction::Info& interaction) {
+                    // Determine if the interaction is outgoing and has been displayed.
+                    bool interactionIsDisplayed = newStatus == interaction::Status::DISPLAYED
+                                                  && interaction::isOutgoing(interaction);
+
+                    // Get the last displayed interaction ID and timestamp for this peer.
+                    auto [lastIdForPeer, lastTimestampForPeer]
+                        = interactions->getDisplayedInfoForPeer(peerUri);
+
+                    if (lastIdForPeer.isEmpty()) {
+                        oldDisplayedUid = "";
+                        if (peerUri != linked.owner.profileInfo.uri)
+                            conversation.interactions->setRead(peerUri, msgId);
+                        updateDisplayedUid = true;
+                    } else {
+                        bool interactionIsLast = lastTimestampForPeer < interaction.timestamp;
+                        updateDisplayedUid = interactionIsDisplayed && interactionIsLast;
+                        if (updateDisplayedUid) {
+                            oldDisplayedUid = messageId;
+                            if (peerUri != linked.owner.profileInfo.uri)
+                                conversation.interactions->setRead(peerUri, msgId);
+                        }
                     }
-                } else {
-                    oldDisplayedUid = "";
-                    if (peerId != linked.owner.profileInfo.uri)
-                        conversation.interactions->setRead(peerId, it->first);
-                    updateDisplayedUid = true;
-                }
-                emitUpdated = true;
+                });
             }
         }
         if (updateDisplayedUid) {
             Q_EMIT linked.displayedInteractionChanged(conversation.uid,
-                                                      peerId,
+                                                      peerUri,
                                                       oldDisplayedUid,
                                                       msgId);
         }
-        if (emitUpdated) {
+        if (updated) {
             invalidateModel();
         }
         return;
     }
+    // swarm conversation
     try {
         auto& conversation = getConversationForUid(conversationId).get();
-        if (conversation.mode != conversation::Mode::NON_SWARM) {
-            std::lock_guard<std::mutex> lk(interactionsLocks[conversation.uid]);
+        if (conversation.isSwarm()) {
+            using namespace libjami::Account;
+            auto msgState = static_cast<MessageStates>(status);
             auto& interactions = conversation.interactions;
-            auto it = interactions->find(messageId);
-            if (it != interactions->end() && it->second.type == interaction::Type::TEXT) {
-                if (static_cast<libjami::Account::MessageStates>(status)
-                    == libjami::Account::MessageStates::SENDING) {
-                    it->second.status = interaction::Status::SENDING;
-                } else if (static_cast<libjami::Account::MessageStates>(status)
-                           == libjami::Account::MessageStates::SENT) {
-                    it->second.status = interaction::Status::SUCCESS;
-                }
-                interactions->emitDataChanged(it, {MessageList::Role::Status});
-            }
-
-            if (static_cast<libjami::Account::MessageStates>(status)
-                == libjami::Account::MessageStates::DISPLAYED) {
-                auto previous = conversation.interactions->getRead(peerId);
-                if (peerId != linked.owner.profileInfo.uri)
-                    conversation.interactions->setRead(peerId, messageId);
+            interactions->with(messageId,
+                               [&](const QString& id, const interaction::Info& interaction) {
+                                   if (interaction.type == interaction::Type::TEXT) {
+                                       interaction::Status newState;
+                                       if (msgState == MessageStates::SENDING) {
+                                           newState = interaction::Status::SENDING;
+                                       } else if (msgState == MessageStates::SENT) {
+                                           newState = interaction::Status::SUCCESS;
+                                       } else {
+                                           return;
+                                       }
+                                       interactions->updateStatus(id, newState);
+                                   }
+                               });
+
+            if (msgState == MessageStates::DISPLAYED) {
+                auto previous = conversation.interactions->getRead(peerUri);
+                if (peerUri != linked.owner.profileInfo.uri)
+                    conversation.interactions->setRead(peerUri, messageId);
                 else {
                     // Here, this means that the daemon synced the displayed message
                     // so, compute the number of unread messages.
@@ -3672,11 +3595,11 @@ ConversationModelPimpl::slotUpdateInteractionStatus(const QString& accountId,
                                                                          conversationId,
                                                                          messageId,
                                                                          "",
-                                                                         peerId);
+                                                                         peerUri);
                     Q_EMIT linked.dataChanged(indexOf(conversationId));
                 }
                 Q_EMIT linked.displayedInteractionChanged(conversationId,
-                                                          peerId,
+                                                          peerUri,
                                                           previous,
                                                           messageId);
             }
@@ -3843,13 +3766,8 @@ ConversationModel::cancelTransfer(const QString& convUid, const QString& fileId)
     auto conversationIdx = pimpl_->indexOf(convUid);
     bool emitUpdated = false;
     if (conversationIdx != -1) {
-        std::lock_guard<std::mutex> lk(pimpl_->interactionsLocks[convUid]);
         auto& interactions = pimpl_->conversations[conversationIdx].interactions;
-        auto it = interactions->find(fileId);
-        if (it != interactions->end()) {
-            it->second.status = interaction::Status::TRANSFER_CANCELED;
-            interactions->emitDataChanged(it, {MessageList::Role::Status});
-
+        if (interactions->updateStatus(fileId, interaction::Status::TRANSFER_CANCELED)) {
             // update information in the db
             storage::updateInteractionStatus(pimpl_->db,
                                              fileId,
@@ -3901,14 +3819,7 @@ ConversationModel::removeFile(const QString& conversationId,
         return;
 
     QFile::remove(path);
-
-    std::lock_guard<std::mutex> lk(pimpl_->interactionsLocks[convOpt->get().uid]);
-    auto& interactions = convOpt->get().interactions;
-    auto it = interactions->find(interactionId);
-    if (it != interactions->end()) {
-        it->second.status = interaction::Status::TRANSFER_AWAITING_HOST;
-        interactions->emitDataChanged(it, {MessageList::Role::Status});
-    }
+    convOpt->get().interactions->updateStatus(interactionId, interaction::Status::TRANSFER_CANCELED);
 }
 
 int
@@ -3991,10 +3902,7 @@ ConversationModelPimpl::slotTransferStatusCreated(const QString& fileId, datatra
         addConversationWith(convId, info.peerUri, isRequest);
         Q_EMIT linked.newConversation(convId);
     } else {
-        {
-            std::lock_guard<std::mutex> lk(interactionsLocks[conversations[conversationIdx].uid]);
-            conversations[conversationIdx].interactions->emplace(interactionId, interaction);
-        }
+        conversations[conversationIdx].interactions->append(interactionId, interaction);
         conversations[conversationIdx].unreadMessages = getNumberOfUnreadMessagesFor(convId);
     }
     Q_EMIT behaviorController.newUnreadInteraction(linked.owner.id,
@@ -4087,15 +3995,18 @@ ConversationModelPimpl::acceptTransfer(const QString& convUid, const QString& in
     if (conversation.isLegacy()) // Ignore legacy
         return;
 
-    auto interaction = conversation.interactions->find(interactionId);
-    if (interaction != conversation.interactions->end()) {
-        auto fileId = interaction->second.commit["fileId"];
-        if (fileId.isEmpty()) {
-            qWarning() << "Cannot download file without fileId";
-            return;
-        }
-        linked.owner.dataTransferModel->download(linked.owner.id, convUid, interactionId, fileId);
-    } else {
+    auto& interactions = conversation.interactions;
+    if (!interactions->with(interactionId, [&](const QString& id, interaction::Info& interaction) {
+            auto fileId = interaction.commit["fileId"];
+            if (fileId.isEmpty()) {
+                qWarning() << "Cannot download file without fileId";
+                return;
+            }
+            linked.owner.dataTransferModel->download(linked.owner.id,
+                                                     convUid,
+                                                     interactionId,
+                                                     fileId);
+        })) {
         qWarning() << "Cannot download file without valid interaction";
     }
 }
@@ -4150,7 +4061,7 @@ ConversationModelPimpl::slotTransferStatusOngoing(const QString& fileId, datatra
     }
     auto conversationIdx = indexOf(conversationId);
     auto* timer = new QTimer();
-    connect(timer, &QTimer::timeout, [=] {
+    connect(timer, &QTimer::timeout, this, [=] {
         updateTransferProgress(timer, conversationIdx, interactionId);
     });
     timer->start(1000);
@@ -4170,20 +4081,15 @@ ConversationModelPimpl::slotTransferStatusFinished(const QString& fileId, datatr
     if (conversationIdx != -1) {
         bool emitUpdated = false;
         auto newStatus = interaction::Status::TRANSFER_FINISHED;
-        {
-            std::lock_guard<std::mutex> lk(interactionsLocks[conversations[conversationIdx].uid]);
-            auto& interactions = conversations[conversationIdx].interactions;
-            auto it = interactions->find(interactionId);
-            if (it != interactions->end()) {
-                // We need to check if current status is ONGOING as CANCELED must not be
-                // transformed into FINISHED
-                if (it->second.status == interaction::Status::TRANSFER_ONGOING) {
-                    emitUpdated = true;
-                    it->second.status = newStatus;
-                    interactions->emitDataChanged(it, {MessageList::Role::Status});
-                }
+        auto& interactions = conversations[conversationIdx].interactions;
+        interactions->with(interactionId, [&](const QString& id, interaction::Info& interaction) {
+            // We need to check if current status is ONGOING as CANCELED must not be
+            // transformed into FINISHED
+            if (interaction.status == interaction::Status::TRANSFER_ONGOING) {
+                emitUpdated = true;
+                interactions->updateStatus(id, newStatus);
             }
-        }
+        });
         if (emitUpdated) {
             invalidateModel();
             if (conversations[conversationIdx].mode != conversation::Mode::NON_SWARM) {
@@ -4256,23 +4162,10 @@ ConversationModelPimpl::updateTransferStatus(const QString& fileId,
     if (conversation.isLegacy()) {
         storage::updateInteractionStatus(db, interactionId, newStatus);
     }
-    bool emitUpdated = false;
-    {
-        std::lock_guard<std::mutex> lk(interactionsLocks[conversations[conversationIdx].uid]);
-        auto& interactions = conversations[conversationIdx].interactions;
-        auto it = interactions->find(interactionId);
-        if (it != interactions->end()) {
-            emitUpdated = true;
-            VectorInt roles;
-            it->second.status = newStatus;
-            roles += MessageList::Role::Status;
-            if (conversation.isSwarm()) {
-                it->second.body = info.path;
-                roles += MessageList::Role::Body;
-            }
-            interactions->emitDataChanged(it, roles);
-        }
-    }
+    auto& interactions = conversations[conversationIdx].interactions;
+    bool emitUpdated = interactions->updateStatus(interactionId,
+                                                  newStatus,
+                                                  conversation.isSwarm() ? info.path : QString());
     if (emitUpdated) {
         invalidateModel();
     }
@@ -4288,15 +4181,13 @@ ConversationModelPimpl::updateTransferProgress(QTimer* timer,
     try {
         bool emitUpdated = false;
         {
-            auto convId = conversations[conversationIdx].uid;
-            std::lock_guard<std::mutex> lk(interactionsLocks[convId]);
             const auto& interactions = conversations[conversationIdx].interactions;
-            const auto& it = interactions->find(interactionId);
-            if (it != interactions->cend()
-                and it->second.status == interaction::Status::TRANSFER_ONGOING) {
-                interactions->emitDataChanged(it, {MessageList::Role::Status});
-                emitUpdated = true;
-            }
+            interactions->with(interactionId, [&](const QString& id, interaction::Info& interaction) {
+                if (interaction.status == interaction::Status::TRANSFER_ONGOING) {
+                    emitUpdated = true;
+                    interactions->updateStatus(id, interaction::Status::TRANSFER_ONGOING);
+                }
+            });
         }
         if (emitUpdated)
             return;
diff --git a/src/libclient/messagelistmodel.cpp b/src/libclient/messagelistmodel.cpp
index 6e3022ca3..c6ee30509 100644
--- a/src/libclient/messagelistmodel.cpp
+++ b/src/libclient/messagelistmodel.cpp
@@ -1,9 +1,6 @@
 /*
  *  Copyright (C) 2020-2023 Savoir-faire Linux Inc.
  *
- *  Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
- *  Author: Trevor Tabah <trevor.tabah@savoirfairelinux.com>
- *
  *  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
@@ -19,365 +16,441 @@
  *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
  */
 
-#include "messagelistmodel.h"
+#include "api/messagelistmodel.h"
 
-#include "authority/storagehelper.h"
 #include "api/accountmodel.h"
 #include "api/contactmodel.h"
 #include "api/conversationmodel.h"
-#include "api/interaction.h"
-#include "qtwrapper/conversions_wrap.hpp"
 
-#include <QAbstractListModel>
 #include <QFileInfo>
 
+static bool
+isOnlyEmoji(const QString& text)
+{
+    if (text.isEmpty())
+        return false;
+    auto codepointList = text.toUcs4();
+    for (QList<uint>::iterator it = codepointList.begin(); it != codepointList.end(); it++) {
+        auto cur = false;
+        if (*it == 20 or *it == 0x200D) {
+            cur = true;
+        } else if (0x1f000 <= *it && 0x1ffff >= *it) {
+            cur = true;
+        } else if (0x2600 <= *it && 0x27BF >= *it) {
+            cur = true;
+        } else if (0xFE00 <= *it && 0xFE0f >= *it) {
+            cur = true;
+        } else if (0xE0000 <= *it && 0xE007F >= *it) {
+            cur = true;
+        }
+        if (!cur)
+            return false;
+    }
+    return true;
+}
+
 namespace lrc {
 
 using namespace api;
 
-using constIterator = MessageListModel::constIterator;
-using iterator = MessageListModel::iterator;
-using reverseIterator = MessageListModel::reverseIterator;
-
 MessageListModel::MessageListModel(const account::Info* account, QObject* parent)
     : QAbstractListModel(parent)
     , account_(account)
 {}
 
-QPair<iterator, bool>
-MessageListModel::emplace(const QString& msgId, interaction::Info message, bool beginning)
+int
+MessageListModel::rowCount(const QModelIndex&) const
 {
-    iterator it;
-    for (it = interactions_.begin(); it != interactions_.end(); ++it) {
-        if (it->first == msgId) {
-            return qMakePair(it, false);
-        }
-    }
-    auto iter = beginning ? interactions_.begin() : interactions_.end();
-    auto iterator = insertMessage(iter, qMakePair(msgId, message));
-    return qMakePair(iterator, true);
+    std::lock_guard<std::recursive_mutex> lk(mutex_);
+    return interactions_.size();
 }
 
-iterator
-MessageListModel::find(const QString& msgId)
+QVariant
+MessageListModel::data(const QModelIndex& index, int role) const
 {
-    iterator it;
-    for (it = interactions_.begin(); it != interactions_.end(); ++it) {
-        if (it->first == msgId) {
-            return it;
-        }
+    std::lock_guard<std::recursive_mutex> lk(mutex_);
+    if (!index.isValid() || index.row() < 0 || index.row() >= rowCount()) {
+        return {};
     }
-    return interactions_.end();
+    return dataForItem(interactions_.at(index.row()), index.row(), role);
 }
 
-iterator
-MessageListModel::findActiveCall(const MapStringString& commit)
+QHash<int, QByteArray>
+MessageListModel::roleNames() const
 {
-    iterator it;
-    for (it = interactions_.begin(); it != interactions_.end(); ++it) {
-        const auto& itCommit = it->second.commit;
-        if (itCommit["confId"] == commit["confId"] && itCommit["uri"] == commit["uri"]
-            && itCommit["device"] == commit["device"]) {
-            return it;
-        }
-    }
-    return interactions_.end();
+    using namespace MessageList;
+    QHash<int, QByteArray> roles;
+#define X(role) roles[role] = #role;
+    MSG_ROLES
+#undef X
+    return roles;
 }
 
-iterator
-MessageListModel::erase(const iterator& it)
+QVariant
+MessageListModel::data(const QString& id, int role) const
 {
-    auto index = std::distance(begin(), it);
-    Q_EMIT beginRemoveRows(QModelIndex(), index, index);
-    auto erased = interactions_.erase(it);
-    Q_EMIT endRemoveRows();
-    return erased;
+    return data(indexOfMessage(id), role);
 }
 
-constIterator
-MessageListModel::find(const QString& msgId) const
+bool
+MessageListModel::empty() const
 {
-    constIterator it;
-    for (it = interactions_.cbegin(); it != interactions_.cend(); ++it) {
-        if (it->first == msgId) {
-            return it;
-        }
+    std::lock_guard<std::recursive_mutex> lk(mutex_);
+    return interactions_.empty();
+}
+
+int
+MessageListModel::indexOfMessage(const QString& messageId) const
+{
+    std::lock_guard<std::recursive_mutex> lk(mutex_);
+    auto it = std::find_if(interactions_.rbegin(),
+                           interactions_.rend(),
+                           [&messageId](const auto& it) { return it.first == messageId; });
+    if (it == interactions_.rend()) {
+        return -1;
     }
-    return interactions_.cend();
+    return std::distance(it, interactions_.rend()) - 1;
 }
 
-QPair<iterator, bool>
-MessageListModel::insert(std::pair<QString, interaction::Info> message, bool beginning)
+void
+MessageListModel::clear()
 {
-    return emplace(message.first, message.second, beginning);
+    std::lock_guard<std::recursive_mutex> lk(mutex_);
+    beginResetModel();
+    interactions_.clear();
+    replyTo_.clear();
+    endResetModel();
 }
 
-int
-MessageListModel::erase(const QString& msgId)
+void
+MessageListModel::reloadHistory()
 {
-    iterator it;
-    int index = 0;
-    for (it = interactions_.begin(); it != interactions_.end(); ++it) {
-        if (it->first == msgId) {
-            removeMessage(index, it);
-            return 1;
-        }
-        index++;
+    std::lock_guard<std::recursive_mutex> lk(mutex_);
+    beginResetModel();
+    for (auto& interaction : interactions_) {
+        interaction.second.linkPreviewInfo.clear();
     }
-    return 0;
+    endResetModel();
 }
 
-interaction::Info&
-MessageListModel::operator[](const QString& messageId)
+bool
+MessageListModel::insert(const QString& id, const interaction::Info& interaction, int index)
 {
-    for (auto it = interactions_.cbegin(); it != interactions_.cend(); ++it) {
-        if (it->first == messageId) {
-            return const_cast<interaction::Info&>(it->second);
-        }
+    const std::lock_guard<std::recursive_mutex> lk(mutex_);
+    // If the index parameter is -1, then insert at the parent of the message.
+    if (index == -1) {
+        index = indexOfMessage(interaction.parentId);
     }
-    // element not find, add it to the end
-    interaction::Info newMessage = {};
-    insertMessage(interactions_.end(), qMakePair(messageId, newMessage));
-    if (interactions_.last().first == messageId) {
-        return const_cast<interaction::Info&>(interactions_.last().second);
+    // The index should be valid and don't add duplicate messages.
+    if (index < 0 || index > interactions_.size() || find(id) != interactions_.end()) {
+        return false;
     }
-    throw std::out_of_range("Cannot find message");
-}
-
-iterator
-MessageListModel::end()
-{
-    return interactions_.end();
+    beginInsertRows(QModelIndex(), index, index);
+    interactions_.emplace(interactions_.cbegin() + index, id, interaction);
+    endInsertRows();
+    return true;
 }
 
-constIterator
-MessageListModel::end() const
+bool
+MessageListModel::append(const QString& id, const interaction::Info& interaction)
 {
-    return interactions_.end();
+    const std::lock_guard<std::recursive_mutex> lk(mutex_);
+    // Don't add duplicate messages.
+    if (find(id) != interactions_.end()) {
+        return false;
+    }
+    beginInsertRows(QModelIndex(), interactions_.size(), interactions_.size());
+    interactions_.emplace_back(id, interaction);
+    endInsertRows();
+    return true;
 }
 
-reverseIterator
-MessageListModel::rend()
+bool
+MessageListModel::update(const QString& id, const interaction::Info& interaction)
 {
-    return interactions_.rend();
+    // There are two cases: a) Parent ID changed, b) body changed (edit/delete).
+    const std::lock_guard<std::recursive_mutex> lk(mutex_);
+    auto it = find(id);
+    if (find(id) == interactions_.end()) {
+        return false;
+    }
+    interaction::Info& current = it->second;
+    if (current.parentId != interaction.parentId) {
+        // Parent ID changed, in this case, move the interaction to the new parent.
+        it->second.parentId = interaction.parentId;
+        auto newIndex = move(it, interaction.parentId);
+        if (newIndex >= 0) {
+            // The iterator is invalid now. But we can update all the roles.
+            auto modelIndex = QAbstractListModel::index(newIndex);
+            Q_EMIT dataChanged(modelIndex, modelIndex, roleNames().keys());
+            return true;
+        }
+    }
+    // Just update bodies notify the view.
+    current.body = interaction.body;
+    current.previousBodies = interaction.previousBodies;
+    current.parsedBody = interaction.parsedBody;
+    auto modelIndex = QAbstractListModel::index(indexOfMessage(id), 0);
+    Q_EMIT dataChanged(modelIndex, modelIndex, {Role::Body, Role::PreviousBodies, Role::ParsedBody});
+    return true;
 }
 
-constIterator
-MessageListModel::cend() const
+bool
+MessageListModel::updateStatus(const QString& id,
+                               interaction::Status newStatus,
+                               const QString& newBody)
 {
-    return interactions_.cend();
+    const std::lock_guard<std::recursive_mutex> lk(mutex_);
+    auto it = find(id);
+    if (it == interactions_.end()) {
+        return false;
+    }
+    VectorInt roles;
+    it->second.status = newStatus;
+    roles.push_back(Role::Status);
+    if (!newBody.isEmpty()) {
+        it->second.body = newBody;
+        roles.push_back(Role::Body);
+    }
+    auto modelIndex = QAbstractListModel::index(indexOfMessage(id), 0);
+    Q_EMIT dataChanged(modelIndex, modelIndex, roles);
+    return true;
 }
 
-iterator
-MessageListModel::begin()
+QPair<bool, bool>
+MessageListModel::addOrUpdate(const QString& id, const interaction::Info& interaction)
 {
-    return interactions_.begin();
+    if (find(id) == interactions_.end()) {
+        // The ID doesn't exist, appending cannot fail here.
+        return {true, append(id, interaction)};
+    } else {
+        // Update can only fail if the new parent ID is invalid.
+        return {false, update(id, interaction)};
+    }
 }
 
-constIterator
-MessageListModel::begin() const
+void
+MessageListModel::forEach(const InteractionCb& callback)
 {
-    return interactions_.begin();
+    const std::lock_guard<std::recursive_mutex> lk(mutex_);
+    for (auto& interaction : interactions_) {
+        callback(interaction.first, interaction.second);
+    }
 }
 
-reverseIterator
-MessageListModel::rbegin()
+bool
+MessageListModel::with(const QString& idHint, const InteractionCb& callback)
 {
-    return interactions_.rbegin();
+    const std::lock_guard<std::recursive_mutex> lk(mutex_);
+    if (interactions_.empty()) {
+        return false;
+    }
+    // If the ID is empty, then return the last interaction.
+    auto it = idHint.isEmpty() ? std::prev(interactions_.end()) : find(idHint);
+    if (it == interactions_.end()) {
+        return false;
+    }
+    callback(it->first, it->second);
+    return true;
 }
 
-int
-MessageListModel::size() const
+bool
+MessageListModel::withLast(const InteractionCb& callback)
 {
-    return interactions_.size();
+    return with(QString(), callback);
 }
 
-void
-MessageListModel::clear()
+std::recursive_mutex&
+MessageListModel::getMutex()
 {
-    Q_EMIT beginResetModel();
-    interactions_.clear();
-    replyTo_.clear();
-    Q_EMIT endResetModel();
+    return mutex_;
 }
 
 void
-MessageListModel::reloadHistory()
+MessageListModel::addHyperlinkInfo(const QString& messageId, const QVariantMap& info)
 {
-    Q_EMIT beginResetModel();
-    for (auto& interaction : interactions_) {
-        interaction.second.linkPreviewInfo.clear();
+    std::lock_guard<std::recursive_mutex> lk(mutex_);
+    int index = indexOfMessage(messageId);
+    if (index == -1) {
+        return;
     }
-    Q_EMIT endResetModel();
-}
-
-bool
-MessageListModel::empty() const
-{
-    return interactions_.empty();
-}
+    QModelIndex modelIndex = QAbstractListModel::index(index, 0);
 
-interaction::Info
-MessageListModel::at(const QString& msgId) const
-{
-    for (auto it = interactions_.cbegin(); it != interactions_.cend(); ++it) {
-        if (it->first == msgId) {
-            return it->second;
-        }
-    }
-    return {};
+    interactions_[index].second.linkPreviewInfo = info;
+    Q_EMIT dataChanged(modelIndex, modelIndex, {Role::LinkPreviewInfo});
 }
 
-QPair<QString, interaction::Info>
-MessageListModel::front() const
+void
+MessageListModel::addReaction(const QString& messageId, const MapStringString& reaction)
 {
-    return interactions_.front();
-}
+    std::lock_guard<std::recursive_mutex> lk(mutex_);
+    int index = indexOfMessage(messageId);
+    if (index == -1)
+        return;
+    QModelIndex modelIndex = QAbstractListModel::index(index, 0);
 
-QPair<QString, interaction::Info>
-MessageListModel::last() const
-{
-    return interactions_.last();
+    auto emoji = api::interaction::Emoji {reaction["id"], reaction["body"]};
+    auto& pList = interactions_[index].second.reactions[reaction["author"]];
+    QList<QVariant> newList = pList.toList();
+    newList.emplace_back(QVariant::fromValue(emoji));
+    pList = QVariantList::fromVector(newList);
+    Q_EMIT dataChanged(modelIndex, modelIndex, {Role::Reactions});
 }
 
-QPair<QString, interaction::Info>
-MessageListModel::atIndex(int index) const
+void
+MessageListModel::rmReaction(const QString& messageId, const QString& reactionId)
 {
-    return interactions_.at(index);
-}
+    std::lock_guard<std::recursive_mutex> lk(mutex_);
+    int index = indexOfMessage(messageId);
+    if (index == -1)
+        return;
+    QModelIndex modelIndex = QAbstractListModel::index(index, 0);
 
-QPair<iterator, bool>
-MessageListModel::insert(int index, QPair<QString, interaction::Info> message)
-{
-    iterator itr;
-    for (itr = interactions_.begin(); itr != interactions_.end(); ++itr) {
-        if (itr->first == message.first) {
-            return qMakePair(itr, false);
+    auto& reactions = interactions_[index].second.reactions;
+    for (auto reactionIt = reactions.begin(); reactionIt != reactions.end(); ++reactionIt) {
+        // Use a temporary QList to store updated emojis
+        QList<QVariant> updatedEmojis;
+        bool found = false;
+        for (const auto& item : reactionIt.value().toList()) {
+            auto emoji = item.value<api::interaction::Emoji>();
+            if (emoji.commitId != reactionId || found)
+                updatedEmojis.append(item);
+            else {
+                found = true;
+                break;
+            }
+        }
+        if (found) {
+            // Update the reactions with the modified list
+            reactionIt.value() = QVariant::fromValue(updatedEmojis);
+            Q_EMIT dataChanged(modelIndex, modelIndex, {Role::Reactions});
+            return;
         }
     }
-    if (index >= size()) {
-        auto iterator = insertMessage(interactions_.end(), message);
-        return qMakePair(iterator, true);
-    }
-    insertMessage(index, message);
-    return qMakePair(interactions_.end(), true);
 }
 
-int
-MessageListModel::indexOfMessage(const QString& msgId, bool reverse) const
+void
+MessageListModel::setParsedMessage(const QString& messageId, const QString& parsed)
 {
-    auto getIndex = [reverse, &msgId](const auto& start, const auto& end) -> int {
-        auto it = std::find_if(start, end, [&msgId](const auto& it) { return it.first == msgId; });
-        if (it == end) {
-            return -1;
-        }
-        return reverse ? std::distance(it, end) - 1 : std::distance(start, it);
-    };
-    return reverse ? getIndex(interactions_.rbegin(), interactions_.rend())
-                   : getIndex(interactions_.begin(), interactions_.end());
+    std::lock_guard<std::recursive_mutex> lk(mutex_);
+    int index = indexOfMessage(messageId);
+    if (index == -1) {
+        return;
+    }
+    QModelIndex modelIndex = QAbstractListModel::index(index, 0);
+    interactions_[index].second.parsedBody = parsed;
+    Q_EMIT dataChanged(modelIndex, modelIndex, {Role::ParsedBody});
 }
 
 void
-MessageListModel::updateReplies(item_t& message)
+MessageListModel::setRead(const QString& peer, const QString& messageId)
 {
-    auto replyId = message.second.commit["reply-to"];
-    auto commitId = message.second.commit["id"];
-    if (!replyId.isEmpty()) {
-        replyTo_[replyId].insert(commitId);
+    std::lock_guard<std::recursive_mutex> lk(mutex_);
+    auto i = lastDisplayedMessageUid_.find(peer);
+    if (i != lastDisplayedMessageUid_.end()) {
+        auto old = i.value();
+        messageToReaders_[old].removeAll(peer);
+        auto msgIdx = indexOfMessage(old);
+        // Remove from latest read
+        if (msgIdx != -1) {
+            QModelIndex modelIndex = QAbstractListModel::index(msgIdx, 0);
+            Q_EMIT dataChanged(modelIndex, modelIndex, {Role::Readers});
+        }
     }
-    for (const auto& msgId : replyTo_[commitId]) {
-        int index = getIndexOfMessage(msgId);
-        if (index == -1)
-            continue;
-        QModelIndex modelIndex = QAbstractListModel::index(index, 0);
-        Q_EMIT dataChanged(modelIndex, modelIndex, {Role::ReplyToAuthor, Role::ReplyToBody});
+    // update map
+    lastDisplayedMessageUid_[peer] = messageId;
+    messageToReaders_[messageId].append(peer);
+    // update interaction
+    auto msgIdx = indexOfMessage(messageId);
+    // Remove from latest read
+    if (msgIdx != -1) {
+        QModelIndex modelIndex = QAbstractListModel::index(msgIdx, 0);
+        Q_EMIT dataChanged(modelIndex, modelIndex, {Role::Readers});
     }
 }
 
-void
-MessageListModel::insertMessage(int index, item_t& message)
+QString
+MessageListModel::getRead(const QString& peer)
 {
-    Q_EMIT beginInsertRows(QModelIndex(), index, index);
-    interactions_.insert(index, message);
-    Q_EMIT endInsertRows();
-    updateReplies(message);
+    std::lock_guard<std::recursive_mutex> lk(mutex_);
+    auto i = lastDisplayedMessageUid_.find(peer);
+    if (i != lastDisplayedMessageUid_.end())
+        return i.value();
+    return "";
 }
 
-iterator
-MessageListModel::insertMessage(iterator it, item_t& message)
+QString
+MessageListModel::lastSelfMessageId(const QString& id) const
 {
-    auto index = std::distance(begin(), it);
-    Q_EMIT beginInsertRows(QModelIndex(), index, index);
-    auto insertion = interactions_.insert(it, message);
-    Q_EMIT endInsertRows();
-    updateReplies(message);
-    return insertion;
+    std::lock_guard<std::recursive_mutex> lk(mutex_);
+    for (auto it = interactions_.rbegin(); it != interactions_.rend(); ++it) {
+        auto lastType = it->second.type;
+        if (lastType == interaction::Type::TEXT and !it->second.body.isEmpty()
+            and (it->second.authorUri.isEmpty() || it->second.authorUri == id)) {
+            return it->first;
+        }
+    }
+    return {};
 }
 
-void
-MessageListModel::removeMessage(int index, iterator it)
+QPair<QString, time_t>
+MessageListModel::getDisplayedInfoForPeer(const QString& peerId)
 {
-    Q_EMIT beginRemoveRows(QModelIndex(), index, index);
-    interactions_.erase(it);
-    Q_EMIT endRemoveRows();
+    std::lock_guard<std::recursive_mutex> lk(mutex_);
+    auto it = lastDisplayedMessageUid_.find(peerId);
+    if (it == lastDisplayedMessageUid_.end())
+        return {};
+    const auto interaction = find(it.value());
+    if (interaction == interactions_.end())
+        return {};
+    return {it.value(), interaction->second.timestamp};
 }
 
-bool
-MessageListModel::contains(const QString& msgId)
+MessageListModel::iterator
+MessageListModel::find(const QString& msgId)
 {
-    return find(msgId) != interactions_.end();
+    // Note: assumes that the caller has locked the mutex.
+    return std::find_if(interactions_.begin(), interactions_.end(), [&msgId](const auto& it) {
+        return it.first == msgId;
+    });
 }
 
 int
-MessageListModel::rowCount(const QModelIndex&) const
-{
-    return interactions_.size();
+MessageListModel::move(iterator it, const QString& newParentId)
+{
+    // Note: assumes the new parent exists and that the caller has locked the mutex.
+    auto oldIndex = indexOfMessage(it->first);
+    auto newIndex = indexOfMessage(newParentId) + 1;
+    if (newIndex >= 0 && oldIndex != newIndex) {
+        qDebug() << "Moving message" << it->first << "from" << oldIndex << "to" << newIndex;
+        beginMoveRows(QModelIndex(), oldIndex, oldIndex, QModelIndex(), newIndex);
+        interactions_.move(oldIndex, newIndex);
+        endMoveRows();
+        return newIndex;
+    }
+    return -1;
 }
 
-QHash<int, QByteArray>
-MessageListModel::roleNames() const
+QVariant
+MessageListModel::data(int idx, int role) const
 {
-    using namespace MessageList;
-    QHash<int, QByteArray> roles;
-#define X(role) roles[role] = #role;
-    MSG_ROLES
-#undef X
-    return roles;
+    QModelIndex index = QAbstractListModel::index(idx, 0);
+    return data(index, role);
 }
 
-bool
-MessageListModel::isOnlyEmoji(const QString& text) const
-{
-    if (text.isEmpty())
-        return false;
-    auto codepointList = text.toUcs4();
-    for (QList<uint>::iterator it = codepointList.begin(); it != codepointList.end(); it++) {
-        auto cur = false;
-        if (*it == 20 or *it == 0x200D) {
-            cur = true;
-        } else if (0x1f000 <= *it && 0x1ffff >= *it) {
-            cur = true;
-        } else if (0x2600 <= *it && 0x27BF >= *it) {
-            cur = true;
-        } else if (0xFE00 <= *it && 0xFE0f >= *it) {
-            cur = true;
-        } else if (0xE0000 <= *it && 0xE007F >= *it) {
-            cur = true;
+QVariant
+MessageListModel::dataForItem(const item_t& item, int, int role) const
+{
+    // Used only for reply roles.
+    const auto getReplyIndex = [this, &item, &role]() -> int {
+        QString replyId = item.second.commit["reply-to"];
+        int repliedMsgIndex = -1;
+        if (!replyId.isEmpty() && (role == Role::ReplyToAuthor || role == Role::ReplyToBody)) {
+            repliedMsgIndex = indexOfMessage(replyId);
         }
-        if (!cur)
-            return false;
-    }
-    return true;
-}
+        return repliedMsgIndex;
+    };
 
-QVariant
-MessageListModel::dataForItem(item_t item, int, int role) const
-{
-    QString replyId = item.second.commit["reply-to"];
-    int repliedMsg = -1;
-    if (!replyId.isEmpty() && (role == Role::ReplyToAuthor || role == Role::ReplyToBody)) {
-        repliedMsg = getIndexOfMessage(replyId);
-    }
     switch (role) {
     case Role::Id:
         return QVariant(item.first);
@@ -440,16 +513,19 @@ MessageListModel::dataForItem(item_t item, int, int role) const
         return variantList;
     }
     case Role::ReplyTo:
-        return QVariant(replyId);
-    case Role::ReplyToAuthor:
-        return repliedMsg == -1 ? QVariant("") : QVariant(data(repliedMsg, Role::Author));
+        return QVariant(item.second.commit["reply-to"]);
+    case Role::ReplyToAuthor: {
+        const auto replyIndex = getReplyIndex();
+        return replyIndex == -1 ? QVariant("") : data(replyIndex, Role::Author);
+    }
     case Role::ReplyToBody: {
-        if (repliedMsg == -1)
+        const auto replyIndex = getReplyIndex();
+        if (replyIndex == -1)
             return QVariant("");
-        auto parsed = data(repliedMsg, Role::ParsedBody).toString();
+        auto parsed = data(replyIndex, Role::ParsedBody).toString();
         if (!parsed.isEmpty())
             return QVariant(parsed);
-        return QVariant(data(repliedMsg, Role::Body).toString());
+        return QVariant(data(replyIndex, Role::Body).toString());
     }
     case Role::TotalSize:
         return QVariant(item.second.commit["totalSize"].toInt());
@@ -460,173 +536,35 @@ MessageListModel::dataForItem(item_t item, int, int role) const
     case Role::Readers:
         return QVariant(messageToReaders_[item.first]);
     case Role::IsEmojiOnly:
-        return QVariant(replyId.isEmpty() && item.second.previousBodies.isEmpty()
-                        && isOnlyEmoji(item.second.body));
+        return QVariant(item.second.commit["reply-to"].isEmpty()
+                        && item.second.previousBodies.isEmpty() && isOnlyEmoji(item.second.body));
     case Role::Reactions:
         return QVariant(item.second.reactions);
+    case Role::Index:
+        // For DEBUG only
+        return QVariant(indexOfMessage(item.first));
     default:
         return {};
     }
 }
 
-QVariant
-MessageListModel::data(int idx, int role) const
-{
-    QModelIndex index = QAbstractListModel::index(idx, 0);
-    if (!index.isValid() || index.row() < 0 || index.row() >= rowCount()) {
-        return {};
-    }
-    return dataForItem(interactions_.at(index.row()), index.row(), role);
-}
-
-QVariant
-MessageListModel::data(const QModelIndex& index, int role) const
-{
-    if (!index.isValid() || index.row() < 0 || index.row() >= rowCount()) {
-        return {};
-    }
-    return dataForItem(interactions_.at(index.row()), index.row(), role);
-}
-
-int
-MessageListModel::getIndexOfMessage(const QString& messageId) const
-{
-    for (int i = 0; i < interactions_.size(); i++) {
-        if (atIndex(i).first == messageId) {
-            return i;
-        }
-    }
-    return -1;
-}
-
-void
-MessageListModel::addHyperlinkInfo(const QString& messageId, const QVariantMap& info)
-{
-    int index = getIndexOfMessage(messageId);
-    if (index == -1) {
-        return;
-    }
-    QModelIndex modelIndex = QAbstractListModel::index(index, 0);
-
-    interactions_[index].second.linkPreviewInfo = info;
-    Q_EMIT dataChanged(modelIndex, modelIndex, {Role::LinkPreviewInfo});
-}
-
-void
-MessageListModel::addReaction(const QString& messageId, const MapStringString& reaction)
-{
-    int index = getIndexOfMessage(messageId);
-    if (index == -1)
-        return;
-    QModelIndex modelIndex = QAbstractListModel::index(index, 0);
-
-    auto emoji = api::interaction::Emoji {reaction["id"], reaction["body"]};
-    auto& pList = interactions_[index].second.reactions[reaction["author"]];
-    QList<QVariant> newList = pList.toList();
-    newList.emplace_back(QVariant::fromValue(emoji));
-    pList = QVariantList::fromVector(newList);
-    Q_EMIT dataChanged(modelIndex, modelIndex, {Role::Reactions});
-}
-
-void
-MessageListModel::rmReaction(const QString& messageId, const QString& reactionId)
-{
-    int index = getIndexOfMessage(messageId);
-    if (index == -1)
-        return;
-    QModelIndex modelIndex = QAbstractListModel::index(index, 0);
-
-    auto& reactions = interactions_[index].second.reactions;
-    for (const auto& key : reactions.keys()) {
-        QList<QVariant> emojis = reactions[key].toList();
-        for (auto it = emojis.begin(); it != emojis.end(); ++it) {
-            auto emoji = it->value<api::interaction::Emoji>();
-            if (emoji.commitId == reactionId) {
-                emojis.erase(it);
-                reactions[key] = emojis;
-                Q_EMIT dataChanged(modelIndex, modelIndex, {Role::Reactions});
-                return;
-            }
-        }
-    }
-}
-
-void
-MessageListModel::setParsedMessage(const QString& messageId, const QString& parsed)
-{
-    int index = getIndexOfMessage(messageId);
-    if (index == -1) {
-        return;
-    }
-    QModelIndex modelIndex = QAbstractListModel::index(index, 0);
-    interactions_[index].second.parsedBody = parsed;
-    Q_EMIT dataChanged(modelIndex, modelIndex, {Role::ParsedBody});
-}
-
-void
-MessageListModel::setRead(const QString& peer, const QString& messageId)
-{
-    auto i = lastDisplayedMessageUid_.find(peer);
-    if (i != lastDisplayedMessageUid_.end()) {
-        auto old = i.value();
-        messageToReaders_[old].removeAll(peer);
-        auto msgIdx = getIndexOfMessage(old);
-        // Remove from latest read
-        if (msgIdx != -1) {
-            QModelIndex modelIndex = QAbstractListModel::index(msgIdx, 0);
-            Q_EMIT dataChanged(modelIndex, modelIndex, {Role::Readers});
-        }
-    }
-    // update map
-    lastDisplayedMessageUid_[peer] = messageId;
-    messageToReaders_[messageId].append(peer);
-    // update interaction
-    auto msgIdx = getIndexOfMessage(messageId);
-    // Remove from latest read
-    if (msgIdx != -1) {
-        QModelIndex modelIndex = QAbstractListModel::index(msgIdx, 0);
-        Q_EMIT dataChanged(modelIndex, modelIndex, {Role::Readers});
-    }
-}
-
-QString
-MessageListModel::getRead(const QString& peer)
-{
-    auto i = lastDisplayedMessageUid_.find(peer);
-    if (i != lastDisplayedMessageUid_.end())
-        return i.value();
-    return "";
-}
-
 void
-MessageListModel::emitDataChanged(iterator it, VectorInt roles)
+MessageListModel::updateReplies(const item_t& message)
 {
-    auto index = std::distance(begin(), it);
-    QModelIndex modelIndex = QAbstractListModel::index(index, 0);
-    Q_EMIT dataChanged(modelIndex, modelIndex, roles);
-}
-
-void
-MessageListModel::emitDataChanged(const QString& msgId, VectorInt roles)
-{
-    int index = getIndexOfMessage(msgId);
-    if (index == -1) {
-        return;
+    auto replyId = message.second.commit["reply-to"];
+    auto commitId = message.second.commit["id"];
+    if (!replyId.isEmpty()) {
+        replyTo_[replyId].insert(commitId);
     }
-    QModelIndex modelIndex = QAbstractListModel::index(index, 0);
-    Q_EMIT dataChanged(modelIndex, modelIndex, roles);
-}
 
-QString
-MessageListModel::lastSelfMessageId(const QString& id) const
-{
-    for (auto it = interactions_.rbegin(); it != interactions_.rend(); ++it) {
-        auto lastType = it->second.type;
-        if (lastType == interaction::Type::TEXT and !it->second.body.isEmpty()
-            and (it->second.authorUri.isEmpty() || it->second.authorUri == id)) {
-            return it->first;
-        }
+    // Use a const reference to avoid detaching
+    const auto& replies = replyTo_[commitId];
+    for (const auto& msgId : replies) {
+        int index = indexOfMessage(msgId);
+        if (index == -1)
+            continue;
+        QModelIndex modelIndex = QAbstractListModel::index(index, 0);
+        Q_EMIT dataChanged(modelIndex, modelIndex, {Role::ReplyToAuthor, Role::ReplyToBody});
     }
-    return {};
 }
 } // namespace lrc
diff --git a/src/libclient/messagelistmodel.h b/src/libclient/messagelistmodel.h
deleted file mode 100644
index 432ad1d0d..000000000
--- a/src/libclient/messagelistmodel.h
+++ /dev/null
@@ -1,167 +0,0 @@
-/*
- *  Copyright (C) 2020-2023 Savoir-faire Linux Inc.
- *
- *  Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
- *  Author: Trevor Tabah <trevor.tabah@savoirfairelinux.com>
- *  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, write to the Free Software
- *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
- */
-#pragma once
-
-#include "api/interaction.h"
-#include "api/account.h"
-
-#include <QAbstractListModel>
-
-namespace lrc {
-namespace api {
-
-namespace interaction {
-struct Info;
-}
-
-#define MSG_ROLES \
-    X(Id) \
-    X(Author) \
-    X(Body) \
-    X(ParentId) \
-    X(Timestamp) \
-    X(Duration) \
-    X(Type) \
-    X(Status) \
-    X(IsRead) \
-    X(ContactAction) \
-    X(ActionUri) \
-    X(ConfId) \
-    X(DeviceId) \
-    X(LinkPreviewInfo) \
-    X(ParsedBody) \
-    X(PreviousBodies) \
-    X(Reactions) \
-    X(ReplyTo) \
-    X(ReplyToBody) \
-    X(ReplyToAuthor) \
-    X(TotalSize) \
-    X(TransferName) \
-    X(FileExtension) \
-    X(Readers) \
-    X(IsEmojiOnly)
-
-namespace MessageList {
-Q_NAMESPACE
-enum Role {
-    DummyRole = Qt::UserRole + 1,
-#define X(role) role,
-    MSG_ROLES
-#undef X
-};
-Q_ENUM_NS(Role)
-} // namespace MessageList
-
-class MessageListModel : public QAbstractListModel
-{
-    Q_OBJECT
-
-public:
-    using item_t = const QPair<QString, interaction::Info>;
-
-    typedef QList<QPair<QString, interaction::Info>>::ConstIterator constIterator;
-    typedef QList<QPair<QString, interaction::Info>>::Iterator iterator;
-    typedef QList<QPair<QString, interaction::Info>>::reverse_iterator reverseIterator;
-
-    explicit MessageListModel(const account::Info* account, QObject* parent = nullptr);
-    ~MessageListModel() = default;
-
-    // map functions
-    QPair<iterator, bool> emplace(const QString& msgId,
-                                  interaction::Info message,
-                                  bool beginning = false);
-    iterator find(const QString& msgId);
-    iterator findActiveCall(const MapStringString& commit);
-    iterator erase(const iterator& it);
-
-    constIterator find(const QString& msgId) const;
-    QPair<iterator, bool> insert(std::pair<QString, interaction::Info> message,
-                                 bool beginning = false);
-    Q_INVOKABLE int erase(const QString& msgId);
-    interaction::Info& operator[](const QString& messageId);
-    iterator end();
-    constIterator end() const;
-    reverseIterator rend();
-
-    constIterator cend() const;
-    iterator begin();
-    constIterator begin() const;
-    reverseIterator rbegin();
-    Q_INVOKABLE int size() const;
-    void clear();
-    void reloadHistory();
-    bool empty() const;
-    interaction::Info at(const QString& intId) const;
-    QPair<QString, interaction::Info> front() const;
-    QPair<QString, interaction::Info> last() const;
-    QPair<QString, interaction::Info> atIndex(int index) const;
-
-    QPair<iterator, bool> insert(int index, QPair<QString, interaction::Info> message);
-    int indexOfMessage(const QString& msgId, bool reverse = true) const;
-
-    int rowCount(const QModelIndex& parent = QModelIndex()) const override;
-    Q_INVOKABLE virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const;
-    Q_INVOKABLE virtual QVariant data(int idx, int role = Qt::DisplayRole) const;
-    QHash<int, QByteArray> roleNames() const override;
-    QVariant dataForItem(item_t item, int indexRow, int role = Qt::DisplayRole) const;
-    bool contains(const QString& msgId);
-    int getIndexOfMessage(const QString& messageId) const;
-    void addHyperlinkInfo(const QString& messageId, const QVariantMap& info);
-    void addReaction(const QString& messageId, const MapStringString& reaction);
-    void rmReaction(const QString& messageId, const QString& reactionId);
-    void setParsedMessage(const QString& messageId, const QString& parsed);
-
-    void setRead(const QString& peer, const QString& messageId);
-    QString getRead(const QString& peer);
-
-    // use these if the underlying data model is changed from conversationmodel
-    // Note: this is not ideal, and this class should be refactored into a proper
-    // view model and absorb the interaction management logic to avoid exposing
-    // these emission wrappers
-    void emitDataChanged(iterator it, VectorInt roles = {});
-    void emitDataChanged(const QString& msgId, VectorInt roles = {});
-    bool isOnlyEmoji(const QString& text) const;
-
-    QVariantMap convertReactMessagetoQVariant(const QSet<QString>&);
-    QString lastSelfMessageId(const QString& id) const;
-
-protected:
-    using Role = MessageList::Role;
-
-private:
-    QList<QPair<QString, interaction::Info>> interactions_;
-    // Note: because read status are updated even if interaction is not loaded
-    // we need to keep track of these status outside the interaction::Info
-    // lastDisplayedMessageUid_ stores: {"peerId":"messageId"}
-    // messageToReaders_ caches: "messageId":["peer1", "peer2"]
-    // to allow quick access.
-    QMap<QString, QString> lastDisplayedMessageUid_;
-    QMap<QString, QStringList> messageToReaders_;
-    QMap<QString, QSet<QString>> replyTo_;
-    const account::Info* account_;
-    void updateReplies(item_t& message);
-
-    void insertMessage(int index, item_t& message);
-    iterator insertMessage(iterator it, item_t& message);
-    void removeMessage(int index, iterator it);
-};
-} // namespace api
-} // namespace lrc
-Q_DECLARE_METATYPE(lrc::api::MessageListModel*)
-- 
GitLab