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