From 6f18afbaacf0716d74a8d0129acb4d26a3f90776 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Blin?= <sebastien.blin@savoirfairelinux.com> Date: Tue, 19 Jul 2022 15:57:44 -0400 Subject: [PATCH] chatview: add ability to reply to a specific message https://git.jami.net/savoirfairelinux/jami-daemon/-/issues/318 Change-Id: I79916422b90c6eb12252c96bb12e209188a81f94 --- qml.qrc | 2 + src/app/commoncomponents/ReplyToRow.qml | 104 ++++++++++++++++ src/app/commoncomponents/SBSContextMenu.qml | 11 ++ src/app/commoncomponents/SBSMessageBase.qml | 71 +++++++---- .../commoncomponents/TextMessageDelegate.qml | 3 +- src/app/constant/JamiStrings.qml | 3 + src/app/constant/JamiTheme.qml | 1 - src/app/currentconversation.cpp | 12 +- src/app/currentconversation.h | 3 +- .../mainview/components/ChatViewFooter.qml | 10 ++ .../mainview/components/MessageListView.qml | 13 ++ .../mainview/components/OngoingCallPage.qml | 2 +- .../mainview/components/ReplyingContainer.qml | 117 ++++++++++++++++++ src/app/messagesadapter.cpp | 49 +++++++- src/app/messagesadapter.h | 7 +- src/libclient/api/conversationmodel.h | 1 + src/libclient/conversationmodel.cpp | 19 +++ src/libclient/messagelistmodel.cpp | 31 +++++ src/libclient/messagelistmodel.h | 9 +- .../qtwrapper/configurationmanager_wrap.h | 10 ++ 20 files changed, 441 insertions(+), 37 deletions(-) create mode 100644 src/app/commoncomponents/ReplyToRow.qml create mode 100644 src/app/mainview/components/ReplyingContainer.qml diff --git a/qml.qrc b/qml.qrc index 36590f64e..1d6af2c7d 100644 --- a/qml.qrc +++ b/qml.qrc @@ -163,6 +163,7 @@ <file>src/app/mainview/components/FilesToSendDelegate.qml</file> <file>src/app/mainview/components/MessageBar.qml</file> <file>src/app/mainview/components/FilesToSendContainer.qml</file> + <file>src/app/mainview/components/ReplyingContainer.qml</file> <file>src/app/commoncomponents/Avatar.qml</file> <file>src/app/mainview/components/ConversationAvatar.qml</file> <file>src/app/mainview/components/InvitationView.qml</file> @@ -178,6 +179,7 @@ <file>src/app/constant/MsgSeq.qml</file> <file>src/app/commoncomponents/SBSContextMenu.qml</file> <file>src/app/commoncomponents/SBSMessageBase.qml</file> + <file>src/app/commoncomponents/ReplyToRow.qml</file> <file>src/app/commoncomponents/ReadStatus.qml</file> <file>src/app/commoncomponents/GeneratedMessageDelegate.qml</file> <file>src/app/commoncomponents/DataTransferMessageDelegate.qml</file> diff --git a/src/app/commoncomponents/ReplyToRow.qml b/src/app/commoncomponents/ReplyToRow.qml new file mode 100644 index 000000000..08c6c6b72 --- /dev/null +++ b/src/app/commoncomponents/ReplyToRow.qml @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2022 Savoir-faire Linux Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import net.jami.Adapters 1.1 +import net.jami.Constants 1.1 + +Item { + id: root + anchors.right: isOutgoing ? parent.right : undefined + + visible: ReplyTo !== "" + width: visible ? replyToRow.width : 0 + height: replyToRow.height + replyToRow.anchors.topMargin + + MouseArea { + + z: 2 + anchors.fill: parent + RowLayout { + id: replyToRow + anchors.top: parent.top + anchors.topMargin: JamiTheme.preferredMarginSize / 2 + + property bool isSelf: ReplyToAuthor === CurrentAccount.uri || ReplyToAuthor === "" + + onVisibleChanged: { + if (visible) { + // 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. + MessagesAdapter.loadConversationUntil(ReplyTo) + } + } + + Label { + id: replyTo + + text: JamiStrings.inReplyTo + + color: UtilsAdapter.luma(bubble.color) ? + JamiTheme.chatviewTextColorLight : + JamiTheme.chatviewTextColorDark + font.pointSize: JamiTheme.textFontSize + font.kerning: true + font.bold: true + Layout.leftMargin: JamiTheme.preferredMarginSize + } + + Avatar { + id: avatarReply + + Layout.preferredWidth: JamiTheme.avatarReadReceiptSize + Layout.preferredHeight: JamiTheme.avatarReadReceiptSize + + showPresenceIndicator: false + + imageId: { + if (replyToRow.isSelf) + return CurrentAccount.id + return ReplyToAuthor + } + mode: replyToRow.isSelf ? Avatar.Mode.Account : Avatar.Mode.Contact + } + + Text { + id: body + Layout.maximumWidth: JamiTheme.preferredFieldWidth - JamiTheme.preferredMarginSize + Layout.rightMargin: JamiTheme.preferredMarginSize + + text: ReplyToBody + elide: Text.ElideRight + + color: UtilsAdapter.luma(bubble.color) ? + JamiTheme.chatviewTextColorLight : + JamiTheme.chatviewTextColorDark + font.pointSize: JamiTheme.textFontSize + font.kerning: true + font.bold: true + } + } + + onClicked: function(mouse) { + CurrentConversation.scrollToMsg(ReplyTo) + } + } +} diff --git a/src/app/commoncomponents/SBSContextMenu.qml b/src/app/commoncomponents/SBSContextMenu.qml index 4af744cd4..8c6f0f094 100644 --- a/src/app/commoncomponents/SBSContextMenu.qml +++ b/src/app/commoncomponents/SBSContextMenu.qml @@ -29,6 +29,7 @@ ContextMenuAutoLoader { id: root property string location + property string msgId property string transferName property string transferId @@ -36,6 +37,7 @@ ContextMenuAutoLoader { GeneralMenuItem { id: saveFile + canTrigger: root.transferId !== "" itemName: JamiStrings.saveFile onClicked: { MessagesAdapter.copyToDownloads(root.transferId, root.transferName) @@ -44,10 +46,19 @@ ContextMenuAutoLoader { GeneralMenuItem { id: openLocation + canTrigger: root.transferId !== "" itemName: JamiStrings.openLocation onClicked: { MessagesAdapter.openDirectory(root.location) } + }, + GeneralMenuItem { + id: reply + + itemName: JamiStrings.reply + onClicked: { + MessagesAdapter.replyToId = root.msgId + } } ] diff --git a/src/app/commoncomponents/SBSMessageBase.qml b/src/app/commoncomponents/SBSMessageBase.qml index 825c663f6..b4b075435 100644 --- a/src/app/commoncomponents/SBSMessageBase.qml +++ b/src/app/commoncomponents/SBSMessageBase.qml @@ -32,6 +32,7 @@ Control { property alias avatarBlockWidth: avatarBlock.width property alias innerContent: innerContent property alias bubble: bubble + property alias selectAnimation: selectAnimation property real extraHeight: 0 // these MUST be set but we won't use the 'required' keyword yet @@ -44,6 +45,7 @@ Control { property string transferName property string formattedTime property string location + property string id: Id property string hoveredLink property var readers: [] @@ -89,7 +91,6 @@ Control { Layout.preferredHeight: innerContent.height + root.extraHeight Layout.topMargin: (seq === MsgSeq.first || seq === MsgSeq.single) ? 6 : 0 - Item { id: avatarBlock Layout.preferredWidth: isOutgoing ? 0 : avatar.width + hPadding/3 @@ -100,34 +101,74 @@ Control { anchors.bottom: parent.bottom width: avatarSize height: avatarSize - imageId: author + imageId: root.author showPresenceIndicator: false mode: Avatar.Mode.Contact } } - Item { + + + MouseArea { + id: itemMouseArea + Layout.fillWidth: true Layout.fillHeight: true + + acceptedButtons: Qt.LeftButton | Qt.RightButton + onClicked: function (mouse) { + if (mouse.button === Qt.RightButton + && (transferId !== "" || Type === Interaction.Type.TEXT)) { + // Context Menu for Transfers + ctxMenu.x = mouse.x + ctxMenu.y = mouse.y + ctxMenu.openMenu() + } else if (root.hoveredLink) { + MessagesAdapter.openUrl(root.hoveredLink) + } + } + Column { id: innerContent width: parent.width // place actual content here + ReplyToRow {} } + MessageBubble { id: bubble z:-1 out: isOutgoing type: seq - color: isOutgoing ? - JamiTheme.messageOutBgColor : - CurrentConversation.isCoreDialog ? JamiTheme.messageInBgColor : Qt.lighter(CurrentConversation.color, 1.5) + function getBaseColor() { + var baseColor = isOutgoing ? JamiTheme.messageOutBgColor + : CurrentConversation.isCoreDialog ? + JamiTheme.messageInBgColor : Qt.lighter(CurrentConversation.color, 1.5) + if (Id === MessagesAdapter.replyToId) { + // If we are replying to + return Qt.darker(baseColor, 1.5) + } + return baseColor + } + color: getBaseColor() radius: msgRadius anchors.right: isOutgoing ? parent.right : undefined anchors.top: parent.top width: innerContent.childrenRect.width height: innerContent.childrenRect.height + (visible ? root.extraHeight : 0) } + + SequentialAnimation { + id: selectAnimation + ColorAnimation { + target: bubble; property: "color" + to: Qt.darker(bubble.getBaseColor(), 1.5); duration: 240 + } + ColorAnimation { + target: bubble; property: "color" + to: bubble.getBaseColor(); duration: 240 + } + } } Item { @@ -220,25 +261,9 @@ Control { SBSContextMenu { id: ctxMenu + msgId: Id location: root.location transferId: root.transferId transferName: root.transferName } - - MouseArea { - id: itemMouseArea - anchors.fill: parent - z: -1 - acceptedButtons: Qt.LeftButton | Qt.RightButton - onClicked: function (mouse) { - - if (mouse.button === Qt.RightButton && transferId !== "") { - // Context Menu for Transfers - ctxMenu.x = mouse.x - ctxMenu.y = mouse.y - ctxMenu.openMenu() - } else if (root.hoveredLink) - MessagesAdapter.openUrl(root.hoveredLink) - } - } } diff --git a/src/app/commoncomponents/TextMessageDelegate.qml b/src/app/commoncomponents/TextMessageDelegate.qml index ab98565d4..35b95d2fb 100644 --- a/src/app/commoncomponents/TextMessageDelegate.qml +++ b/src/app/commoncomponents/TextMessageDelegate.qml @@ -43,7 +43,7 @@ SBSMessageBase { innerContent.children: [ TextEdit { - padding: JamiTheme.chatviewPadding + padding: JamiTheme.preferredMarginSize anchors.right: isOutgoing ? parent.right : undefined text: Body @@ -76,6 +76,7 @@ SBSMessageBase { JamiTheme.chatviewTextColorDark TapHandler { + enabled: parent.selectedText.length > 0 acceptedButtons: Qt.RightButton onTapped: function onTapped(eventPoint) { ctxMenu.openMenuAt(eventPoint.position) diff --git a/src/app/constant/JamiStrings.qml b/src/app/constant/JamiStrings.qml index 8690a1e2f..32af891a0 100644 --- a/src/app/constant/JamiStrings.qml +++ b/src/app/constant/JamiStrings.qml @@ -719,6 +719,9 @@ Item { property string leaveVideoMessage: qsTr("Leave video message") property string send: qsTr("Send") property string remove: qsTr("Remove") + property string replyTo: qsTr("Reply to") + property string inReplyTo: qsTr("In reply to") + property string reply: qsTr("Reply") property string writeTo: qsTr("Write to %1") // Invitation View diff --git a/src/app/constant/JamiTheme.qml b/src/app/constant/JamiTheme.qml index 3b745e022..5a1cb4a73 100644 --- a/src/app/constant/JamiTheme.qml +++ b/src/app/constant/JamiTheme.qml @@ -334,7 +334,6 @@ Item { // MessageWebView property real chatViewHairLineSize: 1 - property real chatviewPadding : 16 property real chatViewMaximumWidth: 900 property real chatViewHeaderPreferredHeight: 64 property real chatViewHeaderMinimumWidth: 200 diff --git a/src/app/currentconversation.cpp b/src/app/currentconversation.cpp index 0038bc9cf..42c9a9fcc 100644 --- a/src/app/currentconversation.cpp +++ b/src/app/currentconversation.cpp @@ -167,7 +167,7 @@ CurrentConversation::updateErrors(const QString& convId) auto& convInfo = optConv->get(); QStringList newErrors; QStringList newBackendErr; - for (const auto& [code, error]: convInfo.errors) { + for (const auto& [code, error] : convInfo.errors) { if (code == 1) { newErrors.append(tr("An error occurred while fetching this repository")); } else if (code == 2) { @@ -175,7 +175,8 @@ CurrentConversation::updateErrors(const QString& convId) } else if (code == 3) { newErrors.append(tr("An invalid message was detected")); } else if (code == 4) { - newErrors.append(tr("Not enough authorization for updating conversation's infos")); + newErrors.append( + tr("Not enough authorization for updating conversation's infos")); } else { continue; } @@ -185,6 +186,11 @@ CurrentConversation::updateErrors(const QString& convId) set_errors(newErrors); } } catch (...) { - } } + +void +CurrentConversation::scrollToMsg(const QString& msg) +{ + Q_EMIT scrollTo(msg); +} diff --git a/src/app/currentconversation.h b/src/app/currentconversation.h index 399f19dd4..7ccbf511e 100644 --- a/src/app/currentconversation.h +++ b/src/app/currentconversation.h @@ -54,10 +54,11 @@ class CurrentConversation final : public QObject public: explicit CurrentConversation(LRCInstance* lrcInstance, QObject* parent = nullptr); ~CurrentConversation() = default; - + Q_INVOKABLE void scrollToMsg(const QString& msgId); Q_INVOKABLE void showSwarmDetails() const; Q_SIGNALS: + void scrollTo(const QString& msgId); void showDetails() const; private Q_SLOTS: diff --git a/src/app/mainview/components/ChatViewFooter.qml b/src/app/mainview/components/ChatViewFooter.qml index ae97243d5..25d7a71cf 100644 --- a/src/app/mainview/components/ChatViewFooter.qml +++ b/src/app/mainview/components/ChatViewFooter.qml @@ -121,6 +121,16 @@ Rectangle { spacing: 0 + ReplyingContainer { + id: replyingContainer + + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: footerColumnLayout.width + Layout.maximumWidth: JamiTheme.chatViewMaximumWidth + Layout.preferredHeight: 36 + visible: MessagesAdapter.replyToId !== "" + } + MessageBar { id: messageBar diff --git a/src/app/mainview/components/MessageListView.qml b/src/app/mainview/components/MessageListView.qml index eba5bfd5c..656b9a95d 100644 --- a/src/app/mainview/components/MessageListView.qml +++ b/src/app/mainview/components/MessageListView.qml @@ -165,6 +165,19 @@ JamiListView { Connections { target: CurrentConversation function onIdChanged() { fadeAnimation.start() } + function onScrollTo(id) { + var idx = -1 + for (var i = 1; i < root.count; i++) { + var delegate = root.itemAtIndex(i) + if (delegate && delegate.id === id) { + idx = i + } + } + positionViewAtIndex(idx, ListView.Center) + var delegate = root.itemAtIndex(idx) + if (delegate.selectAnimation) + delegate.selectAnimation.start() + } } topMargin: 12 diff --git a/src/app/mainview/components/OngoingCallPage.qml b/src/app/mainview/components/OngoingCallPage.qml index 72cc19c04..affad4e69 100644 --- a/src/app/mainview/components/OngoingCallPage.qml +++ b/src/app/mainview/components/OngoingCallPage.qml @@ -333,7 +333,7 @@ Rectangle { target: MessagesAdapter enabled: root.visible - function onNewInteraction(interactionType) { + function onNewInteraction(id, interactionType) { // Ignore call notifications, as we are in the call. if (interactionType !== Interaction.Type.CALL && !inCallMessageWebViewStack.visible) openInCallConversation() diff --git a/src/app/mainview/components/ReplyingContainer.qml b/src/app/mainview/components/ReplyingContainer.qml new file mode 100644 index 000000000..0489888b5 --- /dev/null +++ b/src/app/mainview/components/ReplyingContainer.qml @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2022 Savoir-faire Linux Inc. + * Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import net.jami.Adapters 1.1 +import net.jami.Constants 1.1 +import net.jami.Models 1.1 + +import "../../commoncomponents" + +Rectangle { + id: root + + color: JamiTheme.messageOutBgColor + + property var isSelf: false + property var author: { + if (MessagesAdapter.replyToId === "") + return "" + + var author = MessagesAdapter.dataForInteraction(MessagesAdapter.replyToId, MessageList.Author) + isSelf = author === "" || author === undefined + if (isSelf) { + avatar.mode = Avatar.Mode.Account + avatar.imageId = CurrentAccount.id + } else { + avatar.mode = Avatar.Mode.Contact + avatar.imageId = author + } + return isSelf ? CurrentAccount.uri : author + } + + RowLayout { + anchors.fill: parent + spacing: 12 + + RowLayout { + Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + + Label { + id: replyTo + + text: JamiStrings.replyTo + + color: UtilsAdapter.luma(root.color) ? + JamiTheme.chatviewTextColorLight : + JamiTheme.chatviewTextColorDark + font.pointSize: JamiTheme.textFontSize + font.kerning: true + font.bold: true + Layout.leftMargin: JamiTheme.preferredMarginSize + } + + Avatar { + id: avatar + + Layout.preferredWidth: JamiTheme.avatarReadReceiptSize + Layout.preferredHeight: JamiTheme.avatarReadReceiptSize + + showPresenceIndicator: false + + imageId: "" + mode: Avatar.Mode.Account + } + + Label { + id: username + + text: author === CurrentAccount.uri ? + CurrentAccount.bestName + : UtilsAdapter.getBestNameForUri(CurrentAccount.id, author) + + color: UtilsAdapter.luma(root.color) ? + JamiTheme.chatviewTextColorLight : + JamiTheme.chatviewTextColorDark + font.pointSize: JamiTheme.textFontSize + font.kerning: true + font.bold: true + } + } + + + PushButton { + id: closeReply + + Layout.alignment: Qt.AlignVCenter | Qt.AlignRight + Layout.rightMargin: JamiTheme.preferredMarginSize + + preferredSize: 24 + + source: JamiResources.round_close_24dp_svg + + normalColor: JamiTheme.chatviewBgColor + imageColor: JamiTheme.chatviewButtonColor + + onClicked: MessagesAdapter.replyToId = "" + } + } +} diff --git a/src/app/messagesadapter.cpp b/src/app/messagesadapter.cpp index 5ea91cdda..c715b91c7 100644 --- a/src/app/messagesadapter.cpp +++ b/src/app/messagesadapter.cpp @@ -52,6 +52,7 @@ MessagesAdapter::MessagesAdapter(AppSettingsManager* settingsManager, , filteredMsgListModel_(new FilteredMsgListModel(this)) { connect(lrcInstance_, &LRCInstance::selectedConvUidChanged, [this]() { + set_replyToId(""); const QString& convId = lrcInstance_->get_selectedConvUid(); const auto& conversation = lrcInstance_->getConversationFromConvUid(convId); filteredMsgListModel_->setSourceModel(conversation.interactions.get()); @@ -100,6 +101,23 @@ MessagesAdapter::loadMoreMessages() } } +void +MessagesAdapter::loadConversationUntil(const QString& to) +{ + if (auto* model = static_cast<MessageListModel*>(filteredMsgListModel_->sourceModel())) { + 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(); + convModel->loadConversationUntil(convId, to); + } + } + } +} + void MessagesAdapter::connectConversationModel() { @@ -135,7 +153,8 @@ MessagesAdapter::sendMessage(const QString& message) { try { const auto convUid = lrcInstance_->get_selectedConvUid(); - lrcInstance_->getCurrentConversationModel()->sendMessage(convUid, message); + lrcInstance_->getCurrentConversationModel()->sendMessage(convUid, message, replyToId_); + set_replyToId(""); } catch (...) { qDebug() << "Exception during sendMessage:" << message; } @@ -302,6 +321,26 @@ MessagesAdapter::getTransferStats(const QString& msgId, int status) return {{"totalSize", qint64(info.totalSize)}, {"progress", qint64(info.progress)}}; } +QVariant +MessagesAdapter::dataForInteraction(const QString& interactionId, int role) const +{ + if (auto* model = static_cast<MessageListModel*>(filteredMsgListModel_->sourceModel())) { + auto idx = model->indexOfMessage(interactionId); + if (idx != -1) + return model->data(idx, role); + } + return {}; +} + +int +MessagesAdapter::getIndexOfMessage(const QString& interactionId) const +{ + if (auto* model = static_cast<MessageListModel*>(filteredMsgListModel_->sourceModel())) { + return model->indexOfMessage(interactionId); + } + return {}; +} + void MessagesAdapter::userIsComposing(bool isComposing) { @@ -327,7 +366,7 @@ MessagesAdapter::onNewInteraction(const QString& convUid, auto& accountInfo = lrcInstance_->getAccountInfo(accountId); auto& convModel = accountInfo.conversationModel; convModel->clearUnreadInteractions(convUid); - Q_EMIT newInteraction(static_cast<int>(interaction.type)); + Q_EMIT newInteraction(interactionId, static_cast<int>(interaction.type)); } catch (...) { } } @@ -486,8 +525,10 @@ MessagesAdapter::isLocalImage(const QString& mimename) QImageReader reader; QList<QByteArray> supportedFormats = reader.supportedImageFormats(); auto iterator = std::find_if(supportedFormats.begin(), - supportedFormats.end(), - [fileFormat](QByteArray format) { return format == fileFormat; }); + supportedFormats.end(), + [fileFormat](QByteArray format) { + return format == fileFormat; + }); return {{"isImage", iterator != supportedFormats.end()}}; } return {{"isImage", false}}; diff --git a/src/app/messagesadapter.h b/src/app/messagesadapter.h index 2473fcbb2..26afb48a9 100644 --- a/src/app/messagesadapter.h +++ b/src/app/messagesadapter.h @@ -58,6 +58,7 @@ class MessagesAdapter final : public QmlAdapterBase { Q_OBJECT QML_RO_PROPERTY(QVariant, messageListModel) + QML_PROPERTY(QString, replyToId) QML_RO_PROPERTY(QList<QString>, currentConvComposingList) public: @@ -68,7 +69,7 @@ public: ~MessagesAdapter() = default; Q_SIGNALS: - void newInteraction(int type); + void newInteraction(const QString& id, int type); void newMessageBarPlaceholderText(QString placeholderText); void newFilePasted(QString filePath); void newTextPasted(); @@ -80,6 +81,7 @@ protected: Q_INVOKABLE void setupChatView(const QVariantMap& convInfo); Q_INVOKABLE void loadMoreMessages(); + Q_INVOKABLE void loadConversationUntil(const QString& to); Q_INVOKABLE void connectConversationModel(); Q_INVOKABLE void sendConversationRequest(); Q_INVOKABLE void removeConversation(const QString& convUid); @@ -109,8 +111,11 @@ protected: const QString& msg, bool showPreview); Q_INVOKABLE void onPaste(); + Q_INVOKABLE int getIndexOfMessage(const QString& messageId) const; Q_INVOKABLE QString getStatusString(int status); Q_INVOKABLE QVariantMap getTransferStats(const QString& messageId, int); + Q_INVOKABLE QVariant dataForInteraction(const QString& interactionId, + int role = Qt::DisplayRole) const; // Run corrsponding js functions, c++ to qml. void setMessagesImageContent(const QString& path, bool isBased64 = false); diff --git a/src/libclient/api/conversationmodel.h b/src/libclient/api/conversationmodel.h index 7da8d5aa0..f0307d969 100644 --- a/src/libclient/api/conversationmodel.h +++ b/src/libclient/api/conversationmodel.h @@ -313,6 +313,7 @@ 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 diff --git a/src/libclient/conversationmodel.cpp b/src/libclient/conversationmodel.cpp index 278ff26e1..9c7430ef5 100644 --- a/src/libclient/conversationmodel.cpp +++ b/src/libclient/conversationmodel.cpp @@ -1630,6 +1630,25 @@ ConversationModel::loadConversationMessages(const QString& conversationId, const 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); +} + void ConversationModel::acceptConversationRequest(const QString& conversationId) { diff --git a/src/libclient/messagelistmodel.cpp b/src/libclient/messagelistmodel.cpp index ac79134b5..b81c3e617 100644 --- a/src/libclient/messagelistmodel.cpp +++ b/src/libclient/messagelistmodel.cpp @@ -171,6 +171,7 @@ MessageListModel::clear() { Q_EMIT beginResetModel(); interactions_.clear(); + replyTo_.clear(); Q_EMIT endResetModel(); } @@ -289,6 +290,18 @@ MessageListModel::insertMessage(int index, item_t& message) Q_EMIT beginInsertRows(QModelIndex(), index, index); interactions_.insert(index, message); Q_EMIT endInsertRows(); + auto replyId = message.second.commit["reply-to"]; + auto commitId = message.second.commit["id"]; + if (!replyId.isEmpty()) + replyTo_[replyId].append(commitId); + for (const auto& msgId : replyTo_[commitId]) { + int index = getIndexOfMessage(msgId); + if (index == -1) + continue; + QModelIndex modelIndex = QAbstractListModel::index(index, 0); + Q_EMIT dataChanged(modelIndex, modelIndex, {Role::ReplyToAuthor}); + Q_EMIT dataChanged(modelIndex, modelIndex, {Role::ReplyToBody}); + } } iterator @@ -343,6 +356,8 @@ MessageListModel::roleNames() const QVariant MessageListModel::dataForItem(item_t item, int, int role) const { + auto replyId = item.second.commit["reply-to"]; + auto repliedMsg = getIndexOfMessage(replyId); switch (role) { case Role::Id: return QVariant(item.first); @@ -368,6 +383,12 @@ MessageListModel::dataForItem(item_t item, int, int role) const return QVariant(item.second.commit["uri"]); case Role::ContactAction: return QVariant(item.second.commit["action"]); + case Role::ReplyTo: + return QVariant(replyId); + case Role::ReplyToAuthor: + return repliedMsg == -1 ? QVariant("") : QVariant(data(repliedMsg, Role::Author)); + case Role::ReplyToBody: + return repliedMsg == -1 ? QVariant("") : QVariant(data(repliedMsg, Role::Body)); case Role::TransferName: return QVariant(item.second.commit["displayName"]); case Role::Readers: @@ -377,6 +398,16 @@ MessageListModel::dataForItem(item_t item, int, int role) const } } +QVariant +MessageListModel::data(int idx, int role) const +{ + QModelIndex index = QAbstractListModel::index(idx, 0); + if (!index.isValid() || index.row() < 0 || index.row() >= rowCount()) { + return {}; + } + return dataForItem(interactions_.at(index.row()), index.row(), role); +} + QVariant MessageListModel::data(const QModelIndex& index, int role) const { diff --git a/src/libclient/messagelistmodel.h b/src/libclient/messagelistmodel.h index 3d36e577c..d4137beec 100644 --- a/src/libclient/messagelistmodel.h +++ b/src/libclient/messagelistmodel.h @@ -44,6 +44,9 @@ struct Info; X(ActionUri) \ X(LinkPreviewInfo) \ X(Linkified) \ + X(ReplyTo) \ + X(ReplyToBody) \ + X(ReplyToAuthor) \ X(TransferName) \ X(Readers) @@ -89,7 +92,7 @@ public: iterator begin(); constIterator begin() const; reverseIterator rbegin(); - int size() const; + Q_INVOKABLE int size() const; void clear(); bool empty() const; interaction::Info at(const QString& intId) const; @@ -102,7 +105,8 @@ public: void moveMessages(QList<QString> msgIds, const QString& parentId); int rowCount(const QModelIndex& parent = QModelIndex()) const override; - virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const; + Q_INVOKABLE virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const; + Q_INVOKABLE virtual QVariant data(int idx, int role = Qt::DisplayRole) const; QHash<int, QByteArray> roleNames() const override; QVariant dataForItem(item_t item, int indexRow, int role = Qt::DisplayRole) const; bool contains(const QString& msgId); @@ -132,6 +136,7 @@ private: // to allow quick access. QMap<QString, QString> lastDisplayedMessageUid_; QMap<QString, QStringList> messageToReaders_; + QMap<QString, QStringList> replyTo_; void moveMessage(const QString& msgId, const QString& parentId); void insertMessage(int index, item_t& message); diff --git a/src/libclient/qtwrapper/configurationmanager_wrap.h b/src/libclient/qtwrapper/configurationmanager_wrap.h index ad287fc94..ae1093b29 100644 --- a/src/libclient/qtwrapper/configurationmanager_wrap.h +++ b/src/libclient/qtwrapper/configurationmanager_wrap.h @@ -1056,6 +1056,16 @@ public Q_SLOTS: // METHODS fromId.toStdString(), size); } + uint32_t loadConversationUntil(const QString& accountId, + const QString& conversationId, + const QString& fromId, + const QString& toId) + { + return DRing::loadConversationUntil(accountId.toStdString(), + conversationId.toStdString(), + fromId.toStdString(), + toId.toStdString()); + } void setDefaultModerator(const QString& accountID, const QString& peerURI, const bool& state) { -- GitLab