File indexing completed on 2024-05-12 05:10:40

0001 /*
0002  * SPDX-FileCopyrightText: 2019 Dimitris Kardarakos <dimkard@posteo.net>
0003  *
0004  * SPDX-License-Identifier: GPL-3.0-or-later
0005  */
0006 
0007 #include "alarmnotification.h"
0008 #include "kalendaralarmclient.h"
0009 #include <KLocalizedString>
0010 
0011 #include <QDesktopServices>
0012 #include <QRegularExpression>
0013 #include <QUrlQuery>
0014 
0015 using namespace std::chrono_literals;
0016 
0017 AlarmNotification::AlarmNotification(const QString &uid)
0018     : m_uid{uid}
0019 {
0020 }
0021 
0022 AlarmNotification::~AlarmNotification()
0023 {
0024     if (m_notification) {
0025         // don't delete immediately, in case we end up here as a result
0026         // of a signal from m_notification itself
0027         m_notification->deleteLater();
0028     }
0029 }
0030 
0031 void AlarmNotification::send(KalendarAlarmClient *client, const KCalendarCore::Incidence::Ptr &incidence)
0032 {
0033     const QDateTime startTime = m_occurrence.isValid() ? m_occurrence.toLocalTime() : incidence->dtStart().toLocalTime();
0034     const bool notificationExists = m_notification;
0035     if (!notificationExists) {
0036         m_notification = new KNotification(QStringLiteral("alarm"));
0037 
0038         // dismiss both with the explicit action and just closing the notification
0039         // there is no signal for explicit closing though, we only can observe that
0040         // indirectly from not having received a different signal before closed()
0041         QObject::connect(m_notification, &KNotification::closed, client, [this, client]() {
0042             client->dismiss(this);
0043         });
0044     }
0045 
0046     // change the content unconditionally, that will also update already existing notifications
0047     m_notification->setTitle(incidence->summary());
0048 
0049     auto defaultAction = m_notification->addDefaultAction(i18n("View"));
0050     QObject::connect(defaultAction, &KNotificationAction::activated, client, [this, client, startTime] {
0051         client->showIncidence(uid(), startTime, m_notification->xdgActivationToken());
0052     });
0053 
0054     QString text;
0055     const auto now = QDateTime::currentDateTime();
0056     const auto incidenceType = incidence->type() == KCalendarCore::Incidence::TypeTodo ? i18n("Task") : i18n("Event");
0057     if (incidence->type() == KCalendarCore::Incidence::TypeTodo && !incidence->dtStart().isValid()) {
0058         const auto todo = incidence.staticCast<KCalendarCore::Todo>();
0059         text = i18n("Task due at %1", QLocale().toString(todo->dtDue().time(), QLocale::NarrowFormat));
0060     } else if (!incidence->allDay()) {
0061         const int startOffset = qRound(now.secsTo(startTime) / 60.0);
0062         if (startOffset > 0 && startOffset < 60) {
0063             text = i18ncp("Event starts in 5 minutes", "%2 starts in %1 minute", "%2 starts in %1 minutes", startOffset, incidenceType);
0064         } else if (startTime.date() == now.date()) {
0065             // event is/was today
0066             if (startTime >= now) {
0067                 text = i18nc("Event starts at 10:00", "%1 starts at %2", incidenceType, QLocale().toString(startTime.time(), QLocale::NarrowFormat));
0068             } else {
0069                 text = i18nc("Event started at 10:00", "%1 started at %2", incidenceType, QLocale().toString(startTime.time(), QLocale::NarrowFormat));
0070             }
0071         } else {
0072             // start time on a different day
0073             if (startTime >= now) {
0074                 text = i18nc("Event starts on <DATE> at <TIME>",
0075                              "%1 starts on %2 at %3",
0076                              incidenceType,
0077                              QLocale().toString(startTime.date(), QLocale::NarrowFormat),
0078                              QLocale().toString(startTime.time(), QLocale::NarrowFormat));
0079             } else {
0080                 text = i18nc("Event started on <DATE> at <TIME>",
0081                              "%1 started on %2 at %3",
0082                              incidenceType,
0083                              QLocale().toString(startTime.date(), QLocale::NarrowFormat),
0084                              QLocale().toString(startTime.time(), QLocale::NarrowFormat));
0085             }
0086         }
0087     } else {
0088         // all day events
0089         if (startTime >= now) {
0090             text = i18nc("Event starts on <DATE>", "%1 starts on %2", incidenceType, QLocale().toString(startTime.date(), QLocale::NarrowFormat));
0091         } else {
0092             text = i18nc("Event started on <DATE>", "%1 started on %2", incidenceType, QLocale().toString(startTime.date(), QLocale::NarrowFormat));
0093         }
0094     }
0095 
0096     bool eventHasEnded = false;
0097     if (incidence->type() == KCalendarCore::Incidence::TypeEvent) {
0098         const auto event = incidence.staticCast<KCalendarCore::Event>();
0099         const auto eventEndTime = startTime.addSecs(event->dtStart().secsTo(event->dtEnd()));
0100         eventHasEnded = eventEndTime < QDateTime::currentDateTime();
0101     }
0102     if (m_wasSuspended) {
0103         m_notification->setFlags(KNotification::Persistent);
0104     } else {
0105         m_notification->setFlags(eventHasEnded ? KNotification::CloseOnTimeout : KNotification::Persistent);
0106     }
0107 
0108     if (!m_text.isEmpty() && m_text != incidence->summary()) { // MS Teams sometimes repeats the summary as the alarm text, we don't need that
0109         text = m_text + QLatin1Char('\n') + text;
0110     }
0111     m_notification->setText(text);
0112 
0113     m_notification->setIconName(incidence->type() == KCalendarCore::Incidence::TypeTodo ? QStringLiteral("view-task")
0114                                                                                         : QStringLiteral("view-calendar-upcoming"));
0115 
0116     if (!notificationExists) {
0117         auto remindIn5MAction = m_notification->addAction(i18n("Remind in 5 mins"));
0118         QObject::connect(remindIn5MAction, &KNotificationAction::activated, remindIn5MAction, [this, client] {
0119             QObject::disconnect(m_notification, &KNotification::closed, client, nullptr);
0120             client->suspend(this, 5min);
0121         });
0122 
0123         auto remindIn1hAction = m_notification->addAction(i18n("Remind in 1 hour"));
0124         QObject::connect(remindIn1hAction, &KNotificationAction::activated, remindIn1hAction, [this, client] {
0125             QObject::disconnect(m_notification, &KNotification::closed, client, nullptr);
0126             client->suspend(this, 5min);
0127         });
0128 
0129         auto dismissAction = m_notification->addAction(i18nc("dismiss a reminder notification for an event", "Dismiss"));
0130         QObject::connect(dismissAction, &KNotificationAction::activated, dismissAction, [this, client] {
0131             QObject::disconnect(m_notification, &KNotification::closed, client, nullptr);
0132             client->dismiss(this);
0133         });
0134 
0135         const auto contextActionString = determineContextAction(incidence);
0136         if (!contextActionString.isEmpty()) {
0137             auto contextAction = m_notification->addAction(contextActionString);
0138             QObject::connect(contextAction, &KNotificationAction::activated, contextAction, [this] {
0139                 QDesktopServices::openUrl(m_contextAction);
0140             });
0141         }
0142         m_notification->sendEvent();
0143     }
0144 }
0145 
0146 QString AlarmNotification::uid() const
0147 {
0148     return m_uid;
0149 }
0150 
0151 QString AlarmNotification::text() const
0152 {
0153     return m_text;
0154 }
0155 
0156 void AlarmNotification::setText(const QString &alarmText)
0157 {
0158     m_text = alarmText;
0159 }
0160 
0161 QDateTime AlarmNotification::occurrence() const
0162 {
0163     return m_occurrence;
0164 }
0165 
0166 void AlarmNotification::setOccurrence(const QDateTime &occurrence)
0167 {
0168     m_occurrence = occurrence;
0169 }
0170 
0171 QDateTime AlarmNotification::remindAt() const
0172 {
0173     return m_remind_at;
0174 }
0175 
0176 void AlarmNotification::setRemindAt(const QDateTime &remindAtDt)
0177 {
0178     m_remind_at = remindAtDt;
0179 }
0180 
0181 bool AlarmNotification::hasValidContextAction() const
0182 {
0183     return m_contextAction.isValid() && (m_contextAction.scheme() == QLatin1StringView("https") || m_contextAction.scheme() == QLatin1StringView("geo"));
0184 }
0185 
0186 QString AlarmNotification::determineContextAction(const KCalendarCore::Incidence::Ptr &incidence)
0187 {
0188     // look for possible (meeting) URLs
0189     m_contextAction = incidence->url();
0190     if (!hasValidContextAction()) {
0191         m_contextAction = QUrl(incidence->location());
0192     }
0193     if (!hasValidContextAction()) {
0194         m_contextAction = QUrl(incidence->customProperty("MICROSOFT", "SKYPETEAMSMEETINGURL"));
0195     }
0196     if (!hasValidContextAction()) {
0197         static QRegularExpression urlFinder(QStringLiteral(R"(https://[^\s>]*)"));
0198         const auto match = urlFinder.match(incidence->description());
0199         if (match.hasMatch()) {
0200             m_contextAction = QUrl(match.captured());
0201         }
0202     }
0203 
0204     if (hasValidContextAction()) {
0205         return i18n("Open URL");
0206     }
0207 
0208     // navigate to location
0209     if (incidence->hasGeo()) {
0210         m_contextAction.clear();
0211         m_contextAction.setScheme(QStringLiteral("geo"));
0212         m_contextAction.setPath(QString::number(incidence->geoLatitude()) + QLatin1Char(',') + QString::number(incidence->geoLongitude()));
0213     } else if (!incidence->location().isEmpty()) {
0214         m_contextAction.clear();
0215         m_contextAction.setScheme(QStringLiteral("geo"));
0216         m_contextAction.setPath(QStringLiteral("0,0"));
0217         QUrlQuery query;
0218         query.addQueryItem(QStringLiteral("q"), incidence->location());
0219         m_contextAction.setQuery(query);
0220     }
0221 
0222     if (hasValidContextAction()) {
0223         return i18n("Map");
0224     }
0225 
0226     return QString();
0227 }
0228 
0229 bool AlarmNotification::wasSuspended() const
0230 {
0231     return m_wasSuspended;
0232 }
0233 
0234 void AlarmNotification::setWasSuspended(bool newWasSuspended)
0235 {
0236     m_wasSuspended = newWasSuspended;
0237 }