Manage notifications exceptions in Settings.

This commit is contained in:
John Preston 2023-08-23 13:20:41 +02:00
parent 518f0e22cd
commit b80f5f9706
14 changed files with 646 additions and 118 deletions

View File

@ -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";

View File

@ -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();
}

View File

@ -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<PeerListRowId, QString>();
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<Ui::PopupMenu> PeerListController::rowContextMenu(
QWidget *parent,
not_null<PeerListRow*> row) {

View File

@ -560,6 +560,8 @@ protected:
delegate()->peerListSetSearchNoResults(std::move(noResults));
}
void sortByName();
private:
PeerListDelegate *_delegate = nullptr;
std::unique_ptr<PeerListSearchController> _searchController = nullptr;

View File

@ -594,25 +594,6 @@ void ContactsBoxController::sort() {
}
}
void ContactsBoxController::sortByName() {
auto keys = base::flat_map<PeerListRowId, QString>();
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) {

View File

@ -192,7 +192,6 @@ protected:
private:
void sort();
void sortByName();
void sortByOnline();
void rebuildRows();
void checkForEmptyRows();

View File

@ -42,11 +42,39 @@ constexpr auto kMaxNotifyCheckDelay = 24 * 3600 * crl::time(1000);
return (result > 0);
}
[[nodiscard]] bool SkipAddException(not_null<PeerData*> 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<const PeerData*> 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<Session*> owner)
: _owner(owner)
, _unmuteByFinishedTimer([=] { unmuteByFinished(); }) {
: _owner(owner)
, _unmuteByFinishedTimer([=] { unmuteByFinished(); }) {
}
void NotifySettings::request(not_null<PeerData*> peer) {
@ -63,7 +91,7 @@ void NotifySettings::request(not_null<PeerData*> peer) {
}
}
void NotifySettings::request(not_null<Data::Thread*> thread) {
void NotifySettings::request(not_null<Thread*> thread) {
if (const auto topic = thread->asTopic()) {
if (topic->notify().settingsUnknown()) {
topic->session().api().requestNotifySettings(
@ -145,6 +173,7 @@ void NotifySettings::apply(
not_null<PeerData*> 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<Data::ForumTopic*> topic,
not_null<ForumTopic*> topic,
const MTPPeerNotifySettings &settings) {
if (topic->notify().change(settings)) {
updateLocal(topic);
@ -171,8 +200,8 @@ void NotifySettings::apply(
}
void NotifySettings::update(
not_null<Data::Thread*> thread,
Data::MuteValue muteForSeconds,
not_null<Thread*> thread,
MuteValue muteForSeconds,
std::optional<bool> silentPosts,
std::optional<NotifySound> sound,
std::optional<bool> 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<Data::Thread*> 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*> 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<PeerData*> peer,
Data::MuteValue muteForSeconds,
MuteValue muteForSeconds,
std::optional<bool> silentPosts,
std::optional<NotifySound> sound,
std::optional<bool> 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<PeerData*> 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<Data::Forum*> forum) {
forum->enumerateTopics([&](not_null<Data::ForumTopic*> topic) {
void NotifySettings::forumParentMuteUpdated(not_null<Forum*> forum) {
forum->enumerateTopics([&](not_null<ForumTopic*> topic) {
if (!topic->notify().settingsUnknown()) {
updateLocal(topic);
}
@ -266,11 +281,7 @@ auto NotifySettings::defaultValue(DefaultNotify type) const
const PeerNotifySettings &NotifySettings::defaultSettings(
not_null<const PeerData*> 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<bool> silentPosts,
std::optional<NotifySound> sound,
std::optional<bool> storiesMuted) {
@ -291,7 +302,7 @@ void NotifySettings::defaultUpdate(
}
}
void NotifySettings::updateLocal(not_null<Data::Thread*> thread) {
void NotifySettings::updateLocal(not_null<Thread*> thread) {
const auto topic = thread->asTopic();
if (!topic) {
return updateLocal(thread->peer());
@ -351,7 +362,7 @@ void NotifySettings::cacheSound(not_null<DocumentData*> 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<NotifySound> &sound) {
@ -459,7 +470,7 @@ void NotifySettings::unmuteByFinished() {
}
bool NotifySettings::isMuted(
not_null<const Data::Thread*> thread,
not_null<const Thread*> 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<const Data::Thread*> thread) const {
bool NotifySettings::isMuted(not_null<const Thread*> thread) const {
return isMuted(thread, nullptr);
}
NotifySound NotifySettings::sound(
not_null<const Data::Thread*> thread) const {
NotifySound NotifySettings::sound(not_null<const Thread*> 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<const Data::Thread*> thread) const {
bool NotifySettings::muteUnknown(not_null<const Thread*> 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<const Data::Thread*> thread) const {
bool NotifySettings::soundUnknown(not_null<const Thread*> 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<const PeerData*> peer) const {
bool NotifySettings::soundUnknown(not_null<const PeerData*> peer) const {
return peer->notify().settingsUnknown()
|| (!peer->notify().sound().has_value()
&& defaultSettings(peer).settingsUnknown());
@ -556,8 +563,7 @@ bool NotifySettings::settingsUnknown(not_null<const PeerData*> peer) const {
|| soundUnknown(peer);
}
bool NotifySettings::settingsUnknown(
not_null<const Data::Thread*> thread) const {
bool NotifySettings::settingsUnknown(not_null<const Thread*> 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<DefaultNotify>(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<PeerData*> peer) {
const auto type = DefaultNotifyType(peer);
const auto index = static_cast<int>(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<DefaultNotify>(i));
}
}
});
}
_exceptionsUpdatesScheduled[static_cast<int>(type)] = true;
_exceptionsUpdatesRealtime.fire_copy(type);
}
rpl::producer<DefaultNotify> NotifySettings::exceptionsUpdates() const {
return _exceptionsUpdates.events();
}
auto NotifySettings::exceptionsUpdatesRealtime() const
-> rpl::producer<DefaultNotify> {
return _exceptionsUpdatesRealtime.events();
}
const base::flat_set<not_null<PeerData*>> &NotifySettings::exceptions(
DefaultNotify type) const {
const auto index = static_cast<int>(type);
Assert(index >= 0 && index < kDefaultNotifyTypes);
return _exceptions[index];
}
void NotifySettings::clearExceptions(DefaultNotify type) {
const auto index = static_cast<int>(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

View File

@ -26,13 +26,17 @@ enum class DefaultNotify {
Group,
Broadcast,
};
[[nodiscard]] DefaultNotify DefaultNotifyType(
not_null<const PeerData*> peer);
[[nodiscard]] MTPInputNotifyPeer DefaultNotifyToMTP(DefaultNotify type);
class NotifySettings final {
public:
NotifySettings(not_null<Session*> owner);
void request(not_null<PeerData*> peer);
void request(not_null<Data::Thread*> thread);
void request(not_null<Thread*> thread);
void apply(
const MTPNotifyPeer &notifyPeer,
@ -50,25 +54,25 @@ public:
MsgId topicRootId,
const MTPPeerNotifySettings &settings);
void apply(
not_null<Data::ForumTopic*> topic,
not_null<ForumTopic*> topic,
const MTPPeerNotifySettings &settings);
void update(
not_null<Data::Thread*> thread,
Data::MuteValue muteForSeconds,
not_null<Thread*> thread,
MuteValue muteForSeconds,
std::optional<bool> silentPosts = std::nullopt,
std::optional<NotifySound> sound = std::nullopt,
std::optional<bool> storiesMuted = std::nullopt);
void resetToDefault(not_null<Data::Thread*> thread);
void resetToDefault(not_null<Thread*> thread);
void update(
not_null<PeerData*> peer,
Data::MuteValue muteForSeconds,
MuteValue muteForSeconds,
std::optional<bool> silentPosts = std::nullopt,
std::optional<NotifySound> sound = std::nullopt,
std::optional<bool> storiesMuted = std::nullopt);
void resetToDefault(not_null<PeerData*> peer);
void forumParentMuteUpdated(not_null<Data::Forum*> forum);
void forumParentMuteUpdated(not_null<Forum*> forum);
void cacheSound(DocumentId id);
void cacheSound(not_null<DocumentData*> document);
@ -84,18 +88,15 @@ public:
void defaultUpdate(
DefaultNotify type,
Data::MuteValue muteForSeconds,
MuteValue muteForSeconds,
std::optional<bool> silentPosts = std::nullopt,
std::optional<NotifySound> sound = std::nullopt,
std::optional<bool> storiesMuted = std::nullopt);
[[nodiscard]] bool isMuted(not_null<const Data::Thread*> thread) const;
[[nodiscard]] NotifySound sound(
not_null<const Data::Thread*> thread) const;
[[nodiscard]] bool muteUnknown(
not_null<const Data::Thread*> thread) const;
[[nodiscard]] bool soundUnknown(
not_null<const Data::Thread*> thread) const;
[[nodiscard]] bool isMuted(not_null<const Thread*> thread) const;
[[nodiscard]] NotifySound sound(not_null<const Thread*> thread) const;
[[nodiscard]] bool muteUnknown(not_null<const Thread*> thread) const;
[[nodiscard]] bool soundUnknown(not_null<const Thread*> thread) const;
[[nodiscard]] bool isMuted(not_null<const PeerData*> peer) const;
[[nodiscard]] bool silentPosts(not_null<const PeerData*> peer) const;
@ -105,7 +106,17 @@ public:
not_null<const PeerData*> peer) const;
[[nodiscard]] bool soundUnknown(not_null<const PeerData*> peer) const;
void loadExceptions();
[[nodiscard]] rpl::producer<DefaultNotify> exceptionsUpdates() const;
[[nodiscard]] auto exceptionsUpdatesRealtime() const
-> rpl::producer<DefaultNotify>;
[[nodiscard]] const base::flat_set<not_null<PeerData*>> &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<NotifySound> &sound);
[[nodiscard]] bool isMuted(
not_null<const Data::Thread*> thread,
not_null<const Thread*> thread,
crl::time *changesIn) const;
[[nodiscard]] bool isMuted(
not_null<const PeerData*> peer,
@ -126,21 +137,22 @@ private:
not_null<const PeerData*> peer) const;
[[nodiscard]] bool settingsUnknown(not_null<const PeerData*> peer) const;
[[nodiscard]] bool settingsUnknown(
not_null<const Data::Thread*> thread) const;
not_null<const Thread*> thread) const;
void unmuteByFinished();
void unmuteByFinishedDelayed(crl::time delay);
void updateLocal(not_null<Data::Thread*> thread);
void updateLocal(not_null<Thread*> thread);
void updateLocal(not_null<PeerData*> peer);
void updateLocal(DefaultNotify type);
void updateException(not_null<PeerData*> peer);
void exceptionsUpdated(DefaultNotify type);
const not_null<Session*> _owner;
DefaultValue _defaultValues[3];
std::unordered_set<not_null<const PeerData*>> _mutedPeers;
std::unordered_map<
not_null<Data::ForumTopic*>,
rpl::lifetime> _mutedTopics;
std::unordered_map<not_null<ForumTopic*>, rpl::lifetime> _mutedTopics;
base::Timer _unmuteByFinishedTimer;
struct {
@ -151,6 +163,14 @@ private:
rpl::lifetime pendingLifetime;
} _ringtones;
rpl::event_stream<DefaultNotify> _exceptionsUpdates;
rpl::event_stream<DefaultNotify> _exceptionsUpdatesRealtime;
std::array<
base::flat_set<not_null<PeerData*>>,
kDefaultNotifyTypes> _exceptions;
std::array<mtpRequestId, kDefaultNotifyTypes> _exceptionsRequestId = {};
std::array<bool, kDefaultNotifyTypes> _exceptionsUpdatesScheduled = {};
};
} // namespace Data

View File

@ -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<TimeId> PeerNotifySettings::muteUntil() const {
return _value
? _value->muteUntil()

View File

@ -46,6 +46,7 @@ public:
std::optional<bool> silentPosts,
std::optional<NotifySound> sound,
std::optional<bool> storiesMuted);
bool resetToDefault();
bool settingsUnknown() const;
std::optional<TimeId> muteUntil() const;

View File

@ -80,7 +80,7 @@ QPointer<Ui::RpWidget> Blocked::createPinnedToTop(not_null<QWidget*> parent) {
content,
tr::lng_blocked_list_add(),
st::settingsButtonActive,
{ &st::menuIconBlockSettings, IconType::Round, &st::transparent }
{ &st::menuIconBlockSettings }
)->addClickHandler([=] {
BlockedBoxController::BlockNewPeer(_controller);
});

View File

@ -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<Ui::FlatLabel>(
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<Ui::SettingsButton>(
container.get(),
nullptr,
st);
const auto checkView = toggleButton->lifetime().make_state<Ui::ToggleView>(
const auto checkView = button->lifetime().make_state<Ui::ToggleView>(
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) {

View File

@ -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<Main::Session*> session,
Notify type,
Fn<void(not_null<PeerData*>)> done);
Main::Session &session() const override;
void rowClicked(not_null<PeerListRow*> row) override;
base::unique_qptr<Ui::PopupMenu> rowContextMenu(
QWidget *parent,
not_null<PeerListRow*> row) override;
private:
void prepareViewHook() override;
std::unique_ptr<Row> createRow(not_null<History*> history) override;
const not_null<Main::Session*> _session;
const Notify _type;
const Fn<void(not_null<PeerData*>)> _done;
base::unique_qptr<Ui::PopupMenu> _menu;
PeerData *_lastClickedPeer = nullptr;
rpl::lifetime _lifetime;
};
class ExceptionsController final : public PeerListController {
public:
ExceptionsController(
not_null<Window::SessionController*> window,
Notify type);
Main::Session &session() const override;
void prepare() override;
void rowClicked(not_null<PeerListRow*> row) override;
base::unique_qptr<Ui::PopupMenu> rowContextMenu(
QWidget *parent,
not_null<PeerListRow*> row) override;
void rowRightActionClicked(not_null<PeerListRow*> row) override;
void loadMoreRows() override;
void bringToTop(not_null<PeerData*> peer);
[[nodiscard]] rpl::producer<int> countValue() const;
private:
void refreshRows();
bool appendRow(not_null<PeerData*> peer);
std::unique_ptr<PeerListRow> createRow(not_null<PeerData*> peer) const;
void refreshStatus(not_null<PeerListRow*> row) const;
void sort();
const not_null<Window::SessionController*> _window;
const Notify _type;
base::unique_qptr<Ui::PopupMenu> _menu;
base::flat_map<not_null<PeerData*>, int> _topOrdered;
int _topOrder = 0;
rpl::variable<int> _count;
rpl::lifetime _lifetime;
};
AddExceptionBoxController::AddExceptionBoxController(
not_null<Main::Session*> session,
Notify type,
Fn<void(not_null<PeerData*>)> 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<PeerListRow*> row) {
delegate()->peerListShowRowMenu(row, true);
}
base::unique_qptr<Ui::PopupMenu> AddExceptionBoxController::rowContextMenu(
QWidget *parent,
not_null<PeerListRow*> row) {
const auto peer = row->peer();
auto result = base::make_unique_q<Ui::PopupMenu>(
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<Ui::PopupMenu>(result.get());
_menu->setDestroyedCallback(crl::guard(this, [=] {
_lastClickedPeer = nullptr;
}));
_lastClickedPeer = peer;
return result;
}
auto AddExceptionBoxController::createRow(not_null<History*> history)
-> std::unique_ptr<AddExceptionBoxController::Row> {
if (Data::DefaultNotifyType(history->peer) != _type
|| history->peer->isSelf()
|| history->peer->isRepliesChat()) {
return nullptr;
}
return std::make_unique<Row>(history);
}
ExceptionsController::ExceptionsController(
not_null<Window::SessionController*> 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<PeerData*> peer) {
_topOrdered[peer] = ++_topOrder;
if (delegate()->peerListFindRow(peer->id.value)) {
sort();
}
}
rpl::producer<int> ExceptionsController::countValue() const {
return _count.value();
}
void ExceptionsController::rowClicked(not_null<PeerListRow*> row) {
delegate()->peerListShowRowMenu(row, true);
}
void ExceptionsController::rowRightActionClicked(
not_null<PeerListRow*> row) {
session().data().notifySettings().resetToDefault(row->peer());
}
void ExceptionsController::refreshRows() {
auto seen = base::flat_set<not_null<PeerData*>>();
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<Ui::PopupMenu> ExceptionsController::rowContextMenu(
QWidget *parent,
not_null<PeerListRow*> row) {
const auto peer = row->peer();
auto result = base::make_unique_q<Ui::PopupMenu>(
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<Ui::PopupMenu>(result.get());
return result;
}
bool ExceptionsController::appendRow(not_null<PeerData*> peer) {
delegate()->peerListAppendRow(createRow(peer));
return true;
}
std::unique_ptr<PeerListRow> ExceptionsController::createRow(
not_null<PeerData*> peer) const {
auto row = std::make_unique<PeerListRowWithLink>(peer);
row->setActionLink(tr::lng_notification_exceptions_remove(tr::now));
refreshStatus(row.get());
return row;
}
void ExceptionsController::refreshStatus(not_null<PeerListRow*> 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<PeerListRowId, QString>();
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 <Notify kType>
[[nodiscard]] Type Id() {
return &NotificationsTypeMetaImplementation<kType>::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<Ui::VerticalLayout*> container,
not_null<Window::SessionController*> controller,
Notify type) {
not_null<Ui::VerticalLayout*> container,
not_null<Window::SessionController*> window,
Notify type) {
const auto add = AddButton(
container,
tr::lng_notification_exceptions_add(),
st::settingsButtonActive,
{ &st::menuIconInviteSettings });
auto controller = std::make_unique<ExceptionsController>(window, type);
controller->setStyleOverrides(&st::settingsBlockedList);
const auto content = container->add(
object_ptr<PeerListContent>(container, controller.get()));
struct State {
std::unique_ptr<ExceptionsController> controller;
std::unique_ptr<PeerListContentDelegateSimple> delegate;
};
const auto state = content->lifetime().make_state<State>();
state->controller = std::move(controller);
state->delegate = std::make_unique<PeerListContentDelegateSimple>();
state->delegate->setContent(content);
state->controller->setDelegate(state->delegate.get());
add->setClickedCallback([=] {
const auto box = std::make_shared<QPointer<Ui::BoxContent>>();
const auto done = [=](not_null<PeerData*> peer) {
state->controller->bringToTop(peer);
if (*box) {
(*box)->closeBox();
}
};
auto controller = std::make_unique<AddExceptionBoxController>(
&window->session(),
type,
crl::guard(content, done));
auto initBox = [=](not_null<PeerListBox*> box) {
box->addButton(tr::lng_cancel(), [box] { box->closeBox(); });
};
*box = window->show(
Box<PeerListBox>(std::move(controller), std::move(initBox)));
});
const auto wrap = container->add(
object_ptr<Ui::SlideWrap<Ui::SettingsButton>>(
container,
CreateButton(
container,
tr::lng_notification_exceptions_clear(),
st::settingsAttentionButtonWithIcon,
{ &st::menuIconDeleteAttention })));
wrap->entity()->setClickedCallback([=] {
const auto clear = [=](Fn<void()> 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<bool> NotificationsEnabledForTypeValue(
not_null<Main::Session*> session,
Data::DefaultNotify type) {
Notify type) {
const auto settings = &session->data().notifySettings();
return rpl::single(
rpl::empty

View File

@ -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 }};