2
0
mirror of https://github.com/kotatogram/kotatogram-desktop synced 2025-09-18 14:00:09 +00:00

Native notifications switched off by default. Libnotify supported.

This commit is contained in:
John Preston
2016-10-03 18:07:50 +03:00
parent 6db52f7fa9
commit c9288f2d0a
15 changed files with 348 additions and 85 deletions

View File

@@ -24,6 +24,7 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org
#include "window/notifications_utilities.h"
#include "platform/linux/linux_libnotify.h"
#include "platform/linux/linux_libs.h"
#include "lang.h"
namespace Platform {
namespace Notifications {
@@ -31,7 +32,7 @@ namespace {
NeverFreedPointer<Manager> ManagerInstance;
bool supported() {
bool LibNotifyLoaded() {
return (Libs::notify_init != nullptr)
&& (Libs::notify_uninit != nullptr)
&& (Libs::notify_is_initted != nullptr)
@@ -43,7 +44,7 @@ bool supported() {
&& (Libs::notify_notification_update != nullptr)
&& (Libs::notify_notification_show != nullptr)
// && (Libs::notify_notification_set_app_name != nullptr)
// && (Libs::notify_notification_set_timeout != nullptr)
&& (Libs::notify_notification_set_timeout != nullptr)
// && (Libs::notify_notification_set_category != nullptr)
// && (Libs::notify_notification_set_urgency != nullptr)
// && (Libs::notify_notification_set_icon_from_pixbuf != nullptr)
@@ -52,23 +53,139 @@ bool supported() {
// && (Libs::notify_notification_set_hint_int32 != nullptr)
// && (Libs::notify_notification_set_hint_uint32 != nullptr)
// && (Libs::notify_notification_set_hint_double != nullptr)
// && (Libs::notify_notification_set_hint_string != nullptr)
&& (Libs::notify_notification_set_hint_string != nullptr)
// && (Libs::notify_notification_set_hint_byte != nullptr)
// && (Libs::notify_notification_set_hint_byte_array != nullptr)
// && (Libs::notify_notification_clear_hints != nullptr)
// && (Libs::notify_notification_add_action != nullptr)
// && (Libs::notify_notification_clear_actions != nullptr)
&& (Libs::notify_notification_add_action != nullptr)
&& (Libs::notify_notification_clear_actions != nullptr)
&& (Libs::notify_notification_close != nullptr)
&& (Libs::notify_notification_get_closed_reason != nullptr)
&& (Libs::g_object_ref_sink != nullptr)
&& (Libs::g_object_unref != nullptr)
&& (Libs::g_list_free_full != nullptr)
&& (Libs::g_error_free != nullptr)
&& (Libs::g_signal_connect_data != nullptr)
&& (Libs::g_signal_handler_disconnect != nullptr)
// && (Libs::gdk_pixbuf_new_from_data != nullptr)
&& (Libs::gdk_pixbuf_new_from_file != nullptr);
}
QString escapeNotificationHtml(QString text) {
text = text.replace(QChar('<'), qstr("&lt;"));
text = text.replace(QChar('>'), qstr("&gt;"));
text = text.replace(QChar('&'), qstr("&amp;"));
return text;
}
class NotificationData {
public:
NotificationData(const QString &title, const QString &body, PeerId peerId, MsgId msgId)
: _data(Libs::notify_notification_new(title.toUtf8().constData(), body.toUtf8().constData(), nullptr))
, _peerId(peerId)
, _msgId(msgId) {
if (valid()) {
init();
}
}
bool valid() const {
return (_data != nullptr);
}
NotificationData(const NotificationData &other) = delete;
NotificationData &operator=(const NotificationData &other) = delete;
NotificationData(NotificationData &&other) = delete;
NotificationData &operator=(NotificationData &&other) = delete;
void setImage(const QString &imagePath) {
auto imagePathNative = QFile::encodeName(imagePath);
if (auto pixbuf = Libs::gdk_pixbuf_new_from_file(imagePathNative.constData(), nullptr)) {
Libs::notify_notification_set_image_from_pixbuf(_data, pixbuf);
Libs::g_object_unref(Libs::g_object_cast(pixbuf));
}
}
bool show() {
if (valid()) {
GError *error = nullptr;
Libs::notify_notification_show(_data, &error);
if (!error) {
return true;
}
logError(error);
}
return false;
}
bool close() {
if (valid()) {
GError *error = nullptr;
Libs::notify_notification_close(_data, &error);
if (!error) {
return true;
}
logError(error);
}
return false;
}
~NotificationData() {
if (valid()) {
if (_handlerId > 0) {
Libs::g_signal_handler_disconnect(Libs::g_object_cast(_data), _handlerId);
}
Libs::notify_notification_clear_actions(_data);
Libs::g_object_unref(Libs::g_object_cast(_data));
}
}
private:
void init() {
_handlerId = Libs::g_signal_connect_helper(Libs::g_object_cast(_data), "closed", G_CALLBACK(NotificationData::notificationClosed), this);
Libs::notify_notification_set_timeout(_data, Libs::NOTIFY_EXPIRES_DEFAULT);
if (auto manager = ManagerInstance.data()) {
if (manager->hasActionsSupport()) {
Libs::notify_notification_add_action(_data, "default", lang(lng_context_reply_msg).toUtf8().constData(), NotificationData::notificationClicked, this, nullptr);
}
}
}
void onClose() {
if (auto manager = ManagerInstance.data()) {
manager->clearNotification(_peerId, _msgId);
}
}
void onClick() {
if (auto manager = ManagerInstance.data()) {
manager->notificationActivated(_peerId, _msgId);
}
}
void logError(GError *error) {
LOG(("LibNotify Error: domain %1, code %2, message '%3'").arg(error->domain).arg(error->code).arg(QString::fromUtf8(error->message)));
Libs::g_error_free(error);
}
static void notificationClosed(Libs::NotifyNotification *notification, gpointer data) {
static_cast<NotificationData*>(data)->onClose();
}
static void notificationClicked(Libs::NotifyNotification *notification, char *action, gpointer data) {
static_cast<NotificationData*>(data)->onClick();
}
Libs::NotifyNotification *_data = nullptr;
PeerId _peerId = 0;
MsgId _msgId = 0;
gulong _handlerId = 0;
};
using Notification = QSharedPointer<NotificationData>;
} // namespace
void start() {
if (supported()) {
if (LibNotifyLoaded()) {
if (Libs::notify_is_initted() || Libs::notify_init("Telegram Desktop")) {
ManagerInstance.makeIfNull();
}
@@ -76,7 +193,14 @@ void start() {
}
Manager *manager() {
return ManagerInstance.data();
if (Global::NativeNotifications()) {
return ManagerInstance.data();
}
return nullptr;
}
bool supported() {
return ManagerInstance.data() != nullptr;
}
void finish() {
@@ -88,131 +212,214 @@ void finish() {
class Manager::Impl {
public:
Impl();
void showNotification(PeerData *peer, MsgId msgId, const QString &title, const QString &subtitle, bool showUserpic, const QString &msg, bool showReplyButton);
void clearAll();
void clearFromHistory(History *history);
void clearNotification(PeerId peerId, MsgId msgId);
bool hasPoorSupport() const {
return _poorSupported;
}
bool hasActionsSupport() const {
return _actionsSupported;
}
private:
static void notificationClosed(Libs::NotifyNotification *notification, gpointer data);
void clearNotification(Libs::NotifyNotification *notification);
void showNextNotification();
using Notifications = QMap<PeerId, QMap<MsgId, Libs::NotifyNotification*>>;
struct QueuedNotification {
PeerData *peer = nullptr;
MsgId msgId = 0;
QString title;
QString body;
bool showUserpic = false;
};
using QueuedNotifications = QList<QueuedNotification>;
QueuedNotifications _queuedNotifications;
using Notifications = QMap<PeerId, QMap<MsgId, Notification>>;
Notifications _notifications;
using NotificationsData = QMap<Libs::NotifyNotification*, QPair<PeerId, MsgId>>;
NotificationsData _notificationsData;
Window::Notifications::CachedUserpics _cachedUserpics;
bool _actionsSupported = false;
bool _poorSupported = true;
};
void Manager::Impl::notificationClosed(Libs::NotifyNotification *notification, gpointer data) {
if (auto manager = ManagerInstance.data()) {
manager->_impl->clearNotification(notification);
}
void FreeCapability(void *ptr, void *data) {
Libs::g_free(ptr);
}
void Manager::Impl::clearNotification(Libs::NotifyNotification *notification) {
auto dataIt = _notificationsData.find(notification);
if (dataIt == _notificationsData.cend()) {
return;
Manager::Impl::Impl() {
if (auto capabilities = Libs::notify_get_server_caps()) {
for (auto capability = capabilities; capability; capability = capability->next) {
if (QString::fromUtf8(static_cast<const char*>(capability->data)) == qstr("actions")) {
_actionsSupported = true;
break;
}
}
Libs::g_list_free_full(capabilities, g_free);
Libs::g_list_free(capabilities);
}
auto peerId = dataIt->first;
auto msgId = dataIt->second;
_notificationsData.erase(dataIt);
auto i = _notifications.find(peerId);
if (i != _notifications.cend()) {
auto it = i.value().find(msgId);
if (it != i.value().cend()) {
Libs::g_object_unref(Libs::g_object_cast(it.value()));
i.value().erase(it);
if (i.value().isEmpty()) {
_notifications.erase(i);
// Unity and other Notify OSD users handle desktop notifications
// extremely poor, even without the ability to close() them.
gchar *name = nullptr;
if (Libs::notify_get_server_info(&name, nullptr, nullptr, nullptr)) {
if (name) {
auto serverName = QString::fromUtf8(static_cast<const char*>(name));
LOG(("Notifications Server: %1").arg(serverName));
if (serverName == qstr("notify-osd")) {
_actionsSupported = false;
}
Libs::g_free(name);
}
}
if (!_actionsSupported) {
_poorSupported = true;
}
}
void Manager::Impl::showNotification(PeerData *peer, MsgId msgId, const QString &title, const QString &subtitle, bool showUserpic, const QString &msg, bool showReplyButton) {
auto titleUtf8 = title.toUtf8();
auto bodyUtf8 = (subtitle.isEmpty() ? msg : ("<b>" + subtitle + "</b>\n" + msg)).toUtf8();
auto notification = Libs::notify_notification_new(titleUtf8.constData(), bodyUtf8.constData(), nullptr);
if (!notification) {
auto titleText = escapeNotificationHtml(title);
auto bodyText = subtitle.isEmpty() ? escapeNotificationHtml(msg) : ("<b>" + escapeNotificationHtml(subtitle) + "</b>\n" + escapeNotificationHtml(msg));
QueuedNotification notification;
notification.peer = peer;
notification.msgId = msgId;
notification.title = titleText;
notification.body = bodyText;
notification.showUserpic = showUserpic;
_queuedNotifications.push_back(notification);
showNextNotification();
}
void Manager::Impl::showNextNotification() {
// Show only one notification at a time in Unity / Notify OSD.
if (_poorSupported) {
for (auto b = _notifications.begin(); !_notifications.isEmpty() && b->isEmpty();) {
_notifications.erase(b);
}
if (!_notifications.isEmpty()) {
return;
}
}
QueuedNotification data;
while (!_queuedNotifications.isEmpty()) {
data = _queuedNotifications.front();
_queuedNotifications.pop_front();
if (data.peer) {
break;
}
}
if (!data.peer) {
return;
}
Libs::g_signal_connect_helper(Libs::g_object_cast(notification), "closed", G_CALLBACK(Manager::Impl::notificationClosed), peer);
auto peerId = data.peer->id;
auto msgId = data.msgId;
auto notification = MakeShared<NotificationData>(data.title, data.body, peerId, msgId);
if (!notification->valid()) {
return;
}
StorageKey key;
if (showUserpic) {
key = peer->userpicUniqueKey();
if (data.showUserpic) {
key = data.peer->userpicUniqueKey();
} else {
key = StorageKey(0, 0);
}
auto userpicPath = _cachedUserpics.get(key, peer);
auto userpicPathNative = QFile::encodeName(userpicPath);
if (auto pixbuf = Libs::gdk_pixbuf_new_from_file(userpicPathNative.constData(), nullptr)) {
Libs::notify_notification_set_image_from_pixbuf(notification, pixbuf);
Libs::g_object_unref(Libs::g_object_cast(pixbuf));
}
notification->setImage(_cachedUserpics.get(key, data.peer));
auto i = _notifications.find(peer->id);
auto i = _notifications.find(peerId);
if (i != _notifications.cend()) {
auto j = i->find(msgId);
if (j != i->cend()) {
auto oldNotification = j.value();
i->erase(j);
_notificationsData.remove(oldNotification);
Libs::notify_notification_close(oldNotification, nullptr);
Libs::g_object_unref(Libs::g_object_cast(oldNotification));
i = _notifications.find(peer->id);
oldNotification->close();
i = _notifications.find(peerId);
}
}
if (i == _notifications.cend()) {
i = _notifications.insert(peer->id, QMap<MsgId, Libs::NotifyNotification*>());
i = _notifications.insert(peerId, QMap<MsgId, Notification>());
}
auto result = Libs::notify_notification_show(notification, nullptr);
if (!result) {
Libs::g_object_unref(Libs::g_object_cast(notification));
i = _notifications.find(peer->id);
if (i != _notifications.cend() && i->isEmpty()) _notifications.erase(i);
return;
_notifications[peerId].insert(msgId, notification);
if (!notification->show()) {
i = _notifications.find(peerId);
if (i != _notifications.cend()) {
i->remove(msgId);
if (i->isEmpty()) _notifications.erase(i);
}
showNextNotification();
}
_notifications[peer->id].insert(msgId, notification);
_notificationsData.insert(notification, qMakePair(peer->id, msgId));
}
void Manager::Impl::clearAll() {
_notificationsData.clear();
_queuedNotifications.clear();
auto temp = createAndSwap(_notifications);
for_const (auto &notifications, temp) {
for_const (auto notification, notifications) {
Libs::notify_notification_close(notification, nullptr);
Libs::g_object_unref(Libs::g_object_cast(notification));
notification->close();
}
}
}
void Manager::Impl::clearFromHistory(History *history) {
for (auto i = _queuedNotifications.begin(); i != _queuedNotifications.end();) {
if (i->peer == history->peer) {
i = _queuedNotifications.erase(i);
} else {
++i;
}
}
auto i = _notifications.find(history->peer->id);
if (i != _notifications.cend()) {
auto temp = createAndSwap(i.value());
_notifications.erase(i);
for_const (auto notification, temp) {
_notificationsData.remove(notification);
Libs::notify_notification_close(notification, nullptr);
Libs::g_object_unref(Libs::g_object_cast(notification));
notification->close();
}
}
showNextNotification();
}
void Manager::Impl::clearNotification(PeerId peerId, MsgId msgId) {
auto i = _notifications.find(peerId);
if (i != _notifications.cend()) {
i.value().remove(msgId);
if (i.value().isEmpty()) {
_notifications.erase(i);
}
}
showNextNotification();
}
Manager::Manager() : _impl(std_::make_unique<Impl>()) {
}
void Manager::clearNotification(PeerId peerId, MsgId msgId) {
_impl->clearNotification(peerId, msgId);
}
bool Manager::hasPoorSupport() const {
return _impl->hasPoorSupport();
}
bool Manager::hasActionsSupport() const {
return _impl->hasActionsSupport();
}
Manager::~Manager() = default;
void Manager::doShowNativeNotification(PeerData *peer, MsgId msgId, const QString &title, const QString &subtitle, bool showUserpic, const QString &msg, bool showReplyButton) {