From 06ab19f213db8b350e8d150f4de82d96dd7b3af3 Mon Sep 17 00:00:00 2001 From: Nicolas Vengeon <nicolas.vengeon@savoirfairelinux.com> Date: Fri, 3 Feb 2023 11:29:37 -0500 Subject: [PATCH] Feature: search messages Change-Id: Ia458e2e6ee183cad9d0418af0dbbbcd990f14281 GitLab: #918 --- resources/icons/search.svg | 1 + src/app/constant/JamiStrings.qml | 6 + src/app/constant/JamiTheme.qml | 7 +- src/app/mainview/components/ChatView.qml | 57 ++++-- .../mainview/components/ChatViewHeader.qml | 140 ++++++++------ .../mainview/components/ContactSearchBar.qml | 4 +- .../components/DocumentsScrollview.qml | 26 ++- src/app/mainview/components/FilePreview.qml | 4 +- src/app/mainview/components/MediaPreview.qml | 4 +- .../components/MessagesResearchPanel.qml | 109 +++++++++++ .../components/MessagesResearchView.qml | 163 ++++++++++++++++ src/app/mainview/components/Searchbar.qml | 182 ++++++++++++++++++ src/app/messagesadapter.cpp | 44 +++-- src/app/messagesadapter.h | 8 +- src/libclient/api/conversationmodel.h | 13 +- src/libclient/conversationmodel.cpp | 63 +++--- 16 files changed, 690 insertions(+), 141 deletions(-) create mode 100644 resources/icons/search.svg create mode 100644 src/app/mainview/components/MessagesResearchPanel.qml create mode 100644 src/app/mainview/components/MessagesResearchView.qml create mode 100644 src/app/mainview/components/Searchbar.qml diff --git a/resources/icons/search.svg b/resources/icons/search.svg new file mode 100644 index 000000000..fa7501755 --- /dev/null +++ b/resources/icons/search.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="M39.8 41.95 26.65 28.8q-1.5 1.3-3.5 2.025-2 .725-4.25.725-5.4 0-9.15-3.75T6 18.75q0-5.3 3.75-9.05 3.75-3.75 9.1-3.75 5.3 0 9.025 3.75 3.725 3.75 3.725 9.05 0 2.15-.7 4.15-.7 2-2.1 3.75L42 39.75Zm-20.95-13.4q4.05 0 6.9-2.875Q28.6 22.8 28.6 18.75t-2.85-6.925Q22.9 8.95 18.85 8.95q-4.1 0-6.975 2.875T9 18.75q0 4.05 2.875 6.925t6.975 2.875Z"/></svg> \ No newline at end of file diff --git a/src/app/constant/JamiStrings.qml b/src/app/constant/JamiStrings.qml index c545b3ae1..4872ddc00 100644 --- a/src/app/constant/JamiStrings.qml +++ b/src/app/constant/JamiStrings.qml @@ -354,6 +354,12 @@ Item { property string noNetworkConnectivity: qsTr("No network connectivity") property string deletedMessage: qsTr("Deleted message") + //MessagesResearch + property string jumpTo: qsTr("Jump to") + property string messages: qsTr("Messages") + property string files: qsTr("Files") + property string search: qsTr("Search") + // Chatview footer property string jumpToLatest: qsTr("Jump to latest") property string typeIndicatorSingle: qsTr("{} is typing…") diff --git a/src/app/constant/JamiTheme.qml b/src/app/constant/JamiTheme.qml index d53a320e5..c04d5a68f 100644 --- a/src/app/constant/JamiTheme.qml +++ b/src/app/constant/JamiTheme.qml @@ -376,7 +376,7 @@ Item { property real swarmDetailsPageDocumentsMargins: 5 property real swarmDetailsPageDocumentsMediaRadius: 15 property real swarmDetailsPageDocumentsPaperClipSize: 24 - property real swarmDetailsPageDocumentsMediaSize: 175 + property real swarmDetailsPageDocumentsMediaSize: 150 //Call information property real textFontPointSize: calcSize(10) @@ -401,6 +401,11 @@ Item { // Modal Popup property real modalPopupRadius: 20 + //MessagesResearch + property color blueLinkColor: darkTheme ? "#3366BB" : "#0645AD" + property real jumpToFontSize: calcSize(13) + property real searchbarSize: 200 + // MessageWebView property real chatViewHairLineSize: 1 property real chatViewMaximumWidth: 900 diff --git a/src/app/mainview/components/ChatView.qml b/src/app/mainview/components/ChatView.qml index a56e328a1..77b64b11c 100644 --- a/src/app/mainview/components/ChatView.qml +++ b/src/app/mainview/components/ChatView.qml @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2023 Savoir-faire Linux Inc. + * Copyright (C) 2020-2022 Savoir-faire Linux Inc. * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com> * Author: Trevor Tabah <trevor.tabah@savoirfairelinux.com> * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com> @@ -102,30 +102,53 @@ Rectangle { onBackClicked: root.dismiss() - onShowDetailsClicked: { - addMemberPanel.visible = false - if (swarmDetailsPanel.visible) { + signal panelsVisibilityChange() + + onPanelsVisibilityChange: { + if (!swarmDetailsPanel.visible && !messagesResearchPanel.visible) { chatContents.visible = true } else { if (chatViewHeader.width - JamiTheme.detailsPageMinWidth < JamiTheme.chatViewHeaderMinimumWidth) chatContents.visible = false } + } + + onShowDetailsClicked: { + addMemberPanel.visible = false + messagesResearchPanel.visible = false swarmDetailsPanel.visible = !swarmDetailsPanel.visible + panelsVisibilityChange() + } + + onSearchBarOpened: { + addMemberPanel.visible = false + swarmDetailsPanel.visible = false + messagesResearchPanel.visible = true + panelsVisibilityChange() + } + + onSearchBarClosed: { + chatContents.visible = true + messagesResearchPanel.visible = false + panelsVisibilityChange() } onWidthChanged: { const isExpanding = previousWidth < width - if (!swarmDetailsPanel.visible && !addMemberPanel.visible) + + if (!swarmDetailsPanel.visible && !addMemberPanel.visible && !messagesResearchPanel.visible) return if (chatViewHeader.width < JamiTheme.detailsPageMinWidth + JamiTheme.chatViewHeaderMinimumWidth - && !isExpanding && chatContents.visible) { + && !isExpanding && chatContents.visible) { lastContentsSplitSize = chatContents.width - lastDetailsSplitSize = Math.min(JamiTheme.detailsPageMinWidth, (swarmDetailsPanel.visible ? - swarmDetailsPanel.width : - addMemberPanel.width)) + lastDetailsSplitSize = Math.min(JamiTheme.detailsPageMinWidth, (swarmDetailsPanel.visible + ? swarmDetailsPanel.width + : addMemberPanel.visible + ? addMemberPanel.width + : messagesResearchPanel.width)) chatContents.visible = false } else if (chatViewHeader.width >= JamiTheme.chatViewHeaderMinimumWidth + lastDetailsSplitSize - && isExpanding && !layoutManager.isFullScreen && !chatContents.visible) { + && isExpanding && !layoutManager.isFullScreen && !chatContents.visible) { chatContents.visible = true } previousWidth = width @@ -244,10 +267,7 @@ Rectangle { id: chatContents SplitView.maximumWidth: viewCoordinator.splitView.width SplitView.minimumWidth: JamiTheme.chatViewHeaderMinimumWidth - - SplitView.preferredWidth: chatViewHeader.width - - (swarmDetailsPanel.visible ? swarmDetailsPanel.width : - ( addMemberPanel.visible ? addMemberPanel.width : 0)) + SplitView.fillWidth: true StackLayout { id: chatViewStack @@ -315,6 +335,15 @@ Rectangle { } } + MessagesResearchPanel { + id: messagesResearchPanel + + visible: false + SplitView.maximumWidth: viewCoordinator.splitView.width + SplitView.minimumWidth: JamiTheme.detailsPageMinWidth + SplitView.preferredWidth: JamiTheme.detailsPageMinWidth + } + SwarmDetailsPanel { id: swarmDetailsPanel visible: false diff --git a/src/app/mainview/components/ChatViewHeader.qml b/src/app/mainview/components/ChatViewHeader.qml index 66a820928..599fef401 100644 --- a/src/app/mainview/components/ChatViewHeader.qml +++ b/src/app/mainview/components/ChatViewHeader.qml @@ -19,6 +19,7 @@ import QtQuick import QtQuick.Layouts +import QtQuick.Controls import net.jami.Adapters 1.1 import net.jami.Constants 1.1 @@ -34,6 +35,8 @@ Rectangle { signal addToConversationClicked signal pluginSelector signal showDetailsClicked + signal searchBarOpened + signal searchBarClosed Connections { target: CurrentConversation @@ -145,98 +148,117 @@ Rectangle { RowLayout { id: buttonGroup + property int buttonGroupMargin: 8 + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - Layout.fillWidth: true - Layout.rightMargin: 8 + Layout.rightMargin: buttonGroupMargin spacing: 16 + Layout.fillWidth: true - PushButton { - id: startAAudioCallButton + Searchbar { + id: rowSearchBar - visible: interactionButtonsVisibility && (!addMemberVisibility || UtilsAdapter.getAppValue(Settings.EnableExperimentalSwarm)) + spacing: buttonGroup.spacing + visible: CurrentConversation.isSwarm + } - source: JamiResources.place_audiocall_24dp_svg - toolTipText: JamiStrings.placeAudioCall + RowLayout { + id: pushbuttons - normalColor: JamiTheme.chatviewBgColor - imageColor: JamiTheme.chatviewButtonColor + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + Layout.rightMargin: 8 + spacing: 16 + Layout.fillWidth: true - onClicked: CallAdapter.placeAudioOnlyCall() - } + PushButton { + id: startAAudioCallButton - PushButton { - id: startAVideoCallButton + visible: interactionButtonsVisibility && (!addMemberVisibility || UtilsAdapter.getAppValue(Settings.EnableExperimentalSwarm)) - visible: CurrentAccount.videoEnabled_Video && interactionButtonsVisibility && (!addMemberVisibility || UtilsAdapter.getAppValue(Settings.EnableExperimentalSwarm)) - source: JamiResources.videocam_24dp_svg - toolTipText: JamiStrings.placeVideoCall + source: JamiResources.place_audiocall_24dp_svg + toolTipText: JamiStrings.placeAudioCall - normalColor: JamiTheme.chatviewBgColor - imageColor: JamiTheme.chatviewButtonColor + normalColor: JamiTheme.chatviewBgColor + imageColor: JamiTheme.chatviewButtonColor - onClicked: { - CallAdapter.placeCall() + onClicked: CallAdapter.placeAudioOnlyCall() } - } - PushButton { - id: addParticipantsButton + PushButton { + id: startAVideoCallButton - source: JamiResources.add_people_24dp_svg - toolTipText: JamiStrings.addParticipants + visible: CurrentAccount.videoEnabled_Video && interactionButtonsVisibility && (!addMemberVisibility || UtilsAdapter.getAppValue(Settings.EnableExperimentalSwarm)) + source: JamiResources.videocam_24dp_svg + toolTipText: JamiStrings.placeVideoCall - normalColor: JamiTheme.chatviewBgColor - imageColor: JamiTheme.chatviewButtonColor + normalColor: JamiTheme.chatviewBgColor + imageColor: JamiTheme.chatviewButtonColor - visible: CurrentConversation.uris.length < 8 && addMemberVisibility + onClicked: { + CallAdapter.placeCall() + } + } - onClicked: addToConversationClicked() - } + PushButton { + id: addParticipantsButton - PushButton { - id: selectPluginButton + source: JamiResources.add_people_24dp_svg + toolTipText: JamiStrings.addParticipants - visible: PluginAdapter.isEnabled && PluginAdapter.chatHandlersListCount && - interactionButtonsVisibility + normalColor: JamiTheme.chatviewBgColor + imageColor: JamiTheme.chatviewButtonColor - source: JamiResources.plugins_24dp_svg - toolTipText: JamiStrings.showPlugins + visible: CurrentConversation.uris.length < 8 && addMemberVisibility - normalColor: JamiTheme.chatviewBgColor - imageColor: JamiTheme.chatviewButtonColor + onClicked: addToConversationClicked() + } - onClicked: pluginSelector() - } + PushButton { + id: selectPluginButton - PushButton { - id: sendContactRequestButton + visible: PluginAdapter.isEnabled && PluginAdapter.chatHandlersListCount && + interactionButtonsVisibility - visible: CurrentConversation.isTemporary || CurrentConversation.isBanned + source: JamiResources.plugins_24dp_svg + toolTipText: JamiStrings.showPlugins - source: JamiResources.add_people_24dp_svg - toolTipText: JamiStrings.addToConversations + normalColor: JamiTheme.chatviewBgColor + imageColor: JamiTheme.chatviewButtonColor - normalColor: JamiTheme.chatviewBgColor - imageColor: JamiTheme.chatviewButtonColor + onClicked: pluginSelector() + } - onClicked: CurrentConversation.isBanned ? - MessagesAdapter.unbanConversation(CurrentConversation.id) - : MessagesAdapter.sendConversationRequest() - } + PushButton { + id: sendContactRequestButton + + visible: CurrentConversation.isTemporary || CurrentConversation.isBanned - PushButton { - id: detailsButton + source: JamiResources.add_people_24dp_svg + toolTipText: JamiStrings.addToConversations - visible: swarmDetailsVisibility + normalColor: JamiTheme.chatviewBgColor + imageColor: JamiTheme.chatviewButtonColor + + onClicked: CurrentConversation.isBanned ? + MessagesAdapter.unbanConversation(CurrentConversation.id) + : MessagesAdapter.sendConversationRequest() + } - source: JamiResources.swarm_details_panel_svg - toolTipText: JamiStrings.details + PushButton { + id: detailsButton - normalColor: JamiTheme.chatviewBgColor - imageColor: JamiTheme.chatviewButtonColor + visible: swarmDetailsVisibility - onClicked: showDetailsClicked() + source: JamiResources.swarm_details_panel_svg + toolTipText: JamiStrings.details + + normalColor: JamiTheme.chatviewBgColor + imageColor: JamiTheme.chatviewButtonColor + + onClicked: showDetailsClicked() + } } + } Component.onCompleted: JamiQmlUtils.messagingHeaderRectRowLayout = messagingHeaderRectRowLayout } diff --git a/src/app/mainview/components/ContactSearchBar.qml b/src/app/mainview/components/ContactSearchBar.qml index bebcf91be..67dcd85aa 100644 --- a/src/app/mainview/components/ContactSearchBar.qml +++ b/src/app/mainview/components/ContactSearchBar.qml @@ -128,7 +128,9 @@ Rectangle { Shortcut { sequence: "Ctrl+F" context: Qt.ApplicationShortcut - onActivated: contactSearchBar.forceActiveFocus() + onActivated: { + contactSearchBar.forceActiveFocus() + } } Keys.onPressed: function (keyEvent) { diff --git a/src/app/mainview/components/DocumentsScrollview.qml b/src/app/mainview/components/DocumentsScrollview.qml index 03ce056ec..854a9821c 100644 --- a/src/app/mainview/components/DocumentsScrollview.qml +++ b/src/app/mainview/components/DocumentsScrollview.qml @@ -1,5 +1,6 @@ /* * Copyright (C) 2022-2023 Savoir-faire Linux Inc. + * Author: Nicolas Vengeon <nicolas.vengeon@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 @@ -35,39 +36,46 @@ Flickable { contentWidth: width property int spacingFlow: JamiTheme.swarmDetailsPageDocumentsMargins + property real flickableWidth: width property int numberElementsPerRow: { var sizeW = flow.width var breakSize = JamiTheme.swarmDetailsPageDocumentsMediaSize return Math.floor(sizeW / breakSize) } property int spacingLength: spacingFlow * (numberElementsPerRow - 1) + property color themeColor: CurrentConversation.color + property string textFilter: "" onVisibleChanged: { if (visible) { - MessagesAdapter.getConvMedias() - } else { - MessagesAdapter.mediaMessageListModel = null + MessagesAdapter.startSearch(textFilter,true) } } + onTextFilterChanged: { + MessagesAdapter.startSearch(textFilter,true) + } + Flow { id: flow width: parent.width spacing: spacingFlow - anchors.horizontalCenter: parent.horizontalCenter + anchors.horizontalCenter: parent.horizontalCenter Repeater { - model: MessagesAdapter.mediaMessageListModel + model: root.visible ? MessagesAdapter.mediaMessageListModel : 0 delegate: Loader { id: loaderRoot sourceComponent: { - if(Status === Interaction.Status.TRANSFER_FINISHED || Status === Interaction.Status.SUCCESS ){ - if (Object.keys(MessagesAdapter.getMediaInfo(Body)).length !== 0 && WITH_WEBENGINE) - return localMediaMsgComp + if (MessagesAdapter.isDocument(Type)) { + if(Status === Interaction.Status.TRANSFER_FINISHED || Status === Interaction.Status.SUCCESS ){ + if (Object.keys(MessagesAdapter.getMediaInfo(Body)).length !== 0 && WITH_WEBENGINE) + return localMediaMsgComp - return fileMsgComp + return fileMsgComp + } } } diff --git a/src/app/mainview/components/FilePreview.qml b/src/app/mainview/components/FilePreview.qml index e577231fb..a73d9e5c4 100644 --- a/src/app/mainview/components/FilePreview.qml +++ b/src/app/mainview/components/FilePreview.qml @@ -34,7 +34,7 @@ Component { id: dataTransferRect clip: true - width: (documents.width - spacingLength ) / numberElementsPerRow + width: (contentWidth - spacingLength ) / numberElementsPerRow height: width color: "transparent" @@ -64,7 +64,7 @@ Component { anchors.fill: parent anchors.margins: JamiTheme.swarmDetailsPageDocumentsMargins color: "transparent" - border.color: CurrentConversation.color + border.color: themeColor border.width: 2 radius: JamiTheme.swarmDetailsPageDocumentsMediaRadius layer.enabled: true diff --git a/src/app/mainview/components/MediaPreview.qml b/src/app/mainview/components/MediaPreview.qml index f11cc1654..cdef5b395 100644 --- a/src/app/mainview/components/MediaPreview.qml +++ b/src/app/mainview/components/MediaPreview.qml @@ -33,7 +33,7 @@ Component { Rectangle { id: localMediaRect - width: (documents.width - spacingLength) / numberElementsPerRow + width: (flickableWidth - spacingLength) / numberElementsPerRow height: width color: "transparent" @@ -62,7 +62,7 @@ Component { anchors.fill: parent anchors.margins: JamiTheme.swarmDetailsPageDocumentsMargins - color: CurrentConversation.color + color: themeColor layer.enabled: true layer.effect: OpacityMask { maskSource: Item { diff --git a/src/app/mainview/components/MessagesResearchPanel.qml b/src/app/mainview/components/MessagesResearchPanel.qml new file mode 100644 index 000000000..1f0bf3090 --- /dev/null +++ b/src/app/mainview/components/MessagesResearchPanel.qml @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2023 Savoir-faire Linux Inc. + * Author: Nicolas Vengeon <nicolas.vengeon@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 Qt.labs.platform +import Qt5Compat.GraphicalEffects + +import net.jami.Models 1.1 +import net.jami.Adapters 1.1 +import net.jami.Constants 1.1 + +import "../../commoncomponents" +import "../../settingsview/components" + +Rectangle { + id: root + + color: JamiTheme.chatviewBgColor + + ColumnLayout { + anchors.fill: parent + + TabBar { + id: researchTabBar + + currentIndex: 0 + Layout.preferredHeight: contentHeight + 10 + Layout.preferredWidth: root.width + background.visible: false + signal filterTabChange() + onCurrentIndexChanged: { + filterTabChange() + } + + onVisibleChanged: { + researchTabBar.currentIndex = 0 + } + + FilterTabButton { + id: messagesResearchTabButton + + backgroundColor: "transparent" + hoverColor: "transparent" + borderWidth: 4 + bottomMargin: JamiTheme.settingsMarginSize + fontSize: JamiTheme.menuFontSize + underlineContentOnly: true + + down: researchTabBar.currentIndex === 0 + labelText: JamiStrings.messages + Layout.fillWidth: true + } + + FilterTabButton { + + id: fileResearchTabButton + backgroundColor: "transparent" + hoverColor: "transparent" + borderWidth: 4 + bottomMargin: JamiTheme.settingsMarginSize + fontSize: JamiTheme.menuFontSize + underlineContentOnly: true + + down: researchTabBar.currentIndex === 1 + labelText: JamiStrings.files + Layout.fillWidth: true + } + } + + Rectangle { + id: view + + color: JamiTheme.chatviewBgColor + Layout.fillWidth: true + Layout.fillHeight: true + + MessagesResearchView { + anchors.fill: parent + visible: researchTabBar.currentIndex === 0 + clip: true + } + + DocumentsScrollview { + anchors.fill: parent + visible: researchTabBar.currentIndex === 1 + clip: true + themeColor: JamiTheme.chatviewTextColor + textFilter: MessagesAdapter.searchbarPrompt + } + } + } +} diff --git a/src/app/mainview/components/MessagesResearchView.qml b/src/app/mainview/components/MessagesResearchView.qml new file mode 100644 index 000000000..ed3d263f4 --- /dev/null +++ b/src/app/mainview/components/MessagesResearchView.qml @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2023 Savoir-faire Linux Inc. + * Author: Nicolas Vengeon <nicolas.vengeon@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 Qt.labs.platform +import Qt5Compat.GraphicalEffects +import SortFilterProxyModel + +import net.jami.Models 1.1 +import net.jami.Adapters 1.1 +import net.jami.Constants 1.1 + +import "../../commoncomponents" +import "../../settingsview/components" + + +ListView { + id: root + + spacing: 10 + model: SortFilterProxyModel { + id: proxyModel + + property var messageListModel: MessagesAdapter.mediaMessageListModel + readonly property int textType: Interaction.Type.TEXT + + onMessageListModelChanged: sourceModel = root.visible ? messageListModel : null + + filters: ExpressionFilter { + expression: Type === proxyModel.textType + } + } + + property var prompt: MessagesAdapter.searchbarPrompt + + onPromptChanged: { + MessagesAdapter.startSearch(prompt) + } + + Connections { + target: researchTabBar + function onFilterTabChange() { + MessagesAdapter.startSearch(prompt) + } + } + + delegate: Item { + width: root.width + height: msgLayout.height + + HoverHandler { + id: msgHover + + target: parent + } + + ColumnLayout { + id: msgLayout + + width: root.width + + TimestampInfo { + id: timestampItem + + showDay: true + showTime: true + formattedTime: MessagesAdapter.getFormattedTime(Timestamp) + formattedDay: MessagesAdapter.getFormattedDay(Timestamp) + } + + RowLayout { + id: contentRow + + property bool isMe: Author === CurrentAccount.uri + + Avatar { + id: avatar + + width: 30 + height: 30 + imageId: contentRow.isMe ? CurrentAccount.id : Author + showPresenceIndicator: false + mode: contentRow.isMe ? Avatar.Mode.Account : Avatar.Mode.Contact + Layout.leftMargin: 10 + } + + ColumnLayout { + + Text { + text: contentRow.isMe + ? CurrentAccount.bestName + : UtilsAdapter.getBestNameForUri(CurrentAccount.id, Author) + " :" + Layout.preferredWidth: myText.width + Layout.rightMargin: 10 + Layout.leftMargin: 10 + font.pixelSize: 0 + color: JamiTheme.chatviewUsernameColor + font.bold: true + } + + Text { + id: myText + + text: Body + color: JamiTheme.textColor + Layout.preferredWidth: msgLayout.width - avatar.width - 30 - 10 + elide: Text.ElideRight + Layout.rightMargin: 10 + Layout.leftMargin: 10 + font.pixelSize: IsEmojiOnly? JamiTheme.chatviewEmojiSize : JamiTheme.chatviewFontSize + Layout.alignment:Qt.AlignHCenter + } + } + } + } + + Button { + id: buttonJumpTo + + visible: msgHover.hovered || hovered + anchors.top: msgLayout.top + anchors.right: msgLayout.right + anchors.rightMargin: 20 + anchors.topMargin: timestampItem.height - 20 + width: buttonJumpText.width + 10 + height: buttonJumpText.height + 10 + background.visible: false + + onClicked: { + CurrentConversation.scrollToMsg(Id) + } + + Text { + id: buttonJumpText + + text: JamiStrings.jumpTo + color: buttonJumpTo.hovered ? JamiTheme.blueLinkColor : JamiTheme.chatviewUsernameColor + font.underline: buttonJumpTo.hovered + anchors.centerIn: parent + font.pointSize: JamiTheme.jumpToFontSize + } + } + } + +} + diff --git a/src/app/mainview/components/Searchbar.qml b/src/app/mainview/components/Searchbar.qml new file mode 100644 index 000000000..c31f94df5 --- /dev/null +++ b/src/app/mainview/components/Searchbar.qml @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2023 Savoir-faire Linux Inc. + * Author: Nicolas Vengeon <nicolas.vengeon@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.Layouts +import QtQuick.Controls + +import net.jami.Adapters 1.1 +import net.jami.Constants 1.1 + +import "../../commoncomponents" + +RowLayout { + id: root + + property real messagesResearchPanel: JamiTheme.detailsPageMinWidth + + //TO DO: find a design to set dynamically the size of the searchbar + property real searchBarWidth: JamiTheme.searchbarSize + + property string currentConversationId: CurrentConversation.id + property bool isOpened: false + + function openSearchBar() { + searchBarOpened() + rectTextArea.isSearch = true + anim.start() + textArea.forceActiveFocus() + isOpened = true + } + + function closeSearchbar() { + searchBarClosed() + rectTextArea.isSearch = false + anim.start() + isOpened = false + } + + Connections { + target: chatViewHeader + function onShowDetailsClicked() { + if (rectTextArea.isSearch) + closeSearchbar() + } + } + + onCurrentConversationIdChanged: { + if (isOpened) + closeSearchbar() + } + + + PushButton { + id: startSearchMessages + + source: JamiResources.search_svg + normalColor: JamiTheme.chatviewBgColor + imageColor: JamiTheme.chatviewButtonColor + + onClicked: { + if (rectTextArea.isSearch) + closeSearchbar() + else + openSearchBar() + } + } + + + SequentialAnimation { + id: anim + + PropertyAnimation { + target: rectTextArea; properties: "visible" + to: true + duration: 0 + } + + ParallelAnimation { + + NumberAnimation { + target: rectTextArea; properties: "opacity" + from: rectTextArea.isSearch ? 0 : 1 + to: rectTextArea.isSearch ? 1 : 0 + duration: 150 + } + + NumberAnimation { + target: rectTextArea; properties: "Layout.preferredWidth" + from: rectTextArea.isSearch ? 0 : root.searchBarWidth + to: rectTextArea.isSearch ? root.searchBarWidth : 0 + duration: 150 + } + } + + PropertyAnimation { + target: rectTextArea; properties: "visible" + to: rectTextArea.isSearch + duration: 0 + } + + } + Rectangle { + id: rectTextArea + + visible: false + Layout.preferredHeight: startSearchMessages.height + Layout.alignment: Qt.AlignVCenter + color: "transparent" + border.color: JamiTheme.chatviewTextColor + radius: 10 + border.width: 2 + + property bool isSearch: false + property int textAreaWidth: 200 + property alias searchBarWidth: root.searchBarWidth + + onSearchBarWidthChanged: { + Layout.preferredWidth = root.searchBarWidth + } + + TextField { + id: textArea + + background.visible: false + anchors.right: clearTextButton.left + anchors.left: rectTextArea.left + color: JamiTheme.chatviewTextColor + placeholderText: JamiStrings.search + placeholderTextColor: JamiTheme.chatviewTextColor + + onTextChanged: { + MessagesAdapter.searchbarPrompt = text + } + } + + PushButton { + id: clearTextButton + + anchors.verticalCenter: rectTextArea.verticalCenter + anchors.right: rectTextArea.right + anchors.margins: 5 + preferredSize: 21 + + radius: rectTextArea.radius + visible: textArea.text.length + opacity: visible ? 1 : 0 + normalColor: "transparent" + imageColor: JamiTheme.chatviewButtonColor + source: JamiResources.ic_clear_24dp_svg + toolTipText: JamiStrings.clearText + + property string convId: CurrentConversation.id + onConvIdChanged: { + textArea.clear() + } + + onClicked: { + textArea.clear() + textArea.forceActiveFocus() + } + + Behavior on opacity { + NumberAnimation { duration: 500; easing.type: Easing.OutCubic } + } + } + } +} diff --git a/src/app/messagesadapter.cpp b/src/app/messagesadapter.cpp index cb1e5648f..b7bd9d093 100644 --- a/src/app/messagesadapter.cpp +++ b/src/app/messagesadapter.cpp @@ -67,6 +67,8 @@ MessagesAdapter::MessagesAdapter(AppSettingsManager* settingsManager, filteredMsgListModel_->setSourceModel(conversation.interactions.get()); set_currentConvComposingList(conversationTypersUrlToName(conversation.typers)); + mediaInteractions_.reset(new MessageListModel(this)); + set_mediaMessageListModel(QVariant::fromValue(mediaInteractions_.get())); }); connect(previewEngine_, &PreviewEngine::infoReady, this, &MessagesAdapter::onPreviewInfoReady); @@ -82,6 +84,12 @@ MessagesAdapter::MessagesAdapter(AppSettingsManager* settingsManager, connectConversationModel(); } +bool +MessagesAdapter::isDocument(const interaction::Type& type) +{ + return interaction::Type::DATA_TRANSFER == type; +} + void MessagesAdapter::loadMoreMessages() { @@ -575,25 +583,16 @@ MessagesAdapter::onComposingStatusChanged(const QString& convId, void MessagesAdapter::onMessagesFoundProcessed(const QString& accountId, - const VectorMapStringString& messageIds, - const QVector<interaction::Info>& messageInformations) + const QMap<QString, interaction::Info>& messageInformation) { if (lrcInstance_->get_currentAccountId() != accountId) { return; } - bool isSearchInProgress = messageIds.length(); + + bool isSearchInProgress = messageInformation.size(); if (isSearchInProgress) { - int index = -1; - Q_FOREACH (const MapStringString& msg, messageIds) { - index++; - try { - std::pair<QString, interaction::Info> message(msg["id"], - messageInformations.at(index)); - mediaInteractions_->insert(message); - } catch (...) { - qWarning() << "error in onMessagesFoundProcessed, message insertion on index: " - << index; - } + for (auto it = messageInformation.begin(); it != messageInformation.end(); it++) { + mediaInteractions_->insert(qMakePair(it.key(), it.value())); } } else { set_mediaMessageListModel(QVariant::fromValue(mediaInteractions_.get())); @@ -729,17 +728,24 @@ MessagesAdapter::getFormattedDay(const quint64 timestamp) } void -MessagesAdapter::getConvMedias() +MessagesAdapter::startSearch(QString& text, bool isMedia) { + mediaInteractions_.reset(new MessageListModel(this)); + set_mediaMessageListModel(QVariant::fromValue(mediaInteractions_.get())); + + if (text.isEmpty() && !isMedia) + return; + auto accountId = lrcInstance_->get_currentAccountId(); auto convId = lrcInstance_->get_selectedConvUid(); - mediaInteractions_.reset(new MessageListModel(this)); - try { - lrcInstance_->getCurrentConversationModel()->getConvMediasInfos(accountId, convId); + lrcInstance_->getCurrentConversationModel()->getConvMediasInfos(accountId, + convId, + text, + isMedia); } catch (...) { - qDebug() << "Exception during getConvMedia:"; + qDebug() << "Exception during startSearch()"; } } diff --git a/src/app/messagesadapter.h b/src/app/messagesadapter.h index 8356b4a51..bf5644d1e 100644 --- a/src/app/messagesadapter.h +++ b/src/app/messagesadapter.h @@ -60,6 +60,7 @@ class MessagesAdapter final : public QmlAdapterBase QML_PROPERTY(QString, editId) QML_RO_PROPERTY(QList<QString>, currentConvComposingList) QML_PROPERTY(QVariant, mediaMessageListModel) + QML_PROPERTY(QString, searchbarPrompt) public: explicit MessagesAdapter(AppSettingsManager* settingsManager, @@ -70,6 +71,7 @@ public: Q_SIGNALS: void newInteraction(const QString& id, int type); + void newMessageBarPlaceholderText(QString& placeholderText); void newFilePasted(QString filePath); void newTextPasted(); void previewInformationToQML(QString messageId, QStringList previewInformation); @@ -77,6 +79,7 @@ Q_SIGNALS: void timestampUpdated(); protected: + Q_INVOKABLE bool isDocument(const interaction::Type& type); Q_INVOKABLE void loadMoreMessages(); Q_INVOKABLE void loadConversationUntil(const QString& to); Q_INVOKABLE void connectConversationModel(); @@ -129,7 +132,7 @@ protected: Q_INVOKABLE QVariantMap getTransferStats(const QString& messageId, int); Q_INVOKABLE QVariant dataForInteraction(const QString& interactionId, int role = Qt::DisplayRole) const; - Q_INVOKABLE void getConvMedias(); + Q_INVOKABLE void startSearch(QString& text, bool isMedia = false); Q_INVOKABLE int getMessageIndexFromId(QString& id); // Run corrsponding js functions, c++ to qml. @@ -150,8 +153,7 @@ private Q_SLOTS: const QString& contactUri, bool isComposing); void onMessagesFoundProcessed(const QString& accountId, - const VectorMapStringString& messageIds, - const QVector<interaction::Info>& messageInformations); + const QMap<QString, interaction::Info>& messageInformation); private: QList<QString> conversationTypersUrlToName(const QSet<QString>& typersSet); diff --git a/src/libclient/api/conversationmodel.h b/src/libclient/api/conversationmodel.h index c02857b31..9a60f39ee 100644 --- a/src/libclient/api/conversationmodel.h +++ b/src/libclient/api/conversationmodel.h @@ -316,9 +316,12 @@ public: api::datatransfer::Info& info) const; /** - * Starts a search of all medias and files in a conversation + * Starts a search of all medias in a conversation */ - void getConvMediasInfos(const QString& accountId, const QString& conversationId); + void getConvMediasInfos(const QString& accountId, + const QString& conversationId, + const QString& text, + bool isMedia); /** * @param convUid, uid of the conversation * @return the number of unread messages for the conversation @@ -612,12 +615,10 @@ Q_SIGNALS: /** * Emitted once a message search has been done and processed * @param accountId - * @param messageIds ids of all the messages found by the search - * @param messageInformations message datas + * @param messageInformation message datas */ void messagesFoundProcessed(const QString& accountId, - const VectorMapStringString& messageIds, - const QVector<interaction::Info>& messageInformations) const; + const QMap<QString, interaction::Info>& messageInformation) const; /** * Emitted once a conversation needs somebody to host the call * @param callId diff --git a/src/libclient/conversationmodel.cpp b/src/libclient/conversationmodel.cpp index 13f7d9e0f..b9478a0ea 100644 --- a/src/libclient/conversationmodel.cpp +++ b/src/libclient/conversationmodel.cpp @@ -243,7 +243,8 @@ public: std::map<QString, std::mutex> interactionsLocks; ///< {convId, mutex} MapStringString transfIdToDbIntId; - uint32_t currentMsgRequestId; + uint32_t mediaResearchRequestId; + uint32_t msgResearchRequestId; public Q_SLOTS: /** @@ -2540,29 +2541,34 @@ ConversationModelPimpl::slotMessagesFound(uint32_t requestId, const QString& conversationId, const VectorMapStringString& messageIds) { - if (requestId != currentMsgRequestId) { - return; - } - QVector<interaction::Info> messageDetailedinformations; - Q_FOREACH (const MapStringString& msg, messageIds) { - auto intInfo = interaction::Info(msg, ""); - if (intInfo.type == interaction::Type::DATA_TRANSFER) { - auto fileId = msg["fileId"]; + QMap<QString, interaction::Info> messageDetailedInformation; + if (requestId == mediaResearchRequestId) { + Q_FOREACH (const MapStringString& msg, messageIds) { + auto intInfo = interaction::Info(msg, ""); + if (intInfo.type == interaction::Type::DATA_TRANSFER) { + auto fileId = msg["fileId"]; - QString path; - qlonglong bytesProgress, totalSize; - linked.owner.dataTransferModel->fileTransferInfo(accountId, - conversationId, - fileId, - path, - totalSize, - bytesProgress); - intInfo.body = path; + QString path; + qlonglong bytesProgress, totalSize; + linked.owner.dataTransferModel->fileTransferInfo(accountId, + conversationId, + fileId, + path, + totalSize, + bytesProgress); + intInfo.body = path; + } + messageDetailedInformation[msg["id"]] = intInfo; + } + } else if (requestId == msgResearchRequestId) { + Q_FOREACH (const MapStringString& msg, messageIds) { + auto intInfo = interaction::Info(msg, ""); + if (intInfo.type == interaction::Type::TEXT) { + messageDetailedInformation[msg["id"]] = intInfo; + } } - messageDetailedinformations.append(intInfo); } - - Q_EMIT linked.messagesFoundProcessed(accountId, messageIds, messageDetailedinformations); + Q_EMIT linked.messagesFoundProcessed(accountId, messageDetailedInformation); } void @@ -4000,10 +4006,17 @@ ConversationModel::sendFile(const QString& convUid, const QString& path, const Q } void -ConversationModel::getConvMediasInfos(const QString& accountId, const QString& conversationId) -{ - pimpl_->currentMsgRequestId = ConfigurationManager::instance().searchConversation( - accountId, conversationId, "", "", "", "application/data-transfer+json", 0, 0, 0, 0); +ConversationModel::getConvMediasInfos(const QString& accountId, + const QString& conversationId, + const QString& text, + bool isMedia) +{ + if (isMedia) + pimpl_->mediaResearchRequestId = ConfigurationManager::instance().searchConversation( + accountId, conversationId, "", "", text, "application/data-transfer+json", 0, 0, 0, 0); + else + pimpl_->msgResearchRequestId = ConfigurationManager::instance().searchConversation( + accountId, conversationId, "", "", text, "text/plain", 0, 0, 0, 0); } void -- GitLab