diff --git a/pixmaps/pixmaps.gresource.xml b/pixmaps/pixmaps.gresource.xml index 6f66dc57e67167bb8e26764e32f11ae9236637ef..83a7999c5f26c843b26eff4fcfcda283a77106b1 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 0000000000000000000000000000000000000000..3251e307cb4bda77b49a905cfb4d2bc6182e6f87 --- /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 06cb28911192b51d936463c674ae5d9501386bea..3a9ffbd764284af1a7d35e569bd2ecddf540a50b 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 @@ struct CurrentCallViewPrivate 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 @@ public: 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 @@ on_toggle_smartinfo(GSimpleAction* action, G_GNUC_UNUSED GVariant* state, GtkWid } } +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 @@ CppImpl::init() // 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 @@ CppImpl::setup(WebKitChatContainer* chat_widget, 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 @@ 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_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 @@ 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_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 @@ current_call_view_new(WebKitChatContainer* chat_widget, 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 414504341590dfd87d35e90829dc63b3475d7d2e..74eb7949392068bf62c3c2be5b82850249ba6e0c 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_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> @@ -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>