diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 6fc50700a6..d021af8f49 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -1662,6 +1662,8 @@ PRIVATE settings/sections/settings_notifications.h settings/sections/settings_privacy_security.cpp settings/sections/settings_privacy_security.h + settings/sections/settings_notifications_reactions.cpp + settings/sections/settings_notifications_reactions.h settings/sections/settings_notifications_type.cpp settings/sections/settings_notifications_type.h settings/sections/settings_passkeys.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index baabc03593..96d6db3340 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -565,6 +565,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_notification_private_chats" = "Private chats"; "lng_notification_groups" = "Groups"; "lng_notification_channels" = "Channels"; +"lng_notification_reactions" = "Reactions"; +"lng_notification_reactions_title" = "Notifications for reactions"; +"lng_notification_reactions_notify_about" = "Notify me about"; +"lng_notification_reactions_messages" = "Messages"; +"lng_notification_reactions_messages_full" = "Reactions to my messages"; +"lng_notification_reactions_poll_votes" = "Poll votes"; +"lng_notification_reactions_poll_votes_full" = "Votes in my polls"; +"lng_notification_reactions_from" = "Notify about reactions from"; +"lng_notification_reactions_from_nobody" = "Off"; +"lng_notification_reactions_from_contacts" = "From my contacts"; +"lng_notification_reactions_from_all" = "From everyone"; +"lng_notification_reactions_settings" = "Settings"; +"lng_notification_reactions_show_sender" = "Show sender's name"; "lng_notification_click_to_change" = "Click here to change"; "lng_notification_on" = "On, {exceptions}"; "lng_notification_off" = "Off, {exceptions}"; diff --git a/Telegram/SourceFiles/settings/sections/settings_notifications.cpp b/Telegram/SourceFiles/settings/sections/settings_notifications.cpp index b628a03239..81242142ad 100644 --- a/Telegram/SourceFiles/settings/sections/settings_notifications.cpp +++ b/Telegram/SourceFiles/settings/sections/settings_notifications.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/settings_common_session.h" #include "api/api_authorizations.h" +#include "api/api_reactions_notify_settings.h" #include "api/api_ringtones.h" #include "apiwrap.h" #include "base/platform/base_platform_info.h" @@ -29,6 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/settings_builder.h" #include "settings/sections/settings_main.h" #include "settings/settings_notifications_common.h" +#include "settings/sections/settings_notifications_reactions.h" #include "settings/sections/settings_notifications_type.h" #include "ui/boxes/confirm_box.h" #include "ui/chat/chat_theme.h" @@ -92,6 +94,78 @@ private: }; +SplitToggle SetupSplitToggle( + not_null container, + rpl::producer title, + const style::icon *icon, + bool checked, + rpl::producer details) { + const auto button = AddButtonWithIcon( + container, + std::move(title), + st::settingsNotificationType, + { icon }); + + const auto &st = st::settingsNotificationType; + + const auto label = Ui::CreateChild( + button.get(), + std::move(details), + st::settingsNotificationTypeDetails); + label->show(); + label->moveToLeft( + st.padding.left(), + st.padding.top() + st.height - label->height()); + label->setAttribute(Qt::WA_TransparentForMouseEvents); + + const auto toggle = Ui::CreateChild( + container.get(), + nullptr, + st); + const auto checkView = button->lifetime().make_state( + st.toggle, + checked, + [=] { toggle->update(); }); + + const auto separator = Ui::CreateChild(container.get()); + separator->paintRequest( + ) | rpl::on_next([=, bg = st.textBgOver] { + auto p = QPainter(separator); + p.fillRect(separator->rect(), bg); + }, separator->lifetime()); + const auto separatorHeight = st.height - 2 * st.toggle.border; + button->geometryValue( + ) | rpl::on_next([=](const QRect &r) { + const auto w = st::rightsButtonToggleWidth; + toggle->setGeometry( + r.x() + r.width() - w, + r.y(), + w, + r.height()); + separator->setGeometry( + toggle->x() - st::lineWidth, + r.y() + (r.height() - separatorHeight) / 2, + st::lineWidth, + separatorHeight); + }, toggle->lifetime()); + + const auto checkWidget = Ui::CreateChild(toggle); + checkWidget->resize(checkView->getSize()); + checkWidget->paintRequest( + ) | rpl::on_next([=] { + auto p = QPainter(checkWidget); + checkView->paint(p, 0, 0, checkWidget->width()); + }, checkWidget->lifetime()); + toggle->sizeValue( + ) | rpl::on_next([=](const QSize &s) { + checkWidget->moveToRight( + st.toggleSkip, + (s.height() - checkWidget->height()) / 2); + }, toggle->lifetime()); + + return { button, toggle, checkView }; +} + [[nodiscard]] not_null AddTypeButton( not_null container, not_null controller, @@ -114,18 +188,9 @@ private: } Unexpected("Type value in AddTypeButton."); }(); - const auto button = AddButtonWithIcon( - container, - std::move(label), - st::settingsNotificationType, - { icon }); - button->setClickedCallback([=] { - showOther(NotificationsType::Id(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( @@ -144,60 +209,16 @@ private: lt_count, rpl::single(float64(count)))); }) | rpl::flatten_latest(); - const auto details = Ui::CreateChild( - button.get(), - std::move(status), - st::settingsNotificationTypeDetails); - details->show(); - details->moveToLeft( - st.padding.left(), - st.padding.top() + st.height - details->height()); - details->setAttribute(Qt::WA_TransparentForMouseEvents); - const auto toggleButton = Ui::CreateChild( - container.get(), - nullptr, - st); - const auto checkView = button->lifetime().make_state( - st.toggle, + const auto [button, toggleButton, checkView] = SetupSplitToggle( + container, + std::move(label), + icon, NotificationsEnabledForType(session, type), - [=] { toggleButton->update(); }); - - const auto separator = Ui::CreateChild(container.get()); - separator->paintRequest( - ) | rpl::on_next([=, bg = st.textBgOver] { - auto p = QPainter(separator); - p.fillRect(separator->rect(), bg); - }, separator->lifetime()); - const auto separatorHeight = st.height - 2 * st.toggle.border; - button->geometryValue( - ) | rpl::on_next([=](const QRect &r) { - const auto w = st::rightsButtonToggleWidth; - toggleButton->setGeometry( - r.x() + r.width() - w, - r.y(), - w, - r.height()); - separator->setGeometry( - toggleButton->x() - st::lineWidth, - r.y() + (r.height() - separatorHeight) / 2, - st::lineWidth, - separatorHeight); - }, toggleButton->lifetime()); - - const auto checkWidget = Ui::CreateChild(toggleButton); - checkWidget->resize(checkView->getSize()); - checkWidget->paintRequest( - ) | rpl::on_next([=] { - auto p = QPainter(checkWidget); - checkView->paint(p, 0, 0, checkWidget->width()); - }, checkWidget->lifetime()); - toggleButton->sizeValue( - ) | rpl::on_next([=](const QSize &s) { - checkWidget->moveToRight( - st.toggleSkip, - (s.height() - checkWidget->height()) / 2); - }, toggleButton->lifetime()); + std::move(status)); + button->setClickedCallback([=] { + showOther(NotificationsType::Id(type)); + }); const auto toggle = crl::guard(toggleButton, [=] { const auto enabled = !checkView->checked(); @@ -247,6 +268,57 @@ private: return button; } +[[nodiscard]] not_null AddReactionsButton( + not_null container, + not_null controller, + Fn showOther) { + const auto session = &controller->session(); + auto &rs = session->api().reactionsNotifySettings(); + rs.reload(); + + using From = Api::ReactionsNotifyFrom; + auto status = rpl::combine( + rs.messagesFrom(), + rs.pollVotesFrom() + ) | rpl::map([](From messages, From pollVotes) { + auto parts = QStringList(); + if (messages != From::None) { + parts.push_back( + tr::lng_notification_reactions_messages(tr::now)); + } + if (pollVotes != From::None) { + parts.push_back( + tr::lng_notification_reactions_poll_votes(tr::now)); + } + return parts.isEmpty() + ? tr::lng_notification_click_to_change(tr::now) + : parts.join(u", "_q); + }); + + const auto [button, toggleButton, checkView] = SetupSplitToggle( + container, + tr::lng_notification_reactions(), + &st::menuIconGroupReactions, + rs.enabledCurrent(), + std::move(status)); + button->setClickedCallback([=] { + showOther(NotificationsReactions::Id()); + }); + + rs.enabled( + ) | rpl::on_next([=](bool enabled) { + checkView->setChecked(enabled, anim::type::normal); + }, button->lifetime()); + + toggleButton->clicks( + ) | rpl::on_next([=] { + const auto enabled = !checkView->checked(); + const auto from = enabled ? From::All : From::None; + session->api().reactionsNotifySettings().setAllFrom(from); + }, toggleButton->lifetime()); + return button; +} + NotificationsCount::NotificationsCount( QWidget *parent, not_null controller) @@ -1104,6 +1176,10 @@ void BuildNotifyTypeSection(SectionBuilder &builder) { controller, Data::DefaultNotify::Broadcast, showOther); + const auto reactions = AddReactionsButton( + ctx.container, + controller, + showOther); if (ctx.highlights) { ctx.highlights->push_back({ u"notifications/private"_q, @@ -1117,6 +1193,10 @@ void BuildNotifyTypeSection(SectionBuilder &builder) { u"notifications/channels"_q, { channels.get(), { .rippleShape = true } }, }); + ctx.highlights->push_back({ + u"notifications/reactions"_q, + { reactions.get(), { .rippleShape = true } }, + }); } return SectionBuilder::WidgetToAdd{}; }, [] { @@ -1143,6 +1223,14 @@ void BuildNotifyTypeSection(SectionBuilder &builder) { .icon = { &st::menuIconChannel }, }; }); + builder.add(nullptr, [] { + return SearchEntry{ + .id = u"notifications/reactions"_q, + .title = tr::lng_notification_reactions(tr::now), + .keywords = { u"reactions"_q }, + .icon = { &st::menuIconGroupReactions }, + }; + }); } void BuildEventNotificationsSection(SectionBuilder &builder) { diff --git a/Telegram/SourceFiles/settings/sections/settings_notifications_reactions.cpp b/Telegram/SourceFiles/settings/sections/settings_notifications_reactions.cpp new file mode 100644 index 0000000000..535cc4bbe2 --- /dev/null +++ b/Telegram/SourceFiles/settings/sections/settings_notifications_reactions.cpp @@ -0,0 +1,292 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "settings/sections/settings_notifications_reactions.h" + +#include "api/api_reactions_notify_settings.h" +#include "apiwrap.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "settings/sections/settings_notifications.h" +#include "settings/settings_builder.h" +#include "settings/settings_common.h" +#include "settings/settings_notifications_common.h" +#include "ui/layers/generic_box.h" +#include "ui/vertical_list.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/checkbox.h" +#include "ui/wrap/vertical_layout.h" +#include "window/window_session_controller.h" +#include "styles/style_boxes.h" +#include "styles/style_layers.h" +#include "styles/style_menu_icons.h" +#include "styles/style_settings.h" + +namespace Settings { +namespace { + +using NotifyFrom = Api::ReactionsNotifyFrom; +using namespace Builder; + +[[nodiscard]] rpl::producer FromLabel(NotifyFrom from) { + switch (from) { + case NotifyFrom::None: + return tr::lng_notification_reactions_from_nobody(); + case NotifyFrom::Contacts: + return tr::lng_notification_reactions_from_contacts(); + case NotifyFrom::All: + return tr::lng_notification_reactions_from_all(); + } + Unexpected("Value in FromLabel."); +} + +void ShowFromBox( + not_null controller, + NotifyFrom current, + Fn done) { + controller->show(Box([=](not_null box) { + box->setTitle(tr::lng_notification_reactions_from()); + + const auto initial = (current == NotifyFrom::None) + ? int(NotifyFrom::All) + : int(current); + const auto group = std::make_shared(initial); + + const auto addOption = [&](NotifyFrom value, const QString &label) { + box->addRow( + object_ptr( + box, + group, + int(value), + label, + st::defaultBoxCheckbox), + st::boxOptionListPadding + + QMargins( + st::boxPadding.left(), + 0, + st::boxPadding.right(), + st::boxOptionListSkip)); + }; + addOption( + NotifyFrom::All, + tr::lng_notification_reactions_from_all(tr::now)); + addOption( + NotifyFrom::Contacts, + tr::lng_notification_reactions_from_contacts(tr::now)); + box->addButton(tr::lng_box_ok(), [=] { + done(NotifyFrom(group->current())); + box->closeBox(); + }); + box->addButton(tr::lng_cancel(), [=] { + box->closeBox(); + }); + })); +} + +void AddToggleRow( + not_null container, + not_null controller, + rpl::producer title, + const style::icon *icon, + rpl::producer fromValue, + Fn fromCurrent, + Fn updateFrom) { + auto forToggle = rpl::duplicate(fromValue); + auto status = std::move( + fromValue + ) | rpl::map([](NotifyFrom from) { + return FromLabel(from); + }) | rpl::flatten_latest(); + + const auto [button, toggleButton, checkView] = SetupSplitToggle( + container, + std::move(title), + icon, + fromCurrent() != NotifyFrom::None, + std::move(status)); + + std::move( + forToggle + ) | rpl::on_next([=](NotifyFrom from) { + checkView->setChecked( + from != NotifyFrom::None, + anim::type::normal); + }, button->lifetime()); + + toggleButton->clicks( + ) | rpl::on_next([=] { + const auto enabled = !checkView->checked(); + updateFrom(enabled ? NotifyFrom::All : NotifyFrom::None); + }, toggleButton->lifetime()); + + button->setClickedCallback([=] { + if (fromCurrent() == NotifyFrom::None) { + updateFrom(NotifyFrom::All); + return; + } + ShowFromBox(controller, fromCurrent(), [=](NotifyFrom from) { + updateFrom(from); + }); + }); +} + +void BuildNotificationsReactionsContent(SectionBuilder &builder) { + builder.addSkip(st::settingsCheckboxesSkip); + builder.addSubsectionTitle({ + .id = u"notifications/reactions/about"_q, + .title = tr::lng_notification_reactions_notify_about(), + }); + + builder.add([](const WidgetContext &ctx) { + const auto session = &ctx.controller->session(); + auto &rs = session->api().reactionsNotifySettings(); + + AddToggleRow( + ctx.container, + ctx.controller, + tr::lng_notification_reactions_messages_full(), + &st::menuIconMarkUnread, + rs.messagesFrom(), + [session] { + return session->api().reactionsNotifySettings() + .messagesFromCurrent(); + }, + [session](NotifyFrom from) { + session->api().reactionsNotifySettings() + .updateMessagesFrom(from); + }); + + AddToggleRow( + ctx.container, + ctx.controller, + tr::lng_notification_reactions_poll_votes_full(), + &st::menuIconCreatePoll, + rs.pollVotesFrom(), + [session] { + return session->api().reactionsNotifySettings() + .pollVotesFromCurrent(); + }, + [session](NotifyFrom from) { + session->api().reactionsNotifySettings() + .updatePollVotesFrom(from); + }); + + return SectionBuilder::WidgetToAdd{}; + }, [] { + return SearchEntry{ + .id = u"notifications/reactions/messages"_q, + .title = tr::lng_notification_reactions_messages_full(tr::now), + .keywords = { u"reactions"_q, u"messages"_q }, + }; + }); + + builder.addSkip(st::settingsCheckboxesSkip); + builder.addDivider(); + builder.addSkip(st::settingsCheckboxesSkip); + builder.addSubsectionTitle({ + .id = u"notifications/reactions/settings"_q, + .title = tr::lng_notification_reactions_settings(), + }); + + builder.add([](const WidgetContext &ctx) { + const auto session = &ctx.controller->session(); + auto &rs = session->api().reactionsNotifySettings(); + + const auto showSender = AddButtonWithIcon( + ctx.container, + tr::lng_notification_reactions_show_sender(), + st::settingsButtonNoIcon + )->toggleOn(rs.showPreviews()); + + showSender->toggledChanges( + ) | rpl::filter([session](bool checked) { + return (checked + != session->api().reactionsNotifySettings() + .showPreviewsCurrent()); + }) | rpl::on_next([session](bool checked) { + session->api().reactionsNotifySettings() + .updateShowPreviews(checked); + }, showSender->lifetime()); + + return SectionBuilder::WidgetToAdd{}; + }, [] { + return SearchEntry{ + .id = u"notifications/reactions/preview"_q, + .title = tr::lng_notification_reactions_show_sender(tr::now), + .keywords = { u"sender"_q, u"preview"_q }, + }; + }); +} + +const auto kMeta = BuildHelper({ + .id = NotificationsReactions::Id(), + .parentId = NotificationsId(), + .title = &tr::lng_notification_reactions, + .icon = &st::menuIconGroupReactions, +}, [](SectionBuilder &builder) { + BuildNotificationsReactionsContent(builder); +}); + +} // namespace + +NotificationsReactions::NotificationsReactions( + QWidget *parent, + not_null controller) +: Section(parent, controller) { + setupContent(controller); +} + +rpl::producer NotificationsReactions::title() { + return tr::lng_notification_reactions_title(); +} + +void NotificationsReactions::setupContent( + not_null controller) { + const auto container = Ui::CreateChild(this); + + const SectionBuildMethod buildMethod = []( + not_null container, + not_null controller, + Fn showOther, + rpl::producer<> showFinished) { + auto &lifetime = container->lifetime(); + const auto highlights + = lifetime.make_state(); + + const auto session = &controller->session(); + auto &rs = session->api().reactionsNotifySettings(); + rs.reload(); + + auto builder = SectionBuilder(WidgetContext{ + .container = container, + .controller = controller, + .showOther = std::move(showOther), + .isPaused = Window::PausedIn( + controller, + Window::GifPauseReason::Layer), + .highlights = highlights, + }); + BuildNotificationsReactionsContent(builder); + + std::move(showFinished) | rpl::on_next([=] { + for (const auto &[id, entry] : *highlights) { + if (entry.widget) { + controller->checkHighlightControl( + id, + entry.widget, + base::duplicate(entry.args)); + } + } + }, lifetime); + }; + + build(container, buildMethod); + + Ui::ResizeFitChild(this, container); +} + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/sections/settings_notifications_reactions.h b/Telegram/SourceFiles/settings/sections/settings_notifications_reactions.h new file mode 100644 index 0000000000..21a661dce7 --- /dev/null +++ b/Telegram/SourceFiles/settings/sections/settings_notifications_reactions.h @@ -0,0 +1,27 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "settings/settings_common_session.h" + +namespace Settings { + +class NotificationsReactions : public Section { +public: + NotificationsReactions( + QWidget *parent, + not_null controller); + + [[nodiscard]] rpl::producer title() override; + +private: + void setupContent(not_null controller); + +}; + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings_notifications_common.h b/Telegram/SourceFiles/settings/settings_notifications_common.h index a3fb00de5f..a994906ba0 100644 --- a/Telegram/SourceFiles/settings/settings_notifications_common.h +++ b/Telegram/SourceFiles/settings/settings_notifications_common.h @@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Ui { class Checkbox; class SettingsButton; +class ToggleView; class VerticalLayout; template class SlideWrap; @@ -102,6 +103,19 @@ struct NotifyViewCheckboxes { bool nameShown, bool previewShown); +struct SplitToggle { + not_null button; + not_null toggle; + not_null checkView; +}; + +[[nodiscard]] SplitToggle SetupSplitToggle( + not_null container, + rpl::producer title, + const style::icon *icon, + bool checked, + rpl::producer details); + [[nodiscard]] not_null AddTypeButton( not_null container, not_null controller,