diff --git a/src/jamidht/conversation.cpp b/src/jamidht/conversation.cpp index c5984fa47f842af7257827cd50a2fab24bfda7a1..9e5d01245d331a7679271e563095a3ceecb983a7 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 41adc79dd3f3adff4a42e80d6781bfeb7b05da27..88a3b24f3b70b2820ac147e5a03b2d7a8ae8a209 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 fadcb26021a0ee2395f74edbc8d6d703b294de2b..244dbc1e317702e4299ff4f0f7337ceb31067cf8 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 4ea7e78c1ae7148aee5715163e3afc24b79ca2ba..4d40559faf8f0924648f7205b21bb7a6be7173c4 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