Commit ae6c5f9f authored by Volker Krause's avatar Volker Krause
Browse files

Create notifications from full incidence objects

This allows us to use KNotifications to its full potential, using its
various text fields, icons, etc, and to update notifications as the
incidence changes.

Additionally, this adds a third action to the notification, which so
far can be one of the following:
- Open a URL found in the event. This is useful for the common use-case of
joining an online meeting.
- Open a map at the location of the event.

We also no longer force the notification to critical urgency in order to
avoid it auto-closing, but use the persistence flag for that. This avoids
us interfering with notification inhibiting, and makes this work on systems
that don't imply persistence from critical urgency like Plasma does.

One major feature is still missing now, opening the incidence in a
calendaring app. That's supposed to be bound to the notification default
action, but needs a D-Bus interface on the calendaring app side first.
parent 5b8133da
Pipeline #110674 failed with stage
in 3 minutes and 2 seconds
......@@ -7,7 +7,11 @@
#include "alarmnotification.h"
#include "kalendaralarmclient.h"
#include <KLocalizedString>
#include <QDebug>
#include <QDesktopServices>
#include <QRegularExpression>
#include <QUrlQuery>
AlarmNotification::AlarmNotification(const QString &uid)
: m_uid{uid}
......@@ -22,28 +26,56 @@ AlarmNotification::~AlarmNotification()
m_notification->deleteLater();
}
void AlarmNotification::send(KalendarAlarmClient *client)
void AlarmNotification::send(KalendarAlarmClient *client, const KCalendarCore::Incidence::Ptr &incidence)
{
if (m_notification) {
return; // already active
const bool notificationExists = m_notification;
if (!notificationExists) {
m_notification = new KNotification(QStringLiteral("alarm"));
m_notification->setFlags(KNotification::Persistent);
// dismiss both with the explicit action and just closing the notification
// there is no signal for explicit closing though, we only can observe that
// indirectly from not having received a different signal before closed()
QObject::connect(m_notification, &KNotification::closed, client, [this, client]() {
client->dismiss(this);
});
QObject::connect(m_notification, &KNotification::action1Activated, client, [this, client]() {
client->suspend(this);
QObject::disconnect(m_notification, &KNotification::closed, client, nullptr);
});
QObject::connect(m_notification, &KNotification::action3Activated, client, [this]() {
QDesktopServices::openUrl(m_contextAction);
});
}
m_notification = new KNotification(QStringLiteral("alarm"));
// change the content unconditionally, that will also update already existing notifications
m_notification->setTitle(incidence->summary());
m_notification->setText(m_text);
m_notification->setActions({i18n("Remind in 5 mins"), i18n("Dismiss")});
// dismiss both with the explicit action and just closing the notification
// there is no signal for explicit closing though, we only can observe that
// indirectly from not having received a different signal before closed()
QObject::connect(m_notification, &KNotification::closed, client, [this, client]() {
client->dismiss(this);
});
QObject::connect(m_notification, &KNotification::action1Activated, client, [this, client]() {
client->suspend(this);
QObject::disconnect(m_notification, &KNotification::closed, client, nullptr);
});
m_notification->sendEvent();
if (!m_text.isEmpty() && m_text != incidence->summary()) { // MS Teams sometimes repeats the summary as the alarm text, we don't need that
m_notification->setText(m_text);
} else if (incidence->type() == KCalendarCore::Incidence::TypeTodo && !incidence->dtStart().isValid()) {
const auto todo = incidence.staticCast<KCalendarCore::Todo>();
m_notification->setText(i18n("Task due at %1", QLocale().toString(todo->dtDue().time(), QLocale::NarrowFormat)));
} else {
const QString incidenceType = incidence->type() == KCalendarCore::Incidence::TypeTodo ? i18n("Task") : i18n("Event");
m_notification->setText(
i18nc("Event starts at 10:00", "%1 starts at %2", incidenceType, QLocale().toString(incidence->dtStart().time(), QLocale::NarrowFormat)));
}
m_notification->setIconName(incidence->type() == KCalendarCore::Incidence::TypeTodo ? QStringLiteral("view-task")
: QStringLiteral("view-calendar-upcoming"));
QStringList actions = {i18n("Remind in 5 mins"), i18n("Dismiss")};
const auto contextAction = determineContextAction(incidence);
if (!contextAction.isEmpty()) {
actions.push_back(contextAction);
}
m_notification->setActions(actions);
if (!notificationExists) {
m_notification->sendEvent();
}
}
QString AlarmNotification::uid() const
......@@ -70,3 +102,58 @@ void AlarmNotification::setRemindAt(const QDateTime &remindAtDt)
{
m_remind_at = remindAtDt;
}
bool AlarmNotification::hasValidContextAction() const
{
return m_contextAction.isValid() && m_contextAction.scheme() == QLatin1String("https");
}
QString AlarmNotification::determineContextAction(const KCalendarCore::Incidence::Ptr &incidence)
{
// look for possible (meeting) URLs
m_contextAction = incidence->url();
if (!hasValidContextAction()) {
m_contextAction = QUrl(incidence->location());
}
if (!hasValidContextAction()) {
m_contextAction = QUrl(incidence->customProperty("MICROSOFT", "SKYPETEAMSMEETINGURL"));
}
if (!hasValidContextAction()) {
static QRegularExpression urlFinder(QStringLiteral(R"(https://[^\s>]*)"));
const auto match = urlFinder.match(incidence->description());
if (match.hasMatch()) {
m_contextAction = QUrl(match.captured());
}
}
if (hasValidContextAction()) {
return i18n("Open URL");
}
// navigate to location
// ### geo: URLs would be nicer for this, but we don't have a default handler for those yet
// on a regular Plasma installation without Marble. Therefore use OSM URLs for now.
if (incidence->hasGeo()) {
m_contextAction.clear();
m_contextAction.setScheme(QStringLiteral("https"));
m_contextAction.setHost(QStringLiteral("www.openstreetmap.org"));
m_contextAction.setPath(QStringLiteral("/"));
const QString fragment =
QLatin1String("map=18/") + QString::number(incidence->geoLatitude()) + QLatin1Char('/') + QString::number(incidence->geoLongitude());
m_contextAction.setFragment(fragment);
} else if (!incidence->location().isEmpty()) {
m_contextAction.clear();
m_contextAction.setScheme(QStringLiteral("https"));
m_contextAction.setHost(QStringLiteral("www.openstreetmap.org"));
m_contextAction.setPath(QStringLiteral("/search"));
QUrlQuery query;
query.addQueryItem(QStringLiteral("query"), incidence->location());
m_contextAction.setQuery(query);
}
if (hasValidContextAction()) {
return i18n("Map");
}
return QString();
}
......@@ -6,9 +6,11 @@
#pragma once
#include <KCalendarCore/Incidence>
#include <KNotification>
#include <QDateTime>
#include <QPointer>
#include <QUrl>
class KalendarAlarmClient;
......@@ -25,7 +27,7 @@ public:
/**
* @brief Sends the notification to be displayed
*/
void send(KalendarAlarmClient *client);
void send(KalendarAlarmClient *client, const KCalendarCore::Incidence::Ptr &incidence);
/**
* @return The uid of the Incidence of the alarm of the notification
......@@ -53,8 +55,12 @@ public:
void setRemindAt(const QDateTime &remindAtDt);
private:
bool hasValidContextAction() const;
QString determineContextAction(const KCalendarCore::Incidence::Ptr &incidence);
QPointer<KNotification> m_notification;
QString m_uid;
QString m_text;
QDateTime m_remind_at;
QUrl m_contextAction;
};
......@@ -73,5 +73,3 @@ Comment[uk]=Сповіщення для нагадування
Comment[x-test]=xxAlarm Notificationxx
Action=Popup|Sound
Sound=freedesktop/stereo/bell.oga
Urgency=Critical
......@@ -101,7 +101,7 @@ void KalendarAlarmClient::restoreSuspendedFromConfig()
QDateTime remindAt = suspendedAlarm.readEntry("RemindAt", QDateTime());
qDebug() << "restoreSuspendedFromConfig: Restoring alarm" << uid << "," << txt << "," << remindAt;
if (!uid.isEmpty() && remindAt.isValid() && !txt.isEmpty()) {
if (!uid.isEmpty() && remindAt.isValid()) {
addNotification(uid, txt, remindAt);
}
}
......@@ -153,17 +153,6 @@ void KalendarAlarmClient::addNotification(const QString &uid, const QString &tex
storeNotification(notification);
}
void KalendarAlarmClient::sendNotifications()
{
qDebug() << "Looking for notifications, total:" << m_notifications.count();
for (auto it = m_notifications.begin(); it != m_notifications.end(); ++it) {
if (it.value()->remindAt() <= QDateTime::currentDateTime()) {
qDebug() << "Sending notification for alarm" << it.value()->uid() << ", text is" << it.value()->text();
it.value()->send(this);
}
}
}
bool KalendarAlarmClient::collectionsAvailable() const
{
// The list of collections must be available.
......@@ -199,6 +188,7 @@ void KalendarAlarmClient::checkAlarms()
qDebug() << "Check:" << from.toString() << " -" << mLastChecked.toString();
// look for new alarms
const Alarm::List alarms = mCalendar->alarms(from, mLastChecked, true /* exclude blocked alarms */);
for (const Alarm::Ptr &alarm : alarms) {
#if AKONADICALENDAR_VERSION < QT_VERSION_CHECK(5, 19, 41)
......@@ -206,27 +196,19 @@ void KalendarAlarmClient::checkAlarms()
#else
const QString uid = alarm->parentUid();
#endif
const KCalendarCore::Incidence::Ptr incidence = mCalendar->incidence(uid);
QString timeText;
if (incidence && incidence->type() == KCalendarCore::Incidence::TypeTodo && !incidence->dtStart().isValid()) {
auto todo = incidence.staticCast<KCalendarCore::Todo>();
timeText = i18n("Task due at %1", QLocale::system().toString(todo->dtDue().time(), QLocale::NarrowFormat));
addNotification(uid, QLatin1String("%1\n%2").arg(timeText, incidence->summary()), mLastChecked);
} else if (incidence) {
QString incidenceString = incidence->type() == KCalendarCore::Incidence::TypeTodo ? i18n("Task") : i18n("Event");
timeText = i18nc("Event starts at 10:00",
"%1 starts at %2",
incidenceString,
QLocale::system().toString(incidence->dtStart().time(), QLocale::NarrowFormat));
addNotification(uid, QLatin1String("%1\n%2").arg(timeText, incidence->summary()), mLastChecked);
} else {
QLocale::system().toString(alarm->time(), QLocale::NarrowFormat);
addNotification(uid, QLatin1String("%1\n%2").arg(timeText, alarm->text()), mLastChecked);
addNotification(uid, alarm->text(), mLastChecked);
}
// execute or update active alarms
for (auto it = m_notifications.begin(); it != m_notifications.end(); ++it) {
if (it.value()->remindAt() <= mLastChecked) {
const auto incidence = mCalendar->incidence(it.value()->uid());
if (incidence) { // can still be null when we get here during the early stages of loading/restoring
it.value()->send(this, incidence);
}
}
}
sendNotifications();
saveLastCheckTime();
// schedule next check for the beginning of the next minute
......
......@@ -38,7 +38,6 @@ private:
void storeNotification(AlarmNotification *notification);
void removeNotification(AlarmNotification *notification);
void addNotification(const QString &uid, const QString &text, const QDateTime &remindTime);
void sendNotifications();
void checkAlarms();
void setupAkonadi();
Q_REQUIRED_RESULT bool collectionsAvailable() const;
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment