From 0d2a69a05bb8b95fef216c184ed3963eb77e5c42 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Blin?=
 <sebastien.blin@savoirfairelinux.com>
Date: Wed, 19 Aug 2020 16:49:57 -0400
Subject: [PATCH] conference: handle conferences informations

Remove the change layout button and improve the UX.
Add the possibility to hangup a conference or a participant.

Gitlab: #1187
Change-Id: Ie7052c75a1dd75e3f96d659c97a3653e6145a882
---
 CMakeLists.txt                |   2 +-
 pixmaps/more_vert-24px.svg    |   3 +
 pixmaps/pixmaps.gresource.xml |   1 +
 src/currentcallview.cpp       | 137 +++++++++----
 src/mainwindow.cpp            |   3 -
 src/video/video_widget.cpp    | 363 ++++++++++++++++++++++++++++++++++
 src/video/video_widget.h      |   7 +
 ui/currentcallview.ui         |  25 ---
 8 files changed, 470 insertions(+), 71 deletions(-)
 create mode 100644 pixmaps/more_vert-24px.svg

diff --git a/CMakeLists.txt b/CMakeLists.txt
index b2de4619..92293a06 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -11,7 +11,7 @@ IF (CMAKE_COMPILER_IS_GNUCC)
    ENDIF()
 ENDIF()
 
-set (CMAKE_CXX_STANDARD 14)
+set (CMAKE_CXX_STANDARD 17)
 
 # set project name and version
 PROJECT(jami-client-gnome)
diff --git a/pixmaps/more_vert-24px.svg b/pixmaps/more_vert-24px.svg
new file mode 100644
index 00000000..3a1e4ed2
--- /dev/null
+++ b/pixmaps/more_vert-24px.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/>
+    <path fill="white" d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/>
+</svg>
\ No newline at end of file
diff --git a/pixmaps/pixmaps.gresource.xml b/pixmaps/pixmaps.gresource.xml
index c96fdbbb..04aeb59e 100644
--- a/pixmaps/pixmaps.gresource.xml
+++ b/pixmaps/pixmaps.gresource.xml
@@ -61,5 +61,6 @@
     <file alias="retry-white">retry-white.svg</file>
     <file alias="plugin_white">extension_white_24dp.svg</file>
     <file alias="view">view.svg</file>
+    <file alias="more">more_vert-24px.svg</file>
   </gresource>
 </gresources>
diff --git a/src/currentcallview.cpp b/src/currentcallview.cpp
index 2e170163..d808b9a5 100644
--- a/src/currentcallview.cpp
+++ b/src/currentcallview.cpp
@@ -45,6 +45,7 @@
 #include <glib/gi18n.h>
 
 #include <QSize>
+#include <QJsonObject>
 
 #include <set>
 
@@ -92,7 +93,6 @@ struct CurrentCallViewPrivate
     GtkWidget *video_widget;
     GtkWidget *frame_chat;
     GtkWidget *togglebutton_chat;
-    GtkWidget *togglebutton_view;
     GtkWidget *togglebutton_muteaudio;
     GtkWidget *togglebutton_mutevideo;
     GtkWidget *togglebutton_add_participant;
@@ -233,6 +233,7 @@ public:
     void insertControls();
     void checkControlsFading();
     void update_view();
+    void update_participants_hovers(const QString& callId);
     CurrentCallView* self = nullptr; // The GTK widget itself
     CurrentCallViewPrivate* widgets = nullptr;
 
@@ -241,6 +242,7 @@ public:
     lrc::api::AVModel* avModel_;
 
     QMetaObject::Connection state_change_connection;
+    QMetaObject::Connection layout_change_connection;
     QMetaObject::Connection update_vcard_connection;
     QMetaObject::Connection renderer_connection;
     QMetaObject::Connection smartinfo_refresh_connection;
@@ -342,31 +344,6 @@ on_togglebutton_chat_toggled(GtkToggleButton* widget, CurrentCallView* view)
     }
 }
 
-static void
-on_togglebutton_view_toggled(GtkToggleButton* widget, CurrentCallView* view)
-{
-    g_return_if_fail(IS_CURRENT_CALL_VIEW(view));
-    auto* priv = CURRENT_CALL_VIEW_GET_PRIVATE(view);
-
-    auto confId = priv->cpp->conversation->confId;
-    if (!confId.isEmpty()) {
-        auto call = (*priv->cpp->accountInfo)->callModel->getCall(confId);
-        switch (call.layout) {
-            case lrc::api::call::Layout::GRID:
-                (*priv->cpp->accountInfo)->callModel->setConferenceLayout(confId, lrc::api::call::Layout::ONE_WITH_SMALL);
-                break;
-            case lrc::api::call::Layout::ONE_WITH_SMALL:
-                (*priv->cpp->accountInfo)->callModel->setConferenceLayout(confId, lrc::api::call::Layout::ONE);
-                break;
-            case lrc::api::call::Layout::ONE:
-                (*priv->cpp->accountInfo)->callModel->setConferenceLayout(confId, lrc::api::call::Layout::GRID);
-                break;
-        }
-    }
-        (*priv->cpp->accountInfo)->callModel->setActiveParticipant(confId, "");
-
-}
-
 static gboolean
 on_timer_fade_timeout(CurrentCallView* view)
 {
@@ -390,7 +367,10 @@ on_button_hangup_clicked(CurrentCallView* view)
 {
     g_return_if_fail(IS_CURRENT_CALL_VIEW(view));
     auto* priv = CURRENT_CALL_VIEW_GET_PRIVATE(view);
-    (*priv->cpp->accountInfo)->callModel->hangUp(priv->cpp->conversation->callId);
+    auto callToStop = priv->cpp->conversation->callId;
+    if (!priv->cpp->conversation->confId.isEmpty())
+        callToStop = priv->cpp->conversation->confId;
+    (*priv->cpp->accountInfo)->callModel->hangUp(callToStop);
 }
 
 static void
@@ -460,13 +440,21 @@ on_togglebutton_mutevideo_clicked(CurrentCallView* view)
 }
 
 static gboolean
-on_mouse_moved(CurrentCallView* view)
+on_mouse_moved(CurrentCallView* view, GdkEvent *event, GtkWidget* widget)
 {
     g_return_val_if_fail(IS_CURRENT_CALL_VIEW(view), FALSE);
+    g_return_val_if_fail(IS_VIDEO_WIDGET(widget), FALSE);
     auto priv = CURRENT_CALL_VIEW_GET_PRIVATE(view);
 
     priv->cpp->time_last_mouse_motion = g_get_monotonic_time();
 
+    // HACK: Cf https://gitlab.gnome.org/GNOME/clutter-gtk/-/issues/11
+    // Because the participants_hovers can't have correct mouse events,
+    // we need to pass the event to the video_widget and check if hovers
+    // need to be notified
+    if (event)
+        video_widget_on_event(VIDEO_WIDGET(priv->video_widget), event);
+
     // since the mouse moved, make sure the controls are shown
     if (clutter_timeline_get_direction(CLUTTER_TIMELINE(priv->cpp->fade_info)) == CLUTTER_TIMELINE_FORWARD) {
         clutter_timeline_set_direction(CLUTTER_TIMELINE(priv->cpp->fade_info), CLUTTER_TIMELINE_BACKWARD);
@@ -560,7 +548,7 @@ on_video_widget_focus(GtkWidget* widget, GtkDirectionType direction, CurrentCall
     // otherwise we want the focus to go to and change between the call control buttons
     if (gtk_widget_child_focus(GTK_WIDGET(priv->hbox_call_controls), direction)) {
         // selected a child, make sure call controls are shown
-        on_mouse_moved(view);
+        on_mouse_moved(view, nullptr, widget);
         return TRUE;
     }
 
@@ -962,6 +950,7 @@ CppImpl::CppImpl(CurrentCallView& widget, const lrc::api::Lrc& lrc)
 CppImpl::~CppImpl()
 {
     QObject::disconnect(state_change_connection);
+    QObject::disconnect(layout_change_connection);
     QObject::disconnect(update_vcard_connection);
     QObject::disconnect(renderer_connection);
     QObject::disconnect(smartinfo_refresh_connection);
@@ -1095,7 +1084,6 @@ CppImpl::add_media_handler(lrc::api::plugin::MediaHandlerDetails mediaHandlerDet
 void
 CppImpl::updatePluginList()
 {
-    auto* priv = CURRENT_CALL_VIEW_GET_PRIVATE(self);
     auto row = 0;
     while (GtkWidget* children = GTK_WIDGET(gtk_list_box_get_row_at_index(GTK_LIST_BOX(widgets->list_media_handlers_available), row))) {
         gtk_container_remove(GTK_CONTAINER(widgets->list_media_handlers_available), children);
@@ -1413,7 +1401,9 @@ CppImpl::setCallInfo()
         try {
             auto call = (*accountInfo)->callModel->getCall(callToRender);
             video_widget_set_preview_visible(VIDEO_WIDGET(widgets->video_widget),
-                (call.status != lrc::api::call::Status::PAUSED) && conversation->confId.isEmpty());
+                        call.status != lrc::api::call::Status::PAUSED
+                        && call.type != lrc::api::call::Type::CONFERENCE
+                        && call.participantsInfos.empty());
         } catch (...) {
             g_warning("Can't change preview visibility for non existant call");
         }
@@ -1428,7 +1418,7 @@ CppImpl::setCallInfo()
         // The renderer doesn't exist for now. Ignore
     }
 
-
+    update_participants_hovers(callToRender);
 
     // callback for local renderer
     renderer_connection = QObject::connect(
@@ -1445,7 +1435,9 @@ CppImpl::setCallInfo()
                             avModel_, previewRenderer, VIDEO_RENDERER_LOCAL);
                         auto call = (*accountInfo)->callModel->getCall(callToRender);
                         video_widget_set_preview_visible(VIDEO_WIDGET(widgets->video_widget),
-                            (call.status != lrc::api::call::Status::PAUSED));
+                            call.status != lrc::api::call::Status::PAUSED
+                            && call.type != lrc::api::call::Type::CONFERENCE
+                            && call.participantsInfos.empty());
                     }
                 } catch (...) {
                     g_warning("Preview renderer is not accessible! This should not happen");
@@ -1473,11 +1465,16 @@ CppImpl::setCallInfo()
         &*(*accountInfo)->callModel,
         &lrc::api::NewCallModel::callStatusChanged,
         [this] (const QString& callId) {
+            auto callToRender = conversation->callId;
+            if (!conversation->confId.isEmpty())
+                callToRender = conversation->confId;
             if (callId == conversation->callId) {
                 try {
-                    auto call = (*accountInfo)->callModel->getCall(callId);
+                    auto call = (*accountInfo)->callModel->getCall(callToRender);
                     video_widget_set_preview_visible(VIDEO_WIDGET(widgets->video_widget),
-                        (call.status != lrc::api::call::Status::PAUSED));
+                        call.status != lrc::api::call::Status::PAUSED
+                        && call.type != lrc::api::call::Type::CONFERENCE
+                        && call.participantsInfos.empty());
                 } catch (...) {
                     g_warning("Can't set preview visible for inexistant call");
                 }
@@ -1486,6 +1483,13 @@ CppImpl::setCallInfo()
             }
         });
 
+    layout_change_connection = QObject::connect(
+        &*(*accountInfo)->callModel,
+        &lrc::api::NewCallModel::onParticipantsChanged,
+        [this] (const QString& callId) {
+            update_participants_hovers(callId);
+        });
+
     update_vcard_connection = QObject::connect(
         &*(*accountInfo)->contactModel,
         &lrc::api::ContactModel::contactAdded,
@@ -1552,6 +1556,7 @@ CppImpl::insertControls()
     clutter_actor_add_child(stage, actor_controls);
     clutter_actor_set_x_align(actor_controls, CLUTTER_ACTOR_ALIGN_CENTER);
     clutter_actor_set_y_align(actor_controls, CLUTTER_ACTOR_ALIGN_END);
+    clutter_actor_set_z_position(actor_controls, 4); // Controls should be in front of the hovers
 
     clutter_actor_add_child(stage, actor_smartInfo);
     clutter_actor_set_x_align(actor_smartInfo, CLUTTER_ACTOR_ALIGN_END);
@@ -1602,7 +1607,6 @@ CppImpl::insertControls()
 
     /* toggle whether or not the chat is displayed */
     g_signal_connect(widgets->togglebutton_chat, "toggled", G_CALLBACK(on_togglebutton_chat_toggled), self);
-    g_signal_connect(widgets->togglebutton_view, "toggled", G_CALLBACK(on_togglebutton_view_toggled), self);
 
     /* bind the chat orientation to the gsetting */
     widgets->settings = g_settings_new_full(get_settings_schema(), nullptr, nullptr);
@@ -1669,6 +1673,61 @@ CppImpl::updateDetails()
     update_view();
 }
 
+void
+CppImpl::update_participants_hovers(const QString& callId)
+{
+    if (callId == conversation->callId or callId == conversation->confId) {
+        // Remove previouses hovers, because they are now invalid
+        video_widget_remove_hovers(
+            VIDEO_WIDGET(widgets->video_widget)
+        );
+        // Update callInfo for the video_widget
+        video_widget_set_call_info(VIDEO_WIDGET(widgets->video_widget), *accountInfo, callId);
+        try {
+            auto call = (*accountInfo)->callModel->getCall(callId);
+            // Create participant hovers
+            for (const auto& participant: call.participantsInfos) {
+                QJsonObject data;
+                data["x"] = participant["x"].toInt();
+                data["y"] = participant["y"].toInt();
+                data["w"] = participant["w"].toInt();
+                data["h"] = participant["h"].toInt();
+                data["active"] = participant["active"] == "true";
+                auto bestName = participant["uri"];
+                data["isLocal"] = false;
+                if (bestName == (*accountInfo)->profileInfo.uri) {
+                    bestName = _("me");
+                    data["isLocal"] = true;
+                } else {
+                    try {
+                        auto &contact = (*accountInfo)->contactModel->getContact(participant["uri"]);
+                        bestName = contact.profileInfo.alias;
+                        if (bestName.isEmpty())
+                            bestName = contact.registeredName;
+                        if (bestName.isEmpty())
+                            bestName = contact.profileInfo.uri;
+                        bestName.remove('\r');
+                    } catch (...) {}
+                }
+                data["bestName"] = bestName;
+                data["uri"] = participant["uri"];
+                video_widget_add_participant_hover(
+                    VIDEO_WIDGET(widgets->video_widget),
+                    data
+                );
+            }
+            // Update preview visibility, show preview if the call is not hold and
+            // not a conference host or participant
+            video_widget_set_preview_visible(VIDEO_WIDGET(widgets->video_widget),
+                        call.status != lrc::api::call::Status::PAUSED
+                        && call.type != lrc::api::call::Type::CONFERENCE
+                        && call.participantsInfos.empty());
+        } catch (...) {
+            g_warning("Can't set preview visible for inexistant call");
+        }
+    }
+}
+
 void
 CppImpl::updateState()
 {
@@ -1720,11 +1779,6 @@ CppImpl::updateState()
     } catch (std::out_of_range& e) {
         g_warning("Can't update state for callId=%s", qUtf8Printable(callId));
     }
-
-    if (conversation->confId.isEmpty())
-        gtk_widget_hide(widgets->togglebutton_view);
-    else
-        gtk_widget_show(widgets->togglebutton_view);
 }
 
 void
@@ -1957,7 +2011,6 @@ current_call_view_class_init(CurrentCallViewClass *klass)
     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, frame_video);
     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, frame_chat);
     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, togglebutton_chat);
-    gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, togglebutton_view);
     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, togglebutton_add_participant);
     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, togglebutton_activate_plugin);
     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, togglebutton_transfer);
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index 8e34e6e7..8cfa2e6e 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -2409,9 +2409,6 @@ CppImpl::slotShowCallView(const std::string& id, lrc::api::conversation::Info or
             return;
     }
 
-    if (!origin.confId.isEmpty())
-        accountInfo_->callModel->setActiveParticipant(origin.confId, origin.callId);
-
     changeView(CURRENT_CALL_VIEW_TYPE, origin);
 }
 
diff --git a/src/video/video_widget.cpp b/src/video/video_widget.cpp
index 884d1731..b8279769 100644
--- a/src/video/video_widget.cpp
+++ b/src/video/video_widget.cpp
@@ -32,6 +32,10 @@
 
 // LRC
 #include <api/avmodel.h>
+#include <api/call.h>
+#include <api/newcallmodel.h>
+#include <api/newaccountmodel.h>
+#include <api/conversationmodel.h>
 #include <smartinfohub.h>
 #include <QSize>
 
@@ -48,6 +52,11 @@ static constexpr const char* JOIN_CALL_KEY = "call_data";
  * receive video frames faster than that */
 static constexpr int FRAME_RATE_PERIOD           = 30;
 
+namespace { namespace details
+{
+class CppImpl;
+}}
+
 enum SnapshotStatus {
     NOTHING,
     HAS_TO_TAKE_ONE,
@@ -89,6 +98,9 @@ struct _VideoWidgetPrivate {
     GtkWidget               *popup_menu;
 
     lrc::api::AVModel* avModel_;
+    details::CppImpl* cpp; ///< Non-UI and C++ only code
+
+    GtkWidget *actions_popover;
 };
 
 struct _VideoWidgetRenderer {
@@ -115,6 +127,24 @@ G_DEFINE_TYPE_WITH_PRIVATE(VideoWidget, video_widget, GTK_CLUTTER_TYPE_EMBED);
 
 #define VIDEO_WIDGET_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), VIDEO_WIDGET_TYPE, VideoWidgetPrivate))
 
+namespace { namespace details {
+
+class CppImpl
+{
+public:
+    explicit CppImpl(VideoWidget& widget) : self(&widget) {}
+
+    std::mutex hoversMtx_ {};
+    std::map<std::string, ClutterActor*> hovers_ {};
+    std::map<std::string, QJsonObject> hoversInfos_ {};
+    VideoWidget* self = nullptr; // The GTK widget itself
+    AccountInfoPointer const *accountInfo = nullptr;
+    QString callId {};
+};
+
+}
+}
+
 /* static prototypes */
 static gboolean check_frame_queue              (VideoWidget *);
 static void     renderer_stop                  (VideoWidgetRenderer *);
@@ -160,6 +190,9 @@ video_widget_dispose(GObject *object)
         priv->new_renderer_queue = NULL;
     }
 
+    delete priv->cpp;
+    priv->cpp = nullptr;
+
     gtk_widget_destroy(priv->popup_menu);
 
     G_OBJECT_CLASS(video_widget_parent_class)->dispose(object);
@@ -287,6 +320,294 @@ on_drag_end(G_GNUC_UNUSED ClutterDragAction   *action,
     clutter_actor_add_constraint(actor, constraint_y);
 }
 
+static void
+on_hangup(GtkButton *button, VideoWidget *self)
+{
+    g_return_if_fail(IS_VIDEO_WIDGET(self));
+    VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self);
+    QString uri = QString::fromStdString((gchar*)g_object_get_data(G_OBJECT(button), "uri"));
+    try {
+        auto& callModel = (*priv->cpp->accountInfo)->callModel;
+        auto call = callModel->getCall(priv->cpp->callId);
+        auto callId = "";
+        auto conversations = (*priv->cpp->accountInfo)->conversationModel->allFilteredConversations();
+        for (const auto& conversation: conversations) {
+            if (conversation.participants.empty()) continue;
+            auto participant = conversation.participants.front();
+            if (uri == participant) {
+                callModel->hangUp(conversation.callId);
+                return;
+            }
+        }
+    } catch (...) {}
+}
+
+static void
+on_maximize(GObject *button, VideoWidget *self)
+{
+    g_return_if_fail(IS_VIDEO_WIDGET(self));
+    VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self);
+    QString uri = QString::fromStdString((gchar*)g_object_get_data(G_OBJECT(button), "uri"));
+    bool active = (bool)g_object_get_data(G_OBJECT(button), "active");
+    try {
+        auto& callModel = (*priv->cpp->accountInfo)->callModel;
+        auto call = callModel->getCall(priv->cpp->callId);
+        QString callId = "";
+        auto conversations = (*priv->cpp->accountInfo)->conversationModel->allFilteredConversations();
+        for (const auto& conversation: conversations) {
+            if (conversation.participants.empty()) continue;
+            auto participant = conversation.participants.front();
+            if (uri == participant) {
+                callId = conversation.callId;
+                break;
+            }
+        }
+        switch (call.layout) {
+            case lrc::api::call::Layout::GRID:
+                callModel->setActiveParticipant(priv->cpp->callId, callId);
+                callModel->setConferenceLayout(priv->cpp->callId, lrc::api::call::Layout::ONE_WITH_SMALL);
+                break;
+            case lrc::api::call::Layout::ONE_WITH_SMALL:
+                callModel->setActiveParticipant(priv->cpp->callId, callId);
+                callModel->setConferenceLayout(priv->cpp->callId,
+                    active? lrc::api::call::Layout::ONE : lrc::api::call::Layout::ONE_WITH_SMALL);
+                break;
+            case lrc::api::call::Layout::ONE:
+                callModel->setActiveParticipant(priv->cpp->callId, callId);
+                callModel->setConferenceLayout(priv->cpp->callId, lrc::api::call::Layout::GRID);
+                break;
+        };
+    } catch (...) {}
+}
+
+static void
+on_minimize(GtkButton *button, VideoWidget *self)
+{
+    g_return_if_fail(IS_VIDEO_WIDGET(self));
+    VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self);
+    QString uri = QString::fromStdString((gchar*)g_object_get_data(G_OBJECT(button), "uri"));
+    try {
+        auto& callModel = (*priv->cpp->accountInfo)->callModel;
+        auto call = callModel->getCall(priv->cpp->callId);
+        switch (call.layout) {
+            case lrc::api::call::Layout::GRID:
+                break;
+            case lrc::api::call::Layout::ONE_WITH_SMALL:
+                callModel->setConferenceLayout(priv->cpp->callId, lrc::api::call::Layout::GRID);
+                break;
+            case lrc::api::call::Layout::ONE:
+                callModel->setConferenceLayout(priv->cpp->callId, lrc::api::call::Layout::ONE_WITH_SMALL);
+                break;
+        };
+    } catch (...) {}
+}
+
+static void
+on_show_actions_popover(GtkButton *button, VideoWidget *self)
+{
+    g_return_if_fail(IS_VIDEO_WIDGET(self));
+    VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self);
+    GtkStyleContext* context;
+
+    priv->actions_popover = gtk_popover_new(GTK_WIDGET(self));
+    gtk_popover_set_relative_to(GTK_POPOVER(priv->actions_popover), GTK_WIDGET(button));
+
+    auto* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+    bool isLocal = (bool)g_object_get_data(G_OBJECT(button), "isLocal");
+    bool active = (bool)g_object_get_data(G_OBJECT(button), "active");
+    auto* uri = g_object_get_data(G_OBJECT(button), "uri");
+    g_object_set_data(G_OBJECT(priv->actions_popover), "uri", uri);
+    if (!isLocal) {
+        auto* hangupBtn = gtk_button_new();
+        gtk_button_set_label(GTK_BUTTON(hangupBtn), _("Hangup"));
+        gtk_box_pack_start(GTK_BOX(box), hangupBtn, TRUE, TRUE, 0);
+        g_object_set_data(G_OBJECT(hangupBtn), "uri", uri);
+        context = gtk_widget_get_style_context(hangupBtn);
+        gtk_style_context_add_class(context, "options-btn");
+        auto image = gtk_image_new_from_icon_name("call-stop-symbolic", GTK_ICON_SIZE_BUTTON);
+        gtk_button_set_relief(GTK_BUTTON(hangupBtn), GTK_RELIEF_NONE);
+        gtk_button_set_image(GTK_BUTTON(hangupBtn), image);
+        gtk_button_set_alignment(GTK_BUTTON(hangupBtn), 0, -1);
+        g_signal_connect(hangupBtn, "clicked", G_CALLBACK(on_hangup), self);
+    }
+    try {
+        auto call = (*priv->cpp->accountInfo)->callModel->getCall(priv->cpp->callId);
+        if (call.layout != lrc::api::call::Layout::ONE) {
+            auto* maxBtn = gtk_button_new();
+            gtk_button_set_label(GTK_BUTTON(maxBtn), _("Maximize"));
+            gtk_box_pack_start(GTK_BOX(box), maxBtn, FALSE, TRUE, 0);
+            g_object_set_data(G_OBJECT(maxBtn), "uri", uri);
+            g_object_set_data(G_OBJECT(maxBtn), "active", (void*)active);
+            context = gtk_widget_get_style_context(maxBtn);
+            gtk_style_context_add_class(context, "options-btn");
+            auto image = gtk_image_new_from_icon_name("view-fullscreen-symbolic", GTK_ICON_SIZE_BUTTON);
+            gtk_button_set_relief(GTK_BUTTON(maxBtn), GTK_RELIEF_NONE);
+            gtk_button_set_image(GTK_BUTTON(maxBtn), image);
+            gtk_button_set_alignment(GTK_BUTTON(maxBtn), 0, -1);
+            g_signal_connect(maxBtn, "clicked", G_CALLBACK(on_maximize), self);
+        }
+        if (!(call.layout == lrc::api::call::Layout::GRID
+            || (call.layout == lrc::api::call::Layout::ONE_WITH_SMALL && !active))) {
+            auto* minBtn = gtk_button_new();
+            gtk_button_set_label(GTK_BUTTON(minBtn), _("Minimize"));
+            context = gtk_widget_get_style_context(minBtn);
+            gtk_style_context_add_class(context, "options-btn");
+            gtk_box_pack_start(GTK_BOX(box), minBtn, FALSE, TRUE, 0);
+            g_object_set_data(G_OBJECT(minBtn), "uri", uri);
+            g_object_set_data(G_OBJECT(minBtn), "active", (void*)active);
+            auto image = gtk_image_new_from_icon_name("view-restore-symbolic", GTK_ICON_SIZE_BUTTON);
+            gtk_button_set_relief(GTK_BUTTON(minBtn), GTK_RELIEF_NONE);
+            gtk_button_set_image(GTK_BUTTON(minBtn), image);
+            gtk_button_set_alignment(GTK_BUTTON(minBtn), 0, -1);
+            g_signal_connect(minBtn, "clicked", G_CALLBACK(on_minimize), self);
+        }
+    } catch (...) {}
+    gtk_container_add(GTK_CONTAINER(GTK_POPOVER(priv->actions_popover)), box);
+
+    gtk_widget_set_size_request(priv->actions_popover, -1, -1);
+#if GTK_CHECK_VERSION(3,22,0)
+    gtk_popover_popdown(GTK_POPOVER(priv->actions_popover));
+#else
+    gtk_widget_show_all(GTK_WIDGET(priv->actions_popover));
+#endif
+    gtk_widget_show_all(priv->actions_popover);
+}
+
+void
+video_widget_add_participant_hover(VideoWidget *self, const QJsonObject& participant)
+{
+    VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self);
+    GtkStyleContext* context;
+
+    auto stage = gtk_clutter_embed_get_stage(GTK_CLUTTER_EMBED(self));
+    auto* box_participant = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+    auto* hover_participant = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
+    auto* label_participant = gtk_label_new(participant["bestName"].toString().toLocal8Bit().constData());
+    gtk_label_set_xalign(GTK_LABEL(label_participant), 0);
+    gtk_box_pack_start(GTK_BOX(hover_participant), label_participant, TRUE, TRUE, 0);
+    gtk_widget_set_visible(GTK_WIDGET(label_participant), TRUE);
+
+    auto call = (*priv->cpp->accountInfo)->callModel->getCall(priv->cpp->callId);
+    auto isMaster = call.type == lrc::api::call::Type::CONFERENCE;
+
+    if (isMaster) {
+        auto* options_btn = gtk_button_new();
+
+        GError *error = nullptr;
+        auto image = gtk_image_new();
+        GdkPixbuf* optionsBuf = gdk_pixbuf_new_from_resource_at_scale("/net/jami/JamiGnome/more",
+                                                                    -1, 12, TRUE, &error);
+        if (!optionsBuf) {
+            g_debug("Could not load image: %s", error->message);
+            g_clear_error(&error);
+        } else
+            gtk_image_set_from_pixbuf(GTK_IMAGE(image), optionsBuf);
+
+
+        gtk_button_set_relief(GTK_BUTTON(options_btn), GTK_RELIEF_NONE);
+        gtk_widget_set_tooltip_text(options_btn, _("More options"));
+        gtk_button_set_image(GTK_BUTTON(options_btn), image);
+        g_object_set_data(G_OBJECT(options_btn), "uri", (void*)g_strdup(qUtf8Printable(participant["uri"].toString())));
+        g_object_set_data(G_OBJECT(options_btn), "isLocal", (void*)participant["isLocal"].toBool());
+        g_object_set_data(G_OBJECT(options_btn), "active", (void*)participant["active"].toBool());
+        g_signal_connect(options_btn, "clicked", G_CALLBACK(on_show_actions_popover), self);
+
+        gtk_box_pack_start(GTK_BOX(hover_participant), options_btn, FALSE, TRUE, 0);
+        gtk_widget_set_visible(GTK_WIDGET(options_btn), TRUE);
+        context = gtk_widget_get_style_context(options_btn);
+        gtk_style_context_add_class(context, "options-btn");
+    }
+
+    gtk_box_pack_end(GTK_BOX(box_participant), hover_participant, FALSE, FALSE, 0);
+    gtk_widget_set_visible(GTK_WIDGET(hover_participant), TRUE);
+    gtk_widget_set_size_request(GTK_WIDGET(hover_participant), -1, 32);
+
+    context = gtk_widget_get_style_context(hover_participant);
+    gtk_style_context_add_class(context, "participant-hover");
+    context = gtk_widget_get_style_context(label_participant);
+    gtk_style_context_add_class(context, "label-hover");
+    auto* actor_info = gtk_clutter_actor_new_with_contents(GTK_WIDGET(box_participant));
+
+    g_object_set_data(G_OBJECT(actor_info), "uri", (void*)g_strdup(qUtf8Printable(participant["uri"].toString())));
+    g_object_set_data(G_OBJECT(actor_info), "isLocal", (void*)participant["isLocal"].toBool());
+    g_object_set_data(G_OBJECT(actor_info), "active", (void*)participant["active"].toBool());
+
+    clutter_actor_add_child(stage, actor_info);
+    clutter_actor_set_y_align(actor_info, CLUTTER_ACTOR_ALIGN_START);
+    clutter_actor_set_x_align(actor_info, CLUTTER_ACTOR_ALIGN_START);
+    {
+        std::lock_guard<std::mutex> lk(priv->cpp->hoversMtx_);
+        priv->cpp->hoversInfos_[participant["uri"].toString().toStdString()] = participant;
+        priv->cpp->hovers_[participant["uri"].toString().toStdString()] = actor_info;
+    }
+    clutter_actor_hide(actor_info);
+}
+
+void
+video_widget_set_call_info(VideoWidget *self, AccountInfoPointer const & accountInfo, const QString& callId)
+{
+    VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self);
+    if (not priv or not priv->cpp)
+        return;
+    priv->cpp->callId = callId;
+    priv->cpp->accountInfo = &accountInfo;
+}
+
+
+void
+video_widget_remove_hovers(VideoWidget *self)
+{
+    VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self);
+    std::lock_guard<std::mutex> lk(priv->cpp->hoversMtx_);
+    priv->cpp->hoversInfos_.clear();
+    for (const auto& [uri, actor]: priv->cpp->hovers_)
+        clutter_actor_destroy(actor);
+    priv->cpp->hovers_.clear();
+}
+
+void
+video_widget_on_event(VideoWidget *self, GdkEvent* event)
+{
+    VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self);
+    g_return_if_fail(priv && event);
+
+    guint button;
+    // Ignore right click
+    if (gdk_event_get_button(event, &button) && button == GDK_BUTTON_SECONDARY)
+        return;
+
+    // HACK-HACK-HACK-HACK-HACK
+    // https://gitlab.gnome.org/GNOME/clutter-gtk/-/issues/11
+    // Retrieve real coordinates in widget and avoid coordinates
+    // in hovers
+    GdkDisplay *display = gdk_display_get_default ();
+    GdkDeviceManager *device_manager = gdk_display_get_device_manager (display);
+    GdkDevice *device = gdk_device_manager_get_client_pointer (device_manager);
+    int dx, dy;
+    gdk_device_get_position (device, NULL, &dx, &dy);
+    gint wx, wy;
+    gdk_window_get_origin (gtk_widget_get_window(GTK_WIDGET(self)), &wx, &wy);
+    int posX = dx - wx;
+    int posY = dy - wy;
+
+    std::lock_guard<std::mutex> lk(priv->cpp->hoversMtx_);
+    for (const auto& [uri, actor]: priv->cpp->hovers_) {
+        if (!CLUTTER_IS_ACTOR(actor)) return;
+        gfloat x = clutter_actor_get_x(actor), y = clutter_actor_get_y(actor), w, h;
+        clutter_actor_get_size(actor, &w, &h);
+        if (posX >= x && posX <= x + w && posY >= y && posY <= y + h) {
+            // The mouse is in the hover
+            if (event->type == GDK_BUTTON_PRESS) {
+                // Let the button clickable without maximizing the participant
+                if (!(posX >= x + w - 12 && posY >= y + h - 12))
+                    on_maximize(G_OBJECT(actor), self);
+            }
+            clutter_actor_show(actor);
+        } else {
+            clutter_actor_hide(actor);
+        }
+    }
+}
 
 /*
  * video_widget_init()
@@ -738,6 +1059,31 @@ check_frame_queue(VideoWidget *self)
     if (priv->show_preview)
         clutter_render_image(priv->local);
     clutter_render_image(priv->remote);
+
+    // HACK: https://gitlab.gnome.org/GNOME/clutter-gtk/-/issues/11
+    // Because the CLUTTER_CONTENT_GRAVITY_RESIZE_ASPECT change the ratio of the widget inside the actor
+    // and we can't get the real dimensions of the rendered renderer, we need to
+    // re-calculate the real dimensions the actor has
+    if (priv->remote->actor && priv->remote->v_renderer) {
+        auto zoomX = priv->remote->v_renderer->size().rwidth() / clutter_actor_get_width(priv->remote->actor);
+        auto zoomY = priv->remote->v_renderer->size().rheight() / clutter_actor_get_height(priv->remote->actor);
+        auto zoom = std::max(zoomX, zoomY);
+        auto real_width = priv->remote->v_renderer->size().rwidth() / zoom;
+        auto real_height = priv->remote->v_renderer->size().rheight() / zoom;
+        auto offsetY = (clutter_actor_get_height(priv->remote->actor) - real_height) / 2;
+        auto offsetX = (clutter_actor_get_width(priv->remote->actor) - real_width) / 2;
+
+        std::lock_guard<std::mutex> lk(priv->cpp->hoversMtx_);
+        for (const auto& [uri, actor] : priv->cpp->hovers_) {
+            auto participant = priv->cpp->hoversInfos_[uri];
+
+            clutter_actor_set_height(actor, participant["h"].toInt() / zoom);
+            clutter_actor_set_width(actor, participant["w"].toInt() / zoom);
+            clutter_actor_set_x(actor, offsetX + participant["x"].toInt() / zoom);
+            clutter_actor_set_y(actor, offsetY + participant["y"].toInt() / zoom);
+        }
+    }
+
     if (priv->remote->snapshot_status == HAS_A_NEW_ONE) {
         priv->remote->snapshot_status = NOTHING;
         g_signal_emit(G_OBJECT(self), video_widget_signals[SNAPSHOT_SIGNAL], 0);
@@ -841,6 +1187,19 @@ GtkWidget*
 video_widget_new(void)
 {
     GtkWidget *self = (GtkWidget *)g_object_new(VIDEO_WIDGET_TYPE, NULL);
+    auto* priv = VIDEO_WIDGET_GET_PRIVATE(self);
+    // CSS styles
+    auto provider = gtk_css_provider_new();
+    std::string css = ".participant-hover { background: rgba(0,0,0,0.5); color: white; padding-left: 8; font-size: .8em;}\
+    .options-btn:hover {background: rgba(0,0,0,0); border: 0;} \
+    .options-btn {background: rgba(0,0,0,0); border: 0;} \
+    .label-hover { color: white; }";
+    gtk_css_provider_load_from_data(provider, css.c_str(), -1, nullptr);
+    gtk_style_context_add_provider_for_screen(gdk_display_get_default_screen(gdk_display_get_default()),
+                                              GTK_STYLE_PROVIDER(provider),
+                                              GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+    priv->cpp = new details::CppImpl(*VIDEO_WIDGET(self));
+
     return self;
 }
 
@@ -906,5 +1265,9 @@ video_widget_set_preview_visible(VideoWidget *self, bool show)
     VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self);
     if (priv) {
         priv->show_preview = show;
+        if (!show)
+            clutter_actor_hide(priv->local->actor);
+        else
+            clutter_actor_show(priv->local->actor);
     }
 }
\ No newline at end of file
diff --git a/src/video/video_widget.h b/src/video/video_widget.h
index 786233b8..e2f87697 100644
--- a/src/video/video_widget.h
+++ b/src/video/video_widget.h
@@ -23,6 +23,9 @@
 #include <gtk/gtk.h>
 #include <video/renderer.h>
 #include <api/newvideo.h>
+#include <QJsonObject>
+
+#include "../accountinfopointer.h"
 
 namespace lrc
 {
@@ -67,6 +70,10 @@ gboolean        video_widget_on_button_press_in_screen_event (VideoWidget *self,
 void            video_widget_take_snapshot (VideoWidget *self);
 GdkPixbuf*      video_widget_get_snapshot  (VideoWidget *self);
 void            video_widget_set_preview_visible (VideoWidget *self, bool show);
+void            video_widget_add_participant_hover(VideoWidget *self, const QJsonObject& participant);
+void            video_widget_set_call_info(VideoWidget *self, AccountInfoPointer const & accountInfo, const QString& callId);
+void            video_widget_remove_hovers(VideoWidget *self);
+void            video_widget_on_event(VideoWidget *self, GdkEvent* event);
 
 G_END_DECLS
 
diff --git a/ui/currentcallview.ui b/ui/currentcallview.ui
index a2e50ff3..82fd0ef1 100644
--- a/ui/currentcallview.ui
+++ b/ui/currentcallview.ui
@@ -442,31 +442,6 @@
         <property name="fill">True</property>
       </packing>
     </child>
-    <child>
-      <object class="GtkToggleButton" id="togglebutton_view">
-        <style>
-          <class name="call-button"/>
-        </style>
-        <property name="visible">True</property>
-        <property name="sensitive">True</property>
-        <property name="can_focus">True</property>
-        <property name="width-request">48</property>
-        <property name="height-request">48</property>
-        <property name="has_tooltip">True</property>
-        <property name="relief">normal</property>
-        <property name="tooltip-text" translatable="yes">Toggle view</property>
-        <property name="image">image_view</property>
-        <child internal-child="accessible">
-          <object class="AtkObject" id="togglebutton_view-atkobject">
-            <property name="AtkObject::accessible-name" translatable="yes">Change video layout</property>
-          </object>
-        </child>
-      </object>
-      <packing>
-        <property name="expand">False</property>
-        <property name="fill">True</property>
-      </packing>
-    </child>
     <child>
       <object class="GtkToggleButton" id="togglebutton_chat">
         <style>
-- 
GitLab