diff --git a/resources/icons/share_black_24dp.svg b/resources/icons/share_black_24dp.svg
new file mode 100644
index 00000000..d41fc300
--- /dev/null
+++ b/resources/icons/share_black_24dp.svg
@@ -0,0 +1,11 @@
+
+
+
diff --git a/src/app/commoncomponents/SBSMessageBase.qml b/src/app/commoncomponents/SBSMessageBase.qml
index a57b1ddb..15d7eeeb 100644
--- a/src/app/commoncomponents/SBSMessageBase.qml
+++ b/src/app/commoncomponents/SBSMessageBase.qml
@@ -278,7 +278,7 @@ Control {
anchors.right: isOutgoing ? bubble.left : undefined
anchors.left: !isOutgoing ? bubble.right : undefined
- width: JamiTheme.emojiPushButtonSize * 2
+ width: JamiTheme.emojiPushButtonSize * 4
height: JamiTheme.emojiPushButtonSize
anchors.verticalCenter: bubble.verticalCenter
@@ -299,24 +299,24 @@ Control {
anchors.verticalCenter: parent.verticalCenter
anchors.right: isOutgoing ? optionButtonItem.right : undefined
anchors.left: !isOutgoing ? optionButtonItem.left : undefined
- visible: CurrentAccount.type !== Profile.Type.SIP && root.type !== Interaction.Type.CALL && Body !== "" && (bubbleArea.bubbleHovered || hovered || reply.hovered || bgHandler.hovered)
+ visible: CurrentAccount.type !== Profile.Type.SIP && root.type !== Interaction.Type.CALL && Body !== "" && (bubbleArea.bubbleHovered || hovered || reply.hovered || share.hovered || bgHandler.hovered)
source: JamiResources.more_vert_24dp_svg
- width: optionButtonItem.width / 2
+ width: optionButtonItem.width / 4
height: optionButtonItem.height
circled: false
property bool isOpen: false
property var obj: undefined
- function bind() {
+ function setBindings() {
more.isOpen = false;
- visible = Qt.binding(() => CurrentAccount.type !== Profile.Type.SIP && root.type !== Interaction.Type.CALL && Body !== "" && (bubbleArea.bubbleHovered || hovered || reply.hovered || bgHandler.hovered));
+ visible = Qt.binding(() => CurrentAccount.type !== Profile.Type.SIP && root.type !== Interaction.Type.CALL && Body !== "" && (bubbleArea.bubbleHovered || hovered || reply.hovered || share.hovered || bgHandler.hovered));
imageColor = Qt.binding(() => hovered ? JamiTheme.chatViewFooterImgHoverColor : JamiTheme.chatViewFooterImgColor);
normalColor = Qt.binding(() => JamiTheme.primaryBackgroundColor);
}
onClicked: {
if (more.isOpen) {
- more.bind();
+ more.setBindings();
obj.close();
} else {
var component = Qt.createComponent("qrc:/commoncomponents/ShowMoreMenu.qml");
@@ -332,7 +332,7 @@ Control {
});
obj.open();
more.isOpen = true;
- visible = true;
+ visible = true; // the button stay visible as long the popup is open even if it's not hovered
imageColor = JamiTheme.chatViewFooterImgHoverColor;
normalColor = JamiTheme.hoveredButtonColor;
}
@@ -348,19 +348,75 @@ Control {
normalColor: JamiTheme.primaryBackgroundColor
toolTipText: JamiStrings.reply
source: JamiResources.reply_black_24dp_svg
- width: optionButtonItem.width / 2
+ width: optionButtonItem.width / 4
height: optionButtonItem.height
anchors.verticalCenter: parent.verticalCenter
anchors.rightMargin: 5
anchors.right: isOutgoing ? more.left : undefined
anchors.left: !isOutgoing ? more.right : undefined
- visible: CurrentAccount.type !== Profile.Type.SIP && root.type !== Interaction.Type.CALL && Body !== "" && (bubbleArea.bubbleHovered || hovered || more.hovered || bgHandler.hovered)
+ visible: CurrentAccount.type !== Profile.Type.SIP && root.type !== Interaction.Type.CALL && Body !== "" && (bubbleArea.bubbleHovered || hovered || more.hovered || share.hovered || bgHandler.hovered)
onClicked: {
MessagesAdapter.editId = "";
MessagesAdapter.replyToId = Id;
}
}
+
+ PushButton {
+ id: share
+ objectName: "share"
+
+ circled: false
+ imageColor: hovered ? JamiTheme.chatViewFooterImgHoverColor : JamiTheme.chatViewFooterImgColor
+ normalColor: JamiTheme.primaryBackgroundColor
+ toolTipText: JamiStrings.share
+ source: JamiResources.share_black_24dp_svg
+
+ width: optionButtonItem.width / 4
+ height: optionButtonItem.height
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.rightMargin: 5
+ anchors.right: isOutgoing ? reply.left : undefined
+ anchors.left: !isOutgoing ? reply.right : undefined
+ visible: CurrentAccount.type !== Profile.Type.SIP && root.type !== Interaction.Type.CALL && Body !== "" && (bubbleArea.bubbleHovered || hovered || reply.hovered || more.hovered || bgHandler.hovered)
+ property bool isOpen: false
+ property var obj: undefined
+
+ function setBindings() { // when the popup is closed, setBindings is called to reset the icon's visual settings
+ share.isOpen = false;
+ visible = Qt.binding(() => CurrentAccount.type !== Profile.Type.SIP && root.type !== Interaction.Type.CALL && Body !== "" && (bubbleArea.bubbleHovered || hovered || reply.hovered || more.hovered || bgHandler.hovered));
+ imageColor = Qt.binding(() => hovered ? JamiTheme.chatViewFooterImgHoverColor : JamiTheme.chatViewFooterImgColor);
+ normalColor = Qt.binding(() => JamiTheme.primaryBackgroundColor);
+ }
+
+ onClicked: {
+ if (share.isOpen) {
+ share.setBindings();
+ obj.close();
+ } else {
+ if (root.type === 2 || root.type === 5) {
+ // 2=TEXT and 5=DATA_TRANSFER (any kind of file) defined in interaction.h
+ var component = Qt.createComponent("qrc:/commoncomponents/ShareMessageMenu.qml");
+ obj = component.createObject(share, {
+ "isOutgoing": isOutgoing,
+ "msgId": Id,
+ "msgBody": Body,
+ "type": root.type,
+ "transferName": TransferName,
+ "msgBubble": bubble,
+ "listView": listView,
+ "author": UtilsAdapter.getBestNameForUri(CurrentAccount.id, Author),
+ "formattedTime": formattedTime
+ });
+ obj.open();
+ share.isOpen = true;
+ visible = true; // the PushButton stay visible as long the popup is open even if it's not hovered
+ imageColor = JamiTheme.chatViewFooterImgHoverColor;
+ normalColor = JamiTheme.hoveredButtonColor;
+ }
+ }
+ }
+ }
}
MessageBubble {
@@ -382,11 +438,7 @@ Control {
property bool bubbleHovered
property string imgSource
- width: (root.type === Interaction.Type.TEXT || isDeleted ?
- root.textContentWidth + (IsEmojiOnly || root.bigMsg ?
- 0
- : root.timeWidth + root.editedWidth)
- : innerContent.childrenRect.width)
+ width: (root.type === Interaction.Type.TEXT || isDeleted ? root.textContentWidth + (IsEmojiOnly || root.bigMsg ? 0 : root.timeWidth + root.editedWidth) : innerContent.childrenRect.width)
height: innerContent.childrenRect.height + (visible ? root.extraHeight : 0) + (root.bigMsg ? 15 : 0)
HoverHandler {
@@ -470,6 +522,11 @@ Control {
MessagesAdapter.openUrl(root.hoveredLink);
}
}
+
+ onDoubleClicked: {
+ MessagesAdapter.editId = "";
+ MessagesAdapter.replyToId = Id;
+ }
property bool bubbleHovered: containsMouse || textHovered
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
}
diff --git a/src/app/commoncomponents/ShareMessageMenu.qml b/src/app/commoncomponents/ShareMessageMenu.qml
new file mode 100644
index 00000000..72ee5d9f
--- /dev/null
+++ b/src/app/commoncomponents/ShareMessageMenu.qml
@@ -0,0 +1,264 @@
+/*
+ * Copyright (C) 2024 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 .
+ */
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Qt5Compat.GraphicalEffects
+import net.jami.Constants 1.1
+import net.jami.Models 1.1
+import net.jami.Adapters 1.1
+import SortFilterProxyModel 0.2
+import "contextmenu"
+import "../commoncomponents"
+import "../mainview/components"
+
+BaseContextMenu {
+ id: mainMenu
+
+ height: 330 + Math.min(messageInput.height, textareaMaxHeight)
+ width: 400
+
+ required property string msgId
+ required property string msgBody
+ required property bool isOutgoing
+ required property int type
+ required property string transferName
+ required property Item msgBubble
+ required property ListView listView
+ required property string author
+ required property string formattedTime
+
+ property var selectedUids: []
+ property string shareToId: msgId
+ property string fileLink: msgBody
+ property int textareaMaxHeight: 350
+ function xPosition(width) {
+ // Use the width at function scope to retrigger property evaluation.
+ const listViewWidth = listView.width;
+ const parentX = parent.x;
+ if (isOutgoing) {
+ return parentX - width - 20;
+ } else {
+ return parentX + 20;
+ }
+ }
+
+ x: xPosition(width)
+ y: parent.y
+
+ function xPositionProvider(width) {
+ // Use the width at function scope to retrigger property evaluation.
+ const listViewWidth = listView.width;
+ if (isOutgoing) {
+ return -5 - width;
+ } else {
+ const rightMargin = listViewWidth - (msgBubble.x + width);
+ return width > rightMargin + 35 ? -5 - width : 35;
+ }
+ }
+ function yPositionProvider(height) {
+ const topOffset = msgBubble.mapToItem(listView, 0, 0).y;
+ const listViewHeight = listView.height;
+ const bottomMargin = listViewHeight - height - topOffset;
+ if (bottomMargin < 0 || (topOffset < 0 && topOffset + height > 0)) {
+ return 30 - height;
+ } else {
+ return 0;
+ }
+ }
+
+ SortFilterProxyModel {
+ id: shareConvProxyModel
+
+ sourceModel: ConversationsAdapter.convListProxyModel
+ filterCaseSensitivity: Qt.CaseInsensitive
+ }
+
+ Rectangle {
+ id: header
+
+ width: parent.width
+ height: 0
+ }
+
+ Rectangle {
+ id: sendButton
+
+ height: JamiTheme.chatViewFooterButtonSize
+ anchors.right: parent.right
+ anchors.rightMargin: 10
+ anchors.topMargin: 10
+ anchors.top: header.bottom
+ color: JamiTheme.transparentColor
+
+ PushButton {
+ id: shareMessageButton
+
+ height: JamiTheme.chatViewFooterButtonSize
+ width: scale * JamiTheme.chatViewFooterButtonSize
+ anchors.right: parent.right
+
+ visible: true
+
+ radius: JamiTheme.chatViewFooterButtonRadius
+ preferredSize: JamiTheme.chatViewFooterButtonIconSize - 6
+ imageContainerWidth: 25
+ imageContainerHeight: 25
+
+ toolTipText: JamiStrings.share
+
+ mirror: UtilsAdapter.isRTL
+
+ source: JamiResources.send_black_24dp_svg
+
+ hoverEnabled: enabled
+ normalColor: enabled ? JamiTheme.chatViewFooterSendButtonColor : JamiTheme.chatViewFooterSendButtonDisableColor
+ imageColor: enabled ? JamiTheme.chatViewFooterSendButtonImgColor : JamiTheme.chatViewFooterSendButtonImgColorDisable
+ hoveredColor: JamiTheme.buttonTintedBlueHovered
+ pressedColor: hoveredColor
+
+ opacity: 1
+ scale: opacity
+
+ MouseArea {
+ anchors.fill: parent
+
+ onClicked: {
+ var selectedContacts = mainMenu.selectedUids;
+ var hasText = messageInput.text && selectedContacts.length > 0;
+ function sendMessageOrFile(uid) {
+ if (Type === 2) {
+ // 2=TEXT and 5=DATA_TRANSFER (any kind of file) defined in interaction.h
+ MessagesAdapter.sendMessageToUid(msgBody, uid);
+ } else {
+ MessagesAdapter.sendFileToUid(fileLink, uid);
+ }
+ }
+ for (var i = 0; i < selectedContacts.length; i++) {
+ var uid = selectedContacts[i];
+ sendMessageOrFile(uid);
+ if (hasText) {
+ MessagesAdapter.sendMessageToUid(messageInput.text, uid);
+ }
+ }
+ messageInput.text = "";
+ mainMenu.destroy();
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ id: searchConv
+
+ height: 300
+ width: parent.width
+ anchors.top: header.bottom
+ anchors.topMargin: 10
+
+ property int type: ContactList.CONVERSATION
+
+ color: JamiTheme.transparentColor
+ ColumnLayout {
+ id: contactPickerPopupRectColumnLayout
+
+ anchors.fill: parent
+ Searchbar {
+ id: contactPickerContactSearchBar
+
+ width: parent.width - 20 - JamiTheme.chatViewFooterButtonSize
+ anchors.leftMargin: 10
+ Layout.preferredHeight: 35
+ placeHolderText: "Share to..."
+ onSearchBarTextChanged: function (text) {
+ shareConvProxyModel.filterRole = shareConvProxyModel.roleForName("Title");
+ shareConvProxyModel.filterPattern = text;
+ }
+ }
+ JamiListView {
+ id: contactPickerListView
+
+ Layout.alignment: Qt.AlignCenter
+ Layout.fillWidth: true
+ Layout.preferredHeight: 255
+ Layout.bottomMargin: JamiTheme.preferredMarginSize
+ Layout.topMargin: 5
+
+ model: shareConvProxyModel
+
+ delegate: ConversationPickerItemDelegate {
+ id: conversationDelegate
+ }
+ }
+ }
+ }
+
+ Flickable {
+ id: messageInputContainer
+
+ height: Math.min(contentHeight, mainMenu.textareaMaxHeight)
+ width: parent.width - 20
+ contentHeight: messageInput.height
+ anchors.left: parent.left
+ anchors.leftMargin: 10
+ anchors.rightMargin: 10
+ anchors.topMargin: 10
+ anchors.top: searchConv.bottom
+
+ flickableDirection: Flickable.VerticalFlick
+ clip: true
+
+ ScrollBar.vertical: JamiScrollBar {
+ policy: ScrollBar.AsNeeded
+ }
+
+ onContentHeightChanged: {
+ if (contentHeight > height) {
+ contentY = contentHeight - height;
+ }
+ }
+
+ TextArea {
+ id: messageInput
+
+ height: contentHeight + 12
+ width: parent.width
+ placeholderText: "Add a comment"
+ placeholderTextColor: JamiTheme.messageBarPlaceholderTextColor
+ font.pointSize: JamiTheme.textFontSize + 2
+ color: JamiTheme.textColor
+ wrapMode: Text.WordWrap
+
+ background: Rectangle {
+ color: JamiTheme.transparentColor
+ radius: 5
+ border.color: JamiTheme.chatViewFooterRectangleBorderColor
+ border.width: 2
+ }
+ }
+ }
+
+ // destroy() and setBindings() are needed to unselect the share icon from SBSMessageBase
+
+ onAboutToHide: {
+ mainMenu.destroy();
+ }
+
+ Component.onDestruction: {
+ parent.setBindings();
+ }
+}
diff --git a/src/app/commoncomponents/ShowMoreMenu.qml b/src/app/commoncomponents/ShowMoreMenu.qml
index 26c4cdb8..0a8f3f9c 100644
--- a/src/app/commoncomponents/ShowMoreMenu.qml
+++ b/src/app/commoncomponents/ShowMoreMenu.qml
@@ -198,11 +198,13 @@ BaseContextMenu {
root.loadMenuItems(menuItems);
}
+ // destroy() and setBindings() are needed to unselect the share icon from SBSMessageBase
+
onAboutToHide: {
root.destroy();
}
Component.onDestruction: {
- parent.bind();
+ parent.setBindings();
}
}
diff --git a/src/app/mainview/components/ConversationPickerItemDelegate.qml b/src/app/mainview/components/ConversationPickerItemDelegate.qml
new file mode 100644
index 00000000..300a99a3
--- /dev/null
+++ b/src/app/mainview/components/ConversationPickerItemDelegate.qml
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2024 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 .
+ */
+
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Qt5Compat.GraphicalEffects
+import net.jami.Adapters 1.1
+import net.jami.Constants 1.1
+import net.jami.Enums 1.1
+import net.jami.Models 1.1
+import "../../commoncomponents"
+
+ItemDelegate {
+ id: root
+
+ width: ListView.view.width
+ height: JamiTheme.smartListItemHeight
+
+ RowLayout {
+ anchors.fill: parent
+ anchors.leftMargin: 15
+ anchors.rightMargin: 15
+ spacing: 10
+
+ ConversationAvatar {
+ id: avatar
+ objectName: "smartlistItemDelegateAvatar"
+
+ imageId: UID
+ presenceStatus: Presence
+ showPresenceIndicator: Presence !== undefined ? Presence : false
+
+ Layout.preferredWidth: JamiTheme.smartListAvatarSize
+ Layout.preferredHeight: JamiTheme.smartListAvatarSize
+
+ Rectangle {
+ id: overlayHighlighted
+ visible: highlighted
+
+ anchors.fill: parent
+ color: Qt.rgba(0, 0, 0, 0.5)
+ radius: JamiTheme.smartListAvatarSize / 2
+
+ Image {
+ id: highlightedImage
+
+ width: JamiTheme.smartListAvatarSize / 2
+ height: JamiTheme.smartListAvatarSize / 2
+ anchors.centerIn: parent
+
+ layer {
+ enabled: true
+ effect: ColorOverlay {
+ color: "white"
+ }
+ }
+ source: JamiResources.check_black_24dp_svg
+ }
+ }
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ spacing: 0
+
+ // best name
+ Text {
+ Layout.fillWidth: true
+ Layout.minimumHeight: 20
+ Layout.alignment: Qt.AlignVCenter
+ horizontalAlignment: Text.AlignLeft
+ verticalAlignment: Text.AlignVCenter
+ elide: Text.ElideMiddle
+ text: Title === undefined ? "" : Title
+ textFormat: TextEdit.PlainText
+ font.pointSize: JamiTheme.mediumFontSize
+ font.weight: UnreadMessagesCount ? Font.Bold : Font.Normal
+ color: JamiTheme.textColor
+ }
+
+ Text {
+ Layout.fillWidth: true
+ Layout.minimumHeight: 20
+ Layout.alignment: Qt.AlignVCenter
+ text: JamiStrings.blocked
+ textFormat: TextEdit.PlainText
+ visible: IsBanned
+ font.pointSize: JamiTheme.mediumFontSize
+ font.weight: Font.Bold
+ color: JamiTheme.textColor
+ }
+ }
+
+ Accessible.role: Accessible.Button
+ Accessible.name: Title === undefined ? "" : Title
+ }
+
+ background: Rectangle {
+ color: {
+ if (root.pressed || root.highlighted)
+ return JamiTheme.smartListSelectedColor;
+ else if (root.hovered)
+ return JamiTheme.smartListHoveredColor;
+ else
+ return "transparent";
+ }
+ }
+
+ highlighted: {
+ return mainMenu.selectedUids.includes(UID);
+ }
+
+ onClicked: {
+ const currentSelectedUids = mainMenu.selectedUids;
+ if (currentSelectedUids.includes(UID)) {
+ mainMenu.selectedUids = currentSelectedUids.filter(uid => uid !== UID);
+ } else {
+ mainMenu.selectedUids = currentSelectedUids.concat(UID);
+ }
+ return;
+ }
+}
diff --git a/src/app/messagesadapter.cpp b/src/app/messagesadapter.cpp
index 037dcd7d..43afc61a 100644
--- a/src/app/messagesadapter.cpp
+++ b/src/app/messagesadapter.cpp
@@ -165,6 +165,16 @@ MessagesAdapter::sendMessage(const QString& message)
}
}
+void
+MessagesAdapter::sendMessageToUid(const QString& message, const QString& convUid)
+{
+ try {
+ lrcInstance_->getCurrentConversationModel()->sendMessage(convUid, message, replyToId_);
+ } catch (...) {
+ qDebug() << "Exception during sendMessage:" << message;
+ }
+}
+
void
MessagesAdapter::editMessage(const QString& convId, const QString& newBody, const QString& messageId)
{
@@ -221,6 +231,21 @@ MessagesAdapter::sendFile(const QString& message)
}
}
+void
+MessagesAdapter::sendFileToUid(const QString& message, const QString& convUid)
+{
+ QFileInfo fi(message);
+ QString fileName = fi.fileName();
+ try {
+ lrcInstance_->getCurrentConversationModel()->sendFile(convUid,
+ message,
+ fileName,
+ replyToId_);
+ } catch (...) {
+ qDebug() << "Exception during sendFile";
+ }
+}
+
void
MessagesAdapter::joinCall(const QString& uri,
const QString& deviceId,
@@ -312,8 +337,7 @@ MessagesAdapter::onPaste()
QString path = QDir::temp().filePath(fileName);
if (!pixmap.save(path, "PNG")) {
- qDebug().noquote() << "Errors during QPixmap save"
- << "\n";
+ qDebug().noquote() << "Errors during QPixmap save" << "\n";
return;
}
diff --git a/src/app/messagesadapter.h b/src/app/messagesadapter.h
index c35f3f31..806bffb5 100644
--- a/src/app/messagesadapter.h
+++ b/src/app/messagesadapter.h
@@ -126,6 +126,7 @@ public:
Q_INVOKABLE void unbanContact(int index);
Q_INVOKABLE void unbanConversation(const QString& convUid);
Q_INVOKABLE void sendMessage(const QString& message);
+ Q_INVOKABLE void sendMessageToUid(const QString& message, const QString& convUid);
Q_INVOKABLE void editMessage(const QString& convId,
const QString& newBody,
const QString& messageId = "");
@@ -136,6 +137,7 @@ public:
const QString& emoji,
const QString& messageId);
Q_INVOKABLE void sendFile(const QString& message);
+ Q_INVOKABLE void sendFileToUid(const QString& message, const QString& convUid);
Q_INVOKABLE void acceptFile(const QString& arg);
Q_INVOKABLE void cancelFile(const QString& arg);
Q_INVOKABLE void openUrl(const QString& url);