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