From 86b84ea17e166f21c023b4fc99672e7457c998cf Mon Sep 17 00:00:00 2001
From: Nicolas Vengeon <nicolas.vengeon@savoirfairelinux.com>
Date: Tue, 10 Jan 2023 16:47:18 -0500
Subject: [PATCH] feature: save participant's view

Change-Id: I790f10542aed306a7416a4ce79f2eaf7a770135a
Gitlab: #698
---
 src/app/appsettingsmanager.h                  |  1 +
 src/app/calladapter.cpp                       | 23 +++++-
 src/app/calladapter.h                         |  2 +
 .../contextmenu/GeneralMenuItem.qml           |  1 +
 src/app/constant/JamiStrings.qml              | 10 ++-
 src/app/constant/JamiTheme.qml                |  5 ++
 src/app/mainapplication.cpp                   |  2 +
 src/app/mainview/components/CallOverlay.qml   | 17 ++++-
 .../components/CallViewContextMenu.qml        | 32 +++++++++
 .../KeyboardShortcutKeyDelegate.qml           |  4 +-
 .../components/KeyboardShortcutTable.qml      | 34 +++++++++
 .../mainview/components/OngoingCallPage.qml   | 22 +++++-
 .../components/ParticipantOverlay.qml         | 39 +++++++++-
 .../mainview/components/ParticipantsLayer.qml | 17 ++++-
 src/app/mainview/components/Toast.qml         | 72 +++++++++++++++++++
 src/app/mainview/components/ToastManager.qml  | 28 ++++++++
 .../components/RecordingSettings.qml          | 68 ++++++++++++++++--
 src/app/utilsadapter.cpp                      | 23 ++++++
 src/app/utilsadapter.h                        |  2 +
 src/app/videoprovider.cpp                     |  9 ++-
 src/app/videoprovider.h                       |  1 +
 src/libclient/api/accountmodel.h              |  1 +
 22 files changed, 395 insertions(+), 18 deletions(-)
 create mode 100644 src/app/mainview/components/Toast.qml
 create mode 100644 src/app/mainview/components/ToastManager.qml

diff --git a/src/app/appsettingsmanager.h b/src/app/appsettingsmanager.h
index 30a0573ac..b71bb598b 100644
--- a/src/app/appsettingsmanager.h
+++ b/src/app/appsettingsmanager.h
@@ -36,6 +36,7 @@ extern const QString defaultDownloadPath;
 #define KEYS \
     X(MinimizeOnClose, true) \
     X(DownloadPath, defaultDownloadPath) \
+    X(ScreenshotPath, {}) \
     X(EnableNotifications, true) \
     X(EnableTypingIndicator, true) \
     X(EnableReadReceipt, true) \
diff --git a/src/app/calladapter.cpp b/src/app/calladapter.cpp
index 3c5d99bf2..645fe3e06 100644
--- a/src/app/calladapter.cpp
+++ b/src/app/calladapter.cpp
@@ -553,7 +553,7 @@ CallAdapter::fillParticipantData(QJsonObject& participant) const
     auto uri = participant[URI].toString();
     participant[ISLOCAL] = false;
     if (uri == accInfo.profileInfo.uri && participant[DEVICE] == getCurrentDeviceId(accInfo)) {
-        participant[BESTNAME] = tr("me");
+        participant[BESTNAME] = tr("Me");
         participant[ISLOCAL] = true;
     } else {
         try {
@@ -1145,6 +1145,27 @@ CallAdapter::updateAdvancedInformation()
     }
 }
 
+bool
+CallAdapter::takeScreenshot(const QImage& image, const QString& path)
+{
+    QString name = QString("%1 %2")
+                       .arg(tr("Screenshot"))
+                       .arg(QDateTime::currentDateTime().toString(Qt::ISODate));
+
+    bool fileAlreadyExists = true;
+    int nb = 0;
+    QString filePath = QString("%1%2.png").arg(path).arg(name);
+    while (fileAlreadyExists) {
+        filePath = QString("%1%2.png").arg(path).arg(name);
+        if (nb)
+            filePath = QString("%1(%2).png").arg(filePath).arg(QString::number(nb));
+        QFileInfo check_file(filePath);
+        fileAlreadyExists = check_file.exists() && check_file.isFile();
+        nb++;
+    }
+    return image.save(filePath, "PNG");
+}
+
 void
 CallAdapter::preventScreenSaver(bool state)
 {
diff --git a/src/app/calladapter.h b/src/app/calladapter.h
index e83387e6a..8f37d8a22 100644
--- a/src/app/calladapter.h
+++ b/src/app/calladapter.h
@@ -98,6 +98,8 @@ public:
     Q_INVOKABLE void setCallInfo();
     Q_INVOKABLE void updateAdvancedInformation();
 
+    Q_INVOKABLE bool takeScreenshot(const QImage &image, const QString &path);
+
 Q_SIGNALS:
     void callStatusChanged(int index, const QString& accountId, const QString& convUid);
     void callInfosChanged(const QVariant& infos, const QString& accountId, const QString& convUid);
diff --git a/src/app/commoncomponents/contextmenu/GeneralMenuItem.qml b/src/app/commoncomponents/contextmenu/GeneralMenuItem.qml
index d42b5cd5e..6d77c9acb 100644
--- a/src/app/commoncomponents/contextmenu/GeneralMenuItem.qml
+++ b/src/app/commoncomponents/contextmenu/GeneralMenuItem.qml
@@ -49,6 +49,7 @@ MenuItem {
     property int itemTextMargin: 20
 
     signal clicked
+    property bool itemHovered: menuItemContentRect.hovered
 
     contentItem: AbstractButton {
         id: menuItemContentRect
diff --git a/src/app/constant/JamiStrings.qml b/src/app/constant/JamiStrings.qml
index f69be343e..20ff8d70e 100644
--- a/src/app/constant/JamiStrings.qml
+++ b/src/app/constant/JamiStrings.qml
@@ -298,6 +298,8 @@ Item {
     property string lowerHand: qsTr("Lower hand")
     property string raiseHand: qsTr("Raise hand")
     property string layoutSettings: qsTr("Layout settings")
+    property string tileScreenshot: qsTr("Take tile screenshot")
+    property string screenshotTaken: qsTr("Screenshot saved to %1")
 
     //advanced information
     property string renderersInformation: qsTr("Renderers information")
@@ -508,15 +510,16 @@ Item {
     // Context Menu
     property string saveFile: qsTr("Save file")
     property string openLocation: qsTr("Open location")
+    property string me: qsTr("Me")
 
     // Updates
     property string betaInstall: qsTr("Install beta version")
     property string checkForUpdates: qsTr("Check for updates now")
     property string enableAutoUpdates: qsTr("Enable/Disable automatic updates")
-    property string tipAutoUpdate: qsTr("toggle automatic updates")
+    property string tipAutoUpdate: qsTr("Toggle automatic updates")
     property string updatesTitle: qsTr("Updates")
     property string updateDialogTitle: qsTr("Update")
-    property string updateFound: qsTr("A new version of Jami was found\n Would you like to update now?")
+    property string updateFound: qsTr("A new version of Jami was found\nWould you like to update now?")
     property string updateNotFound: qsTr("No new version of Jami was found")
     property string updateCheckError: qsTr("An error occured when checking for a new version")
     property string updateNetworkError: qsTr("Network error")
@@ -538,7 +541,8 @@ Item {
     // Recording Settings
     property string tipRecordFolder: qsTr("Select a record directory")
     property string quality: qsTr("Quality")
-    property string saveIn: qsTr("Save in")
+    property string saveRecordingsTo: qsTr("Save recordings to")
+    property string saveScreenshotsTo: qsTr("Save screenshots to")
     property string callRecording: qsTr("Call Recording")
     property string alwaysRecordCalls: qsTr("Always record calls")
 
diff --git a/src/app/constant/JamiTheme.qml b/src/app/constant/JamiTheme.qml
index 80c71c474..b81d13441 100644
--- a/src/app/constant/JamiTheme.qml
+++ b/src/app/constant/JamiTheme.qml
@@ -146,6 +146,11 @@ Item {
     property color spinboxBackgroundColor: darkTheme ? editBackgroundColor : selectedColor
     property color spinboxBorderColor: darkTheme ? tintedBlue : Qt.rgba(0, 0.34, 0.6, 0.36)
 
+    //Toast
+    property color toastColor: darkTheme ? "#f0f0f0" : "#000000"
+    property color toastRectColor: !darkTheme ? "#f0f0f0" : "#000000"
+    property real toastFontSize: calcSize(15)
+
     // Call buttons
     property color acceptButtonGreen: "#4caf50"
     property color acceptButtonHoverGreen: "#5db761"
diff --git a/src/app/mainapplication.cpp b/src/app/mainapplication.cpp
index efa1012a3..8119764cb 100644
--- a/src/app/mainapplication.cpp
+++ b/src/app/mainapplication.cpp
@@ -174,10 +174,12 @@ MainApplication::init()
         Qt::DirectConnection);
 
     auto downloadPath = settingsManager_->getValue(Settings::Key::DownloadPath);
+    auto screenshotPath = settingsManager_->getValue(Settings::Key::ScreenshotPath);
     auto allowTransferFromTrusted = settingsManager_->getValue(Settings::Key::AutoAcceptFiles)
                                         .toBool();
     auto acceptTransferBelow = settingsManager_->getValue(Settings::Key::AcceptTransferBelow).toInt();
     lrcInstance_->accountModel().downloadDirectory = downloadPath.toString() + "/";
+    lrcInstance_->accountModel().screenshotDirectory = screenshotPath.toString();
     lrcInstance_->accountModel().autoTransferFromTrusted = allowTransferFromTrusted;
     lrcInstance_->accountModel().autoTransferSizeThreshold = acceptTransferBelow;
 
diff --git a/src/app/mainview/components/CallOverlay.qml b/src/app/mainview/components/CallOverlay.qml
index a506609ed..0a8f6d9a5 100644
--- a/src/app/mainview/components/CallOverlay.qml
+++ b/src/app/mainview/components/CallOverlay.qml
@@ -39,6 +39,7 @@ Item {
 
     signal chatButtonClicked
     signal fullScreenClicked
+    signal closeClicked
 
     function closeContextMenuAndRelatedWindows() {
         ContactPickerCreation.closeContactPicker()
@@ -46,14 +47,22 @@ Item {
         SelectScreenWindowCreation.destroySelectScreenWindow()
         ScreenRubberBandCreation.destroyScreenRubberBandWindow()
         PluginHandlerPickerCreation.closePluginHandlerPicker()
+        root.closeClicked()
         callInformationOverlay.close()
     }
 
     // x, y position does not need to be translated
     // since they all fill the call page
-    function openCallViewContextMenuInPos(x, y) {
+    function openCallViewContextMenuInPos(x, y,
+                                          hoveredOverlayUri,
+                                          hoveredOverlaySinkId,
+                                          hoveredOverVideoMuted)
+    {
         callViewContextMenu.x = x
         callViewContextMenu.y = y
+        callViewContextMenu.hoveredOverlayUri = hoveredOverlayUri
+        callViewContextMenu.hoveredOverlaySinkId = hoveredOverlaySinkId
+        callViewContextMenu.hoveredOverVideoMuted = hoveredOverVideoMuted
         callViewContextMenu.openMenu()
     }
 
@@ -171,10 +180,16 @@ Item {
 
         onTransferCallButtonClicked: openContactPicker(ContactList.TRANSFER)
         onPluginItemClicked: openPluginsMenu()
+        onScreenshotTaken: {
+            toastManager.instantiateToast();
+        }
         onRecordCallClicked: CallAdapter.recordThisCallToggle()
         onOpenSelectionWindow: {
             SelectScreenWindowCreation.createSelectScreenWindowObject(appWindow)
             SelectScreenWindowCreation.showSelectScreenWindow(callPreviewId, windowSelection)
         }
+        onScreenshotButtonHoveredChanged: {
+            participantsLayer.screenshotButtonHovered = screenshotButtonHovered
+        }
     }
 }
diff --git a/src/app/mainview/components/CallViewContextMenu.qml b/src/app/mainview/components/CallViewContextMenu.qml
index 8f27ed55b..7d00825a4 100644
--- a/src/app/mainview/components/CallViewContextMenu.qml
+++ b/src/app/mainview/components/CallViewContextMenu.qml
@@ -37,6 +37,12 @@ ContextMenuAutoLoader {
     signal transferCallButtonClicked
     signal recordCallClicked
     signal openSelectionWindow
+    signal screenshotTaken
+    property bool screenshotButtonHovered: screenShot.itemHovered
+
+    property string hoveredOverlayUri: ""
+    property string hoveredOverlaySinkId: ""
+    property bool hoveredOverVideoMuted: true
 
     property list<GeneralMenuItem> menuItems: [
         GeneralMenuItem {
@@ -194,8 +200,34 @@ ContextMenuAutoLoader {
                 CallAdapter.startTimerInformation();
                 callInformationOverlay.open()
             }
+        },
+        GeneralMenuItem {
+            id: screenShot
+
+            canTrigger: hoveredOverlayUri !== "" && hoveredOverVideoMuted === false
+            itemName: JamiStrings.tileScreenshot
+            iconSource: JamiResources.baseline_camera_alt_24dp_svg
+
+            MaterialToolTip {
+                id: tooltip
+
+                parent: screenShot
+                visible: screenShot.itemHovered
+                delay: Qt.styleHints.mousePressAndHoldInterval
+                property bool isMe: CurrentAccount.uri === hoveredOverlayUri
+                text: isMe ? JamiStrings.me
+                           : UtilsAdapter.getBestNameForUri(CurrentAccount.id, hoveredOverlayUri)
+            }
+
+            onClicked: {
+                if (CallAdapter.takeScreenshot(videoProvider.captureRawVideoFrame(hoveredOverlaySinkId),
+                                               UtilsAdapter.getDirScreenshot())) {
+                    screenshotTaken()
+                }
+            }
         }
     ]
 
+
     Component.onCompleted: menuItemsToLoad = menuItems
 }
diff --git a/src/app/mainview/components/KeyboardShortcutKeyDelegate.qml b/src/app/mainview/components/KeyboardShortcutKeyDelegate.qml
index b354c3bd3..c5ee9d804 100644
--- a/src/app/mainview/components/KeyboardShortcutKeyDelegate.qml
+++ b/src/app/mainview/components/KeyboardShortcutKeyDelegate.qml
@@ -62,7 +62,9 @@ RowLayout {
 
             anchors.centerIn: parent
 
-            text: shortcut
+            text: shortcut2 === "" ?
+                      shortcut :
+                      shortcut + " + " + shortcut2
             font.pointSize: JamiTheme.textFontSize + 3
             font.weight: Font.DemiBold
             color: JamiTheme.textColor
diff --git a/src/app/mainview/components/KeyboardShortcutTable.qml b/src/app/mainview/components/KeyboardShortcutTable.qml
index 377af5538..3cc455bba 100644
--- a/src/app/mainview/components/KeyboardShortcutTable.qml
+++ b/src/app/mainview/components/KeyboardShortcutTable.qml
@@ -40,42 +40,52 @@ Window {
 
         ListElement {
             shortcut: "Ctrl + J"
+            shortcut2: ""
             description: qsTr("Open account list")
         }
         ListElement {
             shortcut: "Ctrl + L"
+            shortcut2: ""
             description: qsTr("Focus conversations list")
         }
         ListElement {
             shortcut: "Ctrl + R"
+            shortcut2: ""
             description: qsTr("Requests list")
         }
         ListElement {
             shortcut: "Ctrl + ↑"
+            shortcut2: ""
             description: qsTr("Previous conversation")
         }
         ListElement {
             shortcut: "Ctrl + ↓"
+            shortcut2: ""
             description: qsTr("Next conversation")
         }
         ListElement {
             shortcut: "Ctrl + F"
+            shortcut2: ""
             description: qsTr("Search bar")
         }
         ListElement {
             shortcut: "F11"
+            shortcut2: ""
             description: qsTr("Full screen")
         }
         ListElement {
             shortcut: "Ctrl + +"
+            shortcut2: ""
             description: qsTr("Increase font size")
         }
         ListElement {
             shortcut: "Ctrl + -"
+            shortcut2: ""
             description: qsTr("Decrease font size")
         }
         ListElement {
             shortcut: "Ctrl + 0"
+            shortcut2: ""
             description: qsTr("Reset font size")
         }
     }
@@ -85,34 +95,42 @@ Window {
 
         ListElement {
             shortcut: "Ctrl + Shift + C"
+            shortcut2: ""
             description: qsTr("Start an audio call")
         }
         ListElement {
             shortcut: "Ctrl + Shift + X"
+            shortcut2: ""
             description: qsTr("Start a video call")
         }
         ListElement {
             shortcut: "Ctrl + Shift + L"
+            shortcut2: ""
             description: qsTr("Clear history")
         }
         ListElement {
             shortcut: "Ctrl + Shift + B"
+            shortcut2: ""
             description: qsTr("Block contact")
         }
         ListElement {
             shortcut: "Ctrl + Shift + Delete"
+            shortcut2: ""
             description: qsTr("Remove conversation")
         }
         ListElement {
             shortcut: "Shift + Ctrl + A"
+            shortcut2: ""
             description: qsTr("Accept contact request")
         }
         ListElement {
             shortcut: "↑"
+            shortcut2: ""
             description: qsTr("Edit last message")
         }
         ListElement {
             shortcut: "Esc"
+            shortcut2: ""
             description: qsTr("Cancel message edition")
         }
     }
@@ -122,26 +140,32 @@ Window {
 
         ListElement {
             shortcut: "Ctrl + M"
+            shortcut2: ""
             description: qsTr("Media settings")
         }
         ListElement {
             shortcut: "Ctrl + G"
+            shortcut2: ""
             description: qsTr("General settings")
         }
         ListElement {
             shortcut: "Ctrl + I"
+            shortcut2: ""
             description: qsTr("Account settings")
         }
         ListElement {
             shortcut: "Ctrl + P"
+            shortcut2: ""
             description: qsTr("Plugin settings")
         }
         ListElement {
             shortcut: "Ctrl + Shift + N"
+            shortcut2: ""
             description: qsTr("Open account creation wizard")
         }
         ListElement {
             shortcut: "F10"
+            shortcut2: ""
             description: qsTr("Open keyboard shortcut table")
         }
     }
@@ -151,24 +175,34 @@ Window {
 
         ListElement {
             shortcut: "Ctrl + Y"
+            shortcut2: ""
             description: qsTr("Answer an incoming call")
         }
         ListElement {
             shortcut: "Ctrl + D"
+            shortcut2: ""
             description: qsTr("End call")
         }
         ListElement {
             shortcut: "Ctrl + Shift + D"
+            shortcut2: ""
             description: qsTr("Decline the call request")
         }
         ListElement {
             shortcut: "M"
+            shortcut2: ""
             description: qsTr("Mute microphone")
         }
         ListElement {
             shortcut: "V"
+            shortcut2: ""
             description: qsTr("Stop camera")
         }
+        ListElement {
+            shortcut: "Ctrl"
+            shortcut2: qsTr("Mouse middle click")
+            description: qsTr("Take tile screenshot")
+        }
     }
 
     Rectangle {
diff --git a/src/app/mainview/components/OngoingCallPage.qml b/src/app/mainview/components/OngoingCallPage.qml
index a1adc306b..ad147b5a6 100644
--- a/src/app/mainview/components/OngoingCallPage.qml
+++ b/src/app/mainview/components/OngoingCallPage.qml
@@ -166,7 +166,10 @@ Rectangle {
                 onTapped: function (eventPoint, button) {
                     if (button === Qt.RightButton) {
                         callOverlay.openCallViewContextMenuInPos(eventPoint.position.x,
-                                                                 eventPoint.position.y)
+                                                                 eventPoint.position.y,
+                                                                 participantsLayer.hoveredOverlayUri,
+                                                                 participantsLayer.hoveredOverlaySinkId,
+                                                                 participantsLayer.hoveredOverVideoMuted)
                     }
                 }
             }
@@ -184,6 +187,7 @@ Rectangle {
 
             ParticipantsLayer {
                 id: participantsLayer
+
                 anchors.fill: parent
                 anchors.centerIn: parent
                 anchors.margins: 1
@@ -191,9 +195,18 @@ Rectangle {
                 participantsSide: callOverlay.participantsSide
             }
 
+            ToastManager {
+                id: toastManager
+
+                anchors.fill: parent
+
+                function instantiateToast() {
+                    instantiate(JamiStrings.screenshotTaken.arg(UtilsAdapter.getDirScreenshot()),1000,400)
+                }
+            }
+
             LocalVideo {
                 id: previewRenderer
-
                 visible: (CurrentCall.isSharing || !CurrentCall.isVideoMuted)
                          && !CurrentCall.isConference
 
@@ -329,6 +342,11 @@ Rectangle {
                             openInCallConversation()
                     }
                 }
+                onCloseClicked: {
+                    participantsLayer.hoveredOverlayUri = ""
+                    participantsLayer.hoveredOverlaySinkId = ""
+                    participantsLayer.hoveredOverVideoMuted = true
+                }
 
                 onChatButtonClicked: {
                     inCallMessageWebViewStack.visible ?
diff --git a/src/app/mainview/components/ParticipantOverlay.qml b/src/app/mainview/components/ParticipantOverlay.qml
index 3de0ad19a..3bbd4af9a 100644
--- a/src/app/mainview/components/ParticipantOverlay.qml
+++ b/src/app/mainview/components/ParticipantOverlay.qml
@@ -67,6 +67,18 @@ Item {
     property string muteAlertMessage: ""
     property bool muteAlertActive: false
 
+    property bool participantHovered: hoverIndicator.hovered
+    property bool isScreenshotButtonHovered: false
+
+    function takeScreenshot() {
+        if (!hoveredOverVideoMuted) {
+            if (CallAdapter.takeScreenshot(videoProvider.captureRawVideoFrame(hoveredOverlaySinkId),
+                                           UtilsAdapter.getDirScreenshot())) {
+                toastManager.instantiateToast();
+            }
+        }
+    }
+
     onMuteAlertActiveChanged: {
         if (muteAlertActive) {
             alertTimer.restart()
@@ -94,9 +106,11 @@ Item {
 
     Rectangle {
         z: -1
-        color: JamiTheme.buttonTintedBlue
+        border.color: JamiTheme.buttonTintedBlue
+        border.width: 2
+        color: "transparent"
         radius: 10
-        visible:voiceActive
+        visible: voiceActive || isScreenshotButtonHovered
         width: participantIsActive ? mediaDistRender.contentRect.width + 2 : undefined
         height: participantIsActive ? mediaDistRender.contentRect.height + 2 : undefined
         anchors.centerIn: participantIsActive ? parent : undefined
@@ -109,7 +123,6 @@ Item {
         anchors.margins: 2
         rendererId: root.sinkId
         crop: !participantIsActive
-
         underlayItems: Avatar {
             property real componentSize: Math.min(mediaDistRender.contentRect.width / 2, mediaDistRender.contentRect.height / 2)
             height:  componentSize
@@ -140,7 +153,25 @@ Item {
             anchors.centerIn: participantIsActive ? parent : undefined
             anchors.fill: participantIsActive ? undefined : parent
 
+            TapHandler {
+                acceptedButtons: Qt.MiddleButton
+                acceptedModifiers: Qt.ControlModifier
+                onTapped: {
+                    takeScreenshot()
+                }
+            }
+
+            MultiPointTouchArea {
+                anchors.fill: parent
+                minimumTouchPoints: 3
+                onPressed: {
+                    takeScreenshot()
+                }
+            }
+
             HoverHandler {
+                id: hoverIndicator
+
                 onPointChanged: {
                     participantRect.opacity = 1
                     fadeOutTimer.restart()
@@ -164,6 +195,7 @@ Item {
                 // Participant buttons for moderation
                 ParticipantOverlayMenu {
                     id: overlayMenu
+
                     visible: isMe || meModerator
                     anchors.fill: parent
 
@@ -209,6 +241,7 @@ Item {
 
                     RowLayout {
                         id: participantFootInfo
+
                         height: parent.height
                         anchors.verticalCenter: parent.verticalCenter
                         Text {
diff --git a/src/app/mainview/components/ParticipantsLayer.qml b/src/app/mainview/components/ParticipantsLayer.qml
index fa17aecca..9b20f5432 100644
--- a/src/app/mainview/components/ParticipantsLayer.qml
+++ b/src/app/mainview/components/ParticipantsLayer.qml
@@ -37,6 +37,10 @@ Item {
     property bool inLine: CallParticipantsModel.conferenceLayout === CallParticipantsModel.ONE_WITH_SMALL
     property bool participantsSide
     property bool enableHideSpectators: CallParticipantsModel.count > 1 && CurrentCall.hideSpectators
+    property string hoveredOverlayUri: ""
+    property string hoveredOverlaySinkId: ""
+    property bool hoveredOverVideoMuted: true
+    property bool screenshotButtonHovered: false
 
     onVisibleChanged: {
         CurrentCall.hideSelf = UtilsAdapter.getAppValue(Settings.HideSelf)
@@ -51,7 +55,10 @@ Item {
 
             anchors.fill: parent
             anchors.leftMargin: leftMargin_
-
+            isScreenshotButtonHovered: screenshotButtonHovered && hoveredOverlaySinkId === sinkId_
+            opacity: screenshotButtonHovered
+                     ? hoveredOverlaySinkId !== sinkId ? 0.1 : 1
+                     : 1
             sinkId: sinkId_
             uri: uri_
             deviceId: deviceId_
@@ -70,6 +77,14 @@ Item {
             participantIsModeratorMuted: audioModeratorMuted_
             participantHandIsRaised: isHandRaised_
 
+            onParticipantHoveredChanged:  {
+                if (participantHovered) {
+                    hoveredOverlayUri = overlay.uri
+                    hoveredOverlaySinkId = overlay.sinkId
+                    hoveredOverVideoMuted = videoMuted_
+                }
+            }
+
             Connections {
                 id: registeredNameFoundConnection
 
diff --git a/src/app/mainview/components/Toast.qml b/src/app/mainview/components/Toast.qml
new file mode 100644
index 000000000..db2347b62
--- /dev/null
+++ b/src/app/mainview/components/Toast.qml
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2023 Savoir-faire Linux Inc.
+ * Author: Vengeon Nicolas <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 net.jami.Constants 1.1
+
+Rectangle {
+    id: root
+
+    anchors.top: parent.top
+    anchors.horizontalCenter: parent.horizontalCenter
+    width: textMessage.width + 20
+    height: textMessage.height + 10
+    anchors.topMargin: 10
+    radius: 15
+    color: JamiTheme.toastRectColor
+
+    property int duration
+    property int fadingTime
+    property string message
+
+    Component.onCompleted: {
+        anim.start();
+    }
+
+    Text {
+        id: textMessage
+
+        anchors.centerIn: root
+        text: message
+        font.pointSize: JamiTheme.toastFontSize
+        color: JamiTheme.toastColor
+    }
+
+    SequentialAnimation on opacity {
+        id: anim
+
+        running: false
+
+        NumberAnimation {
+            to: 0.9
+            duration: root.fadingTime
+        }
+        PauseAnimation {
+            duration: root.duration
+        }
+        NumberAnimation {
+            to: 0
+            duration: root.fadingTime
+        }
+
+        onRunningChanged: {
+            if (!running)
+                root.destroy();
+        }
+    }
+}
diff --git a/src/app/mainview/components/ToastManager.qml b/src/app/mainview/components/ToastManager.qml
new file mode 100644
index 000000000..3ade93324
--- /dev/null
+++ b/src/app/mainview/components/ToastManager.qml
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2023 Savoir-faire Linux Inc.
+ * Author: Vengeon Nicolas <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
+
+Item {
+    id: root
+
+    function instantiate(message, duration, fadingTime) {
+        var component = Qt.createComponent("Toast.qml");
+        var sprite = component.createObject(root, {message: message, duration: duration, fadingTime: fadingTime});
+    }
+}
diff --git a/src/app/settingsview/components/RecordingSettings.qml b/src/app/settingsview/components/RecordingSettings.qml
index 0ea97e8f3..b52d3284d 100644
--- a/src/app/settingsview/components/RecordingSettings.qml
+++ b/src/app/settingsview/components/RecordingSettings.qml
@@ -28,24 +28,32 @@ import net.jami.Constants 1.1
 import "../../commoncomponents"
 
 ColumnLayout {
-    id:root
+    id: root
 
     property int itemWidth
     property string recordPath: AVModel.getRecordPath()
+    property string screenshotPath: UtilsAdapter.getDirScreenshot()
 
     onRecordPathChanged: {
-        if(recordPath === "") return
+        if(recordPath === "")
+            return
 
-        if(AVModel){
+        if(AVModel) {
             AVModel.setRecordPath(recordPath)
         }
     }
 
+    onScreenshotPathChanged: {
+        if (screenshotPath === "")
+            return
+        UtilsAdapter.setScreenshotPath(screenshotPath)
+    }
+
     FolderDialog {
         id: recordPathDialog
 
         title: JamiStrings.selectFolder
-        currentFolder: StandardPaths.writableLocation(StandardPaths.HomeLocation)
+        currentFolder: UtilsAdapter.getDirScreenshot()
         options: FolderDialog.ShowDirsOnly
 
         onAccepted: {
@@ -54,6 +62,19 @@ ColumnLayout {
         }
     }
 
+    FolderDialog {
+        id: screenshotPathDialog
+
+        title: JamiStrings.selectFolder
+        currentFolder: StandardPaths.writableLocation(StandardPaths.PicturesLocation)
+        options: FolderDialog.ShowDirsOnly
+
+        onAccepted: {
+            var dir = UtilsAdapter.getAbsPath(folder.toString())
+            screenshotPath = dir
+        }
+    }
+
     Timer{
         id: updateRecordQualityTimer
 
@@ -172,7 +193,7 @@ ColumnLayout {
             Layout.fillWidth: true
             Layout.fillHeight: true
 
-            text: JamiStrings.saveIn
+            text: JamiStrings.saveRecordingsTo
             color: JamiTheme.textColor
             font.pointSize: JamiTheme.settingsFontSize
             font.kerning: true
@@ -199,4 +220,41 @@ ColumnLayout {
             onClicked: recordPathDialog.open()
         }
     }
+
+    RowLayout {
+        Layout.fillWidth: true
+        Layout.preferredHeight: JamiTheme.preferredFieldHeight
+        Layout.leftMargin: JamiTheme.preferredMarginSize
+
+        Label {
+            Layout.fillWidth: true
+            Layout.fillHeight: true
+
+            text: JamiStrings.saveScreenshotsTo
+            color: JamiTheme.textColor
+            font.pointSize: JamiTheme.settingsFontSize
+            font.kerning: true
+
+            horizontalAlignment: Text.AlignLeft
+            verticalAlignment: Text.AlignVCenter
+        }
+
+        MaterialButton {
+            id: screenshotPathButton
+
+            Layout.alignment: Qt.AlignRight
+
+            preferredWidth: itemWidth
+            preferredHeight: JamiTheme.preferredFieldHeight
+
+            toolTipText: UtilsAdapter.getDirScreenshot()
+            text: screenshotPath
+            iconSource: JamiResources.round_folder_24dp_svg
+            color: JamiTheme.buttonTintedGrey
+            hoveredColor: JamiTheme.buttonTintedGreyHovered
+            pressedColor: JamiTheme.buttonTintedGreyPressed
+
+            onClicked: screenshotPathDialog.open()
+        }
+    }
 }
diff --git a/src/app/utilsadapter.cpp b/src/app/utilsadapter.cpp
index 49d6c2a01..e32f5c576 100644
--- a/src/app/utilsadapter.cpp
+++ b/src/app/utilsadapter.cpp
@@ -387,6 +387,22 @@ UtilsAdapter::getDirDocument()
         QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation));
 }
 
+QString
+UtilsAdapter::getDirScreenshot()
+{
+    QString screenshotPath = lrcInstance_->accountModel().screenshotDirectory;
+    if (screenshotPath.isEmpty()) {
+        QString folderName = "Jami";
+        auto picture = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
+        QDir dir;
+        dir.mkdir(picture + QDir::separator() + folderName);
+        screenshotPath = picture + QDir::separator() + folderName;
+        setScreenshotPath(screenshotPath);
+        lrcInstance_->accountModel().screenshotDirectory = screenshotPath;
+    }
+    return screenshotPath;
+}
+
 QString
 UtilsAdapter::getDirDownload()
 {
@@ -425,6 +441,13 @@ UtilsAdapter::setDownloadPath(QString dir)
     lrcInstance_->accountModel().downloadDirectory = dir + "/";
 }
 
+void
+UtilsAdapter::setScreenshotPath(QString dir)
+{
+    setAppValue(Settings::Key::ScreenshotPath, dir);
+    lrcInstance_->accountModel().screenshotDirectory = dir + QDir::separator();
+}
+
 void
 UtilsAdapter::monitor(const bool& continuous)
 {
diff --git a/src/app/utilsadapter.h b/src/app/utilsadapter.h
index 9fb0c96c0..6ee62ad3a 100644
--- a/src/app/utilsadapter.h
+++ b/src/app/utilsadapter.h
@@ -107,9 +107,11 @@ public:
     Q_INVOKABLE QVariant getAppValue(const Settings::Key key);
     Q_INVOKABLE void setAppValue(const Settings::Key key, const QVariant& value);
     Q_INVOKABLE QString getDirDocument();
+    Q_INVOKABLE QString getDirScreenshot();
     Q_INVOKABLE QString getDirDownload();
     Q_INVOKABLE void setRunOnStartUp(bool state);
     Q_INVOKABLE void setDownloadPath(QString dir);
+    Q_INVOKABLE void setScreenshotPath(QString dir);
     Q_INVOKABLE void monitor(const bool& continuous);
     Q_INVOKABLE void clearInteractionsCache(const QString& accountId, const QString& convUid);
     Q_INVOKABLE QVariantMap supportedLang();
diff --git a/src/app/videoprovider.cpp b/src/app/videoprovider.cpp
index ac9ccde79..d0320e889 100644
--- a/src/app/videoprovider.cpp
+++ b/src/app/videoprovider.cpp
@@ -117,6 +117,13 @@ VideoProvider::frame(const QString& id)
 
 QString
 VideoProvider::captureVideoFrame(const QString& id)
+{
+    auto img = captureRawVideoFrame(id);
+    return Utils::byteArrayToBase64String(Utils::QImageToByteArray(img));
+}
+
+QImage
+VideoProvider::captureRawVideoFrame(const QString& id)
 {
     QMutexLocker framesLk(&framesObjsMutex_);
     if (auto* videoFrame = frame(id)) {
@@ -127,7 +134,7 @@ VideoProvider::captureVideoFrame(const QString& id)
                           videoFrame->height(),
                           videoFrame->bytesPerLine(0),
                           imageFormat);
-        return Utils::byteArrayToBase64String(Utils::QImageToByteArray(img));
+        return img;
     }
     return {};
 }
diff --git a/src/app/videoprovider.h b/src/app/videoprovider.h
index 775e42500..6d496de6e 100644
--- a/src/app/videoprovider.h
+++ b/src/app/videoprovider.h
@@ -46,6 +46,7 @@ public:
     Q_INVOKABLE void registerSink(const QString& id, QVideoSink* obj);
     Q_INVOKABLE void unregisterSink(QVideoSink* obj);
     Q_INVOKABLE QString captureVideoFrame(const QString& id);
+    Q_INVOKABLE QImage captureRawVideoFrame(const QString& id);
 
 private Q_SLOTS:
     void onRendererStarted(const QString& id, const QSize& size);
diff --git a/src/libclient/api/accountmodel.h b/src/libclient/api/accountmodel.h
index 1eca832a0..8e1b47a13 100644
--- a/src/libclient/api/accountmodel.h
+++ b/src/libclient/api/accountmodel.h
@@ -73,6 +73,7 @@ public:
      * Should contains the full directory with the end marker (/ on linux for example)
      */
     QString downloadDirectory;
+    QString screenshotDirectory;
     /**
      * Accept transfer from trusted contacts
      */
-- 
GitLab