/* Copyright (C) 2020 Sebastian J. Wolf and other contributors This file is part of Fernschreiber. Fernschreiber is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Fernschreiber is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Fernschreiber. If not, see . */ #include "notificationmanager.h" #include "fernschreiberutils.h" #include "chatmodel.h" #include #include #include #include #include #include #define DEBUG_MODULE NotificationManager #include "debuglog.h" namespace { const QString _TYPE("@type"); const QString TYPE("type"); const QString ID("id"); const QString CHAT_ID("chat_id"); const QString IS_CHANNEL("is_channel"); const QString TOTAL_COUNT("total_count"); const QString DATE("date"); const QString TITLE("title"); const QString CONTENT("content"); const QString MESSAGE("message"); const QString FIRST_NAME("first_name"); const QString LAST_NAME("last_name"); const QString SENDER_ID("sender_id"); const QString USER_ID("user_id"); const QString NOTIFICATIONS("notifications"); const QString NOTIFICATION_GROUP_ID("notification_group_id"); const QString ADDED_NOTIFICATIONS("added_notifications"); const QString REMOVED_NOTIFICATION_IDS("removed_notification_ids"); const QString CHAT_TYPE_BASIC_GROUP("chatTypeBasicGroup"); const QString CHAT_TYPE_SUPERGROUP("chatTypeSupergroup"); const QString APP_NAME("Fernschreiber"); // Notification hints const QString HINT_GROUP_ID("x-fernschreiber.group_id"); // int const QString HINT_CHAT_ID("x-fernschreiber.chat_id"); // qlonglong const QString HINT_TOTAL_COUNT("x-fernschreiber.total_count"); // int const QString HINT_IMAGE_PATH("image-path"); // QString const QString HINT_VIBRA("x-nemo-vibrate"); // bool const QString HINT_SUPPRESS_SOUND("suppress-sound"); // bool const QString HINT_DISPLAY_ON("x-nemo-display-on"); // bool const QString HINT_VISIBILITY("x-nemo-visibility"); // QString const QString HINT_FEEDBACK("x-nemo-feedback"); // QString const QString HINT_PRIORITY("x-nemo-priority"); // int const QString VISIBILITY_PUBLIC("public"); } class NotificationManager::ChatInfo { public: ChatInfo(const QVariantMap &info); void setChatInfo(const QVariantMap &info); public: TDLibWrapper::ChatType type; bool isChannel; QString title; }; NotificationManager::ChatInfo::ChatInfo(const QVariantMap &chatInfo) { setChatInfo(chatInfo); } void NotificationManager::ChatInfo::setChatInfo(const QVariantMap &chatInfo) { const QVariantMap chatTypeInformation = chatInfo.value(TYPE).toMap(); type = TDLibWrapper::chatTypeFromString(chatTypeInformation.value(_TYPE).toString()); isChannel = chatTypeInformation.value(IS_CHANNEL).toBool(); title = chatInfo.value(TITLE).toString(); } class NotificationManager::NotificationGroup { public: NotificationGroup(int groupId, qlonglong chatId, int count, Notification *notification); NotificationGroup(Notification *notification); ~NotificationGroup(); public: int notificationGroupId; qlonglong chatId; int totalCount; Notification *nemoNotification; QMap activeNotifications; QList notificationOrder; }; NotificationManager::NotificationGroup::NotificationGroup(int group, qlonglong chat, int count, Notification *notification) : notificationGroupId(group), chatId(chat), totalCount(count), nemoNotification(notification) { } NotificationManager::NotificationGroup::~NotificationGroup() { delete nemoNotification; } NotificationManager::NotificationManager(TDLibWrapper *tdLibWrapper, AppSettings *appSettings, MceInterface *mceInterface, ChatModel *chatModel) : appIconFile(SailfishApp::pathTo("images/fernschreiber-notification.png").toLocalFile()) { LOG("Initializing..."); this->tdLibWrapper = tdLibWrapper; this->appSettings = appSettings; this->mceInterface = mceInterface; this->chatModel = chatModel; connect(this->tdLibWrapper, SIGNAL(activeNotificationsUpdated(QVariantList)), this, SLOT(handleUpdateActiveNotifications(QVariantList))); connect(this->tdLibWrapper, SIGNAL(notificationGroupUpdated(QVariantMap)), this, SLOT(handleUpdateNotificationGroup(QVariantMap))); connect(this->tdLibWrapper, SIGNAL(notificationUpdated(QVariantMap)), this, SLOT(handleUpdateNotification(QVariantMap))); connect(this->tdLibWrapper, SIGNAL(newChatDiscovered(QString, QVariantMap)), this, SLOT(handleChatDiscovered(QString, QVariantMap))); connect(this->tdLibWrapper, SIGNAL(chatTitleUpdated(QString, QString)), this, SLOT(handleChatTitleUpdated(QString, QString))); this->controlLedNotification(false); // Restore notifications QList notifications = Notification::notifications(); const int n = notifications.count(); LOG("Found" << n << "existing notifications"); for (int i = 0; i < n; i++) { QObject *notificationObject = notifications.at(i); Notification *notification = qobject_cast(notificationObject); if (notification) { bool groupOk, chatOk, countOk; const int groupId = notification->hintValue(HINT_GROUP_ID).toInt(&groupOk); const qlonglong chatId = notification->hintValue(HINT_CHAT_ID).toLongLong(&chatOk); const int totalCount = notification->hintValue(HINT_TOTAL_COUNT).toInt(&countOk); if (groupOk && chatOk && countOk && !notificationGroups.contains(groupId)) { LOG("Restoring notification group" << groupId << "chatId" << chatId << "count" << totalCount); notificationGroups.insert(groupId, new NotificationGroup(groupId, chatId, totalCount, notification)); continue; } } delete notificationObject; } } NotificationManager::~NotificationManager() { LOG("Destroying myself..."); qDeleteAll(chatMap.values()); qDeleteAll(notificationGroups.values()); } void NotificationManager::handleUpdateActiveNotifications(const QVariantList ¬ificationGroups) { const int n = notificationGroups.size(); LOG("Received active notifications, number of groups:" << n); for (int i = 0; i < n; i++) { const QVariantMap notificationGroupInfo(notificationGroups.at(i).toMap()); updateNotificationGroup(notificationGroupInfo.value(ID).toInt(), notificationGroupInfo.value(CHAT_ID).toLongLong(), notificationGroupInfo.value(TOTAL_COUNT).toInt(), notificationGroupInfo.value(NOTIFICATIONS).toList()); } } void NotificationManager::handleUpdateNotificationGroup(const QVariantMap ¬ificationGroupUpdate) { const int notificationGroupId = notificationGroupUpdate.value(NOTIFICATION_GROUP_ID).toInt(); const int totalCount = notificationGroupUpdate.value(TOTAL_COUNT).toInt(); LOG("Received notification group update, group ID:" << notificationGroupId << "total count" << totalCount); updateNotificationGroup(notificationGroupId, notificationGroupUpdate.value(CHAT_ID).toLongLong(), totalCount, notificationGroupUpdate.value(ADDED_NOTIFICATIONS).toList(), notificationGroupUpdate.value(REMOVED_NOTIFICATION_IDS).toList(), appSettings->notificationFeedback()); } void NotificationManager::updateNotificationGroup(int groupId, qlonglong chatId, int totalCount, const QVariantList &addedNotifications, const QVariantList & removedNotificationIds, AppSettings::NotificationFeedback feedback) { bool needFeedback = false; NotificationGroup* notificationGroup = notificationGroups.value(groupId); LOG("Received notification group update, group ID:" << groupId << "total count" << totalCount); if (totalCount) { if (notificationGroup) { // Notification group already exists notificationGroup->totalCount = totalCount; } else { // New notification Notification *notification = new Notification(this); notification->setCategory("x-nemo.messaging.im"); notification->setAppName(APP_NAME); notification->setAppIcon(appIconFile); notification->setIcon(appIconFile); notification->setHintValue(HINT_GROUP_ID, groupId); notification->setHintValue(HINT_CHAT_ID, chatId); notification->setHintValue(HINT_TOTAL_COUNT, totalCount); notification->setHintValue(HINT_FEEDBACK, "chat_exists"); notification->setHintValue(HINT_PRIORITY, 120); notificationGroups.insert(groupId, notificationGroup = new NotificationGroup(groupId, chatId, totalCount, notification)); } QListIterator addedNotificationIterator(addedNotifications); while (addedNotificationIterator.hasNext()) { const QVariantMap addedNotification = addedNotificationIterator.next().toMap(); const int addedId = addedNotification.value(ID).toInt(); notificationGroup->activeNotifications.insert(addedId, addedNotification); notificationGroup->notificationOrder.append(addedId); } QListIterator removedNotificationIdsIterator(removedNotificationIds); while (removedNotificationIdsIterator.hasNext()) { const int removedId = removedNotificationIdsIterator.next().toInt(); notificationGroup->activeNotifications.remove(removedId); notificationGroup->notificationOrder.removeOne(removedId); } // Make sure that if there's no notifications, order is empty too. // That's usually already the case but double-check won't wort. It's cheap. if (notificationGroup->activeNotifications.isEmpty()) { notificationGroup->notificationOrder.clear(); } // Decide if we need a bzzz switch (feedback) { case AppSettings::NotificationFeedbackNone: break; case AppSettings::NotificationFeedbackNew: // Non-zero replacesId means that notification has already been published needFeedback = !notificationGroup->nemoNotification->replacesId(); break; case AppSettings::NotificationFeedbackAll: // Even in this case don't alert the user just about removals needFeedback = !addedNotifications.isEmpty(); break; } // Publish new or update the existing notification LOG("Feedback" << needFeedback); publishNotification(notificationGroup, needFeedback); } else if (notificationGroup) { // No active notifications left in this group notificationGroup->nemoNotification->close(); notificationGroups.remove(groupId); delete notificationGroup; } if (notificationGroups.isEmpty()) { // No active notifications left at all controlLedNotification(false); } else if (needFeedback) { controlLedNotification(true); } } void NotificationManager::handleUpdateNotification(const QVariantMap &updatedNotification) { LOG("Received notification update, group ID:" << updatedNotification.value(NOTIFICATION_GROUP_ID).toInt()); } void NotificationManager::handleChatDiscovered(const QString &chatId, const QVariantMap &chatInformation) { const qlonglong id = chatId.toLongLong(); ChatInfo *chat = chatMap.value(id); if (chat) { chat->setChatInfo(chatInformation); LOG("Updated chat information" << id << chat->title); } else { chat = new ChatInfo(chatInformation); chatMap.insert(id, chat); LOG("New chat" << id << chat->title); } } void NotificationManager::handleChatTitleUpdated(const QString &chatId, const QString &title) { const qlonglong id = chatId.toLongLong(); ChatInfo *chat = chatMap.value(id); if (chat) { LOG("Chat" << id << "title changed to" << title); chat->title = title; // Silently update notification summary QListIterator groupsIterator(notificationGroups.values()); while (groupsIterator.hasNext()) { const NotificationGroup *group = groupsIterator.next(); if (group->chatId == id) { LOG("Updating summary for group ID" << group->notificationGroupId); publishNotification(group, false); break; } } } } void NotificationManager::publishNotification(const NotificationGroup *notificationGroup, bool needFeedback) { QVariantMap messageMap; const ChatInfo *chatInformation = chatMap.value(notificationGroup->chatId); if (!notificationGroup->notificationOrder.isEmpty()) { const int lastNotificationId = notificationGroup->notificationOrder.last(); const QVariantMap lastNotification(notificationGroup->activeNotifications.value(lastNotificationId)); messageMap = lastNotification.value(TYPE).toMap().value(MESSAGE).toMap(); } Notification *nemoNotification = notificationGroup->nemoNotification; if (!messageMap.isEmpty()) { nemoNotification->setTimestamp(QDateTime::fromMSecsSinceEpoch(messageMap.value(DATE).toLongLong() * 1000)); QVariantList remoteActionArguments; remoteActionArguments.append(QString::number(notificationGroup->chatId)); remoteActionArguments.append(messageMap.value(ID).toString()); nemoNotification->setRemoteAction(Notification::remoteAction("default", "openMessage", "de.ygriega.fernschreiber", "/de/ygriega/fernschreiber", "de.ygriega.fernschreiber", "openMessage", remoteActionArguments)); } QString notificationBody; const QVariantMap senderInformation = messageMap.value(SENDER_ID).toMap(); if (notificationGroup->totalCount == 1 && !messageMap.isEmpty()) { LOG("Group" << notificationGroup->notificationGroupId << "has 1 notification"); if (chatInformation && (chatInformation->type == TDLibWrapper::ChatTypeBasicGroup || (chatInformation->type == TDLibWrapper::ChatTypeSupergroup && !chatInformation->isChannel))) { // Add author QString fullName; if (senderInformation.value(_TYPE).toString() == "messageSenderChat") { fullName = tdLibWrapper->getChat(senderInformation.value(CHAT_ID).toString()).value(TITLE).toString(); } else { fullName = FernschreiberUtils::getUserName(tdLibWrapper->getUserInformation(senderInformation.value(USER_ID).toString())); } notificationBody = notificationBody + fullName.trimmed() + ": "; } notificationBody += FernschreiberUtils::getMessageShortText(tdLibWrapper, messageMap.value(CONTENT).toMap(), (chatInformation ? chatInformation->isChannel : false), tdLibWrapper->getUserInformation().value(ID).toLongLong(), senderInformation ); nemoNotification->setBody(notificationBody); } else { // Either we have more than one notification or we have no content to display LOG("Group" << notificationGroup->notificationGroupId << "has" << notificationGroup->totalCount << "notifications"); notificationBody = tr("%Ln unread messages", "", notificationGroup->totalCount); } const QString summary(chatInformation ? chatInformation->title : QString()); nemoNotification->setBody(notificationBody); nemoNotification->setSummary(summary); nemoNotification->setHintValue(HINT_VIBRA, needFeedback); nemoNotification->setHintValue(HINT_IMAGE_PATH, QString()); // Don't show popup for the currently open chat if (!needFeedback || (chatModel->getChatId() == notificationGroup->chatId && qGuiApp->applicationState() == Qt::ApplicationActive)) { nemoNotification->setHintValue(HINT_SUPPRESS_SOUND, true); nemoNotification->setHintValue(HINT_DISPLAY_ON, false); nemoNotification->setHintValue(HINT_VISIBILITY, QString()); nemoNotification->setUrgency(Notification::Low); } else { nemoNotification->setPreviewBody(notificationBody); nemoNotification->setPreviewSummary(summary); nemoNotification->setHintValue(HINT_SUPPRESS_SOUND, !appSettings->notificationSoundsEnabled()); nemoNotification->setHintValue(HINT_DISPLAY_ON, appSettings->notificationTurnsDisplayOn()); nemoNotification->setHintValue(HINT_VISIBILITY, VISIBILITY_PUBLIC); nemoNotification->setUrgency(Notification::Normal); } nemoNotification->publish(); } void NotificationManager::controlLedNotification(bool enabled) { static const QString PATTERN("PatternCommunicationIM"); if (enabled) { mceInterface->ledPatternActivate(PATTERN); } else { mceInterface->ledPatternDeactivate(PATTERN); } }