| /* |
| * Copyright (C) 2015-2016 Savoir-faire Linux Inc. |
| * Author: Stepan Salenikovich <stepan.salenikovich@savoirfairelinux.com> |
| * |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License as published by |
| * the Free Software Foundation; either version 3 of the License, or |
| * (at your option) any later version. |
| * |
| * This program is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| * GNU General Public License for more details. |
| * |
| * You should have received a copy of the GNU General Public License |
| * along with this program; if not, write to the Free Software |
| * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
| */ |
| |
| #include "ringnotify.h" |
| #include "config.h" |
| #include "ring_client.h" |
| |
| #if USE_LIBNOTIFY |
| #include <glib/gi18n.h> |
| #include <libnotify/notify.h> |
| #include <memory> |
| #include <globalinstances.h> |
| #include "native/pixbufmanipulator.h" |
| #include <call.h> |
| #include <QtCore/QSize> |
| #include <media/text.h> |
| #include <callmodel.h> |
| #include <media/textrecording.h> |
| #include <media/recordingmodel.h> |
| #include <recentmodel.h> |
| #endif |
| |
| #if USE_LIBNOTIFY |
| |
| static constexpr int MAX_NOTIFICATIONS = 10; // max unread chat msgs to display from the same contact |
| static constexpr const char* SERVER_NOTIFY_OSD = "notify-osd"; |
| |
| /* struct to store the parsed list of the notify server capabilities */ |
| struct RingNotifyServerInfo |
| { |
| /* info */ |
| char *name; |
| char *vendor; |
| char *version; |
| char *spec; |
| |
| /* capabilities */ |
| gboolean append; |
| gboolean actions; |
| |
| /* the info strings must be freed */ |
| ~RingNotifyServerInfo() { |
| g_free(name); |
| g_free(vendor); |
| g_free(version); |
| g_free(spec); |
| } |
| }; |
| |
| static struct RingNotifyServerInfo server_info; |
| #endif |
| |
| void |
| ring_notify_init() |
| { |
| #if USE_LIBNOTIFY |
| notify_init("Ring"); |
| |
| /* get notify server info */ |
| if (notify_get_server_info(&server_info.name, |
| &server_info.vendor, |
| &server_info.version, |
| &server_info.spec)) { |
| g_debug("notify server name: %s, vendor: %s, version: %s, spec: %s", |
| server_info.name, server_info.vendor, server_info.version, server_info.spec); |
| } |
| |
| /* check notify server capabilities */ |
| auto list = notify_get_server_caps(); |
| while (list) { |
| if (g_strcmp0((const char *)list->data, "append") == 0 || |
| g_strcmp0((const char *)list->data, "x-canonical-append") == 0) { |
| server_info.append = TRUE; |
| } |
| if (g_strcmp0((const char *)list->data, "actions") == 0) { |
| server_info.actions = TRUE; |
| } |
| |
| list = g_list_next(list); |
| } |
| |
| g_list_free_full(list, g_free); |
| #endif |
| } |
| |
| void |
| ring_notify_uninit() |
| { |
| #if USE_LIBNOTIFY |
| if (notify_is_initted()) |
| notify_uninit(); |
| #endif |
| } |
| |
| gboolean |
| ring_notify_is_initted() |
| { |
| #if USE_LIBNOTIFY |
| return notify_is_initted(); |
| #else |
| return FALSE; |
| #endif |
| } |
| |
| static void |
| ring_notify_show_cm(NotifyNotification*, char *, ContactMethod *cm) |
| { |
| /* show the main window in case its hidden */ |
| if (auto action = g_action_map_lookup_action(G_ACTION_MAP(g_application_get_default()), "show-main-window")) { |
| g_action_change_state(action, g_variant_new_boolean(TRUE)); |
| } |
| /* select the relevant cm */ |
| auto idx = RecentModel::instance().getIndex(cm); |
| if (idx.isValid()) { |
| RecentModel::instance().selectionModel()->setCurrentIndex(idx, QItemSelectionModel::ClearAndSelect); |
| } |
| } |
| |
| gboolean |
| ring_notify_incoming_call( |
| #if !USE_LIBNOTIFY |
| G_GNUC_UNUSED |
| #endif |
| Call* call) |
| { |
| gboolean success = FALSE; |
| #if USE_LIBNOTIFY |
| g_return_val_if_fail(call, FALSE); |
| |
| gchar *body = g_markup_escape_text(call->formattedName().toUtf8().constData(), -1); |
| std::shared_ptr<NotifyNotification> notification( |
| notify_notification_new(_("Incoming call"), body, NULL), g_object_unref); |
| g_free(body); |
| |
| /* get photo */ |
| QVariant var_p = GlobalInstances::pixmapManipulator().callPhoto( |
| call->peerContactMethod(), QSize(50, 50), false); |
| std::shared_ptr<GdkPixbuf> photo = var_p.value<std::shared_ptr<GdkPixbuf>>(); |
| notify_notification_set_image_from_pixbuf(notification.get(), photo.get()); |
| |
| /* calls have highest urgency */ |
| notify_notification_set_urgency(notification.get(), NOTIFY_URGENCY_CRITICAL); |
| notify_notification_set_timeout(notification.get(), NOTIFY_EXPIRES_DEFAULT); |
| |
| /* if the notification server supports actions, make the default action to show the call */ |
| if (server_info.actions) { |
| notify_notification_add_action(notification.get(), |
| "default", |
| C_("notification action name", "Show"), |
| (NotifyActionCallback)ring_notify_show_cm, |
| call->peerContactMethod(), |
| nullptr); |
| } |
| |
| GError *error = NULL; |
| success = notify_notification_show(notification.get(), &error); |
| |
| if (success) { |
| /* monitor the life cycle of the call and try to close the notification |
| * once the call has been aswered */ |
| auto state_changed_conn = std::make_shared<QMetaObject::Connection>(); |
| *state_changed_conn = QObject::connect( |
| call, |
| &Call::lifeCycleStateChanged, |
| [notification, state_changed_conn] (Call::LifeCycleState newState, G_GNUC_UNUSED Call::LifeCycleState previousState) |
| { |
| g_return_if_fail(NOTIFY_IS_NOTIFICATION(notification.get())); |
| if (newState > Call::LifeCycleState::INITIALIZATION) { |
| /* note: not all systems will actually close the notification |
| * even if the above function returns as true */ |
| if (!notify_notification_close(notification.get(), NULL)) |
| g_warning("could not close notification"); |
| |
| /* once we (try to) close the notification, we can |
| * disconnect from this signal; this should also destroy |
| * the notification shared_ptr as its ref count will |
| * drop to 0 */ |
| QObject::disconnect(*state_changed_conn); |
| } |
| } |
| ); |
| } else { |
| g_warning("failed to show notification: %s", error->message); |
| g_clear_error(&error); |
| } |
| #endif |
| return success; |
| } |
| |
| #if USE_LIBNOTIFY |
| |
| static void |
| ring_notify_free_list(gpointer, GList *value, gpointer) |
| { |
| if (value) { |
| g_object_unref(G_OBJECT(value->data)); |
| g_list_free(value); |
| } |
| } |
| |
| static void |
| ring_notify_free_chat_table(GHashTable *table) { |
| if (table) { |
| g_hash_table_foreach(table, (GHFunc)ring_notify_free_list, nullptr); |
| g_hash_table_destroy(table); |
| } |
| } |
| |
| /** |
| * Returns a pointer to a GHashTable which contains key,value pairs where a ContactMethod pointer |
| * is the key and a GList of notifications for that CM is the vlue. |
| */ |
| GHashTable * |
| ring_notify_get_chat_table() |
| { |
| static std::unique_ptr<GHashTable, decltype(ring_notify_free_chat_table)&> chat_table( |
| nullptr, ring_notify_free_chat_table); |
| |
| if (chat_table.get() == nullptr) |
| chat_table.reset(g_hash_table_new(NULL, NULL)); |
| |
| return chat_table.get(); |
| } |
| |
| static void |
| notification_closed(NotifyNotification *notification, ContactMethod *cm) |
| { |
| g_return_if_fail(cm); |
| |
| /* remove from the list */ |
| auto chat_table = ring_notify_get_chat_table(); |
| if (auto list = (GList *)g_hash_table_lookup(chat_table, cm)) { |
| list = g_list_remove(list, notification); |
| if (list) { |
| // the head of the list may have changed |
| g_hash_table_replace(chat_table, cm, list); |
| } else { |
| g_hash_table_remove(chat_table, cm); |
| } |
| } |
| |
| g_object_unref(notification); |
| } |
| |
| static gboolean |
| ring_notify_show_text_message(ContactMethod *cm, const QModelIndex& idx) |
| { |
| g_return_val_if_fail(idx.isValid() && cm, FALSE); |
| gboolean success = FALSE; |
| |
| auto title = g_markup_printf_escaped(C_("Text message notification", "%s says:"), idx.data(static_cast<int>(Ring::Role::Name)).toString().toUtf8().constData()); |
| auto body = g_markup_escape_text(idx.data(Qt::DisplayRole).toString().toUtf8().constData(), -1); |
| |
| NotifyNotification *notification_new = nullptr; |
| NotifyNotification *notification_old = nullptr; |
| |
| /* try to get the previous notification */ |
| auto chat_table = ring_notify_get_chat_table(); |
| auto list = (GList *)g_hash_table_lookup(chat_table, cm); |
| if (list) |
| notification_old = (NotifyNotification *)list->data; |
| |
| /* we display chat notifications in different ways to suit different notification servers and |
| * their capabilities: |
| * 1. if the server doesn't support appending (eg: Notification Daemon) then we update the |
| * previous notification (if exists) with new text; otherwise it takes we have many |
| * notifications from the same person... we don't concatinate the old messages because |
| * servers which don't support append usually don't support multi line bodies |
| * 2. the notify-osd server supports appending; however it doesn't clear the old notifications |
| * on demand, which means in our case that chat messages which have already been read could |
| * still be displayed when a new notification is appended, thus in this case, we update |
| * the old notification body manually to only contain the unread messages |
| * 3. the 3rd case is that the server supports append but is not notify-osd, then we simply use |
| * the append feature |
| */ |
| |
| if (notification_old && !server_info.append) { |
| /* case 1 */ |
| notify_notification_update(notification_old, title, body, nullptr); |
| notification_new = notification_old; |
| } else if (notification_old && g_strcmp0(server_info.name, SERVER_NOTIFY_OSD) == 0) { |
| /* case 2 */ |
| /* print up to MAX_NOTIFICATIONS unread messages */ |
| int msg_count = 0; |
| auto idx_next = idx.sibling(idx.row() - 1, idx.column()); |
| auto read = idx_next.data(static_cast<int>(Media::TextRecording::Role::IsRead)).toBool(); |
| while (idx_next.isValid() && !read && msg_count < MAX_NOTIFICATIONS) { |
| |
| auto body_prev = body; |
| body = g_markup_printf_escaped("%s\n%s", body_prev, idx_next.data(Qt::DisplayRole).toString().toUtf8().constData()); |
| g_free(body_prev); |
| |
| idx_next = idx_next.sibling(idx_next.row() - 1, idx_next.column()); |
| read = idx_next.data(static_cast<int>(Media::TextRecording::Role::IsRead)).toBool(); |
| ++msg_count; |
| } |
| |
| notify_notification_update(notification_old, title, body, nullptr); |
| |
| notification_new = notification_old; |
| } else { |
| /* need new notification for case 1, 2, or 3 */ |
| notification_new = notify_notification_new(title, body, nullptr); |
| |
| /* track in hash table */ |
| auto list = (GList *)g_hash_table_lookup(chat_table, cm); |
| list = g_list_append(list, notification_new); |
| g_hash_table_replace(chat_table, cm, list); |
| |
| /* get photo */ |
| QVariant var_p = GlobalInstances::pixmapManipulator().callPhoto( |
| cm, QSize(50, 50), false); |
| std::shared_ptr<GdkPixbuf> photo = var_p.value<std::shared_ptr<GdkPixbuf>>(); |
| notify_notification_set_image_from_pixbuf(notification_new, photo.get()); |
| |
| /* normal priority for messages */ |
| notify_notification_set_urgency(notification_new, NOTIFY_URGENCY_NORMAL); |
| |
| /* remove the key and value from the hash table once the notification is |
| * closed; note that this will also unref the notification */ |
| g_signal_connect(notification_new, "closed", G_CALLBACK(notification_closed), cm); |
| |
| /* if the notification server supports actions, make the default action to show the chat view */ |
| if (server_info.actions) { |
| notify_notification_add_action(notification_new, |
| "default", |
| C_("notification action name", "Show"), |
| (NotifyActionCallback)ring_notify_show_cm, |
| cm, |
| nullptr); |
| } |
| } |
| |
| GError *error = nullptr; |
| success = notify_notification_show(notification_new, &error); |
| if (!success) { |
| g_warning("failed to show notification: %s", error->message); |
| g_clear_error(&error); |
| } |
| |
| g_free(title); |
| g_free(body); |
| |
| return success; |
| } |
| |
| static gboolean |
| show_message_if_unread(const QModelIndex *idx) |
| { |
| g_return_val_if_fail(idx && idx->isValid(), G_SOURCE_REMOVE); |
| |
| if (!idx->data(static_cast<int>(Media::TextRecording::Role::IsRead)).toBool()) { |
| auto cm = idx->data(static_cast<int>(Media::TextRecording::Role::ContactMethod)).value<ContactMethod *>(); |
| ring_notify_show_text_message(cm, *idx); |
| } |
| |
| return G_SOURCE_REMOVE; |
| } |
| |
| static void |
| delete_idx(QModelIndex *idx) |
| { |
| delete idx; |
| } |
| |
| #endif |
| |
| void |
| ring_notify_message( |
| #if !USE_LIBNOTIFY |
| ContactMethod*, Media::TextRecording*, RingClient*) |
| #else |
| ContactMethod *cm, Media::TextRecording *t, RingClient *client) |
| #endif |
| { |
| |
| #if USE_LIBNOTIFY |
| g_return_if_fail(cm && t && client); |
| |
| // get the message |
| auto model = t->instantMessagingModel(); |
| auto msg_idx = model->index(model->rowCount()-1, 0); |
| |
| // make sure its a text message, or else there is nothing to do |
| if (msg_idx.data(static_cast<int>(Media::TextRecording::Role::HasText)).toBool()) { |
| auto main_window = ring_client_get_main_window(client); |
| if ( main_window && gtk_window_is_active(main_window)) { |
| /* in this case we only want to show the notification if the message is not marked as |
| * read; this will only possibly be done after the the chatview has displayed it in |
| * response to this or another signal; so we must check for the read status after the |
| * chat view handler has completed, we do so via a g_idle function. |
| */ |
| auto new_idx = new QModelIndex(msg_idx); |
| g_idle_add_full(G_PRIORITY_DEFAULT_IDLE, (GSourceFunc)show_message_if_unread, new_idx, (GDestroyNotify)delete_idx); |
| } else { |
| /* always show a notification if the window is not active/visible */ |
| ring_notify_show_text_message(cm, msg_idx); |
| } |
| |
| } |
| #endif |
| } |
| |
| gboolean |
| ring_notify_close_chat_notification( |
| #if !USE_LIBNOTIFY |
| G_GNUC_UNUSED |
| #endif |
| ContactMethod *cm) |
| { |
| gboolean notification_existed = FALSE; |
| |
| #if USE_LIBNOTIFY |
| /* checks if there exists a chat notification associated with the given ContactMethod |
| * and tries to close it; if it did exist, then the function returns TRUE */ |
| g_return_val_if_fail(cm, FALSE); |
| |
| |
| auto chat_table = ring_notify_get_chat_table(); |
| |
| if (auto list = (GList *)g_hash_table_lookup(chat_table, cm)) { |
| while (list) { |
| notification_existed = TRUE; |
| auto notification = (NotifyNotification *)list->data; |
| |
| GError *error = NULL; |
| if (!notify_notification_close(notification, &error)) { |
| g_warning("could not close notification: %s", error->message); |
| g_clear_error(&error); |
| } |
| |
| list = g_list_next(list); |
| } |
| } |
| |
| #endif |
| |
| return notification_existed; |
| } |