diff --git a/bin/dbus/cx.ring.Ring.ConfigurationManager.xml b/bin/dbus/cx.ring.Ring.ConfigurationManager.xml index 2425d163c631a0f08891eca067fb4d7aaab59a1f..145ab265828cb4f2f395625bfee0f296983f5965 100644 --- a/bin/dbus/cx.ring.Ring.ConfigurationManager.xml +++ b/bin/dbus/cx.ring.Ring.ConfigurationManager.xml @@ -1787,6 +1787,18 @@ <arg type="u" name="id" direction="out"/> </method> + <method name="countInteractions" tp:name-for-bindings="countInteractions"> + <tp:added version="10.0.0"/> + <tp:docstring> + Get how many messages there is since an interaction ("" for initial commit) + </tp:docstring> + <arg type="s" name="accountId" direction="in"/> + <arg type="s" name="conversationId" direction="in"/> + <arg type="s" name="toId" direction="in"/> + <arg type="s" name="fromId" direction="in"/> + <arg type="u" name="count" direction="out"/> + </method> + <signal name="mediaParametersChanged" tp:name-for-bindings="mediaParametersChanged"> <tp:added version="2.3.0"/> <tp:docstring> diff --git a/bin/dbus/dbusconfigurationmanager.cpp b/bin/dbus/dbusconfigurationmanager.cpp index 6ff5f7315ccfc5c308337d435be70db4c89a4efe..9991a88f449026f162dcbeece77af57861123362 100644 --- a/bin/dbus/dbusconfigurationmanager.cpp +++ b/bin/dbus/dbusconfigurationmanager.cpp @@ -932,6 +932,15 @@ DBusConfigurationManager::loadConversationMessages(const std::string& accountId, return DRing::loadConversationMessages(accountId, conversationId, fromMessage, n); } +uint32_t +DBusConfigurationManager::countInteractions(const std::string& accountId, + const std::string& conversationId, + const std::string& toId, + const std::string& fromId) +{ + return DRing::countInteractions(accountId, conversationId, toId, fromId); +} + bool DBusConfigurationManager::isAudioMeterActive(const std::string& id) { diff --git a/bin/dbus/dbusconfigurationmanager.h b/bin/dbus/dbusconfigurationmanager.h index 04a5a63ffcf29347635c4c9c2e06a761c6b645e4..ab425a6a859ee9f3291f94353eb15f37ac4c8089 100644 --- a/bin/dbus/dbusconfigurationmanager.h +++ b/bin/dbus/dbusconfigurationmanager.h @@ -283,6 +283,10 @@ public: const std::string& conversationId, const std::string& fromMessage, const uint32_t& n); + uint32_t countInteractions(const std::string& accountId, + const std::string& conversationId, + const std::string& toId, + const std::string& fromId); }; #endif // __RING_DBUSCONFIGURATIONMANAGER_H__ diff --git a/bin/jni/conversation.i b/bin/jni/conversation.i index 042be6048f2af87574d25ad9fb7ec8e49ae8860f..7858045b8481522a7f354bcea27b2cfc66e11544 100644 --- a/bin/jni/conversation.i +++ b/bin/jni/conversation.i @@ -57,7 +57,7 @@ namespace DRing { // Message send/load void sendMessage(const std::string& accountId, const std::string& conversationId, const std::string& message, const std::string& parent); uint32_t loadConversationMessages(const std::string& accountId, const std::string& conversationId, const std::string& fromMessage, size_t n); - + uint32_t countInteractions(const std::string& accountId, const std::string& conversationId, const std::string& toId, const std::string& fromId); } class ConversationCallback { diff --git a/bin/nodejs/conversation.i b/bin/nodejs/conversation.i index 042be6048f2af87574d25ad9fb7ec8e49ae8860f..45605ae27b36cd09ada9a5e0dd95afd7d5cbdc9d 100644 --- a/bin/nodejs/conversation.i +++ b/bin/nodejs/conversation.i @@ -57,6 +57,7 @@ namespace DRing { // Message send/load void sendMessage(const std::string& accountId, const std::string& conversationId, const std::string& message, const std::string& parent); uint32_t loadConversationMessages(const std::string& accountId, const std::string& conversationId, const std::string& fromMessage, size_t n); + uint32_t countInteractions(const std::string& accountId, const std::string& conversationId, const std::string& toId, const std::string& fromId); } diff --git a/src/account.h b/src/account.h index c0b6c28f8310dce17c86e6c16615dcbbb418ebe9..48a7ca128bc6ff7f4dd8d55624969c8c6f9fb1e0 100644 --- a/src/account.h +++ b/src/account.h @@ -319,6 +319,10 @@ public: const std::string& /*conversationId*/, const std::string& /*commitId*/) {}; + virtual void onMessageDisplayed(const std::string& /*peer*/, + const std::string& /*conversationId*/, + const std::string& /*interactionId*/) {}; + // Invites virtual void onConversationRequest(const std::string& /*from*/, const Json::Value&) {}; virtual void onNeedConversationRequest(const std::string& /*from*/, diff --git a/src/client/conversation_interface.cpp b/src/client/conversation_interface.cpp index fa36d42b7baf708de419cfa716115d4e88d7c2c2..0278a8950af0dd9b904a4347bd84de55f49f2d4a 100644 --- a/src/client/conversation_interface.cpp +++ b/src/client/conversation_interface.cpp @@ -146,4 +146,15 @@ loadConversationMessages(const std::string& accountId, return 0; } +uint32_t +countInteractions(const std::string& accountId, + const std::string& conversationId, + const std::string& toId, + const std::string& fromId) +{ + if (auto acc = jami::Manager::instance().getAccount<jami::JamiAccount>(accountId)) + return acc->countInteractions(conversationId, toId, fromId); + return 0; +} + } // namespace DRing diff --git a/src/jami/conversation_interface.h b/src/jami/conversation_interface.h index 4f577e7c9294b43b11a8ded8293622cc299aa6f1..ada5792746bd5ea59dcb4ac1e88eda94d1f9155c 100644 --- a/src/jami/conversation_interface.h +++ b/src/jami/conversation_interface.h @@ -69,6 +69,10 @@ DRING_PUBLIC uint32_t loadConversationMessages(const std::string& accountId, const std::string& conversationId, const std::string& fromMessage, size_t n); +DRING_PUBLIC uint32_t countInteractions(const std::string& accountId, + const std::string& conversationId, + const std::string& toId, + const std::string& fromId); struct DRING_PUBLIC ConversationSignal { diff --git a/src/jamidht/conversation.cpp b/src/jamidht/conversation.cpp index be50f59ee8a80b75652ed7cccf15973a602ef64d..3654904236ba323555c12da7a5ceb41e96a6fe9c 100644 --- a/src/jamidht/conversation.cpp +++ b/src/jamidht/conversation.cpp @@ -158,8 +158,10 @@ public: conversationDataPath_ = fileutils::get_data_dir() + DIR_SEPARATOR_STR + shared->getAccountID() + DIR_SEPARATOR_STR + "conversation_data" + DIR_SEPARATOR_STR + repository_->id(); - fetchedPath_ = fileutils::get_data_dir() + DIR_SEPARATOR_STR + "fetched"; + fetchedPath_ = conversationDataPath_ + DIR_SEPARATOR_STR + "fetched"; + lastDisplayedPath_ = conversationDataPath_ + DIR_SEPARATOR_STR + "lastDisplayed"; loadFetched(); + loadLastDisplayed(); } } @@ -270,6 +272,26 @@ public: msgpack::pack(file, fetchedDevices_); } + void loadLastDisplayed() + { + try { + // read file + auto file = fileutils::loadFile(lastDisplayedPath_); + // load values + msgpack::object_handle oh = msgpack::unpack((const char*) file.data(), file.size()); + std::lock_guard<std::mutex> lk {lastDisplayedMtx_}; + oh.get().convert(lastDisplayed_); + } catch (const std::exception& e) { + return; + } + } + + void saveLastDisplayed() + { + std::ofstream file(lastDisplayedPath_, std::ios::trunc | std::ios::binary); + msgpack::pack(file, lastDisplayed_); + } + std::unique_ptr<ConversationRepository> repository_; std::weak_ptr<JamiAccount> account_; std::atomic_bool isRemoving_ {false}; @@ -289,6 +311,9 @@ public: std::string fetchedPath_ {}; std::mutex fetchedDevicesMtx_ {}; std::set<std::string> fetchedDevices_ {}; + std::string lastDisplayedPath_ {}; + std::mutex lastDisplayedMtx_ {}; + std::map<std::string, std::string> lastDisplayed_ {}; }; bool @@ -554,12 +579,20 @@ Conversation::getMembers(bool includeInvited) const if (!shared) return result; auto members = pimpl_->repository_->members(); + std::lock_guard<std::mutex> lk(pimpl_->lastDisplayedMtx_); for (const auto& member : members) { if (member.role == MemberRole::BANNED) continue; if (member.role == MemberRole::INVITED && !includeInvited) continue; - result.emplace_back(member.map()); + auto mm = member.map(); + std::string lastDisplayed; + auto itDisplayed = pimpl_->lastDisplayed_.find(member.uri); + if (itDisplayed != pimpl_->lastDisplayed_.end()) { + lastDisplayed = itDisplayed->second; + } + mm["lastDisplayed"] = std::move(lastDisplayed); + result.emplace_back(std::move(mm)); } return result; } @@ -1070,4 +1103,19 @@ Conversation::hasFetched(const std::string& deviceId) pimpl_->saveFetched(); } +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(); +} + +uint32_t +Conversation::countInteractions(const std::string& toId, const std::string& fromId) const +{ + // Log but without content to avoid costly convertions. + return pimpl_->repository_->log(fromId, toId, false, true).size(); +} + } // namespace jami \ No newline at end of file diff --git a/src/jamidht/conversation.h b/src/jamidht/conversation.h index d52495f60452f51861a821debb5cddbbc131ff80..4deacf7f5cb7fccb69f4c4426cd6f98bf5fa6e6f 100644 --- a/src/jamidht/conversation.h +++ b/src/jamidht/conversation.h @@ -307,6 +307,21 @@ public: */ void hasFetched(const std::string& deviceId); + /** + * Store last read commit (returned in getMembers) + * @param uri Of the member + * @param interactionId Last interaction displayed + */ + void setMessageDisplayed(const std::string& uri, const std::string& interactionId); + + /** + * Retrieve how many interactions there is from HEAD to interactionId + * @param toId "" for getting the whole history + * @param fromId "" => HEAD + * @return number of interactions since interactionId + */ + uint32_t countInteractions(const std::string& toId, const std::string& fromId = "") const; + private: std::shared_ptr<Conversation> shared() { diff --git a/src/jamidht/conversationrepository.cpp b/src/jamidht/conversationrepository.cpp index 585028fb309625a70ed5537dfabd91f2358e9bb6..88f8015141872b895278bb26a5c0d4cf7a087f8c 100644 --- a/src/jamidht/conversationrepository.cpp +++ b/src/jamidht/conversationrepository.cpp @@ -114,7 +114,8 @@ public: std::vector<ConversationCommit> log(const std::string& from, const std::string& to, unsigned n, - bool logIfNotFound = false) const; + bool logIfNotFound = false, + bool fastLog = false) const; std::optional<std::string> linearizedParent(const std::string& commitId) const; GitObject fileAtTree(const std::string& path, const GitTree& tree) const; @@ -1486,7 +1487,8 @@ std::vector<ConversationCommit> ConversationRepository::Impl::log(const std::string& from, const std::string& to, unsigned n, - bool logIfNotFound) const + bool logIfNotFound, + bool fastLog) const { std::vector<ConversationCommit> commits {}; @@ -1533,6 +1535,12 @@ ConversationRepository::Impl::log(const std::string& from, break; } + if (fastLog) { + // Used to only count commit + commits.emplace(commits.end(), ConversationCommit {}); + continue; + } + const git_signature* sig = git_commit_author(commit.get()); GitAuthor author; author.name = sig->name; @@ -2372,9 +2380,12 @@ ConversationRepository::logN(const std::string& last, unsigned n, bool logIfNotF } std::vector<ConversationCommit> -ConversationRepository::log(const std::string& from, const std::string& to, bool logIfNotFound) const +ConversationRepository::log(const std::string& from, + const std::string& to, + bool logIfNotFound, + bool fastLog) const { - return pimpl_->log(from, to, 0, logIfNotFound); + return pimpl_->log(from, to, 0, logIfNotFound, fastLog); } std::optional<ConversationCommit> diff --git a/src/jamidht/conversationrepository.h b/src/jamidht/conversationrepository.h index 692b9526e0402b68b546ed3f89ce297cda5e922f..63e747d957e22e0b615c3829b47247454b62ccb9 100644 --- a/src/jamidht/conversationrepository.h +++ b/src/jamidht/conversationrepository.h @@ -185,6 +185,7 @@ public: * @param last last commit (default empty) * @param n Max commits number to get (default: 0) * @param logIfNotFound Log if commit is found (false for detect commit before pulling) + * @param fastLog Used to count commits. This will just generate empty ConversationCommit * @return a list of commits */ std::vector<ConversationCommit> logN(const std::string& last = "", @@ -192,7 +193,8 @@ public: bool logIfNotFound = true) const; std::vector<ConversationCommit> log(const std::string& from = "", const std::string& to = "", - bool logIfNotFound = true) const; + bool logIfNotFound = true, + bool fastLog = false) const; std::optional<ConversationCommit> getCommit(const std::string& commitId, bool logIfNotFound = true) const; diff --git a/src/jamidht/jamiaccount.cpp b/src/jamidht/jamiaccount.cpp index 25eb0ae4f9a8ece3e2f3c589550a8b2959ddeb65..53176ef2ab87b04f46e5093cfc0b9cb58dbc2f46 100644 --- a/src/jamidht/jamiaccount.cpp +++ b/src/jamidht/jamiaccount.cpp @@ -4176,6 +4176,19 @@ JamiAccount::loadConversationMessages(const std::string& conversationId, return id; } +uint32_t +JamiAccount::countInteractions(const std::string& convId, + const std::string& toId, + const std::string& fromId) const +{ + std::lock_guard<std::mutex> lk(conversationsMtx_); + auto conversation = conversations_.find(convId); + if (conversation != conversations_.end() && conversation->second) { + return conversation->second->countInteractions(toId, fromId); + } + return 0; +} + void JamiAccount::onNewGitCommit(const std::string& peer, const std::string& deviceId, @@ -4196,6 +4209,18 @@ JamiAccount::onNewGitCommit(const std::string& peer, fetchNewCommits(peer, deviceId, conversationId, commitId); } +void +JamiAccount::onMessageDisplayed(const std::string& peer, + const std::string& conversationId, + const std::string& interactionId) +{ + std::unique_lock<std::mutex> lk(conversationsMtx_); + auto conversation = conversations_.find(conversationId); + if (conversation != conversations_.end() && conversation->second) { + conversation->second->setMessageDisplayed(peer, interactionId); + } +} + void JamiAccount::fetchNewCommits(const std::string& peer, const std::string& deviceId, diff --git a/src/jamidht/jamiaccount.h b/src/jamidht/jamiaccount.h index 88a528769e9a01daa28471697f391d0d53e6e6aa..735b9b4a3ac5fe6870870c08cbe8006f2130346d 100644 --- a/src/jamidht/jamiaccount.h +++ b/src/jamidht/jamiaccount.h @@ -554,11 +554,25 @@ public: const std::string& fromMessage = "", size_t n = 0); + /** + * Retrieve how many interactions there is from HEAD to interactionId + * @param convId + * @param interactionId "" for getting the whole history + * @return number of interactions since interactionId + */ + uint32_t countInteractions(const std::string& convId, + const std::string& toId, + const std::string& fromId) const; + // Received a new commit notification void onNewGitCommit(const std::string& peer, const std::string& deviceId, const std::string& conversationId, const std::string& commitId) override; + // Received that a peer displayed a message + void onMessageDisplayed(const std::string& peer, + const std::string& conversationId, + const std::string& interactionId) override; /** * Pull remote device (do not do it if commitId is already in the current repo) * @param peer Contact URI diff --git a/src/sip/sipaccountbase.cpp b/src/sip/sipaccountbase.cpp index 38bf1ef38fc852321819adbc37afa0ab06ebc995..c563cbb38dea68790520801037d4ff97c8e9e006 100644 --- a/src/sip/sipaccountbase.cpp +++ b/src/sip/sipaccountbase.cpp @@ -613,6 +613,7 @@ SIPAccountBase::onTextMessage(const std::string& id, if (conversationId.empty()) // Old method messageEngine_.onMessageDisplayed(from, from_hex_string(messageId), isDisplayed); else if (isDisplayed) { + onMessageDisplayed(from, conversationId, messageId); JAMI_DBG() << "[message " << messageId << "] Displayed by peer"; emitSignal<DRing::ConfigurationSignal::AccountMessageStatusChanged>( accountID_, @@ -704,6 +705,8 @@ SIPAccountBase::setMessageDisplayed(const std::string& conversationUri, std::string conversationId = {}; if (uri.scheme() == Uri::Scheme::SWARM) conversationId = uri.authority(); + if (!conversationId.empty()) + onMessageDisplayed(getUsername(), conversationId, messageId); if (status == (int) DRing::Account::MessageStates::DISPLAYED) sendInstantMessage(uri.authority(), {{MIME_TYPE_IMDN, getDisplayed(conversationId, messageId)}}); diff --git a/test/unitTest/conversation/conversation.cpp b/test/unitTest/conversation/conversation.cpp index 988963c543be22a1c50f269298f758ae1f10c105..4ca231c915e54cdf4632b3066af65b006f97131f 100644 --- a/test/unitTest/conversation/conversation.cpp +++ b/test/unitTest/conversation/conversation.cpp @@ -155,6 +155,7 @@ private: void testDoNotLoadIncorrectConversation(); void testSyncingWhileAccepting(); void testGetConversationsMembersWhileSyncing(); + void testCountInteractions(); CPPUNIT_TEST_SUITE(ConversationTest); CPPUNIT_TEST(testCreateConversation); @@ -212,6 +213,7 @@ private: CPPUNIT_TEST(testDoNotLoadIncorrectConversation); CPPUNIT_TEST(testSyncingWhileAccepting); CPPUNIT_TEST(testGetConversationsMembersWhileSyncing); + CPPUNIT_TEST(testCountInteractions); CPPUNIT_TEST_SUITE_END(); }; @@ -1250,8 +1252,43 @@ ConversationTest::testSetMessageDisplayed() CPPUNIT_ASSERT( cv.wait_for(lk, std::chrono::seconds(30), [&]() { return memberMessageGenerated; })); + // Last displayed messages should not be set yet + auto membersInfos = bobAccount->getConversationMembers(convId); + CPPUNIT_ASSERT(std::find_if(membersInfos.begin(), + membersInfos.end(), + [&](auto infos) { + return infos["uri"] == aliceUri && infos["lastDisplayed"] == ""; + }) + != membersInfos.end()); + membersInfos = aliceAccount->getConversationMembers(convId); + CPPUNIT_ASSERT(std::find_if(membersInfos.begin(), + membersInfos.end(), + [&](auto infos) { + return infos["uri"] == aliceUri && infos["lastDisplayed"] == ""; + }) + != membersInfos.end()); + aliceAccount->setMessageDisplayed("swarm:" + convId, convId, 3); CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return msgDisplayed; })); + + // Now, the last displayed message should be updated in member's infos (both sides) + membersInfos = bobAccount->getConversationMembers(convId); + CPPUNIT_ASSERT(std::find_if(membersInfos.begin(), + membersInfos.end(), + [&](auto infos) { + return infos["uri"] == aliceUri + && infos["lastDisplayed"] == convId; + }) + != membersInfos.end()); + membersInfos = aliceAccount->getConversationMembers(convId); + CPPUNIT_ASSERT(std::find_if(membersInfos.begin(), + membersInfos.end(), + [&](auto infos) { + return infos["uri"] == aliceUri + && infos["lastDisplayed"] == convId; + }) + != membersInfos.end()); + DRing::unregisterSignalHandlers(); } @@ -4453,6 +4490,37 @@ ConversationTest::testGetConversationsMembersWhileSyncing() != members.end()); } +void +ConversationTest::testCountInteractions() +{ + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto convId = aliceAccount->startConversation(); + std::mutex mtx; + std::unique_lock<std::mutex> lk {mtx}; + std::condition_variable cv; + + std::string msgId1 = "", msgId2 = "", msgId3 = ""; + aliceAccount->sendMessage(convId, "1"s, "", "text/plain", true, [&](bool, std::string commitId) { + msgId1 = commitId; + cv.notify_one(); + }); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { return !msgId1.empty(); })); + aliceAccount->sendMessage(convId, "2"s, "", "text/plain", true, [&](bool, std::string commitId) { + msgId2 = commitId; + cv.notify_one(); + }); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { return !msgId2.empty(); })); + aliceAccount->sendMessage(convId, "3"s, "", "text/plain", true, [&](bool, std::string commitId) { + msgId3 = commitId; + cv.notify_one(); + }); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { return !msgId3.empty(); })); + + CPPUNIT_ASSERT(DRing::countInteractions(aliceId, convId, "", "") == 4 /* 3 + initial */); + CPPUNIT_ASSERT(DRing::countInteractions(aliceId, convId, msgId3, "") == 0); + CPPUNIT_ASSERT(DRing::countInteractions(aliceId, convId, msgId2, "") == 1); +} + } // namespace test } // namespace jami