notifier.cpp 11.7 KB
Newer Older
1
/*
2
 *  Copyright (C) 2015-2019 Savoir-faire Linux Inc.
3
 *  Author: Stepan Salenikovich <stepan.salenikovich@savoirfairelinux.com>
4
 *  Author: Sebastien Blin <sebastien.blin@savoirfairelinux.com>
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
 *
 *  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.
 */

21
#include "notifier.h"
22
#include "config.h"
23
#include "client.h"
24

25 26 27 28
#if USE_CANBERRA
#include <canberra-gtk.h>
#endif // USE_CANBERRA

29
#if USE_LIBNOTIFY
30
#include <glib/gi18n.h>
31 32
#include <libnotify/notify.h>
#include <memory>
33 34
#include <globalinstances.h>
#include "native/pixbufmanipulator.h"
35
#include <QtCore/QSize>
Sébastien Blin's avatar
Sébastien Blin committed
36
#include <map>
37 38
#endif

39 40

static constexpr const char* SERVER_NOTIFY_OSD = "notify-osd";
41 42
static constexpr const char* NOTIFICATION_FILE = SOUNDSDIR "/ringtone_notify.wav";

43 44 45 46 47
namespace details
{
class CppImpl;
}

48
struct _Notifier
49 50 51 52
{
    GtkBox parent;
};

53
struct _NotifierClass
54 55 56 57
{
    GtkBoxClass parent_class;
};

58
typedef struct _NotifierPrivate NotifierPrivate;
59

60
struct _NotifierPrivate
61 62 63 64
{
    details::CppImpl* cpp; ///< Non-UI and C++ only code0
};

65
G_DEFINE_TYPE_WITH_PRIVATE(Notifier, notifier, GTK_TYPE_BOX);
66

67
#define NOTIFIER_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), NOTIFIER_TYPE, NotifierPrivate))
68 69 70 71 72 73 74 75 76 77 78

/* signals */
enum {
    SHOW_CHAT,
    ACCEPT_PENDING,
    REFUSE_PENDING,
    ACCEPT_CALL,
    DECLINE_CALL,
    LAST_SIGNAL
};

79
static guint notifier_signals[LAST_SIGNAL] = { 0 };
80 81 82 83 84 85 86

namespace details
{

class CppImpl
{
public:
87
    explicit CppImpl(Notifier& widget);
88 89
    ~CppImpl();

90 91
    Notifier* self = nullptr; // The GTK widget itself
    NotifierPrivate* priv = nullptr;
92

93 94 95 96 97 98 99 100
    /* server info and capabilities */
    char *name = nullptr;
    char *vendor = nullptr;
    char *version = nullptr;
    char *spec = nullptr;
    gboolean append;
    gboolean actions;

Sébastien Blin's avatar
Sébastien Blin committed
101
#if USE_LIBNOTIFY
102
    std::map<std::string, std::shared_ptr<NotifyNotification>> notifications_;
Sébastien Blin's avatar
Sébastien Blin committed
103
#endif
104 105 106 107 108 109
private:
    CppImpl() = delete;
    CppImpl(const CppImpl&) = delete;
    CppImpl& operator=(const CppImpl&) = delete;
};

110
CppImpl::CppImpl(Notifier& widget)
111 112 113
: self {&widget}
{
}
114 115

CppImpl::~CppImpl()
116 117 118 119 120 121 122 123 124
{
    if (name)
        g_free(name);
    if (vendor)
        g_free(vendor);
    if (version)
        g_free(version);
    if (spec)
        g_free(spec);
125 126
}

127 128
} // namespace details

129
static void
130
notifier_dispose(GObject *object)
131
{
132 133
    auto* self = NOTIFIER(object);
    auto* priv = NOTIFIER_GET_PRIVATE(self);
134 135 136 137 138 139 140

    delete priv->cpp;
    priv->cpp = nullptr;

#if USE_LIBNOTIFY
    if (notify_is_initted())
        notify_uninit();
141 142
#endif

143
    G_OBJECT_CLASS(notifier_parent_class)->dispose(object);
144 145 146
}

static void
147
notifier_init(Notifier *view)
148
{
149
    NotifierPrivate *priv = NOTIFIER_GET_PRIVATE(view);
150 151
    priv->cpp = new details::CppImpl {*view};

152
#if USE_LIBNOTIFY
153
    notify_init("Jami");
154 155

    /* get notify server info */
156 157 158 159
    if (notify_get_server_info(&priv->cpp->name,
                               &priv->cpp->vendor,
                               &priv->cpp->version,
                               &priv->cpp->spec)) {
160
        g_debug("notify server name: %s, vendor: %s, version: %s, spec: %s",
161
                priv->cpp->name, priv->cpp->vendor, priv->cpp->version, priv->cpp->spec);
162 163 164 165 166 167 168
    }

    /* 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) {
169
            priv->cpp->append = TRUE;
170 171
        }
        if (g_strcmp0((const char *)list->data, "actions") == 0) {
172
            priv->cpp->actions = TRUE;
173 174 175 176 177 178
        }

        list = g_list_next(list);
    }

    g_list_free_full(list, g_free);
179 180 181
#endif
}

182
static void
183
notifier_class_init(NotifierClass *klass)
184
{
185
    G_OBJECT_CLASS(klass)->dispose = notifier_dispose;
186

187
    notifier_signals[SHOW_CHAT] = g_signal_new(
188 189 190 191 192 193 194 195 196
        "showChatView",
        G_TYPE_FROM_CLASS(klass),
        (GSignalFlags) (G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED),
        0,
        nullptr,
        nullptr,
        g_cclosure_marshal_VOID__STRING,
        G_TYPE_NONE, 1, G_TYPE_STRING);

197
    notifier_signals[ACCEPT_PENDING] = g_signal_new(
198 199 200 201 202 203 204 205 206
        "acceptPending",
        G_TYPE_FROM_CLASS(klass),
        (GSignalFlags) (G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED),
        0,
        nullptr,
        nullptr,
        g_cclosure_marshal_VOID__STRING,
        G_TYPE_NONE, 1, G_TYPE_STRING);

207
    notifier_signals[REFUSE_PENDING] = g_signal_new(
208 209 210 211 212 213 214 215 216
        "refusePending",
        G_TYPE_FROM_CLASS(klass),
        (GSignalFlags) (G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED),
        0,
        nullptr,
        nullptr,
        g_cclosure_marshal_VOID__STRING,
        G_TYPE_NONE, 1, G_TYPE_STRING);

217
    notifier_signals[ACCEPT_CALL] = g_signal_new(
218 219 220 221 222 223 224 225 226
        "acceptCall",
        G_TYPE_FROM_CLASS(klass),
        (GSignalFlags) (G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED),
        0,
        nullptr,
        nullptr,
        g_cclosure_marshal_VOID__STRING,
        G_TYPE_NONE, 1, G_TYPE_STRING);

227
    notifier_signals[DECLINE_CALL] = g_signal_new(
228 229 230 231 232 233 234 235
        "declineCall",
        G_TYPE_FROM_CLASS(klass),
        (GSignalFlags) (G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED),
        0,
        nullptr,
        nullptr,
        g_cclosure_marshal_VOID__STRING,
        G_TYPE_NONE, 1, G_TYPE_STRING);
236 237
}

238
GtkWidget *
239
notifier_new()
240
{
241
    gpointer view = g_object_new(NOTIFIER_TYPE, NULL);
242
    return (GtkWidget *)view;
243 244
}

245
#if USE_LIBNOTIFY
246
static void
247
show_chat_view(NotifyNotification*, char* id, Notifier* view)
248
{
249
    g_signal_emit(G_OBJECT(view), notifier_signals[SHOW_CHAT], 0, id);
250 251
}

252
static void
253
accept_pending(NotifyNotification*, char* id, Notifier* view)
254
{
255
    std::string newId = id;
256
    g_signal_emit(G_OBJECT(view), notifier_signals[ACCEPT_PENDING], 0, newId.substr(std::string("add:").length()).c_str());
257
}
258

259
static void
260
refuse_pending(NotifyNotification*, char* id, Notifier* view)
261
{
262
    std::string newId = id;
263
    g_signal_emit(G_OBJECT(view), notifier_signals[REFUSE_PENDING], 0, newId.substr(std::string("rm:").length()).c_str());
264 265 266
}

static void
267
accept_call(NotifyNotification*, char* id, Notifier* view)
268
{
269
    std::string newId = id;
270
    g_signal_emit(G_OBJECT(view), notifier_signals[ACCEPT_CALL], 0, newId.substr(std::string("accept:").length()).c_str());
271 272 273
}

static void
274
decline_call(NotifyNotification*, char* id, Notifier* view)
275
{
276
    std::string newId = id;
277
    g_signal_emit(G_OBJECT(view), notifier_signals[DECLINE_CALL], 0, newId.substr(std::string("decline:").length()).c_str());
278 279
}

280 281 282
#endif

gboolean
283
show_notification(Notifier* view, const std::string& icon,
284 285 286
                       const std::string& uri, const std::string& name,
                       const std::string& id, const std::string& title,
                       const std::string& body, NotificationType type)
287
{
288
    g_return_val_if_fail(IS_NOTIFIER(view), false);
289
    gboolean success = FALSE;
290
    NotifierPrivate *priv = NOTIFIER_GET_PRIVATE(view);
291

292 293 294 295 296 297
#if USE_LIBNOTIFY
    std::shared_ptr<NotifyNotification> notification(
        notify_notification_new(title.c_str(), body.c_str(), nullptr), g_object_unref);
    priv->cpp->notifications_.emplace(id, notification);

    // Draw icon
298 299 300
    auto firstLetter = (name == uri || name.empty()) ?
        "" : QString(QString(name.c_str()).at(0)).toStdString();  // NOTE best way to be compatible with UTF-8
    auto default_avatar = Interfaces::PixbufManipulator().generateAvatar(firstLetter, "ring:" + uri);
301 302 303 304 305 306 307
    auto photo = Interfaces::PixbufManipulator().scaleAndFrame(default_avatar.get(), QSize(50, 50));
    if (!icon.empty()) {
        QByteArray byteArray(icon.c_str(), icon.length());
        QVariant avatar = Interfaces::PixbufManipulator().personPhoto(byteArray);
        auto pixbuf_photo = Interfaces::PixbufManipulator().scaleAndFrame(avatar.value<std::shared_ptr<GdkPixbuf>>().get(), QSize(50, 50));
        if (avatar.isValid()) {
            photo = pixbuf_photo;
308
        }
309 310
    }
    notify_notification_set_image_from_pixbuf(notification.get(), photo.get());
311

312 313 314
    if (type != NotificationType::CHAT) {
        notify_notification_set_urgency(notification.get(), NOTIFY_URGENCY_CRITICAL);
        notify_notification_set_timeout(notification.get(), NOTIFY_EXPIRES_DEFAULT);
315
    } else {
316
        notify_notification_set_urgency(notification.get(), NOTIFY_URGENCY_NORMAL);
317 318
    }

319
#if USE_CANBERRA
320 321 322 323 324 325 326 327
    if (type != NotificationType::CALL) {
        auto status = ca_context_play(ca_gtk_context_get(),
                                      0,
                                      CA_PROP_MEDIA_FILENAME,
                                      NOTIFICATION_FILE,
                                      nullptr);
        if (status != 0)
            g_warning("ca_context_play: %s", ca_strerror(status));
328
    }
329
#endif // USE_CANBERRA
330

331
    // if the notification server supports actions, make the default action to show the chat view
332
    if (priv->cpp->actions) {
333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371
        if (type != NotificationType::CALL) {
            notify_notification_add_action(notification.get(),
                id.c_str(),
                C_("", "Open conversation"),
                (NotifyActionCallback)show_chat_view,
                view,
                nullptr);
            if (type != NotificationType::CHAT) {
                auto addId = "add:" + id;
                notify_notification_add_action(notification.get(),
                    addId.c_str(),
                    C_("", "Accept"),
                    (NotifyActionCallback)accept_pending,
                    view,
                    nullptr);
                auto rmId = "rm:" + id;
                notify_notification_add_action(notification.get(),
                    rmId.c_str(),
                    C_("", "Refuse"),
                    (NotifyActionCallback)refuse_pending,
                    view,
                    nullptr);
            }
        } else {
            auto acceptId = "accept:" + id;
            notify_notification_add_action(notification.get(),
                acceptId.c_str(),
                C_("", "Accept"),
                (NotifyActionCallback)accept_call,
                view,
                nullptr);
            auto declineId = "decline:" + id;
            notify_notification_add_action(notification.get(),
                declineId.c_str(),
                C_("", "Decline"),
                (NotifyActionCallback)decline_call,
                view,
                nullptr);
        }
372 373
    }

374 375
    GError *error = nullptr;
    success = notify_notification_show(notification.get(), &error);
376

377 378 379
    if (error) {
        g_warning("failed to show notification: %s", error->message);
        g_clear_error(&error);
380
    }
381
#endif
382
    return success;
383
}
384 385

gboolean
386
hide_notification(Notifier* view, const std::string& id)
387
{
388 389
    g_return_val_if_fail(IS_NOTIFIER(view), false);
    NotifierPrivate *priv = NOTIFIER_GET_PRIVATE(view);
390 391

#if USE_LIBNOTIFY
392 393 394
    // Search
    auto notification = priv->cpp->notifications_.find(id);
    if (notification == priv->cpp->notifications_.end()) {
395
        return FALSE;
396
    }
397

398 399 400 401 402
    // Close
    GError *error = nullptr;
    if (!notify_notification_close(notification->second.get(), &error)) {
        g_warning("could not close notification: %s", error->message);
        g_clear_error(&error);
403
        return FALSE;
404
    }
405

406 407
    // Erase
    priv->cpp->notifications_.erase(id);
408
#endif
409 410

    return TRUE;
411
}