/* 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 "boxes/filters/edit_filter_box.h" #include "boxes/filters/edit_filter_chats_list.h" #include "boxes/filters/edit_filter_links.h" #include "boxes/premium_limits_box.h" #include "chat_helpers/emoji_suggestions_widget.h" #include "ui/layers/generic_box.h" #include "ui/text/text_utilities.h" #include "ui/text/text_options.h" #include "ui/widgets/buttons.h" #include "ui/widgets/fields/input_field.h" #include "ui/wrap/slide_wrap.h" #include "ui/effects/panel_animation.h" #include "ui/filter_icons.h" #include "ui/filter_icon_panel.h" #include "ui/painter.h" #include "data/data_channel.h" #include "data/data_chat_filters.h" #include "data/data_peer.h" #include "data/data_peer_values.h" // Data::AmPremiumValue. #include "data/data_session.h" #include "data/data_user.h" #include "core/application.h" #include "core/core_settings.h" #include "settings/settings_common.h" #include "base/event_filter.h" #include "lang/lang_keys.h" #include "history/history.h" #include "main/main_session.h" #include "window/window_session_controller.h" #include "window/window_controller.h" #include "apiwrap.h" #include "styles/style_settings.h" #include "styles/style_boxes.h" #include "styles/style_layers.h" #include "styles/style_window.h" #include "styles/style_chat.h" #include "styles/style_menu_icons.h" namespace { using namespace Settings; constexpr auto kMaxFilterTitleLength = 12; using Flag = Data::ChatFilter::Flag; using Flags = Data::ChatFilter::Flags; using ExceptionPeersRef = const base::flat_set> &; using ExceptionPeersGetter = ExceptionPeersRef(Data::ChatFilter::*)() const; constexpr auto kAllTypes = { Flag::Contacts, Flag::NonContacts, Flag::Groups, Flag::Channels, Flag::Bots, Flag::NoMuted, Flag::NoRead, Flag::NoArchived, }; class FilterChatsPreview final : public Ui::RpWidget { public: FilterChatsPreview( not_null parent, Flags flags, const base::flat_set> &peers); [[nodiscard]] rpl::producer flagRemoved() const; [[nodiscard]] rpl::producer> peerRemoved() const; void updateData( Flags flags, const base::flat_set> &peers); int resizeGetHeight(int newWidth) override; private: using Button = base::unique_qptr; struct FlagButton { Flag flag = Flag(); Button button; }; struct PeerButton { not_null history; Ui::PeerUserpicView userpic; Ui::Text::String name; Button button; }; void paintEvent(QPaintEvent *e) override; void refresh(); void removeFlag(Flag flag); void removePeer(not_null history); std::vector _removeFlag; std::vector _removePeer; rpl::event_stream _flagRemoved; rpl::event_stream> _peerRemoved; }; struct NameEditing { not_null field; bool custom = false; bool settingDefault = false; }; not_null SetupChatsPreview( not_null content, not_null*> data, Fn updateDefaultTitle, Flags flags, ExceptionPeersGetter peers) { const auto rules = data->current(); const auto preview = content->add(object_ptr( content, rules.flags() & flags, (rules.*peers)())); preview->flagRemoved( ) | rpl::start_with_next([=](Flag flag) { const auto rules = data->current(); auto computed = Data::ChatFilter( rules.id(), rules.title(), rules.iconEmoji(), (rules.flags() & ~flag), rules.always(), rules.pinned(), rules.never()); updateDefaultTitle(computed); *data = std::move(computed); }, preview->lifetime()); preview->peerRemoved( ) | rpl::start_with_next([=](not_null history) { const auto rules = data->current(); auto always = rules.always(); auto pinned = rules.pinned(); auto never = rules.never(); always.remove(history); pinned.erase(ranges::remove(pinned, history), end(pinned)); never.remove(history); auto computed = Data::ChatFilter( rules.id(), rules.title(), rules.iconEmoji(), rules.flags(), std::move(always), std::move(pinned), std::move(never)); updateDefaultTitle(computed); *data = std::move(computed); }, preview->lifetime()); return preview; } FilterChatsPreview::FilterChatsPreview( not_null parent, Flags flags, const base::flat_set> &peers) : RpWidget(parent) { updateData(flags, peers); } void FilterChatsPreview::refresh() { resizeToWidth(width()); } void FilterChatsPreview::updateData( Flags flags, const base::flat_set> &peers) { _removeFlag.clear(); _removePeer.clear(); const auto makeButton = [&](Fn handler) { auto result = base::make_unique_q( this, st::windowFilterSmallRemove); result->setClickedCallback(std::move(handler)); return result; }; for (const auto flag : kAllTypes) { if (flags & flag) { _removeFlag.push_back({ flag, makeButton([=] { removeFlag(flag); }) }); } } for (const auto &history : peers) { _removePeer.push_back(PeerButton{ .history = history, .button = makeButton([=] { removePeer(history); }) }); } refresh(); } int FilterChatsPreview::resizeGetHeight(int newWidth) { const auto right = st::windowFilterSmallRemoveRight; const auto add = (st::windowFilterSmallItem.height - st::windowFilterSmallRemove.height) / 2; auto top = 0; const auto moveNextButton = [&](not_null button) { button->moveToRight(right, top + add, newWidth); top += st::windowFilterSmallItem.height; }; for (const auto &[flag, button] : _removeFlag) { moveNextButton(button.get()); } for (const auto &[history, userpic, name, button] : _removePeer) { moveNextButton(button.get()); } return top; } void FilterChatsPreview::paintEvent(QPaintEvent *e) { auto p = Painter(this); auto top = 0; const auto &st = st::windowFilterSmallItem; const auto iconLeft = st.photoPosition.x(); const auto iconTop = st.photoPosition.y(); const auto nameLeft = st.namePosition.x(); p.setFont(st::windowFilterSmallItem.nameStyle.font); const auto nameTop = st.namePosition.y(); for (const auto &[flag, button] : _removeFlag) { PaintFilterChatsTypeIcon( p, flag, iconLeft, top + iconTop, width(), st.photoSize); p.setPen(st::contactsNameFg); p.drawTextLeft( nameLeft, top + nameTop, width(), FilterChatsTypeName(flag)); top += st.height; } for (auto &[history, userpic, name, button] : _removePeer) { const auto savedMessages = history->peer->isSelf(); const auto repliesMessages = history->peer->isRepliesChat(); if (savedMessages || repliesMessages) { if (savedMessages) { Ui::EmptyUserpic::PaintSavedMessages( p, iconLeft, top + iconTop, width(), st.photoSize); } else { Ui::EmptyUserpic::PaintRepliesMessages( p, iconLeft, top + iconTop, width(), st.photoSize); } p.setPen(st::contactsNameFg); p.drawTextLeft( nameLeft, top + nameTop, width(), (savedMessages ? tr::lng_saved_messages(tr::now) : tr::lng_replies_messages(tr::now))); } else { history->peer->paintUserpicLeft( p, userpic, iconLeft, top + iconTop, width(), st.photoSize); p.setPen(st::contactsNameFg); if (name.isEmpty()) { name.setText( st::msgNameStyle, history->peer->name(), Ui::NameTextOptions()); } name.drawLeftElided( p, nameLeft, top + nameTop, button->x() - nameLeft, width()); } top += st.height; } } void FilterChatsPreview::removeFlag(Flag flag) { const auto i = ranges::find(_removeFlag, flag, &FlagButton::flag); Assert(i != end(_removeFlag)); _removeFlag.erase(i); refresh(); _flagRemoved.fire_copy(flag); } void FilterChatsPreview::removePeer(not_null history) { const auto i = ranges::find(_removePeer, history, &PeerButton::history); Assert(i != end(_removePeer)); _removePeer.erase(i); refresh(); _peerRemoved.fire_copy(history); } rpl::producer FilterChatsPreview::flagRemoved() const { return _flagRemoved.events(); } rpl::producer> FilterChatsPreview::peerRemoved() const { return _peerRemoved.events(); } void EditExceptions( not_null window, not_null context, Flags options, not_null*> data, Fn updateDefaultTitle, Fn refresh) { const auto include = (options & Flag::Contacts) != Flags(0); const auto rules = data->current(); const auto session = &window->session(); auto controller = std::make_unique( session, (include ? tr::lng_filters_include_title() : tr::lng_filters_exclude_title()), options, rules.flags() & options, include ? rules.always() : rules.never(), [=](int count) { return Box(FilterChatsLimitBox, session, count, include); }); const auto rawController = controller.get(); auto initBox = [=](not_null box) { box->setCloseByOutsideClick(false); box->addButton(tr::lng_settings_save(), crl::guard(context, [=] { const auto peers = box->collectSelectedRows(); const auto rules = data->current(); auto &&histories = ranges::views::all( peers ) | ranges::views::transform([=](not_null peer) { return window->session().data().history(peer); }); auto changed = base::flat_set>{ histories.begin(), histories.end() }; auto removeFrom = include ? rules.never() : rules.always(); for (const auto &history : changed) { removeFrom.remove(history); } auto pinned = rules.pinned(); pinned.erase(ranges::remove_if(pinned, [&]( not_null history) { const auto contains = changed.contains(history); return include ? !contains : contains; }), end(pinned)); auto computed = Data::ChatFilter( rules.id(), rules.title(), rules.iconEmoji(), ((rules.flags() & ~options) | rawController->chosenOptions()), include ? std::move(changed) : std::move(removeFrom), std::move(pinned), include ? std::move(removeFrom) : std::move(changed)); updateDefaultTitle(computed); *data = computed; refresh(); box->closeBox(); })); box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); }; window->window().show( Box(std::move(controller), std::move(initBox))); } void CreateIconSelector( not_null outer, not_null box, not_null parent, not_null input, not_null*> data) { const auto rules = data->current(); const auto toggle = Ui::CreateChild(parent.get()); toggle->resize(st::windowFilterIconToggleSize); const auto type = toggle->lifetime().make_state(); data->value( ) | rpl::map([=](const Data::ChatFilter &filter) { return Ui::ComputeFilterIcon(filter); }) | rpl::start_with_next([=](Ui::FilterIcon icon) { *type = icon; toggle->update(); }, toggle->lifetime()); input->geometryValue( ) | rpl::start_with_next([=](QRect geometry) { const auto left = geometry.x() + geometry.width() - toggle->width(); const auto position = st::windowFilterIconTogglePosition; toggle->move( left - position.x(), geometry.y() + position.y()); }, toggle->lifetime()); toggle->paintRequest( ) | rpl::start_with_next([=] { auto p = QPainter(toggle); const auto icons = Ui::LookupFilterIcon(*type); icons.normal->paintInCenter( p, toggle->rect(), st::dialogsUnreadBgMuted->c); }, toggle->lifetime()); const auto panel = toggle->lifetime().make_state( outer); toggle->installEventFilter(panel); toggle->addClickHandler([=] { panel->toggleAnimated(); }); panel->chosen( ) | rpl::filter([=](Ui::FilterIcon icon) { return icon != Ui::ComputeFilterIcon(data->current()); }) | rpl::start_with_next([=](Ui::FilterIcon icon) { panel->hideAnimated(); const auto rules = data->current(); *data = Data::ChatFilter( rules.id(), rules.title(), Ui::LookupFilterIcon(icon).emoji, rules.flags(), rules.always(), rules.pinned(), rules.never()); }, panel->lifetime()); const auto updatePanelGeometry = [=] { const auto global = toggle->mapToGlobal({ toggle->width(), toggle->height() }); const auto local = outer->mapFromGlobal(global); const auto position = st::windwoFilterIconPanelPosition; const auto padding = panel->innerPadding(); panel->move( local.x() - panel->width() + position.x() + padding.right(), local.y() + position.y() - padding.top()); }; const auto filterForGeometry = [=](not_null event) { const auto type = event->type(); if (type == QEvent::Move || type == QEvent::Resize) { // updatePanelGeometry uses not only container geometry, but // also container children geometries that will be updated later. crl::on_main(panel, [=] { updatePanelGeometry(); }); } return base::EventFilterResult::Continue; }; const auto installFilterForGeometry = [&](not_null target) { panel->lifetime().make_state>( base::install_event_filter(target, filterForGeometry)); }; installFilterForGeometry(outer); installFilterForGeometry(box); } [[nodiscard]] QString DefaultTitle(const Data::ChatFilter &filter) { using Icon = Ui::FilterIcon; const auto icon = Ui::ComputeDefaultFilterIcon(filter); switch (icon) { case Icon::Private: return (filter.flags() & Data::ChatFilter::Flag::NonContacts) ? tr::lng_filters_name_people(tr::now) : tr::lng_filters_include_contacts(tr::now); case Icon::Groups: return tr::lng_filters_include_groups(tr::now); case Icon::Channels: return tr::lng_filters_include_channels(tr::now); case Icon::Bots: return tr::lng_filters_include_bots(tr::now); case Icon::Unread: return tr::lng_filters_name_unread(tr::now); case Icon::Unmuted: return tr::lng_filters_name_unmuted(tr::now); } return QString(); } not_null AddToggledButton( not_null container, rpl::producer shown, rpl::producer text, const style::SettingsButton &st, IconDescriptor &&descriptor) { const auto toggled = container->add( object_ptr>( container, CreateButton( container, std::move(text), st, std::move(descriptor))) )->toggleOn(std::move(shown), anim::type::instant)->setDuration(0); return toggled->entity(); } [[nodiscard]] QString TrimDefaultTitle(const QString &title) { return (title.size() <= kMaxFilterTitleLength) ? title : QString(); } } // namespace void EditFilterBox( not_null box, not_null window, const Data::ChatFilter &filter, Fn doneCallback, Fn next)> saveAnd) { using namespace rpl::mappers; struct State { rpl::variable rules; rpl::variable> links; rpl::variable hasLinks; rpl::variable chatlist; rpl::variable creating; }; const auto owner = &window->session().data(); const auto state = box->lifetime().make_state(State{ .rules = filter, .chatlist = filter.chatlist(), .creating = filter.title().isEmpty(), }); state->links = owner->chatsFilters().chatlistLinks(filter.id()), state->hasLinks = state->links.value() | rpl::map([=](const auto &v) { return !v.empty(); }); state->hasLinks.value() | rpl::filter( _1 ) | rpl::start_with_next([=] { state->chatlist = true; }, box->lifetime()); const auto data = &state->rules; owner->chatsFilters().isChatlistChanged( ) | rpl::filter([=](FilterId id) { return (id == data->current().id()); }) | rpl::start_with_next([=](FilterId id) { const auto filters = &owner->chatsFilters(); const auto &list = filters->list(); const auto i = ranges::find(list, id, &Data::ChatFilter::id); if (i == end(list)) { return; } *data = data->current().withChatlist(i->chatlist(), i->hasMyLinks()); if (!i->chatlist() && !state->hasLinks.current()) { state->chatlist = false; } }, box->lifetime()); box->setWidth(st::boxWideWidth); box->setTitle(rpl::conditional( state->creating.value(), tr::lng_filters_new(), tr::lng_filters_edit())); box->setCloseByOutsideClick(false); Data::AmPremiumValue( &window->session() ) | rpl::start_with_next([=] { box->closeBox(); }, box->lifetime()); const auto content = box->verticalLayout(); const auto name = content->add( object_ptr( box, st::windowFilterNameInput, tr::lng_filters_new_name(), filter.title()), st::markdownLinkFieldPadding); name->setMaxLength(kMaxFilterTitleLength); name->setInstantReplaces(Ui::InstantReplaces::Default()); name->setInstantReplacesEnabled( Core::App().settings().replaceEmojiValue()); Ui::Emoji::SuggestionsController::Init( box->getDelegate()->outerContainer(), name, &window->session()); const auto nameEditing = box->lifetime().make_state( NameEditing{ name }); state->creating.value( ) | rpl::filter(!_1) | rpl::start_with_next([=] { nameEditing->custom = true; }, box->lifetime()); name->changes( ) | rpl::start_with_next([=] { if (!nameEditing->settingDefault) { nameEditing->custom = true; } }, name->lifetime()); const auto updateDefaultTitle = [=](const Data::ChatFilter &filter) { if (nameEditing->custom) { return; } const auto title = TrimDefaultTitle(DefaultTitle(filter)); if (nameEditing->field->getLastText() != title) { nameEditing->settingDefault = true; nameEditing->field->setText(title); nameEditing->settingDefault = false; } }; const auto outer = box->getDelegate()->outerContainer(); CreateIconSelector( outer, box, content, name, data); constexpr auto kTypes = Flag::Contacts | Flag::NonContacts | Flag::Groups | Flag::Channels | Flag::Bots; constexpr auto kExcludeTypes = Flag::NoMuted | Flag::NoArchived | Flag::NoRead; box->setFocusCallback([=] { name->setFocusFast(); }); AddSkip(content); AddDivider(content); AddSkip(content); AddSubsectionTitle(content, tr::lng_filters_include()); const auto includeAdd = AddButton( content, tr::lng_filters_add_chats(), st::settingsButtonActive, { &st::settingsIconAdd, IconType::Round, &st::windowBgActive }); const auto include = SetupChatsPreview( content, data, updateDefaultTitle, kTypes, &Data::ChatFilter::always); AddSkip(content); AddDividerText(content, tr::lng_filters_include_about()); AddSkip(content); auto excludeWrap = content->add( object_ptr>( content, object_ptr(content)) )->setDuration(0); excludeWrap->toggleOn(state->chatlist.value() | rpl::map(!_1)); const auto excludeInner = excludeWrap->entity(); AddSubsectionTitle(excludeInner, tr::lng_filters_exclude()); const auto excludeAdd = AddButton( excludeInner, tr::lng_filters_remove_chats(), st::settingsButtonActive, { &st::settingsIconRemove, IconType::Round, &st::windowBgActive }); const auto exclude = SetupChatsPreview( excludeInner, data, updateDefaultTitle, kExcludeTypes, &Data::ChatFilter::never); AddSkip(excludeInner); AddDividerText(excludeInner, tr::lng_filters_exclude_about()); AddSkip(excludeInner); const auto collect = [=]() -> std::optional { const auto title = name->getLastText().trimmed(); const auto rules = data->current(); if (title.isEmpty()) { name->showError(); box->scrollToY(0); return {}; } else if (!(rules.flags() & kTypes) && rules.always().empty()) { window->window().showToast(tr::lng_filters_empty(tr::now)); return {}; } else if ((rules.flags() == (kTypes | Flag::NoArchived)) && rules.always().empty() && rules.never().empty()) { window->window().showToast(tr::lng_filters_default(tr::now)); return {}; } return rules.withTitle(title); }; AddSubsectionTitle( content, rpl::conditional( state->hasLinks.value(), tr::lng_filters_link_has(), tr::lng_filters_link())); state->hasLinks.changes() | rpl::start_with_next([=] { content->resizeToWidth(content->widthNoMargins()); }, content->lifetime()); if (filter.chatlist()) { window->session().data().chatsFilters().reloadChatlistLinks( filter.id()); } const auto createLink = AddToggledButton( content, state->hasLinks.value() | rpl::map(!rpl::mappers::_1), tr::lng_filters_link_create(), st::settingsButtonActive, { &st::settingsFolderShareIcon, IconType::Simple }); const auto addLink = AddToggledButton( content, state->hasLinks.value(), tr::lng_group_invite_add(), st::settingsButtonActive, { &st::settingsIconAdd, IconType::Round, &st::windowBgActive }); SetupFilterLinks( content, window, state->links.value(), [=] { return collect().value_or(Data::ChatFilter()); }); rpl::merge( createLink->clicks(), addLink->clicks() ) | rpl::filter( (rpl::mappers::_1 == Qt::LeftButton) ) | rpl::start_with_next([=](Qt::MouseButton button) { const auto result = collect(); if (!result || !GoodForExportFilterLink(window, *result)) { return; } const auto shared = CollectFilterLinkChats(*result); if (shared.empty()) { window->show(ShowLinkBox(window, *result, {})); return; } saveAnd(*result, crl::guard(box, [=](Data::ChatFilter updated) { state->creating = false; // Comparison of ChatFilter-s don't take id into account! data->force_assign(updated); const auto id = updated.id(); state->links = owner->chatsFilters().chatlistLinks(id); ExportFilterLink(id, shared, crl::guard(box, [=]( Data::ChatFilterLink link) { Expects(link.id == id); *data = data->current().withChatlist(true, true); window->show(ShowLinkBox(window, updated, link)); }), crl::guard(box, [=](QString error) { const auto session = &window->session(); if (error == u"CHATLISTS_TOO_MUCH"_q) { window->show(Box(ShareableFiltersLimitBox, session)); } else if (error == u"INVITES_TOO_MUCH"_q) { window->show(Box(FilterLinksLimitBox, session)); } else if (error == u"CHANNELS_TOO_MUCH"_q) { window->show(Box(ChannelsLimitBox, session)); } else if (error == u"USER_CHANNELS_TOO_MUCH"_q) { window->showToast( { tr::lng_filters_link_group_admin_error(tr::now) }); } else { window->show(ShowLinkBox(window, updated, { .id = id })); } })); })); }, createLink->lifetime()); AddSkip(content); AddDividerText( content, rpl::conditional( state->hasLinks.value(), tr::lng_filters_link_about_many(), tr::lng_filters_link_about())); const auto show = box->uiShow(); const auto refreshPreviews = [=] { include->updateData( data->current().flags() & kTypes, data->current().always()); exclude->updateData( data->current().flags() & kExcludeTypes, data->current().never()); }; includeAdd->setClickedCallback([=] { EditExceptions( window, box, kTypes | (state->chatlist.current() ? Flag::Chatlist : Flag()), data, updateDefaultTitle, refreshPreviews); }); excludeAdd->setClickedCallback([=] { EditExceptions( window, box, kExcludeTypes, data, updateDefaultTitle, refreshPreviews); }); const auto save = [=] { if (const auto result = collect()) { box->closeBox(); doneCallback(*result); } }; box->addButton(rpl::conditional( state->creating.value(), tr::lng_filters_create_button(), tr::lng_settings_save() ), save); box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); } void EditExistingFilter( not_null window, FilterId id) { Expects(id != 0); const auto session = &window->session(); const auto &list = session->data().chatsFilters().list(); const auto i = ranges::find(list, id, &Data::ChatFilter::id); if (i == end(list)) { return; } const auto doneCallback = [=](const Data::ChatFilter &result) { Expects(id == result.id()); const auto tl = result.tl(); session->data().chatsFilters().apply(MTP_updateDialogFilter( MTP_flags(MTPDupdateDialogFilter::Flag::f_filter), MTP_int(id), tl)); session->api().request(MTPmessages_UpdateDialogFilter( MTP_flags(MTPmessages_UpdateDialogFilter::Flag::f_filter), MTP_int(id), tl )).send(); }; const auto saveAnd = [=]( const Data::ChatFilter &data, Fn next) { doneCallback(data); next(data); }; window->window().show(Box( EditFilterBox, window, *i, crl::guard(session, doneCallback), crl::guard(session, saveAnd))); }