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