blob: e36373faedb59b710cd5db47d514b18dd35faf11 [file] [log] [blame]
Stepan Salenikovich76c33e62015-05-22 12:24:07 -04001/*
Guillaume Roguez77c579d2018-01-30 15:54:02 -05002 * Copyright (C) 2015-2018 Savoir-faire Linux Inc.
Stepan Salenikovich76c33e62015-05-22 12:24:07 -04003 * Author: Stepan Salenikovich <stepan.salenikovich@savoirfairelinux.com>
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Stepan Salenikovich76c33e62015-05-22 12:24:07 -040018 */
19
20#include "ringnotify.h"
21#include "config.h"
Stepan Salenikovich5834f832016-05-20 15:32:29 -040022#include "ring_client.h"
Stepan Salenikovich76c33e62015-05-22 12:24:07 -040023
Nicolas Jager655b8db2017-12-15 14:04:32 -050024#if USE_CANBERRA
25#include <canberra-gtk.h>
26#endif // USE_CANBERRA
27
Stepan Salenikovich76c33e62015-05-22 12:24:07 -040028#if USE_LIBNOTIFY
Stepan Salenikovicha1b8cb32015-09-11 14:58:35 -040029#include <glib/gi18n.h>
Stepan Salenikovich76c33e62015-05-22 12:24:07 -040030#include <libnotify/notify.h>
31#include <memory>
Stepan Salenikovichbbd6c132015-08-20 15:21:48 -040032#include <globalinstances.h>
33#include "native/pixbufmanipulator.h"
Stepan Salenikovich76c33e62015-05-22 12:24:07 -040034#include <call.h>
35#include <QtCore/QSize>
Stepan Salenikovich67112d12015-06-16 16:57:06 -040036#include <media/text.h>
37#include <callmodel.h>
Stepan Salenikovich26cd1602016-01-20 13:43:17 -050038#include <media/textrecording.h>
39#include <media/recordingmodel.h>
Stepan Salenikovichf8e78cb2016-09-12 17:14:51 -040040#include <recentmodel.h>
Stepan Salenikovich76c33e62015-05-22 12:24:07 -040041#endif
42
Stepan Salenikovichfc79fc12016-09-12 15:29:38 -040043#if USE_LIBNOTIFY
44
45static constexpr int MAX_NOTIFICATIONS = 10; // max unread chat msgs to display from the same contact
46static constexpr const char* SERVER_NOTIFY_OSD = "notify-osd";
Nicolas Jager655b8db2017-12-15 14:04:32 -050047static constexpr const char* NOTIFICATION_FILE = SOUNDSDIR "/ringtone_notify.wav";
48
Stepan Salenikovichfc79fc12016-09-12 15:29:38 -040049
50/* struct to store the parsed list of the notify server capabilities */
51struct RingNotifyServerInfo
52{
53 /* info */
54 char *name;
55 char *vendor;
56 char *version;
57 char *spec;
58
59 /* capabilities */
60 gboolean append;
61 gboolean actions;
62
63 /* the info strings must be freed */
64 ~RingNotifyServerInfo() {
65 g_free(name);
66 g_free(vendor);
67 g_free(version);
68 g_free(spec);
69 }
70};
71
72static struct RingNotifyServerInfo server_info;
73#endif
74
Stepan Salenikovich76c33e62015-05-22 12:24:07 -040075void
76ring_notify_init()
77{
78#if USE_LIBNOTIFY
79 notify_init("Ring");
Stepan Salenikovichfc79fc12016-09-12 15:29:38 -040080
81 /* get notify server info */
82 if (notify_get_server_info(&server_info.name,
83 &server_info.vendor,
84 &server_info.version,
85 &server_info.spec)) {
86 g_debug("notify server name: %s, vendor: %s, version: %s, spec: %s",
87 server_info.name, server_info.vendor, server_info.version, server_info.spec);
88 }
89
90 /* check notify server capabilities */
91 auto list = notify_get_server_caps();
92 while (list) {
93 if (g_strcmp0((const char *)list->data, "append") == 0 ||
94 g_strcmp0((const char *)list->data, "x-canonical-append") == 0) {
95 server_info.append = TRUE;
96 }
97 if (g_strcmp0((const char *)list->data, "actions") == 0) {
98 server_info.actions = TRUE;
99 }
100
101 list = g_list_next(list);
102 }
103
104 g_list_free_full(list, g_free);
Stepan Salenikovich76c33e62015-05-22 12:24:07 -0400105#endif
106}
107
108void
109ring_notify_uninit()
110{
111#if USE_LIBNOTIFY
112 if (notify_is_initted())
113 notify_uninit();
114#endif
115}
116
117gboolean
118ring_notify_is_initted()
119{
120#if USE_LIBNOTIFY
121 return notify_is_initted();
122#else
123 return FALSE;
124#endif
125}
126
Rafał Babskib2105532017-07-26 00:10:36 +0200127#if USE_LIBNOTIFY
Stepan Salenikovichf8e78cb2016-09-12 17:14:51 -0400128static void
129ring_notify_show_cm(NotifyNotification*, char *, ContactMethod *cm)
130{
131 /* show the main window in case its hidden */
132 if (auto action = g_action_map_lookup_action(G_ACTION_MAP(g_application_get_default()), "show-main-window")) {
133 g_action_change_state(action, g_variant_new_boolean(TRUE));
134 }
135 /* select the relevant cm */
136 auto idx = RecentModel::instance().getIndex(cm);
137 if (idx.isValid()) {
138 RecentModel::instance().selectionModel()->setCurrentIndex(idx, QItemSelectionModel::ClearAndSelect);
139 }
140}
Rafał Babskib2105532017-07-26 00:10:36 +0200141#endif
Stepan Salenikovichf8e78cb2016-09-12 17:14:51 -0400142
Stepan Salenikovich76c33e62015-05-22 12:24:07 -0400143gboolean
144ring_notify_incoming_call(
145#if !USE_LIBNOTIFY
146 G_GNUC_UNUSED
147#endif
148 Call* call)
149{
Stepan Salenikovich9675db32015-06-16 14:36:54 -0400150 gboolean success = FALSE;
Stepan Salenikovich76c33e62015-05-22 12:24:07 -0400151#if USE_LIBNOTIFY
152 g_return_val_if_fail(call, FALSE);
153
Stepan Salenikovichf8e78cb2016-09-12 17:14:51 -0400154 gchar *body = g_markup_escape_text(call->formattedName().toUtf8().constData(), -1);
Stepan Salenikovich9675db32015-06-16 14:36:54 -0400155 std::shared_ptr<NotifyNotification> notification(
Stepan Salenikovicha1b8cb32015-09-11 14:58:35 -0400156 notify_notification_new(_("Incoming call"), body, NULL), g_object_unref);
Stepan Salenikovichc2e44262015-06-16 12:46:33 -0400157 g_free(body);
Stepan Salenikovich76c33e62015-05-22 12:24:07 -0400158
159 /* get photo */
Stepan Salenikovichbbd6c132015-08-20 15:21:48 -0400160 QVariant var_p = GlobalInstances::pixmapManipulator().callPhoto(
Stepan Salenikovich76c33e62015-05-22 12:24:07 -0400161 call->peerContactMethod(), QSize(50, 50), false);
162 std::shared_ptr<GdkPixbuf> photo = var_p.value<std::shared_ptr<GdkPixbuf>>();
Stepan Salenikovich9675db32015-06-16 14:36:54 -0400163 notify_notification_set_image_from_pixbuf(notification.get(), photo.get());
Stepan Salenikovich76c33e62015-05-22 12:24:07 -0400164
165 /* calls have highest urgency */
Stepan Salenikovich9675db32015-06-16 14:36:54 -0400166 notify_notification_set_urgency(notification.get(), NOTIFY_URGENCY_CRITICAL);
167 notify_notification_set_timeout(notification.get(), NOTIFY_EXPIRES_DEFAULT);
Stepan Salenikovich76c33e62015-05-22 12:24:07 -0400168
Stepan Salenikovichf8e78cb2016-09-12 17:14:51 -0400169 /* if the notification server supports actions, make the default action to show the call */
170 if (server_info.actions) {
171 notify_notification_add_action(notification.get(),
172 "default",
173 C_("notification action name", "Show"),
174 (NotifyActionCallback)ring_notify_show_cm,
175 call->peerContactMethod(),
176 nullptr);
177 }
178
Stepan Salenikovich76c33e62015-05-22 12:24:07 -0400179 GError *error = NULL;
Stepan Salenikovich9675db32015-06-16 14:36:54 -0400180 success = notify_notification_show(notification.get(), &error);
181
182 if (success) {
183 /* monitor the life cycle of the call and try to close the notification
184 * once the call has been aswered */
185 auto state_changed_conn = std::make_shared<QMetaObject::Connection>();
186 *state_changed_conn = QObject::connect(
187 call,
188 &Call::lifeCycleStateChanged,
189 [notification, state_changed_conn] (Call::LifeCycleState newState, G_GNUC_UNUSED Call::LifeCycleState previousState)
190 {
191 g_return_if_fail(NOTIFY_IS_NOTIFICATION(notification.get()));
192 if (newState > Call::LifeCycleState::INITIALIZATION) {
193 /* note: not all systems will actually close the notification
194 * even if the above function returns as true */
195 if (!notify_notification_close(notification.get(), NULL))
196 g_warning("could not close notification");
197
198 /* once we (try to) close the notification, we can
199 * disconnect from this signal; this should also destroy
200 * the notification shared_ptr as its ref count will
201 * drop to 0 */
202 QObject::disconnect(*state_changed_conn);
203 }
204 }
205 );
Stepan Salenikovich76c33e62015-05-22 12:24:07 -0400206 } else {
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400207 g_warning("failed to show notification: %s", error->message);
Stepan Salenikovich76c33e62015-05-22 12:24:07 -0400208 g_clear_error(&error);
Stepan Salenikovich76c33e62015-05-22 12:24:07 -0400209 }
Stepan Salenikovich76c33e62015-05-22 12:24:07 -0400210#endif
Stepan Salenikovich9675db32015-06-16 14:36:54 -0400211 return success;
Stepan Salenikovich76c33e62015-05-22 12:24:07 -0400212}
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400213
214#if USE_LIBNOTIFY
215
Stepan Salenikovichfc79fc12016-09-12 15:29:38 -0400216static void
217ring_notify_free_list(gpointer, GList *value, gpointer)
218{
219 if (value) {
220 g_object_unref(G_OBJECT(value->data));
221 g_list_free(value);
222 }
223}
224
225static void
226ring_notify_free_chat_table(GHashTable *table) {
227 if (table) {
228 g_hash_table_foreach(table, (GHFunc)ring_notify_free_list, nullptr);
229 g_hash_table_destroy(table);
230 }
231}
232
233/**
234 * Returns a pointer to a GHashTable which contains key,value pairs where a ContactMethod pointer
235 * is the key and a GList of notifications for that CM is the vlue.
236 */
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400237GHashTable *
238ring_notify_get_chat_table()
239{
Stepan Salenikovichfc79fc12016-09-12 15:29:38 -0400240 static std::unique_ptr<GHashTable, decltype(ring_notify_free_chat_table)&> chat_table(
241 nullptr, ring_notify_free_chat_table);
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400242
243 if (chat_table.get() == nullptr)
Stepan Salenikovichfc79fc12016-09-12 15:29:38 -0400244 chat_table.reset(g_hash_table_new(NULL, NULL));
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400245
246 return chat_table.get();
247}
248
249static void
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500250notification_closed(NotifyNotification *notification, ContactMethod *cm)
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400251{
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500252 g_return_if_fail(cm);
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400253
Stepan Salenikovichfc79fc12016-09-12 15:29:38 -0400254 /* remove from the list */
255 auto chat_table = ring_notify_get_chat_table();
256 if (auto list = (GList *)g_hash_table_lookup(chat_table, cm)) {
257 list = g_list_remove(list, notification);
258 if (list) {
259 // the head of the list may have changed
260 g_hash_table_replace(chat_table, cm, list);
261 } else {
262 g_hash_table_remove(chat_table, cm);
263 }
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400264 }
Stepan Salenikovichfc79fc12016-09-12 15:29:38 -0400265
266 g_object_unref(notification);
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400267}
268
269static gboolean
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500270ring_notify_show_text_message(ContactMethod *cm, const QModelIndex& idx)
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400271{
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500272 g_return_val_if_fail(idx.isValid() && cm, FALSE);
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400273 gboolean success = FALSE;
274
Stepan Salenikovichfc79fc12016-09-12 15:29:38 -0400275 auto title = g_markup_printf_escaped(C_("Text message notification", "%s says:"), idx.data(static_cast<int>(Ring::Role::Name)).toString().toUtf8().constData());
276 auto body = g_markup_escape_text(idx.data(Qt::DisplayRole).toString().toUtf8().constData(), -1);
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400277
Stepan Salenikovichfc79fc12016-09-12 15:29:38 -0400278 NotifyNotification *notification_new = nullptr;
279 NotifyNotification *notification_old = nullptr;
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400280
Stepan Salenikovichfc79fc12016-09-12 15:29:38 -0400281 /* try to get the previous notification */
282 auto chat_table = ring_notify_get_chat_table();
283 auto list = (GList *)g_hash_table_lookup(chat_table, cm);
284 if (list)
285 notification_old = (NotifyNotification *)list->data;
286
287 /* we display chat notifications in different ways to suit different notification servers and
288 * their capabilities:
289 * 1. if the server doesn't support appending (eg: Notification Daemon) then we update the
290 * previous notification (if exists) with new text; otherwise it takes we have many
291 * notifications from the same person... we don't concatinate the old messages because
292 * servers which don't support append usually don't support multi line bodies
293 * 2. the notify-osd server supports appending; however it doesn't clear the old notifications
294 * on demand, which means in our case that chat messages which have already been read could
295 * still be displayed when a new notification is appended, thus in this case, we update
296 * the old notification body manually to only contain the unread messages
297 * 3. the 3rd case is that the server supports append but is not notify-osd, then we simply use
298 * the append feature
299 */
300
301 if (notification_old && !server_info.append) {
302 /* case 1 */
303 notify_notification_update(notification_old, title, body, nullptr);
304 notification_new = notification_old;
305 } else if (notification_old && g_strcmp0(server_info.name, SERVER_NOTIFY_OSD) == 0) {
306 /* case 2 */
307 /* print up to MAX_NOTIFICATIONS unread messages */
308 int msg_count = 0;
309 auto idx_next = idx.sibling(idx.row() - 1, idx.column());
310 auto read = idx_next.data(static_cast<int>(Media::TextRecording::Role::IsRead)).toBool();
311 while (idx_next.isValid() && !read && msg_count < MAX_NOTIFICATIONS) {
312
313 auto body_prev = body;
314 body = g_markup_printf_escaped("%s\n%s", body_prev, idx_next.data(Qt::DisplayRole).toString().toUtf8().constData());
315 g_free(body_prev);
316
317 idx_next = idx_next.sibling(idx_next.row() - 1, idx_next.column());
318 read = idx_next.data(static_cast<int>(Media::TextRecording::Role::IsRead)).toBool();
319 ++msg_count;
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400320 }
Stepan Salenikovichfc79fc12016-09-12 15:29:38 -0400321
322 notify_notification_update(notification_old, title, body, nullptr);
323
324 notification_new = notification_old;
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400325 } else {
Stepan Salenikovichfc79fc12016-09-12 15:29:38 -0400326 /* need new notification for case 1, 2, or 3 */
327 notification_new = notify_notification_new(title, body, nullptr);
328
329 /* track in hash table */
330 auto list = (GList *)g_hash_table_lookup(chat_table, cm);
331 list = g_list_append(list, notification_new);
332 g_hash_table_replace(chat_table, cm, list);
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400333
334 /* get photo */
Stepan Salenikovichbbd6c132015-08-20 15:21:48 -0400335 QVariant var_p = GlobalInstances::pixmapManipulator().callPhoto(
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500336 cm, QSize(50, 50), false);
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400337 std::shared_ptr<GdkPixbuf> photo = var_p.value<std::shared_ptr<GdkPixbuf>>();
Stepan Salenikovichfc79fc12016-09-12 15:29:38 -0400338 notify_notification_set_image_from_pixbuf(notification_new, photo.get());
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400339
340 /* normal priority for messages */
Stepan Salenikovichfc79fc12016-09-12 15:29:38 -0400341 notify_notification_set_urgency(notification_new, NOTIFY_URGENCY_NORMAL);
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400342
343 /* remove the key and value from the hash table once the notification is
344 * closed; note that this will also unref the notification */
Stepan Salenikovichfc79fc12016-09-12 15:29:38 -0400345 g_signal_connect(notification_new, "closed", G_CALLBACK(notification_closed), cm);
Stepan Salenikovichf8e78cb2016-09-12 17:14:51 -0400346
347 /* if the notification server supports actions, make the default action to show the chat view */
348 if (server_info.actions) {
349 notify_notification_add_action(notification_new,
350 "default",
351 C_("notification action name", "Show"),
352 (NotifyActionCallback)ring_notify_show_cm,
353 cm,
354 nullptr);
355 }
Stepan Salenikovichfc79fc12016-09-12 15:29:38 -0400356 }
357
358 GError *error = nullptr;
359 success = notify_notification_show(notification_new, &error);
Nicolas Jager655b8db2017-12-15 14:04:32 -0500360
361#if USE_CANBERRA
362 auto status = ca_context_play(ca_gtk_context_get(),
363 0,
364 CA_PROP_MEDIA_FILENAME,
365 NOTIFICATION_FILE,
366 NULL);
367 if (status != 0)
368 g_warning("ca_context_play: %s", ca_strerror(status));
369#endif // USE_CANBERRA
370
Stepan Salenikovichfc79fc12016-09-12 15:29:38 -0400371 if (!success) {
372 g_warning("failed to show notification: %s", error->message);
373 g_clear_error(&error);
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400374 }
375
376 g_free(title);
377 g_free(body);
378
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400379 return success;
380}
381
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500382static gboolean
383show_message_if_unread(const QModelIndex *idx)
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400384{
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500385 g_return_val_if_fail(idx && idx->isValid(), G_SOURCE_REMOVE);
386
387 if (!idx->data(static_cast<int>(Media::TextRecording::Role::IsRead)).toBool()) {
388 auto cm = idx->data(static_cast<int>(Media::TextRecording::Role::ContactMethod)).value<ContactMethod *>();
389 ring_notify_show_text_message(cm, *idx);
390 }
391
392 return G_SOURCE_REMOVE;
393}
394
395static void
396delete_idx(QModelIndex *idx)
397{
Nicolas Jagerfad674f2017-10-19 11:35:36 -0400398 if (idx) {
399 delete idx;
400 idx = nullptr;
401 }
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500402}
403
Stepan Salenikovich5a127672016-09-13 11:19:50 -0400404#endif
405
406void
407ring_notify_message(
408#if !USE_LIBNOTIFY
Nicolas Jagercfeffc32018-01-12 10:58:55 -0500409 ContactMethod*, Media::TextRecording*)
Stepan Salenikovich5a127672016-09-13 11:19:50 -0400410#else
Nicolas Jagercfeffc32018-01-12 10:58:55 -0500411 ContactMethod *cm, Media::TextRecording *t)
Stepan Salenikovich5a127672016-09-13 11:19:50 -0400412#endif
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500413{
Stepan Salenikovich5a127672016-09-13 11:19:50 -0400414
415#if USE_LIBNOTIFY
Nicolas Jagercfeffc32018-01-12 10:58:55 -0500416 g_return_if_fail(cm && t);
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500417
418 // get the message
419 auto model = t->instantMessagingModel();
420 auto msg_idx = model->index(model->rowCount()-1, 0);
421
Nicolas Jagercfeffc32018-01-12 10:58:55 -0500422 ring_notify_show_text_message(cm, msg_idx);
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500423
Nicolas Jagercfeffc32018-01-12 10:58:55 -0500424 return;
Stepan Salenikovich5a127672016-09-13 11:19:50 -0400425#endif
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400426}
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400427
428gboolean
429ring_notify_close_chat_notification(
430#if !USE_LIBNOTIFY
431 G_GNUC_UNUSED
432#endif
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500433 ContactMethod *cm)
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400434{
435 gboolean notification_existed = FALSE;
436
437#if USE_LIBNOTIFY
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500438 /* checks if there exists a chat notification associated with the given ContactMethod
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400439 * and tries to close it; if it did exist, then the function returns TRUE */
Stepan Salenikovich26cd1602016-01-20 13:43:17 -0500440 g_return_val_if_fail(cm, FALSE);
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400441
442
Stepan Salenikovichfc79fc12016-09-12 15:29:38 -0400443 auto chat_table = ring_notify_get_chat_table();
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400444
Stepan Salenikovichfc79fc12016-09-12 15:29:38 -0400445 if (auto list = (GList *)g_hash_table_lookup(chat_table, cm)) {
446 while (list) {
447 notification_existed = TRUE;
448 auto notification = (NotifyNotification *)list->data;
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400449
Stepan Salenikovichfc79fc12016-09-12 15:29:38 -0400450 GError *error = NULL;
451 if (!notify_notification_close(notification, &error)) {
452 g_warning("could not close notification: %s", error->message);
453 g_clear_error(&error);
454 }
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400455
Stepan Salenikovichfc79fc12016-09-12 15:29:38 -0400456 list = g_list_next(list);
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400457 }
458 }
Stepan Salenikovichfc79fc12016-09-12 15:29:38 -0400459
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400460#endif
461
462 return notification_existed;
463}