diff --git a/src/ringnotify.cpp b/src/ringnotify.cpp index 8499ac571a49f8b1ee6d68695df7170c94611e69..65aea799deba7f1688500c9f66708a1303951949 100644 --- a/src/ringnotify.cpp +++ b/src/ringnotify.cpp @@ -35,11 +35,66 @@ #include <media/recordingmodel.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 } @@ -125,14 +180,35 @@ ring_notify_incoming_call( #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(g_hash_table_destroy)&> chat_table( - nullptr, g_hash_table_destroy); + 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_full(NULL, NULL, NULL, g_object_unref)); + chat_table.reset(g_hash_table_new(NULL, NULL)); return chat_table.get(); } @@ -142,12 +218,19 @@ notification_closed(NotifyNotification *notification, ContactMethod *cm) { g_return_if_fail(cm); - if (!g_hash_table_remove(ring_notify_get_chat_table(), cm)) { - g_warning("could not find notification associated with the given ContactMethod"); - /* normally removing the notification from the hash table will unref it, - * but if it was not found we should do it here */ - g_object_unref(notification); + /* 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 @@ -156,58 +239,89 @@ ring_notify_show_text_message(ContactMethod *cm, const QModelIndex& idx) g_return_val_if_fail(idx.isValid() && cm, FALSE); gboolean success = FALSE; - GHashTable *chat_table = ring_notify_get_chat_table(); - - auto title = g_strdup_printf(C_("Text message notification", "%s says:"), idx.data(static_cast<int>(Ring::Role::Name)).toString().toUtf8().constData()); - auto body = g_strdup_printf("%s", idx.data(Qt::DisplayRole).toString().toUtf8().constData()); - - /* check if a notification already exists for this CM */ - NotifyNotification *notification = (NotifyNotification *)g_hash_table_lookup(chat_table, cm); - if (notification) { - /* update notification; append the new message to the old */ - GValue body_value = G_VALUE_INIT; - g_value_init(&body_value, G_TYPE_STRING); - g_object_get_property(G_OBJECT(notification), "body", &body_value); - const gchar* body_old = g_value_get_string(&body_value); - if (body_old && (strlen(body_old) > 0)) { - gchar *body_new = g_strconcat(body_old, "\n", body, NULL); - g_free(body); - body = body_new; + 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, title, body, NULL); + + notify_notification_update(notification_old, title, body, nullptr); + + notification_new = notification_old; } else { - /* create new notification object and associate it with the CM in the - * hash table; also store the pointer of the CM in the notification - * object so that it knows it's key in the hash table */ - notification = notify_notification_new(title, body, NULL); - g_hash_table_insert(chat_table, cm, notification); - g_object_set_data(G_OBJECT(notification), "ContactMethod", cm); + /* 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, photo.get()); + notify_notification_set_image_from_pixbuf(notification_new, photo.get()); /* normal priority for messages */ - notify_notification_set_urgency(notification, NOTIFY_URGENCY_NORMAL); + 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, "closed", G_CALLBACK(notification_closed), cm); + g_signal_connect(notification_new, "closed", G_CALLBACK(notification_closed), cm); } - g_free(title); - g_free(body); - - GError *error = NULL; - success = notify_notification_show(notification, &error); + 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_hash_table_remove(chat_table, cm); } + g_free(title); + g_free(body); + return success; } @@ -294,24 +408,23 @@ ring_notify_close_chat_notification( g_return_val_if_fail(cm, FALSE); - GHashTable *chat_table = ring_notify_get_chat_table(); - - NotifyNotification *notification = (NotifyNotification *)g_hash_table_lookup(chat_table, cm); + auto chat_table = ring_notify_get_chat_table(); - if (notification) { - notification_existed = TRUE; + 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); + GError *error = NULL; + if (!notify_notification_close(notification, &error)) { + g_warning("could not close notification: %s", error->message); + g_clear_error(&error); + } - /* closing should remove and free the notification from the hash table - * since it failed to close, try to remove the notification from the - * table manually */ - g_hash_table_remove(chat_table, cm); + list = g_list_next(list); } } + #endif return notification_existed;