From b80f5f970605ed0ed714dd7a7722ef2dfb8fd56d Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 23 Aug 2023 13:20:41 +0200 Subject: [PATCH] Manage notifications exceptions in Settings. --- Telegram/Resources/langs/lang.strings | 5 + Telegram/SourceFiles/apiwrap.cpp | 11 +- Telegram/SourceFiles/boxes/peer_list_box.cpp | 22 + Telegram/SourceFiles/boxes/peer_list_box.h | 2 + .../boxes/peer_list_controllers.cpp | 19 - .../SourceFiles/boxes/peer_list_controllers.h | 1 - .../data/notify/data_notify_settings.cpp | 203 ++++++--- .../data/notify/data_notify_settings.h | 62 ++- .../data/notify/data_peer_notify_settings.cpp | 9 + .../data/notify/data_peer_notify_settings.h | 1 + .../settings/settings_blocked_peers.cpp | 2 +- .../settings/settings_notifications.cpp | 27 +- .../settings/settings_notifications_type.cpp | 399 +++++++++++++++++- Telegram/SourceFiles/ui/menu_icons.style | 1 + 14 files changed, 646 insertions(+), 118 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index c01e2f6d8..a045ba1f0 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -498,6 +498,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_notification_title_channels" = "Notifications for channels"; "lng_notification_about_channels#one" = "Please note that **{count} channel** is listed as an exception and won't be affected by this change."; "lng_notification_about_channels#other" = "Please note that **{count} channels** are listed as exceptions and won't be affected by this change."; +"lng_notification_exceptions_view" = "View exceptions"; "lng_notification_enable" = "Enable notifications"; "lng_notification_sound" = "Sound"; "lng_notification_tone" = "Notification tone"; @@ -505,6 +506,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_notification_exceptions_unmuted" = "Unmuted"; "lng_notification_exceptions_add" = "Add an exception"; "lng_notification_exceptions_clear" = "Delete all exceptions"; +"lng_notification_exceptions_clear_sure" = "Are you sure you want to delete all exceptions?"; +"lng_notification_exceptions_clear_button" = "Delete"; +"lng_notification_exceptions_remove" = "Remove"; +"lng_notification_context_remove" = "Remove exception"; "lng_reaction_text" = "{reaction} to your «{text}»"; "lng_reaction_notext" = "{reaction} to your message"; diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index e71e03953..aebc9b809 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -1887,17 +1887,8 @@ void ApiWrap::sendNotifySettingsUpdates() { } const auto &settings = session().data().notifySettings(); for (const auto type : base::take(_updateNotifyDefaults)) { - const auto input = [&] { - switch (type) { - case Data::DefaultNotify::User: return MTP_inputNotifyUsers(); - case Data::DefaultNotify::Group: return MTP_inputNotifyChats(); - case Data::DefaultNotify::Broadcast: - return MTP_inputNotifyBroadcasts(); - } - Unexpected("Default notify type in sendNotifySettingsUpdates"); - }(); request(MTPaccount_UpdateNotifySettings( - input, + Data::DefaultNotifyToMTP(type), settings.defaultSettings(type).serialize() )).afterDelay(kSmallDelayMs).send(); } diff --git a/Telegram/SourceFiles/boxes/peer_list_box.cpp b/Telegram/SourceFiles/boxes/peer_list_box.cpp index 6f27553d0..407eeb185 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_box.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "boxes/peer_list_box.h" +#include "history/history.h" // chatListNameSortKey. #include "main/session/session_show.h" #include "main/main_session.h" #include "mainwidget.h" @@ -396,6 +397,27 @@ void PeerListController::setSearchNoResultsText(const QString &text) { } } +void PeerListController::sortByName() { + auto keys = base::flat_map(); + keys.reserve(delegate()->peerListFullRowsCount()); + const auto key = [&](const PeerListRow &row) { + const auto id = row.id(); + const auto i = keys.find(id); + if (i != end(keys)) { + return i->second; + } + const auto peer = row.peer(); + const auto history = peer->owner().history(peer); + return keys.emplace( + id, + history->chatListNameSortKey()).first->second; + }; + const auto predicate = [&](const PeerListRow &a, const PeerListRow &b) { + return (key(a).compare(key(b)) < 0); + }; + delegate()->peerListSortRows(predicate); +} + base::unique_qptr PeerListController::rowContextMenu( QWidget *parent, not_null row) { diff --git a/Telegram/SourceFiles/boxes/peer_list_box.h b/Telegram/SourceFiles/boxes/peer_list_box.h index ac8ab554b..837bb6e2c 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.h +++ b/Telegram/SourceFiles/boxes/peer_list_box.h @@ -560,6 +560,8 @@ protected: delegate()->peerListSetSearchNoResults(std::move(noResults)); } + void sortByName(); + private: PeerListDelegate *_delegate = nullptr; std::unique_ptr _searchController = nullptr; diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp index 335531fc9..874e61e23 100644 --- a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp @@ -594,25 +594,6 @@ void ContactsBoxController::sort() { } } -void ContactsBoxController::sortByName() { - auto keys = base::flat_map(); - keys.reserve(delegate()->peerListFullRowsCount()); - const auto key = [&](const PeerListRow &row) { - const auto id = row.id(); - const auto i = keys.find(id); - if (i != end(keys)) { - return i->second; - } - const auto peer = row.peer(); - const auto history = peer->owner().history(peer); - return keys.emplace(id, history->chatListNameSortKey()).first->second; - }; - const auto predicate = [&](const PeerListRow &a, const PeerListRow &b) { - return (key(a).compare(key(b)) < 0); - }; - delegate()->peerListSortRows(predicate); -} - void ContactsBoxController::sortByOnline() { const auto now = base::unixtime::now(); const auto key = [&](const PeerListRow &row) { diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.h b/Telegram/SourceFiles/boxes/peer_list_controllers.h index 0ba108f01..35b4e0f15 100644 --- a/Telegram/SourceFiles/boxes/peer_list_controllers.h +++ b/Telegram/SourceFiles/boxes/peer_list_controllers.h @@ -192,7 +192,6 @@ protected: private: void sort(); - void sortByName(); void sortByOnline(); void rebuildRows(); void checkForEmptyRows(); diff --git a/Telegram/SourceFiles/data/notify/data_notify_settings.cpp b/Telegram/SourceFiles/data/notify/data_notify_settings.cpp index 1675d1e92..22fc6e26f 100644 --- a/Telegram/SourceFiles/data/notify/data_notify_settings.cpp +++ b/Telegram/SourceFiles/data/notify/data_notify_settings.cpp @@ -42,11 +42,39 @@ constexpr auto kMaxNotifyCheckDelay = 24 * 3600 * crl::time(1000); return (result > 0); } +[[nodiscard]] bool SkipAddException(not_null peer) { + if (const auto user = peer->asUser()) { + return user->isInaccessible(); + } else if (const auto chat = peer->asChat()) { + return chat->isDeactivated() || chat->isForbidden(); + } else if (const auto channel = peer->asChannel()) { + return channel->isForbidden(); + } + return false; +} + } // namespace +DefaultNotify DefaultNotifyType(not_null peer) { + return peer->isUser() + ? DefaultNotify::User + : (peer->isChat() || peer->isMegagroup()) + ? DefaultNotify::Group + : DefaultNotify::Broadcast; +} + +MTPInputNotifyPeer DefaultNotifyToMTP(DefaultNotify type) { + switch (type) { + case DefaultNotify::User: return MTP_inputNotifyUsers(); + case DefaultNotify::Group: return MTP_inputNotifyChats(); + case DefaultNotify::Broadcast: return MTP_inputNotifyBroadcasts(); + } + Unexpected("Default notify type in sendNotifySettingsUpdates"); +} + NotifySettings::NotifySettings(not_null owner) - : _owner(owner) - , _unmuteByFinishedTimer([=] { unmuteByFinished(); }) { +: _owner(owner) +, _unmuteByFinishedTimer([=] { unmuteByFinished(); }) { } void NotifySettings::request(not_null peer) { @@ -63,7 +91,7 @@ void NotifySettings::request(not_null peer) { } } -void NotifySettings::request(not_null thread) { +void NotifySettings::request(not_null thread) { if (const auto topic = thread->asTopic()) { if (topic->notify().settingsUnknown()) { topic->session().api().requestNotifySettings( @@ -145,6 +173,7 @@ void NotifySettings::apply( not_null peer, const MTPPeerNotifySettings &settings) { if (peer->notify().change(settings)) { + updateException(peer); updateLocal(peer); Core::App().notifications().checkDelayed(); } @@ -162,7 +191,7 @@ void NotifySettings::apply( } void NotifySettings::apply( - not_null topic, + not_null topic, const MTPPeerNotifySettings &settings) { if (topic->notify().change(settings)) { updateLocal(topic); @@ -171,8 +200,8 @@ void NotifySettings::apply( } void NotifySettings::update( - not_null thread, - Data::MuteValue muteForSeconds, + not_null thread, + MuteValue muteForSeconds, std::optional silentPosts, std::optional sound, std::optional storiesMuted) { @@ -181,34 +210,29 @@ void NotifySettings::update( silentPosts, sound, storiesMuted)) { + if (const auto history = thread->asHistory()) { + updateException(history->peer); + } updateLocal(thread); thread->session().api().updateNotifySettingsDelayed(thread); } } -void NotifySettings::resetToDefault(not_null thread) { - const auto empty = MTP_peerNotifySettings( - MTP_flags(0), - MTPBool(), - MTPBool(), - MTPint(), - MTPNotificationSound(), - MTPNotificationSound(), - MTPNotificationSound(), - MTPBool(), - MTPBool(), - MTPNotificationSound(), - MTPNotificationSound(), - MTPNotificationSound()); - if (thread->notify().change(empty)) { +void NotifySettings::resetToDefault(not_null thread) { + // Duplicated in clearExceptions(type) and resetToDefault(peer). + if (thread->notify().resetToDefault()) { + if (const auto history = thread->asHistory()) { + updateException(history->peer); + } updateLocal(thread); thread->session().api().updateNotifySettingsDelayed(thread); + Core::App().notifications().checkDelayed(); } } void NotifySettings::update( not_null peer, - Data::MuteValue muteForSeconds, + MuteValue muteForSeconds, std::optional silentPosts, std::optional sound, std::optional storiesMuted) { @@ -217,33 +241,24 @@ void NotifySettings::update( silentPosts, sound, storiesMuted)) { + updateException(peer); updateLocal(peer); peer->session().api().updateNotifySettingsDelayed(peer); } } void NotifySettings::resetToDefault(not_null peer) { - const auto empty = MTP_peerNotifySettings( - MTP_flags(0), - MTPBool(), - MTPBool(), - MTPint(), - MTPNotificationSound(), - MTPNotificationSound(), - MTPNotificationSound(), - MTPBool(), - MTPBool(), - MTPNotificationSound(), - MTPNotificationSound(), - MTPNotificationSound()); - if (peer->notify().change(empty)) { + // Duplicated in clearExceptions(type) and resetToDefault(thread). + if (peer->notify().resetToDefault()) { + updateException(peer); updateLocal(peer); peer->session().api().updateNotifySettingsDelayed(peer); + Core::App().notifications().checkDelayed(); } } -void NotifySettings::forumParentMuteUpdated(not_null forum) { - forum->enumerateTopics([&](not_null topic) { +void NotifySettings::forumParentMuteUpdated(not_null forum) { + forum->enumerateTopics([&](not_null topic) { if (!topic->notify().settingsUnknown()) { updateLocal(topic); } @@ -266,11 +281,7 @@ auto NotifySettings::defaultValue(DefaultNotify type) const const PeerNotifySettings &NotifySettings::defaultSettings( not_null peer) const { - return defaultSettings(peer->isUser() - ? DefaultNotify::User - : (peer->isChat() || peer->isMegagroup()) - ? DefaultNotify::Group - : DefaultNotify::Broadcast); + return defaultSettings(DefaultNotifyType(peer)); } const PeerNotifySettings &NotifySettings::defaultSettings( @@ -280,7 +291,7 @@ const PeerNotifySettings &NotifySettings::defaultSettings( void NotifySettings::defaultUpdate( DefaultNotify type, - Data::MuteValue muteForSeconds, + MuteValue muteForSeconds, std::optional silentPosts, std::optional sound, std::optional storiesMuted) { @@ -291,7 +302,7 @@ void NotifySettings::defaultUpdate( } } -void NotifySettings::updateLocal(not_null thread) { +void NotifySettings::updateLocal(not_null thread) { const auto topic = thread->asTopic(); if (!topic) { return updateLocal(thread->peer()); @@ -351,7 +362,7 @@ void NotifySettings::cacheSound(not_null document) { const auto view = document->createMediaView(); _ringtones.views.emplace(document->id, view); document->forceToCache(true); - document->save(Data::FileOriginRingtones(), QString()); + document->save(FileOriginRingtones(), QString()); } void NotifySettings::cacheSound(const std::optional &sound) { @@ -459,7 +470,7 @@ void NotifySettings::unmuteByFinished() { } bool NotifySettings::isMuted( - not_null thread, + not_null thread, crl::time *changesIn) const { const auto topic = thread->asTopic(); const auto until = topic ? topic->notify().muteUntil() : std::nullopt; @@ -468,27 +479,24 @@ bool NotifySettings::isMuted( : isMuted(thread->peer(), changesIn); } -bool NotifySettings::isMuted(not_null thread) const { +bool NotifySettings::isMuted(not_null thread) const { return isMuted(thread, nullptr); } -NotifySound NotifySettings::sound( - not_null thread) const { +NotifySound NotifySettings::sound(not_null thread) const { const auto topic = thread->asTopic(); const auto sound = topic ? topic->notify().sound() : std::nullopt; return sound ? *sound : this->sound(thread->peer()); } -bool NotifySettings::muteUnknown( - not_null thread) const { +bool NotifySettings::muteUnknown(not_null thread) const { const auto topic = thread->asTopic(); return (topic && topic->notify().settingsUnknown()) || ((!topic || !topic->notify().muteUntil().has_value()) && muteUnknown(thread->peer())); } -bool NotifySettings::soundUnknown( - not_null thread) const { +bool NotifySettings::soundUnknown(not_null thread) const { const auto topic = thread->asTopic(); return (topic && topic->notify().settingsUnknown()) || ((!topic || !topic->notify().sound().has_value()) @@ -543,8 +551,7 @@ bool NotifySettings::silentPostsUnknown( && defaultSettings(peer).settingsUnknown()); } -bool NotifySettings::soundUnknown( - not_null peer) const { +bool NotifySettings::soundUnknown(not_null peer) const { return peer->notify().settingsUnknown() || (!peer->notify().sound().has_value() && defaultSettings(peer).settingsUnknown()); @@ -556,8 +563,7 @@ bool NotifySettings::settingsUnknown(not_null peer) const { || soundUnknown(peer); } -bool NotifySettings::settingsUnknown( - not_null thread) const { +bool NotifySettings::settingsUnknown(not_null thread) const { const auto topic = thread->asTopic(); return muteUnknown(thread) || soundUnknown(thread) @@ -577,4 +583,85 @@ rpl::producer<> NotifySettings::defaultUpdates( : DefaultNotify::Broadcast); } +void NotifySettings::loadExceptions() { + for (auto i = 0; i != kDefaultNotifyTypes; ++i) { + if (_exceptionsRequestId[i]) { + continue; + } + const auto type = static_cast(i); + const auto api = &_owner->session().api(); + const auto requestId = api->request(MTPaccount_GetNotifyExceptions( + MTP_flags(MTPaccount_GetNotifyExceptions::Flag::f_peer), + DefaultNotifyToMTP(type) + )).done([=](const MTPUpdates &result) { + api->applyUpdates(result); + }).send(); + _exceptionsRequestId[i] = requestId; + } +} + +void NotifySettings::updateException(not_null peer) { + const auto type = DefaultNotifyType(peer); + const auto index = static_cast(type); + const auto exception = peer->notify().muteUntil().has_value(); + if (!exception) { + if (_exceptions[index].remove(peer)) { + exceptionsUpdated(type); + } + } else if (SkipAddException(peer)) { + return; + } else if (_exceptions[index].emplace(peer).second) { + exceptionsUpdated(type); + } +} + +void NotifySettings::exceptionsUpdated(DefaultNotify type) { + if (!ranges::contains(_exceptionsUpdatesScheduled, true)) { + crl::on_main(&_owner->session(), [=] { + const auto scheduled = base::take(_exceptionsUpdatesScheduled); + for (auto i = 0; i != kDefaultNotifyTypes; ++i) { + if (scheduled[i]) { + _exceptionsUpdates.fire(static_cast(i)); + } + } + }); + } + _exceptionsUpdatesScheduled[static_cast(type)] = true; + _exceptionsUpdatesRealtime.fire_copy(type); +} + +rpl::producer NotifySettings::exceptionsUpdates() const { + return _exceptionsUpdates.events(); +} + +auto NotifySettings::exceptionsUpdatesRealtime() const +-> rpl::producer { + return _exceptionsUpdatesRealtime.events(); +} + +const base::flat_set> &NotifySettings::exceptions( + DefaultNotify type) const { + const auto index = static_cast(type); + Assert(index >= 0 && index < kDefaultNotifyTypes); + + return _exceptions[index]; +} + +void NotifySettings::clearExceptions(DefaultNotify type) { + const auto index = static_cast(type); + const auto list = base::take(_exceptions[index]); + if (list.empty()) { + return; + } + for (const auto &peer : list) { + // Duplicated in resetToDefault(peer / thread). + if (peer->notify().resetToDefault()) { + updateLocal(peer); + peer->session().api().updateNotifySettingsDelayed(peer); + } + } + Core::App().notifications().checkDelayed(); + exceptionsUpdated(type); +} + } // namespace Data diff --git a/Telegram/SourceFiles/data/notify/data_notify_settings.h b/Telegram/SourceFiles/data/notify/data_notify_settings.h index 6e51b87ac..b1919a319 100644 --- a/Telegram/SourceFiles/data/notify/data_notify_settings.h +++ b/Telegram/SourceFiles/data/notify/data_notify_settings.h @@ -26,13 +26,17 @@ enum class DefaultNotify { Group, Broadcast, }; +[[nodiscard]] DefaultNotify DefaultNotifyType( + not_null peer); + +[[nodiscard]] MTPInputNotifyPeer DefaultNotifyToMTP(DefaultNotify type); class NotifySettings final { public: NotifySettings(not_null owner); void request(not_null peer); - void request(not_null thread); + void request(not_null thread); void apply( const MTPNotifyPeer ¬ifyPeer, @@ -50,25 +54,25 @@ public: MsgId topicRootId, const MTPPeerNotifySettings &settings); void apply( - not_null topic, + not_null topic, const MTPPeerNotifySettings &settings); void update( - not_null thread, - Data::MuteValue muteForSeconds, + not_null thread, + MuteValue muteForSeconds, std::optional silentPosts = std::nullopt, std::optional sound = std::nullopt, std::optional storiesMuted = std::nullopt); - void resetToDefault(not_null thread); + void resetToDefault(not_null thread); void update( not_null peer, - Data::MuteValue muteForSeconds, + MuteValue muteForSeconds, std::optional silentPosts = std::nullopt, std::optional sound = std::nullopt, std::optional storiesMuted = std::nullopt); void resetToDefault(not_null peer); - void forumParentMuteUpdated(not_null forum); + void forumParentMuteUpdated(not_null forum); void cacheSound(DocumentId id); void cacheSound(not_null document); @@ -84,18 +88,15 @@ public: void defaultUpdate( DefaultNotify type, - Data::MuteValue muteForSeconds, + MuteValue muteForSeconds, std::optional silentPosts = std::nullopt, std::optional sound = std::nullopt, std::optional storiesMuted = std::nullopt); - [[nodiscard]] bool isMuted(not_null thread) const; - [[nodiscard]] NotifySound sound( - not_null thread) const; - [[nodiscard]] bool muteUnknown( - not_null thread) const; - [[nodiscard]] bool soundUnknown( - not_null thread) const; + [[nodiscard]] bool isMuted(not_null thread) const; + [[nodiscard]] NotifySound sound(not_null thread) const; + [[nodiscard]] bool muteUnknown(not_null thread) const; + [[nodiscard]] bool soundUnknown(not_null thread) const; [[nodiscard]] bool isMuted(not_null peer) const; [[nodiscard]] bool silentPosts(not_null peer) const; @@ -105,7 +106,17 @@ public: not_null peer) const; [[nodiscard]] bool soundUnknown(not_null peer) const; + void loadExceptions(); + [[nodiscard]] rpl::producer exceptionsUpdates() const; + [[nodiscard]] auto exceptionsUpdatesRealtime() const + -> rpl::producer; + [[nodiscard]] const base::flat_set> &exceptions( + DefaultNotify type) const; + void clearExceptions(DefaultNotify type); + private: + static constexpr auto kDefaultNotifyTypes = 3; + struct DefaultValue { PeerNotifySettings settings; rpl::event_stream<> updates; @@ -114,7 +125,7 @@ private: void cacheSound(const std::optional &sound); [[nodiscard]] bool isMuted( - not_null thread, + not_null thread, crl::time *changesIn) const; [[nodiscard]] bool isMuted( not_null peer, @@ -126,21 +137,22 @@ private: not_null peer) const; [[nodiscard]] bool settingsUnknown(not_null peer) const; [[nodiscard]] bool settingsUnknown( - not_null thread) const; + not_null thread) const; void unmuteByFinished(); void unmuteByFinishedDelayed(crl::time delay); - void updateLocal(not_null thread); + void updateLocal(not_null thread); void updateLocal(not_null peer); void updateLocal(DefaultNotify type); + void updateException(not_null peer); + void exceptionsUpdated(DefaultNotify type); + const not_null _owner; DefaultValue _defaultValues[3]; std::unordered_set> _mutedPeers; - std::unordered_map< - not_null, - rpl::lifetime> _mutedTopics; + std::unordered_map, rpl::lifetime> _mutedTopics; base::Timer _unmuteByFinishedTimer; struct { @@ -151,6 +163,14 @@ private: rpl::lifetime pendingLifetime; } _ringtones; + rpl::event_stream _exceptionsUpdates; + rpl::event_stream _exceptionsUpdatesRealtime; + std::array< + base::flat_set>, + kDefaultNotifyTypes> _exceptions; + std::array _exceptionsRequestId = {}; + std::array _exceptionsUpdatesScheduled = {}; + }; } // namespace Data diff --git a/Telegram/SourceFiles/data/notify/data_peer_notify_settings.cpp b/Telegram/SourceFiles/data/notify/data_peer_notify_settings.cpp index 4b756a611..48d198d6e 100644 --- a/Telegram/SourceFiles/data/notify/data_peer_notify_settings.cpp +++ b/Telegram/SourceFiles/data/notify/data_peer_notify_settings.cpp @@ -256,6 +256,15 @@ bool PeerNotifySettings::change( SerializeSound(std::nullopt))); // stories_sound } +bool PeerNotifySettings::resetToDefault() { + if (_known && !_value) { + return false; + } + _known = true; + _value = nullptr; + return true; +} + std::optional PeerNotifySettings::muteUntil() const { return _value ? _value->muteUntil() diff --git a/Telegram/SourceFiles/data/notify/data_peer_notify_settings.h b/Telegram/SourceFiles/data/notify/data_peer_notify_settings.h index 76a8ecfd9..b5de9e119 100644 --- a/Telegram/SourceFiles/data/notify/data_peer_notify_settings.h +++ b/Telegram/SourceFiles/data/notify/data_peer_notify_settings.h @@ -46,6 +46,7 @@ public: std::optional silentPosts, std::optional sound, std::optional storiesMuted); + bool resetToDefault(); bool settingsUnknown() const; std::optional muteUntil() const; diff --git a/Telegram/SourceFiles/settings/settings_blocked_peers.cpp b/Telegram/SourceFiles/settings/settings_blocked_peers.cpp index f90bd9ac8..e21666d81 100644 --- a/Telegram/SourceFiles/settings/settings_blocked_peers.cpp +++ b/Telegram/SourceFiles/settings/settings_blocked_peers.cpp @@ -80,7 +80,7 @@ QPointer Blocked::createPinnedToTop(not_null parent) { content, tr::lng_blocked_list_add(), st::settingsButtonActive, - { &st::menuIconBlockSettings, IconType::Round, &st::transparent } + { &st::menuIconBlockSettings } )->addClickHandler([=] { BlockedBoxController::BlockNewPeer(_controller); }); diff --git a/Telegram/SourceFiles/settings/settings_notifications.cpp b/Telegram/SourceFiles/settings/settings_notifications.cpp index cbe3f6c8a..9bf5f35d4 100644 --- a/Telegram/SourceFiles/settings/settings_notifications.cpp +++ b/Telegram/SourceFiles/settings/settings_notifications.cpp @@ -172,10 +172,30 @@ void AddTypeButton( showOther(NotificationsTypeId(type)); }); + const auto session = &controller->session(); + const auto settings = &session->data().notifySettings(); const auto &st = st::settingsNotificationType; + auto status = rpl::combine( + NotificationsEnabledForTypeValue(session, type), + rpl::single( + type + ) | rpl::then(settings->exceptionsUpdates( + ) | rpl::filter(rpl::mappers::_1 == type)) + ) | rpl::map([=](bool enabled, const auto &) { + const auto count = int(settings->exceptions(type).size()); + return !count + ? tr::lng_notification_click_to_change() + : (enabled + ? tr::lng_notification_on + : tr::lng_notification_off)( + lt_exceptions, + tr::lng_notification_exceptions( + lt_count, + rpl::single(float64(count)))); + }) | rpl::flatten_latest(); const auto details = Ui::CreateChild( button.get(), - tr::lng_notification_click_to_change(), + std::move(status), st::settingsNotificationTypeDetails); details->show(); details->moveToLeft( @@ -183,12 +203,11 @@ void AddTypeButton( st.padding.top() + st.height - details->height()); details->setAttribute(Qt::WA_TransparentForMouseEvents); - const auto session = &controller->session(); const auto toggleButton = Ui::CreateChild( container.get(), nullptr, st); - const auto checkView = toggleButton->lifetime().make_state( + const auto checkView = button->lifetime().make_state( st.toggle, NotificationsEnabledForType(session, type), [=] { toggleButton->update(); }); @@ -971,6 +990,8 @@ void SetupNotificationsContent( previewWrap->toggle(settings.desktopNotify(), anim::type::instant); previewDivider->toggle(!settings.desktopNotify(), anim::type::instant); + controller->session().data().notifySettings().loadExceptions(); + AddSkip(container, st::notifyPreviewBottomSkip); AddSubsectionTitle(container, tr::lng_settings_notify_title()); const auto addType = [&](Data::DefaultNotify type) { diff --git a/Telegram/SourceFiles/settings/settings_notifications_type.cpp b/Telegram/SourceFiles/settings/settings_notifications_type.cpp index 29699a398..999ff9165 100644 --- a/Telegram/SourceFiles/settings/settings_notifications_type.cpp +++ b/Telegram/SourceFiles/settings/settings_notifications_type.cpp @@ -11,14 +11,23 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "base/unixtime.h" #include "boxes/ringtones_box.h" +#include "boxes/peer_list_box.h" +#include "boxes/peer_list_controllers.h" #include "data/notify/data_notify_settings.h" +#include "data/data_changes.h" +#include "data/data_peer.h" #include "data/data_session.h" +#include "history/history.h" #include "lang/lang_keys.h" #include "main/main_session.h" +#include "menu/menu_mute.h" +#include "ui/boxes/confirm_box.h" #include "ui/widgets/buttons.h" +#include "ui/widgets/popup_menu.h" #include "ui/wrap/slide_wrap.h" #include "ui/wrap/vertical_layout.h" #include "window/window_session_controller.h" +#include "styles/style_layers.h" #include "styles/style_menu_icons.h" #include "styles/style_settings.h" @@ -27,6 +36,321 @@ namespace { using Notify = Data::DefaultNotify; +class AddExceptionBoxController final + : public ChatsListBoxController + , public base::has_weak_ptr { +public: + AddExceptionBoxController( + not_null session, + Notify type, + Fn)> done); + + Main::Session &session() const override; + void rowClicked(not_null row) override; + base::unique_qptr rowContextMenu( + QWidget *parent, + not_null row) override; + +private: + void prepareViewHook() override; + std::unique_ptr createRow(not_null history) override; + + const not_null _session; + const Notify _type; + const Fn)> _done; + + base::unique_qptr _menu; + PeerData *_lastClickedPeer = nullptr; + + rpl::lifetime _lifetime; + +}; + +class ExceptionsController final : public PeerListController { +public: + ExceptionsController( + not_null window, + Notify type); + + Main::Session &session() const override; + void prepare() override; + void rowClicked(not_null row) override; + base::unique_qptr rowContextMenu( + QWidget *parent, + not_null row) override; + void rowRightActionClicked(not_null row) override; + void loadMoreRows() override; + + void bringToTop(not_null peer); + + [[nodiscard]] rpl::producer countValue() const; + +private: + void refreshRows(); + bool appendRow(not_null peer); + std::unique_ptr createRow(not_null peer) const; + void refreshStatus(not_null row) const; + + void sort(); + + const not_null _window; + const Notify _type; + + base::unique_qptr _menu; + + base::flat_map, int> _topOrdered; + int _topOrder = 0; + + rpl::variable _count; + + rpl::lifetime _lifetime; + +}; + +AddExceptionBoxController::AddExceptionBoxController( + not_null session, + Notify type, + Fn)> done) +: ChatsListBoxController(session) +, _session(session) +, _type(type) +, _done(std::move(done)) { +} + +Main::Session &AddExceptionBoxController::session() const { + return *_session; +} + +void AddExceptionBoxController::prepareViewHook() { + delegate()->peerListSetTitle(tr::lng_notification_exceptions_add()); + + _session->changes().peerUpdates( + Data::PeerUpdate::Flag::Notifications + ) | rpl::filter([=](const Data::PeerUpdate &update) { + return update.peer == _lastClickedPeer; + }) | rpl::start_with_next([=] { + if (const auto onstack = _done) { + onstack(_lastClickedPeer); + } + }, _lifetime); +} + +void AddExceptionBoxController::rowClicked(not_null row) { + delegate()->peerListShowRowMenu(row, true); +} + +base::unique_qptr AddExceptionBoxController::rowContextMenu( + QWidget *parent, + not_null row) { + const auto peer = row->peer(); + auto result = base::make_unique_q( + parent, + st::popupMenuWithIcons); + + MuteMenu::FillMuteMenu( + result.get(), + peer->owner().history(peer), + delegate()->peerListUiShow()); + + // First clear _menu value, so that we don't check row positions yet. + base::take(_menu); + + // Here unique_qptr is used like a shared pointer, where + // not the last destroyed pointer destroys the object, but the first. + _menu = base::unique_qptr(result.get()); + _menu->setDestroyedCallback(crl::guard(this, [=] { + _lastClickedPeer = nullptr; + })); + _lastClickedPeer = peer; + + return result; +} + +auto AddExceptionBoxController::createRow(not_null history) +-> std::unique_ptr { + if (Data::DefaultNotifyType(history->peer) != _type + || history->peer->isSelf() + || history->peer->isRepliesChat()) { + return nullptr; + } + return std::make_unique(history); +} + +ExceptionsController::ExceptionsController( + not_null window, + Notify type) +: _window(window) +, _type(type) { +} + +Main::Session &ExceptionsController::session() const { + return _window->session(); +} + +void ExceptionsController::prepare() { + refreshRows(); + + session().data().notifySettings().exceptionsUpdates( + ) | rpl::filter(rpl::mappers::_1 == _type) | rpl::start_with_next([=] { + refreshRows(); + }, lifetime()); + + session().changes().peerUpdates( + Data::PeerUpdate::Flag::Notifications + ) | rpl::start_with_next([=](const Data::PeerUpdate &update) { + const auto peer = update.peer; + if (const auto row = delegate()->peerListFindRow(peer->id.value)) { + if (peer->notify().muteUntil().has_value()) { + refreshStatus(row); + } else { + delegate()->peerListRemoveRow(row); + delegate()->peerListRefreshRows(); + _count = delegate()->peerListFullRowsCount(); + } + } + }, _lifetime); +} + +void ExceptionsController::loadMoreRows() { +} + +void ExceptionsController::bringToTop(not_null peer) { + _topOrdered[peer] = ++_topOrder; + if (delegate()->peerListFindRow(peer->id.value)) { + sort(); + } +} + +rpl::producer ExceptionsController::countValue() const { + return _count.value(); +} + +void ExceptionsController::rowClicked(not_null row) { + delegate()->peerListShowRowMenu(row, true); +} + +void ExceptionsController::rowRightActionClicked( + not_null row) { + session().data().notifySettings().resetToDefault(row->peer()); +} + +void ExceptionsController::refreshRows() { + auto seen = base::flat_set>(); + const auto &list = session().data().notifySettings().exceptions(_type); + auto removed = false, added = false; + auto already = delegate()->peerListFullRowsCount(); + seen.reserve(std::min(int(list.size()), already)); + for (auto i = 0; i != already;) { + const auto row = delegate()->peerListRowAt(i); + if (list.contains(row->peer())) { + seen.emplace(row->peer()); + ++i; + } else { + delegate()->peerListRemoveRow(row); + --already; + removed = true; + } + } + for (const auto &peer : list) { + if (!seen.contains(peer)) { + appendRow(peer); + added = true; + } + } + if (added || removed) { + if (added) { + sort(); + } + delegate()->peerListRefreshRows(); + _count = delegate()->peerListFullRowsCount(); + } +} + +base::unique_qptr ExceptionsController::rowContextMenu( + QWidget *parent, + not_null row) { + const auto peer = row->peer(); + auto result = base::make_unique_q( + parent, + st::popupMenuWithIcons); + + result->addAction( + (peer->isUser() + ? tr::lng_context_view_profile + : peer->isBroadcast() + ? tr::lng_context_view_channel + : tr::lng_context_view_group)(tr::now), + crl::guard(_window, [window = _window.get(), peer] { + window->showPeerInfo(peer); + }), + (peer->isUser() ? &st::menuIconProfile : &st::menuIconInfo)); + result->addSeparator(); + + MuteMenu::FillMuteMenu( + result.get(), + peer->owner().history(peer), + _window->uiShow()); + + // First clear _menu value, so that we don't check row positions yet. + base::take(_menu); + + // Here unique_qptr is used like a shared pointer, where + // not the last destroyed pointer destroys the object, but the first. + _menu = base::unique_qptr(result.get()); + + return result; +} + +bool ExceptionsController::appendRow(not_null peer) { + delegate()->peerListAppendRow(createRow(peer)); + return true; +} + +std::unique_ptr ExceptionsController::createRow( + not_null peer) const { + auto row = std::make_unique(peer); + row->setActionLink(tr::lng_notification_exceptions_remove(tr::now)); + refreshStatus(row.get()); + return row; +} + +void ExceptionsController::refreshStatus(not_null row) const { + const auto peer = row->peer(); + const auto status = peer->owner().notifySettings().isMuted(peer) + ? tr::lng_notification_exceptions_muted(tr::now) + : tr::lng_notification_exceptions_unmuted(tr::now); + row->setCustomStatus(status); +} + +void ExceptionsController::sort() { + auto keys = base::flat_map(); + keys.reserve(delegate()->peerListFullRowsCount()); + const auto length = QString::number(_topOrder).size(); + const auto key = [&](const PeerListRow &row) { + const auto id = row.id(); + const auto i = keys.find(id); + if (i != end(keys)) { + return i->second; + } + const auto peer = row.peer(); + const auto top = _topOrdered.find(peer); + if (top != end(_topOrdered)) { + const auto order = _topOrder - top->second; + return keys.emplace( + id, + u"0%1"_q.arg(order, length, 10, QChar('0'))).first->second; + } + const auto history = peer->owner().history(peer); + return keys.emplace( + id, + '1' + history->chatListNameSortKey()).first->second; + }; + const auto predicate = [&](const PeerListRow &a, const PeerListRow &b) { + return (key(a).compare(key(b)) < 0); + }; + delegate()->peerListSortRows(predicate); +} + template [[nodiscard]] Type Id() { return &NotificationsTypeMetaImplementation::Meta; @@ -103,7 +427,7 @@ void SetupChecks( : ExtractRingtoneName(session->data().document(now.id)); }; settings->defaultUpdates( - Data::DefaultNotify::User + Notify::User ) | rpl::start_with_next([=] { toneLabel->fire(label()); }, toneInner->lifetime()); @@ -147,9 +471,74 @@ void SetupChecks( } void SetupExceptions( - not_null container, - not_null controller, - Notify type) { + not_null container, + not_null window, + Notify type) { + const auto add = AddButton( + container, + tr::lng_notification_exceptions_add(), + st::settingsButtonActive, + { &st::menuIconInviteSettings }); + + auto controller = std::make_unique(window, type); + controller->setStyleOverrides(&st::settingsBlockedList); + const auto content = container->add( + object_ptr(container, controller.get())); + + struct State { + std::unique_ptr controller; + std::unique_ptr delegate; + }; + const auto state = content->lifetime().make_state(); + state->controller = std::move(controller); + state->delegate = std::make_unique(); + + state->delegate->setContent(content); + state->controller->setDelegate(state->delegate.get()); + + add->setClickedCallback([=] { + const auto box = std::make_shared>(); + const auto done = [=](not_null peer) { + state->controller->bringToTop(peer); + if (*box) { + (*box)->closeBox(); + } + }; + auto controller = std::make_unique( + &window->session(), + type, + crl::guard(content, done)); + auto initBox = [=](not_null box) { + box->addButton(tr::lng_cancel(), [box] { box->closeBox(); }); + }; + *box = window->show( + Box(std::move(controller), std::move(initBox))); + }); + + const auto wrap = container->add( + object_ptr>( + container, + CreateButton( + container, + tr::lng_notification_exceptions_clear(), + st::settingsAttentionButtonWithIcon, + { &st::menuIconDeleteAttention }))); + wrap->entity()->setClickedCallback([=] { + const auto clear = [=](Fn close) { + window->session().data().notifySettings().clearExceptions(type); + close(); + }; + window->show(Ui::MakeConfirmBox({ + .text = tr::lng_notification_exceptions_clear_sure(), + .confirmed = clear, + .confirmText = tr::lng_notification_exceptions_clear_button(), + .confirmStyle = &st::attentionBoxButton, + .title = tr::lng_notification_exceptions_clear(), + })); + }); + wrap->toggleOn( + state->controller->countValue() | rpl::map(rpl::mappers::_1 > 1), + anim::type::instant); } } // namespace @@ -211,7 +600,7 @@ bool NotificationsEnabledForType( rpl::producer NotificationsEnabledForTypeValue( not_null session, - Data::DefaultNotify type) { + Notify type) { const auto settings = &session->data().notifySettings(); return rpl::single( rpl::empty diff --git a/Telegram/SourceFiles/ui/menu_icons.style b/Telegram/SourceFiles/ui/menu_icons.style index bfb508773..715c6a2fb 100644 --- a/Telegram/SourceFiles/ui/menu_icons.style +++ b/Telegram/SourceFiles/ui/menu_icons.style @@ -173,6 +173,7 @@ menuIconReportAttention: icon {{ "menu/report", menuIconAttentionColor }}; menuIconRestoreAttention: icon {{ "menu/restore", menuIconAttentionColor }}; menuIconBlockSettings: icon {{ "menu/block", windowBgActive }}; +menuIconInviteSettings: icon {{ "menu/invite", windowBgActive }}; playerSpeedSlow: icon {{ "player/speed/audiospeed_menu_0.5", menuIconColor }}; playerSpeedSlowActive: icon {{ "player/speed/audiospeed_menu_0.5", mediaPlayerActiveFg }};