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;