From b7d9d549ff846382ccec6aadba2ec3c775d1a8ef Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 21 Mar 2023 20:27:16 +0400 Subject: [PATCH] Start filter share links management. --- Telegram/CMakeLists.txt | 2 + .../Resources/icons/settings/folder_links.png | Bin 0 -> 467 bytes .../icons/settings/folder_links@2x.png | Bin 0 -> 883 bytes .../icons/settings/folder_links@3x.png | Bin 0 -> 1356 bytes Telegram/Resources/langs/lang.strings | 25 + .../boxes/filters/edit_filter_box.cpp | 202 +++- .../boxes/filters/edit_filter_box.h | 3 +- .../boxes/filters/edit_filter_chats_list.cpp | 14 +- .../boxes/filters/edit_filter_chats_list.h | 1 + .../boxes/filters/edit_filter_links.cpp | 871 ++++++++++++++++++ .../boxes/filters/edit_filter_links.h | 47 + Telegram/SourceFiles/boxes/peer_list_box.cpp | 4 + Telegram/SourceFiles/boxes/peer_list_box.h | 5 + .../boxes/peers/edit_peer_invite_link.cpp | 10 +- .../boxes/peers/edit_peer_invite_link.h | 7 + .../SourceFiles/data/data_chat_filters.cpp | 105 ++- Telegram/SourceFiles/data/data_chat_filters.h | 32 +- Telegram/SourceFiles/settings/settings.style | 1 + .../SourceFiles/settings/settings_folders.cpp | 87 +- 19 files changed, 1347 insertions(+), 69 deletions(-) create mode 100644 Telegram/Resources/icons/settings/folder_links.png create mode 100644 Telegram/Resources/icons/settings/folder_links@2x.png create mode 100644 Telegram/Resources/icons/settings/folder_links@3x.png create mode 100644 Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp create mode 100644 Telegram/SourceFiles/boxes/filters/edit_filter_links.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 2419fa804..709a7290b 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -173,6 +173,8 @@ PRIVATE boxes/filters/edit_filter_box.h boxes/filters/edit_filter_chats_list.cpp boxes/filters/edit_filter_chats_list.h + boxes/filters/edit_filter_links.cpp + boxes/filters/edit_filter_links.h boxes/peers/add_bot_to_chat_box.cpp boxes/peers/add_bot_to_chat_box.h boxes/peers/add_participants_box.cpp diff --git a/Telegram/Resources/icons/settings/folder_links.png b/Telegram/Resources/icons/settings/folder_links.png new file mode 100644 index 0000000000000000000000000000000000000000..da8497860bd8cd3ceacfbd7818e16a773899e398 GIT binary patch literal 467 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9jKx9jP7LeL$-D$|Tv8)E(|mmy zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uv49!D1}S`GTDlgf%+}MzF~maf z>6DFKEeTO`hM!>Hd~&X)P{egw zsuqX-jm3+*JlJ;?2nt^<-J2`DHN@=7%NK8R8{|b;@2*<4qA4`Ape~Wc{>oMUn6QWQ zeO5X4>#C;y61{#Pp){R2-o>BY?OIO6Lar`)wb_R7On*G#1griD9oJmcrr mnSa@Jhpoh9ubS=e)qmONRV;V*TIIb46ri52elF{r5}E)ed$2_S literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/settings/folder_links@2x.png b/Telegram/Resources/icons/settings/folder_links@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..26dfa285efff645450e8e87a5b0874ec3bab64df GIT binary patch literal 883 zcmV-(1C0EMP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91FrWhf1ONa40RR91FaQ7m0NXcg3;+NE-AP12R9Fe^m@%p*zfLkonnLsj`%xpH3i64*0a=GmH`{i=^!)tVS z#Kc@#8=({&15pucDrpf8bN5h!a;Ega(l_{XR}(Za=Dxt4u|!6Eu7wRcH{9_ z(AlJHHfv@hZ{Z~SL_EbGiL;Y(VuuaOaA(7bbM6uVgTcUnsegrx%3xwp?-6EiU?ZUe zCRq{$XtI-&LWnHY`jOS-Ha_N@lCkK8pg{{A(E?hGs1^_?7&@kPMfW`Y`{dNL4$6p@ zqNt!bvWdhum7)SvDiw;r`FvJ$YdiS#dcA73I-k#3q1MN4v>urqBC3b}MEIT(z*egj z6*=y4E@>Y$r?mzL4K&%gWfF;2VSIuh!^tV4*zvCNG~D#)UOE(tUvIcSDSK=kRb2Uk zF_$x@#6fAXV}?f|s;(dAPeH1teB!w4O%yhaL0AZkmY3cI6;f3gF~4*b|1_zNQzqDTLv`#Asr002ov JPDHLkV1gwNgiZhe literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/settings/folder_links@3x.png b/Telegram/Resources/icons/settings/folder_links@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..fef135b0dbd2215849e7011f6c91eac4e4905b5c GIT binary patch literal 1356 zcmY*ZeK->c7~hA_xy59uNo}*CO_-0CL#(hVcI+{940CSdlxZP7DTlHf))bY{CGvUq zbiQXLjQLzi4BIZbp6)C{sHt8b-stXmZQ}ENxIZf}5P;Lh1^^&00id%rq9sC0003M81c0>) z+^SW8{_9dI!2icvgM|dYHvoW6yT1=9Bwlx^^s&7i$!t))EtZLD;_4b1rR_kYB}Pb? z@JAhEWT;0@cuZfZc_R{S8jHH}ljVW%4h(uxs{Z8LFLw2;QKzH|_Yhr;8Xkp;fgsE+o12@tOeTt~eE(i1llAxaZ*Fc53=A|h;4ZG*IKV;H5WS5F+`P-0 z^E$4st`QLt?bprC&8w=a;BYwV=$N~c6R7%eM^jUOL zByu1t3mA;G##QA#v|a_C(wv^-a5%)lGw8aYoKMzl!_C0?2YuUYi6@9eB9FJV)x^X^ zSy@@q;S=|!r^7O4#>Pw>T9kH*gP4=qd!Mmp^D?dGHYAiay2$b&%a!%kw^&`K(WD0d zDiVdME+M@16sxOEG4b*7g=u$k-5sYqZQR*0sHekS>VV$-oi{Za%`oJ%jX=?x&ZMqO zB$73yu9`byo%-*7VdX<6Ti(4?4#-- zl~8?tE*238fJb%I*4kV(uz)K{h*0;l0)kE^o5gamgT)V7E(S1J#zZueP8)UT&C`V8uClgnf$0ioeJ5wL$5>jT0k5-@8&{p z<6+x7TP5{Ov(lbO7#tHFR(|UiJVxs3lt#(1^$*`21glcw3pX&QB8QSMGfYpK}o#qIQb#?ec8%&DoVM~-n&U(_y;6ReTI89C^PyzViivli!vy@_}JxvxOi zEHxdIiOrw9-LvgT-0t-fnWO((GY4oSgE3q&8){GpyJgXzwY0RvPr1%({mjda`16WA zm!2UXA0MBY5mdIdwW-x=GMUV`6?B)DmiE4_S+Djkf7MlJh-v>L?wixE2ZjgNnhbF- zQ4`PZKkHELN-3B118|+m37_a)3@~R==9?5W(|9FXDf$gtPy$l%yj0cbP?&Jz1Y!hm zPFw+UCOO_nGQBfiLF<_|dXdPFT1ygNb6Jc-V++82YPBq?QTHohW!%)o;tdWBNj=oP zL7S;_P{ynuk9~p~Jo6#U$B_LYnC@`&i5uUzaN(L@Np_%k%qCK>uc6u**!94pTYhZu zNPr19XM|xT;?Dg(^7?h66FBx&QPFmG-&sEn@C#(Uvs*FuNPeUuybJHy`egpTRG)i? HW3vARc8g%i literal 0 HcmV?d00001 diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index c10c0ca3c..e694cc160 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -3541,6 +3541,31 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_filters_toast_add" = "{chat} added to {folder} folder"; "lng_filters_toast_remove" = "{chat} removed from {folder} folder"; +"lng_filters_link" = "Invite Link"; +"lng_filters_link_create" = "Share Folder"; +"lng_filters_link_add" = "Add Link"; +"lng_filters_link_cant" = "You can only share folders that have several explicit chats selected, no general chat types and no exclusions."; +"lng_filters_link_about" = "Share access to some of this folder's groups and channels with others."; +"lng_filters_link_title" = "Share Folder"; +"lng_filters_link_select" = "Select the chats you want to share with the link."; +"lng_filters_by_link_title" = "Add Folder"; +"lng_filters_by_link_sure" = "Do you want to add a new chat folder {folder} and join its groups and channels?"; +"lng_filters_by_link_join#one" = "{count} chat to join"; +"lng_filters_by_link_join#other" = "{count} chats to join"; +"lng_filters_by_link_add" = "Add {folder}"; +"lng_filters_by_link_more" = "Add Chats to Folder"; +"lng_filters_by_link_more_sure" = "Do you want to join chats and add them to your folder {folder}?"; +"lng_filters_by_link_about" = "You can deselect the chats you don't want to join."; +"lng_filters_by_link_join_button" = "Join Chats"; +"lng_filters_by_link_remove" = "Remove Folder"; +"lng_filters_by_link_remove_sure" = "Do you want to quit the chats you joined when added the folder {folder}?"; +"lng_filters_by_link_quit#one" = "{count} chat to quit"; +"lng_filters_by_link_quit#other" = "{count} chats to quit"; +"lng_filters_by_link_select" = "Select All"; +"lng_filters_by_link_deselect" = "Deselect All"; +"lng_filters_by_link_remove_button" = "Remove Folder"; +"lng_filters_by_link_quit_button" = "Remove Folder and Chats"; + "lng_chat_theme_change" = "Change colors"; "lng_chat_theme_none" = "No\nTheme"; "lng_chat_theme_apply" = "Apply Theme"; diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp index e8fd6a843..26a0a6d31 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp @@ -8,20 +8,25 @@ 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 "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/toasts/common_toasts.h" #include "ui/widgets/buttons.h" #include "ui/widgets/input_fields.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" @@ -37,6 +42,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_layers.h" #include "styles/style_window.h" #include "styles/style_chat.h" +#include "styles/style_menu_icons.h" namespace { @@ -499,6 +505,24 @@ void CreateIconSelector( 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(); } @@ -509,7 +533,12 @@ void EditFilterBox( not_null box, not_null window, const Data::ChatFilter &filter, - Fn doneCallback) { + Fn doneCallback, + Fn next)> saveAnd) { + using namespace rpl::mappers; + const auto creating = filter.title().isEmpty(); box->setWidth(st::boxWideWidth); box->setTitle(creating ? tr::lng_filters_new() : tr::lng_filters_edit()); @@ -521,8 +550,27 @@ void EditFilterBox( box->closeBox(); }, box->lifetime()); - using State = rpl::variable; - const auto data = box->lifetime().make_state(filter); + struct State { + rpl::variable rules; + rpl::variable> links; + rpl::variable hasLinks; + rpl::variable community; + }; + const auto owner = &window->session().data(); + const auto state = box->lifetime().make_state(State{ + .rules = filter, + .community = filter.community(), + }); + state->links = owner->chatsFilters().communityLinks(filter.id()), + state->hasLinks = state->links.value() | rpl::map([=](const auto &v) { + return !v.empty(); + }); + if (!state->community.current()) { + state->community = state->hasLinks.value() | rpl::filter( + _1 + ) | rpl::take(1); + } + const auto data = &state->rules; const auto content = box->verticalLayout(); const auto name = content->add( @@ -604,24 +652,123 @@ void EditFilterBox( AddDividerText(content, tr::lng_filters_include_about()); AddSkip(content); - AddSubsectionTitle(content, tr::lng_filters_exclude()); + auto excludeWrap = content->add( + object_ptr>( + content, + object_ptr(content)) + )->setDuration(0); + excludeWrap->toggleOn(state->community.value() | rpl::map(!_1)); + const auto excludeInner = excludeWrap->entity(); + + AddSubsectionTitle(excludeInner, tr::lng_filters_exclude()); const auto excludeAdd = AddButton( - content, + excludeInner, tr::lng_filters_remove_chats(), st::settingsButtonActive, { &st::settingsIconRemove, 0, IconType::Round, &st::windowBgActive }); const auto exclude = SetupChatsPreview( - content, + excludeInner, data, updateDefaultTitle, kExcludeTypes, &Data::ChatFilter::never); - AddSkip(content); - AddDividerText(content, tr::lng_filters_exclude_about()); + 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(); + const auto result = Data::ChatFilter( + rules.id(), + title, + rules.iconEmoji(), + rules.flags(), + rules.always(), + rules.pinned(), + rules.never()); + if (title.isEmpty()) { + name->showError(); + 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 result; + }; + + AddSubsectionTitle(content, tr::lng_filters_link()); + + if (filter.community()) { + window->session().data().chatsFilters().reloadCommunityLinks( + filter.id()); + } + + const auto createLink = AddToggledButton( + content, + state->hasLinks.value() | rpl::map(!rpl::mappers::_1), + tr::lng_filters_link_create(), + st::settingsButtonActive, + { &st::settingsFolderShareIcon, 0, IconType::Simple }); + const auto addLink = AddToggledButton( + content, + state->hasLinks.value(), + tr::lng_group_invite_add(), + st::settingsButtonActive, + { &st::settingsIconAdd, 0, 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()) { + // langs + Ui::ShowMultilineToast({ + .parentOverride = Window::Show(window).toastParent(), + .text = { tr::lng_filters_link_cant(tr::now) }, + }); + return; + } + saveAnd(*result, crl::guard(box, [=](Data::ChatFilter updated) { + box->setTitle(tr::lng_filters_edit()); + nameEditing->custom = true; + + *data = updated; + const auto id = updated.id(); + state->links = owner->chatsFilters().communityLinks(id); + ExportFilterLink(id, shared, [=](Data::ChatFilterLink link) { + Expects(link.id == id); + + window->show(ShowLinkBox(window, updated, link)); + }); + })); + }, createLink->lifetime()); + + AddSkip(content); + AddDividerText(content, tr::lng_filters_link_about()); + + const auto show = std::make_shared(box); const auto refreshPreviews = [=] { include->updateData( data->current().flags() & kTypes, @@ -634,7 +781,7 @@ void EditFilterBox( EditExceptions( window, box, - kTypes, + kTypes | (state->community.current() ? Flag::Community : Flag()), data, updateDefaultTitle, refreshPreviews); @@ -650,32 +797,12 @@ void EditFilterBox( }); const auto save = [=] { - const auto title = name->getLastText().trimmed(); - const auto rules = data->current(); - const auto result = Data::ChatFilter( - rules.id(), - title, - rules.iconEmoji(), - rules.flags(), - rules.always(), - rules.pinned(), - rules.never()); - if (title.isEmpty()) { - name->showError(); - 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; + if (const auto result = collect()) { + box->closeBox(); + doneCallback(*result); } - box->closeBox(); - - doneCallback(result); }; + box->addButton( creating ? tr::lng_filters_create_button() : tr::lng_settings_save(), save); @@ -707,9 +834,16 @@ void EditExistingFilter( 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, doneCallback), + crl::guard(session, saveAnd))); } diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_box.h b/Telegram/SourceFiles/boxes/filters/edit_filter_box.h index 43b97adb1..695dbbe92 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_box.h +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_box.h @@ -21,7 +21,8 @@ void EditFilterBox( not_null box, not_null window, const Data::ChatFilter &filter, - Fn doneCallback); + Fn doneCallback, + Fn)> saveAnd); void EditExistingFilter( not_null window, diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp index e1aacddf9..ae50f0bcc 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp @@ -343,9 +343,10 @@ EditFilterChatsListController::EditFilterChatsListController( , _session(session) , _title(std::move(title)) , _peers(peers) -, _options(options) +, _options(options & ~Flag::Community) , _selected(selected) -, _limit(Limit(session)) { +, _limit(Limit(session)) +, _community(options & Flag::Community) { } Main::Session &EditFilterChatsListController::session() const { @@ -353,8 +354,11 @@ Main::Session &EditFilterChatsListController::session() const { } int EditFilterChatsListController::selectedTypesCount() const { - Expects(_typesDelegate != nullptr); + Expects(_community || _typesDelegate != nullptr); + if (_community) { + return 0; + } auto result = 0; for (auto i = 0; i != _typesDelegate->peerListFullRowsCount(); ++i) { if (_typesDelegate->peerListRowAt(i)->checked()) { @@ -396,7 +400,9 @@ bool EditFilterChatsListController::handleDeselectForeignRow( void EditFilterChatsListController::prepareViewHook() { delegate()->peerListSetTitle(std::move(_title)); - delegate()->peerListSetAboveWidget(prepareTypesList()); + if (!_community) { + delegate()->peerListSetAboveWidget(prepareTypesList()); + } const auto count = int(_peers.size()); const auto rows = std::make_unique[]>(count); diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.h b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.h index f6a658672..b752fb739 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.h +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.h @@ -75,6 +75,7 @@ private: Flags _options; Flags _selected; int _limit = 0; + bool _community = false; Fn _deselectOption; diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp new file mode 100644 index 000000000..5a6a6fa72 --- /dev/null +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp @@ -0,0 +1,871 @@ +/* +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_links.h" + +#include "apiwrap.h" +#include "boxes/peers/edit_peer_invite_link.h" // InviteLinkQrBox. +#include "boxes/peer_list_box.h" +#include "data/data_channel.h" +#include "data/data_chat_filters.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "history/history.h" +#include "lang/lang_keys.h" +#include "lottie/lottie_icon.h" +#include "main/main_session.h" +#include "settings/settings_common.h" +#include "ui/boxes/confirm_box.h" +#include "ui/controls/invite_link_buttons.h" +#include "ui/controls/invite_link_label.h" +#include "ui/toasts/common_toasts.h" +#include "ui/widgets/input_fields.h" +#include "ui/widgets/popup_menu.h" +#include "ui/wrap/vertical_layout.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/painter.h" +#include "window/window_session_controller.h" +#include "styles/style_info.h" +#include "styles/style_layers.h" +#include "styles/style_menu_icons.h" +#include "styles/style_settings.h" + +#include + +namespace { + +constexpr auto kMaxLinkTitleLength = 32; + +using InviteLinkData = Data::ChatFilterLink; +class Row; + +enum class Color { + Permanent, + + Count, +}; + +struct InviteLinkAction { + enum class Type { + Copy, + Share, + Edit, + Delete, + }; + QString link; + Type type = Type::Copy; +}; + +[[nodiscard]] std::optional ErrorForSharing( + not_null history) { + const auto peer = history->peer; + if (const auto user = peer->asUser()) { // langs + return user->isBot() + ? u"you can't share chats with bots"_q + : u"you can't share private chats"_q; + } else if (const auto channel = history->peer->asChannel()) { + if (!channel->canHaveInviteLink()) { + return u"you can't invite others here"_q; + } + return std::nullopt; + } else { + return u"you can't share this :("_q; + } +} + +void ChatFilterLinkBox( + not_null box, + Data::ChatFilterLink data) { + using namespace rpl::mappers; + + const auto link = data.url; + box->setTitle(tr::lng_group_invite_edit_title()); + + const auto container = box->verticalLayout(); + const auto addTitle = [&]( + not_null container, + rpl::producer text) { + container->add( + object_ptr( + container, + std::move(text), + st::settingsSubsectionTitle), + (st::settingsSubsectionTitlePadding + + style::margins(0, st::settingsSectionSkip, 0, 0))); + }; + const auto addDivider = [&]( + not_null container, + rpl::producer text, + style::margins margins = style::margins()) { + container->add( + object_ptr( + container, + object_ptr( + container, + std::move(text), + st::boxDividerLabel), + st::settingsDividerLabelPadding), + margins); + }; + + struct State { + }; + const auto state = box->lifetime().make_state(State{ + }); + + const auto labelField = container->add( + object_ptr( + container, + st::defaultInputField, + tr::lng_group_invite_label_header(), + data.title), + style::margins( + st::settingsSubsectionTitlePadding.left(), + st::settingsSectionSkip, + st::settingsSubsectionTitlePadding.right(), + st::settingsSectionSkip * 2)); + labelField->setMaxLength(kMaxLinkTitleLength); + Settings::AddDivider(container); + + const auto &saveLabel = link.isEmpty() + ? tr::lng_formatting_link_create + : tr::lng_settings_save; + box->addButton(saveLabel(), [=] {}); + box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); +} + +class RowDelegate { +public: + virtual void rowUpdateRow(not_null row) = 0; + virtual void rowPaintIcon( + QPainter &p, + int x, + int y, + int size, + Color color) = 0; +}; + +class Row final : public PeerListRow { +public: + Row(not_null delegate, const InviteLinkData &data); + + void update(const InviteLinkData &data); + + [[nodiscard]] InviteLinkData data() const; + + QString generateName() override; + QString generateShortName() override; + PaintRoundImageCallback generatePaintUserpicCallback( + bool forceRound) override; + + QSize rightActionSize() const override; + QMargins rightActionMargins() const override; + void rightActionPaint( + Painter &p, + int x, + int y, + int outerWidth, + bool selected, + bool actionSelected) override; + +private: + const not_null _delegate; + InviteLinkData _data; + QString _status; + Color _color = Color::Permanent; + +}; + +[[nodiscard]] uint64 ComputeRowId(const QString &link) { + return XXH64(link.data(), link.size() * sizeof(ushort), 0); +} + +[[nodiscard]] uint64 ComputeRowId(const InviteLinkData &data) { + return ComputeRowId(data.url); +} + +[[nodiscard]] Color ComputeColor(const InviteLinkData &link) { + return Color::Permanent; +} + +[[nodiscard]] QString ComputeStatus(const InviteLinkData &link) { + return tr::lng_filters_chats_count(tr::now, lt_count, link.chats.size()); +} + +Row::Row(not_null delegate, const InviteLinkData &data) +: PeerListRow(ComputeRowId(data)) +, _delegate(delegate) +, _data(data) +, _color(ComputeColor(data)) { + setCustomStatus(ComputeStatus(data)); +} + +void Row::update(const InviteLinkData &data) { + _data = data; + _color = ComputeColor(data); + setCustomStatus(ComputeStatus(data)); + refreshName(st::inviteLinkList.item); + _delegate->rowUpdateRow(this); +} + +InviteLinkData Row::data() const { + return _data; +} + +QString Row::generateName() { + if (!_data.title.isEmpty()) { + return _data.title; + } + auto result = _data.url; + return result.replace( + u"https://"_q, + QString() + ).replace( + u"t.me/+"_q, + QString() + ).replace( + u"t.me/joinchat/"_q, + QString() + ); +} + +QString Row::generateShortName() { + return generateName(); +} + +PaintRoundImageCallback Row::generatePaintUserpicCallback(bool forceRound) { + return [=]( + QPainter &p, + int x, + int y, + int outerWidth, + int size) { + _delegate->rowPaintIcon(p, x, y, size, _color); + }; +} + +QSize Row::rightActionSize() const { + return QSize( + st::inviteLinkThreeDotsIcon.width(), + st::inviteLinkThreeDotsIcon.height()); +} + +QMargins Row::rightActionMargins() const { + return QMargins( + 0, + (st::inviteLinkList.item.height - rightActionSize().height()) / 2, + st::inviteLinkThreeDotsSkip, + 0); +} + +void Row::rightActionPaint( + Painter &p, + int x, + int y, + int outerWidth, + bool selected, + bool actionSelected) { + (actionSelected + ? st::inviteLinkThreeDotsIconOver + : st::inviteLinkThreeDotsIcon).paint(p, x, y, outerWidth); +} + +class LinksController final + : public PeerListController + , public RowDelegate + , public base::has_weak_ptr { +public: + LinksController( + not_null window, + rpl::producer> content, + Fn currentFilter); + + void prepare() override; + void rowClicked(not_null row) override; + void rowRightActionClicked(not_null row) override; + base::unique_qptr rowContextMenu( + QWidget *parent, + not_null row) override; + Main::Session &session() const override; + + void rowUpdateRow(not_null row) override; + void rowPaintIcon( + QPainter &p, + int x, + int y, + int size, + Color color) override; + +private: + void appendRow(const InviteLinkData &data); + bool removeRow(const QString &link); + + void rebuild(const std::vector &rows); + + [[nodiscard]] base::unique_qptr createRowContextMenu( + QWidget *parent, + not_null row); + + const not_null _window; + Fn _currentFilter; + rpl::variable> _rows; + base::unique_qptr _menu; + + std::array _icons; + rpl::lifetime _lifetime; + +}; + +class LinkController final + : public PeerListController + , public base::has_weak_ptr { +public: + LinkController( + not_null window, + const Data::ChatFilter &filter, + InviteLinkData data); + + void prepare() override; + void rowClicked(not_null row) override; + Main::Session &session() const override; + + void showFinished() override; + + [[nodiscard]] rpl::producer hasChangesValue() const; + +private: + void setupAboveWidget(); + void addHeader(not_null container); + void addLinkBlock(not_null container); + + const not_null _window; + InviteLinkData _data; + + base::flat_set> _filterChats; + base::flat_set> _allowed; + rpl::variable _selected = 0; + + base::unique_qptr _menu; + + QString _link; + + Ui::RpWidget *_headerWidget = nullptr; + rpl::variable _addedHeight; + rpl::variable _hasChanges = false; + + rpl::event_stream<> _showFinished; + + rpl::lifetime _lifetime; + +}; + +LinkController::LinkController( + not_null window, + const Data::ChatFilter &filter, + InviteLinkData data) +: _window(window) +, _filterChats(filter.always()) { + _data = std::move(data); + _link = _data.url; +} + +void LinkController::addHeader(not_null container) { + using namespace Settings; + + const auto divider = Ui::CreateChild( + container.get()); + const auto verticalLayout = container->add( + object_ptr(container.get())); + + auto icon = CreateLottieIcon( + verticalLayout, + { + .name = u"filters"_q, + .sizeOverride = { + st::settingsFilterIconSize, + st::settingsFilterIconSize, + }, + }, + st::settingsFilterIconPadding); + _showFinished.events( + ) | rpl::start_with_next([animate = std::move(icon.animate)] { + animate(anim::repeat::once); + }, verticalLayout->lifetime()); + verticalLayout->add(std::move(icon.widget)); + + verticalLayout->add( + object_ptr>( + verticalLayout, + object_ptr( + verticalLayout, + tr::lng_filters_about(), // langs + st::settingsFilterDividerLabel)), + st::settingsFilterDividerLabelPadding); + + verticalLayout->geometryValue( + ) | rpl::start_with_next([=](const QRect &r) { + divider->setGeometry(r); + }, divider->lifetime()); +} + +void LinkController::addLinkBlock(not_null container) { + using namespace Settings; + + const auto link = _data.url; + const auto weak = Ui::MakeWeak(container); + const auto copyLink = crl::guard(weak, [=] { + CopyInviteLink(delegate()->peerListToastParent(), link); + }); + const auto shareLink = crl::guard(weak, [=] { + delegate()->peerListShowBox( + ShareInviteLinkBox(&_window->session(), link), + Ui::LayerOption::KeepOther); + }); + const auto getLinkQr = crl::guard(weak, [=] { + delegate()->peerListShowBox( + InviteLinkQrBox(link), + Ui::LayerOption::KeepOther); + }); + const auto editLink = crl::guard(weak, [=] { + //delegate()->peerListShowBox( + // EditLinkBox(_window, _data.current()), + // Ui::LayerOption::KeepOther); + }); + const auto deleteLink = crl::guard(weak, [=] { + //delegate()->peerListShowBox( + // DeleteLinkBox(_window, _data.current()), + // Ui::LayerOption::KeepOther); + }); + + const auto createMenu = [=] { + auto result = base::make_unique_q( + container, + st::popupMenuWithIcons); + result->addAction( + tr::lng_group_invite_context_copy(tr::now), + copyLink, + &st::menuIconCopy); + result->addAction( + tr::lng_group_invite_context_share(tr::now), + shareLink, + &st::menuIconShare); + result->addAction( + tr::lng_group_invite_context_qr(tr::now), + getLinkQr, + &st::menuIconQrCode); + result->addAction( + tr::lng_group_invite_context_edit(tr::now), + editLink, + &st::menuIconEdit); + result->addAction( + tr::lng_group_invite_context_delete(tr::now), + deleteLink, + &st::menuIconDelete); + return result; + }; + + AddSubsectionTitle(container, tr::lng_manage_peer_link_invite()); + + const auto prefix = u"https://"_q; + const auto label = container->lifetime().make_state( + container, + rpl::single(link.startsWith(prefix) + ? link.mid(prefix.size()) + : link), + createMenu); + container->add( + label->take(), + st::inviteLinkFieldPadding); + + label->clicks( + ) | rpl::start_with_next(copyLink, label->lifetime()); + + AddCopyShareLinkButtons(container, copyLink, shareLink); + + AddSkip(container, st::inviteLinkJoinedRowPadding.bottom() * 2); + + AddSkip(container); + + AddDivider(container); +} + +void LinkController::prepare() { + setupAboveWidget(); + auto selected = 0; + for (const auto &history : _data.chats) { + const auto peer = history->peer; + _allowed.emplace(peer); + auto row = std::make_unique(peer); + const auto raw = row.get(); + delegate()->peerListAppendRow(std::move(row)); + delegate()->peerListSetRowChecked(raw, true); + ++selected; + } + for (const auto &history : _filterChats) { + if (delegate()->peerListFindRow(history->peer->id.value)) { + continue; + } + const auto peer = history->peer; + auto row = std::make_unique(peer); + const auto raw = row.get(); + delegate()->peerListAppendRow(std::move(row)); + if (const auto error = ErrorForSharing(history)) { + raw->setCustomStatus(*error); + } else { + _allowed.emplace(peer); + } + } + delegate()->peerListRefreshRows(); + _selected = selected; +} + +void LinkController::rowClicked(not_null row) { + if (_allowed.contains(row->peer())) { + const auto checked = row->checked(); + delegate()->peerListSetRowChecked(row, !checked); + _selected = _selected.current() + (checked ? -1 : 1); + } +} + +void LinkController::showFinished() { + _showFinished.fire({}); +} + +void LinkController::setupAboveWidget() { + using namespace Settings; + + auto wrap = object_ptr((QWidget*)nullptr); + const auto container = wrap.data(); + + addHeader(container); + if (!_data.url.isEmpty()) { + addLinkBlock(container); + } + + Settings::AddSubsectionTitle( + container, + rpl::single(u"3 chats selected"_q)); + + delegate()->peerListSetAboveWidget(std::move(wrap)); +} + +Main::Session &LinkController::session() const { + return _window->session(); +} + +rpl::producer LinkController::hasChangesValue() const { + return _hasChanges.value(); +} + +LinksController::LinksController( + not_null window, + rpl::producer> content, + Fn currentFilter) +: _window(window) +, _currentFilter(std::move(currentFilter)) +, _rows(std::move(content)) { + style::PaletteChanged( + ) | rpl::start_with_next([=] { + for (auto &image : _icons) { + image = QImage(); + } + }, _lifetime); +} + +void LinksController::prepare() { + _rows.value( + ) | rpl::start_with_next([=](const std::vector &rows) { + rebuild(rows); + }, _lifetime); +} + +void LinksController::rebuild(const std::vector &rows) { + auto i = 0; + auto count = delegate()->peerListFullRowsCount(); + while (i < rows.size()) { + if (i < count) { + const auto row = delegate()->peerListRowAt(i); + static_cast(row.get())->update(rows[i]); + } else { + appendRow(rows[i]); + } + ++i; + } + while (i < count) { + delegate()->peerListRemoveRow(delegate()->peerListRowAt(i)); + --count; + } + delegate()->peerListRefreshRows(); +} + +void LinksController::rowClicked(not_null row) { + const auto link = static_cast(row.get())->data(); + delegate()->peerListShowBox( + ShowLinkBox(_window, _currentFilter(), link), + Ui::LayerOption::KeepOther); +} + +void LinksController::rowRightActionClicked(not_null row) { + delegate()->peerListShowRowMenu(row, true); +} + +base::unique_qptr LinksController::rowContextMenu( + QWidget *parent, + not_null row) { + auto result = createRowContextMenu(parent, row); + + if (result) { + // 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; +} + +base::unique_qptr LinksController::createRowContextMenu( + QWidget *parent, + not_null row) { + const auto real = static_cast(row.get()); + const auto data = real->data(); + const auto link = data.url; + auto result = base::make_unique_q( + parent, + st::popupMenuWithIcons); + result->addAction(tr::lng_group_invite_context_copy(tr::now), [=] { + //CopyInviteLink(delegate()->peerListToastParent(), link); + }, &st::menuIconCopy); + result->addAction(tr::lng_group_invite_context_share(tr::now), [=] { + //delegate()->peerListShowBox( + // ShareInviteLinkBox(_peer, link), + // Ui::LayerOption::KeepOther); + }, &st::menuIconShare); + result->addAction(tr::lng_group_invite_context_qr(tr::now), [=] { + //delegate()->peerListShowBox( + // InviteLinkQrBox(link), + // Ui::LayerOption::KeepOther); + }, &st::menuIconQrCode); + result->addAction(tr::lng_group_invite_context_edit(tr::now), [=] { + //delegate()->peerListShowBox( + // EditLinkBox(_peer, data), + // Ui::LayerOption::KeepOther); + }, &st::menuIconEdit); + result->addAction(tr::lng_group_invite_context_delete(tr::now), [=] { + //delegate()->peerListShowBox( + // DeleteLinkBox(_peer, _admin, link), + // Ui::LayerOption::KeepOther); + }, &st::menuIconDelete); + return result; +} + +Main::Session &LinksController::session() const { + return _window->session(); +} + +void LinksController::appendRow(const InviteLinkData &data) { + delegate()->peerListAppendRow(std::make_unique(this, data)); +} + +bool LinksController::removeRow(const QString &link) { + if (const auto row = delegate()->peerListFindRow(ComputeRowId(link))) { + delegate()->peerListRemoveRow(row); + return true; + } + return false; +} + +void LinksController::rowUpdateRow(not_null row) { + delegate()->peerListUpdateRow(row); +} + +void LinksController::rowPaintIcon( + QPainter &p, + int x, + int y, + int size, + Color color) { + const auto skip = st::inviteLinkIconSkip; + const auto inner = size - 2 * skip; + const auto bg = [&] { + switch (color) { + case Color::Permanent: return &st::msgFile1Bg; + } + Unexpected("Color in LinksController::rowPaintIcon."); + }(); + const auto stroke = st::inviteLinkIconStroke; + auto &icon = _icons[int(color)]; + if (icon.isNull()) { + icon = QImage( + QSize(inner, inner) * style::DevicePixelRatio(), + QImage::Format_ARGB32_Premultiplied); + icon.fill(Qt::transparent); + icon.setDevicePixelRatio(style::DevicePixelRatio()); + + auto p = QPainter(&icon); + p.setPen(Qt::NoPen); + p.setBrush(*bg); + { + auto hq = PainterHighQualityEnabler(p); + p.drawEllipse(QRect(0, 0, inner, inner)); + } + st::inviteLinkIcon.paintInCenter(p, { 0, 0, inner, inner }); + } + p.drawImage(x + skip, y + skip, icon); +} + +class LinkChatsController final + : public PeerListController + , public base::has_weak_ptr { +public: + LinkChatsController( + not_null controller, + FilterId id, + const InviteLinkData &data); + ~LinkChatsController(); + + void prepare() override; + void rowClicked(not_null row) override; + Main::Session &session() const override; + +private: + const not_null _controller; + const FilterId _id = 0; + InviteLinkData _data; + +}; + +LinkChatsController::LinkChatsController( + not_null controller, + FilterId id, + const InviteLinkData &data) +: _controller(controller) +, _id(id) +, _data(data) { +} + +LinkChatsController::~LinkChatsController() = default; + +void LinkChatsController::prepare() { + for (const auto &history : _data.chats) { + delegate()->peerListAppendRow( + std::make_unique(history->peer)); + } + delegate()->peerListRefreshRows(); +} + +void LinkChatsController::rowClicked(not_null row) { +} + +Main::Session &LinkChatsController::session() const { + return _controller->session(); +} + +} // namespace + +std::vector> CollectFilterLinkChats( + const Data::ChatFilter &filter) { + return filter.always() | ranges::views::filter([]( + not_null history) { + return !ErrorForSharing(history); + }) | ranges::views::transform(&History::peer) | ranges::to_vector; +} + +bool GoodForExportFilterLink( + not_null window, + const Data::ChatFilter &filter) { + using Flag = Data::ChatFilter::Flag; + if (!filter.never().empty() || (filter.flags() & ~Flag::Community)) { + Ui::ShowMultilineToast({ + .parentOverride = Window::Show(window).toastParent(), + .text = { tr::lng_filters_link_cant(tr::now) }, + }); + return false; + } + return true; +} + +void ExportFilterLink( + FilterId id, + const std::vector> &peers, + Fn done) { + Expects(!peers.empty()); + + const auto front = peers.front(); + const auto session = &front->session(); + auto mtpPeers = peers | ranges::views::transform( + [](not_null peer) { return MTPInputPeer(peer->input); } + ) | ranges::to(); + session->api().request(MTPcommunities_ExportCommunityInvite( + MTP_inputCommunityDialogFilter(MTP_int(id)), + MTP_string(), + MTP_vector(std::move(mtpPeers)) + )).done([=](const MTPcommunities_ExportedCommunityInvite &result) { + const auto &data = result.data(); + session->data().chatsFilters().apply(MTP_updateDialogFilter( + MTP_flags(MTPDupdateDialogFilter::Flag::f_filter), + MTP_int(id), + data.vfilter())); + const auto link = session->data().chatsFilters().add( + id, + data.vinvite()); + done(link); + }).fail([=](const MTP::Error &error) { + done({ .id = id }); + }).send(); +} + +object_ptr ShowLinkBox( + not_null window, + const Data::ChatFilter &filter, + const Data::ChatFilterLink &link) { + auto controller = std::make_unique(window, filter, link); + const auto raw = controller.get(); + auto initBox = [=](not_null box) { + box->setTitle(!link.title.isEmpty() + ? rpl::single(link.title) + : tr::lng_manage_peer_link_invite()); + + raw->hasChangesValue( + ) | rpl::start_with_next([=](bool has) { + box->clearButtons(); + if (has) { + box->addButton(tr::lng_settings_save(), [=] { + + }); + box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); + } else { + box->addButton(tr::lng_about_done(), [=] { + box->closeBox(); + }); + } + }, box->lifetime()); + }; + return Box(std::move(controller), std::move(initBox)); +} + +void SetupFilterLinks( + not_null container, + not_null window, + rpl::producer> value, + Fn currentFilter) { + auto &lifetime = container->lifetime(); + const auto delegate = lifetime.make_state( + std::make_shared(window)); + const auto controller = lifetime.make_state( + window, + std::move(value), + std::move(currentFilter)); + controller->setStyleOverrides(&st::inviteLinkList); + const auto content = container->add(object_ptr( + container, + controller)); + delegate->setContent(content); + controller->setDelegate(delegate); +} diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_links.h b/Telegram/SourceFiles/boxes/filters/edit_filter_links.h new file mode 100644 index 000000000..d632d3472 --- /dev/null +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_links.h @@ -0,0 +1,47 @@ +/* +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 "base/object_ptr.h" + +namespace Ui { +class Show; +class BoxContent; +class VerticalLayout; +} // namespace Ui + +namespace Data { +class ChatFilter; +struct ChatFilterLink; +} // namespace Data + +namespace Window { +class SessionController; +} // namespace Window + +[[nodiscard]] std::vector> CollectFilterLinkChats( + const Data::ChatFilter &filter); +[[nodiscard]] bool GoodForExportFilterLink( + not_null window, + const Data::ChatFilter &filter); + +void ExportFilterLink( + FilterId id, + const std::vector> &peers, + Fn done); + +object_ptr ShowLinkBox( + not_null window, + const Data::ChatFilter &filter, + const Data::ChatFilterLink &link); + +void SetupFilterLinks( + not_null container, + not_null window, + rpl::producer> value, + Fn currentFilter); diff --git a/Telegram/SourceFiles/boxes/peer_list_box.cpp b/Telegram/SourceFiles/boxes/peer_list_box.cpp index eda808ae6..2dbe7c67a 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_box.cpp @@ -143,6 +143,10 @@ void PeerListBox::setAddedTopScrollSkip(int skip) { updateScrollSkips(); } +void PeerListBox::showFinished() { + _controller->showFinished(); +} + int PeerListBox::getTopScrollSkip() const { auto result = _addedTopScrollSkip; if (_select && !_select->isHidden()) { diff --git a/Telegram/SourceFiles/boxes/peer_list_box.h b/Telegram/SourceFiles/boxes/peer_list_box.h index 3a09aebf9..1f1c0e10a 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.h +++ b/Telegram/SourceFiles/boxes/peer_list_box.h @@ -447,6 +447,9 @@ public: virtual void prepare() = 0; + virtual void showFinished() { + } + virtual void rowClicked(not_null row) = 0; virtual void rowRightActionClicked(not_null row) { } @@ -1050,6 +1053,8 @@ public: void setAddedTopScrollSkip(int skip); + void showFinished() override; + protected: void prepare() override; void setInnerFocus() override; diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp index 416c4ecb7..050dccc99 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp @@ -1128,6 +1128,12 @@ void CopyInviteLink(not_null toastParent, const QString &link) { object_ptr ShareInviteLinkBox( not_null peer, const QString &link) { + return ShareInviteLinkBox(&peer->session(), link); +} + +object_ptr ShareInviteLinkBox( + not_null session, + const QString &link) { const auto sending = std::make_shared(); const auto box = std::make_shared>(); @@ -1187,7 +1193,7 @@ object_ptr ShareInviteLinkBox( } else { comment.text = link; } - auto &api = peer->session().api(); + auto &api = session->api(); for (const auto thread : result) { auto message = Api::MessageToSend( Api::SendAction(thread, options)); @@ -1204,7 +1210,7 @@ object_ptr ShareInviteLinkBox( return Data::CanSendTexts(thread); }; auto object = Box(ShareBox::Descriptor{ - .session = &peer->session(), + .session = session, .copyCallback = std::move(copyCallback), .submitCallback = std::move(submitCallback), .filterCallback = std::move(filterCallback), diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.h b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.h index 0c190da4c..7fc9c2d53 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.h +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.h @@ -15,6 +15,10 @@ namespace Api { struct InviteLink; } // namespace Api +namespace Main { +class Session; +} // namespace Main + namespace Ui { class VerticalLayout; class Show; @@ -38,6 +42,9 @@ void CopyInviteLink(not_null toastParent, const QString &link); [[nodiscard]] object_ptr ShareInviteLinkBox( not_null peer, const QString &link); +[[nodiscard]] object_ptr ShareInviteLinkBox( + not_null session, + const QString &link); [[nodiscard]] object_ptr InviteLinkQrBox(const QString &link); [[nodiscard]] object_ptr RevokeLinkBox( not_null peer, diff --git a/Telegram/SourceFiles/data/data_chat_filters.cpp b/Telegram/SourceFiles/data/data_chat_filters.cpp index fb0531c14..7f255f4a6 100644 --- a/Telegram/SourceFiles/data/data_chat_filters.cpp +++ b/Telegram/SourceFiles/data/data_chat_filters.cpp @@ -148,14 +148,19 @@ ChatFilter ChatFilter::FromTL( data.vid().v, qs(data.vtitle()), qs(data.vemoticon().value_or_empty()), - (Flag::Community - | (data.is_community_can_admin() ? Flag::Admin : Flag())), + Flag::Community, std::move(list), std::move(pinned), {}); }); } +ChatFilter ChatFilter::withId(FilterId id) const { + auto result = *this; + result._id = id; + return result; +} + MTPDialogFilter ChatFilter::tl(FilterId replaceId) const { auto always = _always; auto pinned = QVector(); @@ -171,10 +176,7 @@ MTPDialogFilter ChatFilter::tl(FilterId replaceId) const { } if (_flags & Flag::Community) { using TLFlag = MTPDdialogFilterCommunity::Flag; - const auto flags = TLFlag::f_emoticon - | ((_flags & Flag::Admin) - ? TLFlag::f_community_can_admin - : TLFlag(0)); + const auto flags = TLFlag::f_emoticon; return MTP_dialogFilterCommunity( MTP_flags(flags), MTP_int(replaceId ? replaceId : _id), @@ -226,6 +228,10 @@ ChatFilter::Flags ChatFilter::flags() const { return _flags; } +bool ChatFilter::community() const { + return _flags & Flag::Community; +} + const base::flat_set> &ChatFilter::always() const { return _always; } @@ -392,6 +398,93 @@ void ChatFilters::apply(const MTPUpdate &update) { }); } +ChatFilterLink ChatFilters::add( + FilterId id, + const MTPExportedCommunityInvite &update) { + const auto i = ranges::find(_list, id, &ChatFilter::id); + if (i == end(_list) || !i->community()) { + LOG(("Api Error: " + "Attempt to add community link to a non-community filter: %1" + ).arg(id)); + return {}; + } + auto &links = _communityLinks[id]; + const auto &data = update.data(); + const auto url = qs(data.vurl()); + const auto title = qs(data.vtitle()); + auto chats = data.vpeers().v | ranges::views::transform([&]( + const MTPPeer &peer) { + return _owner->history(peerFromMTP(peer)); + }) | ranges::to_vector; + const auto j = ranges::find(links, url, &ChatFilterLink::url); + if (j != end(links)) { + if (j->title != title || j->chats != chats) { + j->title = title; + j->chats = std::move(chats); + _communityLinksUpdated.fire_copy(id); + } + return *j; + } + links.push_back({ + .id = id, + .url = url, + .title = title, + .chats = std::move(chats), + }); + _communityLinksUpdated.fire_copy(id); + return links.back(); +} + +void ChatFilters::edit( + FilterId id, + const QString &url, + const QString &title) { + auto &links = _communityLinks[id]; + const auto i = ranges::find(links, url, &ChatFilterLink::url); + if (i != end(links)) { + i->title = title; + _communityLinksUpdated.fire_copy(id); + } +} + +void ChatFilters::remove(FilterId id, const QString &url) { + auto &links = _communityLinks[id]; + const auto i = ranges::find(links, url, &ChatFilterLink::url); + if (i != end(links)) { + links.erase(i); + _communityLinksUpdated.fire_copy(id); + } +} + +rpl::producer> ChatFilters::communityLinks( + FilterId id) const { + return _communityLinksUpdated.events_starting_with_copy( + id + ) | rpl::filter(rpl::mappers::_1 == id) | rpl::map([=] { + const auto i = _communityLinks.find(id); + return (i != end(_communityLinks)) + ? i->second + : std::vector(); + }); +} + +void ChatFilters::reloadCommunityLinks(FilterId id) { + const auto api = &_owner->session().api(); + api->request(_linksRequestId).cancel(); + _linksRequestId = api->request(MTPcommunities_GetExportedInvites( + MTP_inputCommunityDialogFilter(MTP_int(id)) + )).done([=](const MTPcommunities_ExportedInvites &result) { + const auto &data = result.data(); + _owner->processUsers(data.vusers()); + _owner->processChats(data.vchats()); + _communityLinks[id].clear(); + for (const auto &link : data.vinvites().v) { + add(id, link); + } + _communityLinksUpdated.fire_copy(id); + }).send(); +} + void ChatFilters::set(ChatFilter filter) { if (!filter.id()) { return; diff --git a/Telegram/SourceFiles/data/data_chat_filters.h b/Telegram/SourceFiles/data/data_chat_filters.h index 96214f96a..ff5e697a4 100644 --- a/Telegram/SourceFiles/data/data_chat_filters.h +++ b/Telegram/SourceFiles/data/data_chat_filters.h @@ -33,7 +33,6 @@ public: NoArchived = (1 << 7), Community = (1 << 8), - Admin = (1 << 9), }; friend constexpr inline bool is_flag_type(Flag) { return true; }; using Flags = base::flags; @@ -48,6 +47,8 @@ public: std::vector> pinned, base::flat_set> never); + [[nodiscard]] ChatFilter withId(FilterId id) const; + [[nodiscard]] static ChatFilter FromTL( const MTPDialogFilter &data, not_null owner); @@ -57,7 +58,7 @@ public: [[nodiscard]] QString title() const; [[nodiscard]] QString iconEmoji() const; [[nodiscard]] Flags flags() const; - [[nodiscard]] bool admin() const; + [[nodiscard]] bool community() const; [[nodiscard]] const base::flat_set> &always() const; [[nodiscard]] const std::vector> &pinned() const; [[nodiscard]] const base::flat_set> &never() const; @@ -87,6 +88,17 @@ inline bool operator!=(const ChatFilter &a, const ChatFilter &b) { return !(a == b); } +struct ChatFilterLink { + FilterId id = 0; + QString url; + QString title; + std::vector> chats; + + friend inline bool operator==( + const ChatFilterLink &a, + const ChatFilterLink &b) = default; +}; + struct SuggestedFilter { ChatFilter filter; QString description; @@ -134,6 +146,18 @@ public: -> const std::vector &; [[nodiscard]] rpl::producer<> suggestedUpdated() const; + ChatFilterLink add( + FilterId id, + const MTPExportedCommunityInvite &update); + void edit( + FilterId id, + const QString &url, + const QString &title); + void remove(FilterId id, const QString &url); + rpl::producer> communityLinks( + FilterId id) const; + void reloadCommunityLinks(FilterId id); + private: void load(bool force); void received(const QVector &list); @@ -160,6 +184,10 @@ private: std::deque _exceptionsToLoad; mtpRequestId _exceptionsLoadRequestId = 0; + base::flat_map> _communityLinks; + rpl::event_stream _communityLinksUpdated; + mtpRequestId _linksRequestId = 0; + }; } // namespace Data diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style index e2a72af3b..85a982fb6 100644 --- a/Telegram/SourceFiles/settings/settings.style +++ b/Telegram/SourceFiles/settings/settings.style @@ -133,6 +133,7 @@ settingsTTLChatsOn: icon {{ "settings/ttl/autodelete_on", windowActiveTextFg }}; settingsIconAdd: icon {{ "settings/add", windowFgActive }}; settingsIconRemove: icon {{ "settings/remove", windowFgActive }}; +settingsFolderShareIcon: icon {{ "settings/folder_links", lightButtonFg }}; settingsCheckbox: Checkbox(defaultBoxCheckbox) { textPosition: point(15px, 1px); diff --git a/Telegram/SourceFiles/settings/settings_folders.cpp b/Telegram/SourceFiles/settings/settings_folders.cpp index 414455bea..4a8dc7e70 100644 --- a/Telegram/SourceFiles/settings/settings_folders.cpp +++ b/Telegram/SourceFiles/settings/settings_folders.cpp @@ -334,16 +334,24 @@ void FilterRowButton::paintEvent(QPaintEvent *e) { AddSkip(container, st::settingsSectionSkip); AddSubsectionTitle(container, tr::lng_filters_subtitle()); - const auto rows = lifetime.make_state>(); - const auto rowsCount = lifetime.make_state>(); + struct State { + std::vector rows; + rpl::variable count; + rpl::variable suggested; + Fn)> save; + }; + + const auto state = lifetime.make_state(); const auto find = [=](not_null button) { - const auto i = ranges::find(*rows, button, &FilterRow::button); - Assert(i != end(*rows)); + const auto i = ranges::find(state->rows, button, &FilterRow::button); + Assert(i != end(state->rows)); return &*i; }; const auto showLimitReached = [=] { - const auto removed = ranges::count_if(*rows, &FilterRow::removed); - if (rows->size() < limit() + removed) { + const auto removed = ranges::count_if( + state->rows, + &FilterRow::removed); + if (state->rows.size() < limit() + removed) { return false; } controller->show(Box(FiltersLimitBox, session)); @@ -376,14 +384,23 @@ void FilterRowButton::paintEvent(QPaintEvent *e) { find(button)->filter = result; button->updateData(result); }; + const auto saveAnd = [=]( + const Data::ChatFilter &data, + Fn next) { + const auto found = find(button); + found->filter = data; + button->updateData(data); + state->save(button, next); + }; controller->window().show(Box( EditFilterBox, controller, found->filter, - crl::guard(button, doneCallback))); + crl::guard(button, doneCallback), + crl::guard(button, saveAnd))); }); - rows->push_back({ button, filter }); - *rowsCount = rows->size(); + state->rows.push_back({ button, filter }); + state->count = state->rows.size(); const auto filters = &controller->session().data().chatsFilters(); const auto id = filter.id(); @@ -418,6 +435,8 @@ void FilterRowButton::paintEvent(QPaintEvent *e) { } wrap->resizeToWidth(container->width()); + + return button; }; const auto &list = session->data().chatsFilters().list(); for (const auto &filter : list) { @@ -438,11 +457,17 @@ void FilterRowButton::paintEvent(QPaintEvent *e) { const auto doneCallback = [=](const Data::ChatFilter &result) { addFilter(result); }; + const auto saveAnd = [=]( + const Data::ChatFilter &data, + Fn next) { + state->save(addFilter(data), next); + }; controller->window().show(Box( EditFilterBox, controller, Data::ChatFilter(), - crl::guard(container, doneCallback))); + crl::guard(container, doneCallback), + crl::guard(container, saveAnd))); }); AddSkip(container); const auto nonEmptyAbout = container->add( @@ -455,7 +480,6 @@ void FilterRowButton::paintEvent(QPaintEvent *e) { AddSkip(aboutRows); AddSubsectionTitle(aboutRows, tr::lng_filters_recommended()); - const auto suggested = lifetime.make_state>(); rpl::single(rpl::empty) | rpl::then( session->data().chatsFilters().suggestedUpdated() ) | rpl::map([=] { @@ -468,10 +492,10 @@ void FilterRowButton::paintEvent(QPaintEvent *e) { const std::vector &suggestions) { for (const auto &suggestion : suggestions) { const auto &filter = suggestion.filter; - if (ranges::contains(*rows, filter, &FilterRow::filter)) { + if (ranges::contains(state->rows, filter, &FilterRow::filter)) { continue; } - *suggested = suggested->current() + 1; + state->suggested = state->suggested.current() + 1; const auto button = aboutRows->add(object_ptr( aboutRows, filter, @@ -482,7 +506,7 @@ void FilterRowButton::paintEvent(QPaintEvent *e) { return; } addFilter(filter); - *suggested = suggested->current() - 1; + state->suggested = state->suggested.current() - 1; delete button; }, button->lifetime()); } @@ -491,8 +515,8 @@ void FilterRowButton::paintEvent(QPaintEvent *e) { }, aboutRows->lifetime()); auto showSuggestions = rpl::combine( - suggested->value(), - rowsCount->value(), + state->suggested.value(), + state->count.value(), Data::AmPremiumValue(session) ) | rpl::map([limit](int suggested, int count, bool) { return suggested > 0 && count < limit(); @@ -511,7 +535,7 @@ void FilterRowButton::paintEvent(QPaintEvent *e) { return localId; }; auto result = base::flat_map, FilterId>(); - for (auto &row : *rows) { + for (auto &row : state->rows) { const auto id = row.filter.id(); if (row.removed) { continue; @@ -523,9 +547,13 @@ void FilterRowButton::paintEvent(QPaintEvent *e) { return result; }; - return [=] { + state->save = [=]( + const FilterRowButton *single, + Fn next) { auto ids = prepareGoodIdsForNewFilters(); + auto updated = Data::ChatFilter(); + auto order = std::vector(); auto updates = std::vector(); auto addRequests = std::vector(); @@ -533,8 +561,11 @@ void FilterRowButton::paintEvent(QPaintEvent *e) { auto &realFilters = session->data().chatsFilters(); const auto &list = realFilters.list(); - order.reserve(rows->size()); - for (const auto &row : *rows) { + order.reserve(state->rows.size()); + for (auto &row : state->rows) { + if (row.button.get() == single) { + updated = row.filter; + } const auto id = row.filter.id(); const auto removed = row.removed; const auto i = ranges::find(list, id, &Data::ChatFilter::id); @@ -545,6 +576,13 @@ void FilterRowButton::paintEvent(QPaintEvent *e) { continue; } const auto newId = ids.take(row.button).value_or(id); + if (newId != id) { + row.filter = row.filter.withId(newId); + row.button->updateData(row.filter); + if (row.button.get() == single) { + updated = row.filter; + } + } const auto tl = removed ? MTPDialogFilter() : row.filter.tl(newId); @@ -582,6 +620,8 @@ void FilterRowButton::paintEvent(QPaintEvent *e) { } crl::on_main(session, [ session, + next, + updated, order = std::move(order), updates = std::move(updates), addRequests = std::move(addRequests), @@ -604,8 +644,15 @@ void FilterRowButton::paintEvent(QPaintEvent *e) { if (!order.empty() && !addRequests.empty()) { filters->saveOrder(order, previousId); } + if (next) { + Assert(updated.id() != 0); + next(updated); + } }); }; + return [copy = state->save] { + copy(nullptr, nullptr); + }; } void SetupTopContent(