From df044dbd839c9fbc7a39c7b0c687f5065f519931 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 14 Jan 2022 16:41:29 +0300 Subject: [PATCH] Show local notifications about contact reactions. --- Telegram/Resources/langs/lang.strings | 18 +- Telegram/SourceFiles/history/history.cpp | 33 +- Telegram/SourceFiles/history/history.h | 20 +- Telegram/SourceFiles/history/history_item.cpp | 38 ++ Telegram/SourceFiles/history/history_item.h | 4 +- .../SourceFiles/history/history_message.cpp | 2 +- .../window/notifications_manager.cpp | 525 ++++++++++++------ .../window/notifications_manager.h | 83 ++- .../window/notifications_manager_default.cpp | 42 +- .../window/notifications_manager_default.h | 9 +- 10 files changed, 564 insertions(+), 210 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 6a9dea937..b62441914 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -348,6 +348,23 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_notification_sample" = "This is a sample notification"; "lng_notification_reminder" = "Reminder"; +"lng_reaction_text" = "{reaction} to your «{text}»"; +"lng_reaction_notext" = "{reaction} to your message"; +"lng_reaction_photo" = "{reaction} to your photo"; +"lng_reaction_video" = "{reaction} to your video"; +"lng_reaction_video_message" = "{reaction} to your video message"; +"lng_reaction_document" = "{reaction} to your file"; +"lng_reaction_sticker" = "{reaction} to your {emoji}sticker"; +"lng_reaction_voice_message" = "{reaction} to your voice message"; +"lng_reaction_contact" = "{reaction} to your contact {name}"; +"lng_reaction_location" = "{reaction} to your map"; +"lng_reaction_live_location" = "{reaction} to your live location"; +"lng_reaction_poll" = "{reaction} to your poll {title}"; +"lng_reaction_quiz" = "{reaction} to your quiz {title}"; +"lng_reaction_game" = "{reaction} to your game"; +"lng_reaction_invoice" = "{reaction} to your invoice"; +"lng_reaction_gif" = "{reaction} to your GIF"; + "lng_settings_section_general" = "General"; "lng_settings_change_lang" = "Change language"; "lng_languages" = "Languages"; @@ -1825,7 +1842,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_forward_share_contact" = "Share contact to {recipient}?"; "lng_forward_share_cant" = "Sorry, no way to share contact here :("; - "lng_forward_send_files_cant" = "Sorry, no way to send media here :("; "lng_forward_send" = "Send"; "lng_forward_messages#one" = "{count} forwarded message"; diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index 49534db40..159c2cd5a 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -97,14 +97,14 @@ int History::height() const { void History::removeNotification(not_null item) { _notifications.erase( - ranges::remove(_notifications, item), + ranges::remove(_notifications, item, &ItemNotification::item), end(_notifications)); } -HistoryItem *History::currentNotification() { +auto History::currentNotification() const -> std::optional { return empty(_notifications) - ? nullptr - : _notifications.front().get(); + ? std::nullopt + : std::make_optional(_notifications.front()); } bool History::hasNotification() const { @@ -117,8 +117,12 @@ void History::skipNotification() { } } -void History::popNotification(HistoryItem *item) { - if (!empty(_notifications) && (_notifications.back() == item)) { +void History::pushNotification(ItemNotification notification) { + _notifications.push_back(notification); +} + +void History::popNotification(ItemNotification notification) { + if (!empty(_notifications) && (_notifications.back() == notification)) { _notifications.pop_back(); } } @@ -1146,13 +1150,17 @@ void History::newItemAdded(not_null item) { from->madeAction(item->date()); } item->contributeToSlowmode(); + auto notification = ItemNotification{ + item, + ItemNotificationType::Message, + }; if (item->showNotification()) { - _notifications.push_back(item); + pushNotification(notification); } owner().notifyNewItemAdded(item); const auto stillShow = item->showNotification(); // Could be read already. if (stillShow) { - Core::App().notifications().schedule(item); + Core::App().notifications().schedule(notification); if (!item->out() && item->unread()) { if (unreadCountKnown()) { setUnreadCount(unreadCount() + 1); @@ -2147,8 +2155,11 @@ void History::clearNotifications() { void History::clearIncomingNotifications() { if (!peer->isSelf()) { + const auto proj = [](ItemNotification notification) { + return notification.item->out(); + }; _notifications.erase( - ranges::remove(_notifications, false, &HistoryItem::out), + ranges::remove(_notifications, false, proj), end(_notifications)); } } @@ -2983,7 +2994,9 @@ void History::reactionsEnabledChanged(bool enabled) { item->updateReactions(nullptr); } } else { - + for (const auto &item : _messages) { + item->updateReactionsUnknown(); + } } } diff --git a/Telegram/SourceFiles/history/history.h b/Telegram/SourceFiles/history/history.h index bae332352..122b22e46 100644 --- a/Telegram/SourceFiles/history/history.h +++ b/Telegram/SourceFiles/history/history.h @@ -76,6 +76,19 @@ enum class UnreadMentionType { Existing, // when some messages slice was received }; +enum class ItemNotificationType { + Message, + Reaction, +}; +struct ItemNotification { + not_null item; + ItemNotificationType type = ItemNotificationType::Message; + + friend inline bool operator==(ItemNotification a, ItemNotification b) { + return (a.item == b.item) && (a.type == b.type); + } +}; + class History final : public Dialogs::Entry { public: using Element = HistoryView::Element; @@ -304,10 +317,11 @@ public: void itemRemoved(not_null item); void itemVanished(not_null item); - HistoryItem *currentNotification(); + [[nodiscard]] std::optional currentNotification() const; bool hasNotification() const; void skipNotification(); - void popNotification(HistoryItem *item); + void pushNotification(ItemNotification notification); + void popNotification(ItemNotification notification); bool hasPendingResizedItems() const; void setHasPendingResizedItems(); @@ -645,7 +659,7 @@ private: HistoryView::SendActionPainter _sendActionPainter; - std::deque> _notifications; + std::deque _notifications; }; diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 94095aaf3..8098f9b5e 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -808,6 +808,26 @@ void HistoryItem::toggleReaction(const QString &reaction) { } void HistoryItem::updateReactions(const MTPMessageReactions *reactions) { + const auto history = this->history(); + const auto toUser = (reactions && out()) + ? history->peer->asUser() + : nullptr; + const auto toContact = toUser && toUser->isContact(); + const auto maybeNotify = toContact && lookupHisReaction().isEmpty(); + setReactions(reactions); + if (maybeNotify) { + if (const auto reaction = lookupHisReaction(); !reaction.isEmpty()) { + const auto notification = ItemNotification{ + this, + ItemNotificationType::Reaction, + }; + history->pushNotification(notification); + Core::App().notifications().schedule(notification); + } + } +} + +void HistoryItem::setReactions(const MTPMessageReactions *reactions) { if (reactions || _reactionsLastRefreshed) { _reactionsLastRefreshed = crl::now(); } @@ -868,6 +888,24 @@ QString HistoryItem::chosenReaction() const { return _reactions ? _reactions->chosen() : QString(); } +QString HistoryItem::lookupHisReaction() const { + if (!_reactions) { + return QString(); + } + const auto &list = _reactions->list(); + if (list.empty()) { + return QString(); + } + const auto chosen = _reactions->chosen(); + const auto &[first, count] = list.front(); + if (chosen.isEmpty() || first != chosen || count > 1) { + return first; + } else if (list.size() == 1) { + return QString(); + } + return list.back().first; +} + crl::time HistoryItem::lastReactionsRefreshTime() const { return _reactionsLastRefreshed; } diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index b2203f355..28ea5bc30 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -363,6 +363,7 @@ public: -> const base::flat_map>> &; [[nodiscard]] bool canViewReactions() const; [[nodiscard]] QString chosenReaction() const; + [[nodiscard]] QString lookupHisReaction() const; [[nodiscard]] crl::time lastReactionsRefreshTime() const; [[nodiscard]] bool hasDirectLink() const; @@ -442,6 +443,8 @@ protected: void finishEdition(int oldKeyboardTop); void finishEditionToEmpty(); + void setReactions(const MTPMessageReactions *reactions); + const not_null _history; const not_null _from; MessageFlags _flags = 0; @@ -469,7 +472,6 @@ protected: crl::time _reactionsLastRefreshed = 0; private: - TimeId _date = 0; TimeId _ttlDestroyAt = 0; diff --git a/Telegram/SourceFiles/history/history_message.cpp b/Telegram/SourceFiles/history/history_message.cpp index a2400f2b6..ab004089e 100644 --- a/Telegram/SourceFiles/history/history_message.cpp +++ b/Telegram/SourceFiles/history/history_message.cpp @@ -512,7 +512,7 @@ HistoryMessage::HistoryMessage( MessageGroupId::FromRaw(history->peer->id, groupedId->v)); } if (const auto reactions = data.vreactions()) { - updateReactions(reactions); + setReactions(reactions); } applyTTL(data); diff --git a/Telegram/SourceFiles/window/notifications_manager.cpp b/Telegram/SourceFiles/window/notifications_manager.cpp index 950496eb0..1ffd7e7cf 100644 --- a/Telegram/SourceFiles/window/notifications_manager.cpp +++ b/Telegram/SourceFiles/window/notifications_manager.cpp @@ -18,6 +18,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_session.h" #include "data/data_channel.h" #include "data/data_user.h" +#include "data/data_document.h" +#include "data/data_poll.h" #include "base/unixtime.h" #include "window/window_controller.h" #include "window/window_session_controller.h" @@ -28,6 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_account.h" #include "main/main_session.h" #include "main/main_domain.h" +#include "ui/text/text_utilities.h" #include "facades.h" #include "app.h" @@ -38,8 +41,11 @@ namespace Notifications { namespace { // not more than one sound in 500ms from one peer - grouping +constexpr auto kMinimalDelay = crl::time(100); +constexpr auto kMinimalForwardDelay = crl::time(500); constexpr auto kMinimalAlertDelay = crl::time(500); constexpr auto kWaitingForAllGroupedDelay = crl::time(1000); +constexpr auto kReactionNotificationEach = 60 * 60 * crl::time(1000); #ifdef Q_OS_MAC constexpr auto kSystemAlertDuration = crl::time(1000); @@ -63,6 +69,18 @@ QString TextWithPermanentSpoiler(const TextWithEntities &textWithEntities) { } // namespace +System::NotificationInHistoryKey::NotificationInHistoryKey( + ItemNotification notification) +: NotificationInHistoryKey(notification.item->id, notification.type) { +} + +System::NotificationInHistoryKey::NotificationInHistoryKey( + MsgId messageId, + ItemNotificationType type) +: messageId(messageId) +, type(type) { +} + System::System() : _waitTimer([=] { showNext(); }) , _waitForAllGroupedTimer([=] { showGrouped(); }) { @@ -108,13 +126,55 @@ Main::Session *System::findSession(uint64 sessionId) const { return nullptr; } +bool System::skipReactionNotification(not_null item) const { + const auto id = ReactionNotificationId{ + .itemId = item->fullId(), + .sessionId = item->history()->session().uniqueId(), + }; + const auto now = crl::now(); + const auto clearBefore = now - kReactionNotificationEach; + for (auto i = begin(_sentReactionNotifications) + ; i != end(_sentReactionNotifications) + ;) { + if (i->second <= clearBefore) { + i = _sentReactionNotifications.erase(i); + } else { + ++i; + } + } + return !_sentReactionNotifications.emplace(id, now).second; +} + System::SkipState System::skipNotification( - not_null item) const { + ItemNotification notification) const { + const auto item = notification.item; + const auto type = notification.type; + const auto messageNotification = (type == ItemNotificationType::Message); + if (!item->history()->currentNotification() + || (messageNotification && item->skipNotification()) + || (type == ItemNotificationType::Reaction + && skipReactionNotification(item))) { + return { SkipState::Skip }; + } + const auto showForMuted = messageNotification + && item->out() + && item->isFromScheduled(); + auto result = computeSkipState(item, showForMuted); + if (showForMuted) { + result.alsoMuted = true; + } + if (messageNotification && item->isSilent()) { + result.silent = true; + } + return result; +} + +System::SkipState System::computeSkipState( + not_null item, + bool showForMuted) const { const auto history = item->history(); const auto notifyBy = item->specialNotificationPeer(); - if (App::quitting() - || !history->currentNotification() - || item->skipNotification()) { + if (App::quitting()) { return { SkipState::Skip }; } else if (!Core::App().settings().notifyFromAll() && &history->session().account() != &Core::App().domain().active()) { @@ -126,15 +186,14 @@ System::SkipState System::skipNotification( history->owner().requestNotifySettings(notifyBy); } - const auto scheduled = item->out() && item->isFromScheduled(); if (history->owner().notifyMuteUnknown(history->peer)) { - return { SkipState::Unknown, item->isSilent() }; + return { SkipState::Unknown }; } else if (!history->owner().notifyIsMuted(history->peer)) { - return { SkipState::DontSkip, item->isSilent() }; + return { SkipState::DontSkip }; } else if (!notifyBy) { return { - scheduled ? SkipState::DontSkip : SkipState::Skip, - item->isSilent() || scheduled + showForMuted ? SkipState::DontSkip : SkipState::Skip, + showForMuted, }; } else if (history->owner().notifyMuteUnknown(notifyBy)) { return { SkipState::Unknown, item->isSilent() }; @@ -142,26 +201,16 @@ System::SkipState System::skipNotification( return { SkipState::DontSkip, item->isSilent() }; } else { return { - scheduled ? SkipState::DontSkip : SkipState::Skip, - item->isSilent() || scheduled + showForMuted ? SkipState::DontSkip : SkipState::Skip, + showForMuted, }; } } -void System::schedule(not_null item) { - Expects(_manager != nullptr); - - const auto history = item->history(); - const auto skip = skipNotification(item); - if (skip.value == SkipState::Skip) { - history->popNotification(item); - return; - } - const auto notifyBy = item->specialNotificationPeer(); - const auto ready = (skip.value != SkipState::Unknown) - && item->notificationReady(); - - auto delay = item->Has() ? 500 : 100; +System::Timing System::countTiming( + not_null history, + crl::time minimalDelay) const { + auto delay = minimalDelay; const auto t = base::unixtime::now(); const auto ms = crl::now(); const auto &updates = history->session().updates(); @@ -174,33 +223,62 @@ void System::schedule(not_null item) { } else if (cOtherOnline() >= t) { delay = config.notifyDefaultDelay; } + return { + .delay = delay, + .when = ms + delay, + }; +} - auto when = ms + delay; +void System::schedule(ItemNotification notification) { + Expects(_manager != nullptr); + + const auto item = notification.item; + const auto type = notification.type; + const auto history = item->history(); + const auto skip = skipNotification(notification); + if (skip.value == SkipState::Skip) { + history->popNotification(notification); + return; + } + const auto ready = (skip.value != SkipState::Unknown) + && item->notificationReady(); + + const auto minimalDelay = (type == ItemNotificationType::Reaction) + ? kMinimalDelay + : item->Has() + ? kMinimalForwardDelay + : kMinimalDelay; + const auto timing = countTiming(history, minimalDelay); + const auto notifyBy = item->specialNotificationPeer(); if (!skip.silent) { - _whenAlerts[history].emplace(when, notifyBy); + _whenAlerts[history].emplace(timing.when, notifyBy); } if (Core::App().settings().desktopNotify() && !_manager->skipToast()) { + const auto key = NotificationInHistoryKey(notification); auto &whenMap = _whenMaps[history]; - if (whenMap.find(item->id) == whenMap.end()) { - whenMap.emplace(item->id, when); + if (whenMap.find(key) == whenMap.end()) { + whenMap.emplace(key, timing.when); } auto &addTo = ready ? _waiters : _settingWaiters; const auto it = addTo.find(history); - if (it == addTo.end() || it->second.when > when) { + if (it == addTo.end() || it->second.when > timing.when) { addTo.emplace(history, Waiter{ - .msg = item->id, - .when = when, - .notifyBy = notifyBy + .key = key, + .when = timing.when, + .notifyBy = notifyBy, + .alsoMuted = skip.alsoMuted, }); } } if (ready) { - if (!_waitTimer.isActive() || _waitTimer.remainingTime() > delay) { - _waitTimer.callOnce(delay); + if (!_waitTimer.isActive() + || _waitTimer.remainingTime() > timing.delay) { + _waitTimer.callOnce(timing.delay); } } + } void System::clearAll() { @@ -292,6 +370,7 @@ void System::checkDelayed() { for (auto i = _settingWaiters.begin(); i != _settingWaiters.end();) { const auto history = i->first; const auto peer = history->peer; + auto notifyMuted = i->second.alsoMuted; auto loaded = false; auto muted = false; if (!peer->owner().notifyMuteUnknown(peer)) { @@ -310,7 +389,9 @@ void System::checkDelayed() { } } if (loaded) { - const auto fullId = FullMsgId(history->peer->id, i->second.msg); + const auto fullId = FullMsgId( + history->peer->id, + i->second.key.messageId); if (const auto item = peer->owner().message(fullId)) { if (!item->notificationReady()) { loaded = false; @@ -320,7 +401,7 @@ void System::checkDelayed() { } } if (loaded) { - if (!muted) { + if (!muted || i->second.alsoMuted) { _waiters.emplace(i->first, i->second); } i = _settingWaiters.erase(i); @@ -338,7 +419,10 @@ void System::showGrouped() { if (const auto session = findSession(_lastHistorySessionId)) { if (const auto lastItem = session->data().message(_lastHistoryItemId)) { _waitForAllGroupedTimer.cancel(); - _manager->showNotification(lastItem, _lastForwardedCount); + _manager->showNotification({ + .item = lastItem, + .forwardedCount = _lastForwardedCount, + }); _lastForwardedCount = 0; _lastHistoryItemId = FullMsgId(); _lastHistorySessionId = 0; @@ -426,11 +510,12 @@ void System::showNext() { while (true) { auto next = 0LL; - HistoryItem *notifyItem = nullptr; - History *notifyHistory = nullptr; + auto notify = std::optional(); + auto notifyHistory = (History*)nullptr; for (auto i = _waiters.begin(); i != _waiters.end();) { const auto history = i->first; - if (history->currentNotification() && history->currentNotification()->id != i->second.msg) { + auto current = history->currentNotification(); + if (current && current->item->id != i->second.key.messageId) { auto j = _whenMaps.find(history); if (j == _whenMaps.end()) { history->clearNotifications(); @@ -438,131 +523,156 @@ void System::showNext() { continue; } do { - auto k = j->second.find(history->currentNotification()->id); + auto k = j->second.find(*current); if (k != j->second.cend()) { - i->second.msg = k->first; + i->second.key = k->first; i->second.when = k->second; break; } history->skipNotification(); - } while (history->currentNotification()); + current = history->currentNotification(); + } while (current); } - if (!history->currentNotification()) { + if (!current) { _whenMaps.remove(history); i = _waiters.erase(i); continue; } auto when = i->second.when; - if (!notifyItem || next > when) { + if (!notify || next > when) { next = when; - notifyItem = history->currentNotification(); + notify = current, notifyHistory = history; } ++i; } - if (notifyItem) { - if (next > ms) { - if (nextAlert && nextAlert < next) { - next = nextAlert; - nextAlert = 0; - } - _waitTimer.callOnce(next - ms); - break; - } else { - const auto isForwarded = notifyItem->Has(); - const auto isAlbum = notifyItem->groupId(); + if (!notify) { + break; + } else if (next > ms) { + if (nextAlert && nextAlert < next) { + next = nextAlert; + nextAlert = 0; + } + _waitTimer.callOnce(next - ms); + break; + } + const auto notifyItem = notify->item; + const auto messageNotification = (notify->type + == ItemNotificationType::Message); + const auto isForwarded = messageNotification + && notifyItem->Has(); + const auto isAlbum = messageNotification + && notifyItem->groupId(); - auto groupedItem = (isForwarded || isAlbum) ? notifyItem : nullptr; // forwarded and album notify grouping - auto forwardedCount = isForwarded ? 1 : 0; - - const auto history = notifyItem->history(); - const auto j = _whenMaps.find(history); - if (j == _whenMaps.cend()) { - history->clearNotifications(); - } else { - auto nextNotify = (HistoryItem*)nullptr; - do { - history->skipNotification(); - if (!history->hasNotification()) { - break; - } - - j->second.remove((groupedItem ? groupedItem : notifyItem)->id); - do { - const auto k = j->second.find(history->currentNotification()->id); - if (k != j->second.cend()) { - nextNotify = history->currentNotification(); - _waiters.emplace(notifyHistory, Waiter{ - .msg = k->first, - .when = k->second - }); - break; - } - history->skipNotification(); - } while (history->hasNotification()); - if (nextNotify) { - if (groupedItem) { - const auto canNextBeGrouped = (isForwarded && nextNotify->Has()) - || (isAlbum && nextNotify->groupId()); - const auto nextItem = canNextBeGrouped ? nextNotify : nullptr; - if (nextItem - && qAbs(int64(nextItem->date()) - int64(groupedItem->date())) < 2) { - if (isForwarded - && groupedItem->author() == nextItem->author()) { - ++forwardedCount; - groupedItem = nextItem; - continue; - } - if (isAlbum - && groupedItem->groupId() == nextItem->groupId()) { - groupedItem = nextItem; - continue; - } - } - } - nextNotify = nullptr; - } - } while (nextNotify); - } - - if (!_lastHistoryItemId && groupedItem) { - _lastHistorySessionId = groupedItem->history()->session().uniqueId(); - _lastHistoryItemId = groupedItem->fullId(); - } - - // If the current notification is grouped. - if (isAlbum || isForwarded) { - // If the previous notification is grouped - // then reset the timer. - if (_waitForAllGroupedTimer.isActive()) { - _waitForAllGroupedTimer.cancel(); - // If this is not the same group - // then show the previous group immediately. - if (!isSameGroup(groupedItem)) { - showGrouped(); - } - } - // We have to wait until all the messages in this group are loaded. - _lastForwardedCount += forwardedCount; - _lastHistorySessionId = groupedItem->history()->session().uniqueId(); - _lastHistoryItemId = groupedItem->fullId(); - _waitForAllGroupedTimer.callOnce(kWaitingForAllGroupedDelay); - } else { - // If the current notification is not grouped - // then there is no reason to wait for the timer - // to show the previous notification. - showGrouped(); - _manager->showNotification(notifyItem, forwardedCount); - } + // Forwarded and album notify grouping. + auto groupedItem = (isForwarded || isAlbum) + ? notifyItem.get() + : nullptr; + auto forwardedCount = isForwarded ? 1 : 0; + const auto history = notifyItem->history(); + const auto j = _whenMaps.find(history); + if (j == _whenMaps.cend()) { + history->clearNotifications(); + } else { + while (true) { + auto nextNotify = std::optional(); + history->skipNotification(); if (!history->hasNotification()) { - _waiters.remove(history); - _whenMaps.remove(history); - continue; + break; + } + + j->second.remove({ + (groupedItem ? groupedItem : notifyItem.get())->id, + notify->type, + }); + do { + const auto k = j->second.find( + history->currentNotification()); + if (k != j->second.cend()) { + nextNotify = history->currentNotification(); + _waiters.emplace(notifyHistory, Waiter{ + .key = k->first, + .when = k->second + }); + break; + } + history->skipNotification(); + } while (history->hasNotification()); + if (!nextNotify || !groupedItem) { + break; + } + const auto nextMessageNotification + = (nextNotify->type + == ItemNotificationType::Message); + const auto canNextBeGrouped = nextMessageNotification + && ((isForwarded && nextNotify->item->Has()) + || (isAlbum && nextNotify->item->groupId())); + const auto nextItem = canNextBeGrouped + ? nextNotify->item.get() + : nullptr; + if (nextItem + && qAbs(int64(nextItem->date()) - int64(groupedItem->date())) < 2) { + if (isForwarded + && groupedItem->author() == nextItem->author()) { + ++forwardedCount; + groupedItem = nextItem; + continue; + } + if (isAlbum + && groupedItem->groupId() == nextItem->groupId()) { + groupedItem = nextItem; + continue; + } + } + break; + } + } + + if (!_lastHistoryItemId && groupedItem) { + _lastHistorySessionId = groupedItem->history()->session().uniqueId(); + _lastHistoryItemId = groupedItem->fullId(); + } + + // If the current notification is grouped. + if (isAlbum || isForwarded) { + // If the previous notification is grouped + // then reset the timer. + if (_waitForAllGroupedTimer.isActive()) { + _waitForAllGroupedTimer.cancel(); + // If this is not the same group + // then show the previous group immediately. + if (!isSameGroup(groupedItem)) { + showGrouped(); } } + // We have to wait until all the messages in this group are loaded. + _lastForwardedCount += forwardedCount; + _lastHistorySessionId = groupedItem->history()->session().uniqueId(); + _lastHistoryItemId = groupedItem->fullId(); + _waitForAllGroupedTimer.callOnce(kWaitingForAllGroupedDelay); } else { - break; + // If the current notification is not grouped + // then there is no reason to wait for the timer + // to show the previous notification. + showGrouped(); + const auto reactionNotification + = (notify->type == ItemNotificationType::Reaction); + const auto reaction = reactionNotification + ? notify->item->lookupHisReaction() + : QString(); + if (!reactionNotification || !reaction.isEmpty()) { + _manager->showNotification({ + .item = notify->item, + .forwardedCount = forwardedCount, + .reaction = reaction, + }); + } + } + + if (!history->hasNotification()) { + _waiters.remove(history); + _whenMaps.remove(history); } } if (nextAlert) { @@ -595,17 +705,19 @@ void System::notifySettingsChanged(ChangeType type) { } Manager::DisplayOptions Manager::getNotificationOptions( - HistoryItem *item) const { + HistoryItem *item, + ItemNotificationType type) const { const auto hideEverything = Core::App().passcodeLocked() || forceHideDetails(); - const auto view = Core::App().settings().notifyView(); - DisplayOptions result; + + auto result = DisplayOptions(); result.hideNameAndPhoto = hideEverything || (view > Core::Settings::NotifyView::ShowName); result.hideMessageText = hideEverything || (view > Core::Settings::NotifyView::ShowPreview); result.hideMarkAsRead = result.hideMessageText + || (type != ItemNotificationType::Message) || !item || ((item->out() || item->history()->peer->isSelf()) && item->isFromScheduled()); @@ -616,6 +728,89 @@ Manager::DisplayOptions Manager::getNotificationOptions( return result; } +TextWithEntities Manager::ComposeReactionNotification( + not_null item, + const QString &reaction, + bool hideContent) { + const auto simple = [&](const auto &phrase) { + return TextWithEntities{ phrase(tr::now, lt_reaction, reaction) }; + }; + if (hideContent) { + return simple(tr::lng_reaction_notext); + } + const auto media = item->media(); + const auto text = [&] { + return tr::lng_reaction_text( + tr::now, + lt_reaction, + Ui::Text::WithEntities(reaction), + lt_text, + item->notificationText(), + Ui::Text::WithEntities); + }; + if (!media || media->webpage()) { + return text(); + } else if (media->photo()) { + return simple(tr::lng_reaction_photo); + } else if (const auto document = media->document()) { + if (document->isVoiceMessage()) { + return simple(tr::lng_reaction_voice_message); + } else if (document->isVideoMessage()) { + return simple(tr::lng_reaction_video_message); + } else if (document->isAnimation()) { + return simple(tr::lng_reaction_gif); + } else if (document->isVideoFile()) { + return simple(tr::lng_reaction_video); + } else if (const auto sticker = document->sticker()) { + return { + tr::lng_reaction_sticker( + tr::now, + lt_reaction, + reaction, + lt_emoji, + sticker->alt) + }; + } + return simple(tr::lng_reaction_document); + } else if (const auto contact = media->sharedContact()) { + const auto name = contact->firstName.isEmpty() + ? contact->lastName + : contact->lastName.isEmpty() + ? contact->firstName + : tr::lng_full_name( + tr::now, + lt_first_name, + contact->firstName, + lt_last_name, + contact->lastName); + return { + tr::lng_reaction_contact( + tr::now, + lt_reaction, + reaction, + lt_name, + name) + }; + } else if (media->location()) { + return simple(tr::lng_reaction_location); + // lng_reaction_live_location not used right now :( + } else if (const auto poll = media->poll()) { + return { + (poll->quiz() ? tr::lng_reaction_quiz : tr::lng_reaction_poll)( + tr::now, + lt_reaction, + reaction, + lt_title, + poll->question) + }; + } else if (media->game()) { + return simple(tr::lng_reaction_game); + } else if (media->invoice()) { + return simple(tr::lng_reaction_invoice); + } + return text(); +} + QString Manager::addTargetAccountName( const QString &title, not_null session) { @@ -730,13 +925,20 @@ void Manager::notificationReplied( } } -void NativeManager::doShowNotification( - not_null item, - int forwardedCount) { - const auto options = getNotificationOptions(item); - +void NativeManager::doShowNotification(NotificationFields &&fields) { + const auto options = getNotificationOptions( + fields.item, + (fields.reaction.isEmpty() + ? ItemNotificationType::Message + : ItemNotificationType::Reaction)); + const auto item = fields.item; const auto peer = item->history()->peer; + const auto reaction = fields.reaction; + if (!reaction.isEmpty() && options.hideNameAndPhoto) { + return; + } const auto scheduled = !options.hideNameAndPhoto + && fields.reaction.isEmpty() && (item->out() || peer->isSelf()) && item->isFromScheduled(); const auto title = options.hideNameAndPhoto @@ -745,16 +947,21 @@ void NativeManager::doShowNotification( ? tr::lng_notification_reminder(tr::now) : peer->name; const auto fullTitle = addTargetAccountName(title, &peer->session()); - const auto subtitle = options.hideNameAndPhoto + const auto subtitle = (options.hideNameAndPhoto || !reaction.isEmpty()) ? QString() : item->notificationHeader(); - const auto text = options.hideMessageText + const auto text = !reaction.isEmpty() + ? TextWithPermanentSpoiler(ComposeReactionNotification( + item, + reaction, + options.hideMessageText)) + : options.hideMessageText ? tr::lng_notification_preview(tr::now) - : (forwardedCount < 2 - ? (item->groupId() - ? tr::lng_in_dlg_album(tr::now) - : TextWithPermanentSpoiler(item->notificationText())) - : tr::lng_forward_messages(tr::now, lt_count, forwardedCount)); + : (fields.forwardedCount > 1) + ? tr::lng_forward_messages(tr::now, lt_count, fields.forwardedCount) + : item->groupId() + ? tr::lng_in_dlg_album(tr::now) + : TextWithPermanentSpoiler(item->notificationText()); // #TODO optimize auto userpicView = item->history()->peer->createUserpicView(); diff --git a/Telegram/SourceFiles/window/notifications_manager.h b/Telegram/SourceFiles/window/notifications_manager.h index 26afab1be..5dd643cf7 100644 --- a/Telegram/SourceFiles/window/notifications_manager.h +++ b/Telegram/SourceFiles/window/notifications_manager.h @@ -10,6 +10,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/timer.h" class History; +struct ItemNotification; +enum class ItemNotificationType; namespace Data { class CloudImageView; @@ -84,7 +86,7 @@ public: [[nodiscard]] std::optional managerType() const; void checkDelayed(); - void schedule(not_null item); + void schedule(ItemNotification notification); void clearFromHistory(not_null history); void clearIncomingFromHistory(not_null history); void clearFromSession(not_null session); @@ -109,14 +111,53 @@ private: }; Value value = Value::Unknown; bool silent = false; + bool alsoMuted = false; + }; + struct NotificationInHistoryKey { + NotificationInHistoryKey(ItemNotification notification); + NotificationInHistoryKey(MsgId messageId, ItemNotificationType type); + + MsgId messageId = 0; + ItemNotificationType type = ItemNotificationType(); + + friend inline bool operator<( + NotificationInHistoryKey a, + NotificationInHistoryKey b) { + return std::pair(a.messageId, a.type) + < std::pair(b.messageId, b.type); + } }; struct Waiter { - MsgId msg; - crl::time when; + NotificationInHistoryKey key; + crl::time when = 0; PeerData *notifyBy = nullptr; + bool alsoMuted = false; + }; + struct Timing { + crl::time delay = 0; + crl::time when = 0; + }; + struct ReactionNotificationId { + FullMsgId itemId; + uint64 sessionId = 0; + + friend inline bool operator<( + ReactionNotificationId a, + ReactionNotificationId b) { + return std::pair(a.itemId, a.sessionId) + < std::pair(b.itemId, b.sessionId); + } }; [[nodiscard]] SkipState skipNotification( + ItemNotification notification) const; + [[nodiscard]] SkipState computeSkipState( + not_null item, + bool showForMuted) const; + [[nodiscard]] Timing countTiming( + not_null history, + crl::time minimalDelay) const; + [[nodiscard]] bool skipReactionNotification( not_null item) const; void showNext(); @@ -125,14 +166,20 @@ private: base::flat_map< not_null, - base::flat_map> _whenMaps; + base::flat_map> _whenMaps; base::flat_map, Waiter> _waiters; base::flat_map, Waiter> _settingWaiters; base::Timer _waitTimer; base::Timer _waitForAllGroupedTimer; - base::flat_map, base::flat_map> _whenAlerts; + base::flat_map< + not_null, + base::flat_map> _whenAlerts; + + mutable base::flat_map< + ReactionNotificationId, + crl::time> _sentReactionNotifications; std::unique_ptr _manager; @@ -169,14 +216,17 @@ public: return std::tie(a.full, a.msgId) < std::tie(b.full, b.msgId); } }; + struct NotificationFields { + not_null item; + int forwardedCount = 0; + QString reaction; + }; explicit Manager(not_null system) : _system(system) { } - void showNotification( - not_null item, - int forwardedCount) { - doShowNotification(item, forwardedCount); + void showNotification(NotificationFields fields) { + doShowNotification(std::move(fields)); } void updateAll() { doUpdateAll(); @@ -209,7 +259,12 @@ public: bool hideReplyButton = false; }; [[nodiscard]] DisplayOptions getNotificationOptions( - HistoryItem *item) const; + HistoryItem *item, + ItemNotificationType type) const; + [[nodiscard]] static TextWithEntities ComposeReactionNotification( + not_null item, + const QString &reaction, + bool hideContent); [[nodiscard]] QString addTargetAccountName( const QString &title, @@ -235,9 +290,7 @@ protected: } virtual void doUpdateAll() = 0; - virtual void doShowNotification( - not_null item, - int forwardedCount) = 0; + virtual void doShowNotification(NotificationFields &&fields) = 0; virtual void doClearAll() = 0; virtual void doClearAllFast() = 0; virtual void doClearFromItem(not_null item) = 0; @@ -281,9 +334,7 @@ protected: void doClearAll() override { doClearAllFast(); } - void doShowNotification( - not_null item, - int forwardedCount) override; + void doShowNotification(NotificationFields &&fields) override; bool forceHideDetails() const override; diff --git a/Telegram/SourceFiles/window/notifications_manager_default.cpp b/Telegram/SourceFiles/window/notifications_manager_default.cpp index 174f70ca6..9b5f07194 100644 --- a/Telegram/SourceFiles/window/notifications_manager_default.cpp +++ b/Telegram/SourceFiles/window/notifications_manager_default.cpp @@ -75,15 +75,15 @@ Manager::Manager(System *system) }, _lifetime); } -Manager::QueuedNotification::QueuedNotification( - not_null item, - int forwardedCount) -: history(item->history()) +Manager::QueuedNotification::QueuedNotification(NotificationFields &&fields) +: history(fields.item->history()) , peer(history->peer) -, author(item->notificationHeader()) -, item((forwardedCount < 2) ? item.get() : nullptr) -, forwardedCount(forwardedCount) -, fromScheduled((item->out() || peer->isSelf()) && item->isFromScheduled()) { +, reaction(fields.reaction) +, author(reaction.isEmpty() ? fields.item->notificationHeader() : QString()) +, item((fields.forwardedCount < 2) ? fields.item.get() : nullptr) +, forwardedCount(fields.forwardedCount) +, fromScheduled(reaction.isEmpty() && (fields.item->out() || peer->isSelf()) + && fields.item->isFromScheduled()) { } QPixmap Manager::hiddenUserpicPlaceholder() const { @@ -230,6 +230,7 @@ void Manager::showNextFromQueue() { queued.peer, queued.author, queued.item, + queued.reaction, queued.forwardedCount, queued.fromScheduled, startPosition, @@ -343,10 +344,8 @@ void Manager::removeWidget(internal::Widget *remove) { showNextFromQueue(); } -void Manager::doShowNotification( - not_null item, - int forwardedCount) { - _queuedNotifications.emplace_back(item, forwardedCount); +void Manager::doShowNotification(NotificationFields &&fields) { + _queuedNotifications.emplace_back(std::move(fields)); showNextFromQueue(); } @@ -596,6 +595,7 @@ Notification::Notification( not_null peer, const QString &author, HistoryItem *item, + const QString &reaction, int forwardedCount, bool fromScheduled, QPoint startPosition, @@ -607,6 +607,7 @@ Notification::Notification( , _history(history) , _userpicView(_peer->createUserpicView()) , _author(author) +, _reaction(reaction) , _item(item) , _forwardedCount(forwardedCount) , _fromScheduled(fromScheduled) @@ -746,7 +747,11 @@ void Notification::actionsOpacityCallback() { void Notification::updateNotifyDisplay() { if (!_history || (!_item && _forwardedCount < 2)) return; - const auto options = manager()->getNotificationOptions(_item); + const auto options = manager()->getNotificationOptions( + _item, + (_reaction.isEmpty() + ? ItemNotificationType::Message + : ItemNotificationType::Reaction)); _hideReplyButton = options.hideReplyButton; int32 w = width(), h = height(); @@ -796,7 +801,9 @@ void Notification::updateNotifyDisplay() { } } - if (!options.hideMessageText) { + const auto composeText = !options.hideMessageText + || (!_reaction.isEmpty() && !options.hideNameAndPhoto); + if (composeText) { auto itemTextCache = Ui::Text::String(itemWidth); auto r = QRect( st::notifyPhotoPos.x() + st::notifyPhotoSize + st::notifyTextLeft, @@ -806,7 +813,12 @@ void Notification::updateNotifyDisplay() { p.setTextPalette(st::dialogsTextPalette); p.setPen(st::dialogsTextFg); p.setFont(st::dialogsTextFont); - const auto text = _item + const auto text = !_reaction.isEmpty() + ? Manager::ComposeReactionNotification( + _item, + _reaction, + options.hideMessageText) + : _item ? _item->toPreview({ .hideSender = reminder, .generateImages = false, diff --git a/Telegram/SourceFiles/window/notifications_manager_default.h b/Telegram/SourceFiles/window/notifications_manager_default.h index 9ad491bcc..9816905af 100644 --- a/Telegram/SourceFiles/window/notifications_manager_default.h +++ b/Telegram/SourceFiles/window/notifications_manager_default.h @@ -68,9 +68,7 @@ private: [[nodiscard]] QPixmap hiddenUserpicPlaceholder() const; void doUpdateAll() override; - void doShowNotification( - not_null item, - int forwardedCount) override; + void doShowNotification(NotificationFields &&fields) override; void doClearAll() override; void doClearAllFast() override; void doClearFromHistory(not_null history) override; @@ -110,10 +108,11 @@ private: base::Timer _inputCheckTimer; struct QueuedNotification { - QueuedNotification(not_null item, int forwardedCount); + QueuedNotification(NotificationFields &&fields); not_null history; not_null peer; + QString reaction; QString author; HistoryItem *item = nullptr; int forwardedCount = 0; @@ -208,6 +207,7 @@ public: not_null peer, const QString &author, HistoryItem *item, + const QString &reaction, int forwardedCount, bool fromScheduled, QPoint startPosition, @@ -277,6 +277,7 @@ private: History *_history = nullptr; std::shared_ptr _userpicView; QString _author; + QString _reaction; HistoryItem *_item = nullptr; int _forwardedCount = 0; bool _fromScheduled = false;