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 }