sip: fix the transfer feature

Readd the ability to transfer a SIP call (blind or attended) from
the current call view.

Change-Id: I46f5ade5ef71e5479bc3410f9b1f4c200825132c
Gitlab: #803
Reviewed-by: Philippe Gorley <philippe.gorley@savoirfairelinux.com>
diff --git a/pixmaps/pixmaps.gresource.xml b/pixmaps/pixmaps.gresource.xml
index 6f66dc5..83a7999 100644
--- a/pixmaps/pixmaps.gresource.xml
+++ b/pixmaps/pixmaps.gresource.xml
@@ -33,5 +33,6 @@
     <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>
+    <file alias="transfer">transfer.svg</file>
   </gresource>
 </gresources>
diff --git a/pixmaps/transfer.svg b/pixmaps/transfer.svg
new file mode 100644
index 0000000..3251e30
--- /dev/null
+++ b/pixmaps/transfer.svg
@@ -0,0 +1,4 @@
+<svg fill="#ffffff" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
+    <path d="M0 0h24v24H0z" fill="none"/>
+    <path d="M18 11l5-5-5-5v3h-4v4h4v3zm2 4.5c-1.25 0-2.45-.2-3.57-.57-.35-.11-.74-.03-1.02.24l-2.2 2.2c-2.83-1.44-5.15-3.75-6.59-6.59l2.2-2.21c.28-.26.36-.65.25-1C8.7 6.45 8.5 5.25 8.5 4c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1 0 9.39 7.61 17 17 17 .55 0 1-.45 1-1v-3.5c0-.55-.45-1-1-1z"/>
+</svg>
diff --git a/src/currentcallview.cpp b/src/currentcallview.cpp
index 06cb289..3a9ffbd 100644
--- a/src/currentcallview.cpp
+++ b/src/currentcallview.cpp
@@ -44,6 +44,8 @@
 #include "utils/files.h"
 #include "video/video_widget.h"
 
+#include <iostream>
+
 namespace { namespace details
 {
 class CppImpl;
@@ -79,6 +81,10 @@
     GtkWidget *togglebutton_chat;
     GtkWidget *togglebutton_muteaudio;
     GtkWidget *togglebutton_mutevideo;
+    GtkWidget *togglebutton_transfer;
+    GtkWidget* siptransfer_popover;
+    GtkWidget* siptransfer_filter_entry;
+    GtkWidget* list_conversations;
     GtkWidget *togglebutton_hold;
     GtkWidget *togglebutton_record;
     GtkWidget *button_hangup;
@@ -234,6 +240,7 @@
     void setup(WebKitChatContainer* chat_widget,
                AccountInfoPointer const & account_info,
                lrc::api::conversation::Info* conversation);
+    void add_transfer_contact(const std::string& uri);
 
     void insertControls();
     void checkControlsFading();
@@ -542,6 +549,127 @@
     }
 }
 
+static void
+transfer_to_peer(CurrentCallViewPrivate* priv, const std::string& peerUri)
+{
+    if (peerUri == priv->cpp->conversation->participants.front()) {
+        g_warning("avoid to transfer to the same call, abort.");
+#if GTK_CHECK_VERSION(3,22,0)
+        gtk_popover_popdown(GTK_POPOVER(priv->siptransfer_popover));
+#else
+        gtk_widget_hide(GTK_WIDGET(priv->siptransfer_popover));
+#endif
+        return;
+    }
+    try {
+        // If a call is already present with a peer, try an attended transfer.
+        auto callInfo = (*priv->cpp->accountInfo)->callModel->getCallFromURI(peerUri, true);
+        (*priv->cpp->accountInfo)->callModel->transferToCall(
+            priv->cpp->conversation->callId, callInfo.id);
+    } catch (std::out_of_range&) {
+        // No current call found with this URI, perform a blind transfer
+        (*priv->cpp->accountInfo)->callModel->transfer(
+            priv->cpp->conversation->callId, peerUri);
+    }
+#if GTK_CHECK_VERSION(3,22,0)
+    gtk_popover_popdown(GTK_POPOVER(priv->siptransfer_popover));
+#else
+    gtk_widget_hide(GTK_WIDGET(priv->siptransfer_popover));
+#endif
+}
+
+static void
+on_siptransfer_filter_activated(CurrentCallView* self)
+{
+    g_return_if_fail(IS_CURRENT_CALL_VIEW(self));
+    auto* priv = CURRENT_CALL_VIEW_GET_PRIVATE(self);
+
+    transfer_to_peer(priv, gtk_entry_get_text(GTK_ENTRY(priv->siptransfer_filter_entry)));
+}
+
+static GtkLabel*
+get_sip_address_label(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_LABEL(g_list_last(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);
+    transfer_to_peer(priv, gtk_label_get_text(GTK_LABEL(sip_address)));
+}
+
+static void
+filter_transfer_list(CurrentCallView *self)
+{
+    g_return_if_fail(IS_CURRENT_CALL_VIEW(self));
+    auto* priv = CURRENT_CALL_VIEW_GET_PRIVATE(self);
+
+    std::string currentFilter = gtk_entry_get_text(GTK_ENTRY(priv->siptransfer_filter_entry));
+
+    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));;
+        if (row == 0) {
+            // Update searching item
+            if (currentFilter.empty() || currentFilter == priv->cpp->conversation->participants.front()) {
+                // Hide temporary item if filter is empty or same number
+                gtk_widget_hide(children);
+            } else {
+                // Else, show the temporary item (and select it)
+                gtk_label_set_text(GTK_LABEL(sip_address), currentFilter.c_str());
+                gtk_widget_show_all(children);
+                gtk_list_box_select_row(GTK_LIST_BOX(priv->list_conversations), GTK_LIST_BOX_ROW(children));
+            }
+        } else {
+            // It's a contact
+            std::string item_address = gtk_label_get_text(GTK_LABEL(sip_address));
+
+            if (item_address == priv->cpp->conversation->participants.front())
+                // if item is the current conversation, hide it
+                gtk_widget_hide(children);
+            else if (currentFilter.empty())
+                // filter is empty, show all items
+                gtk_widget_show_all(children);
+            else if (item_address.find(currentFilter) == std::string::npos || item_address == currentFilter)
+                // avoid duplicates and unwanted numbers
+                gtk_widget_hide(children);
+            else
+                // Item is filtered
+                gtk_widget_show_all(children);
+        }
+        ++row;
+    }
+}
+
+static void
+on_button_transfer_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->siptransfer_popover), GTK_WIDGET(priv->togglebutton_transfer));
+#if GTK_CHECK_VERSION(3,22,0)
+    gtk_popover_popdown(GTK_POPOVER(priv->siptransfer_popover));
+#else
+    gtk_widget_show_all(GTK_WIDGET(priv->siptransfer_popover));
+#endif
+    gtk_widget_show_all(priv->siptransfer_popover);
+    filter_transfer_list(self);
+}
+
+static void
+on_siptransfer_text_changed(GtkSearchEntry*, CurrentCallView* self)
+{
+    filter_transfer_list(self);
+}
+
 } // namespace gtk_callbacks
 
 CppImpl::CppImpl(CurrentCallView& widget)
@@ -571,7 +699,8 @@
     // CSS styles
     auto provider = gtk_css_provider_new();
     gtk_css_provider_load_from_data(provider,
-        ".smartinfo-block-style { color: #8ae234; background-color: rgba(1, 1, 1, 0.33); } \
+        ".search-entry-style { border: 0; border-radius: 0; } \
+        .smartinfo-block-style { color: #8ae234; background-color: rgba(1, 1, 1, 0.33); } \
         @keyframes blink { 0% {opacity: 1;} 49% {opacity: 1;} 50% {opacity: 0;} 100% {opacity: 0;} } \
         .record-button { background: rgba(0, 0, 0, 1); border-radius: 50%; border: 0; transition: all 0.3s ease; } \
         .record-button:checked { animation: blink 1s; animation-iteration-count: infinite; } \
@@ -605,6 +734,34 @@
     conversation = conv_info;
     accountInfo = &account_info;
     setCallInfo();
+
+    if ((*accountInfo)->profileInfo.type == lrc::api::profile::Type::RING)
+        gtk_widget_hide(widgets->togglebutton_transfer);
+    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);
+        // Fill with SIP contacts
+        add_transfer_contact("");  // Temporary item
+        for (const auto& c : (*accountInfo)->conversationModel->getFilteredConversations(lrc::api::profile::Type::SIP))
+            add_transfer_contact(c.participants.front());
+        gtk_widget_show_all(widgets->list_conversations);
+        gtk_widget_show(widgets->togglebutton_transfer);
+    }
+}
+
+void
+CppImpl::add_transfer_contact(const std::string& uri)
+{
+    auto* box_item = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+    auto pixbufmanipulator = Interfaces::PixbufManipulator();
+    auto image_buf = pixbufmanipulator.generateAvatar("", uri.empty() ? uri : "sip" + uri);
+    auto scaled = pixbufmanipulator.scaleAndFrame(image_buf.get(), QSize(48, 48));
+    auto* avatar = gtk_image_new_from_pixbuf(scaled.get());
+    auto* address = gtk_label_new(uri.c_str());
+    gtk_container_add(GTK_CONTAINER(box_item), GTK_WIDGET(avatar));
+    gtk_container_add(GTK_CONTAINER(box_item), GTK_WIDGET(address));
+    gtk_list_box_insert(GTK_LIST_BOX(widgets->list_conversations), GTK_WIDGET(box_item), -1);
 }
 
 void
@@ -748,6 +905,10 @@
 
     /* 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_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_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);
@@ -1065,12 +1226,16 @@
     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_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);
     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, togglebutton_record);
     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, togglebutton_mutevideo);
     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, button_hangup);
     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, scalebutton_quality);
+    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);
 
     details::current_call_view_signals[VIDEO_DOUBLE_CLICKED] = g_signal_new (
         "video-double-clicked",
@@ -1092,6 +1257,5 @@
     auto* priv = CURRENT_CALL_VIEW_GET_PRIVATE(self);
 
     priv->cpp->setup(chat_widget, accountInfo, conversation);
-
     return GTK_WIDGET(self);
 }
diff --git a/ui/currentcallview.ui b/ui/currentcallview.ui
index 4145043..74eb794 100644
--- a/ui/currentcallview.ui
+++ b/ui/currentcallview.ui
@@ -262,6 +262,25 @@
       </packing>
     </child>
     <child>
+      <object class="GtkToggleButton" id="togglebutton_transfer">
+        <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">Toggle transfer</property>
+        <property name="image">image_transfer</property>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+      </packing>
+    </child>
+    <child>
       <object class="GtkToggleButton" id="togglebutton_hold">
         <style>
           <class name="call-button"/>
@@ -449,6 +468,15 @@
       </object>
     </child>
   </object>
+  <object class="GtkImage" id="image_transfer">
+    <property name="visible">True</property>
+    <property name="resource">/cx/ring/RingGnome/transfer</property>
+    <child internal-child="accessible">
+      <object class="AtkObject" id="image_transfer-atkobject">
+        <property name="AtkObject::accessible-description" translatable="yes">Transfer</property>
+      </object>
+    </child>
+  </object>
   <object class="GtkImage" id="image_end">
     <property name="visible">True</property>
     <property name="resource">/cx/ring/RingGnome/call_end</property>
@@ -483,4 +511,67 @@
     <property name="step_increment">1</property>
     <property name="page_increment">10</property>
   </object>
+  <object class="GtkPopover" id="siptransfer_popover">
+    <property name="can_focus">False</property>
+    <property name="height_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">Transfer to</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="siptransfer_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">
+                <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>