Support GNotification

It's used if there's a gtk notification daemon or application is running sandboxed without access to the freedesktop protocol.

GNotification API is poor, but should feel native on environments using GNOME technologies.
This commit is contained in:
Ilya Fedin 2022-11-11 15:49:50 +04:00 committed by John Preston
parent afaad155a0
commit f9dd2b4a0a
10 changed files with 439 additions and 16 deletions

View File

@ -1125,6 +1125,7 @@ PRIVATE
platform/platform_integration.h
platform/platform_main_window.h
platform/platform_notifications_manager.h
platform/platform_specific.cpp
platform/platform_specific.h
platform/platform_tray.h
platform/platform_window_title.h

View File

@ -9,10 +9,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "platform/linux/notifications_manager_linux.h"
#include "window/notifications_utilities.h"
#include "base/options.h"
#include "base/platform/base_platform_info.h"
#include "base/platform/linux/base_linux_glibmm_helper.h"
#include "base/platform/linux/base_linux_dbus_utilities.h"
#include "platform/platform_specific.h"
#include "core/application.h"
#include "core/sandbox.h"
#include "core/core_settings.h"
#include "data/data_forum_topic.h"
#include "history/history.h"
@ -27,6 +30,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include <glibmm.h>
#include <giomm.h>
#include <dlfcn.h>
namespace Platform {
namespace Notifications {
namespace {
@ -341,6 +346,9 @@ private:
const not_null<Manager*> _manager;
NotificationId _id;
Glib::RefPtr<Gio::Application> _application;
Glib::RefPtr<Gio::Notification> _notification;
Glib::RefPtr<Gio::DBus::Connection> _dbusConnection;
Glib::ustring _title;
Glib::ustring _body;
@ -367,7 +375,8 @@ NotificationData::NotificationData(
not_null<Manager*> manager,
NotificationId id)
: _manager(manager)
, _id(id) {
, _id(id)
, _application(Gio::Application::get_default()) {
}
bool NotificationData::init(
@ -375,6 +384,64 @@ bool NotificationData::init(
const QString &subtitle,
const QString &msg,
Window::Notifications::Manager::DisplayOptions options) {
if (_application) {
_notification = Gio::Notification::create(title.toStdString());
_notification->set_body(
subtitle.isEmpty()
? msg.toStdString()
: qsl("%1\n%2").arg(subtitle, msg).toStdString());
_notification->set_icon(
Gio::ThemedIcon::create(base::IconName().toStdString()));
// glib 2.42+, we keep glib 2.40+ compatibility
static const auto set_priority = [] {
// reset dlerror after dlsym call
const auto guard = gsl::finally([] { dlerror(); });
return reinterpret_cast<decltype(&g_notification_set_priority)>(
dlsym(RTLD_DEFAULT, "g_notification_set_priority"));
}();
if (set_priority) {
// for chat messages, according to
// https://docs.gtk.org/gio/enum.NotificationPriority.html
set_priority(_notification->gobj(), G_NOTIFICATION_PRIORITY_HIGH);
}
// glib 2.70+, we keep glib 2.40+ compatibility
static const auto set_category = [] {
// reset dlerror after dlsym call
const auto guard = gsl::finally([] { dlerror(); });
return reinterpret_cast<decltype(&g_notification_set_category)>(
dlsym(RTLD_DEFAULT, "g_notification_set_category"));
}();
if (set_category) {
set_category(_notification->gobj(), "im.received");
}
const auto idTuple = _id.toTuple();
_notification->set_default_action(
"app.notification-reply",
idTuple);
if (!options.hideMarkAsRead) {
_notification->add_button(
tr::lng_context_mark_read(tr::now).toStdString(),
"app.notification-mark-as-read",
idTuple);
}
_notification->add_button(
tr::lng_notification_reply(tr::now).toStdString(),
"app.notification-reply",
idTuple);
return true;
}
Noexcept([&] {
_dbusConnection = Gio::DBus::Connection::get_sync(
Gio::DBus::BusType::SESSION);
@ -545,6 +612,17 @@ NotificationData::~NotificationData() {
}
void NotificationData::show() {
if (_application && _notification) {
_application->send_notification(
std::to_string(_id.contextId.sessionId)
+ '-'
+ std::to_string(_id.contextId.peerId.value)
+ '-'
+ std::to_string(_id.msgId.bare),
_notification);
return;
}
// a hack for snap's activation restriction
const auto weak = base::make_weak(this);
StartServiceAsync(crl::guard(weak, [=] {
@ -587,6 +665,17 @@ void NotificationData::show() {
}
void NotificationData::close() {
if (_application) {
_application->withdraw_notification(
std::to_string(_id.contextId.sessionId)
+ '-'
+ std::to_string(_id.contextId.peerId.value)
+ '-'
+ std::to_string(_id.msgId.bare));
_manager->clearNotification(_id);
return;
}
_dbusConnection->call(
std::string(kObjectPath),
std::string(kInterface),
@ -602,7 +691,16 @@ void NotificationData::close() {
}
void NotificationData::setImage(const QString &imagePath) {
if (imagePath.isEmpty() || _imageKey.empty()) {
if (imagePath.isEmpty()) {
return;
}
if (_notification) {
_notification->set_icon(Gio::Icon::create(imagePath.toStdString()));
return;
}
if (_imageKey.empty()) {
return;
}
@ -696,13 +794,13 @@ bool SkipFlashBounceForCustom() {
}
bool Supported() {
return ServiceRegistered;
return ServiceRegistered || Gio::Application::get_default();
}
bool Enforced() {
// Wayland doesn't support positioning
// and custom notifications don't work here
return IsWayland();
return IsWayland() || OptionGApplication.value();
}
bool ByDefault() {
@ -728,25 +826,30 @@ bool ByDefault() {
}
void Create(Window::Notifications::System *system) {
static const auto ServiceWatcher = CreateServiceWatcher();
const auto managerSetter = [=] {
using ManagerType = Window::Notifications::ManagerType;
if ((Core::App().settings().nativeNotifications() || Enforced())
&& Supported()) {
if (system->managerType() != ManagerType::Native) {
if (system->manager().type() != ManagerType::Native) {
system->setManager(std::make_unique<Manager>(system));
}
} else if (Enforced()) {
if (system->managerType() != ManagerType::Dummy) {
if (system->manager().type() != ManagerType::Dummy) {
using DummyManager = Window::Notifications::DummyManager;
system->setManager(std::make_unique<DummyManager>(system));
}
} else if (system->managerType() != ManagerType::Default) {
} else if (system->manager().type() != ManagerType::Default) {
system->setManager(nullptr);
}
};
if (Gio::Application::get_default()) {
managerSetter();
return;
}
static const auto ServiceWatcher = CreateServiceWatcher();
const auto counter = std::make_shared<int>(2);
const auto oneReady = [=] {
if (!--*counter) {
@ -1098,7 +1201,7 @@ void Manager::doClearFromSession(not_null<Main::Session*> session) {
}
bool Manager::doSkipAudio() const {
return _private->inhibited();
return _private->inhibited() || Gio::Application::get_default();
}
bool Manager::doSkipToast() const {
@ -1106,7 +1209,7 @@ bool Manager::doSkipToast() const {
}
bool Manager::doSkipFlashBounce() const {
return _private->inhibited();
return _private->inhibited() || Gio::Application::get_default();
}
} // namespace Notifications

View File

@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "platform/linux/specific_linux.h"
#include "base/random.h"
#include "base/options.h"
#include "base/platform/base_platform_info.h"
#include "platform/linux/linux_desktop_environment.h"
#include "platform/linux/linux_wayland_integration.h"
@ -16,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "storage/localstorage.h"
#include "core/sandbox.h"
#include "core/application.h"
#include "core/local_url_handlers.h"
#include "core/core_settings.h"
#include "core/update_checker.h"
#include "window/window_controller.h"
@ -57,6 +59,58 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
using namespace Platform;
using Platform::internal::WaylandIntegration;
#ifndef DESKTOP_APP_DISABLE_DBUS_INTEGRATION
typedef GApplication TDesktopApplication;
typedef GApplicationClass TDesktopApplicationClass;
G_DEFINE_TYPE(
TDesktopApplication,
t_desktop_application,
G_TYPE_APPLICATION)
static void t_desktop_application_class_init(
TDesktopApplicationClass *klass) {
const auto application_class = G_APPLICATION_CLASS(klass);
application_class->before_emit = [](
GApplication *application,
GVariant *platformData) {
if (Platform::IsWayland()) {
static const auto keys = {
"activation-token",
"desktop-startup-id",
};
for (const auto key : keys) {
const char *token = nullptr;
g_variant_lookup(platformData, key, "&s", &token);
if (token) {
qputenv("XDG_ACTIVATION_TOKEN", token);
break;
}
}
}
};
application_class->add_platform_data = [](
GApplication *application,
GVariantBuilder *builder) {
if (Platform::IsWayland()) {
const auto token = qgetenv("XDG_ACTIVATION_TOKEN");
if (!token.isEmpty()) {
g_variant_builder_add(
builder,
"{sv}",
"activation-token",
g_variant_new_string(token.constData()));
}
}
};
}
static void t_desktop_application_init(TDesktopApplication *application) {
}
#endif // !DESKTOP_APP_DISABLE_DBUS_INTEGRATION
namespace Platform {
namespace {
@ -172,6 +226,197 @@ void PortalAutostart(bool start, bool silent) {
}
}
}
void LaunchGApplication() {
const auto connection = [] {
try {
return Gio::DBus::Connection::get_sync(
Gio::DBus::BusType::SESSION);
} catch (...) {
return Glib::RefPtr<Gio::DBus::Connection>();
}
}();
using namespace base::Platform::DBus;
const auto activatableNames = [&] {
try {
if (connection) {
return ListActivatableNames(connection);
}
} catch (...) {
}
return std::vector<Glib::ustring>();
}();
const auto freedesktopNotifications = [&] {
try {
if (connection && NameHasOwner(
connection,
"org.freedesktop.Notifications")) {
return true;
}
} catch (...) {
}
if (ranges::contains(
activatableNames,
"org.freedesktop.Notifications")) {
return true;
}
return false;
};
const auto gtkNotifications = [&] {
try {
if (connection && NameHasOwner(
connection,
"org.gtk.Notifications")) {
return true;
}
} catch (...) {
}
if (ranges::contains(activatableNames, "org.gtk.Notifications")) {
return true;
}
return false;
};
if (OptionGApplication.value()
|| gtkNotifications()
|| (KSandbox::isFlatpak() && !freedesktopNotifications())) {
Glib::signal_idle().connect_once([] {
const auto appId = QGuiApplication::desktopFileName()
.chopped(8)
.toStdString();
const auto app = Glib::wrap(
G_APPLICATION(
g_object_new(
t_desktop_application_get_type(),
"application-id",
Gio::Application::id_is_valid(appId)
? appId.c_str()
: nullptr,
"flags",
G_APPLICATION_HANDLES_OPEN,
nullptr)));
app->signal_startup().connect([=] {
QEventLoop loop;
loop.exec(QEventLoop::ApplicationExec);
app->quit();
}, true);
app->signal_activate().connect([] {
Core::Sandbox::Instance().customEnterFromEventLoop([] {
if (const auto w = App::wnd()) {
w->activate();
}
});
}, true);
app->signal_open().connect([](
const Gio::Application::type_vec_files &files,
const Glib::ustring &hint) {
Core::Sandbox::Instance().customEnterFromEventLoop([&] {
for (const auto file : files) {
if (file->get_uri_scheme() == "file") {
gSendPaths.append(
QString::fromStdString(file->get_path()));
continue;
}
const auto url = QString::fromStdString(
file->get_uri());
if (url.isEmpty()) {
continue;
}
if (url.startsWith(qstr("interpret://"))) {
gSendPaths.append(url);
continue;
}
if (Core::StartUrlRequiresActivate(url)) {
if (const auto w = App::wnd()) {
w->activate();
}
}
cSetStartUrl(url);
Core::App().checkStartUrl();
}
if (!cSendPaths().isEmpty()) {
if (const auto w = App::wnd()) {
w->sendPaths();
}
}
});
}, true);
app->add_action("Quit", [] {
Core::Sandbox::Instance().customEnterFromEventLoop([] {
Core::Quit();
});
});
using Window::Notifications::Manager;
using NotificationId = Manager::NotificationId;
using NotificationIdTuple = std::result_of<
decltype(&NotificationId::toTuple)(NotificationId*)
>::type;
const auto notificationIdVariantType = [] {
try {
return base::Platform::MakeGlibVariant(
NotificationId().toTuple()).get_type();
} catch (...) {
return Glib::VariantType();
}
}();
app->add_action_with_parameter(
"notification-reply",
notificationIdVariantType,
[](const Glib::VariantBase &parameter) {
Core::Sandbox::Instance().customEnterFromEventLoop([&] {
try {
const auto &app = Core::App();
const auto &notifications = app.notifications();
notifications.manager().notificationActivated(
NotificationId::FromTuple(
base::Platform::GlibVariantCast<
NotificationIdTuple
>(parameter)));
} catch (...) {
}
});
});
app->add_action_with_parameter(
"notification-mark-as-read",
notificationIdVariantType,
[](const Glib::VariantBase &parameter) {
Core::Sandbox::Instance().customEnterFromEventLoop([&] {
try {
const auto &app = Core::App();
const auto &notifications = app.notifications();
notifications.manager().notificationReplied(
NotificationId::FromTuple(
base::Platform::GlibVariantCast<
NotificationIdTuple
>(parameter)),
{});
} catch (...) {
}
});
});
app->hold();
app->run(0, nullptr);
});
}
}
#endif // !DESKTOP_APP_DISABLE_DBUS_INTEGRATION
bool GenerateDesktopFile(
@ -587,6 +832,10 @@ namespace ThirdParty {
void start() {
LOG(("Icon theme: %1").arg(QIcon::themeName()));
LOG(("Fallback icon theme: %1").arg(QIcon::fallbackThemeName()));
#ifndef DESKTOP_APP_DISABLE_DBUS_INTEGRATION
LaunchGApplication();
#endif // !DESKTOP_APP_DISABLE_DBUS_INTEGRATION
}
void finish() {

View File

@ -0,0 +1,26 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "platform/platform_specific.h"
#include "base/options.h"
namespace Platform {
const char kOptionGApplication[] = "gapplication";
base::options::toggle OptionGApplication({
.id = kOptionGApplication,
.name = "GApplication",
.description = "Force enable GNOME's GApplication and GNotification."
" This changes notification behavior to be native to GNOME."
" When disabled, autodetect is used.",
.scope = base::options::linux,
.restartRequired = true,
});
} // namespace Platform

View File

@ -7,8 +7,20 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
namespace base::options {
template <typename Type>
class option;
using toggle = option<bool>;
} // namespace base::options
namespace Platform {
extern const char kOptionGApplication[];
extern base::options::toggle OptionGApplication;
void start();
void finish();

View File

@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/chat/chat_style_radius.h"
#include "base/options.h"
#include "core/application.h"
#include "platform/platform_specific.h"
#include "chat_helpers/tabbed_panel.h"
#include "dialogs/dialogs_inner_widget.h"
#include "history/history_widget.h"
@ -143,6 +144,7 @@ void SetupExperimental(
addToggle(Settings::kOptionMonoSettingsIcons);
addToggle(Webview::kOptionWebviewDebugEnabled);
addToggle(kOptionAutoScrollInactiveChat);
addToggle(Platform::kOptionGApplication);
}
} // namespace

View File

@ -125,9 +125,9 @@ void System::setManager(std::unique_ptr<Manager> manager) {
}
}
ManagerType System::managerType() const {
Manager &System::manager() const {
Expects(_manager != nullptr);
return _manager->type();
return *_manager;
}
Main::Session *System::findSession(uint64 sessionId) const {

View File

@ -84,7 +84,7 @@ public:
void createManager();
void setManager(std::unique_ptr<Manager> manager);
[[nodiscard]] ManagerType managerType() const;
[[nodiscard]] Manager &manager() const;
void checkDelayed();
void schedule(Data::ItemNotification notification);
@ -217,6 +217,22 @@ public:
friend inline auto operator<=>(
const ContextId&,
const ContextId&) = default;
[[nodiscard]] auto toTuple() const {
return std::make_tuple(
sessionId,
peerId.value,
topicRootId.bare);
}
template<typename T>
[[nodiscard]] static auto FromTuple(const T &tuple) {
return ContextId{
std::get<0>(tuple),
PeerIdHelper(std::get<1>(tuple)),
std::get<2>(tuple),
};
}
};
struct NotificationId {
ContextId contextId;
@ -225,6 +241,20 @@ public:
friend inline auto operator<=>(
const NotificationId&,
const NotificationId&) = default;
[[nodiscard]] auto toTuple() const {
return std::make_tuple(
contextId.toTuple(),
msgId.bare);
}
template<typename T>
[[nodiscard]] static auto FromTuple(const T &tuple) {
return NotificationId{
ContextId::FromTuple(std::get<0>(tuple)),
std::get<1>(tuple),
};
}
};
struct NotificationFields {
not_null<HistoryItem*> item;

@ -1 +1 @@
Subproject commit 1cc6aba584869df04341467cf7c5f72c8354086f
Subproject commit d3f9e826197c6ed42f8ef2d021d8eb74b2d390cc

@ -1 +1 @@
Subproject commit b9d81771a0d7533dd07805f0618193277715da80
Subproject commit 9623bb460b9be6b0481d775f9e01e5084170024e