mirror of
https://git.jami.net/savoirfairelinux/jami-client-qt.git
synced 2025-08-12 10:45:38 +02:00
chatview: add ability to reply to a specific message
https://git.jami.net/savoirfairelinux/jami-daemon/-/issues/318 Change-Id: I79916422b90c6eb12252c96bb12e209188a81f94
This commit is contained in:
parent
71a4b4a252
commit
6f18afbaac
20 changed files with 441 additions and 37 deletions
2
qml.qrc
2
qml.qrc
|
@ -163,6 +163,7 @@
|
||||||
<file>src/app/mainview/components/FilesToSendDelegate.qml</file>
|
<file>src/app/mainview/components/FilesToSendDelegate.qml</file>
|
||||||
<file>src/app/mainview/components/MessageBar.qml</file>
|
<file>src/app/mainview/components/MessageBar.qml</file>
|
||||||
<file>src/app/mainview/components/FilesToSendContainer.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/commoncomponents/Avatar.qml</file>
|
||||||
<file>src/app/mainview/components/ConversationAvatar.qml</file>
|
<file>src/app/mainview/components/ConversationAvatar.qml</file>
|
||||||
<file>src/app/mainview/components/InvitationView.qml</file>
|
<file>src/app/mainview/components/InvitationView.qml</file>
|
||||||
|
@ -178,6 +179,7 @@
|
||||||
<file>src/app/constant/MsgSeq.qml</file>
|
<file>src/app/constant/MsgSeq.qml</file>
|
||||||
<file>src/app/commoncomponents/SBSContextMenu.qml</file>
|
<file>src/app/commoncomponents/SBSContextMenu.qml</file>
|
||||||
<file>src/app/commoncomponents/SBSMessageBase.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/ReadStatus.qml</file>
|
||||||
<file>src/app/commoncomponents/GeneratedMessageDelegate.qml</file>
|
<file>src/app/commoncomponents/GeneratedMessageDelegate.qml</file>
|
||||||
<file>src/app/commoncomponents/DataTransferMessageDelegate.qml</file>
|
<file>src/app/commoncomponents/DataTransferMessageDelegate.qml</file>
|
||||||
|
|
104
src/app/commoncomponents/ReplyToRow.qml
Normal file
104
src/app/commoncomponents/ReplyToRow.qml
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -29,6 +29,7 @@ ContextMenuAutoLoader {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property string location
|
property string location
|
||||||
|
property string msgId
|
||||||
property string transferName
|
property string transferName
|
||||||
property string transferId
|
property string transferId
|
||||||
|
|
||||||
|
@ -36,6 +37,7 @@ ContextMenuAutoLoader {
|
||||||
GeneralMenuItem {
|
GeneralMenuItem {
|
||||||
id: saveFile
|
id: saveFile
|
||||||
|
|
||||||
|
canTrigger: root.transferId !== ""
|
||||||
itemName: JamiStrings.saveFile
|
itemName: JamiStrings.saveFile
|
||||||
onClicked: {
|
onClicked: {
|
||||||
MessagesAdapter.copyToDownloads(root.transferId, root.transferName)
|
MessagesAdapter.copyToDownloads(root.transferId, root.transferName)
|
||||||
|
@ -44,10 +46,19 @@ ContextMenuAutoLoader {
|
||||||
GeneralMenuItem {
|
GeneralMenuItem {
|
||||||
id: openLocation
|
id: openLocation
|
||||||
|
|
||||||
|
canTrigger: root.transferId !== ""
|
||||||
itemName: JamiStrings.openLocation
|
itemName: JamiStrings.openLocation
|
||||||
onClicked: {
|
onClicked: {
|
||||||
MessagesAdapter.openDirectory(root.location)
|
MessagesAdapter.openDirectory(root.location)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
GeneralMenuItem {
|
||||||
|
id: reply
|
||||||
|
|
||||||
|
itemName: JamiStrings.reply
|
||||||
|
onClicked: {
|
||||||
|
MessagesAdapter.replyToId = root.msgId
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,7 @@ Control {
|
||||||
property alias avatarBlockWidth: avatarBlock.width
|
property alias avatarBlockWidth: avatarBlock.width
|
||||||
property alias innerContent: innerContent
|
property alias innerContent: innerContent
|
||||||
property alias bubble: bubble
|
property alias bubble: bubble
|
||||||
|
property alias selectAnimation: selectAnimation
|
||||||
property real extraHeight: 0
|
property real extraHeight: 0
|
||||||
|
|
||||||
// these MUST be set but we won't use the 'required' keyword yet
|
// these MUST be set but we won't use the 'required' keyword yet
|
||||||
|
@ -44,6 +45,7 @@ Control {
|
||||||
property string transferName
|
property string transferName
|
||||||
property string formattedTime
|
property string formattedTime
|
||||||
property string location
|
property string location
|
||||||
|
property string id: Id
|
||||||
property string hoveredLink
|
property string hoveredLink
|
||||||
property var readers: []
|
property var readers: []
|
||||||
|
|
||||||
|
@ -89,7 +91,6 @@ Control {
|
||||||
Layout.preferredHeight: innerContent.height + root.extraHeight
|
Layout.preferredHeight: innerContent.height + root.extraHeight
|
||||||
Layout.topMargin: (seq === MsgSeq.first || seq === MsgSeq.single) ? 6 : 0
|
Layout.topMargin: (seq === MsgSeq.first || seq === MsgSeq.single) ? 6 : 0
|
||||||
|
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: avatarBlock
|
id: avatarBlock
|
||||||
Layout.preferredWidth: isOutgoing ? 0 : avatar.width + hPadding/3
|
Layout.preferredWidth: isOutgoing ? 0 : avatar.width + hPadding/3
|
||||||
|
@ -100,34 +101,74 @@ Control {
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
width: avatarSize
|
width: avatarSize
|
||||||
height: avatarSize
|
height: avatarSize
|
||||||
imageId: author
|
imageId: root.author
|
||||||
showPresenceIndicator: false
|
showPresenceIndicator: false
|
||||||
mode: Avatar.Mode.Contact
|
mode: Avatar.Mode.Contact
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Item {
|
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: itemMouseArea
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: 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 {
|
Column {
|
||||||
id: innerContent
|
id: innerContent
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
|
||||||
// place actual content here
|
// place actual content here
|
||||||
|
ReplyToRow {}
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageBubble {
|
MessageBubble {
|
||||||
id: bubble
|
id: bubble
|
||||||
z:-1
|
z:-1
|
||||||
out: isOutgoing
|
out: isOutgoing
|
||||||
type: seq
|
type: seq
|
||||||
color: isOutgoing ?
|
function getBaseColor() {
|
||||||
JamiTheme.messageOutBgColor :
|
var baseColor = isOutgoing ? JamiTheme.messageOutBgColor
|
||||||
CurrentConversation.isCoreDialog ? JamiTheme.messageInBgColor : Qt.lighter(CurrentConversation.color, 1.5)
|
: 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
|
radius: msgRadius
|
||||||
anchors.right: isOutgoing ? parent.right : undefined
|
anchors.right: isOutgoing ? parent.right : undefined
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
width: innerContent.childrenRect.width
|
width: innerContent.childrenRect.width
|
||||||
height: innerContent.childrenRect.height + (visible ? root.extraHeight : 0)
|
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 {
|
Item {
|
||||||
|
@ -220,25 +261,9 @@ Control {
|
||||||
SBSContextMenu {
|
SBSContextMenu {
|
||||||
id: ctxMenu
|
id: ctxMenu
|
||||||
|
|
||||||
|
msgId: Id
|
||||||
location: root.location
|
location: root.location
|
||||||
transferId: root.transferId
|
transferId: root.transferId
|
||||||
transferName: root.transferName
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,7 +43,7 @@ SBSMessageBase {
|
||||||
innerContent.children: [
|
innerContent.children: [
|
||||||
TextEdit {
|
TextEdit {
|
||||||
|
|
||||||
padding: JamiTheme.chatviewPadding
|
padding: JamiTheme.preferredMarginSize
|
||||||
anchors.right: isOutgoing ? parent.right : undefined
|
anchors.right: isOutgoing ? parent.right : undefined
|
||||||
|
|
||||||
text: Body
|
text: Body
|
||||||
|
@ -76,6 +76,7 @@ SBSMessageBase {
|
||||||
JamiTheme.chatviewTextColorDark
|
JamiTheme.chatviewTextColorDark
|
||||||
|
|
||||||
TapHandler {
|
TapHandler {
|
||||||
|
enabled: parent.selectedText.length > 0
|
||||||
acceptedButtons: Qt.RightButton
|
acceptedButtons: Qt.RightButton
|
||||||
onTapped: function onTapped(eventPoint) {
|
onTapped: function onTapped(eventPoint) {
|
||||||
ctxMenu.openMenuAt(eventPoint.position)
|
ctxMenu.openMenuAt(eventPoint.position)
|
||||||
|
|
|
@ -719,6 +719,9 @@ Item {
|
||||||
property string leaveVideoMessage: qsTr("Leave video message")
|
property string leaveVideoMessage: qsTr("Leave video message")
|
||||||
property string send: qsTr("Send")
|
property string send: qsTr("Send")
|
||||||
property string remove: qsTr("Remove")
|
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")
|
property string writeTo: qsTr("Write to %1")
|
||||||
|
|
||||||
// Invitation View
|
// Invitation View
|
||||||
|
|
|
@ -334,7 +334,6 @@ Item {
|
||||||
|
|
||||||
// MessageWebView
|
// MessageWebView
|
||||||
property real chatViewHairLineSize: 1
|
property real chatViewHairLineSize: 1
|
||||||
property real chatviewPadding : 16
|
|
||||||
property real chatViewMaximumWidth: 900
|
property real chatViewMaximumWidth: 900
|
||||||
property real chatViewHeaderPreferredHeight: 64
|
property real chatViewHeaderPreferredHeight: 64
|
||||||
property real chatViewHeaderMinimumWidth: 200
|
property real chatViewHeaderMinimumWidth: 200
|
||||||
|
|
|
@ -175,7 +175,8 @@ CurrentConversation::updateErrors(const QString& convId)
|
||||||
} else if (code == 3) {
|
} else if (code == 3) {
|
||||||
newErrors.append(tr("An invalid message was detected"));
|
newErrors.append(tr("An invalid message was detected"));
|
||||||
} else if (code == 4) {
|
} 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 {
|
} else {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -185,6 +186,11 @@ CurrentConversation::updateErrors(const QString& convId)
|
||||||
set_errors(newErrors);
|
set_errors(newErrors);
|
||||||
}
|
}
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
void
|
||||||
|
CurrentConversation::scrollToMsg(const QString& msg)
|
||||||
|
{
|
||||||
|
Q_EMIT scrollTo(msg);
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,10 +54,11 @@ class CurrentConversation final : public QObject
|
||||||
public:
|
public:
|
||||||
explicit CurrentConversation(LRCInstance* lrcInstance, QObject* parent = nullptr);
|
explicit CurrentConversation(LRCInstance* lrcInstance, QObject* parent = nullptr);
|
||||||
~CurrentConversation() = default;
|
~CurrentConversation() = default;
|
||||||
|
Q_INVOKABLE void scrollToMsg(const QString& msgId);
|
||||||
Q_INVOKABLE void showSwarmDetails() const;
|
Q_INVOKABLE void showSwarmDetails() const;
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
|
void scrollTo(const QString& msgId);
|
||||||
void showDetails() const;
|
void showDetails() const;
|
||||||
|
|
||||||
private Q_SLOTS:
|
private Q_SLOTS:
|
||||||
|
|
|
@ -121,6 +121,16 @@ Rectangle {
|
||||||
|
|
||||||
spacing: 0
|
spacing: 0
|
||||||
|
|
||||||
|
ReplyingContainer {
|
||||||
|
id: replyingContainer
|
||||||
|
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
Layout.preferredWidth: footerColumnLayout.width
|
||||||
|
Layout.maximumWidth: JamiTheme.chatViewMaximumWidth
|
||||||
|
Layout.preferredHeight: 36
|
||||||
|
visible: MessagesAdapter.replyToId !== ""
|
||||||
|
}
|
||||||
|
|
||||||
MessageBar {
|
MessageBar {
|
||||||
id: messageBar
|
id: messageBar
|
||||||
|
|
||||||
|
|
|
@ -165,6 +165,19 @@ JamiListView {
|
||||||
Connections {
|
Connections {
|
||||||
target: CurrentConversation
|
target: CurrentConversation
|
||||||
function onIdChanged() { fadeAnimation.start() }
|
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
|
topMargin: 12
|
||||||
|
|
|
@ -333,7 +333,7 @@ Rectangle {
|
||||||
target: MessagesAdapter
|
target: MessagesAdapter
|
||||||
enabled: root.visible
|
enabled: root.visible
|
||||||
|
|
||||||
function onNewInteraction(interactionType) {
|
function onNewInteraction(id, interactionType) {
|
||||||
// Ignore call notifications, as we are in the call.
|
// Ignore call notifications, as we are in the call.
|
||||||
if (interactionType !== Interaction.Type.CALL && !inCallMessageWebViewStack.visible)
|
if (interactionType !== Interaction.Type.CALL && !inCallMessageWebViewStack.visible)
|
||||||
openInCallConversation()
|
openInCallConversation()
|
||||||
|
|
117
src/app/mainview/components/ReplyingContainer.qml
Normal file
117
src/app/mainview/components/ReplyingContainer.qml
Normal file
|
@ -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 = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -52,6 +52,7 @@ MessagesAdapter::MessagesAdapter(AppSettingsManager* settingsManager,
|
||||||
, filteredMsgListModel_(new FilteredMsgListModel(this))
|
, filteredMsgListModel_(new FilteredMsgListModel(this))
|
||||||
{
|
{
|
||||||
connect(lrcInstance_, &LRCInstance::selectedConvUidChanged, [this]() {
|
connect(lrcInstance_, &LRCInstance::selectedConvUidChanged, [this]() {
|
||||||
|
set_replyToId("");
|
||||||
const QString& convId = lrcInstance_->get_selectedConvUid();
|
const QString& convId = lrcInstance_->get_selectedConvUid();
|
||||||
const auto& conversation = lrcInstance_->getConversationFromConvUid(convId);
|
const auto& conversation = lrcInstance_->getConversationFromConvUid(convId);
|
||||||
filteredMsgListModel_->setSourceModel(conversation.interactions.get());
|
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
|
void
|
||||||
MessagesAdapter::connectConversationModel()
|
MessagesAdapter::connectConversationModel()
|
||||||
{
|
{
|
||||||
|
@ -135,7 +153,8 @@ MessagesAdapter::sendMessage(const QString& message)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
const auto convUid = lrcInstance_->get_selectedConvUid();
|
const auto convUid = lrcInstance_->get_selectedConvUid();
|
||||||
lrcInstance_->getCurrentConversationModel()->sendMessage(convUid, message);
|
lrcInstance_->getCurrentConversationModel()->sendMessage(convUid, message, replyToId_);
|
||||||
|
set_replyToId("");
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
qDebug() << "Exception during sendMessage:" << message;
|
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)}};
|
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
|
void
|
||||||
MessagesAdapter::userIsComposing(bool isComposing)
|
MessagesAdapter::userIsComposing(bool isComposing)
|
||||||
{
|
{
|
||||||
|
@ -327,7 +366,7 @@ MessagesAdapter::onNewInteraction(const QString& convUid,
|
||||||
auto& accountInfo = lrcInstance_->getAccountInfo(accountId);
|
auto& accountInfo = lrcInstance_->getAccountInfo(accountId);
|
||||||
auto& convModel = accountInfo.conversationModel;
|
auto& convModel = accountInfo.conversationModel;
|
||||||
convModel->clearUnreadInteractions(convUid);
|
convModel->clearUnreadInteractions(convUid);
|
||||||
Q_EMIT newInteraction(static_cast<int>(interaction.type));
|
Q_EMIT newInteraction(interactionId, static_cast<int>(interaction.type));
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -487,7 +526,9 @@ MessagesAdapter::isLocalImage(const QString& mimename)
|
||||||
QList<QByteArray> supportedFormats = reader.supportedImageFormats();
|
QList<QByteArray> supportedFormats = reader.supportedImageFormats();
|
||||||
auto iterator = std::find_if(supportedFormats.begin(),
|
auto iterator = std::find_if(supportedFormats.begin(),
|
||||||
supportedFormats.end(),
|
supportedFormats.end(),
|
||||||
[fileFormat](QByteArray format) { return format == fileFormat; });
|
[fileFormat](QByteArray format) {
|
||||||
|
return format == fileFormat;
|
||||||
|
});
|
||||||
return {{"isImage", iterator != supportedFormats.end()}};
|
return {{"isImage", iterator != supportedFormats.end()}};
|
||||||
}
|
}
|
||||||
return {{"isImage", false}};
|
return {{"isImage", false}};
|
||||||
|
|
|
@ -58,6 +58,7 @@ class MessagesAdapter final : public QmlAdapterBase
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
QML_RO_PROPERTY(QVariant, messageListModel)
|
QML_RO_PROPERTY(QVariant, messageListModel)
|
||||||
|
QML_PROPERTY(QString, replyToId)
|
||||||
QML_RO_PROPERTY(QList<QString>, currentConvComposingList)
|
QML_RO_PROPERTY(QList<QString>, currentConvComposingList)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
@ -68,7 +69,7 @@ public:
|
||||||
~MessagesAdapter() = default;
|
~MessagesAdapter() = default;
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void newInteraction(int type);
|
void newInteraction(const QString& id, int type);
|
||||||
void newMessageBarPlaceholderText(QString placeholderText);
|
void newMessageBarPlaceholderText(QString placeholderText);
|
||||||
void newFilePasted(QString filePath);
|
void newFilePasted(QString filePath);
|
||||||
void newTextPasted();
|
void newTextPasted();
|
||||||
|
@ -80,6 +81,7 @@ protected:
|
||||||
|
|
||||||
Q_INVOKABLE void setupChatView(const QVariantMap& convInfo);
|
Q_INVOKABLE void setupChatView(const QVariantMap& convInfo);
|
||||||
Q_INVOKABLE void loadMoreMessages();
|
Q_INVOKABLE void loadMoreMessages();
|
||||||
|
Q_INVOKABLE void loadConversationUntil(const QString& to);
|
||||||
Q_INVOKABLE void connectConversationModel();
|
Q_INVOKABLE void connectConversationModel();
|
||||||
Q_INVOKABLE void sendConversationRequest();
|
Q_INVOKABLE void sendConversationRequest();
|
||||||
Q_INVOKABLE void removeConversation(const QString& convUid);
|
Q_INVOKABLE void removeConversation(const QString& convUid);
|
||||||
|
@ -109,8 +111,11 @@ protected:
|
||||||
const QString& msg,
|
const QString& msg,
|
||||||
bool showPreview);
|
bool showPreview);
|
||||||
Q_INVOKABLE void onPaste();
|
Q_INVOKABLE void onPaste();
|
||||||
|
Q_INVOKABLE int getIndexOfMessage(const QString& messageId) const;
|
||||||
Q_INVOKABLE QString getStatusString(int status);
|
Q_INVOKABLE QString getStatusString(int status);
|
||||||
Q_INVOKABLE QVariantMap getTransferStats(const QString& messageId, int);
|
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.
|
// Run corrsponding js functions, c++ to qml.
|
||||||
void setMessagesImageContent(const QString& path, bool isBased64 = false);
|
void setMessagesImageContent(const QString& path, bool isBased64 = false);
|
||||||
|
|
|
@ -313,6 +313,7 @@ public:
|
||||||
* @return id for loading request. -1 if not loaded
|
* @return id for loading request. -1 if not loaded
|
||||||
*/
|
*/
|
||||||
int loadConversationMessages(const QString& conversationId, const int size = 1);
|
int loadConversationMessages(const QString& conversationId, const int size = 1);
|
||||||
|
int loadConversationUntil(const QString& conversationId, const QString& to);
|
||||||
/**
|
/**
|
||||||
* accept request for conversation
|
* accept request for conversation
|
||||||
* @param conversationId conversation's id
|
* @param conversationId conversation's id
|
||||||
|
|
|
@ -1630,6 +1630,25 @@ ConversationModel::loadConversationMessages(const QString& conversationId, const
|
||||||
size);
|
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
|
void
|
||||||
ConversationModel::acceptConversationRequest(const QString& conversationId)
|
ConversationModel::acceptConversationRequest(const QString& conversationId)
|
||||||
{
|
{
|
||||||
|
|
|
@ -171,6 +171,7 @@ MessageListModel::clear()
|
||||||
{
|
{
|
||||||
Q_EMIT beginResetModel();
|
Q_EMIT beginResetModel();
|
||||||
interactions_.clear();
|
interactions_.clear();
|
||||||
|
replyTo_.clear();
|
||||||
Q_EMIT endResetModel();
|
Q_EMIT endResetModel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -289,6 +290,18 @@ MessageListModel::insertMessage(int index, item_t& message)
|
||||||
Q_EMIT beginInsertRows(QModelIndex(), index, index);
|
Q_EMIT beginInsertRows(QModelIndex(), index, index);
|
||||||
interactions_.insert(index, message);
|
interactions_.insert(index, message);
|
||||||
Q_EMIT endInsertRows();
|
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
|
iterator
|
||||||
|
@ -343,6 +356,8 @@ MessageListModel::roleNames() const
|
||||||
QVariant
|
QVariant
|
||||||
MessageListModel::dataForItem(item_t item, int, int role) const
|
MessageListModel::dataForItem(item_t item, int, int role) const
|
||||||
{
|
{
|
||||||
|
auto replyId = item.second.commit["reply-to"];
|
||||||
|
auto repliedMsg = getIndexOfMessage(replyId);
|
||||||
switch (role) {
|
switch (role) {
|
||||||
case Role::Id:
|
case Role::Id:
|
||||||
return QVariant(item.first);
|
return QVariant(item.first);
|
||||||
|
@ -368,6 +383,12 @@ MessageListModel::dataForItem(item_t item, int, int role) const
|
||||||
return QVariant(item.second.commit["uri"]);
|
return QVariant(item.second.commit["uri"]);
|
||||||
case Role::ContactAction:
|
case Role::ContactAction:
|
||||||
return QVariant(item.second.commit["action"]);
|
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:
|
case Role::TransferName:
|
||||||
return QVariant(item.second.commit["displayName"]);
|
return QVariant(item.second.commit["displayName"]);
|
||||||
case Role::Readers:
|
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
|
QVariant
|
||||||
MessageListModel::data(const QModelIndex& index, int role) const
|
MessageListModel::data(const QModelIndex& index, int role) const
|
||||||
{
|
{
|
||||||
|
|
|
@ -44,6 +44,9 @@ struct Info;
|
||||||
X(ActionUri) \
|
X(ActionUri) \
|
||||||
X(LinkPreviewInfo) \
|
X(LinkPreviewInfo) \
|
||||||
X(Linkified) \
|
X(Linkified) \
|
||||||
|
X(ReplyTo) \
|
||||||
|
X(ReplyToBody) \
|
||||||
|
X(ReplyToAuthor) \
|
||||||
X(TransferName) \
|
X(TransferName) \
|
||||||
X(Readers)
|
X(Readers)
|
||||||
|
|
||||||
|
@ -89,7 +92,7 @@ public:
|
||||||
iterator begin();
|
iterator begin();
|
||||||
constIterator begin() const;
|
constIterator begin() const;
|
||||||
reverseIterator rbegin();
|
reverseIterator rbegin();
|
||||||
int size() const;
|
Q_INVOKABLE int size() const;
|
||||||
void clear();
|
void clear();
|
||||||
bool empty() const;
|
bool empty() const;
|
||||||
interaction::Info at(const QString& intId) const;
|
interaction::Info at(const QString& intId) const;
|
||||||
|
@ -102,7 +105,8 @@ public:
|
||||||
void moveMessages(QList<QString> msgIds, const QString& parentId);
|
void moveMessages(QList<QString> msgIds, const QString& parentId);
|
||||||
|
|
||||||
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
|
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;
|
QHash<int, QByteArray> roleNames() const override;
|
||||||
QVariant dataForItem(item_t item, int indexRow, int role = Qt::DisplayRole) const;
|
QVariant dataForItem(item_t item, int indexRow, int role = Qt::DisplayRole) const;
|
||||||
bool contains(const QString& msgId);
|
bool contains(const QString& msgId);
|
||||||
|
@ -132,6 +136,7 @@ private:
|
||||||
// to allow quick access.
|
// to allow quick access.
|
||||||
QMap<QString, QString> lastDisplayedMessageUid_;
|
QMap<QString, QString> lastDisplayedMessageUid_;
|
||||||
QMap<QString, QStringList> messageToReaders_;
|
QMap<QString, QStringList> messageToReaders_;
|
||||||
|
QMap<QString, QStringList> replyTo_;
|
||||||
|
|
||||||
void moveMessage(const QString& msgId, const QString& parentId);
|
void moveMessage(const QString& msgId, const QString& parentId);
|
||||||
void insertMessage(int index, item_t& message);
|
void insertMessage(int index, item_t& message);
|
||||||
|
|
|
@ -1056,6 +1056,16 @@ public Q_SLOTS: // METHODS
|
||||||
fromId.toStdString(),
|
fromId.toStdString(),
|
||||||
size);
|
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)
|
void setDefaultModerator(const QString& accountID, const QString& peerURI, const bool& state)
|
||||||
{
|
{
|
||||||
|
|
Loading…
Add table
Reference in a new issue