init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Close stale issues and PRs / stale (push) Successful in 13s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s

This commit is contained in:
allhaileris
2026-02-16 15:50:16 +03:00
commit afb81b8278
13816 changed files with 3689732 additions and 0 deletions

View File

@@ -0,0 +1,105 @@
/*
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/linux/current_geo_location_linux.h"
#include "core/current_geo_location.h"
#include "base/platform/linux/base_linux_library.h"
#include <gio/gio.h>
namespace Platform {
namespace {
typedef struct _GClueSimple GClueSimple;
typedef struct _GClueLocation GClueLocation;
typedef enum {
GCLUE_ACCURACY_LEVEL_NONE = 0,
GCLUE_ACCURACY_LEVEL_COUNTRY = 1,
GCLUE_ACCURACY_LEVEL_CITY = 4,
GCLUE_ACCURACY_LEVEL_NEIGHBORHOOD = 5,
GCLUE_ACCURACY_LEVEL_STREET = 6,
GCLUE_ACCURACY_LEVEL_EXACT = 8,
} GClueAccuracyLevel;
void (*gclue_simple_new)(
const char *desktop_id,
GClueAccuracyLevel accuracy_level,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data);
GClueSimple *(*gclue_simple_new_finish)(GAsyncResult *result, GError **error);
GClueLocation *(*gclue_simple_get_location)(GClueSimple *simple);
gdouble (*gclue_location_get_latitude)(GClueLocation *loc);
gdouble (*gclue_location_get_longitude)(GClueLocation *loc);
} // namespace
void ResolveCurrentExactLocation(Fn<void(Core::GeoLocation)> callback) {
static const auto Inited = [] {
const auto lib = base::Platform::LoadLibrary(
"libgeoclue-2.so.0",
RTLD_NODELETE);
return lib
&& LOAD_LIBRARY_SYMBOL(lib, gclue_simple_new)
&& LOAD_LIBRARY_SYMBOL(lib, gclue_simple_new_finish)
&& LOAD_LIBRARY_SYMBOL(lib, gclue_simple_get_location)
&& LOAD_LIBRARY_SYMBOL(lib, gclue_location_get_latitude)
&& LOAD_LIBRARY_SYMBOL(lib, gclue_location_get_longitude);
}();
if (!Inited) {
callback({});
return;
}
gclue_simple_new(
QGuiApplication::desktopFileName().toUtf8().constData(),
GCLUE_ACCURACY_LEVEL_EXACT,
nullptr,
GAsyncReadyCallback(+[](
GObject *object,
GAsyncResult* res,
Fn<void(Core::GeoLocation)> *callback) {
const auto callbackGuard = gsl::finally([&] {
delete callback;
});
const auto simple = gclue_simple_new_finish(res, nullptr);
if (!simple) {
(*callback)({});
return;
}
const auto simpleGuard = gsl::finally([&] {
g_object_unref(simple);
});
const auto location = gclue_simple_get_location(simple);
(*callback)({
.point = {
gclue_location_get_latitude(location),
gclue_location_get_longitude(location),
},
.accuracy = Core::GeoLocationAccuracy::Exact,
});
}),
new Fn(callback));
}
void ResolveLocationAddress(
const Core::GeoLocation &location,
const QString &language,
Fn<void(Core::GeoAddress)> callback) {
callback({});
}
} // namespace Platform

View File

@@ -0,0 +1,10 @@
/*
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
*/
#pragma once
#include "platform/platform_current_geo_location.h"

View File

@@ -0,0 +1,126 @@
/*
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/linux/file_utilities_linux.h"
#include "base/platform/base_platform_info.h"
#include "base/platform/linux/base_linux_xdp_utilities.h"
#include "base/platform/linux/base_linux_xdg_activation_token.h"
#include "base/random.h"
#include <fcntl.h>
#include <xdpopenuri/xdpopenuri.hpp>
#include <xdprequest/xdprequest.hpp>
namespace Platform {
namespace File {
namespace {
using namespace gi::repository;
using base::Platform::XdgActivationToken;
} // namespace
bool UnsafeShowOpenWith(const QString &filepath) {
auto proxy = XdpOpenURI::OpenURIProxy::new_for_bus_sync(
Gio::BusType::SESSION_,
Gio::DBusProxyFlags::NONE_,
base::Platform::XDP::kService,
base::Platform::XDP::kObjectPath,
nullptr);
if (!proxy) {
return false;
}
auto interface = XdpOpenURI::OpenURI(proxy);
if (interface.get_version() < 3) {
return false;
}
const auto fd = open(
QFile::encodeName(filepath).constData(),
O_RDONLY | O_CLOEXEC);
if (fd == -1) {
return false;
}
const auto handleToken = "tdesktop"
+ std::to_string(base::RandomValue<uint>());
std::string uniqueName = proxy.get_connection().get_unique_name();
uniqueName.erase(0, 1);
uniqueName.replace(uniqueName.find('.'), 1, 1, '_');
auto request = XdpRequest::Request(
XdpRequest::RequestProxy::new_sync(
proxy.get_connection(),
Gio::DBusProxyFlags::NONE_,
base::Platform::XDP::kService,
base::Platform::XDP::kObjectPath
+ std::string("/request/")
+ uniqueName
+ '/'
+ handleToken,
nullptr,
nullptr));
if (!request) {
close(fd);
return false;
}
auto loop = GLib::MainLoop::new_();
const auto signalId = request.signal_response().connect([=](
XdpRequest::Request,
guint,
GLib::Variant) mutable {
loop.quit();
});
const auto signalGuard = gsl::finally([&] {
request.disconnect(signalId);
});
auto result = interface.call_open_file_sync(
base::Platform::XDP::ParentWindowID(),
GLib::Variant::new_handle(0),
GLib::Variant::new_array({
GLib::Variant::new_dict_entry(
GLib::Variant::new_string("handle_token"),
GLib::Variant::new_variant(
GLib::Variant::new_string(handleToken))),
GLib::Variant::new_dict_entry(
GLib::Variant::new_string("activation_token"),
GLib::Variant::new_variant(
GLib::Variant::new_string(
XdgActivationToken().toStdString()))),
GLib::Variant::new_dict_entry(
GLib::Variant::new_string("ask"),
GLib::Variant::new_variant(
GLib::Variant::new_boolean(true))),
}),
Gio::UnixFDList::new_from_array(&fd, 1),
nullptr);
if (!result) {
return false;
}
QWidget window;
window.setAttribute(Qt::WA_DontShowOnScreen);
window.setWindowModality(Qt::ApplicationModal);
window.show();
loop.run();
return true;
}
} // namespace File
} // namespace Platform

View File

@@ -0,0 +1,65 @@
/*
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
*/
#pragma once
#include "platform/platform_file_utilities.h"
namespace Platform {
namespace File {
inline QString UrlToLocal(const QUrl &url) {
return ::File::internal::UrlToLocalDefault(url);
}
inline void UnsafeOpenUrl(const QString &url) {
return ::File::internal::UnsafeOpenUrlDefault(url);
}
inline void UnsafeOpenEmailLink(const QString &email) {
return ::File::internal::UnsafeOpenEmailLinkDefault(email);
}
inline bool UnsafeShowOpenWithDropdown(const QString &filepath) {
return false;
}
inline void UnsafeLaunch(const QString &filepath) {
return ::File::internal::UnsafeLaunchDefault(filepath);
}
inline void PostprocessDownloaded(const QString &filepath) {
}
} // namespace File
namespace FileDialog {
inline void InitLastPath() {
::FileDialog::internal::InitLastPathDefault();
}
inline bool Get(
QPointer<QWidget> parent,
QStringList &files,
QByteArray &remoteContent,
const QString &caption,
const QString &filter,
::FileDialog::internal::Type type,
QString startFile) {
return ::FileDialog::internal::GetDefault(
parent,
files,
remoteContent,
caption,
filter,
type,
startFile);
}
} // namespace FileDialog
} // namespace Platform

View File

@@ -0,0 +1,268 @@
/*
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/linux/integration_linux.h"
#include "platform/platform_integration.h"
#include "base/platform/base_platform_info.h"
#include "base/platform/linux/base_linux_xdp_utilities.h"
#include "core/sandbox.h"
#include "core/application.h"
#if QT_VERSION < QT_VERSION_CHECK(6, 5, 0)
#include "core/core_settings.h"
#endif
#include "base/random.h"
#include "base/qt_connection.h"
#include <QtCore/QAbstractEventDispatcher>
#include <gio/gio.hpp>
#include <xdpinhibit/xdpinhibit.hpp>
#ifdef __GLIBC__
#include <malloc.h>
#endif // __GLIBC__
namespace Platform {
namespace {
using namespace gi::repository;
namespace GObject = gi::repository::GObject;
class Application : public Gio::impl::ApplicationImpl {
public:
Application();
void before_emit_(GLib::Variant platformData) noexcept override {
if (Platform::IsWayland()) {
static const auto keys = {
"activation-token",
"desktop-startup-id",
};
for (const auto &key : keys) {
if (auto token = platformData.lookup_value(key)) {
qputenv(
"XDG_ACTIVATION_TOKEN",
token.get_string(nullptr).c_str());
break;
}
}
}
}
void activate_() noexcept override {
Core::Sandbox::Instance().customEnterFromEventLoop([] {
Core::App().activate();
});
}
void open_(
gi::Collection<gi::DSpan, ::GFile*, gi::transfer_none_t> files,
const gi::cstring_v hint) noexcept override {
for (auto file : files) {
QFileOpenEvent e(QUrl(QString::fromStdString(file.get_uri())));
QGuiApplication::sendEvent(qApp, &e);
}
}
void add_platform_data_(
GLib::VariantBuilder_Ref builder) noexcept override {
if (Platform::IsWayland()) {
const auto token = qgetenv("XDG_ACTIVATION_TOKEN");
if (!token.isEmpty()) {
builder.add_value(
GLib::Variant::new_dict_entry(
GLib::Variant::new_string("activation-token"),
GLib::Variant::new_variant(
GLib::Variant::new_string(token.toStdString()))));
qunsetenv("XDG_ACTIVATION_TOKEN");
}
}
}
};
Application::Application()
: Gio::impl::ApplicationImpl(this) {
const auto appId = QGuiApplication::desktopFileName().toStdString();
if (Gio::Application::id_is_valid(appId)) {
set_application_id(appId);
}
set_flags(Gio::ApplicationFlags::HANDLES_OPEN_);
auto actionMap = Gio::ActionMap(*this);
auto quitAction = Gio::SimpleAction::new_("quit");
quitAction.signal_activate().connect([](
Gio::SimpleAction,
GLib::Variant parameter) {
Core::Sandbox::Instance().customEnterFromEventLoop([] {
Core::Quit();
});
});
actionMap.add_action(quitAction);
const auto notificationIdVariantType = GLib::VariantType::new_("a{sv}");
auto notificationActivateAction = Gio::SimpleAction::new_(
"notification-activate",
notificationIdVariantType);
actionMap.add_action(notificationActivateAction);
auto notificationMarkAsReadAction = Gio::SimpleAction::new_(
"notification-mark-as-read",
notificationIdVariantType);
actionMap.add_action(notificationMarkAsReadAction);
}
gi::ref_ptr<Application> MakeApplication() {
const auto result = gi::make_ref<Application>();
if (const auto registered = result->register_(); !registered) {
LOG(("App Error: Failed to register: %1").arg(
registered.error().message_().c_str()));
return nullptr;
}
return result;
}
class LinuxIntegration final : public Integration, public base::has_weak_ptr {
public:
LinuxIntegration();
void init() override;
private:
[[nodiscard]] XdpInhibit::Inhibit inhibit() {
return _inhibitProxy;
}
void initInhibit();
const gi::ref_ptr<Application> _application;
XdpInhibit::InhibitProxy _inhibitProxy;
#if QT_VERSION < QT_VERSION_CHECK(6, 5, 0)
base::Platform::XDP::SettingWatcher _darkModeWatcher;
#endif // Qt < 6.5.0
#ifdef __GLIBC__
base::qt_connection _memoryTrim;
crl::time _memoryTrimmed = 0;
#endif // __GLIBC__
};
LinuxIntegration::LinuxIntegration()
: _application(MakeApplication())
#if QT_VERSION < QT_VERSION_CHECK(6, 5, 0)
, _darkModeWatcher(
"org.freedesktop.appearance",
"color-scheme",
[](GLib::Variant value) {
Core::Sandbox::Instance().customEnterFromEventLoop([&] {
Core::App().settings().setSystemDarkMode(value.get_uint32() == 1);
});
})
#endif // Qt < 6.5.0
{
LOG(("Icon theme: %1").arg(QIcon::themeName()));
LOG(("Fallback icon theme: %1").arg(QIcon::fallbackThemeName()));
if (!QCoreApplication::eventDispatcher()->inherits(
"QEventDispatcherGlib")) {
g_warning("Qt is running without GLib event loop integration, "
"expect various functionality to not to work.");
}
#ifdef __GLIBC__
_memoryTrim = QObject::connect(
QCoreApplication::eventDispatcher(),
&QAbstractEventDispatcher::aboutToBlock,
[=] {
if (crl::now() - _memoryTrimmed >= 10000) {
malloc_trim(0);
_memoryTrimmed = crl::now();
}
});
#endif // __GLIBC__
}
void LinuxIntegration::init() {
XdpInhibit::InhibitProxy::new_for_bus(
Gio::BusType::SESSION_,
Gio::DBusProxyFlags::NONE_,
base::Platform::XDP::kService,
base::Platform::XDP::kObjectPath,
crl::guard(this, [=](GObject::Object, Gio::AsyncResult res) {
_inhibitProxy = XdpInhibit::InhibitProxy::new_for_bus_finish(
res,
nullptr);
initInhibit();
}));
}
void LinuxIntegration::initInhibit() {
if (!_inhibitProxy) {
return;
}
std::string uniqueName = _inhibitProxy.get_connection().get_unique_name();
uniqueName.erase(0, 1);
uniqueName.replace(uniqueName.find('.'), 1, 1, '_');
const auto handleToken = "tdesktop"
+ std::to_string(base::RandomValue<uint>());
const auto sessionHandleToken = "tdesktop"
+ std::to_string(base::RandomValue<uint>());
const auto sessionHandle = base::Platform::XDP::kObjectPath
+ std::string("/session/")
+ uniqueName
+ '/'
+ sessionHandleToken;
inhibit().signal_state_changed().connect([
mySessionHandle = sessionHandle
](
XdpInhibit::Inhibit,
const std::string &sessionHandle,
GLib::Variant state) {
if (sessionHandle != mySessionHandle) {
return;
}
Core::App().setScreenIsLocked(
GLib::VariantDict::new_(
state
).lookup_value(
"screensaver-active"
).get_boolean()
);
});
inhibit().call_create_monitor(
"",
GLib::Variant::new_array({
GLib::Variant::new_dict_entry(
GLib::Variant::new_string("handle_token"),
GLib::Variant::new_variant(
GLib::Variant::new_string(handleToken))),
GLib::Variant::new_dict_entry(
GLib::Variant::new_string("session_handle_token"),
GLib::Variant::new_variant(
GLib::Variant::new_string(sessionHandleToken))),
}),
nullptr);
}
} // namespace
std::unique_ptr<Integration> CreateIntegration() {
return std::make_unique<LinuxIntegration>();
}
} // namespace Platform

View File

@@ -0,0 +1,16 @@
/*
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
*/
#pragma once
namespace Platform {
class Integration;
[[nodiscard]] std::unique_ptr<Integration> CreateIntegration();
} // namespace Platform

View File

@@ -0,0 +1,147 @@
/*
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/linux/launcher_linux.h"
#include "core/crash_reports.h"
#include "core/update_checker.h"
#include "webview/platform/linux/webview_linux_webkitgtk.h"
#include <QtWidgets/QApplication>
#include <glib/glib.hpp>
#ifdef __GLIBC__
#include <malloc.h>
#endif // __GLIBC__
using namespace gi::repository;
namespace Platform {
Launcher::Launcher(int argc, char *argv[])
: Core::Launcher(argc, argv) {
#ifdef __GLIBC__
mallopt(M_ARENA_MAX, 1);
#endif // __GLIBC__
}
int Launcher::exec() {
for (auto i = arguments().begin(), e = arguments().end(); i != e; ++i) {
if (*i == u"-webviewhelper"_q && std::distance(i, e) > 1) {
Webview::WebKitGTK::SetSocketPath((i + 1)->toStdString());
return Webview::WebKitGTK::Exec();
}
}
return Core::Launcher::exec();
}
bool Launcher::launchUpdater(UpdaterLaunch action) {
if (cExeName().isEmpty()) {
return false;
}
const auto justRelaunch = action == UpdaterLaunch::JustRelaunch;
if (action == UpdaterLaunch::PerformUpdate) {
_updating = true;
}
std::vector<std::string> argumentsList;
// What we are launching.
const auto launching = justRelaunch
? (cExeDir() + cExeName())
: cWriteProtected()
? GLib::find_program_in_path("run0")
? u"run0"_q
: u"pkexec"_q
: (cExeDir() + u"Updater"_q);
argumentsList.push_back(launching.toStdString());
if (justRelaunch) {
// argv[0] that is passed to what we are launching.
// It should be added explicitly in case of FILE_AND_ARGV_ZERO_.
const auto argv0 = !arguments().isEmpty()
? arguments().first()
: launching;
argumentsList.push_back(argv0.toStdString());
} else if (cWriteProtected()) {
// Elevated process that run0/pkexec should launch.
const auto elevated = cWorkingDir() + u"tupdates/temp/Updater"_q;
argumentsList.push_back(elevated.toStdString());
}
if (Logs::DebugEnabled()) {
argumentsList.push_back("-debug");
}
if (justRelaunch) {
if (cLaunchMode() == LaunchModeAutoStart) {
argumentsList.push_back("-autostart");
}
if (cStartInTray()) {
argumentsList.push_back("-startintray");
}
if (cDataFile() != u"data"_q) {
argumentsList.push_back("-key");
argumentsList.push_back(cDataFile().toStdString());
}
if (!_updating) {
argumentsList.push_back("-noupdate");
argumentsList.push_back("-tosettings");
}
if (customWorkingDir()) {
argumentsList.push_back("-workdir");
argumentsList.push_back(cWorkingDir().toStdString());
}
} else {
// Don't relaunch Telegram.
argumentsList.push_back("-justupdate");
argumentsList.push_back("-workpath");
argumentsList.push_back(cWorkingDir().toStdString());
argumentsList.push_back("-exename");
argumentsList.push_back(cExeName().toStdString());
argumentsList.push_back("-exepath");
argumentsList.push_back(cExeDir().toStdString());
if (cWriteProtected()) {
argumentsList.push_back("-writeprotected");
}
}
Logs::closeMain();
CrashReports::Finish();
int waitStatus = 0;
if (justRelaunch) {
return GLib::spawn_async(
initialWorkingDir().toStdString(),
argumentsList,
{},
GLib::SpawnFlags::FILE_AND_ARGV_ZERO_,
nullptr,
nullptr,
nullptr);
} else if (!GLib::spawn_sync(
argumentsList,
{},
// if the spawn is sync, working directory is not set
// and GLib::SpawnFlags::LEAVE_DESCRIPTORS_OPEN_ is set,
// it goes through an optimized code path
GLib::SpawnFlags::SEARCH_PATH_
| GLib::SpawnFlags::LEAVE_DESCRIPTORS_OPEN_,
nullptr,
nullptr,
nullptr,
&waitStatus,
nullptr) || !g_spawn_check_exit_status(waitStatus, nullptr)) {
return false;
}
return launchUpdater(UpdaterLaunch::JustRelaunch);
}
} // namespace

View File

@@ -0,0 +1,27 @@
/*
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
*/
#pragma once
#include "core/launcher.h"
namespace Platform {
class Launcher : public Core::Launcher {
public:
Launcher(int argc, char *argv[]);
int exec() override;
private:
bool launchUpdater(UpdaterLaunch action) override;
bool _updating = false;
};
} // namespace Platform

View File

@@ -0,0 +1,574 @@
/*
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/linux/main_window_linux.h"
#include "styles/style_window.h"
#include "platform/linux/specific_linux.h"
#include "history/history.h"
#include "history/history_widget.h"
#include "history/history_inner_widget.h"
#include "main/main_account.h" // Account::sessionChanges.
#include "main/main_session.h"
#include "mainwindow.h"
#include "core/application.h"
#include "core/core_settings.h"
#include "core/sandbox.h"
#include "boxes/peer_list_controllers.h"
#include "boxes/about_box.h"
#include "lang/lang_keys.h"
#include "storage/localstorage.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
#include "base/platform/base_platform_info.h"
#include "base/event_filter.h"
#include "ui/platform/ui_platform_window_title.h"
#include "ui/widgets/popup_menu.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/ui_utility.h"
#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
#include "base/platform/linux/base_linux_xcb_utilities.h"
#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION
#include <QtCore/QSize>
#include <QtCore/QMimeData>
#include <QtGui/QWindow>
#include <QtWidgets/QMenuBar>
#include <QtWidgets/QLineEdit>
#include <QtWidgets/QTextEdit>
#include <gio/gio.hpp>
namespace Platform {
namespace {
using WorkMode = Core::Settings::WorkMode;
#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
void XCBSkipTaskbar(QWindow *window, bool skip) {
const base::Platform::XCB::Connection connection;
if (!connection || xcb_connection_has_error(connection)) {
return;
}
const auto root = base::Platform::XCB::GetRootWindow(connection);
if (!root) {
return;
}
const auto stateAtom = base::Platform::XCB::GetAtom(
connection,
"_NET_WM_STATE");
if (!stateAtom) {
return;
}
const auto skipTaskbarAtom = base::Platform::XCB::GetAtom(
connection,
"_NET_WM_STATE_SKIP_TASKBAR");
if (!skipTaskbarAtom) {
return;
}
xcb_client_message_event_t xev;
xev.response_type = XCB_CLIENT_MESSAGE;
xev.type = stateAtom;
xev.sequence = 0;
xev.window = window->winId();
xev.format = 32;
xev.data.data32[0] = skip ? 1 : 0;
xev.data.data32[1] = skipTaskbarAtom;
xev.data.data32[2] = 0;
xev.data.data32[3] = 0;
xev.data.data32[4] = 0;
free(
xcb_request_check(
connection,
xcb_send_event_checked(
connection,
false,
root,
XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT
| XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY,
reinterpret_cast<const char*>(&xev))));
}
#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION
void SkipTaskbar(QWindow *window, bool skip) {
#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
if (IsX11()) {
XCBSkipTaskbar(window, skip);
return;
}
#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION
}
void SendKeySequence(
Qt::Key key,
Qt::KeyboardModifiers modifiers = Qt::NoModifier) {
const auto focused = QApplication::focusWidget();
if (qobject_cast<QLineEdit*>(focused)
|| qobject_cast<QTextEdit*>(focused)
|| dynamic_cast<HistoryInner*>(focused)) {
QApplication::postEvent(
focused,
new QKeyEvent(QEvent::KeyPress, key, modifiers));
QApplication::postEvent(
focused,
new QKeyEvent(QEvent::KeyRelease, key, modifiers));
}
}
void ForceDisabled(QAction *action, bool disabled) {
if (action->isEnabled()) {
if (disabled) action->setDisabled(true);
} else if (!disabled) {
action->setDisabled(false);
}
}
} // namespace
MainWindow::MainWindow(not_null<Window::Controller*> controller)
: Window::MainWindow(controller) {
}
void MainWindow::workmodeUpdated(Core::Settings::WorkMode mode) {
if (!TrayIconSupported()) {
return;
}
SkipTaskbar(windowHandle(), mode == WorkMode::TrayOnly);
}
void MainWindow::unreadCounterChangedHook() {
updateUnityCounter();
}
void MainWindow::updateWindowIcon() {
const auto session = sessionController()
? &sessionController()->session()
: nullptr;
setWindowIcon(Window::CreateIcon(session));
}
void MainWindow::updateUnityCounter() {
#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
qApp->setBadgeNumber(Core::App().unreadBadge());
#else // Qt >= 6.6.0
using namespace gi::repository;
static const auto djbStringHash = [](const std::string &string) {
uint hash = 5381;
for (const auto &curChar : string) {
hash = (hash << 5) + hash + curChar;
}
return hash;
};
const auto launcherUrl = "application://"
+ QGuiApplication::desktopFileName().toStdString()
+ ".desktop";
const auto counterSlice = std::min(Core::App().unreadBadge(), 9999);
auto connection = Gio::bus_get_sync(Gio::BusType::SESSION_, nullptr);
if (!connection) {
return;
}
connection.emit_signal(
{},
"/com/canonical/unity/launcherentry/"
+ std::to_string(djbStringHash(launcherUrl)),
"com.canonical.Unity.LauncherEntry",
"Update",
GLib::Variant::new_tuple({
GLib::Variant::new_string(launcherUrl),
GLib::Variant::new_array({
GLib::Variant::new_dict_entry(
GLib::Variant::new_string("count"),
GLib::Variant::new_variant(
GLib::Variant::new_int64(counterSlice))),
GLib::Variant::new_dict_entry(
GLib::Variant::new_string("count-visible"),
GLib::Variant::new_variant(
GLib::Variant::new_boolean(counterSlice))),
}),
}));
#endif // Qt < 6.6.0
}
void MainWindow::createGlobalMenu() {
const auto ensureWindowShown = [=] {
if (isHidden()) {
showFromTray();
}
};
psMainMenu = new QMenuBar(this);
psMainMenu->hide();
auto file = psMainMenu->addMenu(tr::lng_mac_menu_file(tr::now));
psLogout = file->addAction(
tr::lng_mac_menu_logout(tr::now),
this,
[=] {
ensureWindowShown();
controller().showLogoutConfirmation();
});
auto quit = file->addAction(
tr::lng_mac_menu_quit_telegram(tr::now, lt_telegram, u"Telegram"_q),
this,
[=] { quitFromTray(); },
QKeySequence::Quit);
quit->setMenuRole(QAction::QuitRole);
quit->setShortcutContext(Qt::WidgetShortcut);
auto edit = psMainMenu->addMenu(tr::lng_mac_menu_edit(tr::now));
psUndo = edit->addAction(
tr::lng_linux_menu_undo(tr::now),
[] { SendKeySequence(Qt::Key_Z, Qt::ControlModifier); },
QKeySequence::Undo);
psUndo->setShortcutContext(Qt::WidgetShortcut);
psRedo = edit->addAction(
tr::lng_linux_menu_redo(tr::now),
[] {
SendKeySequence(
Qt::Key_Z,
Qt::ControlModifier | Qt::ShiftModifier);
},
QKeySequence::Redo);
psRedo->setShortcutContext(Qt::WidgetShortcut);
edit->addSeparator();
psCut = edit->addAction(
tr::lng_mac_menu_cut(tr::now),
[] { SendKeySequence(Qt::Key_X, Qt::ControlModifier); },
QKeySequence::Cut);
psCut->setShortcutContext(Qt::WidgetShortcut);
psCopy = edit->addAction(
tr::lng_mac_menu_copy(tr::now),
[] { SendKeySequence(Qt::Key_C, Qt::ControlModifier); },
QKeySequence::Copy);
psCopy->setShortcutContext(Qt::WidgetShortcut);
psPaste = edit->addAction(
tr::lng_mac_menu_paste(tr::now),
[] { SendKeySequence(Qt::Key_V, Qt::ControlModifier); },
QKeySequence::Paste);
psPaste->setShortcutContext(Qt::WidgetShortcut);
psDelete = edit->addAction(
tr::lng_mac_menu_delete(tr::now),
[] { SendKeySequence(Qt::Key_Delete); },
QKeySequence(Qt::ControlModifier | Qt::Key_Backspace));
psDelete->setShortcutContext(Qt::WidgetShortcut);
edit->addSeparator();
psBold = edit->addAction(
tr::lng_menu_formatting_bold(tr::now),
[] { SendKeySequence(Qt::Key_B, Qt::ControlModifier); },
QKeySequence::Bold);
psBold->setShortcutContext(Qt::WidgetShortcut);
psItalic = edit->addAction(
tr::lng_menu_formatting_italic(tr::now),
[] { SendKeySequence(Qt::Key_I, Qt::ControlModifier); },
QKeySequence::Italic);
psItalic->setShortcutContext(Qt::WidgetShortcut);
psUnderline = edit->addAction(
tr::lng_menu_formatting_underline(tr::now),
[] { SendKeySequence(Qt::Key_U, Qt::ControlModifier); },
QKeySequence::Underline);
psUnderline->setShortcutContext(Qt::WidgetShortcut);
psStrikeOut = edit->addAction(
tr::lng_menu_formatting_strike_out(tr::now),
[] {
SendKeySequence(
Qt::Key_X,
Qt::ControlModifier | Qt::ShiftModifier);
},
Ui::kStrikeOutSequence);
psStrikeOut->setShortcutContext(Qt::WidgetShortcut);
psBlockquote = edit->addAction(
tr::lng_menu_formatting_blockquote(tr::now),
[] {
SendKeySequence(
Qt::Key_Period,
Qt::ControlModifier | Qt::ShiftModifier);
},
Ui::kBlockquoteSequence);
psBlockquote->setShortcutContext(Qt::WidgetShortcut);
psMonospace = edit->addAction(
tr::lng_menu_formatting_monospace(tr::now),
[] {
SendKeySequence(
Qt::Key_M,
Qt::ControlModifier | Qt::ShiftModifier);
},
Ui::kMonospaceSequence);
psMonospace->setShortcutContext(Qt::WidgetShortcut);
psClearFormat = edit->addAction(
tr::lng_menu_formatting_clear(tr::now),
[] {
SendKeySequence(
Qt::Key_N,
Qt::ControlModifier | Qt::ShiftModifier);
},
Ui::kClearFormatSequence);
psClearFormat->setShortcutContext(Qt::WidgetShortcut);
edit->addSeparator();
psSelectAll = edit->addAction(
tr::lng_mac_menu_select_all(tr::now),
[] { SendKeySequence(Qt::Key_A, Qt::ControlModifier); },
QKeySequence::SelectAll);
psSelectAll->setShortcutContext(Qt::WidgetShortcut);
edit->addSeparator();
auto prefs = edit->addAction(
tr::lng_mac_menu_preferences(tr::now),
this,
[=] {
ensureWindowShown();
controller().showSettings();
},
QKeySequence(Qt::ControlModifier | Qt::Key_Comma));
prefs->setMenuRole(QAction::PreferencesRole);
prefs->setShortcutContext(Qt::WidgetShortcut);
auto tools = psMainMenu->addMenu(tr::lng_linux_menu_tools(tr::now));
psContacts = tools->addAction(
tr::lng_mac_menu_contacts(tr::now),
crl::guard(this, [=] {
if (isHidden()) {
showFromTray();
}
if (!sessionController()) {
return;
}
sessionController()->show(
PrepareContactsBox(sessionController()));
}));
psAddContact = tools->addAction(
tr::lng_mac_menu_add_contact(tr::now),
this,
[=] {
Expects(sessionController() != nullptr);
ensureWindowShown();
sessionController()->showAddContact();
});
tools->addSeparator();
psNewGroup = tools->addAction(
tr::lng_mac_menu_new_group(tr::now),
this,
[=] {
Expects(sessionController() != nullptr);
ensureWindowShown();
sessionController()->showNewGroup();
});
psNewChannel = tools->addAction(
tr::lng_mac_menu_new_channel(tr::now),
this,
[=] {
Expects(sessionController() != nullptr);
ensureWindowShown();
sessionController()->showNewChannel();
});
auto help = psMainMenu->addMenu(tr::lng_linux_menu_help(tr::now));
auto about = help->addAction(
tr::lng_mac_menu_about_telegram(
tr::now,
lt_telegram,
u"Telegram"_q),
[=] {
ensureWindowShown();
controller().show(Box(AboutBox));
});
about->setMenuRole(QAction::AboutQtRole);
updateGlobalMenu();
}
void MainWindow::updateGlobalMenuHook() {
if (!positionInited()) {
return;
}
const auto focused = QApplication::focusWidget();
auto canUndo = false;
auto canRedo = false;
auto canCut = false;
auto canCopy = false;
auto canPaste = false;
auto canDelete = false;
auto canSelectAll = false;
const auto mimeData = QGuiApplication::clipboard()->mimeData();
const auto clipboardHasText = mimeData ? mimeData->hasText() : false;
auto markdownState = Ui::MarkdownEnabledState();
if (const auto edit = qobject_cast<QLineEdit*>(focused)) {
canCut = canCopy = canDelete = edit->hasSelectedText();
canSelectAll = !edit->text().isEmpty();
canUndo = edit->isUndoAvailable();
canRedo = edit->isRedoAvailable();
canPaste = clipboardHasText;
} else if (const auto edit = qobject_cast<QTextEdit*>(focused)) {
canCut = canCopy = canDelete = edit->textCursor().hasSelection();
canSelectAll = !edit->document()->isEmpty();
canUndo = edit->document()->isUndoAvailable();
canRedo = edit->document()->isRedoAvailable();
canPaste = clipboardHasText;
if (canCopy) {
if (const auto inputField = dynamic_cast<Ui::InputField*>(
focused->parentWidget())) {
markdownState = inputField->markdownEnabledState();
}
}
} else if (const auto list = dynamic_cast<HistoryInner*>(focused)) {
canCopy = list->canCopySelected();
canDelete = list->canDeleteSelected();
}
updateIsActive();
const auto logged = (sessionController() != nullptr);
const auto inactive = !logged || controller().locked();
const auto support = logged
&& sessionController()->session().supportMode();
ForceDisabled(psLogout, !logged && !Core::App().passcodeLocked());
ForceDisabled(psUndo, !canUndo);
ForceDisabled(psRedo, !canRedo);
ForceDisabled(psCut, !canCut);
ForceDisabled(psCopy, !canCopy);
ForceDisabled(psPaste, !canPaste);
ForceDisabled(psDelete, !canDelete);
ForceDisabled(psSelectAll, !canSelectAll);
ForceDisabled(psContacts, inactive || support);
ForceDisabled(psAddContact, inactive);
ForceDisabled(psNewGroup, inactive || support);
ForceDisabled(psNewChannel, inactive || support);
const auto diabled = [=](const QString &tag) {
return !markdownState.enabledForTag(tag);
};
using Field = Ui::InputField;
ForceDisabled(psBold, diabled(Field::kTagBold));
ForceDisabled(psItalic, diabled(Field::kTagItalic));
ForceDisabled(psUnderline, diabled(Field::kTagUnderline));
ForceDisabled(psStrikeOut, diabled(Field::kTagStrikeOut));
ForceDisabled(psBlockquote, diabled(Field::kTagBlockquote));
ForceDisabled(
psMonospace,
diabled(Field::kTagPre) || diabled(Field::kTagCode));
ForceDisabled(psClearFormat, markdownState.disabled());
}
bool MainWindow::eventFilter(QObject *obj, QEvent *evt) {
const auto t = evt->type();
if (t == QEvent::FocusIn || t == QEvent::FocusOut) {
if (qobject_cast<QLineEdit*>(obj)
|| qobject_cast<QTextEdit*>(obj)
|| dynamic_cast<HistoryInner*>(obj)) {
if (QApplication::focusWidget()) {
updateGlobalMenu();
}
}
} else if (obj == this && t == QEvent::Paint) {
if (!_exposed) {
_exposed = true;
SkipTaskbar(
windowHandle(),
(Core::App().settings().workMode() == WorkMode::TrayOnly)
&& TrayIconSupported());
}
} else if (obj == this && t == QEvent::Hide) {
_exposed = false;
} else if (obj == this && t == QEvent::ThemeChange) {
updateWindowIcon();
}
return Window::MainWindow::eventFilter(obj, evt);
}
MainWindow::~MainWindow() {
}
int32 ScreenNameChecksum(const QString &name) {
return Window::DefaultScreenNameChecksum(name);
}
int32 ScreenNameChecksum(const QScreen *screen) {
return ScreenNameChecksum(screen->name());
}
QString ScreenDisplayLabel(const QScreen *screen) {
if (!screen) {
return QString();
}
const auto model = (screen->manufacturer()
+ ' '
+ screen->model()).simplified();
if (!model.isEmpty()) {
if (!screen->name().isEmpty()) {
return (model
+ ' '
+ QChar(8212)
+ ' '
+ screen->name()).simplified();
}
return model;
}
return screen->name();
}
} // namespace Platform

View File

@@ -0,0 +1,71 @@
/*
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
*/
#pragma once
#include "platform/platform_main_window.h"
#include "base/unique_qptr.h"
class QMenuBar;
namespace Ui {
class PopupMenu;
} // namespace Ui
namespace Platform {
class MainWindow : public Window::MainWindow {
public:
explicit MainWindow(not_null<Window::Controller*> controller);
~MainWindow();
void updateWindowIcon() override;
protected:
bool eventFilter(QObject *obj, QEvent *evt) override;
void unreadCounterChangedHook() override;
void updateGlobalMenuHook() override;
void workmodeUpdated(Core::Settings::WorkMode mode) override;
void createGlobalMenu() override;
private:
void updateUnityCounter();
QMenuBar *psMainMenu = nullptr;
QAction *psLogout = nullptr;
QAction *psUndo = nullptr;
QAction *psRedo = nullptr;
QAction *psCut = nullptr;
QAction *psCopy = nullptr;
QAction *psPaste = nullptr;
QAction *psDelete = nullptr;
QAction *psSelectAll = nullptr;
QAction *psContacts = nullptr;
QAction *psAddContact = nullptr;
QAction *psNewGroup = nullptr;
QAction *psNewChannel = nullptr;
QAction *psBold = nullptr;
QAction *psItalic = nullptr;
QAction *psUnderline = nullptr;
QAction *psStrikeOut = nullptr;
QAction *psBlockquote = nullptr;
QAction *psMonospace = nullptr;
QAction *psClearFormat = nullptr;
bool _exposed = false;
};
[[nodiscard]] int32 ScreenNameChecksum(const QString &name);
[[nodiscard]] int32 ScreenNameChecksum(const QScreen *screen);
[[nodiscard]] QString ScreenDisplayLabel(const QScreen *screen);
} // namespace Platform

View File

@@ -0,0 +1,938 @@
/*
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/linux/notifications_manager_linux.h"
#include "base/options.h"
#include "base/platform/base_platform_info.h"
#include "base/platform/linux/base_linux_dbus_utilities.h"
#include "platform/platform_specific.h"
#include "core/application.h"
#include "core/sandbox.h"
#include "data/data_forum_topic.h"
#include "data/data_saved_sublist.h"
#include "data/data_peer.h"
#include "history/history.h"
#include "history/history_item.h"
#include "main/main_session.h"
#include "media/audio/media_audio_local_cache.h"
#include "lang/lang_keys.h"
#include "base/weak_ptr.h"
#include "window/notifications_utilities.h"
#include <QtCore/QBuffer>
#include <QtCore/QVersionNumber>
#include <QtGui/QGuiApplication>
#include <ksandbox.h>
#include <xdgnotifications/xdgnotifications.hpp>
#include <dlfcn.h>
namespace Platform {
namespace Notifications {
namespace {
using namespace gi::repository;
namespace GObject = gi::repository::GObject;
constexpr auto kService = "org.freedesktop.Notifications";
constexpr auto kObjectPath = "/org/freedesktop/Notifications";
struct ServerInformation {
std::string name;
std::string vendor;
QVersionNumber version;
QVersionNumber specVersion;
};
bool ServiceRegistered = false;
ServerInformation CurrentServerInformation;
std::vector<std::string> CurrentCapabilities;
[[nodiscard]] bool HasCapability(const char *value) {
return ranges::contains(CurrentCapabilities, value);
}
std::optional<base::Platform::DBus::ServiceWatcher> CreateServiceWatcher() {
auto connection = Gio::bus_get_sync(Gio::BusType::SESSION_, nullptr);
if (!connection) {
return {};
}
const auto activatable = [&] {
const auto names = base::Platform::DBus::ListActivatableNames(
connection.gobj_());
if (!names) {
// avoid service restart loop in sandboxed environments
return true;
}
return ranges::contains(*names, kService);
}();
return std::make_optional<base::Platform::DBus::ServiceWatcher>(
connection.gobj_(),
kService,
[=](
const std::string &service,
const std::string &oldOwner,
const std::string &newOwner) {
Core::Sandbox::Instance().customEnterFromEventLoop([&] {
if (activatable && newOwner.empty()) {
Core::App().notifications().clearAll();
} else {
Core::App().notifications().createManager();
}
});
});
}
void StartServiceAsync(Gio::DBusConnection connection, Fn<void()> callback) {
namespace DBus = base::Platform::DBus;
DBus::StartServiceByNameAsync(
connection.gobj_(),
kService,
[=](Fn<DBus::Result<DBus::StartReply>()> result) {
Core::Sandbox::Instance().customEnterFromEventLoop([&] {
// get the error if any
if (const auto ret = result(); !ret) {
const auto &error = *static_cast<GLib::Error*>(
ret.error().get());
if (error.gobj_()->domain != G_DBUS_ERROR
|| error.code_()
!= G_DBUS_ERROR_SERVICE_UNKNOWN) {
Gio::DBusErrorNS_::strip_remote_error(error);
LOG(("Native Notification Error: %1").arg(
error.message_().c_str()));
}
}
callback();
});
});
}
std::string GetImageKey() {
const auto &specVersion = CurrentServerInformation.specVersion;
if (specVersion >= QVersionNumber(1, 2)) {
return "image-data";
} else if (specVersion == QVersionNumber(1, 1)) {
return "image_data";
}
return "icon_data";
}
bool UseGNotification() {
if (!Gio::Application::get_default()) {
return false;
}
if (Window::Notifications::OptionGNotification.value()) {
return true;
}
return KSandbox::isFlatpak() && !ServiceRegistered;
}
} // namespace
class Manager::Private : public base::has_weak_ptr {
public:
explicit Private(not_null<Manager*> manager);
void init(XdgNotifications::NotificationsProxy proxy);
void showNotification(
NotificationInfo &&info,
Ui::PeerUserpicView &userpicView);
void clearAll();
void clearFromItem(not_null<HistoryItem*> item);
void clearFromTopic(not_null<Data::ForumTopic*> topic);
void clearFromSublist(not_null<Data::SavedSublist*> sublist);
void clearFromHistory(not_null<History*> history);
void clearFromSession(not_null<Main::Session*> session);
void clearNotification(NotificationId id);
void invokeIfNotInhibited(Fn<void()> callback);
private:
struct NotificationData : public base::has_weak_ptr {
std::variant<v::null_t, uint, std::string> id;
rpl::lifetime lifetime;
};
using Notification = std::unique_ptr<NotificationData>;
const not_null<Manager*> _manager;
Gio::Application _application;
XdgNotifications::NotificationsProxy _proxy;
XdgNotifications::Notifications _interface;
Media::Audio::LocalDiskCache _sounds;
base::flat_map<
ContextId,
base::flat_map<MsgId, Notification>> _notifications;
rpl::lifetime _lifetime;
};
bool SkipToastForCustom() {
return false;
}
void MaybePlaySoundForCustom(Fn<void()> playSound) {
playSound();
}
void MaybeFlashBounceForCustom(Fn<void()> flashBounce) {
flashBounce();
}
bool WaitForInputForCustom() {
return true;
}
bool Supported() {
return ServiceRegistered || UseGNotification();
}
bool Enforced() {
// Wayland doesn't support positioning
// and custom notifications don't work here
return IsWayland()
|| (Gio::Application::get_default()
&& Window::Notifications::OptionGNotification.value());
}
bool ByDefault() {
// The capabilities are static, equivalent to 'body' and 'actions' only
if (UseGNotification()) {
return false;
}
// A list of capabilities that offer feature parity
// with custom notifications
return ranges::all_of(std::array{
// To show message content
"body",
// To have buttons on notifications
"actions",
// To have quick reply
"inline-reply",
}, HasCapability) && ranges::any_of(std::array{
// To not to play sound with Don't Disturb activated
"sound",
"inhibitions",
}, HasCapability);
}
bool VolumeSupported() {
return UseGNotification() || !HasCapability("sound");
}
void Create(Window::Notifications::System *system) {
static const auto ServiceWatcher = CreateServiceWatcher();
const auto managerSetter = [=](
XdgNotifications::NotificationsProxy proxy) {
Core::Sandbox::Instance().customEnterFromEventLoop([&] {
system->setManager([=] {
auto manager = std::make_unique<Manager>(system);
manager->_private->init(proxy);
return manager;
});
});
};
const auto counter = std::make_shared<int>(2);
const auto oneReady = [=](XdgNotifications::NotificationsProxy proxy) {
if (!--*counter) {
managerSetter(proxy);
}
};
XdgNotifications::NotificationsProxy::new_for_bus(
Gio::BusType::SESSION_,
Gio::DBusProxyFlags::NONE_,
kService,
kObjectPath,
[=](GObject::Object, Gio::AsyncResult res) {
auto result =
XdgNotifications::NotificationsProxy::new_for_bus_finish(res);
if (result) {
ServiceRegistered = bool(result->get_name_owner());
} else {
Gio::DBusErrorNS_::strip_remote_error(result.error());
LOG(("Native Notification Error: %1").arg(
result.error().message_().c_str()));
ServiceRegistered = false;
}
if (!ServiceRegistered) {
CurrentServerInformation = {};
CurrentCapabilities = {};
managerSetter({});
return;
}
auto proxy = *result;
auto interface = XdgNotifications::Notifications(proxy);
interface.call_get_server_information([=](
GObject::Object,
Gio::AsyncResult res) mutable {
const auto result =
interface.call_get_server_information_finish(res);
if (result) {
CurrentServerInformation = {
std::get<1>(*result),
std::get<2>(*result),
QVersionNumber::fromString(
QString::fromStdString(std::get<3>(*result))
).normalized(),
QVersionNumber::fromString(
QString::fromStdString(std::get<4>(*result))
).normalized(),
};
} else {
Gio::DBusErrorNS_::strip_remote_error(result.error());
LOG(("Native Notification Error: %1").arg(
result.error().message_().c_str()));
CurrentServerInformation = {};
}
oneReady(proxy);
});
interface.call_get_capabilities([=](
GObject::Object,
Gio::AsyncResult res) mutable {
const auto result = interface.call_get_capabilities_finish(
res);
if (result) {
CurrentCapabilities = std::get<1>(*result)
| ranges::to<std::vector<std::string>>;
} else {
Gio::DBusErrorNS_::strip_remote_error(result.error());
LOG(("Native Notification Error: %1").arg(
result.error().message_().c_str()));
CurrentCapabilities = {};
}
oneReady(proxy);
});
});
}
Manager::Private::Private(not_null<Manager*> manager)
: _manager(manager)
, _application(UseGNotification()
? Gio::Application::get_default()
: nullptr)
, _sounds(cWorkingDir() + u"tdata/audio_cache"_q) {
const auto &serverInformation = CurrentServerInformation;
if (!serverInformation.name.empty()) {
LOG(("Notification daemon product name: %1")
.arg(serverInformation.name.c_str()));
}
if (!serverInformation.vendor.empty()) {
LOG(("Notification daemon vendor name: %1")
.arg(serverInformation.vendor.c_str()));
}
if (!serverInformation.version.isNull()) {
LOG(("Notification daemon version: %1")
.arg(serverInformation.version.toString()));
}
if (!serverInformation.specVersion.isNull()) {
LOG(("Notification daemon specification version: %1")
.arg(serverInformation.specVersion.toString()));
}
if (!CurrentCapabilities.empty()) {
LOG(("Notification daemon capabilities: %1").arg(
ranges::fold_left(
CurrentCapabilities,
"",
[](const std::string &a, const std::string &b) {
return a + (a.empty() ? "" : ", ") + b;
}).c_str()));
}
if (auto actionMap = Gio::ActionMap(_application)) {
const auto dictToNotificationId = [](GLib::VariantDict dict) {
return NotificationId{
.contextId = ContextId{
.sessionId = dict.lookup_value("session").get_uint64(),
.peerId = PeerId(dict.lookup_value("peer").get_uint64()),
.topicRootId = MsgId(
dict.lookup_value("topic").get_int64()),
.monoforumPeerId = PeerId(dict.lookup_value(
"monoforumpeer").get_uint64()),
},
.msgId = dict.lookup_value("msgid").get_int64(),
};
};
auto activate = gi::object_cast<Gio::SimpleAction>(
actionMap.lookup_action("notification-activate"));
const auto activateSig = activate.signal_activate().connect([=](
Gio::SimpleAction,
GLib::Variant parameter) {
Core::Sandbox::Instance().customEnterFromEventLoop([&] {
_manager->notificationActivated(
dictToNotificationId(GLib::VariantDict::new_(parameter)));
});
});
_lifetime.add([=]() mutable {
activate.disconnect(activateSig);
});
auto markAsRead = gi::object_cast<Gio::SimpleAction>(
actionMap.lookup_action("notification-mark-as-read"));
const auto markAsReadSig = markAsRead.signal_activate().connect([=](
Gio::SimpleAction,
GLib::Variant parameter) {
Core::Sandbox::Instance().customEnterFromEventLoop([&] {
_manager->notificationReplied(
dictToNotificationId(GLib::VariantDict::new_(parameter)),
{});
});
});
_lifetime.add([=]() mutable {
markAsRead.disconnect(markAsReadSig);
});
}
}
void Manager::Private::init(XdgNotifications::NotificationsProxy proxy) {
_proxy = proxy;
_interface = proxy;
if (_application || !_interface) {
return;
}
const auto actionInvoked = _interface.signal_action_invoked().connect([=](
XdgNotifications::Notifications,
uint id,
std::string actionName) {
Core::Sandbox::Instance().customEnterFromEventLoop([&] {
for (const auto &[key, notifications] : _notifications) {
for (const auto &[msgId, notification] : notifications) {
const auto &nid = notification->id;
if (v::is<uint>(nid) && v::get<uint>(nid) == id) {
if (actionName == "default") {
_manager->notificationActivated({ key, msgId });
} else if (actionName == "mail-mark-read") {
_manager->notificationReplied({ key, msgId }, {});
}
return;
}
}
}
});
});
_lifetime.add([=] {
_interface.disconnect(actionInvoked);
});
const auto replied = _interface.signal_notification_replied().connect([=](
XdgNotifications::Notifications,
uint id,
std::string text) {
Core::Sandbox::Instance().customEnterFromEventLoop([&] {
for (const auto &[key, notifications] : _notifications) {
for (const auto &[msgId, notification] : notifications) {
const auto &nid = notification->id;
if (v::is<uint>(nid) && v::get<uint>(nid) == id) {
_manager->notificationReplied(
{ key, msgId },
{ QString::fromStdString(text), {} });
return;
}
}
}
});
});
_lifetime.add([=] {
_interface.disconnect(replied);
});
const auto tokenSignal = _interface.signal_activation_token().connect([=](
XdgNotifications::Notifications,
uint id,
std::string token) {
for (const auto &[key, notifications] : _notifications) {
for (const auto &[msgId, notification] : notifications) {
const auto &nid = notification->id;
if (v::is<uint>(nid) && v::get<uint>(nid) == id) {
GLib::setenv("XDG_ACTIVATION_TOKEN", token, true);
return;
}
}
}
});
_lifetime.add([=] {
_interface.disconnect(tokenSignal);
});
const auto closed = _interface.signal_notification_closed().connect([=](
XdgNotifications::Notifications,
uint id,
uint reason) {
Core::Sandbox::Instance().customEnterFromEventLoop([&] {
for (const auto &[key, notifications] : _notifications) {
for (const auto &[msgId, notification] : notifications) {
/*
* From: https://specifications.freedesktop.org/notification-spec/latest/ar01s09.html
* The reason the notification was closed
* 1 - The notification expired.
* 2 - The notification was dismissed by the user.
* 3 - The notification was closed by a call to CloseNotification.
* 4 - Undefined/reserved reasons.
*
* If the notification was dismissed by the user (reason == 2), the notification is not kept in notification history.
* We do not need to send a "CloseNotification" call later to clear it from history.
* Therefore we can drop the notification reference now.
* In all other cases we keep the notification reference so that we may clear the notification later from history,
* if the message for that notification is read (e.g. chat is opened or read from another device).
*/
const auto &nid = notification->id;
if (v::is<uint>(nid) && v::get<uint>(nid) == id && reason == 2) {
clearNotification({ key, msgId });
return;
}
}
}
});
});
_lifetime.add([=] {
_interface.disconnect(closed);
});
}
void Manager::Private::showNotification(
NotificationInfo &&info,
Ui::PeerUserpicView &userpicView) {
const auto peer = info.peer;
const auto options = info.options;
const auto key = ContextId{
.sessionId = peer->session().uniqueId(),
.peerId = peer->id,
.topicRootId = info.topicRootId,
.monoforumPeerId = info.monoforumPeerId,
};
const auto notificationId = NotificationId{
.contextId = key,
.msgId = info.itemId,
};
auto notification = _application
? Gio::Notification::new_(info.title.toStdString())
: Gio::Notification();
std::vector<gi::cstring> actions;
auto hints = GLib::VariantDict::new_();
if (notification) {
notification.set_body(info.subtitle.isEmpty()
? info.message.toStdString()
: tr::lng_dialogs_text_with_from(
tr::now,
lt_from_part,
tr::lng_dialogs_text_from_wrapped(
tr::now,
lt_from,
info.subtitle),
lt_message,
info.message).toStdString());
notification.set_icon(
Gio::ThemedIcon::new_(ApplicationIconName().toStdString()));
// for chat messages, according to
// https://docs.gtk.org/gio/enum.NotificationPriority.html
notification.set_priority(Gio::NotificationPriority::HIGH_);
// glib 2.70+, we keep glib 2.56+ compatibility
static const auto set_category = [] {
// reset dlerror after dlsym call
const auto guard = gsl::finally([] { dlerror(); });
return reinterpret_cast<void(*)(GNotification*, const gchar*)>(
dlsym(RTLD_DEFAULT, "g_notification_set_category"));
}();
if (set_category) {
set_category(notification.gobj_(), "im.received");
}
const auto notificationVariant = GLib::Variant::new_array({
GLib::Variant::new_dict_entry(
GLib::Variant::new_string("session"),
GLib::Variant::new_variant(
GLib::Variant::new_uint64(peer->session().uniqueId()))),
GLib::Variant::new_dict_entry(
GLib::Variant::new_string("peer"),
GLib::Variant::new_variant(
GLib::Variant::new_uint64(peer->id.value))),
GLib::Variant::new_dict_entry(
GLib::Variant::new_string("peer"),
GLib::Variant::new_variant(
GLib::Variant::new_uint64(peer->id.value))),
GLib::Variant::new_dict_entry(
GLib::Variant::new_string("topic"),
GLib::Variant::new_variant(
GLib::Variant::new_int64(info.topicRootId.bare))),
GLib::Variant::new_dict_entry(
GLib::Variant::new_string("monoforumpeer"),
GLib::Variant::new_variant(
GLib::Variant::new_uint64(info.monoforumPeerId.value))),
GLib::Variant::new_dict_entry(
GLib::Variant::new_string("msgid"),
GLib::Variant::new_variant(
GLib::Variant::new_int64(info.itemId.bare))),
});
notification.set_default_action_and_target(
"app.notification-activate",
notificationVariant);
if (!options.hideMarkAsRead) {
notification.add_button_with_target(
tr::lng_context_mark_read(tr::now).toStdString(),
"app.notification-mark-as-read",
notificationVariant);
}
} else {
if (HasCapability("actions")) {
actions.push_back("default");
actions.push_back(tr::lng_open_link(tr::now).toStdString());
if (!options.hideMarkAsRead) {
// icon name according to https://specifications.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html
actions.push_back("mail-mark-read");
actions.push_back(
tr::lng_context_mark_read(tr::now).toStdString());
}
if (HasCapability("inline-reply")
&& !options.hideReplyButton) {
actions.push_back("inline-reply");
actions.push_back(
tr::lng_notification_reply(tr::now).toStdString());
}
}
actions.push_back({});
if (HasCapability("action-icons")) {
hints.insert_value(
"action-icons",
GLib::Variant::new_boolean(true));
}
if (HasCapability("sound")) {
const auto sound = info.sound
? info.sound()
: Media::Audio::LocalSound();
const auto path = sound
? _sounds.path(sound).toStdString()
: std::string();
if (!path.empty()) {
hints.insert_value(
"sound-file",
GLib::Variant::new_string(path));
} else {
hints.insert_value(
"suppress-sound",
GLib::Variant::new_boolean(true));
}
}
if (HasCapability("x-canonical-append")) {
hints.insert_value(
"x-canonical-append",
GLib::Variant::new_string("true"));
}
hints.insert_value(
"category",
GLib::Variant::new_string("im.received"));
hints.insert_value("desktop-entry", GLib::Variant::new_string(
QGuiApplication::desktopFileName().toStdString()));
}
const auto imageKey = GetImageKey();
if (!options.hideNameAndPhoto) {
if (notification) {
QByteArray imageData;
QBuffer buffer(&imageData);
buffer.open(QIODevice::WriteOnly);
Window::Notifications::GenerateUserpic(peer, userpicView).save(
&buffer,
"PNG");
notification.set_icon(
Gio::BytesIcon::new_(
GLib::Bytes::new_with_free_func(
reinterpret_cast<const uchar*>(imageData.constData()),
imageData.size(),
[imageData] {})));
} else if (!imageKey.empty()) {
const auto image = Window::Notifications::GenerateUserpic(
peer,
userpicView
).convertToFormat(QImage::Format_RGBA8888);
hints.insert_value(imageKey, GLib::Variant::new_tuple({
GLib::Variant::new_int32(image.width()),
GLib::Variant::new_int32(image.height()),
GLib::Variant::new_int32(image.bytesPerLine()),
GLib::Variant::new_boolean(true),
GLib::Variant::new_int32(8),
GLib::Variant::new_int32(4),
GLib::Variant::new_from_data(
GLib::VariantType::new_("ay"),
reinterpret_cast<const uchar*>(image.constBits()),
image.sizeInBytes(),
true,
[image] {}),
}));
}
}
const auto &data
= _notifications[key][info.itemId]
= std::make_unique<NotificationData>();
data->lifetime.add([=, notification = data.get()] {
v::match(notification->id, [&](const std::string &id) {
_application.withdraw_notification(id);
}, [&](uint id) {
_interface.call_close_notification(id, nullptr);
}, [](v::null_t) {});
});
if (notification) {
const auto id = Gio::dbus_generate_guid();
data->id = id;
_application.send_notification(id, notification);
} else {
// work around snap's activation restriction
const auto weak = base::make_weak(data);
StartServiceAsync(
_proxy.get_connection(),
crl::guard(weak, [=]() mutable {
const auto hasImage = !imageKey.empty()
&& hints.lookup_value(imageKey);
const auto callbackWrap = gi::unwrap(
Gio::AsyncReadyCallback(
crl::guard(this, [=](
GObject::Object,
Gio::AsyncResult res) {
auto &sandbox = Core::Sandbox::Instance();
sandbox.customEnterFromEventLoop([&] {
const auto result
= _interface.call_notify_finish(res);
if (!result) {
Gio::DBusErrorNS_::strip_remote_error(
result.error());
LOG(("Native Notification Error: %1").arg(
result.error().message_().c_str()));
clearNotification(notificationId);
return;
}
if (!weak) {
_interface.call_close_notification(
std::get<1>(*result),
nullptr);
return;
}
weak->id = std::get<1>(*result);
});
})),
gi::scope_async);
xdg_notifications_notifications_call_notify(
_interface.gobj_(),
AppName.data(),
0,
(!hasImage
? ApplicationIconName().toStdString()
: std::string()).c_str(),
info.title.toStdString().c_str(),
(HasCapability("body-markup")
? info.subtitle.isEmpty()
? info.message.toHtmlEscaped().toStdString()
: u"<b>%1</b>\n%2"_q.arg(
info.subtitle.toHtmlEscaped(),
info.message.toHtmlEscaped()).toStdString()
: info.subtitle.isEmpty()
? info.message.toStdString()
: tr::lng_dialogs_text_with_from(
tr::now,
lt_from_part,
tr::lng_dialogs_text_from_wrapped(
tr::now,
lt_from,
info.subtitle),
lt_message,
info.message).toStdString()).c_str(),
(actions
| ranges::views::transform(&gi::cstring::c_str)
| ranges::to_vector).data(),
hints.end().gobj_(),
-1,
nullptr,
&callbackWrap->wrapper,
callbackWrap);
}));
}
}
void Manager::Private::clearAll() {
_notifications.clear();
}
void Manager::Private::clearFromItem(not_null<HistoryItem*> item) {
const auto i = _notifications.find(ContextId{
.sessionId = item->history()->session().uniqueId(),
.peerId = item->history()->peer->id,
.topicRootId = item->topicRootId(),
.monoforumPeerId = item->sublistPeerId(),
});
if (i != _notifications.cend()
&& i->second.remove(item->id)
&& i->second.empty()) {
_notifications.erase(i);
}
}
void Manager::Private::clearFromTopic(not_null<Data::ForumTopic*> topic) {
_notifications.remove(ContextId{
.sessionId = topic->session().uniqueId(),
.peerId = topic->history()->peer->id,
.topicRootId = topic->rootId(),
});
}
void Manager::Private::clearFromSublist(
not_null<Data::SavedSublist*> sublist) {
_notifications.remove(ContextId{
.sessionId = sublist->session().uniqueId(),
.peerId = sublist->owningHistory()->peer->id,
.monoforumPeerId = sublist->sublistPeer()->id,
});
}
void Manager::Private::clearFromHistory(not_null<History*> history) {
const auto sessionId = history->session().uniqueId();
const auto peerId = history->peer->id;
auto i = _notifications.lower_bound(ContextId{
.sessionId = sessionId,
.peerId = peerId,
});
while (i != _notifications.cend()
&& i->first.sessionId == sessionId
&& i->first.peerId == peerId) {
i = _notifications.erase(i);
}
}
void Manager::Private::clearFromSession(not_null<Main::Session*> session) {
const auto sessionId = session->uniqueId();
auto i = _notifications.lower_bound(ContextId{
.sessionId = sessionId,
});
while (i != _notifications.cend() && i->first.sessionId == sessionId) {
i = _notifications.erase(i);
}
}
void Manager::Private::clearNotification(NotificationId id) {
auto i = _notifications.find(id.contextId);
if (i != _notifications.cend()
&& i->second.remove(id.msgId)
&& i->second.empty()) {
_notifications.erase(i);
}
}
void Manager::Private::invokeIfNotInhibited(Fn<void()> callback) {
if (!_interface.get_inhibited()) {
callback();
}
}
Manager::Manager(not_null<Window::Notifications::System*> system)
: NativeManager(system)
, _private(std::make_unique<Private>(this)) {
}
Manager::~Manager() = default;
void Manager::doShowNativeNotification(
NotificationInfo &&info,
Ui::PeerUserpicView &userpicView) {
_private->showNotification(std::move(info), userpicView);
}
void Manager::doClearAllFast() {
_private->clearAll();
}
void Manager::doClearFromItem(not_null<HistoryItem*> item) {
_private->clearFromItem(item);
}
void Manager::doClearFromTopic(not_null<Data::ForumTopic*> topic) {
_private->clearFromTopic(topic);
}
void Manager::doClearFromSublist(not_null<Data::SavedSublist*> sublist) {
_private->clearFromSublist(sublist);
}
void Manager::doClearFromHistory(not_null<History*> history) {
_private->clearFromHistory(history);
}
void Manager::doClearFromSession(not_null<Main::Session*> session) {
_private->clearFromSession(session);
}
bool Manager::doSkipToast() const {
return false;
}
void Manager::doMaybePlaySound(Fn<void()> playSound) {
_private->invokeIfNotInhibited(std::move(playSound));
}
void Manager::doMaybeFlashBounce(Fn<void()> flashBounce) {
_private->invokeIfNotInhibited(std::move(flashBounce));
}
} // namespace Notifications
} // namespace Platform

View File

@@ -0,0 +1,42 @@
/*
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
*/
#pragma once
#include "platform/platform_notifications_manager.h"
namespace Platform {
namespace Notifications {
class Manager : public Window::Notifications::NativeManager {
public:
Manager(not_null<Window::Notifications::System*> system);
~Manager();
protected:
void doShowNativeNotification(
NotificationInfo &&info,
Ui::PeerUserpicView &userpicView) override;
void doClearAllFast() override;
void doClearFromItem(not_null<HistoryItem*> item) override;
void doClearFromTopic(not_null<Data::ForumTopic*> topic) override;
void doClearFromSublist(not_null<Data::SavedSublist*> sublist) override;
void doClearFromHistory(not_null<History*> history) override;
void doClearFromSession(not_null<Main::Session*> session) override;
bool doSkipToast() const override;
void doMaybePlaySound(Fn<void()> playSound) override;
void doMaybeFlashBounce(Fn<void()> flashBounce) override;
private:
friend void Create(Window::Notifications::System *system);
class Private;
const std::unique_ptr<Private> _private;
};
} // namespace Notifications
} // namespace Platform

View File

@@ -0,0 +1,59 @@
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
<interface name="org.freedesktop.Notifications">
<signal name="NotificationClosed">
<arg direction="out" type="u" name="id"/>
<arg direction="out" type="u" name="reason"/>
</signal>
<signal name="ActionInvoked">
<arg direction="out" type="u" name="id"/>
<arg direction="out" type="s" name="action_key"/>
</signal>
<signal name="NotificationReplied">
<arg direction="out" type="u" name="id"/>
<arg direction="out" type="s" name="text"/>
</signal>
<signal name="ActivationToken">
<arg direction="out" type="u" name="id"/>
<arg direction="out" type="s" name="activation_token"/>
</signal>
<method name="Notify">
<annotation value="QVariantMap" name="org.qtproject.QtDBus.QtTypeName.In6"/>
<arg direction="out" type="u"/>
<arg direction="in" type="s" name="app_name"/>
<arg direction="in" type="u" name="replaces_id"/>
<arg direction="in" type="s" name="app_icon"/>
<arg direction="in" type="s" name="summary"/>
<arg direction="in" type="s" name="body"/>
<arg direction="in" type="as" name="actions"/>
<arg direction="in" type="a{sv}" name="hints"/>
<arg direction="in" type="i" name="timeout"/>
</method>
<method name="CloseNotification">
<arg direction="in" type="u" name="id"/>
</method>
<method name="GetCapabilities">
<arg direction="out" type="as" name="caps"/>
</method>
<method name="GetServerInformation">
<arg direction="out" type="s" name="name"/>
<arg direction="out" type="s" name="vendor"/>
<arg direction="out" type="s" name="version"/>
<arg direction="out" type="s" name="spec_version"/>
</method>
<method name="Inhibit">
<annotation value="QVariantMap" name="org.qtproject.QtDBus.QtTypeName.In2"/>
<arg direction="out" type="u"/>
<arg direction="in" type="s" name="desktop_entry"/>
<arg direction="in" type="s" name="reason"/>
<arg direction="in" type="a{sv}" name="hints"/>
</method>
<method name="UnInhibit">
<arg direction="in" type="u"/>
</method>
<property access="read" type="b" name="Inhibited">
<annotation value="true" name="org.freedesktop.DBus.Property.EmitsChangedSignal"/>
</property>
</interface>
</node>

View File

@@ -0,0 +1,20 @@
/*
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
*/
#pragma once
namespace Platform {
inline std::unique_ptr<OverlayWidgetHelper> CreateOverlayWidgetHelper(
not_null<Ui::RpWindow*> window,
Fn<void(bool)> maximize) {
return std::make_unique<DefaultOverlayWidgetHelper>(
window,
std::move(maximize));
}
} // namespace Platform

View File

@@ -0,0 +1,915 @@
/*
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/linux/specific_linux.h"
#include "base/openssl_help.h"
#include "base/random.h"
#include "base/platform/base_platform_info.h"
#include "base/platform/linux/base_linux_dbus_utilities.h"
#include "base/platform/linux/base_linux_xdp_utilities.h"
#include "base/platform/linux/base_linux_app_launch_context.h"
#include "lang/lang_keys.h"
#include "core/launcher.h"
#include "core/sandbox.h"
#include "core/application.h"
#include "core/update_checker.h"
#include "data/data_location.h"
#include "window/window_controller.h"
#include "webview/platform/linux/webview_linux_webkitgtk.h"
#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
#include "base/platform/linux/base_linux_xcb_utilities.h"
#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION
#include <QtWidgets/QApplication>
#include <QtWidgets/QSystemTrayIcon>
#include <QtGui/QDesktopServices>
#include <QtCore/QStandardPaths>
#include <QtCore/QProcess>
#include <kshell.h>
#include <ksandbox.h>
#include <xdgdbus/xdgdbus.hpp>
#include <xdpbackground/xdpbackground.hpp>
#include <xdpopenuri/xdpopenuri.hpp>
#include <xdprequest/xdprequest.hpp>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/un.h>
#include <cstdlib>
#include <unistd.h>
#include <dirent.h>
#include <pwd.h>
namespace {
using namespace gi::repository;
namespace GObject = gi::repository::GObject;
using namespace Platform;
void PortalAutostart(bool enabled, Fn<void(bool)> done) {
const auto executable = ExecutablePathForShortcuts();
if (executable.isEmpty()) {
if (done) {
done(false);
}
return;
}
XdpBackground::BackgroundProxy::new_for_bus(
Gio::BusType::SESSION_,
Gio::DBusProxyFlags::NONE_,
base::Platform::XDP::kService,
base::Platform::XDP::kObjectPath,
[=](GObject::Object, Gio::AsyncResult res) {
auto proxy = XdpBackground::BackgroundProxy::new_for_bus_finish(
res);
if (!proxy) {
if (done) {
Gio::DBusErrorNS_::strip_remote_error(proxy.error());
LOG(("Portal Autostart Error: %1").arg(
proxy.error().message_().c_str()));
done(false);
}
return;
}
auto interface = XdpBackground::Background(*proxy);
const auto handleToken = "tdesktop"
+ std::to_string(base::RandomValue<uint>());
auto uniqueName = std::string(
proxy->get_connection().get_unique_name());
uniqueName.erase(0, 1);
uniqueName.replace(uniqueName.find('.'), 1, 1, '_');
const auto parent = []() -> QPointer<QWidget> {
const auto active = Core::App().activeWindow();
if (!active) {
return nullptr;
}
return active->widget().get();
}();
const auto window = std::make_shared<base::unique_qptr<QWidget>>(
std::in_place,
parent);
auto &raw = **window;
raw.setAttribute(Qt::WA_DontShowOnScreen);
raw.setWindowFlag(Qt::Window);
raw.setWindowModality(Qt::WindowModal);
raw.show();
XdpRequest::RequestProxy::new_(
proxy->get_connection(),
Gio::DBusProxyFlags::NONE_,
base::Platform::XDP::kService,
base::Platform::XDP::kObjectPath
+ std::string("/request/")
+ uniqueName
+ '/'
+ handleToken,
nullptr,
[=](GObject::Object, Gio::AsyncResult res) mutable {
auto requestProxy = XdpRequest::RequestProxy::new_finish(
res);
if (!requestProxy) {
if (done) {
Gio::DBusErrorNS_::strip_remote_error(
requestProxy.error());
LOG(("Portal Autostart Error: %1").arg(
requestProxy.error().message_().c_str()));
done(false);
}
return;
}
auto request = XdpRequest::Request(*requestProxy);
const auto signalId = std::make_shared<ulong>();
*signalId = request.signal_response().connect([=](
XdpRequest::Request,
guint response,
GLib::Variant) mutable {
auto &sandbox = Core::Sandbox::Instance();
sandbox.customEnterFromEventLoop([&] {
(void)window; // don't destroy until finish
if (response) {
if (done) {
LOG(("Portal Autostart Error: "
"Request denied"));
done(false);
}
} else if (done) {
done(enabled);
}
request.disconnect(*signalId);
});
});
std::vector<std::string> commandline;
commandline.push_back(executable.toStdString());
if (Core::Launcher::Instance().customWorkingDir()) {
commandline.push_back("-workdir");
commandline.push_back(cWorkingDir().toStdString());
}
commandline.push_back("-autostart");
interface.call_request_background(
base::Platform::XDP::ParentWindowID(parent
? parent->windowHandle()
: nullptr),
GLib::Variant::new_array({
GLib::Variant::new_dict_entry(
GLib::Variant::new_string("handle_token"),
GLib::Variant::new_variant(
GLib::Variant::new_string(handleToken))),
GLib::Variant::new_dict_entry(
GLib::Variant::new_string("reason"),
GLib::Variant::new_variant(
GLib::Variant::new_string(
tr::lng_settings_auto_start(tr::now)
.toStdString()))),
GLib::Variant::new_dict_entry(
GLib::Variant::new_string("autostart"),
GLib::Variant::new_variant(
GLib::Variant::new_boolean(enabled))),
GLib::Variant::new_dict_entry(
GLib::Variant::new_string("commandline"),
GLib::Variant::new_variant(
GLib::Variant::new_strv(commandline))),
GLib::Variant::new_dict_entry(
GLib::Variant::new_string("dbus-activatable"),
GLib::Variant::new_variant(
GLib::Variant::new_boolean(false))),
}),
[=](GObject::Object, Gio::AsyncResult res) mutable {
auto &sandbox = Core::Sandbox::Instance();
sandbox.customEnterFromEventLoop([&] {
const auto result =
interface.call_request_background_finish(
res);
if (!result) {
if (done) {
const auto &error = result.error();
Gio::DBusErrorNS_::strip_remote_error(
error);
LOG(("Portal Autostart Error: %1").arg(
error.message_().c_str()));
done(false);
}
request.disconnect(*signalId);
}
});
});
});
});
}
bool GenerateDesktopFile(
const QString &targetPath,
const QStringList &args = {},
bool onlyMainGroup = false,
bool silent = false) {
const auto executable = ExecutablePathForShortcuts();
if (targetPath.isEmpty() || executable.isEmpty()) {
return false;
}
DEBUG_LOG(("App Info: placing .desktop file to %1").arg(targetPath));
if (!QDir(targetPath).exists()) QDir().mkpath(targetPath);
const auto sourceFile = u":/misc/org.telegram.desktop.desktop"_q;
const auto targetFile = targetPath
+ QGuiApplication::desktopFileName()
+ u".desktop"_q;
const auto sourceText = [&] {
QFile source(sourceFile);
if (source.open(QIODevice::ReadOnly)) {
return source.readAll().toStdString();
}
return std::string();
}();
if (sourceText.empty()) {
if (!silent) {
LOG(("App Error: Could not open '%1' for read").arg(sourceFile));
}
return false;
}
auto target = GLib::KeyFile::new_();
const auto loaded = target.load_from_data(
sourceText,
-1,
GLib::KeyFileFlags::KEEP_COMMENTS_
| GLib::KeyFileFlags::KEEP_TRANSLATIONS_);
if (!loaded) {
if (!silent) {
LOG(("App Error: %1").arg(loaded.error().message_().c_str()));
}
return false;
}
for (const auto &group : target.get_groups(nullptr)) {
if (onlyMainGroup && group != "Desktop Entry") {
const auto removed = target.remove_group(group);
if (!removed) {
if (!silent) {
LOG(("App Error: %1").arg(
removed.error().message_().c_str()));
}
return false;
}
continue;
}
if (target.has_key(group, "TryExec", nullptr)) {
target.set_string(
group,
"TryExec",
KShell::joinArgs({ executable }).replace(
'\\',
qstr("\\\\")).toStdString());
}
if (target.has_key(group, "Exec", nullptr)) {
if (group == "Desktop Entry" && !args.isEmpty()) {
QStringList exec;
exec.append(executable);
if (Core::Launcher::Instance().customWorkingDir()) {
exec.append(u"-workdir"_q);
exec.append(cWorkingDir());
}
exec.append(args);
target.set_string(
group,
"Exec",
KShell::joinArgs(exec).replace(
'\\',
qstr("\\\\")).toStdString());
} else {
auto exec = KShell::splitArgs(
QString::fromStdString(
target.get_string(group, "Exec", nullptr)
).replace(
qstr("\\\\"),
qstr("\\")));
if (!exec.isEmpty()) {
exec[0] = executable;
if (Core::Launcher::Instance().customWorkingDir()) {
exec.insert(1, u"-workdir"_q);
exec.insert(2, cWorkingDir());
}
target.set_string(
group,
"Exec",
KShell::joinArgs(exec).replace(
'\\',
qstr("\\\\")).toStdString());
}
}
}
}
if (!args.isEmpty()) {
target.remove_key("Desktop Entry", "DBusActivatable");
}
const auto saved = target.save_to_file(targetFile.toStdString());
if (!saved) {
if (!silent) {
LOG(("App Error: %1").arg(saved.error().message_().c_str()));
}
return false;
}
QFile::setPermissions(
targetFile,
QFile::permissions(targetFile)
| QFileDevice::ExeOwner
| QFileDevice::ExeGroup
| QFileDevice::ExeOther);
if (!Core::UpdaterDisabled()) {
DEBUG_LOG(("App Info: removing old .desktop files"));
QFile::remove(u"%1telegram.desktop"_q.arg(targetPath));
QFile::remove(u"%1telegramdesktop.desktop"_q.arg(targetPath));
const auto appimagePath = u"file://%1%2"_q.arg(
cExeDir(),
cExeName()).toUtf8();
char md5Hash[33] = { 0 };
hashMd5Hex(
appimagePath.constData(),
appimagePath.size(),
md5Hash);
QFile::remove(u"%1appimagekit_%2-%3.desktop"_q.arg(
targetPath,
md5Hash,
AppName.utf16().replace(' ', '_')));
const auto d = QFile::encodeName(QDir(cWorkingDir()).absolutePath());
hashMd5Hex(d.constData(), d.size(), md5Hash);
if (!Core::Launcher::Instance().customWorkingDir()) {
QFile::remove(u"%1org.telegram.desktop._%2.desktop"_q.arg(
targetPath,
md5Hash));
const auto exePath = QFile::encodeName(
cExeDir() + cExeName());
hashMd5Hex(exePath.constData(), exePath.size(), md5Hash);
}
QFile::remove(u"%1org.telegram.desktop.%2.desktop"_q.arg(
targetPath,
md5Hash));
}
return true;
}
bool GenerateServiceFile(bool silent = false) {
const auto executable = ExecutablePathForShortcuts();
if (executable.isEmpty()) {
return false;
}
const auto targetPath = QStandardPaths::writableLocation(
QStandardPaths::GenericDataLocation) + u"/dbus-1/services/"_q;
const auto targetFile = targetPath
+ QGuiApplication::desktopFileName()
+ u".service"_q;
DEBUG_LOG(("App Info: placing D-Bus service file to %1").arg(targetPath));
if (!QDir(targetPath).exists()) QDir().mkpath(targetPath);
auto target = GLib::KeyFile::new_();
constexpr auto group = "D-BUS Service";
target.set_string(
group,
"Name",
QGuiApplication::desktopFileName().toStdString());
QStringList exec;
exec.append(executable);
if (Core::Launcher::Instance().customWorkingDir()) {
exec.append(u"-workdir"_q);
exec.append(cWorkingDir());
}
target.set_string(
group,
"Exec",
KShell::joinArgs(exec).toStdString());
const auto saved = target.save_to_file(targetFile.toStdString());
if (!saved) {
if (!silent) {
LOG(("App Error: %1").arg(saved.error().message_().c_str()));
}
return false;
}
if (!Core::UpdaterDisabled()
&& !Core::Launcher::Instance().customWorkingDir()) {
DEBUG_LOG(("App Info: removing old D-Bus service files"));
char md5Hash[33] = { 0 };
const auto d = QFile::encodeName(QDir(cWorkingDir()).absolutePath());
hashMd5Hex(d.constData(), d.size(), md5Hash);
QFile::remove(u"%1org.telegram.desktop._%2.service"_q.arg(
targetPath,
md5Hash));
}
XdgDBus::DBusProxy::new_for_bus(
Gio::BusType::SESSION_,
Gio::DBusProxyFlags::NONE_,
base::Platform::DBus::kService,
base::Platform::DBus::kObjectPath,
[=](GObject::Object, Gio::AsyncResult res) {
auto interface = XdgDBus::DBus(
XdgDBus::DBusProxy::new_for_bus_finish(res, nullptr));
if (!interface) {
return;
}
interface.call_reload_config(nullptr);
});
return true;
}
void InstallLauncher() {
static const auto DisabledByEnv = !qEnvironmentVariableIsEmpty(
"DESKTOPINTEGRATION");
// don't update desktop file for alpha version or if updater is disabled
if (cAlphaVersion() || Core::UpdaterDisabled() || DisabledByEnv) {
return;
}
const auto applicationsPath = QStandardPaths::writableLocation(
QStandardPaths::ApplicationsLocation) + '/';
GenerateDesktopFile(applicationsPath);
GenerateServiceFile();
const auto icons = QStandardPaths::writableLocation(
QStandardPaths::GenericDataLocation) + u"/icons/"_q;
const auto appIcons = icons + u"/hicolor/256x256/apps/"_q;
if (!QDir(appIcons).exists()) QDir().mkpath(appIcons);
const auto icon = appIcons + ApplicationIconName() + u".png"_q;
QFile::remove(icon);
QFile::remove(icons + u"telegram.png"_q);
if (QFile::copy(u":/gui/art/logo_256.png"_q, icon)) {
DEBUG_LOG(("App Info: Icon copied to '%1'").arg(icon));
}
const auto symbolicIcons = icons + u"/hicolor/symbolic/apps/"_q;
if (!QDir().exists(symbolicIcons)) QDir().mkpath(symbolicIcons);
const auto monochromeIcons = {
QString(),
u"attention"_q,
u"mute"_q,
};
for (const auto &icon : monochromeIcons) {
QFile::copy(
u":/gui/icons/tray/monochrome%1.svg"_q.arg(
!icon.isEmpty() ? u"_"_q + icon : QString()),
symbolicIcons
+ ApplicationIconName()
+ (!icon.isEmpty() ? u"-"_q + icon : QString())
+ u"-symbolic.svg"_q);
}
QProcess::execute("update-desktop-database", {
applicationsPath
});
}
[[nodiscard]] QByteArray HashForSocketPath() {
constexpr auto kHashForSocketPathLength = 24;
const auto binary = openssl::Sha256(
bytes::make_span(Core::Launcher::Instance().instanceHash()));
const auto base64 = QByteArray(
reinterpret_cast<const char*>(binary.data()),
binary.size()).toBase64(QByteArray::Base64UrlEncoding);
return base64.mid(0, kHashForSocketPathLength);
}
void AppInfoCheckScheme(
const std::string &scheme,
Fn<void(Gio::AppInfo, Fn<void()>)> callback,
Fn<void()> fail) {
// TODO: use get_default_for_uri_scheme_async once we can use GLib 2.74
if (auto appInfo = Gio::AppInfo::get_default_for_uri_scheme(scheme)) {
callback(appInfo, fail);
return;
}
fail();
}
void PortalCheckScheme(
const std::string &scheme,
Fn<void(Fn<void()>)> callback,
Fn<void()> fail) {
XdpOpenURI::OpenURIProxy::new_for_bus(
Gio::BusType::SESSION_,
Gio::DBusProxyFlags::NONE_,
base::Platform::XDP::kService,
base::Platform::XDP::kObjectPath,
[=](GObject::Object, Gio::AsyncResult res) {
auto interface = XdpOpenURI::OpenURI(
XdpOpenURI::OpenURIProxy::new_for_bus_finish(res, nullptr));
if (!interface) {
fail();
return;
}
interface.call_scheme_supported(
scheme,
GLib::Variant::new_array(
GLib::VariantType::new_("{sv}"),
{}),
[=](GObject::Object, Gio::AsyncResult res) mutable {
const auto result
= interface.call_scheme_supported_finish(res);
if (!result || !std::get<1>(*result)) {
fail();
return;
}
callback(fail);
});
});
}
} // namespace
namespace Platform {
void SetApplicationIcon(const QIcon &icon) {
QApplication::setWindowIcon(icon);
}
QString SingleInstanceLocalServerName(const QString &hash) {
#if defined Q_OS_LINUX && QT_VERSION >= QT_VERSION_CHECK(6, 2, 0)
if (KSandbox::isSnap()) {
return u"snap."_q
+ qEnvironmentVariable("SNAP_INSTANCE_NAME")
+ '.'
+ hash;
}
return hash + '-' + QCoreApplication::applicationName();
#else // Q_OS_LINUX && Qt >= 6.2.0
return QDir::tempPath()
+ '/'
+ hash
+ '-'
+ QCoreApplication::applicationName();
#endif // !Q_OS_LINUX || Qt < 6.2.0
}
#if QT_VERSION < QT_VERSION_CHECK(6, 5, 0)
std::optional<bool> IsDarkMode() {
auto result = base::Platform::XDP::ReadSetting(
"org.freedesktop.appearance",
"color-scheme");
return result.has_value()
? std::make_optional(result->get_uint32() == 1)
: std::nullopt;
}
#endif // Qt < 6.5.0
bool AutostartSupported() {
return true;
}
void AutostartToggle(bool enabled, Fn<void(bool)> done) {
if (KSandbox::isFlatpak()) {
PortalAutostart(enabled, done);
return;
}
const auto success = [&] {
const auto autostart = QStandardPaths::writableLocation(
QStandardPaths::GenericConfigLocation)
+ u"/autostart/"_q;
if (!enabled) {
return QFile::remove(
autostart
+ QGuiApplication::desktopFileName()
+ u".desktop"_q);
}
return GenerateDesktopFile(
autostart,
{ u"-autostart"_q },
true,
!done);
}();
if (done) {
done(enabled && success);
}
}
bool AutostartSkip() {
return !cAutoStart();
}
bool TrayIconSupported() {
return QSystemTrayIcon::isSystemTrayAvailable();
}
bool SkipTaskbarSupported() {
#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
if (IsX11()) {
return base::Platform::XCB::IsSupportedByWM(
base::Platform::XCB::Connection(),
"_NET_WM_STATE_SKIP_TASKBAR");
}
#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION
return false;
}
QString ExecutablePathForShortcuts() {
if (Core::UpdaterDisabled()) {
const auto &arguments = Core::Launcher::Instance().arguments();
if (!arguments.isEmpty()) {
const auto result = QFileInfo(arguments.first()).fileName();
if (!result.isEmpty()) {
return result;
}
}
return cExeName();
}
return cExeDir() + cExeName();
}
} // namespace Platform
QString psAppDataPath() {
// Previously we used ~/.TelegramDesktop, so look there first.
// If we find data there, we should still use it.
auto home = QDir::homePath();
if (!home.isEmpty()) {
auto oldPath = home + u"/.TelegramDesktop/"_q;
auto oldSettingsBase = oldPath + u"tdata/settings"_q;
if (QFile::exists(oldSettingsBase + '0')
|| QFile::exists(oldSettingsBase + '1')
|| QFile::exists(oldSettingsBase + 's')) {
return oldPath;
}
}
return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + '/';
}
void psDoCleanup() {
try {
Platform::AutostartToggle(false);
psSendToMenu(false, true);
} catch (...) {
}
}
int psCleanup() {
psDoCleanup();
return 0;
}
void psDoFixPrevious() {
}
int psFixPrevious() {
psDoFixPrevious();
return 0;
}
namespace Platform {
void start() {
QGuiApplication::setDesktopFileName([&] {
if (KSandbox::isFlatpak()) {
return qEnvironmentVariable("FLATPAK_ID");
}
if (KSandbox::isSnap()) {
return qEnvironmentVariable("SNAP_INSTANCE_NAME")
+ '_'
+ cExeName();
}
if (!Core::UpdaterDisabled()) {
return u"org.telegram.desktop._%1"_q.arg(
Core::Launcher::Instance().instanceHash().constData());
}
return u"org.telegram.desktop"_q;
}());
LOG(("App ID: %1").arg(QGuiApplication::desktopFileName()));
if (!qEnvironmentVariableIsSet("XDG_ACTIVATION_TOKEN")
&& qEnvironmentVariableIsSet("DESKTOP_STARTUP_ID")) {
qputenv("XDG_ACTIVATION_TOKEN", qgetenv("DESKTOP_STARTUP_ID"));
}
qputenv("PULSE_PROP_application.name", AppName.utf8());
qputenv(
"PULSE_PROP_application.icon_name",
ApplicationIconName().toUtf8());
GLib::set_prgname(cExeName().toStdString());
GLib::set_application_name(AppName.data());
Webview::WebKitGTK::SetSocketPath(u"%1/%2-%3-webview-{}"_q.arg(
QDir::tempPath(),
HashForSocketPath(),
u"TD"_q).toStdString());
InstallLauncher();
}
void finish() {
}
PermissionStatus GetPermissionStatus(PermissionType type) {
return PermissionStatus::Granted;
}
void RequestPermission(PermissionType type, Fn<void(PermissionStatus)> resultCallback) {
resultCallback(PermissionStatus::Granted);
}
void OpenSystemSettingsForPermission(PermissionType type) {
}
bool OpenSystemSettings(SystemSettingsType type) {
if (type == SystemSettingsType::Audio) {
struct Command {
QString command;
QStringList arguments;
};
auto options = std::vector<Command>();
const auto add = [&](const char *option, const char *arg = nullptr) {
auto command = Command{ .command = option };
if (arg) {
command.arguments.push_back(arg);
}
options.push_back(std::move(command));
};
add("unity-control-center", "sound");
add("kcmshell6", "kcm_pulseaudio");
add("kcmshell5", "kcm_pulseaudio");
add("kcmshell4", "phonon");
add("gnome-control-center", "sound");
add("cinnamon-settings", "sound");
add("mate-volume-control");
add("pavucontrol-qt");
add("pavucontrol");
add("alsamixergui");
return ranges::any_of(options, [](const Command &command) {
return QProcess::startDetached(
command.command,
command.arguments);
});
}
return true;
}
void NewVersionLaunched(int oldVersion) {
if (oldVersion <= 5014003 && cAutoStart()) {
AutostartToggle(true);
}
}
QImage DefaultApplicationIcon() {
return Window::Logo();
}
QString ApplicationIconName() {
static const auto Result = KSandbox::isSnap()
? u"snap.%1."_q.arg(qEnvironmentVariable("SNAP_INSTANCE_NAME"))
: QGuiApplication::desktopFileName().remove(
u"._"_q + Core::Launcher::Instance().instanceHash());
return Result;
}
void LaunchMaps(const Data::LocationPoint &point, Fn<void()> fail) {
const auto url = QUrl(
u"geo:%1,%2"_q.arg(point.latAsString(), point.lonAsString()));
AppInfoCheckScheme(url.scheme().toStdString(), [=](
Gio::AppInfo appInfo,
Fn<void()> fail) {
// TODO: use launch_uris_async once we can use GLib 2.60
if (!appInfo.launch_uris(
{ url.toString().toStdString() },
base::Platform::AppLaunchContext(),
nullptr)) {
fail();
}
}, [=] {
PortalCheckScheme(url.scheme().toStdString(), [=](Fn<void()> fail) {
if (!QDesktopServices::openUrl(url)) {
fail();
}
}, fail);
});
}
namespace ThirdParty {
void start() {
}
} // namespace ThirdParty
} // namespace Platform
void psSendToMenu(bool send, bool silent) {
}
bool linuxMoveFile(const char *from, const char *to) {
FILE *ffrom = fopen(from, "rb"), *fto = fopen(to, "wb");
if (!ffrom) {
if (fto) fclose(fto);
return false;
}
if (!fto) {
fclose(ffrom);
return false;
}
static const int BufSize = 65536;
char buf[BufSize];
while (size_t size = fread(buf, 1, BufSize, ffrom)) {
fwrite(buf, 1, size, fto);
}
struct stat fst; // from http://stackoverflow.com/questions/5486774/keeping-fileowner-and-permissions-after-copying-file-in-c
//let's say this wont fail since you already worked OK on that fp
if (fstat(fileno(ffrom), &fst) != 0) {
fclose(ffrom);
fclose(fto);
return false;
}
//update to the same uid/gid
if (fchown(fileno(fto), fst.st_uid, fst.st_gid) != 0) {
fclose(ffrom);
fclose(fto);
return false;
}
//update the permissions
if (fchmod(fileno(fto), fst.st_mode) != 0) {
fclose(ffrom);
fclose(fto);
return false;
}
fclose(ffrom);
fclose(fto);
if (unlink(from)) {
return false;
}
return true;
}

View File

@@ -0,0 +1,58 @@
/*
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
*/
#pragma once
#include "platform/platform_specific.h"
namespace Platform {
inline void IgnoreApplicationActivationRightNow() {
}
inline void WriteCrashDumpDetails() {
}
inline void AutostartRequestStateFromSystem(Fn<void(bool)> callback) {
}
inline bool PreventsQuit(Core::QuitReason reason) {
return false;
}
inline void ActivateThisProcess() {
}
inline uint64 ActivationWindowId(not_null<QWidget*> window) {
return 1;
}
inline void ActivateOtherProcess(uint64 processId, uint64 windowId) {
}
} // namespace Platform
inline void psCheckLocalSocket(const QString &serverName) {
QFile address(serverName);
if (address.exists()) {
address.remove();
}
}
QString psAppDataPath();
void psSendToMenu(bool send, bool silent = false);
int psCleanup();
int psFixPrevious();
inline QByteArray psDownloadPathBookmark(const QString &path) {
return QByteArray();
}
inline void psDownloadPathEnableAccess() {
}
bool linuxMoveFile(const char *from, const char *to);

View File

@@ -0,0 +1,24 @@
/*
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
*/
#pragma once
#include "platform/platform_text_recognition.h"
namespace Platform {
namespace TextRecognition {
inline bool IsAvailable() {
return false;
}
inline Result RecognizeText(const QImage &image) {
return {};
}
} // namespace TextRecognition
} // namespace Platform

View File

@@ -0,0 +1,422 @@
/*
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/linux/tray_linux.h"
#include "base/invoke_queued.h"
#include "base/qt_signal_producer.h"
#include "base/platform/linux/base_linux_dbus_utilities.h"
#include "core/application.h"
#include "core/sandbox.h"
#include "platform/platform_specific.h"
#include "ui/ui_utility.h"
#include "ui/widgets/popup_menu.h"
#include "window/window_controller.h"
#include "styles/style_window.h"
#include <QtCore/QCoreApplication>
#include <QtWidgets/QMenu>
#include <QtWidgets/QSystemTrayIcon>
#include <gio/gio.hpp>
namespace Platform {
namespace {
using namespace gi::repository;
[[nodiscard]] QString PanelIconName(int counter, bool muted) {
return ApplicationIconName() + ((counter > 0)
? (muted
? u"-mute"_q
: u"-attention"_q)
: QString()) + u"-symbolic"_q;
}
} // namespace
class IconGraphic final {
public:
explicit IconGraphic();
~IconGraphic();
void updateState();
[[nodiscard]] bool isRefreshNeeded() const;
[[nodiscard]] QIcon trayIcon();
private:
struct State {
QIcon systemIcon;
QString iconThemeName;
bool monochrome = false;
int32 counter = 0;
bool muted = false;
};
[[nodiscard]] QIcon systemIcon() const;
[[nodiscard]] bool isCounterNeeded(const State &state) const;
[[nodiscard]] int counterSlice(int counter) const;
[[nodiscard]] QSize dprSize(const QImage &image) const;
const int _iconSizes[7];
base::flat_map<int, QImage> _imageBack;
QIcon _trayIcon;
State _current;
State _new;
};
IconGraphic::IconGraphic()
: _iconSizes{ 16, 22, 32, 48, 64, 128, 256 } {
updateState();
}
IconGraphic::~IconGraphic() = default;
QIcon IconGraphic::systemIcon() const {
if (_new.iconThemeName == _current.iconThemeName
&& _new.monochrome == _current.monochrome
&& (_new.counter > 0) == (_current.counter > 0)
&& _new.muted == _current.muted) {
return _current.systemIcon;
}
const auto candidates = {
_new.monochrome ? PanelIconName(_new.counter, _new.muted) : QString(),
ApplicationIconName(),
};
for (const auto &candidate : candidates) {
if (candidate.isEmpty()) {
continue;
}
const auto icon = QIcon::fromTheme(candidate, QIcon());
if (icon.name() == candidate) {
return icon;
}
}
return QIcon();
}
bool IconGraphic::isCounterNeeded(const State &state) const {
return state.systemIcon.name() != PanelIconName(
state.counter,
state.muted);
}
int IconGraphic::counterSlice(int counter) const {
return (counter >= 100)
? (100 + (counter % 10))
: counter;
}
QSize IconGraphic::dprSize(const QImage &image) const {
return image.size() / image.devicePixelRatio();
}
void IconGraphic::updateState() {
_new.iconThemeName = QIcon::themeName();
_new.monochrome = Core::App().settings().trayIconMonochrome();
_new.counter = Core::App().unreadBadge();
_new.muted = Core::App().unreadBadgeMuted();
_new.systemIcon = systemIcon();
}
bool IconGraphic::isRefreshNeeded() const {
return _trayIcon.isNull()
|| _new.iconThemeName != _current.iconThemeName
|| _new.systemIcon.name() != _current.systemIcon.name()
|| (isCounterNeeded(_new)
? _new.muted != _current.muted
|| counterSlice(_new.counter) != counterSlice(
_current.counter)
: false);
}
QIcon IconGraphic::trayIcon() {
if (!isRefreshNeeded()) {
return _trayIcon;
}
const auto guard = gsl::finally([&] {
_current = _new;
});
if (!isCounterNeeded(_new)) {
_trayIcon = _new.systemIcon;
return _trayIcon;
}
QIcon result;
for (const auto iconSize : _iconSizes) {
auto &currentImageBack = _imageBack[iconSize];
const auto desiredSize = QSize(iconSize, iconSize);
if (currentImageBack.isNull()
|| _new.iconThemeName != _current.iconThemeName
|| _new.systemIcon.name() != _current.systemIcon.name()) {
currentImageBack = {};
if (!_new.systemIcon.isNull()) {
// We can't use QIcon::actualSize here
// since it works incorrectly with svg icon themes
currentImageBack = _new.systemIcon
.pixmap(desiredSize)
.toImage();
const auto firstAttemptSize = dprSize(currentImageBack);
// if current icon theme is not a svg one, Qt can return
// a pixmap that less in size even if there are a bigger one
if (firstAttemptSize.width() < desiredSize.width()) {
const auto availableSizes
= _new.systemIcon.availableSizes();
const auto biggestSize = ranges::max_element(
availableSizes,
std::less<>(),
&QSize::width);
if (biggestSize->width() > firstAttemptSize.width()) {
currentImageBack = _new.systemIcon
.pixmap(*biggestSize)
.toImage();
}
}
}
if (currentImageBack.isNull()) {
currentImageBack = Window::Logo();
}
if (dprSize(currentImageBack) != desiredSize) {
currentImageBack = currentImageBack.scaled(
desiredSize * currentImageBack.devicePixelRatio(),
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
}
}
result.addPixmap(Ui::PixmapFromImage(_new.counter > 0
? Window::WithSmallCounter(std::move(currentImageBack), {
.size = iconSize,
.count = _new.counter,
.bg = _new.muted ? st::trayCounterBgMute : st::trayCounterBg,
.fg = st::trayCounterFg,
}) : std::move(currentImageBack)));
}
_trayIcon = result;
return _trayIcon;
}
class TrayEventFilter final : public QObject {
public:
TrayEventFilter(not_null<QObject*> parent);
[[nodiscard]] rpl::producer<> contextMenuFilters() const;
protected:
bool eventFilter(QObject *watched, QEvent *event) override;
private:
const QString _iconObjectName;
rpl::event_stream<> _contextMenuFilters;
};
TrayEventFilter::TrayEventFilter(not_null<QObject*> parent)
: QObject(parent)
, _iconObjectName("QSystemTrayIconSys") {
parent->installEventFilter(this);
}
bool TrayEventFilter::eventFilter(QObject *obj, QEvent *event) {
if (event->type() == QEvent::MouseButtonPress
&& obj->objectName() == _iconObjectName) {
const auto m = static_cast<QMouseEvent*>(event);
if (m->button() == Qt::RightButton) {
Core::Sandbox::Instance().customEnterFromEventLoop([&] {
_contextMenuFilters.fire({});
});
return true;
}
}
return false;
}
rpl::producer<> TrayEventFilter::contextMenuFilters() const {
return _contextMenuFilters.events();
}
Tray::Tray() {
auto connection = Gio::bus_get_sync(Gio::BusType::SESSION_, nullptr);
if (connection) {
_sniWatcher = std::make_unique<base::Platform::DBus::ServiceWatcher>(
connection.gobj_(),
"org.kde.StatusNotifierWatcher",
[=](
const std::string &service,
const std::string &oldOwner,
const std::string &newOwner) {
Core::Sandbox::Instance().customEnterFromEventLoop([&] {
if (hasIcon()) {
destroyIcon();
createIcon();
}
});
});
}
}
void Tray::createIcon() {
if (!_icon) {
LOG(("System tray available: %1").arg(Logs::b(TrayIconSupported())));
if (!_iconGraphic) {
_iconGraphic = std::make_unique<IconGraphic>();
}
const auto showCustom = [=] {
_aboutToShowRequests.fire({});
InvokeQueued(_menuCustom.get(), [=] {
_menuCustom->popup(QCursor::pos());
});
};
_icon = base::make_unique_q<QSystemTrayIcon>(nullptr);
_icon->setIcon(_iconGraphic->trayIcon());
_icon->setToolTip(AppName.utf16());
using Reason = QSystemTrayIcon::ActivationReason;
base::qt_signal_producer(
_icon.get(),
&QSystemTrayIcon::activated
) | rpl::on_next([=](Reason reason) {
if (reason == QSystemTrayIcon::Context) {
showCustom();
} else {
_iconClicks.fire({});
}
}, _lifetime);
_icon->setContextMenu(_menu.get());
if (!_eventFilter) {
_eventFilter = base::make_unique_q<TrayEventFilter>(
QCoreApplication::instance());
_eventFilter->contextMenuFilters(
) | rpl::on_next([=] {
showCustom();
}, _lifetime);
}
}
updateIcon();
_icon->show();
}
void Tray::destroyIcon() {
_icon = nullptr;
}
void Tray::updateIcon() {
if (!_icon || !_iconGraphic) {
return;
}
_iconGraphic->updateState();
if (_iconGraphic->isRefreshNeeded()) {
_icon->setIcon(_iconGraphic->trayIcon());
}
}
void Tray::createMenu() {
if (!_menu) {
_menu = base::make_unique_q<QMenu>(nullptr);
}
if (!_menuCustom) {
_menuCustom = base::make_unique_q<Ui::PopupMenu>(nullptr);
_menuCustom->deleteOnHide(false);
}
}
void Tray::destroyMenu() {
_menuCustom = nullptr;
if (_menu) {
_menu->clear();
}
_actionsLifetime.destroy();
}
void Tray::addAction(rpl::producer<QString> text, Fn<void()> &&callback) {
if (_menuCustom) {
const auto action = _menuCustom->addAction(QString(), callback);
rpl::duplicate(
text
) | rpl::on_next([=](const QString &text) {
action->setText(text);
}, _actionsLifetime);
}
if (_menu) {
const auto action = _menu->addAction(QString(), std::move(callback));
std::move(
text
) | rpl::on_next([=](const QString &text) {
action->setText(text);
}, _actionsLifetime);
}
}
void Tray::showTrayMessage() const {
}
bool Tray::hasTrayMessageSupport() const {
return false;
}
rpl::producer<> Tray::aboutToShowRequests() const {
return rpl::merge(
_aboutToShowRequests.events(),
_menu
? base::qt_signal_producer(_menu.get(), &QMenu::aboutToShow)
: rpl::never<>() | rpl::type_erased);
}
rpl::producer<> Tray::showFromTrayRequests() const {
return rpl::never<>();
}
rpl::producer<> Tray::hideToTrayRequests() const {
return rpl::never<>();
}
rpl::producer<> Tray::iconClicks() const {
return _iconClicks.events();
}
bool Tray::hasIcon() const {
return _icon;
}
rpl::lifetime &Tray::lifetime() {
return _lifetime;
}
Tray::~Tray() = default;
bool HasMonochromeSetting() {
return QIcon::hasThemeIcon(
PanelIconName(
Core::App().unreadBadge(),
Core::App().unreadBadgeMuted()));
}
} // namespace Platform

View File

@@ -0,0 +1,75 @@
/*
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
*/
#pragma once
#include "platform/platform_tray.h"
#include "base/unique_qptr.h"
namespace base::Platform::DBus {
class ServiceWatcher;
} // namespace base::Platform::DBus
namespace Ui {
class PopupMenu;
} // namespace Ui
class QMenu;
class QSystemTrayIcon;
namespace Platform {
class IconGraphic;
class TrayEventFilter;
class Tray final {
public:
Tray();
~Tray();
[[nodiscard]] rpl::producer<> aboutToShowRequests() const;
[[nodiscard]] rpl::producer<> showFromTrayRequests() const;
[[nodiscard]] rpl::producer<> hideToTrayRequests() const;
[[nodiscard]] rpl::producer<> iconClicks() const;
[[nodiscard]] bool hasIcon() const;
void createIcon();
void destroyIcon();
void updateIcon();
void createMenu();
void destroyMenu();
void addAction(rpl::producer<QString> text, Fn<void()> &&callback);
void showTrayMessage() const;
[[nodiscard]] bool hasTrayMessageSupport() const;
[[nodiscard]] rpl::lifetime &lifetime();
private:
std::unique_ptr<base::Platform::DBus::ServiceWatcher> _sniWatcher;
std::unique_ptr<IconGraphic> _iconGraphic;
base::unique_qptr<QSystemTrayIcon> _icon;
base::unique_qptr<QMenu> _menu;
base::unique_qptr<Ui::PopupMenu> _menuCustom;
base::unique_qptr<TrayEventFilter> _eventFilter;
rpl::event_stream<> _iconClicks;
rpl::event_stream<> _aboutToShowRequests;
rpl::lifetime _actionsLifetime;
rpl::lifetime _lifetime;
};
} // namespace Platform

View File

@@ -0,0 +1,29 @@
/*
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_webauthn.h"
namespace Platform::WebAuthn {
bool IsSupported() {
return false;
}
void RegisterKey(
const Data::Passkey::RegisterData &data,
Fn<void(RegisterResult result)> callback) {
callback({});
}
void Login(
const Data::Passkey::LoginData &data,
Fn<void(LoginResult result)> callback) {
callback({});
}
} // namespace Platform::WebAuthn