From 32b76c8da4a0e9d269b841d54bb51d948bb22a9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Blin?= <sebastien.blin@savoirfairelinux.com> Date: Thu, 6 Jul 2023 11:31:20 -0400 Subject: [PATCH] messagelist: use history given from daemon (except SIP accounts) With Jami-Daemon >= 14.0.0, the client doesn't need to construct itself the history. This part is now handled by the daemon. This patch uses the new API: + loadConversationMessages->loadConversation + SwarmMessageReceived/SwarmMessageUpdated/ReactionAdded/ReactionRemoved + remove MessageReceived + ConversationLoaded->SwarmLoaded + No need to use loadConversationUntil, the daemon will load whatever the client needs. + No need to clear cache, just reset the body and emit data changes Everything should work like before (even re-translation & changing preview preference) Change-Id: Iaf1fa3e84e8e157ae2d0bec210977f9a34415ebc --- daemon | 2 +- src/app/appsettingsmanager.cpp | 7 + src/app/appsettingsmanager.h | 2 + .../commoncomponents/EmojiReactionPopup.qml | 8 +- src/app/commoncomponents/EmojiReactions.qml | 4 +- src/app/commoncomponents/ReplyToRow.qml | 10 - src/app/commoncomponents/ShowMoreMenu.qml | 9 +- src/app/conversationlistmodelbase.cpp | 23 +- src/app/lrcinstance.h | 2 +- src/app/mainview/ConversationView.qml | 14 - src/app/mainview/components/ChatView.qml | 3 +- .../mainview/components/MessageListView.qml | 23 +- .../components/SmartListItemDelegate.qml | 7 - src/app/messagesadapter.cpp | 54 +- src/app/messagesadapter.h | 2 - src/app/utilsadapter.cpp | 13 +- src/app/utilsadapter.h | 1 - src/libclient/accountmodel.cpp | 8 + src/libclient/api/accountmodel.h | 1 + src/libclient/api/conversation.h | 18 +- src/libclient/api/conversationmodel.h | 13 +- src/libclient/api/interaction.h | 110 +++- src/libclient/authority/storagehelper.cpp | 108 +--- src/libclient/authority/storagehelper.h | 25 - src/libclient/callbackshandler.cpp | 60 +- src/libclient/callbackshandler.h | 44 +- src/libclient/callmodel.cpp | 2 +- src/libclient/conversationmodel.cpp | 571 +++++++----------- src/libclient/dbus/metatypes.h | 35 ++ src/libclient/messagelistmodel.cpp | 296 +++------ src/libclient/messagelistmodel.h | 23 +- .../qtwrapper/configurationmanager_wrap.h | 120 +++- src/libclient/typedefs.h | 11 + tests/qml/src/tst_MessageOptions.qml | 4 +- 34 files changed, 752 insertions(+), 881 deletions(-) diff --git a/daemon b/daemon index 317b7317d..8468f1592 160000 --- a/daemon +++ b/daemon @@ -1 +1 @@ -Subproject commit 317b7317dcda4afb733ddb9bd5b450d4635941ae +Subproject commit 8468f15927ec7c83a5fca671bac1a4112883b8c9 diff --git a/src/app/appsettingsmanager.cpp b/src/app/appsettingsmanager.cpp index be47216d8..9ad672c55 100644 --- a/src/app/appsettingsmanager.cpp +++ b/src/app/appsettingsmanager.cpp @@ -137,4 +137,11 @@ AppSettingsManager::loadTranslations() } Q_EMIT retranslate(); + loadHistory(); +} + +void +AppSettingsManager::loadHistory() +{ + Q_EMIT reloadHistory(); } diff --git a/src/app/appsettingsmanager.h b/src/app/appsettingsmanager.h index a12709eac..eaada016d 100644 --- a/src/app/appsettingsmanager.h +++ b/src/app/appsettingsmanager.h @@ -140,9 +140,11 @@ public: QString getLanguage(); void loadTranslations(); + void loadHistory(); Q_SIGNALS: void retranslate(); + void reloadHistory(); private: QSettings* settings_; diff --git a/src/app/commoncomponents/EmojiReactionPopup.qml b/src/app/commoncomponents/EmojiReactionPopup.qml index 2358870a2..c29084686 100644 --- a/src/app/commoncomponents/EmojiReactionPopup.qml +++ b/src/app/commoncomponents/EmojiReactionPopup.qml @@ -131,7 +131,7 @@ Popup { Repeater { model: emojiArray.length < 15 ? emojiArray.length : 15 delegate: Text { - text: emojiArray[index] + text: emojiArray[index].body horizontalAlignment: Text.AlignRight font.pointSize: JamiTheme.emojiPopupFontsize } @@ -147,7 +147,7 @@ Popup { delegate: Button { id: emojiButton - text: emojiArray[index] + text: emojiArray[index].body font.pointSize: JamiTheme.emojiPopupFontsize background.visible: false padding: 0 @@ -155,13 +155,13 @@ Popup { Text { visible: emojiButton.hovered anchors.centerIn: parent - text: emojiArray[index] + text: emojiArray[index].body font.pointSize: JamiTheme.emojiPopupFontsizeBig z: 1 } onClicked: { - MessagesAdapter.removeEmojiReaction(CurrentConversation.id, emojiButton.text, msgId); + MessagesAdapter.removeEmojiReaction(CurrentConversation.id, emojiButton.text, emojiArray[index].commitId); if (emojiArray.length === 1) close(); } diff --git a/src/app/commoncomponents/EmojiReactions.qml b/src/app/commoncomponents/EmojiReactions.qml index 64b61c676..36d87c51b 100644 --- a/src/app/commoncomponents/EmojiReactions.qml +++ b/src/app/commoncomponents/EmojiReactions.qml @@ -43,7 +43,7 @@ Item { for (const reaction of Object.entries(reactions)) { var authorEmojiList = reaction[1]; for (var emojiIndex in authorEmojiList) { - var emoji = authorEmojiList[emojiIndex]; + var emoji = authorEmojiList[emojiIndex].body; if (emojiList.includes(emoji)) { var findIndex = emojiList.indexOf(emoji); if (findIndex != -1) @@ -75,7 +75,7 @@ Item { var authorEmojiList = reaction[1]; if (CurrentAccount.uri === authorUri) { for (var emojiIndex in authorEmojiList) { - list[index] = authorEmojiList[emojiIndex]; + list[index] = authorEmojiList[emojiIndex].body; index++; } return list; diff --git a/src/app/commoncomponents/ReplyToRow.qml b/src/app/commoncomponents/ReplyToRow.qml index 0fcff7cfd..91527e610 100644 --- a/src/app/commoncomponents/ReplyToRow.qml +++ b/src/app/commoncomponents/ReplyToRow.qml @@ -31,16 +31,6 @@ Item { property int requestId: -1 property var replyTransferName: MessagesAdapter.dataForInteraction(ReplyTo, MessageList.TransferName) - Component.onCompleted: { - // Make sure we show the original post - // In the future, we may just want to load the previous interaction of the thread - // and not show it, but for now we can simplify. - if (ReplyTo !== "") { - // Store the request Id for later filtering. - requestId = MessagesAdapter.loadConversationUntil(ReplyTo); - } - } - Connections { target: MessagesAdapter diff --git a/src/app/commoncomponents/ShowMoreMenu.qml b/src/app/commoncomponents/ShowMoreMenu.qml index c3c2ec0d6..2f77d1857 100644 --- a/src/app/commoncomponents/ShowMoreMenu.qml +++ b/src/app/commoncomponents/ShowMoreMenu.qml @@ -110,19 +110,20 @@ BaseContextMenu { onClosed: if (emojiPicker) emojiPicker.closeEmojiPicker() - function getModel() { + function getQuickEmojiListModel() { const defaultModel = ["ðŸ‘", "👎", "😂"]; const reactedEmojis = Array.isArray(emojiReplied) ? emojiReplied.slice(0, defaultModel.length) : []; const uniqueEmojis = Array.from(new Set(reactedEmojis)); const missingEmojis = defaultModel.filter(emoji => !uniqueEmojis.includes(emoji)); - return uniqueEmojis.concat(missingEmojis); + const result = uniqueEmojis.concat(missingEmojis); + return result; } property list<MenuItem> menuItems: [ GeneralMenuItemList { - id: audioMessage + id: emojiQuickReactions - modelList: getModel() + modelList: getQuickEmojiListModel() canTrigger: true iconSource: JamiResources.add_reaction_svg itemName: JamiStrings.copy diff --git a/src/app/conversationlistmodelbase.cpp b/src/app/conversationlistmodelbase.cpp index 3fc5ce470..250e72464 100644 --- a/src/app/conversationlistmodelbase.cpp +++ b/src/app/conversationlistmodelbase.cpp @@ -109,19 +109,32 @@ ConversationListModelBase::dataForItem(item_t item, int role) const return QVariant(item.unreadMessages); case Role::LastInteractionTimeStamp: { if (!item.interactions->empty()) { - auto ts = static_cast<qint32>(item.interactions->at(item.lastMessageUid).timestamp); + auto ts = static_cast<qint32>(item.interactions->rbegin()->second.timestamp); return QVariant(ts); } break; } case Role::LastInteraction: { if (!item.interactions->empty()) { - auto interaction = item.interactions->at(item.lastMessageUid); - auto body_ = interaction.body; + auto interaction = item.interactions->rbegin()->second; + auto& accInfo = lrcInstance_->getCurrentAccountInfo(); if (interaction.type == interaction::Type::DATA_TRANSFER) { - body_ = interaction.commit.value("displayName"); + return QVariant(interaction.commit.value("displayName")); + } else if (interaction.type == lrc::api::interaction::Type::CALL) { + return QVariant(interaction::getCallInteractionString(interaction.authorUri + == accInfo.profileInfo.uri, + 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"]))); } - return QVariant(body_); + return QVariant(interaction.body); } break; } diff --git a/src/app/lrcinstance.h b/src/app/lrcinstance.h index 502165c3f..7656f970c 100644 --- a/src/app/lrcinstance.h +++ b/src/app/lrcinstance.h @@ -158,7 +158,7 @@ private: MapStringString contentDrafts_; MapStringString lastConferences_; - conversation::Info invalid {}; + conversation::Info invalid {"", nullptr}; bool debugMode_ {false}; bool muteDaemon_ {true}; diff --git a/src/app/mainview/ConversationView.qml b/src/app/mainview/ConversationView.qml index 3bc72cd0d..2ab9c41b9 100644 --- a/src/app/mainview/ConversationView.qml +++ b/src/app/mainview/ConversationView.qml @@ -35,14 +35,6 @@ ListSelectionView { visible: false onPresented: visible = true - Connections { - target: CurrentConversation - function onReloadInteractions() { - UtilsAdapter.clearInteractionsCache(CurrentAccount.id, CurrentConversation.id); - MessagesAdapter.loadMoreMessages(); - } - } - onDismissed: { callStackView.needToCloseInCallConversationAndPotentialWindow(); LRCInstance.deselectConversation(); @@ -51,12 +43,6 @@ ListSelectionView { property string currentAccountId: CurrentAccount.id onCurrentAccountIdChanged: dismiss() - onVisibleChanged: { - if (visible) - return; - UtilsAdapter.clearInteractionsCache(CurrentAccount.id, CurrentConversation.id); - } - color: JamiTheme.transparentColor leftPaneItem: viewCoordinator.getView("SidePanel") diff --git a/src/app/mainview/components/ChatView.qml b/src/app/mainview/components/ChatView.qml index 96ba2492b..dc582eb0e 100644 --- a/src/app/mainview/components/ChatView.qml +++ b/src/app/mainview/components/ChatView.qml @@ -328,9 +328,8 @@ Rectangle { } onHeightChanged: { - if (loader.item != null) { + if (loader.item) Qt.callLater(loader.item.scrollToBottom); - } } Layout.alignment: Qt.AlignHCenter diff --git a/src/app/mainview/components/MessageListView.qml b/src/app/mainview/components/MessageListView.qml index 6ab323677..3e6660fe8 100644 --- a/src/app/mainview/components/MessageListView.qml +++ b/src/app/mainview/components/MessageListView.qml @@ -36,8 +36,10 @@ JamiListView { } function loadMoreMsgsIfNeeded() { - if (atYBeginning && !CurrentConversation.allMessagesLoaded) + if (atYBeginning && !CurrentConversation.allMessagesLoaded) { + print("load more messages", atYBeginning, CurrentConversation.allMessagesLoaded) MessagesAdapter.loadMoreMessages() + } } function computeTimestampVisibility(item1, item1Index, item2, item2Index) { @@ -252,6 +254,19 @@ JamiListView { onAtYBeginningChanged: loadMoreMsgsIfNeeded() + Timer { + id: chunkLoadDebounceTimer + + interval: 100 + repeat: false + running: false + onTriggered: { + if (root.contentHeight < root.height) { + root.loadMoreMsgsIfNeeded(); + } + } + } + Connections { target: MessagesAdapter @@ -263,9 +278,9 @@ JamiListView { } function onMoreMessagesLoaded(loadingRequestId) { - if (root.contentHeight < root.height || root.atYBeginning) { - root.loadMoreMsgsIfNeeded() - } + // This needs to be throttled, otherwise we will continue to load more messages + // prior to the loaded chunk being rendered and changing the contentHeight. + chunkLoadDebounceTimer.restart(); } function onFileCopied(dest) { diff --git a/src/app/mainview/components/SmartListItemDelegate.qml b/src/app/mainview/components/SmartListItemDelegate.qml index 6d21ceceb..edbf26c5b 100644 --- a/src/app/mainview/components/SmartListItemDelegate.qml +++ b/src/app/mainview/components/SmartListItemDelegate.qml @@ -46,13 +46,6 @@ ItemDelegate { property string lastInteractionFormattedDate: MessagesAdapter.getBestFormattedDate(lastInteractionDate) - Connections { - target: UtilsAdapter - function onChangeLanguage() { - UtilsAdapter.clearInteractionsCache(root.accountId, root.convId) - } - } - property bool showSharePositionIndicator: PositionManager.isPositionSharedToConv(accountId, UID) property bool showSharedPositionIndicator: PositionManager.isConvSharingPosition(accountId, UID) diff --git a/src/app/messagesadapter.cpp b/src/app/messagesadapter.cpp index 4f2760e50..f39f0f127 100644 --- a/src/app/messagesadapter.cpp +++ b/src/app/messagesadapter.cpp @@ -51,13 +51,18 @@ MessagesAdapter::MessagesAdapter(AppSettingsManager* settingsManager, , settingsManager_(settingsManager) , messageParser_(new MessageParser(previewEngine, this)) , filteredMsgListModel_(new FilteredMsgListModel(this)) - , mediaInteractions_(std::make_unique<MessageListModel>()) + , mediaInteractions_(std::make_unique<MessageListModel>(nullptr)) , timestampTimer_(new QTimer(this)) { setObjectName(typeid(*this).name()); set_messageListModel(QVariant::fromValue(filteredMsgListModel_)); + connect(settingsManager_, + &AppSettingsManager::reloadHistory, + &lrcInstance_->accountModel(), + &AccountModel::reloadHistory); + connect(lrcInstance_, &LRCInstance::selectedConvUidChanged, this, [this]() { set_replyToId(""); set_editId(""); @@ -68,7 +73,7 @@ MessagesAdapter::MessagesAdapter(AppSettingsManager* settingsManager, filteredMsgListModel_->setSourceModel(conversation.interactions.get()); set_currentConvComposingList(conversationTypersUrlToName(conversation.typers)); - mediaInteractions_.reset(new MessageListModel(this)); + mediaInteractions_.reset(new MessageListModel(&lrcInstance_->getCurrentAccountInfo(), this)); set_mediaMessageListModel(QVariant::fromValue(mediaInteractions_.get())); }); @@ -98,37 +103,14 @@ MessagesAdapter::loadMoreMessages() auto convId = lrcInstance_->get_selectedConvUid(); try { const auto& convInfo = lrcInstance_->getConversationFromConvUid(convId, accountId); - if (convInfo.isSwarm()) { - auto* convModel = lrcInstance_->getCurrentConversationModel(); - convModel->loadConversationMessages(convId, loadChunkSize_); - } + if (convInfo.isSwarm()) + lrcInstance_->getCurrentConversationModel()->loadConversationMessages(convId, + loadChunkSize_); } catch (const std::exception& e) { qWarning() << e.what(); } } -int -MessagesAdapter::loadConversationUntil(const QString& to) -{ - try { - if (auto* model = getMsgListSourceModel()) { - auto idx = model->indexOfMessage(to); - if (idx == -1) { - auto accountId = lrcInstance_->get_currentAccountId(); - auto convId = lrcInstance_->get_selectedConvUid(); - const auto& convInfo = lrcInstance_->getConversationFromConvUid(convId, accountId); - if (convInfo.isSwarm()) { - auto* convModel = lrcInstance_->getCurrentConversationModel(); - return convModel->loadConversationUntil(convId, to); - } - } - } - } catch (const std::exception& e) { - qWarning() << e.what(); - } - return 0; -} - void MessagesAdapter::connectConversationModel() { @@ -200,11 +182,8 @@ MessagesAdapter::removeEmojiReaction(const QString& convId, const QString& messageId) { try { - const auto authorUri = lrcInstance_->getCurrentAccountInfo().profileInfo.uri; // check if this emoji has already been added by this author - auto emojiId = lrcInstance_->getConversationFromConvUid(convId) - .interactions->findEmojiReaction(emoji, authorUri, messageId); - editMessage(convId, "", emojiId); + editMessage(convId, "", messageId); } catch (...) { qDebug() << "Exception during removeEmojiReaction():" << messageId; } @@ -267,13 +246,6 @@ MessagesAdapter::copyToDownloads(const QString& interactionId, const QString& di } } -void -MessagesAdapter::deleteInteraction(const QString& interactionId) -{ - lrcInstance_->getCurrentConversationModel() - ->clearInteractionFromConversation(lrcInstance_->get_selectedConvUid(), interactionId); -} - void MessagesAdapter::openUrl(const QString& url) { @@ -754,13 +726,13 @@ MessagesAdapter::getFormattedDay(const quint64 timestamp) void MessagesAdapter::startSearch(const QString& text, bool isMedia) { - mediaInteractions_.reset(new MessageListModel(this)); + auto accountId = lrcInstance_->get_currentAccountId(); + mediaInteractions_.reset(new MessageListModel(&lrcInstance_->getCurrentAccountInfo(), this)); set_mediaMessageListModel(QVariant::fromValue(mediaInteractions_.get())); if (text.isEmpty() && !isMedia) return; - auto accountId = lrcInstance_->get_currentAccountId(); auto convId = lrcInstance_->get_selectedConvUid(); try { diff --git a/src/app/messagesadapter.h b/src/app/messagesadapter.h index db3916ed1..0ae1bdd48 100644 --- a/src/app/messagesadapter.h +++ b/src/app/messagesadapter.h @@ -83,7 +83,6 @@ Q_SIGNALS: protected: Q_INVOKABLE bool isDocument(const interaction::Type& type); Q_INVOKABLE void loadMoreMessages(); - Q_INVOKABLE qint32 loadConversationUntil(const QString& to); Q_INVOKABLE void connectConversationModel(); Q_INVOKABLE void sendConversationRequest(); Q_INVOKABLE void removeConversation(const QString& convUid); @@ -112,7 +111,6 @@ protected: Q_INVOKABLE void openUrl(const QString& url); Q_INVOKABLE void openDirectory(const QString& arg); Q_INVOKABLE void removeFile(const QString& interactionId, const QString& path); - Q_INVOKABLE void deleteInteraction(const QString& interactionId); Q_INVOKABLE void joinCall(const QString& uri, const QString& deviceId, const QString& confId, diff --git a/src/app/utilsadapter.cpp b/src/app/utilsadapter.cpp index 3b5d4f4a6..241f425b5 100644 --- a/src/app/utilsadapter.cpp +++ b/src/app/utilsadapter.cpp @@ -86,6 +86,8 @@ UtilsAdapter::setAppValue(const Settings::Key key, const QVariant& value) set_isRTL(isRTL()); } else if (key == Settings::Key::BaseZoom) Q_EMIT changeFontSize(); + else if (key == Settings::Key::DisplayHyperlinkPreviews) + settingsManager_->loadHistory(); else if (key == Settings::Key::EnableExperimentalSwarm) Q_EMIT showExperimentalCallSwarm(); else if (key == Settings::Key::ShowChatviewHorizontally) @@ -517,17 +519,6 @@ UtilsAdapter::monitor(const bool& continuous) lrcInstance_->monitor(continuous); } -void -UtilsAdapter::clearInteractionsCache(const QString& accountId, const QString& convId) -{ - try { - auto& accInfo = lrcInstance_->accountModel().getAccountInfo(accountId); - auto& convModel = accInfo.conversationModel; - convModel->clearInteractionsCache(convId); - } catch (...) { - } -} - QVariantMap UtilsAdapter::supportedLang() { diff --git a/src/app/utilsadapter.h b/src/app/utilsadapter.h index 7bafca689..14adcd7e5 100644 --- a/src/app/utilsadapter.h +++ b/src/app/utilsadapter.h @@ -128,7 +128,6 @@ public: Q_INVOKABLE void setDownloadPath(QString dir); Q_INVOKABLE void setScreenshotPath(QString dir); Q_INVOKABLE void monitor(const bool& continuous); - Q_INVOKABLE void clearInteractionsCache(const QString& accountId, const QString& convUid); Q_INVOKABLE QVariantMap supportedLang(); Q_INVOKABLE QString tempCreationImage(const QString& imageId = "temp") const; Q_INVOKABLE void setTempCreationImageFromString(const QString& image = "", diff --git a/src/libclient/accountmodel.cpp b/src/libclient/accountmodel.cpp index d89aa6ee6..9d7c43699 100644 --- a/src/libclient/accountmodel.cpp +++ b/src/libclient/accountmodel.cpp @@ -1205,6 +1205,14 @@ AccountModel::notificationsCount() const return total; } +void +AccountModel::reloadHistory() +{ + for (const auto& [_id, account] : pimpl_->accounts) { + account.first.conversationModel->reloadHistory(); + } +} + QString AccountModel::avatar(const QString& accountId) const { diff --git a/src/libclient/api/accountmodel.h b/src/libclient/api/accountmodel.h index 62f4c5453..b414bf64f 100644 --- a/src/libclient/api/accountmodel.h +++ b/src/libclient/api/accountmodel.h @@ -252,6 +252,7 @@ public: * Get notifications count across accounts */ int notificationsCount() const; + void reloadHistory(); /** * Retrieve account's avatar */ diff --git a/src/libclient/api/conversation.h b/src/libclient/api/conversation.h index 7b856fd24..580cf9d54 100644 --- a/src/libclient/api/conversation.h +++ b/src/libclient/api/conversation.h @@ -59,17 +59,26 @@ to_mode(const int intMode) struct Info { - Info() - : interactions(std::make_unique<MessageListModel>(nullptr)) - {} + explicit Info(const QString& uid, const account::Info* acc) + : uid(uid) + , interactions(std::make_unique<MessageListModel>(acc, nullptr)) + { + account = acc; + if (acc) { + accountId = acc->id; + accountUri = acc->profileInfo.uri; + } + } Info(const Info& other) = delete; Info(Info&& other) = default; Info& operator=(const Info& other) = delete; Info& operator=(Info&& other) = default; bool allMessagesLoaded = false; - QString uid = ""; + QString uid; QString accountId; + const account::Info* account {nullptr}; + QString accountUri; QVector<member::Member> participants; VectorMapStringString activeCalls; VectorMapStringString ignoredActiveCalls; @@ -77,7 +86,6 @@ struct Info QString callId; QString confId; std::unique_ptr<MessageListModel> interactions; - QString lastMessageUid; QString lastSelfMessageId; QHash<QString, QString> parentsId; // pair messageid/parentid for messages without parent loaded unsigned int unreadMessages = 0; diff --git a/src/libclient/api/conversationmodel.h b/src/libclient/api/conversationmodel.h index dcfeb31dd..7fc797e71 100644 --- a/src/libclient/api/conversationmodel.h +++ b/src/libclient/api/conversationmodel.h @@ -268,17 +268,6 @@ public: * clear all history */ void clearAllHistory(); - /** - * Clear one interaction from the history - * @param convId - * @param interactionId - */ - void clearInteractionFromConversation(const QString& convId, const QString& interactionId); - /** - * Clear the cache for interactions in the conversation - * @param convId - */ - void clearInteractionsCache(const QString& convId); /** * @param convId * @param interactionId @@ -335,7 +324,6 @@ public: * @return id for loading request. -1 if not loaded */ int loadConversationMessages(const QString& conversationId, const int size = 1); - int loadConversationUntil(const QString& conversationId, const QString& to); /** * accept request for conversation * @param conversationId conversation's id @@ -413,6 +401,7 @@ public: * @return number of conversations requests + unread */ int notificationsCount() const; + void reloadHistory() const; const VectorString peersForConversation(const QString& conversationId); // Presentation diff --git a/src/libclient/api/interaction.h b/src/libclient/api/interaction.h index be83d4780..61dc57d08 100644 --- a/src/libclient/api/interaction.h +++ b/src/libclient/api/interaction.h @@ -269,9 +269,61 @@ getContactInteractionString(const QString& authorUri, const ContactAction& actio case ContactAction::UNBANNED: return QObject::tr("%1 was re-added").arg(authorUri); case ContactAction::INVALID: + return QObject::tr("Contact added"); + } + return QObject::tr("Contact added"); +} + +static inline QString +getFormattedCallDuration(const std::time_t duration) +{ + if (duration == 0) return {}; + std::string formattedString; + auto minutes = duration / 60; + auto seconds = duration % 60; + if (minutes > 0) { + formattedString += std::to_string(minutes) + ":"; + if (formattedString.length() == 2) { + formattedString = "0" + formattedString; + } + } else { + formattedString += "00:"; + } + if (seconds < 10) + formattedString += "0"; + formattedString += std::to_string(seconds); + return QString::fromStdString(formattedString); +} + +/** + * Get a formatted string for a call interaction's body + * @param isSelf + * @param info + * @return the formatted and translated call message string + */ +static inline QString +getCallInteractionStringNonSwarm(bool isSelf, const std::time_t& duration) +{ + if (duration < 0) { + if (isSelf) { + return QObject::tr("Outgoing call"); + } else { + return QObject::tr("Incoming call"); + } + } else if (isSelf) { + if (duration) { + return QObject::tr("Outgoing call") + " - " + getFormattedCallDuration(duration); + } else { + return QObject::tr("Missed outgoing call"); + } + } else { + if (duration) { + return QObject::tr("Incoming call") + " - " + getFormattedCallDuration(duration); + } else { + return QObject::tr("Missed incoming call"); + } } - return {}; } struct Body @@ -287,6 +339,17 @@ public: std::time_t timestamp; }; +struct Emoji +{ + Q_GADGET + + Q_PROPERTY(QString commitId MEMBER commitId) + Q_PROPERTY(QString body MEMBER body) +public: + QString commitId; + QString body; +}; + /** * @var authorUri * @var body @@ -336,7 +399,7 @@ struct Info this->isRead = isRead; } - Info(const MapStringString& message, const QString& accountURI) + void init(const MapStringString& message, const QString& accountURI) { type = to_type(message["type"]); if (message.contains("react-to") && type == Type::TEXT) { @@ -345,7 +408,7 @@ struct Info } authorUri = message["author"]; - if (type == Type::TEXT || type == Type::EDITED || type == Type::REACTION) { + if (type == Type::TEXT) { body = message["body"]; } timestamp = message["timestamp"].toInt(); @@ -367,6 +430,36 @@ struct Info } commit = message; } + + Info(const MapStringString& message, const QString& accountURI) + { + init(message, accountURI); + } + + Info(const SwarmMessage& msg, const QString& accountUri) + { + MapStringString msgBody; + for (const auto& key : msg.body.keys()) { + msgBody.insert(key, msg.body.value(key)); + } + init(msgBody, accountUri); + parentId = msg.linearizedParent; + type = to_type(msg.type); + for (const auto& edition : msg.editions) + previousBodies.append(Body {edition.value("id"), + edition.value("body"), + QString(edition.value("timestamp")).toInt()}); + QMap<QString, QVariantList> mapStringEmoji; + for (const auto& reaction : msg.reactions) { + auto author = reaction.value("author"); + auto body = reaction.value("body"); + auto emoji = Emoji {reaction.value("id"), body}; + QVariant variant = QVariant::fromValue(emoji); + mapStringEmoji[author].append(variant); + } + for (auto i = mapStringEmoji.begin(); i != mapStringEmoji.end(); i++) + reactions.insert(i.key(), i.value()); + } }; static inline bool @@ -375,6 +468,17 @@ isOutgoing(const Info& interaction) return interaction.authorUri.isEmpty(); } +static inline QString +getCallInteractionString(bool isSelf, const Info& info) +{ + if (!info.confId.isEmpty()) { + if (info.duration <= 0) { + return QObject::tr("Join call"); + } + } + return getCallInteractionStringNonSwarm(isSelf, info.duration); +} + } // namespace interaction } // namespace api } // namespace lrc diff --git a/src/libclient/authority/storagehelper.cpp b/src/libclient/authority/storagehelper.cpp index 8e505441e..866ed07c1 100644 --- a/src/libclient/authority/storagehelper.cpp +++ b/src/libclient/authority/storagehelper.cpp @@ -144,78 +144,6 @@ prepareUri(const QString& uri, api::profile::Type type) } } -QString -getFormattedCallDuration(const std::time_t duration) -{ - if (duration == 0) - return {}; - std::string formattedString; - auto minutes = duration / 60; - auto seconds = duration % 60; - if (minutes > 0) { - formattedString += std::to_string(minutes) + ":"; - if (formattedString.length() == 2) { - formattedString = "0" + formattedString; - } - } else { - formattedString += "00:"; - } - if (seconds < 10) - formattedString += "0"; - formattedString += std::to_string(seconds); - return QString::fromStdString(formattedString); -} - -QString -getCallInteractionStringNonSwarm(bool isSelf, const std::time_t& duration) -{ - if (duration < 0) { - if (isSelf) { - return QObject::tr("Outgoing call"); - } else { - return QObject::tr("Incoming call"); - } - } else if (isSelf) { - if (duration) { - return QObject::tr("Outgoing call") + " - " + getFormattedCallDuration(duration); - } else { - return QObject::tr("Missed outgoing call"); - } - } else { - if (duration) { - return QObject::tr("Incoming call") + " - " + getFormattedCallDuration(duration); - } else { - return QObject::tr("Missed incoming call"); - } - } -} - -QString -getCallInteractionString(bool isSelf, const api::interaction::Info& info) -{ - if (!info.confId.isEmpty()) { - if (info.duration <= 0) { - return QObject::tr("Join call"); - } - } - return getCallInteractionStringNonSwarm(isSelf, info.duration); -} - -QString -getContactInteractionString(const QString& authorUri, const api::interaction::Status& status) -{ - if (authorUri.isEmpty()) { - return QObject::tr("Contact added"); - } else { - if (status == api::interaction::Status::UNKNOWN) { - return QObject::tr("Invitation received"); - } else if (status == api::interaction::Status::SUCCESS) { - return QObject::tr("Invitation accepted"); - } - } - return {}; -} - namespace vcard { QString compressedAvatar(const QString& image) @@ -515,6 +443,21 @@ beginConversationWithPeer(Database& db, return newConversationsId; } +QString +getContactInteractionString(const QString& authorUri, const api::interaction::Status& status) +{ + if (authorUri.isEmpty()) { + return QObject::tr("Contact added"); + } else { + if (status == api::interaction::Status::UNKNOWN) { + return QObject::tr("Invitation received"); + } else if (status == api::interaction::Status::SUCCESS) { + return QObject::tr("Invitation accepted"); + } + } + return {}; +} + void getHistory(Database& db, api::conversation::Info& conversation, const QString& localUri) { @@ -540,9 +483,11 @@ getHistory(Database& db, api::conversation::Info& conversation, const QString& l : std::stoi(durationString.toStdString()); auto status = api::interaction::to_status(payloads[i + 5]); if (type == api::interaction::Type::CALL) { - body = getCallInteractionStringNonSwarm(payloads[i + 1] == localUri, duration); + body = api::interaction::getCallInteractionStringNonSwarm(payloads[i + 1] + == localUri, + duration); } else if (type == api::interaction::Type::CONTACT) { - body = getContactInteractionString(payloads[i + 1], status); + body = storage::getContactInteractionString(payloads[i + 1], status); } auto msg = api::interaction::Info({payloads[i + 1], body, @@ -552,7 +497,6 @@ getHistory(Database& db, api::conversation::Info& conversation, const QString& l status, (payloads[i + 6] == "1" ? true : false)}); conversation.interactions->emplace(payloads[i], std::move(msg)); - conversation.lastMessageUid = payloads[i]; if (status != api::interaction::Status::DISPLAYED || !payloads[i + 1].isEmpty()) { continue; } @@ -764,20 +708,6 @@ clearHistory(Database& db, const QString& conversationId) } } -void -clearInteractionFromConversation(Database& db, - const QString& conversationId, - const QString& interactionId) -{ - try { - db.deleteFrom("interactions", - "conversation=:conversation AND id=:id", - {{":conversation", conversationId}, {":id", interactionId}}); - } catch (Database::QueryDeleteError& e) { - qWarning() << "deleteFrom error: " << e.details(); - } -} - void clearAllHistory(Database& db) { diff --git a/src/libclient/authority/storagehelper.h b/src/libclient/authority/storagehelper.h index eaf66aa06..c7187936a 100644 --- a/src/libclient/authority/storagehelper.h +++ b/src/libclient/authority/storagehelper.h @@ -54,15 +54,6 @@ QString getPath(); */ QString prepareUri(const QString& uri, api::profile::Type type); -/** - * Get a formatted string for a call interaction's body - * @param isSelf - * @param info - * @return the formatted and translated call message string - */ -QString getCallInteractionString(bool isSelf, const api::interaction::Info& info); -QString getCallInteractionStringNonSwarm(bool isSelf, const std::time_t& duration); - /** * Get a formatted string for a contact interaction's body * @param author_uri @@ -99,12 +90,6 @@ void setProfile(const QString& accountId, } // namespace vcard -/** - * @param duration - * @return a human readable call duration (M:ss) - */ -QString getFormattedCallDuration(const std::time_t duration); - /** * Get all conversations with a given participant's URI * @param db @@ -311,16 +296,6 @@ void setInteractionRead(Database& db, const QString& id); */ void clearHistory(Database& db, const QString& conversationId); -/** - * Clear interaction from history - * @param db - * @param conversationId - * @param interactionId - */ -void clearInteractionFromConversation(Database& db, - const QString& conversationId, - const QString& interactionId); - /** * Clear all history stored in the interactions table of the database * @param db diff --git a/src/libclient/callbackshandler.cpp b/src/libclient/callbackshandler.cpp index b594ebddb..48e244342 100644 --- a/src/libclient/callbackshandler.cpp +++ b/src/libclient/callbackshandler.cpp @@ -301,9 +301,9 @@ CallbacksHandler::CallbacksHandler(const Lrc& parent) &CallbacksHandler::slotAudioMeterReceived, Qt::QueuedConnection); connect(&ConfigurationManager::instance(), - &ConfigurationManagerInterface::conversationLoaded, + &ConfigurationManagerInterface::swarmLoaded, this, - &CallbacksHandler::slotConversationLoaded, + &CallbacksHandler::slotSwarmLoaded, Qt::QueuedConnection); connect(&ConfigurationManager::instance(), &ConfigurationManagerInterface::messagesFound, @@ -311,10 +311,25 @@ CallbacksHandler::CallbacksHandler(const Lrc& parent) &CallbacksHandler::slotMessagesFound, Qt::QueuedConnection); connect(&ConfigurationManager::instance(), - &ConfigurationManagerInterface::messageReceived, + &ConfigurationManagerInterface::swarmMessageReceived, this, &CallbacksHandler::slotMessageReceived, Qt::QueuedConnection); + connect(&ConfigurationManager::instance(), + &ConfigurationManagerInterface::swarmMessageUpdated, + this, + &CallbacksHandler::slotMessageUpdated, + Qt::QueuedConnection); + connect(&ConfigurationManager::instance(), + &ConfigurationManagerInterface::reactionAdded, + this, + &CallbacksHandler::slotReactionAdded, + Qt::QueuedConnection); + connect(&ConfigurationManager::instance(), + &ConfigurationManagerInterface::reactionRemoved, + this, + &CallbacksHandler::slotReactionRemoved, + Qt::QueuedConnection); connect(&ConfigurationManager::instance(), &ConfigurationManagerInterface::conversationProfileUpdated, this, @@ -721,13 +736,14 @@ CallbacksHandler::slotRemoteRecordingChanged(const QString& callId, } void -CallbacksHandler::slotConversationLoaded(uint32_t requestId, - const QString& accountId, - const QString& conversationId, - const VectorMapStringString& messages) +CallbacksHandler::slotSwarmLoaded(uint32_t requestId, + const QString& accountId, + const QString& conversationId, + const VectorSwarmMessage& messages) { - Q_EMIT conversationLoaded(requestId, accountId, conversationId, messages); + Q_EMIT swarmLoaded(requestId, accountId, conversationId, messages); } + void CallbacksHandler::slotMessagesFound(uint32_t requestId, const QString& accountId, @@ -740,11 +756,37 @@ CallbacksHandler::slotMessagesFound(uint32_t requestId, void CallbacksHandler::slotMessageReceived(const QString& accountId, const QString& conversationId, - const MapStringString& message) + const SwarmMessage& message) { Q_EMIT messageReceived(accountId, conversationId, message); } +void +CallbacksHandler::slotMessageUpdated(const QString& accountId, + const QString& conversationId, + const SwarmMessage& message) +{ + Q_EMIT messageUpdated(accountId, conversationId, message); +} + +void +CallbacksHandler::slotReactionAdded(const QString& accountId, + const QString& conversationId, + const QString& messageId, + const MapStringString& reaction) +{ + Q_EMIT reactionAdded(accountId, conversationId, messageId, reaction); +} + +void +CallbacksHandler::slotReactionRemoved(const QString& accountId, + const QString& conversationId, + const QString& messageId, + const QString& reactionId) +{ + Q_EMIT reactionRemoved(accountId, conversationId, messageId, reactionId); +} + void CallbacksHandler::slotConversationProfileUpdated(const QString& accountId, const QString& conversationId, diff --git a/src/libclient/callbackshandler.h b/src/libclient/callbackshandler.h index ea7033a47..e2f72b31c 100644 --- a/src/libclient/callbackshandler.h +++ b/src/libclient/callbackshandler.h @@ -22,6 +22,8 @@ #include "api/datatransfer.h" #include "qtwrapper/conversions_wrap.hpp" +#include <conversation_interface.h> + #include <QObject> #include <memory> @@ -335,17 +337,28 @@ Q_SIGNALS: * @param code */ void remoteRecordingChanged(const QString& callId, const QString& peerNumber, bool state); - void conversationLoaded(uint32_t requestId, - const QString& accountId, - const QString& conversationId, - const VectorMapStringString& messages); + void swarmLoaded(uint32_t requestId, + const QString& accountId, + const QString& conversationId, + const VectorSwarmMessage& messages); void messagesFound(uint32_t requestId, const QString& accountId, const QString& conversationId, const VectorMapStringString& messages); void messageReceived(const QString& accountId, const QString& conversationId, - const MapStringString& message); + const SwarmMessage& message); + void messageUpdated(const QString& accountId, + const QString& conversationId, + const SwarmMessage& message); + void reactionAdded(const QString& accountId, + const QString& conversationId, + const QString& messageId, + const MapStringString& reaction); + void reactionRemoved(const QString& accountId, + const QString& conversationId, + const QString& messageId, + const QString& reactionId); void conversationProfileUpdated(const QString& accountId, const QString& conversationId, const MapStringString& profile); @@ -643,17 +656,28 @@ private Q_SLOTS: * @param state, new state */ void slotRemoteRecordingChanged(const QString& callId, const QString& contactId, bool state); - void slotConversationLoaded(uint32_t requestId, - const QString& accountId, - const QString& conversationId, - const VectorMapStringString& messages); + void slotSwarmLoaded(uint32_t requestId, + const QString& accountId, + const QString& conversationId, + const VectorSwarmMessage& messages); void slotMessagesFound(uint32_t requestId, const QString& accountId, const QString& conversationId, const VectorMapStringString& messages); void slotMessageReceived(const QString& accountId, const QString& conversationId, - const MapStringString& message); + const SwarmMessage& message); + void slotMessageUpdated(const QString& accountId, + const QString& conversationId, + const SwarmMessage& message); + void slotReactionAdded(const QString& accountId, + const QString& conversationId, + const QString& messageId, + const MapStringString& reaction); + void slotReactionRemoved(const QString& accountId, + const QString& conversationId, + const QString& messageId, + const QString& reactionId); void slotConversationProfileUpdated(const QString& accountId, const QString& conversationId, const MapStringString& message); diff --git a/src/libclient/callmodel.cpp b/src/libclient/callmodel.cpp index ee52a15e3..c1a4d693e 100644 --- a/src/libclient/callmodel.cpp +++ b/src/libclient/callmodel.cpp @@ -914,7 +914,7 @@ CallModel::getFormattedCallDuration(const QString& callId) const auto d = std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch() - startTime.time_since_epoch()) .count(); - return authority::storage::getFormattedCallDuration(d); + return interaction::getFormattedCallDuration(d); } bool diff --git a/src/libclient/conversationmodel.cpp b/src/libclient/conversationmodel.cpp index de2df8a41..87b4a602a 100644 --- a/src/libclient/conversationmodel.cpp +++ b/src/libclient/conversationmodel.cpp @@ -215,11 +215,6 @@ public: // filter out ourself from conversation participants. const VectorString peersForConversation(const conversation::Info& conversation) const; - // insert swarm interactions. Return false if interaction already exists. - bool insertSwarmInteraction(const QString& interactionId, - interaction::Info& interaction, - conversation::Info& conversation, - bool insertAtBegin); void invalidateModel(); void emplaceBackConversation(conversation::Info&& conversation); void eraseConversation(const QString& convId); @@ -350,10 +345,10 @@ public Q_SLOTS: datatransfer::Info info, interaction::Status newStatus, bool& updated); - void slotConversationLoaded(uint32_t requestId, - const QString& accountId, - const QString& conversationId, - const VectorMapStringString& messages); + void slotSwarmLoaded(uint32_t requestId, + const QString& accountId, + const QString& conversationId, + const VectorSwarmMessage& messages); /** * Listen messageFound signal. * Is the search response from MessagesAdapter::getConvMedias() @@ -368,7 +363,18 @@ public Q_SLOTS: const VectorMapStringString& messages); void slotMessageReceived(const QString& accountId, const QString& conversationId, - const MapStringString& message); + const SwarmMessage& message); + void slotMessageUpdated(const QString& accountId, + const QString& conversationId, + const SwarmMessage& message); + void slotReactionAdded(const QString& accountId, + const QString& conversationId, + const QString& messageId, + const MapStringString& reaction); + void slotReactionRemoved(const QString& accountId, + const QString& conversationId, + const QString& messageId, + const QString& reactionId); void slotConversationProfileUpdated(const QString& accountId, const QString& conversationId, const MapStringString& profile); @@ -1149,6 +1155,16 @@ ConversationModel::notificationsCount() const return notificationsCount; } +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)); + }); +} + QString ConversationModel::title(const QString& conversationId) const { @@ -1345,7 +1361,6 @@ ConversationModel::sendMessage(const QString& uid, const QString& body, const QS return; } - newConv.lastMessageUid = msgId; newConv.lastSelfMessageId = msgId; // Emit this signal for chatview in the client Q_EMIT newInteraction(convId, msgId, msg); @@ -1501,101 +1516,6 @@ ConversationModel::clearHistory(const QString& uid) Q_EMIT dataChanged(conversationIdx); } -void -ConversationModel::clearInteractionFromConversation(const QString& convId, - const QString& interactionId) -{ - auto conversationIdx = pimpl_->indexOf(convId); - if (conversationIdx == -1) - return; - - auto erased_keys = 0; - bool lastInteractionUpdated = false; - bool updateDisplayedUid = false; - QString newDisplayedUid = 0; - QString participantURI = ""; - { - std::lock_guard<std::mutex> lk(pimpl_->interactionsLocks[convId]); - try { - auto& conversation = pimpl_->conversations.at(conversationIdx); - if (conversation.isSwarm()) { - // WARNING: clearInteractionFromConversation not implemented for swarm - return; - } - storage::clearInteractionFromConversation(pimpl_->db, convId, interactionId); - erased_keys = conversation.interactions->erase(interactionId); - participantURI = pimpl_->peersForConversation(conversation).front(); - auto messageId = conversation.interactions->getRead(participantURI); - - if (messageId != "" && messageId == interactionId) { - for (auto iter = conversation.interactions->find(interactionId); - iter != conversation.interactions->end(); - --iter) { - if (isOutgoing(iter->second) && iter->first != interactionId) { - newDisplayedUid = iter->first; - break; - } - } - updateDisplayedUid = true; - conversation.interactions->setRead(participantURI, newDisplayedUid); - } - - if (conversation.lastMessageUid == interactionId) { - // Update lastMessageUid - auto newLastId = QString::number(0); - if (!conversation.interactions->empty()) - newLastId = conversation.interactions->rbegin()->first; - conversation.lastMessageUid = newLastId; - lastInteractionUpdated = true; - } - if (conversation.lastSelfMessageId == interactionId) { - conversation.lastSelfMessageId = conversation.interactions->lastSelfMessageId( - owner.profileInfo.uri); - } - - } catch (const std::out_of_range& e) { - qDebug() << "can't clear interaction from conversation: " << e.what(); - } - } - if (updateDisplayedUid) { - Q_EMIT displayedInteractionChanged(convId, participantURI, interactionId, newDisplayedUid); - } - if (erased_keys > 0) { - pimpl_->filteredConversations.invalidate(); - Q_EMIT interactionRemoved(convId, interactionId); - } - if (lastInteractionUpdated) { - // last interaction as changed, so the order can change. - Q_EMIT modelChanged(); - Q_EMIT dataChanged(conversationIdx); - } -} - -void -ConversationModel::clearInteractionsCache(const QString& convId) -{ - auto conversationIdx = pimpl_->indexOf(convId); - if (conversationIdx == -1) - return; - - try { - auto& conversation = pimpl_->conversations.at(conversationIdx); - if (!conversation.isRequest && !conversation.needsSyncing && conversation.isSwarm()) { - { - std::lock_guard<std::mutex> lk(pimpl_->interactionsLocks[conversation.uid]); - conversation.interactions->clear(); - } - conversation.allMessagesLoaded = false; - conversation.lastMessageUid = ""; - conversation.lastSelfMessageId = ""; - ConfigurationManager::instance().loadConversationMessages(owner.id, convId, "", 1); - } - } catch (const std::out_of_range& e) { - qDebug() << "can't find interaction from conversation: " << e.what(); - return; - } -} - bool ConversationModel::isLastDisplayed(const QString& convId, const QString& interactionId, @@ -1689,29 +1609,10 @@ ConversationModel::loadConversationMessages(const QString& conversationId, const } auto lastMsgId = conversation.interactions->empty() ? "" : conversation.interactions->front().first; - return ConfigurationManager::instance().loadConversationMessages(owner.id, - conversationId, - lastMsgId, - size); -} - -int -ConversationModel::loadConversationUntil(const QString& conversationId, const QString& to) -{ - auto conversationOpt = getConversationForUid(conversationId); - if (!conversationOpt.has_value()) { - return -1; - } - auto& conversation = conversationOpt->get(); - if (conversation.allMessagesLoaded) { - return -1; - } - auto lastMsgId = conversation.interactions->empty() ? "" - : conversation.interactions->front().first; - return ConfigurationManager::instance().loadConversationUntil(owner.id, - conversationId, - lastMsgId, - to); + return ConfigurationManager::instance().loadConversation(owner.id, + conversationId, + lastMsgId, + size); } void @@ -1892,9 +1793,9 @@ ConversationModelPimpl::ConversationModelPimpl(const ConversationModel& linked, &ConversationModelPimpl::slotTransferStatusUnjoinable); // swarm conversations connect(&callbacksHandler, - &CallbacksHandler::conversationLoaded, + &CallbacksHandler::swarmLoaded, this, - &ConversationModelPimpl::slotConversationLoaded); + &ConversationModelPimpl::slotSwarmLoaded); connect(&callbacksHandler, &CallbacksHandler::messagesFound, this, @@ -1903,6 +1804,18 @@ ConversationModelPimpl::ConversationModelPimpl(const ConversationModel& linked, &CallbacksHandler::messageReceived, this, &ConversationModelPimpl::slotMessageReceived); + connect(&callbacksHandler, + &CallbacksHandler::messageUpdated, + this, + &ConversationModelPimpl::slotMessageUpdated); + connect(&callbacksHandler, + &CallbacksHandler::reactionAdded, + this, + &ConversationModelPimpl::slotReactionAdded); + connect(&callbacksHandler, + &CallbacksHandler::reactionRemoved, + this, + &ConversationModelPimpl::slotReactionRemoved); connect(&callbacksHandler, &CallbacksHandler::conversationProfileUpdated, this, @@ -2044,9 +1957,9 @@ ConversationModelPimpl::~ConversationModelPimpl() &ConversationModelPimpl::slotTransferStatusUnjoinable); // swarm conversations disconnect(&callbacksHandler, - &CallbacksHandler::conversationLoaded, + &CallbacksHandler::swarmLoaded, this, - &ConversationModelPimpl::slotConversationLoaded); + &ConversationModelPimpl::slotSwarmLoaded); disconnect(&callbacksHandler, &CallbacksHandler::messagesFound, this, @@ -2055,6 +1968,18 @@ ConversationModelPimpl::~ConversationModelPimpl() &CallbacksHandler::messageReceived, this, &ConversationModelPimpl::slotMessageReceived); + disconnect(&callbacksHandler, + &CallbacksHandler::messageUpdated, + this, + &ConversationModelPimpl::slotMessageUpdated); + disconnect(&callbacksHandler, + &CallbacksHandler::reactionAdded, + this, + &ConversationModelPimpl::slotReactionAdded); + disconnect(&callbacksHandler, + &CallbacksHandler::reactionRemoved, + this, + &ConversationModelPimpl::slotReactionRemoved); disconnect(&callbacksHandler, &CallbacksHandler::conversationProfileUpdated, this, @@ -2332,8 +2257,8 @@ ConversationModelPimpl::sort(const conversation::Info& convA, const conversation return true; // Sort by last Interaction try { - auto lastMessageA = historyA->at(convA.lastMessageUid); - auto lastMessageB = historyB->at(convB.lastMessageUid); + 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"; @@ -2353,38 +2278,26 @@ ConversationModelPimpl::sendContactRequest(const QString& contactUri) } catch (std::out_of_range& e) { } } + void -ConversationModelPimpl::slotConversationLoaded(uint32_t requestId, - const QString& accountId, - const QString& conversationId, - const VectorMapStringString& messages) +ConversationModelPimpl::slotSwarmLoaded(uint32_t requestId, + const QString& accountId, + const QString& conversationId, + const VectorSwarmMessage& messages) { - if (accountId != linked.owner.id) { + if (accountId != linked.owner.id) return; - } - - auto allLoaded = messages.size() == 0; - + auto allLoaded = false; try { auto& conversation = getConversationForUid(conversationId).get(); - QString oldLast, oldBegin; // Used to detect loading loops just in case. - if (conversation.interactions->size() != 0) { - oldBegin = conversation.interactions->begin()->first; - oldLast = conversation.interactions->rbegin()->first; - } for (const auto& message : messages) { - if (message["type"].isEmpty()) { - continue; - } - auto msgId = message["id"]; + QString msgId = message.id; auto msg = interaction::Info(message, linked.owner.profileInfo.uri); - conversation.interactions->editMessage(msgId, msg); - conversation.interactions->reactToMessage(msgId, msg); auto downloadFile = false; if (msg.type == interaction::Type::INITIAL) { allLoaded = true; } else if (msg.type == interaction::Type::DATA_TRANSFER) { - auto fileId = message["fileId"]; + QString fileId = message.body.value("fileId"); QString path; qlonglong bytesProgress, totalSize; linked.owner.dataTransferModel->fileTransferInfo(accountId, @@ -2404,68 +2317,43 @@ ConversationModelPimpl::slotConversationLoaded(uint32_t requestId, : interaction::Status::TRANSFER_ONGOING; linked.owner.dataTransferModel->registerTransferId(fileId, msgId); downloadFile = (bytesProgress == 0); - } else if (msg.type == interaction::Type::CALL) { - msg.body = storage::getCallInteractionString(msg.authorUri - == linked.owner.profileInfo.uri, - msg); - } else if (msg.type == interaction::Type::CONTACT) { - auto bestName = msg.authorUri == linked.owner.profileInfo.uri - ? linked.owner.accountModel->bestNameForAccount(linked.owner.id) - : linked.owner.contactModel->bestNameForContact(msg.authorUri); - msg.body = interaction::getContactInteractionString(bestName, - interaction::to_action( - message["action"])); - } else if (msg.type == interaction::Type::EDITED) { - conversation.interactions->addEdition(msgId, msg, false); - } else if (msg.type == interaction::Type::REACTION) { - conversation.interactions->addReaction(msg.react_to, msgId); } - insertSwarmInteraction(msgId, msg, conversation, true); + + { + // 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 (downloadFile) { - // Note, we must do this after insertSwarmInteraction to find the interaction - handleIncomingFile(conversationId, msgId, message["totalSize"].toInt()); + handleIncomingFile(conversationId, + msgId, + QString(message.body.value("totalSize")).toInt()); } } - conversation.lastMessageUid = conversation.interactions->lastMessageUid(); conversation.lastSelfMessageId = conversation.interactions->lastSelfMessageId( linked.owner.profileInfo.uri); - if (conversation.lastMessageUid.isEmpty() && !conversation.allMessagesLoaded - && messages.size() != 0) { - if (conversation.interactions->size() > 0) { - QString newLast, newBegin; - if (conversation.interactions->size() > 0) { - newBegin = conversation.interactions->begin()->first; - newLast = conversation.interactions->rbegin()->first; - } - if (newLast == oldLast && !newLast.isEmpty() && newBegin == oldBegin - && !newBegin.isEmpty()) { // [[unlikely]] in c++20 - qCritical() << "Loading loop detected for " << conversationId << "(" << newBegin - << " ; " << newLast << ")"; - return; - } - } - // In this case, we only have loaded merge commits. Load more messages - ConfigurationManager::instance().loadConversationMessages(linked.owner.id, - conversationId, - messages.rbegin()->value( - "id"), - 2); - return; - } invalidateModel(); Q_EMIT linked.modelChanged(); Q_EMIT linked.newMessagesAvailable(linked.owner.id, conversationId); auto conversationIdx = indexOf(conversationId); Q_EMIT linked.dataChanged(conversationIdx); Q_EMIT linked.conversationMessagesLoaded(requestId, conversationId); - if (allLoaded) { conversation.allMessagesLoaded = true; Q_EMIT linked.conversationUpdated(conversationId); } } catch (const std::exception& e) { - qDebug() << "messages loaded for not existing conversation"; + qWarning() << e.what(); } } @@ -2508,35 +2396,27 @@ ConversationModelPimpl::slotMessagesFound(uint32_t requestId, void ConversationModelPimpl::slotMessageReceived(const QString& accountId, const QString& conversationId, - const MapStringString& message) + const SwarmMessage& message) { - if (accountId != linked.owner.id) { + if (accountId != linked.owner.id) return; - } try { auto& conversation = getConversationForUid(conversationId).get(); - if (message["type"].isEmpty() || message["type"] == "application/update-profile") { - return; - } - if (message["type"] == "initial") { + if (message.type == "initial") { conversation.allMessagesLoaded = true; Q_EMIT linked.conversationUpdated(conversationId); - if (message.find("invited") == message.end()) { + if (message.body.find("invited") == message.body.end()) { return; } } - auto msgId = message["id"]; + QString msgId = message.id; auto msg = interaction::Info(message, linked.owner.profileInfo.uri); - conversation.interactions->editMessage(msgId, msg); api::datatransfer::Info info; - QString fileId; - - auto updateUnread = false; if (msg.type == interaction::Type::DATA_TRANSFER) { // save data transfer interaction to db and assosiate daemon id with interaction id, // conversation id and db id - QString fileId = message["fileId"]; + QString fileId = message.body.value("fileId"); QString path; qlonglong bytesProgress, totalSize; linked.owner.dataTransferModel->fileTransferInfo(accountId, @@ -2555,60 +2435,32 @@ ConversationModelPimpl::slotMessageReceived(const QString& accountId, : bytesProgress == totalSize ? interaction::Status::TRANSFER_FINISHED : interaction::Status::TRANSFER_ONGOING; linked.owner.dataTransferModel->registerTransferId(fileId, msgId); - if (msg.authorUri != linked.owner.profileInfo.uri) { - updateUnread = true; - } - } else if (msg.type == interaction::Type::CALL) { - // If we're a call in a swarm - if (msg.authorUri != linked.owner.profileInfo.uri) - updateUnread = true; - msg.body = storage::getCallInteractionString(msg.authorUri - == linked.owner.profileInfo.uri, - msg); - } else if (msg.type == interaction::Type::CONTACT) { - auto bestName = msg.authorUri == linked.owner.profileInfo.uri - ? linked.owner.accountModel->bestNameForAccount(linked.owner.id) - : linked.owner.contactModel->bestNameForContact(msg.authorUri); - msg.body = interaction::getContactInteractionString(bestName, - interaction::to_action( - message["action"])); - if (msg.authorUri != linked.owner.profileInfo.uri) { - updateUnread = true; - } - } else if (msg.type == interaction::Type::TEXT) { - if (msg.authorUri != linked.owner.profileInfo.uri) { - updateUnread = true; - } - } else if (msg.type == interaction::Type::REACTION) { - conversation.interactions->addReaction(msg.react_to, msgId); - } else if (msg.type == interaction::Type::EDITED) { - conversation.interactions->addEdition(msgId, msg, true); } - if (!insertSwarmInteraction(msgId, msg, conversation, false)) { - // message already exists - return; - } - // once the reaction is added to interactions, we can update the reacted - // message - if (msg.type == interaction::Type::REACTION) { - auto reactInteraction = conversation.interactions->find(msg.react_to); - if (reactInteraction != conversation.interactions->end()) { - conversation.interactions->reactToMessage(msg.react_to, reactInteraction->second); + { + // 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 (updateUnread) { + auto updateUnread = msg.authorUri != linked.owner.profileInfo.uri; + if (updateUnread) conversation.unreadMessages++; - } - if (msg.type == interaction::Type::MERGE) { - invalidateModel(); - return; - } - conversation.lastMessageUid = conversation.interactions->lastMessageUid(); conversation.lastSelfMessageId = conversation.interactions->lastSelfMessageId( linked.owner.profileInfo.uri); invalidateModel(); - if (!interaction::isOutgoing(msg)) { + if (!interaction::isOutgoing(msg) && updateUnread) { Q_EMIT behaviorController.newUnreadInteraction(linked.owner.id, conversationId, msgId, @@ -2616,15 +2468,93 @@ ConversationModelPimpl::slotMessageReceived(const QString& accountId, } Q_EMIT linked.newInteraction(conversationId, msgId, msg); Q_EMIT linked.modelChanged(); - if (msg.status == interaction::Status::TRANSFER_AWAITING_HOST) { - handleIncomingFile(conversationId, msgId, message["totalSize"].toInt()); + if (msg.status == interaction::Status::TRANSFER_AWAITING_HOST && updateUnread) { + handleIncomingFile(conversationId, + msgId, + QString(message.body.value("totalSize")).toInt()); + } + Q_EMIT linked.dataChanged(indexOf(conversationId)); + } catch (const std::exception& e) { + qDebug() << "messages received for not existing conversation"; + } +} + +void +ConversationModelPimpl::slotMessageUpdated(const QString& accountId, + const QString& conversationId, + const SwarmMessage& message) +{ + if (accountId != linked.owner.id) + return; + try { + auto& conversation = getConversationForUid(conversationId).get(); + 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; + } } + invalidateModel(); + Q_EMIT linked.modelChanged(); Q_EMIT linked.dataChanged(indexOf(conversationId)); } catch (const std::exception& e) { qDebug() << "messages received for not existing conversation"; } } +void +ConversationModelPimpl::slotReactionAdded(const QString& accountId, + const QString& conversationId, + const QString& messageId, + const MapStringString& reaction) +{ + if (accountId != linked.owner.id) { + return; + } + try { + // qInfo() << "Add Reaction to " << messageId << " in " << conversationId; + auto& conversation = getConversationForUid(conversationId).get(); + conversation.interactions->addReaction(messageId, reaction); + } catch (const std::exception& e) { + qWarning() << e.what(); + } +} + +void +ConversationModelPimpl::slotReactionRemoved(const QString& accountId, + const QString& conversationId, + const QString& messageId, + const QString& reactionId) +{ + if (accountId != linked.owner.id) { + return; + } + try { + // qInfo() << "Remove Reaction from " << messageId << " in " << conversationId; + auto& conversation = getConversationForUid(conversationId).get(); + conversation.interactions->rmReaction(messageId, reactionId); + } catch (const std::exception& e) { + qWarning() << e.what(); + } +} + void ConversationModelPimpl::slotConversationProfileUpdated(const QString& accountId, const QString& conversationId, @@ -2641,51 +2571,6 @@ ConversationModelPimpl::slotConversationProfileUpdated(const QString& accountId, } } -bool -ConversationModelPimpl::insertSwarmInteraction(const QString& interactionId, - interaction::Info& interaction, - conversation::Info& conversation, - bool insertAtBegin) -{ - std::lock_guard<std::mutex> lk(interactionsLocks[conversation.uid]); - auto itExists = conversation.interactions->find(interactionId); - if (itExists != conversation.interactions->end()) { - // Erase interaction if exists, as it will be updated via a re-insertion - if (itExists->second.previousBodies.size() != 0) { - // If the message was edited, we should keep this state - interaction.body = itExists->second.body; - interaction.previousBodies = itExists->second.previousBodies; - } - itExists = conversation.interactions->erase(itExists); - if (itExists != conversation.interactions->end()) { - // next interaction doesn't have parent anymore. - conversation.parentsId[itExists->first] = interactionId; - } - } - int index = conversation.interactions->indexOfMessage(interaction.parentId); - if (index >= 0) { - auto result = conversation.interactions->insert(index + 1, - qMakePair(interactionId, interaction)); - if (!result.second) - return false; - } else { - auto result = conversation.interactions->insert(std::make_pair(interactionId, interaction), - insertAtBegin); - if (!result.second) - return false; - if (!interaction.parentId.isEmpty()) - conversation.parentsId[interactionId] = interaction.parentId; - } - if (!conversation.parentsId.values().contains(interactionId)) { - return true; - } - auto msgIds = conversation.parentsId.keys(interactionId); - conversation.interactions->moveMessages(msgIds, interactionId); - for (auto& msg : msgIds) - conversation.parentsId.remove(msg); - return true; -} - void ConversationModelPimpl::slotConversationRequestReceived(const QString& accountId, const QString&, @@ -2749,10 +2634,7 @@ ConversationModelPimpl::slotConversationReady(const QString& accountId, conversation.needsSyncing = false; Q_EMIT linked.conversationUpdated(conversationId); Q_EMIT linked.dataChanged(conversationIdx); - ConfigurationManager::instance().loadConversationMessages(linked.owner.id, - conversationId, - "", - 0); + ConfigurationManager::instance().loadConversation(linked.owner.id, conversationId, "", 0); auto& peers = peersForConversation(conversation); if (peers.size() == 1) Q_EMIT linked.conversationReady(conversationId, peers.front()); @@ -2895,7 +2777,6 @@ ConversationModelPimpl::slotActiveCallsChanged(const QString& accountId, void ConversationModelPimpl::slotContactAdded(const QString& contactUri) { - QString convId; try { convId = linked.owner.contactModel->getContact(contactUri).conversationId; @@ -2904,14 +2785,15 @@ ConversationModelPimpl::slotContactAdded(const QString& contactUri) } auto isSwarm = !convId.isEmpty(); - auto conv = !isSwarm? storage::getConversationsWithPeer(db, contactUri) : VectorString {convId}; + auto conv = !isSwarm ? storage::getConversationsWithPeer(db, contactUri) + : VectorString {convId}; if (conv.isEmpty()) { if (linked.owner.profileInfo.type == profile::Type::SIP) { auto convId = storage::beginConversationWithPeer(db, - contactUri, - true, - linked.owner.contactModel->getAddedTs( - contactUri)); + contactUri, + true, + linked.owner.contactModel->getAddedTs( + contactUri)); addConversationWith(convId, contactUri, false); Q_EMIT linked.conversationReady(convId, contactUri); Q_EMIT linked.newConversation(convId); @@ -2922,7 +2804,7 @@ ConversationModelPimpl::slotContactAdded(const QString& contactUri) try { auto& conversation = getConversationForUid(convId).get(); MapStringString details = ConfigurationManager::instance() - .conversationInfos(linked.owner.id, conversation.uid); + .conversationInfos(linked.owner.id, conversation.uid); bool needsSyncing = details["syncing"] == "true"; if (conversation.needsSyncing != needsSyncing) { conversation.isRequest = false; @@ -2948,9 +2830,7 @@ ConversationModelPimpl::addContactRequest(const QString& contactUri) return; } catch (std::out_of_range&) { // no conversation exists. Add contact request - conversation::Info conversation; - conversation.uid = contactUri; - conversation.accountId = linked.owner.id; + conversation::Info conversation(contactUri, &linked.owner); conversation.participants = {{contactUri, member::Role::INVITED}}; conversation.mode = conversation::Mode::NON_SWARM; conversation.isRequest = true; @@ -2974,12 +2854,10 @@ ConversationModelPimpl::addConversationRequest(const MapStringString& convReques QString callId, confId; const MapStringString& details = ConfigurationManager::instance() .conversationInfos(linked.owner.id, convId); - conversation::Info conversation; - conversation.uid = convId; + conversation::Info conversation(convId, &linked.owner); conversation.infos = details; conversation.callId = callId; conversation.confId = confId; - conversation.accountId = linked.owner.id; conversation.participants = {{linked.owner.profileInfo.uri, member::Role::INVITED}, {peerUri, member::Role::MEMBER}}; conversation.mode = mode; @@ -2993,8 +2871,10 @@ ConversationModelPimpl::addConversationRequest(const MapStringString& convReques }; auto msg = interaction::Info(messageMap, linked.owner.profileInfo.uri); - insertSwarmInteraction(convId, msg, conversation, true); - conversation.lastMessageUid = convId; + { + std::lock_guard<std::mutex> lk(interactionsLocks[convId]); + conversation.interactions->insert(std::make_pair(convId, msg), true); + } // add the author to the contact model's contact list as a PENDING // if they aren't already a contact @@ -3023,7 +2903,7 @@ ConversationModelPimpl::addConversationRequest(const MapStringString& convReques Q_EMIT linked.modelChanged(); if (!callId.isEmpty()) { // If we replace a non swarm request by a swarm request while having a call. - Q_EMIT linked.selectConversation(convId); + linked.selectConversation(convId); } if (emitToClient) Q_EMIT behaviorController.newTrustRequest(linked.owner.id, convId, peerUri); @@ -3032,7 +2912,7 @@ ConversationModelPimpl::addConversationRequest(const MapStringString& convReques void ConversationModelPimpl::slotPendingContactAccepted(const QString& uri) { - auto type = linked.owner.profileInfo.type; + profile::Type type; try { type = linked.owner.contactModel->getContact(uri).profileInfo.type; } catch (std::out_of_range& e) { @@ -3110,16 +2990,14 @@ ConversationModelPimpl::slotContactModelUpdated(const QString& uri) searchResults.clear(); auto users = linked.owner.contactModel->getSearchResults(); for (auto& user : users) { - conversation::Info conversationInfo; + auto uid = linked.owner.profileInfo.type == profile::Type::SIP ? "SEARCHSIP" + : user.profileInfo.uri; + conversation::Info conversationInfo(uid, &linked.owner); // For SIP, we always got one search result, so "" is ok as there is no empty uri // For Jami accounts, the nameserver can return several results, so we use the uniqueness of // the id as id for a temporary conversation. - conversationInfo.uid = linked.owner.profileInfo.type == profile::Type::SIP - ? "SEARCHSIP" - : user.profileInfo.uri; conversationInfo.participants.append( member::Member {user.profileInfo.uri, member::Role::MEMBER}); - conversationInfo.accountId = linked.owner.id; searchResults.emplace_front(std::move(conversationInfo)); } Q_EMIT linked.searchResultUpdated(); @@ -3129,6 +3007,11 @@ ConversationModelPimpl::slotContactModelUpdated(const QString& uri) void ConversationModelPimpl::addSwarmConversation(const QString& convId) { + if (Lrc::dbusIsValid()) { + // Because the daemon may have already loaded interactions + // we clear them to receive all signals + ConfigurationManager::instance().clearCache(linked.owner.id, convId); + } QVector<member::Member> participants; const VectorMapStringString& members = ConfigurationManager::instance() .getConversationMembers(linked.owner.id, convId); @@ -3137,10 +3020,8 @@ ConversationModelPimpl::addSwarmConversation(const QString& convId) const MapStringString& details = ConfigurationManager::instance() .conversationInfos(linked.owner.id, convId); auto mode = conversation::to_mode(details["mode"].toInt()); - conversation::Info conversation; + conversation::Info conversation(convId, &linked.owner); conversation.infos = details; - conversation.uid = convId; - conversation.accountId = linked.owner.id; VectorMapStringString activeCalls = ConfigurationManager::instance() .getActiveCalls(linked.owner.id, convId); conversation.activeCalls = activeCalls; @@ -3212,14 +3093,16 @@ ConversationModelPimpl::addSwarmConversation(const QString& convId) }; auto msg = interaction::Info(messageMap, linked.owner.profileInfo.uri); - insertSwarmInteraction(convId, msg, conversation, true); - conversation.lastMessageUid = convId; + { + std::lock_guard<std::mutex> lk(interactionsLocks[convId]); + conversation.interactions->insert(std::make_pair(convId, msg), true); + } conversation.needsSyncing = true; Q_EMIT linked.conversationUpdated(conversation.uid); Q_EMIT linked.dataChanged(indexOf(conversation.uid)); } emplaceBackConversation(std::move(conversation)); - ConfigurationManager::instance().loadConversationMessages(linked.owner.id, convId, "", 1); + ConfigurationManager::instance().loadConversation(linked.owner.id, convId, "", 1); } void @@ -3227,9 +3110,7 @@ ConversationModelPimpl::addConversationWith(const QString& convId, const QString& contactUri, bool isRequest) { - conversation::Info conversation; - conversation.uid = convId; - conversation.accountId = linked.owner.id; + conversation::Info conversation(convId, &linked.owner); conversation.participants = {{contactUri, member::Role::MEMBER}}; conversation.mode = conversation::Mode::NON_SWARM; conversation.needsSyncing = false; @@ -3510,14 +3391,14 @@ ConversationModelPimpl::addOrUpdateCallMessage(const QString& callId, // update the db auto msgId = storage::addOrUpdateMessage(db, conv_it->uid, msg, callId); // now set the formatted call message string in memory only - msg.body = storage::getCallInteractionString(msg.authorUri == linked.owner.profileInfo.uri, msg); + 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->lastMessageUid = msgId; conv_it->interactions->emplace(msgId, msg); } else { interactionIt->second = msg; @@ -3627,7 +3508,6 @@ ConversationModelPimpl::addIncomingMessage(const QString& peerId, std::lock_guard<std::mutex> lk(interactionsLocks[conversations[conversationIdx].uid]); conversations[conversationIdx].interactions->emplace(msgId, msg); } - conversations[conversationIdx].lastMessageUid = msgId; conversations[conversationIdx].unreadMessages = getNumberOfUnreadMessagesFor(convIds[0]); } @@ -4115,7 +3995,6 @@ ConversationModelPimpl::slotTransferStatusCreated(const QString& fileId, datatra std::lock_guard<std::mutex> lk(interactionsLocks[conversations[conversationIdx].uid]); conversations[conversationIdx].interactions->emplace(interactionId, interaction); } - conversations[conversationIdx].lastMessageUid = interactionId; conversations[conversationIdx].unreadMessages = getNumberOfUnreadMessagesFor(convId); } Q_EMIT behaviorController.newUnreadInteraction(linked.owner.id, diff --git a/src/libclient/dbus/metatypes.h b/src/libclient/dbus/metatypes.h index c8347c040..375308152 100644 --- a/src/libclient/dbus/metatypes.h +++ b/src/libclient/dbus/metatypes.h @@ -42,6 +42,7 @@ Q_DECLARE_METATYPE(VectorString) Q_DECLARE_METATYPE(MapStringVectorString) Q_DECLARE_METATYPE(VectorVectorByte) Q_DECLARE_METATYPE(DataTransferInfo) +Q_DECLARE_METATYPE(SwarmMessage) Q_DECLARE_METATYPE(uint64_t) Q_DECLARE_METATYPE(Message) @@ -86,6 +87,36 @@ operator>>(const QDBusArgument& argument, DataTransferInfo& info) return argument; } +static inline QDBusArgument& +operator<<(QDBusArgument& argument, const SwarmMessage& m) +{ + argument.beginStructure(); + argument << m.id; + argument << m.type; + argument << m.linearizedParent; + argument << m.body; + argument << m.reactions; + argument << m.editions; + argument.endStructure(); + + return argument; +} + +static inline const QDBusArgument& +operator>>(const QDBusArgument& argument, SwarmMessage& m) +{ + argument.beginStructure(); + argument >> m.id; + argument >> m.type; + argument >> m.linearizedParent; + argument >> m.body; + argument >> m.reactions; + argument >> m.editions; + argument.endStructure(); + + return argument; +} + static inline QDBusArgument& operator<<(QDBusArgument& argument, const Message& m) { @@ -140,6 +171,10 @@ registerCommTypes() qDBusRegisterMetaType<VectorVectorByte>(); qRegisterMetaType<DataTransferInfo>("DataTransferInfo"); qDBusRegisterMetaType<DataTransferInfo>(); + qRegisterMetaType<SwarmMessage>("SwarmMessage"); + qDBusRegisterMetaType<SwarmMessage>(); + qRegisterMetaType<VectorSwarmMessage>("VectorSwarmMessage"); + qDBusRegisterMetaType<VectorSwarmMessage>(); qRegisterMetaType<Message>("Message"); qDBusRegisterMetaType<Message>(); qRegisterMetaType<QVector<Message>>("QVector<Message>"); diff --git a/src/libclient/messagelistmodel.cpp b/src/libclient/messagelistmodel.cpp index 108165ca9..6e3022ca3 100644 --- a/src/libclient/messagelistmodel.cpp +++ b/src/libclient/messagelistmodel.cpp @@ -21,6 +21,9 @@ #include "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" @@ -36,9 +39,9 @@ using constIterator = MessageListModel::constIterator; using iterator = MessageListModel::iterator; using reverseIterator = MessageListModel::reverseIterator; -MessageListModel::MessageListModel(QObject* parent) +MessageListModel::MessageListModel(const account::Info* account, QObject* parent) : QAbstractListModel(parent) - + , account_(account) {} QPair<iterator, bool> @@ -195,8 +198,16 @@ MessageListModel::clear() Q_EMIT beginResetModel(); interactions_.clear(); replyTo_.clear(); - editedBodies_.clear(); - reactedMessages_.clear(); + Q_EMIT endResetModel(); +} + +void +MessageListModel::reloadHistory() +{ + Q_EMIT beginResetModel(); + for (auto& interaction : interactions_) { + interaction.second.linkPreviewInfo.clear(); + } Q_EMIT endResetModel(); } @@ -266,54 +277,6 @@ MessageListModel::indexOfMessage(const QString& msgId, bool reverse) const : getIndex(interactions_.begin(), interactions_.end()); } -void -MessageListModel::moveMessages(QList<QString> msgIds, const QString& parentId) -{ - for (auto msgId : msgIds) { - moveMessage(msgId, parentId); - } -} - -void -MessageListModel::moveMessage(const QString& msgId, const QString& parentId) -{ - int currentIndex = indexOfMessage(msgId); - if (currentIndex == -1) { - qWarning() << "Incorrect index detected in MessageListModel::moveMessage"; - return; - } - - // if we have a next element check if it is a child interaction - QString childMessageIdToMove; - if (currentIndex < (interactions_.size() - 1)) { - const auto& next = interactions_.at(currentIndex + 1); - if (next.second.parentId == msgId) { - childMessageIdToMove = next.first; - } - } - - auto endIdx = currentIndex; - auto pId = msgId; - - // move a message - int newIndex = indexOfMessage(parentId) + 1; - if (newIndex >= interactions_.size()) { - newIndex = interactions_.size() - 1; - // If we can move all the messages after the current one, we can do it directly - childMessageIdToMove.clear(); - endIdx = std::max(endIdx, newIndex - 1); - } - - if (currentIndex == newIndex || newIndex == -1) - return; - - // Pretty every messages is moved - moveMessages(currentIndex, endIdx, newIndex); - // move a child message - if (!childMessageIdToMove.isEmpty()) - moveMessage(childMessageIdToMove, msgId); -} - void MessageListModel::updateReplies(item_t& message) { @@ -359,19 +322,6 @@ MessageListModel::removeMessage(int index, iterator it) Q_EMIT endRemoveRows(); } -void -MessageListModel::moveMessages(int from, int last, int to) -{ - if (last < from) - return; - QModelIndex sourceIndex = QAbstractListModel::index(from, 0); - QModelIndex destinationIndex = QAbstractListModel::index(to, 0); - Q_EMIT beginMoveRows(sourceIndex, from, last, destinationIndex, to); - for (int i = 0; i < (last - from); ++i) - interactions_.move(last, to); - Q_EMIT endMoveRows(); -} - bool MessageListModel::contains(const QString& msgId) { @@ -433,8 +383,26 @@ MessageListModel::dataForItem(item_t item, int, int role) const return QVariant(item.first); case Role::Author: return QVariant(item.second.authorUri); - case Role::Body: + case Role::Body: { + if (account_) { + if (item.second.type == lrc::api::interaction::Type::CALL) { + return QVariant( + interaction::getCallInteractionString(item.second.authorUri + == account_->profileInfo.uri, + item.second)); + } else if (item.second.type == lrc::api::interaction::Type::CONTACT) { + auto bestName = item.second.authorUri == account_->profileInfo.uri + ? account_->accountModel->bestNameForAccount(account_->id) + : account_->contactModel->bestNameForContact( + item.second.authorUri); + return QVariant( + interaction::getContactInteractionString(bestName, + interaction::to_action( + item.second.commit["action"]))); + } + } return QVariant(item.second.body); + } case Role::Timestamp: return QVariant::fromValue(item.second.timestamp); case Role::Duration: @@ -544,6 +512,45 @@ MessageListModel::addHyperlinkInfo(const QString& messageId, const QVariantMap& 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) { @@ -610,147 +617,6 @@ MessageListModel::emitDataChanged(const QString& msgId, VectorInt roles) Q_EMIT dataChanged(modelIndex, modelIndex, roles); } -void -MessageListModel::addEdition(const QString& msgId, const interaction::Info& info, bool end) -{ - auto editedId = info.commit["edit"]; - if (editedId.isEmpty()) - return; - auto& edited = editedBodies_[editedId]; - auto editedMsgIt = std::find_if(edited.begin(), edited.end(), [&](const auto& v) { - return msgId == v.commitId; - }); - if (editedMsgIt != edited.end()) - return; // Already added - auto value = interaction::Body {msgId, info.body, info.timestamp}; - if (end) - edited.push_back(value); - else - edited.push_front(value); - auto editedIt = find(editedId); - if (editedIt != interactions_.end()) { - // If already there, we can update the content - editMessage(editedId, editedIt->second); - if (!editedIt->second.react_to.isEmpty()) { - auto reactToIt = find(editedIt->second.react_to); - if (reactToIt != interactions_.end()) - reactToMessage(editedIt->second.react_to, reactToIt->second); - } - } -} - -void -MessageListModel::addReaction(const QString& messageId, const QString& reactionId) -{ - auto itReacted = reactedMessages_.find(messageId); - if (itReacted != reactedMessages_.end()) { - itReacted->insert(reactionId); - } else { - QSet<QString> emojiList; - emojiList.insert(reactionId); - reactedMessages_.insert(messageId, emojiList); - } - auto interaction = find(reactionId); - if (interaction != interactions_.end()) { - // Edit reaction if needed - editMessage(reactionId, interaction->second); - } -} - -QVariantMap -MessageListModel::convertReactMessagetoQVariant(const QSet<QString>& emojiIdList) -{ - QVariantMap convertedMap; - QMap<QString, QStringList> mapStringEmoji; - for (auto emojiId = emojiIdList.begin(); emojiId != emojiIdList.end(); emojiId++) { - auto interaction = find(*emojiId); - if (interaction != interactions_.end()) { - auto author = interaction->second.authorUri; - auto body = interaction->second.body; - if (!body.isEmpty()) { - auto itAuthor = mapStringEmoji.find(author); - if (itAuthor != mapStringEmoji.end()) { - mapStringEmoji[author].append(body); - } else { - QStringList emojiList; - emojiList.append(body); - mapStringEmoji.insert(author, emojiList); - } - } - } - } - for (auto i = mapStringEmoji.begin(); i != mapStringEmoji.end(); i++) { - convertedMap.insert(i.key(), i.value()); - } - return convertedMap; -} - -void -MessageListModel::editMessage(const QString& msgId, interaction::Info& info) -{ - auto it = editedBodies_.find(msgId); - if (it != editedBodies_.end()) { - if (info.previousBodies.isEmpty()) { - info.previousBodies.push_back(interaction::Body {msgId, info.body, info.timestamp}); - } - // Find if already added (because MessageReceived can be triggered - // multiple times for same message) - for (const auto& editedBody : *it) { - auto itCommit = std::find_if(info.previousBodies.begin(), - info.previousBodies.end(), - [&](const auto& element) { - return element.commitId == editedBody.commitId; - }); - if (itCommit == info.previousBodies.end()) { - info.previousBodies.push_back(editedBody); - } - } - info.body = it->rbegin()->body; - info.parsedBody.clear(); - editedBodies_.erase(it); - emitDataChanged(msgId, - {MessageList::Role::Body, - MessageList::Role::ParsedBody, - MessageList::Role::PreviousBodies, - MessageList::Role::IsEmojiOnly}); - - // Body changed, replies should update - for (const auto& replyId : replyTo_[msgId]) { - int index = getIndexOfMessage(replyId); - if (index == -1) - continue; - QModelIndex modelIndex = QAbstractListModel::index(index, 0); - Q_EMIT dataChanged(modelIndex, modelIndex, {Role::ReplyToBody}); - } - } -} - -void -MessageListModel::reactToMessage(const QString& msgId, interaction::Info& info) -{ - // If already there, we can update the content - auto itReact = reactedMessages_.find(msgId); - - if (itReact != reactedMessages_.end()) { - auto convertedMap = convertReactMessagetoQVariant(reactedMessages_[msgId]); - info.reactions = convertedMap; - emitDataChanged(find(msgId), {Role::Reactions}); - } -} - -QString -MessageListModel::lastMessageUid() const -{ - for (auto it = interactions_.rbegin(); it != interactions_.rend(); ++it) { - auto lastType = it->second.type; - if (lastType != interaction::Type::MERGE and lastType != interaction::Type::EDITED - and !it->second.body.isEmpty()) { - return it->first; - } - } - return {}; -} - QString MessageListModel::lastSelfMessageId(const QString& id) const { @@ -763,20 +629,4 @@ MessageListModel::lastSelfMessageId(const QString& id) const } return {}; } - -QString -MessageListModel::findEmojiReaction(const QString& emoji, - const QString& authorURI, - const QString& messageId) -{ - auto& messageReactions = reactedMessages_[messageId]; - for (auto it = messageReactions.begin(); it != messageReactions.end(); it++) { - auto interaction = find(*it); - if (interaction != interactions_.end() && interaction->second.body == emoji - && interaction->second.authorUri == authorURI) { - return *it; - } - } - return {}; -} } // namespace lrc diff --git a/src/libclient/messagelistmodel.h b/src/libclient/messagelistmodel.h index 7345862fd..432ad1d0d 100644 --- a/src/libclient/messagelistmodel.h +++ b/src/libclient/messagelistmodel.h @@ -20,6 +20,7 @@ #pragma once #include "api/interaction.h" +#include "api/account.h" #include <QAbstractListModel> @@ -79,7 +80,7 @@ public: typedef QList<QPair<QString, interaction::Info>>::Iterator iterator; typedef QList<QPair<QString, interaction::Info>>::reverse_iterator reverseIterator; - explicit MessageListModel(QObject* parent = nullptr); + explicit MessageListModel(const account::Info* account, QObject* parent = nullptr); ~MessageListModel() = default; // map functions @@ -105,6 +106,7 @@ public: 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; @@ -113,7 +115,6 @@ public: QPair<iterator, bool> insert(int index, QPair<QString, interaction::Info> message); int indexOfMessage(const QString& msgId, bool reverse = true) const; - void moveMessages(QList<QString> msgIds, const QString& parentId); int rowCount(const QModelIndex& parent = QModelIndex()) const override; Q_INVOKABLE virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const; @@ -123,6 +124,8 @@ public: 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); @@ -136,18 +139,9 @@ public: void emitDataChanged(const QString& msgId, VectorInt roles = {}); bool isOnlyEmoji(const QString& text) const; - void addEdition(const QString& msgId, const interaction::Info& info, bool end); - void addReaction(const QString& messageId, const QString& reactionId); - void editMessage(const QString& msgId, interaction::Info& info); - void reactToMessage(const QString& msgId, interaction::Info& info); QVariantMap convertReactMessagetoQVariant(const QSet<QString>&); - QString lastMessageUid() const; QString lastSelfMessageId(const QString& id) const; - QString findEmojiReaction(const QString& emoji, - const QString& authorURI, - const QString& messageId); - protected: using Role = MessageList::Role; @@ -161,17 +155,12 @@ private: QMap<QString, QString> lastDisplayedMessageUid_; QMap<QString, QStringList> messageToReaders_; QMap<QString, QSet<QString>> replyTo_; + const account::Info* account_; void updateReplies(item_t& message); - QMap<QString, QVector<interaction::Body>> editedBodies_; - - // key = messageId and values = QSet of reactionIds - QMap<QString, QSet<QString>> reactedMessages_; - void moveMessage(const QString& msgId, const QString& parentId); void insertMessage(int index, item_t& message); iterator insertMessage(iterator it, item_t& message); void removeMessage(int index, iterator it); - void moveMessages(int from, int last, int to); }; } // namespace api } // namespace lrc diff --git a/src/libclient/qtwrapper/configurationmanager_wrap.h b/src/libclient/qtwrapper/configurationmanager_wrap.h index 2ecafd2a2..448dda083 100644 --- a/src/libclient/qtwrapper/configurationmanager_wrap.h +++ b/src/libclient/qtwrapper/configurationmanager_wrap.h @@ -259,15 +259,25 @@ public: }), }; conversationsHandlers - = {exportable_callback<ConversationSignal::ConversationLoaded>( + = {exportable_callback<ConversationSignal::SwarmLoaded>( [this](uint32_t id, const std::string& accountId, const std::string& conversationId, - const std::vector<std::map<std::string, std::string>>& messages) { - Q_EMIT conversationLoaded(id, - QString(accountId.c_str()), - QString(conversationId.c_str()), - convertVecMap(messages)); + const std::vector<libjami::SwarmMessage>& messages) { + VectorSwarmMessage vec; + for (const auto& msg : messages) { + vec.push_back({msg.id.c_str(), + msg.type.c_str(), + msg.linearizedParent.c_str(), + convertMap(msg.body), + convertVecMap(msg.reactions), + convertVecMap(msg.editions)}); + } + + Q_EMIT swarmLoaded(id, + QString(accountId.c_str()), + QString(conversationId.c_str()), + vec); }), exportable_callback<ConversationSignal::MessagesFound>( [this](uint32_t id, @@ -279,13 +289,53 @@ public: QString(conversationId.c_str()), convertVecMap(messages)); }), - exportable_callback<ConversationSignal::MessageReceived>( + exportable_callback<ConversationSignal::SwarmMessageReceived>( + [this](const std::string& accountId, + const std::string& conversationId, + const libjami::SwarmMessage& message) { + ::SwarmMessage msg = {message.id.c_str(), + message.type.c_str(), + message.linearizedParent.c_str(), + convertMap(message.body), + convertVecMap(message.reactions), + convertVecMap(message.editions)}; + Q_EMIT swarmMessageReceived(QString(accountId.c_str()), + QString(conversationId.c_str()), + msg); + }), + exportable_callback<ConversationSignal::SwarmMessageUpdated>( [this](const std::string& accountId, const std::string& conversationId, - const std::map<std::string, std::string>& message) { - Q_EMIT messageReceived(QString(accountId.c_str()), + const libjami::SwarmMessage& message) { + ::SwarmMessage msg = {message.id.c_str(), + message.type.c_str(), + message.linearizedParent.c_str(), + convertMap(message.body), + convertVecMap(message.reactions), + convertVecMap(message.editions)}; + Q_EMIT swarmMessageUpdated(QString(accountId.c_str()), + QString(conversationId.c_str()), + msg); + }), + exportable_callback<ConversationSignal::ReactionAdded>( + [this](const std::string& accountId, + const std::string& conversationId, + const std::string& messageId, + const std::map<std::string, std::string>& reaction) { + Q_EMIT reactionAdded(QString(accountId.c_str()), + QString(conversationId.c_str()), + QString(messageId.c_str()), + convertMap(reaction)); + }), + exportable_callback<ConversationSignal::ReactionRemoved>( + [this](const std::string& accountId, + const std::string& conversationId, + const std::string& messageId, + const std::string& reactionId) { + Q_EMIT reactionRemoved(QString(accountId.c_str()), QString(conversationId.c_str()), - convertMap(message)); + QString(messageId.c_str()), + QString(reactionId.c_str())); }), exportable_callback<ConversationSignal::ConversationProfileUpdated>( [this](const std::string& accountId, @@ -970,25 +1020,15 @@ public Q_SLOTS: // METHODS flags); } - uint32_t loadConversationMessages(const QString& accountId, - const QString& conversationId, - const QString& fromId, - const int size) - { - return libjami::loadConversationMessages(accountId.toStdString(), - conversationId.toStdString(), - fromId.toStdString(), - size); - } - uint32_t loadConversationUntil(const QString& accountId, - const QString& conversationId, - const QString& fromId, - const QString& toId) + uint32_t loadConversation(const QString& accountId, + const QString& conversationId, + const QString& fromId, + const int size) { - return libjami::loadConversationUntil(accountId.toStdString(), - conversationId.toStdString(), - fromId.toStdString(), - toId.toStdString()); + return libjami::loadConversation(accountId.toStdString(), + conversationId.toStdString(), + fromId.toStdString(), + size); } void setDefaultModerator(const QString& accountID, const QString& peerURI, const bool& state) @@ -1070,6 +1110,11 @@ public Q_SLOTS: // METHODS convertMap(prefs)); } + void clearCache(const QString& accountId, const QString& conversationId) + { + return libjami::clearCache(accountId.toStdString(), conversationId.toStdString()); + } + uint32_t countInteractions(const QString& accountId, const QString& conversationId, const QString& toId, @@ -1169,9 +1214,24 @@ Q_SIGNALS: // SIGNALS const QString& accountId, const QString& conversationId, const VectorMapStringString& messages); - void messageReceived(const QString& accountId, + void swarmLoaded(uint32_t requestId, + const QString& accountId, + const QString& conversationId, + const VectorSwarmMessage& messages); + void swarmMessageReceived(const QString& accountId, + const QString& conversationId, + const SwarmMessage& message); + void swarmMessageUpdated(const QString& accountId, + const QString& conversationId, + const SwarmMessage& message); + void reactionAdded(const QString& accountId, + const QString& conversationId, + const QString& messageId, + const MapStringString& message); + void reactionRemoved(const QString& accountId, const QString& conversationId, - const MapStringString& message); + const QString& messageId, + const QString& reactionId); void messagesFound(uint32_t requestId, const QString& accountId, const QString& conversationId, diff --git a/src/libclient/typedefs.h b/src/libclient/typedefs.h index 2974f6a86..14a242c37 100644 --- a/src/libclient/typedefs.h +++ b/src/libclient/typedefs.h @@ -72,6 +72,17 @@ struct DataTransferInfo QString mimetype; }; +struct SwarmMessage +{ + QString id; + QString type; + QString linearizedParent; + MapStringString body; + VectorMapStringString reactions; + VectorMapStringString editions; +}; +typedef QVector<SwarmMessage> VectorSwarmMessage; + struct Message { QString from; diff --git a/tests/qml/src/tst_MessageOptions.qml b/tests/qml/src/tst_MessageOptions.qml index 8385c311c..27ac2ad25 100644 --- a/tests/qml/src/tst_MessageOptions.qml +++ b/tests/qml/src/tst_MessageOptions.qml @@ -86,8 +86,8 @@ Item { // Add some emoji reactions (one from current account uri, one from another uri) emojiReactions.reactions = { - "currentAccountUsername": ["ðŸŒ"], - "notCurrentAccountUri": ["🌮"] + "currentAccountUsername": [{"commitId":"hotdog", "body":"ðŸŒ"}], + "notCurrentAccountUri": [{"commitId":"tacos", "body":"🌮"}] }; var optionsPopup = getOptionsPopup(true, getId(), "test", 0, "test"); -- GitLab