From 71f1beeef54459b4a77b7049a8b38d73845fa942 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Blin?=
 <sebastien.blin@savoirfairelinux.com>
Date: Fri, 26 Jul 2019 12:00:51 -0400
Subject: [PATCH] currencallview: improve conference UI

Add a button to call a contact or easily join calls

Change-Id: I88c94f8a10a49393ef76237b70ca65380f8dc307
Gitlab: #1052
---
 pixmaps/ic_people_black_24px.svg     |   5 +-
 pixmaps/ic_person_add_white_24px.svg |   4 +
 pixmaps/pixmaps.gresource.xml        |   1 +
 src/currentcallview.cpp              | 513 +++++++++++++++++++++++++--
 src/currentcallview.h                |   4 +-
 src/ringmainwindow.cpp               |   6 +-
 src/video/video_widget.cpp           |  40 ++-
 src/video/video_widget.h             |   2 +-
 ui/currentcallview.ui                |  92 +++++
 9 files changed, 606 insertions(+), 61 deletions(-)
 create mode 100644 pixmaps/ic_person_add_white_24px.svg

diff --git a/pixmaps/ic_people_black_24px.svg b/pixmaps/ic_people_black_24px.svg
index 58befee7..6bcb8591 100644
--- a/pixmaps/ic_people_black_24px.svg
+++ b/pixmaps/ic_people_black_24px.svg
@@ -1,4 +1 @@
-<svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
-    <path d="M0 0h24v24H0z" fill="none"/>
-    <path d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z"/>
-</svg>
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z"/></svg>
\ No newline at end of file
diff --git a/pixmaps/ic_person_add_white_24px.svg b/pixmaps/ic_person_add_white_24px.svg
new file mode 100644
index 00000000..57bf52d4
--- /dev/null
+++ b/pixmaps/ic_person_add_white_24px.svg
@@ -0,0 +1,4 @@
+<svg fill="#ffffff" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+    <path d="M0 0h24v24H0z" fill="none"/>
+    <path d="M15 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm-9-2V7H4v3H1v2h3v3h2v-3h3v-2H6zm9 4c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
+</svg>
\ No newline at end of file
diff --git a/pixmaps/pixmaps.gresource.xml b/pixmaps/pixmaps.gresource.xml
index 61ae7a76..f641e566 100644
--- a/pixmaps/pixmaps.gresource.xml
+++ b/pixmaps/pixmaps.gresource.xml
@@ -36,6 +36,7 @@
     <file alias="block">block.svg</file>
     <file alias="block_black">block_black.svg</file>
     <file alias="invite">ic_person_add_black_24px.svg</file>
+    <file alias="invite_white">ic_person_add_white_24px.svg</file>
     <file alias="temporary-item">ic_search_black_48px.svg</file>
     <file alias="audio_only_call_start">ic_call_black_24px.svg</file>
     <file alias="fallbackavatar">fallbackavatar.svg</file>
diff --git a/src/currentcallview.cpp b/src/currentcallview.cpp
index d8dc69ea..97cbba20 100644
--- a/src/currentcallview.cpp
+++ b/src/currentcallview.cpp
@@ -19,13 +19,17 @@
 
 #include "currentcallview.h"
 
-// Gtk
-#include <clutter-gtk/clutter-gtk.h>
-#include <gtk/gtk.h>
-#include <glib/gi18n.h>
+ // Client
+#include "chatview.h"
+#include "native/pixbufmanipulator.h"
+#include "ringnotify.h"
+#include "utils/drawing.h"
+#include "utils/files.h"
+#include "video/video_widget.h"
 
 // Lrc
 #include <api/avmodel.h>
+#include <api/newaccountmodel.h>
 #include <api/conversationmodel.h>
 #include <api/contact.h>
 #include <api/contactmodel.h>
@@ -33,15 +37,22 @@
 #include <api/newcodecmodel.h>
 #include <globalinstances.h>
 #include <smartinfohub.h>
+
+// Gtk
+#include <clutter-gtk/clutter-gtk.h>
+#include <gtk/gtk.h>
+#include <glib/gi18n.h>
+
 #include <QSize>
 
-// Client
-#include "chatview.h"
-#include "native/pixbufmanipulator.h"
-#include "ringnotify.h"
-#include "utils/drawing.h"
-#include "utils/files.h"
-#include "video/video_widget.h"
+#include <set>
+
+enum class RowType {
+    CONTACT,
+    CALL,
+    CONFERENCE,
+    TITLE
+};
 
 namespace { namespace details
 {
@@ -80,10 +91,14 @@ struct CurrentCallViewPrivate
     GtkWidget *togglebutton_chat;
     GtkWidget *togglebutton_muteaudio;
     GtkWidget *togglebutton_mutevideo;
+    GtkWidget *togglebutton_add_participant;
     GtkWidget *togglebutton_transfer;
-    GtkWidget* siptransfer_popover;
-    GtkWidget* siptransfer_filter_entry;
-    GtkWidget* list_conversations;
+    GtkWidget *siptransfer_popover;
+    GtkWidget *siptransfer_filter_entry;
+    GtkWidget *list_conversations;
+    GtkWidget *add_participant_popover;
+    GtkWidget *conversation_filter_entry;
+    GtkWidget *list_conversations_invite;
     GtkWidget *togglebutton_hold;
     GtkWidget *togglebutton_record;
     GtkWidget *button_hangup;
@@ -191,7 +206,7 @@ gtk_scale_button_get_scale(GtkScaleButton* button)
 class CppImpl
 {
 public:
-    explicit CppImpl(CurrentCallView& widget);
+    explicit CppImpl(CurrentCallView& widget, const lrc::api::Lrc& lrc);
     ~CppImpl();
 
     void init();
@@ -200,6 +215,9 @@ public:
                lrc::api::conversation::Info* conversation,
                lrc::api::AVModel& avModel);
     void add_transfer_contact(const std::string& uri);
+    void add_title(const std::string& title);
+    void add_present_contact(const std::string& uri, const std::string& custom_data, RowType custom_type, const std::string& accountId);
+    void add_conference(const std::vector<std::string>& uris, const std::string& custom_data, const std::string& accountId);
 
     void insertControls();
     void checkControlsFading();
@@ -229,6 +247,10 @@ public:
     gulong insert_controls_id = 0;
     guint smartinfo_action = 0;
 
+    const lrc::api::Lrc& lrc_;
+
+    std::vector<std::string> titles_;
+    std::set<std::string> hiddenTitles_;
 private:
     CppImpl() = delete;
     CppImpl(const CppImpl&) = delete;
@@ -570,7 +592,7 @@ on_siptransfer_filter_activated(CurrentCallView* self)
 }
 
 static GtkLabel*
-get_sip_address_label(GtkListBoxRow* row)
+get_address_label(GtkListBoxRow* row)
 {
     auto* row_children = gtk_container_get_children(GTK_CONTAINER(row));
     auto* box_infos = g_list_first(row_children)->data;
@@ -578,15 +600,152 @@ get_sip_address_label(GtkListBoxRow* row)
     return GTK_LABEL(g_list_last(children)->data);
 }
 
+static GtkImage*
+get_image(GtkListBoxRow* row)
+{
+    auto* row_children = gtk_container_get_children(GTK_CONTAINER(row));
+    auto* box_infos = g_list_first(row_children)->data;
+    auto* children = gtk_container_get_children(GTK_CONTAINER(box_infos));
+    return GTK_IMAGE(g_list_first(children)->data);
+}
+
 static void
 transfer_to_conversation(GtkListBox*, GtkListBoxRow* row, CurrentCallView* self)
 {
     g_return_if_fail(IS_CURRENT_CALL_VIEW(self));
     auto* priv = CURRENT_CALL_VIEW_GET_PRIVATE(self);
-    auto* sip_address = get_sip_address_label(row);
+    auto* sip_address = get_address_label(row);
     transfer_to_peer(priv, gtk_label_get_text(GTK_LABEL(sip_address)));
 }
 
+static void
+on_search_participant(GtkSearchEntry* search_entry, CurrentCallView* self)
+{
+    g_return_if_fail(IS_CURRENT_CALL_VIEW(self));
+    auto* priv = CURRENT_CALL_VIEW_GET_PRIVATE(self);
+
+    std::string search_text = gtk_entry_get_text(GTK_ENTRY(search_entry));
+    std::transform(search_text.begin(), search_text.end(), search_text.begin(), ::tolower);
+
+    auto row = 0, lastTitleRow = -1;
+    auto hideTitle = true;
+    while (GtkWidget* children = GTK_WIDGET(gtk_list_box_get_row_at_index(
+            GTK_LIST_BOX(priv->list_conversations_invite), row))) {
+        auto* addr_label = get_address_label(GTK_LIST_BOX_ROW(children));
+        std::string content = gtk_label_get_text(addr_label);
+        std::transform(content.begin(), content.end(), content.begin(), ::tolower);
+        if (content.find(search_text) == std::string::npos) {
+            bool hide = true;
+            for (auto title: priv->cpp->titles_) {
+                std::transform(title.begin(), title.end(), title.begin(), ::tolower);
+                if (title == content) {
+                    hide = false;
+                    // Hide last title if needed
+                    if (lastTitleRow != -1 && hideTitle) {
+                        auto* lastTitle = GTK_WIDGET(gtk_list_box_get_row_at_index(GTK_LIST_BOX(priv->list_conversations_invite), lastTitleRow));
+                        gtk_widget_hide(lastTitle);
+                    }
+                    lastTitleRow = row;
+                    hideTitle = true;
+                }
+            }
+            if (hide) gtk_widget_hide(children);
+        } else {
+            if (lastTitleRow != -1 && hideTitle) {
+                auto* lastTitle = GTK_WIDGET(gtk_list_box_get_row_at_index(GTK_LIST_BOX(priv->list_conversations_invite), lastTitleRow));
+                gtk_widget_show(lastTitle);
+            }
+            hideTitle = false;
+            gtk_widget_show(children);
+        }
+        row++;
+    }
+
+    // Hide last title if needed
+    if (lastTitleRow != -1 && hideTitle) {
+        auto* lastTitle = GTK_WIDGET(gtk_list_box_get_row_at_index(GTK_LIST_BOX(priv->list_conversations_invite), lastTitleRow));
+        gtk_widget_hide(lastTitle);
+    }
+}
+
+static void
+invite_to_conversation(GtkListBox*, GtkListBoxRow* row, CurrentCallView* self)
+{
+    auto priv = CURRENT_CALL_VIEW_GET_PRIVATE(self);
+
+    auto* label = get_address_label(GTK_LIST_BOX_ROW(row));
+    std::string content = gtk_label_get_text(label);
+    for (auto title: priv->cpp->titles_) {
+        if (content != title) continue;
+        bool isHiddenTitle = priv->cpp->hiddenTitles_.find(content) != priv->cpp->hiddenTitles_.end();
+        auto* image = get_image(row);
+        if (!isHiddenTitle) {
+            priv->cpp->hiddenTitles_.insert(content);
+            gtk_image_set_from_icon_name(image, "pan-up-symbolic", GTK_ICON_SIZE_MENU);
+        } else {
+            priv->cpp->hiddenTitles_.erase(content);
+            gtk_image_set_from_icon_name(image, "pan-down-symbolic", GTK_ICON_SIZE_MENU);
+        }
+        auto changeState = false;
+        auto rowIdx = 0;
+        while (auto* children = gtk_list_box_get_row_at_index(GTK_LIST_BOX(priv->list_conversations_invite), rowIdx)) {
+            rowIdx++;
+            if (children == row) {
+                changeState = true;
+                continue;
+            }
+            if (!changeState) continue;
+            auto* addr_label = get_address_label(GTK_LIST_BOX_ROW(children));
+            std::string content2 = gtk_label_get_text(addr_label);
+            for (auto title: priv->cpp->titles_) {
+                if (content2 == title) return; // Other title, stop here.
+            }
+            if (!isHiddenTitle)
+                gtk_widget_hide(GTK_WIDGET(children));
+            else {
+                gtk_widget_show(GTK_WIDGET(children));
+                // refilter if needed
+                std::string currentFilter = gtk_entry_get_text(GTK_ENTRY(priv->conversation_filter_entry));
+                if (!currentFilter.empty())
+                    on_search_participant(GTK_SEARCH_ENTRY(priv->conversation_filter_entry), self);
+            }
+        }
+        return;
+    }
+
+    auto callToRender = priv->cpp->conversation->callId;
+    if (!priv->cpp->conversation->confId.empty())
+        callToRender = priv->cpp->conversation->confId;
+
+    auto rowIdx = 0;
+    while (auto* children = gtk_list_box_get_row_at_index(GTK_LIST_BOX(priv->list_conversations_invite), rowIdx)) {
+        if (children == row) {
+            auto* custom_type = g_object_get_data(G_OBJECT(label), "custom_type");
+            std::string custom_data = (gchar*)g_object_get_data(G_OBJECT(label), "custom_data");
+            if (GPOINTER_TO_INT(custom_type) == (int)RowType::CONTACT) {
+                try {
+                    const auto& call = (*priv->cpp->accountInfo)->callModel->getCall(callToRender);
+                    (*priv->cpp->accountInfo)->callModel->callAndAddParticipant(custom_data, callToRender, call.isAudioOnly);
+                } catch (...) {
+                    g_warning("Can't add participant to inexistant call");
+                }
+            } else if (GPOINTER_TO_INT(custom_type)  == (int)RowType::CALL
+                    || GPOINTER_TO_INT(custom_type)  == (int)RowType::CONFERENCE) {
+                (*priv->cpp->accountInfo)->callModel->joinCalls(custom_data, callToRender);
+            }
+            break;
+        }
+        ++rowIdx;
+    }
+
+
+#if GTK_CHECK_VERSION(3,22,0)
+    gtk_popover_popdown(GTK_POPOVER(priv->add_participant_popover));
+#else
+    gtk_widget_hide(GTK_WIDGET(priv->add_participant_popover));
+#endif
+}
+
 static void
 filter_transfer_list(CurrentCallView *self)
 {
@@ -597,7 +756,7 @@ filter_transfer_list(CurrentCallView *self)
 
     auto row = 0;
     while (GtkWidget* children = GTK_WIDGET(gtk_list_box_get_row_at_index(GTK_LIST_BOX(priv->list_conversations), row))) {
-        auto* sip_address = get_sip_address_label(GTK_LIST_BOX_ROW(children));;
+        auto* sip_address = get_address_label(GTK_LIST_BOX_ROW(children));
         if (row == 0) {
             // Update searching item
             if (currentFilter.empty() || currentFilter == priv->cpp->conversation->participants.front()) {
@@ -630,6 +789,22 @@ filter_transfer_list(CurrentCallView *self)
     }
 }
 
+static void
+on_button_add_participant_clicked(CurrentCallView *self)
+{
+    // Show and init list
+    g_return_if_fail(IS_CURRENT_CALL_VIEW(self));
+    auto* priv = CURRENT_CALL_VIEW_GET_PRIVATE(self);
+    gtk_popover_set_relative_to(GTK_POPOVER(priv->add_participant_popover), GTK_WIDGET(priv->togglebutton_add_participant));
+#if GTK_CHECK_VERSION(3,22,0)
+    gtk_popover_popdown(GTK_POPOVER(priv->add_participant_popover));
+#else
+    gtk_widget_show_all(GTK_WIDGET(priv->add_participant_popover));
+#endif
+    gtk_widget_show_all(priv->add_participant_popover);
+    filter_transfer_list(self);
+}
+
 static void
 on_button_transfer_clicked(CurrentCallView *self)
 {
@@ -654,8 +829,9 @@ on_siptransfer_text_changed(GtkSearchEntry*, CurrentCallView* self)
 
 } // namespace gtk_callbacks
 
-CppImpl::CppImpl(CurrentCallView& widget)
+CppImpl::CppImpl(CurrentCallView& widget, const lrc::api::Lrc& lrc)
     : self {&widget}
+    , lrc_ {lrc}
     , widgets {CURRENT_CALL_VIEW_GET_PRIVATE(&widget)}
 {}
 
@@ -667,7 +843,7 @@ CppImpl::~CppImpl()
     QObject::disconnect(smartinfo_refresh_connection);
     g_clear_object(&widgets->settings);
 
-    g_source_remove(timer_fade);
+    if (timer_fade) g_source_remove(timer_fade);
 
     auto* display_smartinfo = g_action_map_lookup_action(G_ACTION_MAP(g_application_get_default()),
                                                         "display-smartinfo");
@@ -718,9 +894,106 @@ CppImpl::setup(WebKitChatContainer* chat_widget,
     avModel_ = &avModel;
     setCallInfo();
 
-    if ((*accountInfo)->profileInfo.type == lrc::api::profile::Type::RING)
+    if ((*accountInfo)->profileInfo.type == lrc::api::profile::Type::RING) {
         gtk_widget_hide(widgets->togglebutton_transfer);
-    else {
+
+        auto callToRender = conversation->callId;
+        if (!conversation->confId.empty())
+            callToRender = conversation->confId;
+
+        std::vector<std::string> callsId;
+        std::vector<std::string> uris;
+        bool first = true;
+        for (const auto& c : lrc_.getConferences()) {
+            // Get subcalls
+            auto cid = lrc_.getConferenceSubcalls(c);
+            callsId.insert(callsId.end(), cid.begin(), cid.end());
+            // Get participants
+            std::string uri, accountId;
+            std::vector<std::string> curis;
+            for (const auto& callId: cid) {
+                for (const auto &account_id : lrc_.getAccountModel().getAccountList()) {
+                    try {
+                        auto &accountInfo = lrc_.getAccountModel().getAccountInfo(account_id);
+                        if (accountInfo.callModel->hasCall(callId)) {
+                            const auto& call = accountInfo.callModel->getCall(callId);
+                            uri = call.peerUri.find("ring:") == std::string::npos ?
+                                call.peerUri : call.peerUri.substr(std::string("ring:").length());
+                            uris.emplace_back(uri);
+                            curis.emplace_back(uri);
+                            accountId = account_id;
+                            break;
+                        }
+                    } catch (...) {}
+                }
+            }
+
+            if (c == callToRender) {
+                continue;
+            }
+
+            if (first) {
+                add_title(_("Current conference (all accounts)"));
+                first = false;
+            }
+            if (!uri.empty()) add_conference(curis, c, accountId);
+        }
+
+        first = true;
+        for (const auto& c : lrc_.getCalls()) {
+            std::string uri, accountId;
+            for (const auto &account_id : lrc_.getAccountModel().getAccountList()) {
+                try {
+                    auto &accountInfo = lrc_.getAccountModel().getAccountInfo(account_id);
+                    if (accountInfo.callModel->hasCall(c)) {
+                        const auto& call = accountInfo.callModel->getCall(c);
+                        if (call.status != lrc::api::call::Status::PAUSED
+                            && call.status != lrc::api::call::Status::IN_PROGRESS) {
+                            // Ignore non active calls
+                            callsId.emplace_back(call.id);
+                            continue;
+                        }
+                        uri = call.peerUri.find("ring:") == std::string::npos ?
+                            call.peerUri : call.peerUri.substr(std::string("ring:").length());
+                        accountId = account_id;
+                        uris.emplace_back(uri);
+                        break;
+                    }
+                } catch (...) {}
+            }
+            auto isPresent = std::find(callsId.cbegin(), callsId.cend(), c) != callsId.cend();
+            if (c == callToRender || isPresent) {
+                continue;
+            }
+
+            if (first) {
+                add_title(_("Current calls (all accounts)"));
+                first = false;
+            }
+            if (!uri.empty()) add_present_contact(uri, c, RowType::CALL, accountId);
+        }
+
+        first = true;
+        for (const auto& c : (*accountInfo)->conversationModel->getFilteredConversations(lrc::api::profile::Type::RING)) {
+            try {
+                auto participant = c.participants.front();
+                auto contactInfo = (*accountInfo)->contactModel->getContact(participant);
+                auto isPresent = std::find(uris.cbegin(), uris.cend(), participant) != uris.cend();
+                if (contactInfo.isPresent && !isPresent) {
+                    if (first) {
+                        add_title(_("Online contacts"));
+                        first = false;
+                    }
+                    add_present_contact(participant, participant, RowType::CONTACT, (*accountInfo)->id);
+                }
+            } catch (...) {}
+        }
+
+
+        for (const auto& c : (*accountInfo)->conversationModel->getFilteredConversations(lrc::api::profile::Type::SIP))
+            add_transfer_contact(c.participants.front());
+        g_signal_connect(widgets->conversation_filter_entry, "search-changed", G_CALLBACK(on_search_participant), self);
+    } else {
         // Remove previous list
         while (GtkWidget* children = GTK_WIDGET(gtk_list_box_get_row_at_index(GTK_LIST_BOX(widgets->list_conversations), 10)))
             gtk_container_remove(GTK_CONTAINER(widgets->list_conversations), children);
@@ -735,6 +1008,156 @@ CppImpl::setup(WebKitChatContainer* chat_widget,
     set_record_animation(widgets);
 }
 
+void
+CppImpl::add_title(const std::string& title) {
+    auto* box_item = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+    auto* avatar = gtk_image_new_from_icon_name("pan-down-symbolic", GTK_ICON_SIZE_MENU);
+    auto* info = gtk_label_new(nullptr);
+    gtk_label_set_markup(GTK_LABEL(info), title.c_str());
+    gtk_widget_set_halign(info, GTK_ALIGN_CENTER);
+    gtk_container_add(GTK_CONTAINER(box_item), GTK_WIDGET(avatar));
+    gtk_container_add(GTK_CONTAINER(box_item), GTK_WIDGET(info));
+    g_object_set(G_OBJECT(info), "ellipsize", PANGO_ELLIPSIZE_END, NULL);
+    gtk_list_box_insert(GTK_LIST_BOX(widgets->list_conversations_invite), GTK_WIDGET(box_item), -1);
+
+    titles_.emplace_back(title);
+}
+
+void
+CppImpl::add_present_contact(const std::string& uri, const std::string& custom_data, RowType custom_type, const std::string& accountId)
+{
+    auto bestName = uri;
+    auto default_avatar = Interfaces::PixbufManipulator().generateAvatar("", "");
+    auto default_scaled = Interfaces::PixbufManipulator().scaleAndFrame(default_avatar.get(), QSize(50, 50));
+    auto photo = default_scaled;
+
+    try {
+        auto &accInfo = lrc_.getAccountModel().getAccountInfo(accountId);
+        auto contactInfo = accInfo.contactModel->getContact(uri);
+        auto photostr = contactInfo.profileInfo.avatar;
+        auto alias = contactInfo.profileInfo.alias;
+
+        if (!alias.empty()) {
+            bestName = alias;
+        } else if (!contactInfo.registeredName.empty()) {
+            bestName = contactInfo.registeredName;
+        }
+
+        if (!photostr.empty()) {
+            QByteArray byteArray(photostr.c_str(), photostr.length());
+            QVariant avatar = Interfaces::PixbufManipulator().personPhoto(byteArray);
+            auto pixbuf_photo = Interfaces::PixbufManipulator().scaleAndFrame(avatar.value<std::shared_ptr<GdkPixbuf>>().get(), QSize(48, 48));
+            if (avatar.isValid()) {
+                photo = pixbuf_photo;
+            }
+        } else {
+            auto name = alias.empty()? contactInfo.registeredName : alias;
+            auto firstLetter = (name == contactInfo.profileInfo.uri || name.empty()) ?
+            "" : QString(QString(name.c_str()).at(0)).toStdString();  // NOTE best way to be compatible with UTF-8
+            auto fullUri = contactInfo.profileInfo.uri;
+            if (accInfo.profileInfo.type != lrc::api::profile::Type::SIP)
+                fullUri = "ring:" + fullUri;
+            else
+                fullUri = "sip:" + fullUri;
+            photo = Interfaces::PixbufManipulator().generateAvatar(firstLetter, fullUri);
+            photo = Interfaces::PixbufManipulator().scaleAndFrame(photo.get(), QSize(48, 48));
+        }
+    } catch (const std::out_of_range&) {
+        // ContactModel::getContact() exception
+    }
+
+    gchar* text = nullptr;
+    if (uri != bestName) {
+        bestName.erase(std::remove(bestName.begin(), bestName.end(), '\r'), bestName.end());
+        bestName.erase(std::remove(bestName.begin(), bestName.end(), '\n'), bestName.end());
+        text = g_markup_printf_escaped(
+            "<span font_weight=\"bold\">%s</span>\n<span size=\"smaller\" color=\"#666\">%s</span>",
+            bestName.c_str(),
+            uri.c_str()
+        );
+    } else {
+        text = g_markup_printf_escaped(
+            "<span font=\"10\">%s</span>",
+            bestName.c_str()
+        );
+    }
+
+    auto* box_item = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+    auto* avatar = gtk_image_new_from_pixbuf(photo.get());
+    auto* info = gtk_label_new(nullptr);
+    gtk_label_set_markup(GTK_LABEL(info), text);
+    gtk_container_add(GTK_CONTAINER(box_item), GTK_WIDGET(avatar));
+    gtk_container_add(GTK_CONTAINER(box_item), GTK_WIDGET(info));
+    g_object_set(G_OBJECT(info), "ellipsize", PANGO_ELLIPSIZE_END, NULL);
+
+    g_object_set_data(G_OBJECT(info), "custom_type", GINT_TO_POINTER(custom_type));
+    g_object_set_data(G_OBJECT(info), "custom_data", (void*)g_strdup(custom_data.c_str()));
+
+    gtk_list_box_insert(GTK_LIST_BOX(widgets->list_conversations_invite), GTK_WIDGET(box_item), -1);
+}
+
+void
+CppImpl::add_conference(const std::vector<std::string>& uris, const std::string& custom_data, const std::string& accountId)
+{
+    GError *error = nullptr;
+    auto default_avatar = std::shared_ptr<GdkPixbuf>(
+        gdk_pixbuf_new_from_resource_at_scale("/net/jami/JamiGnome/contacts_list", 50, 50, true, &error),
+        g_object_unref
+    );
+    if (default_avatar == nullptr) {
+        g_debug("Could not load icon: %s", error->message);
+        g_clear_error(&error);
+        return;
+    }
+    auto default_scaled = Interfaces::PixbufManipulator().scaleAndFrame(default_avatar.get(), QSize(50, 50));
+    auto photo = default_scaled;
+
+    std::string label;
+    auto idx = 0;
+
+    for (const auto& uri: uris) {
+        try {
+            auto bestName = uri;
+            auto &accInfo = lrc_.getAccountModel().getAccountInfo(accountId);
+            auto contactInfo = accInfo.contactModel->getContact(uri);
+            auto alias = contactInfo.profileInfo.alias;
+
+            if (!alias.empty()) {
+                bestName = alias;
+            } else if (!contactInfo.registeredName.empty()) {
+                bestName = contactInfo.registeredName;
+            }
+            bestName.erase(std::remove(bestName.begin(), bestName.end(), '\r'), bestName.end());
+            bestName.erase(std::remove(bestName.begin(), bestName.end(), '\n'), bestName.end());
+            label += bestName;
+            if (idx != static_cast<int>(uris.size()) - 1)
+                label += ", ";
+            idx ++;
+        } catch (const std::out_of_range&) {
+            // ContactModel::getContact() exception
+        }
+    }
+
+
+    gchar* text = g_markup_printf_escaped(
+        "<span font=\"10\">%s</span>",
+        label.c_str()
+    );
+
+    auto* box_item = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+    auto* avatar = gtk_image_new_from_pixbuf(photo.get());
+    auto* info = gtk_label_new(nullptr);
+    gtk_label_set_markup(GTK_LABEL(info), text);
+    gtk_container_add(GTK_CONTAINER(box_item), GTK_WIDGET(avatar));
+    gtk_container_add(GTK_CONTAINER(box_item), GTK_WIDGET(info));
+    g_object_set(G_OBJECT(info), "ellipsize", PANGO_ELLIPSIZE_END, NULL);
+
+    g_object_set_data(G_OBJECT(info), "custom_type", GINT_TO_POINTER(RowType::CONFERENCE));
+    g_object_set_data(G_OBJECT(info), "custom_data", (void*)g_strdup(custom_data.c_str()));
+
+    gtk_list_box_insert(GTK_LIST_BOX(widgets->list_conversations_invite), GTK_WIDGET(box_item), -1);
+}
+
 void
 CppImpl::add_transfer_contact(const std::string& uri)
 {
@@ -770,9 +1193,17 @@ CppImpl::setCallInfo()
         const lrc::api::video::Renderer* previewRenderer =
              &avModel_->getRenderer(
              lrc::api::video::PREVIEW_RENDERER_ID);
-        if (previewRenderer->isRendering())
-             video_widget_add_new_renderer(VIDEO_WIDGET(widgets->video_widget),
-                    avModel_, previewRenderer, VIDEO_RENDERER_LOCAL);
+        if (previewRenderer->isRendering()) {
+            video_widget_add_new_renderer(VIDEO_WIDGET(widgets->video_widget),
+                avModel_, previewRenderer, VIDEO_RENDERER_LOCAL);
+        }
+        try {
+            auto call = (*accountInfo)->callModel->getCall(callToRender);
+            video_widget_set_preview_visible(VIDEO_WIDGET(widgets->video_widget),
+                (call.status != lrc::api::call::Status::PAUSED));
+        } catch (...) {
+            g_warning("Can't change preview visibility for non existant call");
+        }
 
         const lrc::api::video::Renderer* vRenderer =
             &avModel_->getRenderer(
@@ -780,7 +1211,6 @@ CppImpl::setCallInfo()
 
         video_widget_add_new_renderer(VIDEO_WIDGET(widgets->video_widget),
             avModel_, vRenderer, VIDEO_RENDERER_REMOTE);
-
     } catch (...) {
         // The renderer doesn't exist for now. Ignore
     }
@@ -800,11 +1230,14 @@ CppImpl::setCallInfo()
                     if (previewRenderer->isRendering()) {
                         video_widget_add_new_renderer(VIDEO_WIDGET(widgets->video_widget),
                             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));
                     }
                 } catch (...) {
                     g_warning("Preview renderer is not accessible! This should not happen");
                 }
-            } else if (id == conversation->callId) {
+            } else if (id == callToRender) {
                 try {
                     const lrc::api::video::Renderer* vRenderer =
                         &avModel_->getRenderer(
@@ -828,6 +1261,13 @@ CppImpl::setCallInfo()
         &lrc::api::NewCallModel::callStatusChanged,
         [this] (const std::string& callId) {
             if (callId == conversation->callId) {
+                try {
+                    auto call = (*accountInfo)->callModel->getCall(callId);
+                    video_widget_set_preview_visible(VIDEO_WIDGET(widgets->video_widget),
+                        (call.status != lrc::api::call::Status::PAUSED));
+                } catch (...) {
+                    g_warning("Can't set preview visible for inexistant call");
+                }
                 updateNameAndPhoto();
                 updateState();
             }
@@ -926,10 +1366,12 @@ CppImpl::insertControls()
 
     /* connect the controllers (new model) */
     g_signal_connect_swapped(widgets->button_hangup, "clicked", G_CALLBACK(on_button_hangup_clicked), self);
+    g_signal_connect_swapped(widgets->togglebutton_add_participant, "clicked", G_CALLBACK(on_button_add_participant_clicked), self);
     g_signal_connect_swapped(widgets->togglebutton_transfer, "clicked", G_CALLBACK(on_button_transfer_clicked), self);
     g_signal_connect_swapped(widgets->siptransfer_filter_entry, "activate", G_CALLBACK(on_siptransfer_filter_activated), self);
     g_signal_connect(widgets->siptransfer_filter_entry, "search-changed", G_CALLBACK(on_siptransfer_text_changed), self);
     g_signal_connect(widgets->list_conversations, "row-activated", G_CALLBACK(transfer_to_conversation), self);
+    g_signal_connect(widgets->list_conversations_invite, "row-activated", G_CALLBACK(invite_to_conversation), self);
     g_signal_connect_swapped(widgets->togglebutton_hold, "clicked", G_CALLBACK(on_togglebutton_hold_clicked), self);
     g_signal_connect_swapped(widgets->togglebutton_muteaudio, "clicked", G_CALLBACK(on_togglebutton_muteaudio_clicked), self);
     g_signal_connect_swapped(widgets->togglebutton_record, "clicked", G_CALLBACK(on_togglebutton_record_clicked), self);
@@ -1222,12 +1664,7 @@ current_call_view_show_chat(CurrentCallView* view)
 static void
 current_call_view_init(CurrentCallView *view)
 {
-    auto* priv = CURRENT_CALL_VIEW_GET_PRIVATE(view);
     gtk_widget_init_template(GTK_WIDGET(view));
-
-    // CppImpl ctor
-    priv->cpp = new details::CppImpl {*view};
-    priv->cpp->init();
 }
 
 static void
@@ -1272,6 +1709,7 @@ 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_add_participant);
     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, togglebutton_transfer);
     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, togglebutton_hold);
     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, togglebutton_muteaudio);
@@ -1282,6 +1720,9 @@ current_call_view_class_init(CurrentCallViewClass *klass)
     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, siptransfer_popover);
     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, siptransfer_filter_entry);
     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, list_conversations);
+    gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, add_participant_popover);
+    gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, conversation_filter_entry);
+    gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, list_conversations_invite);
 
     details::current_call_view_signals[VIDEO_DOUBLE_CLICKED] = g_signal_new (
         "video-double-clicked",
@@ -1298,11 +1739,16 @@ GtkWidget *
 current_call_view_new(WebKitChatContainer* chat_widget,
                       AccountInfoPointer const & accountInfo,
                       lrc::api::conversation::Info* conversation,
-                      lrc::api::AVModel& avModel)
+                      lrc::api::AVModel& avModel,
+                      const lrc::api::Lrc& lrc)
 {
     auto* self = g_object_new(CURRENT_CALL_VIEW_TYPE, NULL);
     auto* priv = CURRENT_CALL_VIEW_GET_PRIVATE(self);
 
+    // CppImpl ctor
+    CurrentCallView* view = CURRENT_CALL_VIEW(self);
+    priv->cpp = new details::CppImpl(*view, lrc);
+    priv->cpp->init();
     priv->cpp->setup(chat_widget, accountInfo, conversation, avModel);
     return GTK_WIDGET(self);
 }
@@ -1311,6 +1757,7 @@ void
 current_call_view_handup_focus(GtkWidget *current_call_view)
 {
     auto* priv = CURRENT_CALL_VIEW_GET_PRIVATE(current_call_view);
+    g_return_if_fail(priv);
     gtk_widget_set_can_focus (priv->button_hangup, true);
     gtk_widget_grab_focus(priv->button_hangup);
 }
diff --git a/src/currentcallview.h b/src/currentcallview.h
index 58568412..2bd17d91 100644
--- a/src/currentcallview.h
+++ b/src/currentcallview.h
@@ -23,6 +23,7 @@
 
 #include <gtk/gtk.h>
 #include "api/account.h"
+#include "api/lrc.h"
 #include "webkitchatcontainer.h"
 #include "accountinfopointer.h"
 
@@ -54,7 +55,8 @@ GType      current_call_view_get_type      (void) G_GNUC_CONST;
 GtkWidget *current_call_view_new           (WebKitChatContainer* view,
                                            AccountInfoPointer const & accountInfo,
                                            lrc::api::conversation::Info* conversation,
-                                           lrc::api::AVModel& avModel);
+                                           lrc::api::AVModel& avModel,
+                                           const lrc::api::Lrc& lrc);
 lrc::api::conversation::Info current_call_view_get_conversation(CurrentCallView*);
 GtkWidget *current_call_view_get_chat_view(CurrentCallView*);
 void current_call_view_show_chat(CurrentCallView*);
diff --git a/src/ringmainwindow.cpp b/src/ringmainwindow.cpp
index 651659ba..73696136 100644
--- a/src/ringmainwindow.cpp
+++ b/src/ringmainwindow.cpp
@@ -373,6 +373,7 @@ public:
     std::string eventBody_;
 
     bool isCreatingAccount {false};
+    QHash<QString, QMetaObject::Connection> pendingConferences_;
 private:
     CppImpl() = delete;
     CppImpl(const CppImpl&) = delete;
@@ -1468,7 +1469,7 @@ CppImpl::displayCurrentCallView(lrc::api::conversation::Info conversation, bool
     auto* new_view = current_call_view_new(webkitChatContainer(redraw_webview),
                                            accountInfo_,
                                            chatViewConversation_.get(),
-                                           lrc_->getAVModel());
+                                           lrc_->getAVModel(), *lrc_.get()); // TODO improve. Only LRC is needed
 
     try {
         auto contactUri = chatViewConversation_->participants.front();
@@ -2378,8 +2379,7 @@ CppImpl::slotShowCallView(const std::string& id, lrc::api::conversation::Info or
     if (IS_CURRENT_CALL_VIEW(old_view))
         current_item = current_call_view_get_conversation(CURRENT_CALL_VIEW(old_view));
 
-    if (current_item.uid != origin.uid)
-        changeView(CURRENT_CALL_VIEW_TYPE, origin);
+    changeView(CURRENT_CALL_VIEW_TYPE, origin);
 }
 
 void
diff --git a/src/video/video_widget.cpp b/src/video/video_widget.cpp
index 0fc85f71..e66425cc 100644
--- a/src/video/video_widget.cpp
+++ b/src/video/video_widget.cpp
@@ -74,6 +74,7 @@ struct _VideoWidgetPrivate {
 
     /* local peer data */
     VideoWidgetRenderer     *local;
+    bool show_preview {true};
 
     guint                    frame_timeout_source;
 
@@ -106,7 +107,6 @@ struct _VideoWidgetRenderer {
      * this will be set back to false once the black frame is rendered
      */
     std::atomic_bool         show_black_frame;
-    std::atomic_bool         pause_rendering;
     QMetaObject::Connection  render_stop;
     QMetaObject::Connection  render_start;
 };
@@ -595,9 +595,6 @@ clutter_render_image(VideoWidgetRenderer* wg_renderer)
     auto actor = wg_renderer->actor;
     g_return_if_fail(CLUTTER_IS_ACTOR(actor));
 
-    if (wg_renderer->pause_rendering)
-        return;
-
     if (wg_renderer->show_black_frame) {
         /* render a black frame set the bool back to false, this is likely done
          * when the renderer is stopped so we ignore whether or not it is running
@@ -729,7 +726,8 @@ check_frame_queue(VideoWidget *self)
     VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self);
 
     /* display renderer's frames */
-    clutter_render_image(priv->local);
+    if (priv->show_preview)
+        clutter_render_image(priv->local);
     clutter_render_image(priv->remote);
     if (priv->remote->snapshot_status == HAS_A_NEW_ONE) {
         priv->remote->snapshot_status = NOTHING;
@@ -858,33 +856,26 @@ video_widget_add_new_renderer(VideoWidget* self, lrc::api::AVModel* avModel,
     new_video_renderer->render_stop = QObject::connect(
         &*avModel,
         &lrc::api::AVModel::rendererStopped,
-        [=](const std::string&) {
-            renderer_stop(new_video_renderer);
+        [=](const std::string& id) {
+            if (renderer->getId() == id)
+                renderer_stop(new_video_renderer);
         });
 
     new_video_renderer->render_start = QObject::connect(
         &*avModel,
         &lrc::api::AVModel::rendererStarted,
-        [=](const std::string&) {
-            renderer_start(new_video_renderer);
+        [=](const std::string& id) {
+            if (renderer->getId() == id)
+                renderer_start(new_video_renderer);
         });
 
     g_async_queue_push(priv->new_renderer_queue, new_video_renderer);
 }
 
-void
-video_widget_pause_rendering(VideoWidget *self, gboolean pause)
-{
-    g_return_if_fail(IS_VIDEO_WIDGET(self));
-    VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self);
-
-    priv->local->pause_rendering = pause;
-    priv->remote->pause_rendering = pause;
-}
-
 void
 video_widget_take_snapshot(VideoWidget *self)
 {
+    g_return_if_fail(IS_VIDEO_WIDGET(self));
     VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self);
 
     priv->remote->snapshot_status = HAS_TO_TAKE_ONE;
@@ -893,7 +884,18 @@ video_widget_take_snapshot(VideoWidget *self)
 GdkPixbuf*
 video_widget_get_snapshot(VideoWidget *self)
 {
+    g_return_val_if_fail(IS_VIDEO_WIDGET(self), nullptr);
     VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self);
 
     return priv->remote->snapshot;
 }
+
+void
+video_widget_set_preview_visible(VideoWidget *self, bool show)
+{
+    g_return_if_fail(IS_VIDEO_WIDGET(self));
+    VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self);
+    if (priv) {
+        priv->show_preview = show;
+    }
+}
\ No newline at end of file
diff --git a/src/video/video_widget.h b/src/video/video_widget.h
index 89fce6bc..f957b2cf 100644
--- a/src/video/video_widget.h
+++ b/src/video/video_widget.h
@@ -53,7 +53,6 @@ typedef enum {
 GType           video_widget_get_type          (void) G_GNUC_CONST;
 GtkWidget*      video_widget_new               (void);
 void            video_widget_add_new_renderer (VideoWidget*, lrc::api::AVModel* avModel, const lrc::api::video::Renderer*, VideoRendererType);
-void            video_widget_pause_rendering   (VideoWidget *self, gboolean pause);
 void            video_widget_on_drag_data_received (GtkWidget *self,
                                                     GdkDragContext *context,
                                                     gint x,
@@ -67,6 +66,7 @@ gboolean        video_widget_on_button_press_in_screen_event (VideoWidget *self,
                                                               G_GNUC_UNUSED gpointer);
 void            video_widget_take_snapshot (VideoWidget *self);
 GdkPixbuf*      video_widget_get_snapshot  (VideoWidget *self);
+void            video_widget_set_preview_visible (VideoWidget *self, bool show);
 
 G_END_DECLS
 
diff --git a/ui/currentcallview.ui b/ui/currentcallview.ui
index ec5d8ba6..359937df 100644
--- a/ui/currentcallview.ui
+++ b/ui/currentcallview.ui
@@ -261,6 +261,25 @@
         <property name="fill">True</property>
       </packing>
     </child>
+    <child>
+      <object class="GtkToggleButton" id="togglebutton_add_participant">
+        <style>
+          <class name="call-button"/>
+        </style>
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="receives_default">True</property>
+        <property name="width-request">48</property>
+        <property name="height-request">48</property>
+        <property name="has_tooltip">True</property>
+        <property name="tooltip-text" translatable="yes">Add participant</property>
+        <property name="image">image_add_participant</property>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+      </packing>
+    </child>
     <child>
       <object class="GtkToggleButton" id="togglebutton_transfer">
         <style>
@@ -466,6 +485,15 @@
       </object>
     </child>
   </object>
+  <object class="GtkImage" id="image_add_participant">
+    <property name="visible">True</property>
+    <property name="resource">/net/jami/JamiGnome/invite_white</property>
+    <child internal-child="accessible">
+      <object class="AtkObject" id="image_add_participant-atkobject">
+        <property name="AtkObject::accessible-description" translatable="yes">Add participant</property>
+      </object>
+    </child>
+  </object>
   <object class="GtkImage" id="image_transfer">
     <property name="visible">True</property>
     <property name="resource">/net/jami/JamiGnome/transfer</property>
@@ -572,4 +600,68 @@
       </object>
     </child>
   </object>
+  <object class="GtkPopover" id="add_participant_popover">
+    <property name="can_focus">False</property>
+    <property name="height_request">400</property>
+    <property name="width_request">300</property>
+    <child>
+      <object class="GtkBox">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="orientation">vertical</property>
+        <child>
+          <object class="GtkLabel">
+            <property name="visible">False</property>
+            <property name="halign">center</property>
+            <property name="label" translatable="yes">Add</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkSearchEntry" id="conversation_filter_entry">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="primary_icon_name">edit-find-symbolic</property>
+            <property name="primary_icon_activatable">False</property>
+            <property name="primary_icon_sensitive">False</property>
+            <style>
+              <class name="search-entry-style"/>
+            </style>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkScrolledWindow">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="shadow_type">in</property>
+            <child>
+              <object class="GtkListBox" id="list_conversations_invite">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+              </object>
+              <packing>
+                <property name="expand">True</property>
+                <property name="fill">True</property>
+                <property name="position">0</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">True</property>
+            <property name="fill">True</property>
+            <property name="position">2</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </object>
 </interface>
-- 
GitLab