Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Native notifications on Linux without libnotify #6825

Merged
merged 2 commits into from Dec 29, 2019

Conversation

ilya-fedin
Copy link
Contributor

@ilya-fedin ilya-fedin commented Nov 26, 2019

This change removes the need of gtk for native notifications on Linux
Closes #1026

@CLAassistant
Copy link

CLAassistant commented Nov 26, 2019

CLA assistant check
All committers have signed the CLA.

@ilya-fedin
Copy link
Contributor Author

Added possible fix for #1026 and (duplicate) #6507
But I have no idea for which notification daemon there was a hack that caused this problem

@Aokromes
Copy link
Collaborator

@ilya-fedin edit 1st post and add a note "closes #1026" plz :)

@ilya-fedin
Copy link
Contributor Author

@Aokromes ok

@ilya-fedin
Copy link
Contributor Author

🤔 travis-ci failed with -Werror=enum-compare

/home/travis/build/telegramdesktop/tdesktop/upstream/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp: In member function ‘QStringList Platform::Notifications::NotificationData::getServerInformation()’:

/home/travis/build/telegramdesktop/tdesktop/upstream/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp:90:67: error: comparison between ‘enum QVariant::Type’ and ‘enum QMetaType::Type’ [-Werror=enum-compare]

    if (serverInformationReply.arguments()[i].type() == QMetaType::QString) {

But Qt documentation recommends comparsion with QMetaType https://doc.qt.io/QT-5/qvariant.html#type

Should I add -Wno-enum-compare to build scripts?

@23rd
Copy link
Collaborator

23rd commented Nov 30, 2019

Should I add -Wno-enum-compare to build scripts?

You can just replace type() with userType().
So it will be:

if (serverInformationReply.arguments()[i].userType() == QMetaType::QString) {

@ilya-fedin
Copy link
Contributor Author

ilya-fedin commented Dec 1, 2019

I remembered that this PR also fixes #3750, because now there is no call to GetServerName in Supported(), instead the interface is checking for validity. I don’t know if I need to add this to the first post, because issue is auto-closed.

// }
// Libs::notify_notification_clear_actions(_data);
Libs::g_object_unref(Libs::g_object_cast(_data));
if (std::find(capabilities.begin(), capabilities.end(), qsl("body-markup")) != capabilities.end()) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All such lines can be replaced with the range-v3.
Something like this.

if (ranges::find(capabilities, qsl("body-markup")) != capabilities.end()) {

Also to shorten the code we can move the capabilities.end() to a new variable. For example.

const auto endCap = end(capabilities);
if (ranges::find(capabilities, qsl("body-markup")) != endCap) {
...
if (ranges::find(capabilities, qsl("actions")) != endCap) {
...
etc

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Comment on lines 204 to 230
QString NotificationData::escapeHtml(const QString &text) {
auto result = QString();
auto copyFrom = 0, textSize = text.size();
auto data = text.constData();
for (auto i = 0; i != textSize; ++i) {
auto ch = data[i];
if (ch == '<' || ch == '>' || ch == '&') {
if (!copyFrom) {
result.reserve(textSize * 5);
}
if (i > copyFrom) {
result.append(data + copyFrom, i - copyFrom);
}
switch (ch.unicode()) {
case '<': result.append(qstr("&lt;")); break;
case '>': result.append(qstr("&gt;")); break;
case '&': result.append(qstr("&amp;")); break;
}
copyFrom = i + 1;
}
}
if (copyFrom > 0) {
result.append(data + copyFrom, textSize - copyFrom);
return result;
}
return text;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure, but why we can't just use toHtmlEscaped()?

Copy link
Contributor Author

@ilya-fedin ilya-fedin Dec 1, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, this code is from the old implementation, I just reused it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Comment on lines 49 to 53
if (!subtitle.isEmpty()) {
_body = qstr("<b>") + escapeHtml(subtitle) + qstr("</b>") + '\n' + escapeHtml(msg);
} else {
_body = escapeHtml(msg);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be simplified.

_body = subtitle.isEmpty()
	? escapeHtml(msg)
	: QString("<b>%1</b>\n%2")
		.arg(escapeHtml(subtitle))
		.arg(escapeHtml(msg));

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

return _actionsSupported;
}
bool NotificationData::show() {
QStringList actionsStringList(QList<QString>::fromVector(QVector<QString>::fromStdVector(_actions)));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, this is insane.
Perhaps I was wrong and it will be better to store _actions as QStringList in this particular case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Comment on lines 136 to 189
if (_specificationVersion.majorVersion() == 1 && _specificationVersion.minorVersion() >= 2 || _specificationVersion.majorVersion() > 1) {
imageKey = "image-data";
} else if (_specificationVersion.majorVersion() == 1 && _specificationVersion.minorVersion() == 1) {
imageKey = "image_data";
} else if (_specificationVersion.majorVersion() == 1 && _specificationVersion.minorVersion() < 1 || _specificationVersion.majorVersion() < 1) {
imageKey = "icon_data";
} else {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These huge conditions can be easily shortened with a new variables.

const auto major = _specificationVersion.majorVersion();
const auto minor = _specificationVersion.minorVersion();
if (major == 1 && minor >= 2) {
...

It would be better to have a clearly if ((a && b) || c) condition instead of unclearly if (a && b || c).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Comment on lines 94 to 96
for (int i = 0; i < serverInformationReply.arguments().size(); ++i) {
if (static_cast<QMetaType::Type>(serverInformationReply.arguments()[i].type()) == QMetaType::QString) {
serverInformation.push_back(serverInformationReply.arguments()[i].toString());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be simplified with the for ( a : b ) loop.
For example.

for (const auto &arg : serverInformationReply.arguments()) {
	if (static_cast<QMetaType::Type>(arg.type()) == QMetaType::QString) {
		serverInformation.push_back(arg.toString());

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

return text;
}
NotificationData::NotificationData(const std::shared_ptr<Manager*> &guarded, const QString &title, const QString &subtitle, const QString &msg, PeerId peerId, MsgId msgId)
: _notificationInterface("org.freedesktop.Notifications", "/org/freedesktop/Notifications", "org.freedesktop.Notifications")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest you to move the repeated constant values to the separate constants.
Something like this.

namespace {

const auto kService = QString("org.freedesktop.Notifications");
const auto kPath = QString("/org/freedesktop/Notifications");

} // namespace

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving such consts into an anonymous namespace is welcome.
An example from the project code.

namespace {
constexpr auto kThemeFileSizeLimit = 5 * 1024 * 1024;
constexpr auto kBackgroundSizeLimit = 25 * 1024 * 1024;
constexpr auto kNightThemeFile = str_const(":/gui/night.tdesktop-theme");
constexpr auto kMinimumTiledSize = 512;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Comment on lines 39 to 44
if (!capabilities.empty()) {
QString capabilitiesString;

for (const auto &capability : capabilities) {
if (!capabilitiesString.isEmpty()) {
capabilitiesString += qstr(", ");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a useless job for the Release build.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it is better to check if debug logs are enabled than this is a debug build? This should be very helpful if there will be issues with notifications.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be very helpful if there will be issues with notifications.

Well, if this is useful for problems with notifications, we will not get this information from the users, as no one uses the Debug version. I think for a one-time log entry this will be fine to use LOG().

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Libs::g_list_free_full(capabilities, g_free);
const QDBusArgument &operator>>(const QDBusArgument &argument, NotificationData::ImageData &imageData) {
argument.beginStructure();
argument >> imageData.width >> imageData.height >> imageData.rowStride >> imageData.hasAlpha >> imageData.bitsPerSample >> imageData.channels >> imageData.data;;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
argument >> imageData.width >> imageData.height >> imageData.rowStride >> imageData.hasAlpha >> imageData.bitsPerSample >> imageData.channels >> imageData.data;;
argument >> imageData.width >> imageData.height >> imageData.rowStride >> imageData.hasAlpha >> imageData.bitsPerSample >> imageData.channels >> imageData.data;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

bool hideNameAndPhoto = false;
};
if (capabilitiesReply.isValid()) {
return capabilitiesReply.value().toVector().toStdVector();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my opinion, we can remove the blank elements from the vector right away.

Suggested change
return capabilitiesReply.value().toVector().toStdVector();
auto capabilities = capabilitiesReply.value();
capabilities.removeAll(QString());
return capabilities | ranges::to_vector;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which blank elements? o_O

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have the following lines in your code.

if (!capabilitiesString.isEmpty()) {
capabilitiesString += qstr(", ");
}

So you are probably assuming that there may be blank elements?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it is a list of daemon capabilities for debug purposes.
It is also a port of

LOG(("LibNotify capabilities: %1").arg(_capabilities.join(qstr(", "))));

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, I misread the condition, sorry.

Comment on lines 40 to 48
QString capabilitiesString;

for (const auto &capability : capabilities) {
if (!capabilitiesString.isEmpty()) {
capabilitiesString += qstr(", ");
}

logError(error);
capabilitiesString += capability;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This loop can be replaced with std::accumulate.
For example.

const auto capabilitiesString = std::accumulate(
	capabilities.begin(),
	capabilities.end(),
	QString{},
	[](auto &s, auto &p) { return s += (p + qstr(", ")); }).chopped(2);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

@23rd
Copy link
Collaborator

23rd commented Dec 21, 2019

By the way, all new project files follow the style with the column limit of 78.
It's quite difficult to work with long lines. =(
So, I think it would be great to split up them.
(Except for comments with links.)

Comment on lines 232 to 264
auto NotificationDaemonRunning = false;
bool Supported() {
static auto Checked = false;
if (!Checked) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the reason to keep the naked NotificationDaemonRunning variable here? You can move it to the Supported() like you did with the static auto Checked.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I understand, the point here is that the check is performed only once - at startup.
static auto Checked was in Supported() already:


Windows version has similar code:
bool Supported() {
if (!Checked) {
Checked = true;
Check();
}
return InitSucceeded;
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, and you can see that these variables are at least in the anonymous namespace.

auto Checked = false;
auto InitSucceeded = false;
void Check() {
InitSucceeded = init();
}
} // namespace
bool Supported() {
if (!Checked) {
Checked = true;
Check();
}
return InitSucceeded;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I'll put it in an anonymous namespace

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Comment on lines 185 to 193
QVersionNumber NotificationData::parseSpecificationVersion(const std::vector<QString> &serverInformation) {
if (serverInformation.size() >= 4) {
return QVersionNumber::fromString(serverInformation[3]);
} else {
LOG(("Native notification error: server information should have 4 elements"));
}

using Notifications = QMap<PeerId, QMap<MsgId, Notification>>;
Notifications _notifications;
return QVersionNumber();
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: There is no reason to keep this function as a class method and it can be safely moved to anonymous namespace.

namespace {

auto ParseSpecificationVersion(const std::vector<QString> &serverInformation) {
	if (serverInformation.size() >= 4) {
		return QVersionNumber::fromString(serverInformation[3]);
	} else {
		LOG(("Native notification error: server information should have 4 elements"));
	}

	return QVersionNumber();
}

} // namespace

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed


if (!_specificationVersion.isNull()) {
const auto majorVersion = _specificationVersion.majorVersion();
const auto minorVersion = _specificationVersion.majorVersion();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const auto minorVersion = _specificationVersion.majorVersion();
const auto minorVersion = _specificationVersion.minorVersion();

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

@@ -23,16 +28,62 @@ inline bool SkipToast() {
inline void FlashBounce() {
}

void Finish();
class NotificationData : public QObject {
Q_OBJECT
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can safely remove Q_OBJECT to avoid an unnecessary extra MOC.

Suggested change
Q_OBJECT

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, we can't

connect(&_notificationInterface, SIGNAL(ActionInvoked(uint, QString)), this, SLOT(notificationClicked(uint)));

connect(&_notificationInterface, SIGNAL(NotificationClosed(uint, uint)), this, SLOT(notificationClosed(uint)));

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you try?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I added Q_OBJECT only after a lot of tries without it

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay.

@23rd
Copy link
Collaborator

23rd commented Dec 22, 2019

Hmm, do we really need to create the QDBusInterface in each new NotificationData? Can we create a single interface and use it for all future notifications? Can the provided information be changed during the tdesktop execution?

@ilya-fedin
Copy link
Contributor Author

By the way, all new project files follow the style with the column limit of 78.
It's quite difficult to work with long lines. =(
So, I think it would be great to split up them.
(Except for comments with links.)

fixed

Hmm, do we really need to create the QDBusInterface in each new NotificationData? Can we create a single interface and use it for all future notifications? Can the provided information be changed during the tdesktop execution?

I checked - it works without that. But it is still necessary to request capabilities and specification version every time, because user can replace notification daemon (or it can be replaced by system upgrade, because notification daemon doesn't works all the time and only starts upon notification request by dbus activation).

@ilya-fedin ilya-fedin requested a review from 23rd December 24, 2019 12:53
@john-preston john-preston merged commit 3d36b4f into telegramdesktop:dev Dec 29, 2019
@john-preston
Copy link
Member

Thanks a lot!

@github-actions
Copy link

github-actions bot commented Mar 4, 2021

This pull request has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Mar 4, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Telegram comes to focus when clicking notifications from other applications
5 participants