1
0
Fork 0
mirror of https://git.jami.net/savoirfairelinux/jami-client-qt.git synced 2024-10-28 00:39:36 +01:00

share message: add share feature

A share feature have been requested to share
to contacts pictures or text received from other contacts
you can access it by hovering the message you want to share

Gitlab: #1830
Change-Id: I2555433417867371161f927e9fc78bb47fec68d3
This commit is contained in:
aeberhardt 2024-08-30 15:04:11 -04:00 committed by Alexandre Eberhardt
parent 59f3aa7c44
commit 31bd0392da
7 changed files with 515 additions and 17 deletions

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
<path d="M17.1,15.7c-0.8,0-1.5,0.3-2.1,0.8l-5.3-3.3C9.9,12.8,10,12.4,10,12c0-0.4-0.1-0.8-0.3-1.2l5.3-3.3c0.6,0.5,1.3,0.8,2.1,0.8
c1.7,0,3.1-1.4,3.1-3.1S18.9,2,17.1,2C15.4,2,14,3.4,14,5.1c0,0.4,0.1,0.8,0.3,1.2L8.9,9.6C8.3,9.1,7.6,8.9,6.9,8.9
c-1.7,0-3.1,1.4-3.1,3.1s1.4,3.1,3.1,3.1c0.8,0,1.5-0.3,2.1-0.8l5.3,3.3C14.1,18,14,18.4,14,18.9c0,1.7,1.4,3.1,3.1,3.1
c1.7,0,3.1-1.4,3.1-3.1S18.9,15.7,17.1,15.7z M17.1,20.6c-1,0-1.8-0.8-1.8-1.8s0.8-1.8,1.8-1.8c1,0,1.8,0.8,1.8,1.8
S18.1,20.6,17.1,20.6z M17.1,3.4c1,0,1.8,0.8,1.8,1.8s-0.8,1.8-1.8,1.8c-1,0-1.8-0.8-1.8-1.8S16.2,3.4,17.1,3.4z M6.9,13.8
c-1,0-1.8-0.8-1.8-1.8s0.8-1.8,1.8-1.8S8.6,11,8.6,12S7.8,13.8,6.9,13.8z"/>
</svg>

After

Width:  |  Height:  |  Size: 1,019 B

View file

@ -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
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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();
}
}

View file

@ -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();
}
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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;
}
}

View file

@ -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;
}

View file

@ -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);