From 84c7c9de7557f7424d074833cdaeb066bd755ac7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Blin?=
 <sebastien.blin@savoirfairelinux.com>
Date: Wed, 12 Jan 2022 13:11:16 -0500
Subject: [PATCH] swarm: sync read status across devices

Because swarm is a synched history compatible with multi-devices,
if a message from the swarm is read on one device it should be
synchronized with other devices as much as possible.
The idea of this patch is to add lastDisplayed sent in synched
datas to allow clients to update the read status. However, there
is several scenarios to take into account, because the history
can be partially synched across devices.

5 scenarios are supported:
+ if the last displayed sent by other devices is the same as the
current one, there is nothing to do.
+ if there is no last displayed for the current device, the remote
displayed message is used.
+ if the remote last displayed is not present in the repo, it means
that the commit will be fetched later, so cache the result
+ if the remote is already fetched, we check that the local last
displayed is before in the history to replace it
+ Finally if a message is announced from the same author, it means
that we need to update the last displayed.

If the last displayed message is updated, AccountMessageStatusChanged
is triggered for the client.

Doc: https://git.jami.net/savoirfairelinux/jami-project/-/wikis/technical/2.3.%20Swarm
Change-Id: Iedd29129d72cbeb43499471bdfd492dd4d49dcb6
---
 src/jamidht/conversation.cpp              | 159 +++++++---
 src/jamidht/conversation.h                |  34 ++-
 src/jamidht/conversation_module.cpp       |  52 +++-
 test/unitTest/syncHistory/syncHistory.cpp | 342 ++++++++++++++++++----
 4 files changed, 485 insertions(+), 102 deletions(-)

diff --git a/src/jamidht/conversation.cpp b/src/jamidht/conversation.cpp
index c5984fa47f..9e5d01245d 100644
--- a/src/jamidht/conversation.cpp
+++ b/src/jamidht/conversation.cpp
@@ -40,43 +40,45 @@ namespace jami {
 
 ConvInfo::ConvInfo(const Json::Value& json)
 {
-    id = json["id"].asString();
-    created = json["created"].asLargestUInt();
-    removed = json["removed"].asLargestUInt();
-    erased = json["erased"].asLargestUInt();
-    for (const auto& v : json["members"]) {
+    id = json[ConversationMapKeys::ID].asString();
+    created = json[ConversationMapKeys::CREATED].asLargestUInt();
+    removed = json[ConversationMapKeys::REMOVED].asLargestUInt();
+    erased = json[ConversationMapKeys::ERASED].asLargestUInt();
+    for (const auto& v : json[ConversationMapKeys::MEMBERS]) {
         members.emplace_back(v["uri"].asString());
     }
+    lastDisplayed = json[ConversationMapKeys::LAST_DISPLAYED].asString();
 }
 
 Json::Value
 ConvInfo::toJson() const
 {
     Json::Value json;
-    json["id"] = id;
-    json["created"] = Json::Int64(created);
+    json[ConversationMapKeys::ID] = id;
+    json[ConversationMapKeys::CREATED] = Json::Int64(created);
     if (removed) {
-        json["removed"] = Json::Int64(removed);
+        json[ConversationMapKeys::REMOVED] = Json::Int64(removed);
     }
     if (erased) {
-        json["erased"] = Json::Int64(erased);
+        json[ConversationMapKeys::ERASED] = Json::Int64(erased);
     }
     for (const auto& m : members) {
         Json::Value member;
         member["uri"] = m;
-        json["members"].append(member);
+        json[ConversationMapKeys::MEMBERS].append(member);
     }
+    json[ConversationMapKeys::LAST_DISPLAYED] = lastDisplayed;
     return json;
 }
 
 // ConversationRequest
 ConversationRequest::ConversationRequest(const Json::Value& json)
 {
-    received = json["received"].asLargestUInt();
-    declined = json["declined"].asLargestUInt();
-    from = json["from"].asString();
-    conversationId = json["conversationId"].asString();
-    auto& md = json["metadatas"];
+    received = json[ConversationMapKeys::RECEIVED].asLargestUInt();
+    declined = json[ConversationMapKeys::DECLINED].asLargestUInt();
+    from = json[ConversationMapKeys::FROM].asString();
+    conversationId = json[ConversationMapKeys::CONVERSATIONID].asString();
+    auto& md = json[ConversationMapKeys::METADATAS];
     for (const auto& member : md.getMemberNames()) {
         metadatas.emplace(member, md[member].asString());
     }
@@ -86,13 +88,13 @@ Json::Value
 ConversationRequest::toJson() const
 {
     Json::Value json;
-    json["conversationId"] = conversationId;
-    json["from"] = from;
-    json["received"] = static_cast<uint32_t>(received);
+    json[ConversationMapKeys::CONVERSATIONID] = conversationId;
+    json[ConversationMapKeys::FROM] = from;
+    json[ConversationMapKeys::RECEIVED] = static_cast<uint32_t>(received);
     if (declined)
-        json["declined"] = static_cast<uint32_t>(declined);
+        json[ConversationMapKeys::DECLINED] = static_cast<uint32_t>(declined);
     for (const auto& [key, value] : metadatas) {
-        json["metadatas"][key] = value;
+        json[ConversationMapKeys::METADATAS][key] = value;
     }
     return json;
 }
@@ -101,11 +103,11 @@ std::map<std::string, std::string>
 ConversationRequest::toMap() const
 {
     auto result = metadatas;
-    result["id"] = conversationId;
-    result["from"] = from;
+    result[ConversationMapKeys::ID] = conversationId;
+    result[ConversationMapKeys::FROM] = from;
     if (declined)
-        result["declined"] = std::to_string(declined);
-    result["received"] = std::to_string(received);
+        result[ConversationMapKeys::DECLINED] = std::to_string(declined);
+    result[ConversationMapKeys::RECEIVED] = std::to_string(received);
     return result;
 }
 
@@ -161,7 +163,7 @@ public:
                                     + shared->getAccountID() + DIR_SEPARATOR_STR
                                     + "conversation_data" + DIR_SEPARATOR_STR + repository_->id();
             fetchedPath_ = conversationDataPath_ + DIR_SEPARATOR_STR + "fetched";
-            lastDisplayedPath_ = conversationDataPath_ + DIR_SEPARATOR_STR + "lastDisplayed";
+            lastDisplayedPath_ = conversationDataPath_ + DIR_SEPARATOR_STR + ConversationMapKeys::LAST_DISPLAYED;
             loadFetched();
             loadLastDisplayed();
         }
@@ -202,7 +204,7 @@ public:
         }
         auto convId = repository_->id();
         auto ok = !commits.empty();
-        auto lastId = ok ? commits.rbegin()->at("id") : "";
+        auto lastId = ok ? commits.rbegin()->at(ConversationMapKeys::ID) : "";
         if (ok) {
             bool announceMember = false;
             for (const auto& c : commits) {
@@ -244,6 +246,27 @@ public:
                 emitSignal<DRing::ConversationSignal::MessageReceived>(shared->getAccountID(),
                                                                        convId,
                                                                        c);
+                // check if we should update lastDisplayed
+                std::unique_lock<std::mutex> lk(lastDisplayedMtx_);
+                auto cached = lastDisplayed_.find(ConversationMapKeys::CACHED);
+                auto updateCached = false;
+                if (cached != lastDisplayed_.end()) {
+                    // If we found the commit we wait
+                    if (cached->second == c.at(ConversationMapKeys::ID)) {
+                        updateCached = true;
+                        lastDisplayed_.erase(cached);
+                    }
+                } else if (c.at("author") == shared->getUsername()) {
+                    updateCached = true;
+                }
+
+                if (updateCached) {
+                    lastDisplayed_[shared->getUsername()] = c.at(ConversationMapKeys::ID);
+                    saveLastDisplayed();
+                    lk.unlock();
+                    if (lastDisplayedUpdatedCb_)
+                        lastDisplayedUpdatedCb_(convId, c.at(ConversationMapKeys::ID));
+                }
             }
 
             if (announceMember) {
@@ -274,7 +297,7 @@ public:
         msgpack::pack(file, fetchedDevices_);
     }
 
-    void loadLastDisplayed()
+    void loadLastDisplayed() const
     {
         try {
             // read file
@@ -288,7 +311,7 @@ public:
         }
     }
 
-    void saveLastDisplayed()
+    void saveLastDisplayed() const
     {
         std::ofstream file(lastDisplayedPath_, std::ios::trunc | std::ios::binary);
         msgpack::pack(file, lastDisplayed_);
@@ -313,9 +336,11 @@ public:
     std::string fetchedPath_ {};
     std::mutex fetchedDevicesMtx_ {};
     std::set<std::string> fetchedDevices_ {};
+    // Manage last message displayed
     std::string lastDisplayedPath_ {};
-    std::mutex lastDisplayedMtx_ {};
-    std::map<std::string, std::string> lastDisplayed_ {};
+    mutable std::mutex lastDisplayedMtx_ {}; // for lastDisplayed_
+    mutable std::map<std::string, std::string> lastDisplayed_ {};
+    std::function<void(const std::string&, const std::string&)> lastDisplayedUpdatedCb_ {};
 };
 
 bool
@@ -404,7 +429,7 @@ Conversation::Impl::convCommitToMap(const ConversationCommit& commit) const
         if (!extension.empty())
             message["fileId"] += "." + extension;
     }
-    message["id"] = commit.id;
+    message[ConversationMapKeys::ID] = commit.id;
     message["parents"] = parents;
     message["linearizedParent"] = commit.linearized_parent;
     message["author"] = authorId;
@@ -574,7 +599,7 @@ Conversation::getMembers(bool includeInvited, bool includeLeft) const
         if (itDisplayed != pimpl_->lastDisplayed_.end()) {
             lastDisplayed = itDisplayed->second;
         }
-        mm["lastDisplayed"] = std::move(lastDisplayed);
+        mm[ConversationMapKeys::LAST_DISPLAYED] = std::move(lastDisplayed);
         result.emplace_back(std::move(mm));
     }
     return result;
@@ -751,7 +776,7 @@ Conversation::lastCommitId() const
     auto messages = pimpl_->loadMessages("", "", 1);
     if (messages.empty())
         return {};
-    return messages.front().at("id");
+    return messages.front().at(ConversationMapKeys::ID);
 }
 
 bool
@@ -914,9 +939,9 @@ Conversation::generateInvitation() const
     std::map<std::string, std::string> invite;
     Json::Value root;
     for (const auto& [k, v] : infos()) {
-        root["metadatas"][k] = v;
+        root[ConversationMapKeys::METADATAS][k] = v;
     }
-    root["conversationId"] = id();
+    root[ConversationMapKeys::CONVERSATIONID] = id();
     Json::StreamWriterBuilder wbuilder;
     wbuilder["commentStyle"] = "None";
     wbuilder["indentation"] = "";
@@ -1125,9 +1150,67 @@ Conversation::hasFetched(const std::string& deviceId)
 void
 Conversation::setMessageDisplayed(const std::string& uri, const std::string& interactionId)
 {
-    std::lock_guard<std::mutex> lk(pimpl_->lastDisplayedMtx_);
-    pimpl_->lastDisplayed_[uri] = interactionId;
-    pimpl_->saveLastDisplayed();
+    if (auto acc = pimpl_->account_.lock()) {
+        if (uri == acc->getUsername() && pimpl_->lastDisplayedUpdatedCb_)
+            pimpl_->lastDisplayedUpdatedCb_(pimpl_->repository_->id(), interactionId);
+        std::lock_guard<std::mutex> lk(pimpl_->lastDisplayedMtx_);
+        pimpl_->lastDisplayed_[uri] = interactionId;
+        pimpl_->saveLastDisplayed();
+    }
+}
+
+void
+Conversation::updateLastDisplayed(const std::string& lastDisplayed)
+{
+    auto acc = pimpl_->account_.lock();
+    if (!acc or !pimpl_->repository_)
+        return;
+
+    // Here, there can be several different scenarios
+    // 1. lastDisplayed is the current last displayed interaction. Nothing to do.
+    std::unique_lock<std::mutex> lk(pimpl_->lastDisplayedMtx_);
+    auto& currentLastDisplayed = pimpl_->lastDisplayed_[acc->getUsername()];
+    if (lastDisplayed == currentLastDisplayed)
+        return;
+
+    auto updateLastDisplayed = [&]() {
+        currentLastDisplayed = lastDisplayed;
+        pimpl_->saveLastDisplayed();
+        lk.unlock();
+        if (pimpl_->lastDisplayedUpdatedCb_)
+            pimpl_->lastDisplayedUpdatedCb_(pimpl_->repository_->id(), lastDisplayed);
+    };
+
+    auto hasCommit = pimpl_->repository_->getCommit(lastDisplayed, false) != std::nullopt;
+
+    // 2. lastDisplayed can be a future commit, not fetched yet
+    // In this case, we can cache it here, and check future announces to update it
+    if (!hasCommit) {
+        pimpl_->lastDisplayed_[ConversationMapKeys::CACHED] = lastDisplayed;
+        pimpl_->saveLastDisplayed();
+        return;
+    }
+
+    // 3. There is no lastDisplayed yet
+    if (currentLastDisplayed.empty()) {
+        updateLastDisplayed();
+        return;
+    }
+
+    // 4. lastDisplayed is present in the repository. In this can, if it's a more recent
+    // commit than the current one, update it, else drop it.
+    auto commitsSinceLast = pimpl_->repository_->log("", lastDisplayed, false, true).size();
+    auto commitsSincePreviousLast = pimpl_->repository_->log("", currentLastDisplayed, false, true)
+                                        .size();
+    if (commitsSincePreviousLast > commitsSinceLast)
+        updateLastDisplayed();
+}
+
+void
+Conversation::onLastDisplayedUpdated(
+    std::function<void(const std::string&, const std::string&)>&& lastDisplayedUpdatedCb)
+{
+    pimpl_->lastDisplayedUpdatedCb_ = std::move(lastDisplayedUpdatedCb);
 }
 
 uint32_t
diff --git a/src/jamidht/conversation.h b/src/jamidht/conversation.h
index 41adc79dd3..88a3b24f3b 100644
--- a/src/jamidht/conversation.h
+++ b/src/jamidht/conversation.h
@@ -31,6 +31,21 @@
 
 namespace jami {
 
+namespace ConversationMapKeys {
+static constexpr const char* ID = "id";
+static constexpr const char* CREATED = "created";
+static constexpr const char* REMOVED = "removed";
+static constexpr const char* ERASED = "erased";
+static constexpr const char* MEMBERS = "members";
+static constexpr const char* LAST_DISPLAYED = "lastDisplayed";
+static constexpr const char* CACHED = "cached";
+static constexpr const char* RECEIVED = "received";
+static constexpr const char* DECLINED = "declined";
+static constexpr const char* FROM = "from";
+static constexpr const char* CONVERSATIONID = "conversationId";
+static constexpr const char* METADATAS = "metadatas";
+}
+
 /**
  * A ConversationRequest is a request which corresponds to a trust request, but for conversations
  * It's signed by the sender and contains the members list, the conversationId, and the metadatas
@@ -69,13 +84,14 @@ struct ConvInfo
     time_t removed {0};
     time_t erased {0};
     std::vector<std::string> members;
+    std::string lastDisplayed {};
 
     ConvInfo() = default;
     ConvInfo(const Json::Value& json);
 
     Json::Value toJson() const;
 
-    MSGPACK_DEFINE_MAP(id, created, removed, erased, members)
+    MSGPACK_DEFINE_MAP(id, created, removed, erased, members, lastDisplayed)
 };
 
 class JamiAccount;
@@ -102,6 +118,14 @@ public:
                  const std::string& conversationId);
     ~Conversation();
 
+    /**
+     * Add a callback to update upper layers
+     * @note to call after the construction (and before ConversationReady)
+     * @param lastDisplayedUpdatedCb    Triggered when last displayed for account is updated
+     */
+    void onLastDisplayedUpdated(
+        std::function<void(const std::string&, const std::string&)>&& lastDisplayedUpdatedCb);
+
     std::string id() const;
 
     // Member management
@@ -119,7 +143,7 @@ public:
      * {
      *  "uri":"xxx",
      *  "role":"member/admin/invited",
-     *  "lastRead":"id"
+     *  "lastDisplayed":"id"
      *  ...
      * }
      */
@@ -332,6 +356,12 @@ public:
      */
     void setMessageDisplayed(const std::string& uri, const std::string& interactionId);
 
+    /**
+     * Compute, with multi device support the last message displayed of a conversation
+     * @param lastDisplayed      Latest displayed interaction
+     */
+    void updateLastDisplayed(const std::string& lastDisplayed);
+
     /**
      * Retrieve how many interactions there is from HEAD to interactionId
      * @param toId      "" for getting the whole history
diff --git a/src/jamidht/conversation_module.cpp b/src/jamidht/conversation_module.cpp
index fadcb26021..244dbc1e31 100644
--- a/src/jamidht/conversation_module.cpp
+++ b/src/jamidht/conversation_module.cpp
@@ -58,10 +58,12 @@ public:
      * Clone a conversation (initial) from device
      * @param deviceId
      * @param convId
+     * @param lastDisplayed      Last message displayed by account
      */
     void cloneConversation(const std::string& deviceId,
                            const std::string& peer,
-                           const std::string& convId);
+                           const std::string& convId,
+                           const std::string& lastDisplayed = "");
 
     /**
      * Pull remote device
@@ -122,6 +124,27 @@ public:
         saveConvInfos();
     }
 
+    /**
+     * Updates last displayed for sync infos and client
+     */
+    void onLastDisplayedUpdated(const std::string& convId, const std::string& lastId)
+    {
+        // must not lock as used in callback from a conversation,
+        // so convInfos_ cannot change for convId
+        auto itConv = convInfos_.find(convId);
+        if (itConv != convInfos_.end())
+            itConv->second.lastDisplayed = lastId;
+        saveConvInfos();
+
+        // Updates info for client
+        emitSignal<DRing::ConfigurationSignal::AccountMessageStatusChanged>(
+            accountId_,
+            convId,
+            username_,
+            lastId,
+            static_cast<int>(DRing::Account::MessageStates::DISPLAYED));
+    }
+
     std::weak_ptr<JamiAccount> account_;
     NeedsSyncingCb needsSyncingCb_;
     SengMsgCb sendMsgCb_;
@@ -256,7 +279,8 @@ ConversationModule::Impl::Impl(std::weak_ptr<JamiAccount>&& account,
 void
 ConversationModule::Impl::cloneConversation(const std::string& deviceId,
                                             const std::string& peerUri,
-                                            const std::string& convId)
+                                            const std::string& convId,
+                                            const std::string& lastDisplayed)
 {
     JAMI_DBG("[Account %s] Clone conversation on device %s", accountId_.c_str(), deviceId.c_str());
 
@@ -268,6 +292,13 @@ ConversationModule::Impl::cloneConversation(const std::string& deviceId,
         // at the same time.
         if (!startFetch(convId, deviceId)) {
             JAMI_WARN("[Account %s] Already fetching %s", accountId_.c_str(), convId.c_str());
+            std::lock_guard<std::mutex> lk(convInfosMtx_);
+            auto ci = convInfos_.find(convId);
+            if (ci != convInfos_.end() && ci->second.lastDisplayed.empty()) {
+                // If fetchNewCommits called before sync
+                ci->second.lastDisplayed = lastDisplayed;
+                saveConvInfos();
+            }
             return;
         }
         onNeedSocket_(convId, deviceId, [=](const auto& channel) {
@@ -297,6 +328,7 @@ ConversationModule::Impl::cloneConversation(const std::string& deviceId,
         info.id = convId;
         info.created = std::time(nullptr);
         info.members.emplace_back(username_);
+        info.lastDisplayed = lastDisplayed;
         if (peerUri != username_)
             info.members.emplace_back(peerUri);
 
@@ -304,6 +336,10 @@ ConversationModule::Impl::cloneConversation(const std::string& deviceId,
         convInfos_[info.id] = std::move(info);
         saveConvInfos();
     } else {
+        std::unique_lock<std::mutex> lk(conversationsMtx_);
+        auto conversation = conversations_.find(convId);
+        if (conversation != conversations_.end() && conversation->second)
+            conversation->second->updateLastDisplayed(lastDisplayed);
         JAMI_INFO("[Account %s] Already have conversation %s", accountId_.c_str(), convId.c_str());
     }
 }
@@ -450,6 +486,8 @@ ConversationModule::Impl::handlePendingConversation(const std::string& conversat
     };
     try {
         auto conversation = std::make_shared<Conversation>(account_, deviceId, conversationId);
+        conversation->onLastDisplayedUpdated(
+            std::move([&](auto convId, auto lastId) { onLastDisplayedUpdated(convId, lastId); }));
         if (!conversation->isMember(username_, true)) {
             JAMI_ERR("Conversation cloned but doesn't seems to be a valid member");
             conversation->erase();
@@ -464,6 +502,9 @@ ConversationModule::Impl::handlePendingConversation(const std::string& conversat
             auto itConv = convInfos_.find(conversationId);
             if (itConv != convInfos_.end() && itConv->second.removed)
                 removeRepo = true;
+            if (itConv != convInfos_.end() && !itConv->second.lastDisplayed.empty()) {
+                conversation->updateLastDisplayed(itConv->second.lastDisplayed);
+            }
             conversations_.emplace(conversationId, conversation);
         }
         if (removeRepo) {
@@ -726,6 +767,8 @@ ConversationModule::loadConversations()
     for (const auto& repository : conversationsRepositories) {
         try {
             auto conv = std::make_shared<Conversation>(pimpl_->account_, repository);
+            conv->onLastDisplayedUpdated(std::move(
+                [&](auto convId, auto lastId) { pimpl_->onLastDisplayedUpdated(convId, lastId); }));
             auto convInfo = pimpl_->convInfos_.find(repository);
             if (convInfo == pimpl_->convInfos_.end()) {
                 JAMI_ERR() << "Missing conv info for " << repository << ". This is a bug!";
@@ -733,6 +776,7 @@ ConversationModule::loadConversations()
                 info.id = repository;
                 info.created = std::time(nullptr);
                 info.members = conv->memberUris();
+                info.lastDisplayed = conv->infos()[ConversationMapKeys::LAST_DISPLAYED];
                 addConvInfo(info);
             }
             pimpl_->conversations_.emplace(repository, std::move(conv));
@@ -942,6 +986,8 @@ ConversationModule::startConversation(ConversationMode mode, const std::string&
     std::shared_ptr<Conversation> conversation;
     try {
         conversation = std::make_shared<Conversation>(pimpl_->account_, mode, otherMember);
+        conversation->onLastDisplayedUpdated(std::move(
+            [&](auto convId, auto lastId) { pimpl_->onLastDisplayedUpdated(convId, lastId); }));
     } catch (const std::exception& e) {
         JAMI_ERR("[Account %s] Error while generating a conversation %s",
                  pimpl_->accountId_.c_str(),
@@ -1175,7 +1221,7 @@ ConversationModule::onSyncData(const SyncMsg& msg,
             auto itConv = pimpl_->convInfos_.find(convId);
             if (itConv != pimpl_->convInfos_.end() && itConv->second.removed)
                 continue;
-            pimpl_->cloneConversation(deviceId, peerId, convId);
+            pimpl_->cloneConversation(deviceId, peerId, convId, convInfo.lastDisplayed);
         } else {
             {
                 std::lock_guard<std::mutex> lk(pimpl_->conversationsMtx_);
diff --git a/test/unitTest/syncHistory/syncHistory.cpp b/test/unitTest/syncHistory/syncHistory.cpp
index 4ea7e78c1a..4d40559faf 100644
--- a/test/unitTest/syncHistory/syncHistory.cpp
+++ b/test/unitTest/syncHistory/syncHistory.cpp
@@ -20,6 +20,7 @@
 #include <cppunit/TestFixture.h>
 #include <cppunit/extensions/HelperMacros.h>
 
+#include <chrono>
 #include <condition_variable>
 #include <filesystem>
 
@@ -34,6 +35,7 @@
 #include "common.h"
 
 using namespace DRing::Account;
+using namespace std::literals::chrono_literals;
 
 namespace jami {
 namespace test {
@@ -70,6 +72,8 @@ private:
     void testSyncOneToOne();
     void testConversationRequestRemoved();
     void testProfileReceivedMultiDevice();
+    void testLastInteractionAfterClone();
+    void testLastInteractionAfterSomeMessages();
 
     CPPUNIT_TEST_SUITE(SyncHistoryTest);
     CPPUNIT_TEST(testCreateConversationThenSync);
@@ -84,6 +88,8 @@ private:
     CPPUNIT_TEST(testSyncOneToOne);
     CPPUNIT_TEST(testConversationRequestRemoved);
     CPPUNIT_TEST(testProfileReceivedMultiDevice);
+    CPPUNIT_TEST(testLastInteractionAfterClone);
+    CPPUNIT_TEST(testLastInteractionAfterSomeMessages);
     CPPUNIT_TEST_SUITE_END();
 };
 
@@ -153,9 +159,7 @@ SyncHistoryTest::testCreateConversationThenSync()
             }));
     DRing::registerSignalHandlers(confHandlers);
     alice2Id = Manager::instance().addAccount(details);
-    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] {
-        return alice2Ready && conversationReady;
-    }));
+    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return alice2Ready && conversationReady; }));
     DRing::unregisterSignalHandlers();
 }
 
@@ -203,9 +207,7 @@ SyncHistoryTest::testCreateConversationWithOnlineDevice()
             }));
     DRing::registerSignalHandlers(confHandlers);
     alice2Id = Manager::instance().addAccount(details);
-    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(60), [&] {
-        return alice2Ready && conversationReady;
-    }));
+    CPPUNIT_ASSERT(cv.wait_for(lk, 60s, [&] { return alice2Ready && conversationReady; }));
     DRing::unregisterSignalHandlers();
 }
 
@@ -241,13 +243,13 @@ SyncHistoryTest::testCreateConversationWithMessagesThenAddDevice()
     // Start conversation
     messageReceived = false;
     DRing::sendMessage(aliceId, convId, std::string("Message 1"), "");
-    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(10), [&] { return messageReceived; }));
+    CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] { return messageReceived; }));
     messageReceived = false;
     DRing::sendMessage(aliceId, convId, std::string("Message 2"), "");
-    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(10), [&] { return messageReceived; }));
+    CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] { return messageReceived; }));
     messageReceived = false;
     DRing::sendMessage(aliceId, convId, std::string("Message 3"), "");
-    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(10), [&] { return messageReceived; }));
+    CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] { return messageReceived; }));
 
     // Now create alice2
     auto aliceArchive = std::filesystem::current_path().string() + "/alice.gz";
@@ -263,7 +265,7 @@ SyncHistoryTest::testCreateConversationWithMessagesThenAddDevice()
     alice2Id = Manager::instance().addAccount(details);
 
     // Check if conversation is ready
-    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(60), [&]() { return conversationReady; }));
+    CPPUNIT_ASSERT(cv.wait_for(lk, 60s, [&]() { return conversationReady; }));
     std::vector<std::map<std::string, std::string>> messages;
     confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationLoaded>(
         [&](uint32_t,
@@ -277,7 +279,7 @@ SyncHistoryTest::testCreateConversationWithMessagesThenAddDevice()
         }));
     DRing::registerSignalHandlers(confHandlers);
     DRing::loadConversationMessages(alice2Id, convId, "", 0);
-    cv.wait_for(lk, std::chrono::seconds(30));
+    cv.wait_for(lk, 30s);
     DRing::unregisterSignalHandlers();
     confHandlers.clear();
 
@@ -297,17 +299,17 @@ SyncHistoryTest::testCreateMultipleConversationThenAddDevice()
     DRing::sendMessage(aliceId, convId, std::string("Message 1"), "");
     DRing::sendMessage(aliceId, convId, std::string("Message 2"), "");
     DRing::sendMessage(aliceId, convId, std::string("Message 3"), "");
-    std::this_thread::sleep_for(std::chrono::seconds(1));
+    std::this_thread::sleep_for(1s);
     auto convId2 = DRing::startConversation(aliceId);
     DRing::sendMessage(aliceId, convId2, std::string("Message 1"), "");
     DRing::sendMessage(aliceId, convId2, std::string("Message 2"), "");
     DRing::sendMessage(aliceId, convId2, std::string("Message 3"), "");
-    std::this_thread::sleep_for(std::chrono::seconds(1));
+    std::this_thread::sleep_for(1s);
     auto convId3 = DRing::startConversation(aliceId);
     DRing::sendMessage(aliceId, convId3, std::string("Message 1"), "");
     DRing::sendMessage(aliceId, convId3, std::string("Message 2"), "");
     DRing::sendMessage(aliceId, convId3, std::string("Message 3"), "");
-    std::this_thread::sleep_for(std::chrono::seconds(1));
+    std::this_thread::sleep_for(1s);
     auto convId4 = DRing::startConversation(aliceId);
     DRing::sendMessage(aliceId, convId4, std::string("Message 1"), "");
     DRing::sendMessage(aliceId, convId4, std::string("Message 2"), "");
@@ -342,8 +344,7 @@ SyncHistoryTest::testCreateMultipleConversationThenAddDevice()
     confHandlers.clear();
 
     // Check if conversation is ready
-    CPPUNIT_ASSERT(
-        cv.wait_for(lk, std::chrono::seconds(60), [&]() { return conversationReady == 4; }));
+    CPPUNIT_ASSERT(cv.wait_for(lk, 60s, [&]() { return conversationReady == 4; }));
     DRing::unregisterSignalHandlers();
 }
 
@@ -391,8 +392,7 @@ SyncHistoryTest::testReceivesInviteThenAddDevice()
 
     memberEvent = false;
     DRing::addConversationMember(bobId, convId, uri);
-    CPPUNIT_ASSERT(
-        cv.wait_for(lk, std::chrono::seconds(30), [&] { return memberEvent && requestReceived; }));
+    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return memberEvent && requestReceived; }));
     DRing::unregisterSignalHandlers();
     confHandlers.clear();
 
@@ -420,7 +420,7 @@ SyncHistoryTest::testReceivesInviteThenAddDevice()
             }));
     DRing::registerSignalHandlers(confHandlers);
 
-    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { return requestReceived; }));
+    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return requestReceived; }));
     DRing::unregisterSignalHandlers();
 }
 
@@ -477,11 +477,9 @@ SyncHistoryTest::testRemoveConversationOnAllDevices()
     DRing::registerSignalHandlers(confHandlers);
 
     alice2Id = Manager::instance().addAccount(details);
-    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(60), [&] {
-        return alice2Ready && conversationReady;
-    }));
+    CPPUNIT_ASSERT(cv.wait_for(lk, 60s, [&] { return alice2Ready && conversationReady; }));
     DRing::removeConversation(aliceId, convId);
-    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { return conversationRemoved; }));
+    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return conversationRemoved; }));
 
     DRing::unregisterSignalHandlers();
 }
@@ -489,7 +487,6 @@ SyncHistoryTest::testRemoveConversationOnAllDevices()
 void
 SyncHistoryTest::testSyncCreateAccountExportDeleteReimportOldBackup()
 {
-    std::this_thread::sleep_for(std::chrono::seconds(10));
     auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
     auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId);
     auto bobUri = bobAccount->getUsername();
@@ -553,17 +550,17 @@ SyncHistoryTest::testSyncCreateAccountExportDeleteReimportOldBackup()
     DRing::registerSignalHandlers(confHandlers);
 
     DRing::addConversationMember(aliceId, convId, bobUri);
-    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; }));
+    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return requestReceived; }));
 
     DRing::acceptConversationRequest(bobId, convId);
-    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return conversationReady; }));
+    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return conversationReady; }));
 
     // Wait that alice sees Bob
-    cv.wait_for(lk, std::chrono::seconds(30), [&]() { return messageAliceReceived == 1; });
+    cv.wait_for(lk, 30s, [&]() { return messageAliceReceived == 1; });
 
     // disable account (same as removed)
     Manager::instance().sendRegister(aliceId, false);
-    std::this_thread::sleep_for(std::chrono::seconds(5));
+    std::this_thread::sleep_for(5s);
 
     std::map<std::string, std::string> details = DRing::getAccountTemplate("RING");
     details[ConfProperties::TYPE] = "RING";
@@ -576,18 +573,18 @@ SyncHistoryTest::testSyncCreateAccountExportDeleteReimportOldBackup()
     requestReceived = false;
     conversationReady = false;
     alice2Id = Manager::instance().addAccount(details);
-    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { return alice2Ready; }));
+    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return alice2Ready; }));
 
     // This will trigger a conversation request. Cause alice2 can't know first conversation
     DRing::sendMessage(bobId, convId, std::string("hi"), "");
-    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { return requestReceived; }));
+    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return requestReceived; }));
 
     DRing::acceptConversationRequest(alice2Id, convId);
-    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { return conversationReady; }));
+    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return conversationReady; }));
 
     messageBobReceived = 0;
     DRing::sendMessage(alice2Id, convId, std::string("hi"), "");
-    cv.wait_for(lk, std::chrono::seconds(30), [&]() { return messageBobReceived == 1; });
+    cv.wait_for(lk, 30s, [&]() { return messageBobReceived == 1; });
     DRing::unregisterSignalHandlers();
 }
 
@@ -661,16 +658,16 @@ SyncHistoryTest::testSyncCreateAccountExportDeleteReimportWithConvId()
     DRing::registerSignalHandlers(confHandlers);
 
     DRing::addConversationMember(aliceId, convId, bobUri);
-    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; }));
+    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return requestReceived; }));
 
     DRing::acceptConversationRequest(bobId, convId);
-    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return conversationReady; }));
+    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return conversationReady; }));
 
     // We need to track presence to know when to sync
     bobAccount->trackBuddyPresence(aliceUri, true);
 
     // Wait that alice sees Bob
-    cv.wait_for(lk, std::chrono::seconds(30), [&]() { return memberAddGenerated; });
+    cv.wait_for(lk, 30s, [&]() { return memberAddGenerated; });
 
     // Backup alice after startConversation with member
     auto aliceArchive = std::filesystem::current_path().string() + "/alice.gz";
@@ -678,7 +675,7 @@ SyncHistoryTest::testSyncCreateAccountExportDeleteReimportWithConvId()
 
     // disable account (same as removed)
     Manager::instance().sendRegister(aliceId, false);
-    std::this_thread::sleep_for(std::chrono::seconds(5));
+    std::this_thread::sleep_for(5s);
 
     std::map<std::string, std::string> details = DRing::getAccountTemplate("RING");
     details[ConfProperties::TYPE] = "RING";
@@ -692,12 +689,12 @@ SyncHistoryTest::testSyncCreateAccountExportDeleteReimportWithConvId()
     conversationReady = false;
     alice2Id = Manager::instance().addAccount(details);
     // Should retrieve conversation, no need for action as the convInfos is in the archive
-    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { return alice2Ready; }));
-    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { return conversationReady; }));
+    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return alice2Ready; }));
+    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return conversationReady; }));
 
     messageBobReceived = 0;
     DRing::sendMessage(alice2Id, convId, std::string("hi"), "");
-    cv.wait_for(lk, std::chrono::seconds(30), [&]() { return messageBobReceived == 1; });
+    cv.wait_for(lk, 30s, [&]() { return messageBobReceived == 1; });
     DRing::unregisterSignalHandlers();
 }
 
@@ -764,7 +761,7 @@ SyncHistoryTest::testSyncCreateAccountExportDeleteReimportWithConvReq()
     DRing::registerSignalHandlers(confHandlers);
 
     DRing::addConversationMember(bobId, convId, aliceUri);
-    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; }));
+    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return requestReceived; }));
 
     // Backup alice after startConversation with member
     auto aliceArchive = std::filesystem::current_path().string() + "/alice.gz";
@@ -772,7 +769,7 @@ SyncHistoryTest::testSyncCreateAccountExportDeleteReimportWithConvReq()
 
     // disable account (same as removed)
     Manager::instance().sendRegister(aliceId, false);
-    std::this_thread::sleep_for(std::chrono::seconds(5));
+    std::this_thread::sleep_for(5s);
 
     std::map<std::string, std::string> details = DRing::getAccountTemplate("RING");
     details[ConfProperties::TYPE] = "RING";
@@ -784,13 +781,12 @@ SyncHistoryTest::testSyncCreateAccountExportDeleteReimportWithConvReq()
     details[ConfProperties::ARCHIVE_PATH] = aliceArchive;
     conversationReady = false;
     alice2Id = Manager::instance().addAccount(details);
-    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { return alice2Ready; }));
+    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return alice2Ready; }));
 
     // Should get the same request as before.
     DRing::acceptConversationRequest(alice2Id, convId);
-    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() {
-        return conversationReady && messageBobReceived == 1;
-    }));
+    CPPUNIT_ASSERT(
+        cv.wait_for(lk, 30s, [&]() { return conversationReady && messageBobReceived == 1; }));
     DRing::unregisterSignalHandlers();
 }
 
@@ -828,7 +824,7 @@ SyncHistoryTest::testSyncOneToOne()
     DRing::registerSignalHandlers(confHandlers);
 
     aliceAccount->addContact(bobAccount->getUsername());
-    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { return !convId.empty(); }));
+    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return !convId.empty(); }));
 
     // Now create alice2
     auto aliceArchive = std::filesystem::current_path().string() + "/alice.gz";
@@ -843,9 +839,7 @@ SyncHistoryTest::testSyncOneToOne()
     details[ConfProperties::ARCHIVE_PATH] = aliceArchive;
 
     alice2Id = Manager::instance().addAccount(details);
-    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] {
-        return alice2Ready && conversationReady;
-    }));
+    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return alice2Ready && conversationReady; }));
     DRing::unregisterSignalHandlers();
 }
 
@@ -883,7 +877,7 @@ SyncHistoryTest::testConversationRequestRemoved()
     DRing::registerSignalHandlers(confHandlers);
 
     DRing::addConversationMember(bobId, convId, uri);
-    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { return requestReceived; }));
+    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return requestReceived; }));
     DRing::unregisterSignalHandlers();
     confHandlers.clear();
 
@@ -923,13 +917,11 @@ SyncHistoryTest::testConversationRequestRemoved()
             }));
     DRing::registerSignalHandlers(confHandlers);
 
-    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { return requestReceived; }));
+    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return requestReceived; }));
     // Now decline trust request, this should trigger ConversationRequestDeclined both sides for Alice
     DRing::declineConversationRequest(aliceId, convId);
 
-    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] {
-        return requestDeclined && requestDeclined2;
-    }));
+    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return requestDeclined && requestDeclined2; }));
 
     DRing::unregisterSignalHandlers();
 }
@@ -1018,10 +1010,10 @@ END:VCARD";
     DRing::registerSignalHandlers(confHandlers);
     aliceAccount->addContact(bobUri);
     aliceAccount->sendTrustRequest(bobUri, {});
-    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; }));
+    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return requestReceived; }));
 
     CPPUNIT_ASSERT(bobAccount->acceptTrustRequest(aliceUri));
-    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() {
+    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() {
         return conversationReady && bobProfileReceived && aliceProfileReceived;
     }));
     CPPUNIT_ASSERT(fileutils::isFile(bobDest));
@@ -1038,12 +1030,244 @@ END:VCARD";
     bobProfileReceived = false, aliceProfileReceived = false;
     alice2Id = Manager::instance().addAccount(details);
 
-    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(60), [&] {
-        return aliceProfileReceived && bobProfileReceived;
-    }));
+    CPPUNIT_ASSERT(cv.wait_for(lk, 60s, [&] { return aliceProfileReceived && bobProfileReceived; }));
     DRing::unregisterSignalHandlers();
 }
 
+void
+SyncHistoryTest::testLastInteractionAfterClone()
+{
+    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
+    auto aliceUri = aliceAccount->getUsername();
+    auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId);
+    auto bobUri = bobAccount->getUsername();
+    std::string convId;
+
+    std::mutex mtx;
+    std::unique_lock<std::mutex> lk {mtx};
+    std::condition_variable cv;
+    std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers;
+    auto messageReceived = false;
+    std::string msgId = "";
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>(
+        [&](const std::string& /* accountId */,
+            const std::string& /* conversationId */,
+            std::map<std::string, std::string> message) {
+            messageReceived = true;
+            msgId = message["id"];
+            cv.notify_one();
+        }));
+    auto conversationReady = false;
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>(
+        [&](const std::string& accountId, const std::string& conversationId) {
+            if (accountId == bobId || accountId == alice2Id) {
+                convId = conversationId;
+                conversationReady = true;
+            }
+            cv.notify_one();
+        }));
+    auto requestReceived = false;
+    confHandlers.insert(
+        DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>(
+            [&](const std::string& accountId,
+                const std::string& /*conversationId*/,
+                std::map<std::string, std::string> /*metadatas*/) {
+                if (accountId == bobId)
+                    requestReceived = true;
+                cv.notify_one();
+            }));
+    auto messageDisplayed = false;
+    confHandlers.insert(
+        DRing::exportable_callback<DRing::ConfigurationSignal::AccountMessageStatusChanged>(
+            [&](const std::string& /* accountId */,
+                const std::string& /* conversationId */,
+                const std::string& /* username */,
+                const std::string& /* msgId */,
+                int status) {
+                if (status == 3)
+                    messageDisplayed = true;
+                cv.notify_one();
+            }));
+    DRing::registerSignalHandlers(confHandlers);
+    confHandlers.clear();
+
+    aliceAccount->addContact(bobUri);
+    aliceAccount->sendTrustRequest(bobUri, {});
+    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return requestReceived; }));
+
+    CPPUNIT_ASSERT(bobAccount->acceptTrustRequest(aliceUri));
+    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return conversationReady; }));
+
+    // Start conversation
+    messageReceived = false;
+    DRing::sendMessage(bobId, convId, std::string("Message 1"), "");
+    CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] { return messageReceived; }));
+    messageReceived = false;
+    DRing::sendMessage(bobId, convId, std::string("Message 2"), "");
+    CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] { return messageReceived; }));
+    messageReceived = false;
+    DRing::sendMessage(bobId, convId, std::string("Message 3"), "");
+    CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] { return messageReceived; }));
+
+    messageDisplayed = false;
+    DRing::setMessageDisplayed(aliceId, "swarm:" + convId, msgId, 3);
+    CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] { return messageDisplayed; }));
+
+    // Now create alice2
+    conversationReady = false;
+    auto aliceArchive = std::filesystem::current_path().string() + "/alice.gz";
+    aliceAccount->exportArchive(aliceArchive);
+    std::map<std::string, std::string> details = DRing::getAccountTemplate("RING");
+    details[ConfProperties::TYPE] = "RING";
+    details[ConfProperties::DISPLAYNAME] = "ALICE2";
+    details[ConfProperties::ALIAS] = "ALICE2";
+    details[ConfProperties::UPNP_ENABLED] = "true";
+    details[ConfProperties::ARCHIVE_PASSWORD] = "";
+    details[ConfProperties::ARCHIVE_PIN] = "";
+    details[ConfProperties::ARCHIVE_PATH] = aliceArchive;
+    alice2Id = Manager::instance().addAccount(details);
+
+    // Check if conversation is ready
+    CPPUNIT_ASSERT(cv.wait_for(lk, 60s, [&]() { return conversationReady; }));
+    // Check that last displayed is synched
+    auto membersInfos = DRing::getConversationMembers(alice2Id, convId);
+    CPPUNIT_ASSERT(std::find_if(membersInfos.begin(),
+                                membersInfos.end(),
+                                [&](auto infos) {
+                                    return infos["uri"] == aliceUri
+                                           && infos["lastDisplayed"] == msgId;
+                                })
+                   != membersInfos.end());
+}
+
+void
+SyncHistoryTest::testLastInteractionAfterSomeMessages()
+{
+    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
+    auto aliceUri = aliceAccount->getUsername();
+    auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId);
+    auto bobUri = bobAccount->getUsername();
+    std::string convId;
+
+    std::mutex mtx;
+    std::unique_lock<std::mutex> lk {mtx};
+    std::condition_variable cv;
+    std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers;
+    auto messageReceived = false;
+    std::string msgId = "";
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>(
+        [&](const std::string& /* accountId */,
+            const std::string& /* conversationId */,
+            std::map<std::string, std::string> message) {
+            messageReceived = true;
+            msgId = message["id"];
+            cv.notify_one();
+        }));
+    auto conversationReady = false, conversationAlice2Ready = false;
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>(
+        [&](const std::string& accountId, const std::string& conversationId) {
+            if (accountId == bobId) {
+                convId = conversationId;
+                conversationReady = true;
+            } else if (accountId == alice2Id) {
+                conversationAlice2Ready = true;
+            }
+            cv.notify_one();
+        }));
+    auto requestReceived = false;
+    confHandlers.insert(
+        DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>(
+            [&](const std::string& accountId,
+                const std::string& /*conversationId*/,
+                std::map<std::string, std::string> /*metadatas*/) {
+                if (accountId == bobId)
+                    requestReceived = true;
+                cv.notify_one();
+            }));
+    auto messageDisplayed = false;
+    confHandlers.insert(
+        DRing::exportable_callback<DRing::ConfigurationSignal::AccountMessageStatusChanged>(
+            [&](const std::string& /* accountId */,
+                const std::string& /* conversationId */,
+                const std::string& /* username */,
+                const std::string& /* msgId */,
+                int status) {
+                if (status == 3)
+                    messageDisplayed = true;
+                cv.notify_one();
+            }));
+    auto alice2Ready = false, alice2Stopped = false;
+    confHandlers.insert(
+        DRing::exportable_callback<DRing::ConfigurationSignal::VolatileDetailsChanged>(
+            [&](const std::string& accountId, const std::map<std::string, std::string>& details) {
+                if (alice2Id != accountId) {
+                    return;
+                }
+                alice2Ready = details.at(DRing::Account::VolatileProperties::DEVICE_ANNOUNCED)
+                              == "true";
+                auto daemonStatus = details.at(DRing::Account::ConfProperties::Registration::STATUS);
+                if (daemonStatus == "UNREGISTERED")
+                    alice2Stopped = true;
+                cv.notify_one();
+            }));
+    DRing::registerSignalHandlers(confHandlers);
+    confHandlers.clear();
+
+    // Creates alice2
+    auto aliceArchive = std::filesystem::current_path().string() + "/alice.gz";
+    aliceAccount->exportArchive(aliceArchive);
+    std::map<std::string, std::string> details = DRing::getAccountTemplate("RING");
+    details[ConfProperties::TYPE] = "RING";
+    details[ConfProperties::DISPLAYNAME] = "ALICE2";
+    details[ConfProperties::ALIAS] = "ALICE2";
+    details[ConfProperties::UPNP_ENABLED] = "true";
+    details[ConfProperties::ARCHIVE_PASSWORD] = "";
+    details[ConfProperties::ARCHIVE_PIN] = "";
+    details[ConfProperties::ARCHIVE_PATH] = aliceArchive;
+    alice2Id = Manager::instance().addAccount(details);
+
+    aliceAccount->addContact(bobUri);
+    aliceAccount->sendTrustRequest(bobUri, {});
+    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return requestReceived; }));
+
+    CPPUNIT_ASSERT(bobAccount->acceptTrustRequest(aliceUri));
+    CPPUNIT_ASSERT(
+        cv.wait_for(lk, 30s, [&]() { return conversationReady && conversationAlice2Ready; }));
+
+    // Disable alice 2
+    Manager::instance().sendRegister(aliceId, false);
+    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return alice2Stopped; }));
+
+    // Start conversation
+    messageReceived = false;
+    DRing::sendMessage(bobId, convId, std::string("Message 1"), "");
+    CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] { return messageReceived; }));
+    messageReceived = false;
+    DRing::sendMessage(bobId, convId, std::string("Message 2"), "");
+    CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] { return messageReceived; }));
+    messageReceived = false;
+    DRing::sendMessage(bobId, convId, std::string("Message 3"), "");
+    CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] { return messageReceived; }));
+
+    messageDisplayed = false;
+    auto displayedId = msgId;
+    DRing::setMessageDisplayed(aliceId, "swarm:" + convId, displayedId, 3);
+    CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] { return messageDisplayed; }));
+
+    // Now restart alice2
+    messageDisplayed = false;
+    Manager::instance().sendRegister(aliceId, true);
+    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return messageDisplayed; }));
+    auto membersInfos = DRing::getConversationMembers(alice2Id, convId);
+    CPPUNIT_ASSERT(std::find_if(membersInfos.begin(),
+                                membersInfos.end(),
+                                [&](auto infos) {
+                                    return infos["uri"] == aliceUri
+                                           && infos["lastDisplayed"] == displayedId;
+                                })
+                   != membersInfos.end());
+}
+
 } // namespace test
 } // namespace jami
 
-- 
GitLab