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