From 725c22e776925cf4748e55aaa08fbbd7f9bfc39c Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 20 Mar 2023 17:12:17 +0400 Subject: [PATCH 001/676] Update API scheme to layer 158. --- Telegram/Resources/tl/api.tl | 47 +++++++++-- Telegram/SourceFiles/api/api_peer_photo.cpp | 5 ++ .../SourceFiles/data/data_chat_filters.cpp | 84 ++++++++++++++++--- Telegram/SourceFiles/data/data_chat_filters.h | 22 +++-- 4 files changed, 127 insertions(+), 31 deletions(-) diff --git a/Telegram/Resources/tl/api.tl b/Telegram/Resources/tl/api.tl index eeb3dc749..c2ac7601d 100644 --- a/Telegram/Resources/tl/api.tl +++ b/Telegram/Resources/tl/api.tl @@ -110,7 +110,7 @@ storage.fileMp4#b3cea0e4 = storage.FileType; storage.fileWebp#1081464c = storage.FileType; userEmpty#d3bc4b7a id:long = User; -user#8f97c628 flags:# self:flags.10?true contact:flags.11?true mutual_contact:flags.12?true deleted:flags.13?true bot:flags.14?true bot_chat_history:flags.15?true bot_nochats:flags.16?true verified:flags.17?true restricted:flags.18?true min:flags.20?true bot_inline_geo:flags.21?true support:flags.23?true scam:flags.24?true apply_min_photo:flags.25?true fake:flags.26?true bot_attach_menu:flags.27?true premium:flags.28?true attach_menu_enabled:flags.29?true flags2:# id:long access_hash:flags.0?long first_name:flags.1?string last_name:flags.2?string username:flags.3?string phone:flags.4?string photo:flags.5?UserProfilePhoto status:flags.6?UserStatus bot_info_version:flags.14?int restriction_reason:flags.18?Vector bot_inline_placeholder:flags.19?string lang_code:flags.22?string emoji_status:flags.30?EmojiStatus usernames:flags2.0?Vector = User; +user#8f97c628 flags:# self:flags.10?true contact:flags.11?true mutual_contact:flags.12?true deleted:flags.13?true bot:flags.14?true bot_chat_history:flags.15?true bot_nochats:flags.16?true verified:flags.17?true restricted:flags.18?true min:flags.20?true bot_inline_geo:flags.21?true support:flags.23?true scam:flags.24?true apply_min_photo:flags.25?true fake:flags.26?true bot_attach_menu:flags.27?true premium:flags.28?true attach_menu_enabled:flags.29?true flags2:# bot_can_edit:flags2.1?true id:long access_hash:flags.0?long first_name:flags.1?string last_name:flags.2?string username:flags.3?string phone:flags.4?string photo:flags.5?UserProfilePhoto status:flags.6?UserStatus bot_info_version:flags.14?int restriction_reason:flags.18?Vector bot_inline_placeholder:flags.19?string lang_code:flags.22?string emoji_status:flags.30?EmojiStatus usernames:flags2.0?Vector = User; userProfilePhotoEmpty#4f11bae1 = UserProfilePhoto; userProfilePhoto#82d1f706 flags:# has_video:flags.0?true personal:flags.2?true photo_id:long stripped_thumb:flags.1?bytes dc_id:int = UserProfilePhoto; @@ -191,7 +191,7 @@ messageActionSetChatTheme#aa786345 emoticon:string = MessageAction; messageActionChatJoinedByRequest#ebbca3cb = MessageAction; messageActionWebViewDataSentMe#47dd8079 text:string data:string = MessageAction; messageActionWebViewDataSent#b4c38cb5 text:string = MessageAction; -messageActionGiftPremium#aba0f5c6 currency:string amount:long months:int = MessageAction; +messageActionGiftPremium#c83d6aec flags:# currency:string amount:long months:int crypto_currency:flags.0?string crypto_amount:flags.0?long = MessageAction; messageActionTopicCreate#d999256 flags:# title:string icon_color:int icon_emoji_id:flags.0?long = MessageAction; messageActionTopicEdit#c0944820 flags:# title:flags.0?string icon_emoji_id:flags.1?long closed:flags.2?Bool hidden:flags.3?Bool = MessageAction; messageActionSuggestProfilePhoto#57de635e photo:Photo = MessageAction; @@ -737,7 +737,7 @@ auth.sentCodeTypeSms#c000bba2 length:int = auth.SentCodeType; auth.sentCodeTypeCall#5353e5a7 length:int = auth.SentCodeType; auth.sentCodeTypeFlashCall#ab03c6d9 pattern:string = auth.SentCodeType; auth.sentCodeTypeMissedCall#82006484 prefix:string length:int = auth.SentCodeType; -auth.sentCodeTypeEmailCode#5a159841 flags:# apple_signin_allowed:flags.0?true google_signin_allowed:flags.1?true email_pattern:string length:int next_phone_login_date:flags.2?int = auth.SentCodeType; +auth.sentCodeTypeEmailCode#f450f59b flags:# apple_signin_allowed:flags.0?true google_signin_allowed:flags.1?true email_pattern:string length:int reset_available_period:flags.3?int reset_pending_date:flags.4?int = auth.SentCodeType; auth.sentCodeTypeSetUpEmailRequired#a5491dea flags:# apple_signin_allowed:flags.0?true google_signin_allowed:flags.1?true = auth.SentCodeType; auth.sentCodeTypeFragmentSms#d9565c39 url:string length:int = auth.SentCodeType; auth.sentCodeTypeFirebaseSms#e57b1432 flags:# nonce:flags.0?bytes receipt:flags.1?string push_timeout:flags.1?int length:int = auth.SentCodeType; @@ -1227,6 +1227,7 @@ payments.bankCardData#3e24e573 title:string open_urls:Vector = dialogFilter#7438f7e8 flags:# contacts:flags.0?true non_contacts:flags.1?true groups:flags.2?true broadcasts:flags.3?true bots:flags.4?true exclude_muted:flags.11?true exclude_read:flags.12?true exclude_archived:flags.13?true id:int title:string emoticon:flags.25?string pinned_peers:Vector include_peers:Vector exclude_peers:Vector = DialogFilter; dialogFilterDefault#363293ae = DialogFilter; +dialogFilterCommunity#d8565037 flags:# id:int title:string emoticon:flags.25?string pinned_peers:Vector include_peers:Vector = DialogFilter; dialogFilterSuggested#77744d4a filter:DialogFilter description:string = DialogFilterSuggested; @@ -1524,6 +1525,21 @@ inlineBotWebView#b57295d5 text:string url:string = InlineBotWebView; readParticipantDate#4a4ff172 user_id:long date:int = ReadParticipantDate; +inputCommunityDialogFilter#1ae107a1 filter_id:int = InputCommunity; + +exportedCommunityInvite#ebddc1aa flags:# title:string url:string peers:Vector = ExportedCommunityInvite; + +communities.exportedCommunityInvite#6b97a8ea filter:DialogFilter invite:ExportedCommunityInvite = communities.ExportedCommunityInvite; + +communities.exportedInvites#ffd75fa7 invites:Vector chats:Vector users:Vector = communities.ExportedInvites; + +communities.communityInviteAlready#c745ee07 filter_id:int missing_peers:Vector already_peers:Vector chats:Vector users:Vector = communities.CommunityInvite; +communities.communityInvite#3857da1 flags:# title:string emoticon:flags.0?string peers:Vector chats:Vector users:Vector = communities.CommunityInvite; + +communities.communityUpdates#e74660b3 missing_peers:Vector chats:Vector users:Vector = communities.CommunityUpdates; + +bots.botInfo#e8a775b0 name:string about:string description:string = bots.BotInfo; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -1555,6 +1571,7 @@ auth.acceptLoginToken#e894ad4d token:bytes = Authorization; auth.checkRecoveryPassword#d36bf79 code:string = Bool; auth.importWebTokenAuthorization#2db873a9 api_id:int api_hash:string web_auth_token:string = auth.Authorization; auth.requestFirebaseSms#89464b50 flags:# phone_number:string phone_code_hash:string safety_net_token:flags.0?string ios_push_secret:flags.1?string = Bool; +auth.resetLoginEmail#7e960193 phone_number:string phone_code_hash:string = auth.SentCode; account.registerDevice#ec86017a flags:# no_muted:flags.0?true token_type:int token:string app_sandbox:Bool secret:bytes other_uids:Vector = Bool; account.unregisterDevice#6a0d3206 token_type:int token:string other_uids:Vector = Bool; @@ -1864,8 +1881,8 @@ updates.getState#edd4882a = updates.State; updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference; updates.getChannelDifference#3173d78 flags:# force:flags.0?true channel:InputChannel filter:ChannelMessagesFilter pts:int limit:int = updates.ChannelDifference; -photos.updateProfilePhoto#1c3d5956 flags:# fallback:flags.0?true id:InputPhoto = photos.Photo; -photos.uploadProfilePhoto#93c9a51 flags:# fallback:flags.3?true file:flags.0?InputFile video:flags.1?InputFile video_start_ts:flags.2?double video_emoji_markup:flags.4?VideoSize = photos.Photo; +photos.updateProfilePhoto#9e82039 flags:# fallback:flags.0?true bot:flags.1?InputUser id:InputPhoto = photos.Photo; +photos.uploadProfilePhoto#388a3b5 flags:# fallback:flags.3?true bot:flags.5?InputUser file:flags.0?InputFile video:flags.1?InputFile video_start_ts:flags.2?double video_emoji_markup:flags.4?VideoSize = photos.Photo; photos.deletePhotos#87cf7f2f id:Vector = Vector; photos.getUserPhotos#91cd32a8 user_id:InputUser offset:int max_id:long limit:int = photos.Photos; photos.uploadContactProfilePhoto#e14c4a71 flags:# suggest:flags.3?true save:flags.4?true user_id:InputUser file:flags.0?InputFile video:flags.1?InputFile video_start_ts:flags.2?double video_emoji_markup:flags.5?VideoSize = photos.Photo; @@ -1968,8 +1985,10 @@ bots.setBotMenuButton#4504d54f user_id:InputUser button:BotMenuButton = Bool; bots.getBotMenuButton#9c60eb28 user_id:InputUser = BotMenuButton; bots.setBotBroadcastDefaultAdminRights#788464e1 admin_rights:ChatAdminRights = Bool; bots.setBotGroupDefaultAdminRights#925ec9ea admin_rights:ChatAdminRights = Bool; -bots.setBotInfo#a365df7a flags:# lang_code:string about:flags.0?string description:flags.1?string = Bool; -bots.getBotInfo#75ec12e6 lang_code:string = Vector; +bots.setBotInfo#10cf3123 flags:# bot:flags.2?InputUser lang_code:string name:flags.3?string about:flags.0?string description:flags.1?string = Bool; +bots.getBotInfo#dcd914fd flags:# bot:flags.0?InputUser lang_code:string = bots.BotInfo; +bots.reorderUsernames#9709b1c2 bot:InputUser order:Vector = Bool; +bots.toggleUsername#53ca973 bot:InputUser username:string active:Bool = Bool; payments.getPaymentForm#37148dbb flags:# invoice:InputInvoice theme_params:flags.0?DataJSON = payments.PaymentForm; payments.getPaymentReceipt#2478d1cc peer:InputPeer msg_id:int = payments.PaymentReceipt; @@ -2033,7 +2052,6 @@ langpack.getLanguages#42c6978f lang_pack:string = Vector; langpack.getLanguage#6a596502 lang_pack:string lang_code:string = LangPackLanguage; folders.editPeerFolders#6847d0ab folder_peers:Vector = Updates; -folders.deleteFolder#1c295881 folder_id:int = Updates; stats.getBroadcastStats#ab42441a flags:# dark:flags.0?true channel:InputChannel = stats.BroadcastStats; stats.loadAsyncGraph#621d5fa0 flags:# token:string x:flags.0?long = StatsGraph; @@ -2041,4 +2059,15 @@ stats.getMegagroupStats#dcdf8607 flags:# dark:flags.0?true channel:InputChannel stats.getMessagePublicForwards#5630281b channel:InputChannel msg_id:int offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages; stats.getMessageStats#b6e0a3f5 flags:# dark:flags.0?true channel:InputChannel msg_id:int = stats.MessageStats; -// LAYER 155 +communities.exportCommunityInvite#41fe69d9 community:InputCommunity title:string peers:Vector = communities.ExportedCommunityInvite; +communities.deleteExportedInvite#f96e4616 community:InputCommunity slug:string = Bool; +communities.editExportedInvite#27140512 flags:# community:InputCommunity slug:string title:flags.1?string peers:flags.2?Vector = ExportedCommunityInvite; +communities.getExportedInvites#4688a39d community:InputCommunity = communities.ExportedInvites; +communities.checkCommunityInvite#99ba9e5 slug:string = communities.CommunityInvite; +communities.joinCommunityInvite#4eff927 slug:string peers:Vector = Updates; +communities.getCommunityUpdates#2956d635 community:InputCommunity = communities.CommunityUpdates; +communities.joinCommunityUpdates#51d42216 community:InputCommunity peers:Vector = Updates; +communities.hideCommunityUpdates#d678baf community:InputCommunity = Bool; +communities.leaveCommunity#35d9755f community:InputCommunity peers:Vector = Updates; + +// LAYER 158 diff --git a/Telegram/SourceFiles/api/api_peer_photo.cpp b/Telegram/SourceFiles/api/api_peer_photo.cpp index 892ca1090..5056473d6 100644 --- a/Telegram/SourceFiles/api/api_peer_photo.cpp +++ b/Telegram/SourceFiles/api/api_peer_photo.cpp @@ -186,6 +186,7 @@ void PeerPhoto::updateSelf( const auto usedFileReference = photo->fileReference(); _api.request(MTPphotos_UpdateProfilePhoto( MTP_flags(0), + MTPInputUser(), // bot photo->mtpInput() )).done([=](const MTPphotos_Photo &result) { result.match([&](const MTPDphotos_photo &data) { @@ -252,6 +253,7 @@ void PeerPhoto::clear(not_null photo) { if (self->userpicPhotoId() == photo->id) { _api.request(MTPphotos_UpdateProfilePhoto( MTP_flags(0), + MTPInputUser(), // bot MTP_inputPhotoEmpty() )).done([=](const MTPphotos_Photo &result) { self->setPhoto(MTP_userProfilePhotoEmpty()); @@ -276,6 +278,7 @@ void PeerPhoto::clear(not_null photo) { if (fallbackPhotoId && (*fallbackPhotoId) == photo->id) { _api.request(MTPphotos_UpdateProfilePhoto( MTP_flags(MTPphotos_UpdateProfilePhoto::Flag::f_fallback), + MTPInputUser(), // bot MTP_inputPhotoEmpty() )).send(); _session->storage().add(Storage::UserPhotosSetBack( @@ -321,6 +324,7 @@ void PeerPhoto::set(not_null peer, not_null photo) { if (peer == _session->user()) { _api.request(MTPphotos_UpdateProfilePhoto( MTP_flags(0), + MTPInputUser(), // bot photo->mtpInput() )).done([=](const MTPphotos_Photo &result) { result.match([&](const MTPDphotos_photo &data) { @@ -370,6 +374,7 @@ void PeerPhoto::ready( MTP_flags((file ? Flag::f_file : none) | (videoSize ? Flag::f_video_emoji_markup : none) | ((type == UploadType::Fallback) ? Flag::f_fallback : none)), + MTPInputUser(), // bot file ? (*file) : MTPInputFile(), MTPInputFile(), // video MTPdouble(), // video_start_ts diff --git a/Telegram/SourceFiles/data/data_chat_filters.cpp b/Telegram/SourceFiles/data/data_chat_filters.cpp index 02c308b38..fb0531c14 100644 --- a/Telegram/SourceFiles/data/data_chat_filters.cpp +++ b/Telegram/SourceFiles/data/data_chat_filters.cpp @@ -109,22 +109,54 @@ ChatFilter ChatFilter::FromTL( { never.begin(), never.end() }); }, [](const MTPDdialogFilterDefault &d) { return ChatFilter(); + }, [&](const MTPDdialogFilterCommunity &data) { + auto &&to_histories = ranges::views::transform([&]( + const MTPInputPeer &data) { + const auto peer = data.match([&](const MTPDinputPeerUser &data) { + const auto user = owner->user(data.vuser_id().v); + user->setAccessHash(data.vaccess_hash().v); + return (PeerData*)user; + }, [&](const MTPDinputPeerChat &data) { + return (PeerData*)owner->chat(data.vchat_id().v); + }, [&](const MTPDinputPeerChannel &data) { + const auto channel = owner->channel(data.vchannel_id().v); + channel->setAccessHash(data.vaccess_hash().v); + return (PeerData*)channel; + }, [&](const MTPDinputPeerSelf &data) { + return (PeerData*)owner->session().user(); + }, [&](const auto &data) { + return (PeerData*)nullptr; + }); + return peer ? owner->history(peer).get() : nullptr; + }) | ranges::views::filter([](History *history) { + return history != nullptr; + }) | ranges::views::transform([](History *history) { + return not_null(history); + }); + auto &&always = ranges::views::concat( + data.vinclude_peers().v + ) | to_histories; + auto pinned = ranges::views::all( + data.vpinned_peers().v + ) | to_histories | ranges::to_vector; + auto &&all = ranges::views::concat(always, pinned); + auto list = base::flat_set>{ + all.begin(), + all.end() + }; + return ChatFilter( + data.vid().v, + qs(data.vtitle()), + qs(data.vemoticon().value_or_empty()), + (Flag::Community + | (data.is_community_can_admin() ? Flag::Admin : Flag())), + std::move(list), + std::move(pinned), + {}); }); } MTPDialogFilter ChatFilter::tl(FilterId replaceId) const { - using TLFlag = MTPDdialogFilter::Flag; - const auto flags = TLFlag(0) - | ((_flags & Flag::Contacts) ? TLFlag::f_contacts : TLFlag(0)) - | ((_flags & Flag::NonContacts) ? TLFlag::f_non_contacts : TLFlag(0)) - | ((_flags & Flag::Groups) ? TLFlag::f_groups : TLFlag(0)) - | ((_flags & Flag::Channels) ? TLFlag::f_broadcasts : TLFlag(0)) - | ((_flags & Flag::Bots) ? TLFlag::f_bots : TLFlag(0)) - | ((_flags & Flag::NoMuted) ? TLFlag::f_exclude_muted : TLFlag(0)) - | ((_flags & Flag::NoRead) ? TLFlag::f_exclude_read : TLFlag(0)) - | ((_flags & Flag::NoArchived) - ? TLFlag::f_exclude_archived - : TLFlag(0)); auto always = _always; auto pinned = QVector(); pinned.reserve(_pinned.size()); @@ -137,13 +169,39 @@ MTPDialogFilter ChatFilter::tl(FilterId replaceId) const { for (const auto &history : always) { include.push_back(history->peer->input); } + if (_flags & Flag::Community) { + using TLFlag = MTPDdialogFilterCommunity::Flag; + const auto flags = TLFlag::f_emoticon + | ((_flags & Flag::Admin) + ? TLFlag::f_community_can_admin + : TLFlag(0)); + return MTP_dialogFilterCommunity( + MTP_flags(flags), + MTP_int(replaceId ? replaceId : _id), + MTP_string(_title), + MTP_string(_iconEmoji), + MTP_vector(pinned), + MTP_vector(include)); + } + using TLFlag = MTPDdialogFilter::Flag; + const auto flags = TLFlag::f_emoticon + | ((_flags & Flag::Contacts) ? TLFlag::f_contacts : TLFlag(0)) + | ((_flags & Flag::NonContacts) ? TLFlag::f_non_contacts : TLFlag(0)) + | ((_flags & Flag::Groups) ? TLFlag::f_groups : TLFlag(0)) + | ((_flags & Flag::Channels) ? TLFlag::f_broadcasts : TLFlag(0)) + | ((_flags & Flag::Bots) ? TLFlag::f_bots : TLFlag(0)) + | ((_flags & Flag::NoMuted) ? TLFlag::f_exclude_muted : TLFlag(0)) + | ((_flags & Flag::NoRead) ? TLFlag::f_exclude_read : TLFlag(0)) + | ((_flags & Flag::NoArchived) + ? TLFlag::f_exclude_archived + : TLFlag(0)); auto never = QVector(); never.reserve(_never.size()); for (const auto &history : _never) { never.push_back(history->peer->input); } return MTP_dialogFilter( - MTP_flags(flags | TLFlag::f_emoticon), + MTP_flags(flags), MTP_int(replaceId ? replaceId : _id), MTP_string(_title), MTP_string(_iconEmoji), diff --git a/Telegram/SourceFiles/data/data_chat_filters.h b/Telegram/SourceFiles/data/data_chat_filters.h index 4e2cf9cc0..96214f96a 100644 --- a/Telegram/SourceFiles/data/data_chat_filters.h +++ b/Telegram/SourceFiles/data/data_chat_filters.h @@ -22,15 +22,18 @@ class Session; class ChatFilter final { public: - enum class Flag : uchar { - Contacts = 0x01, - NonContacts = 0x02, - Groups = 0x04, - Channels = 0x08, - Bots = 0x10, - NoMuted = 0x20, - NoRead = 0x40, - NoArchived = 0x80, + enum class Flag : ushort { + Contacts = (1 << 0), + NonContacts = (1 << 1), + Groups = (1 << 2), + Channels = (1 << 3), + Bots = (1 << 4), + NoMuted = (1 << 5), + NoRead = (1 << 6), + NoArchived = (1 << 7), + + Community = (1 << 8), + Admin = (1 << 9), }; friend constexpr inline bool is_flag_type(Flag) { return true; }; using Flags = base::flags; @@ -54,6 +57,7 @@ public: [[nodiscard]] QString title() const; [[nodiscard]] QString iconEmoji() const; [[nodiscard]] Flags flags() const; + [[nodiscard]] bool admin() const; [[nodiscard]] const base::flat_set> &always() const; [[nodiscard]] const std::vector> &pinned() const; [[nodiscard]] const base::flat_set> &never() const; From b7d9d549ff846382ccec6aadba2ec3c775d1a8ef Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 21 Mar 2023 20:27:16 +0400 Subject: [PATCH 002/676] 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( From 8a9d13c6e417da53d7f416e3aa9ebc146730a587 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 28 Mar 2023 18:14:53 +0400 Subject: [PATCH 003/676] Initial add filter / chats / edit filter link. --- Telegram/SourceFiles/api/api_chat_filters.cpp | 388 +++++++++++++++++- Telegram/SourceFiles/api/api_chat_filters.h | 8 + Telegram/SourceFiles/apiwrap.cpp | 10 + Telegram/SourceFiles/apiwrap.h | 5 + .../boxes/filters/edit_filter_links.cpp | 80 +++- .../SourceFiles/core/local_url_handlers.cpp | 19 + .../SourceFiles/data/data_chat_filters.cpp | 12 +- Telegram/SourceFiles/data/data_chat_filters.h | 2 + Telegram/SourceFiles/settings/settings.style | 10 + 9 files changed, 520 insertions(+), 14 deletions(-) diff --git a/Telegram/SourceFiles/api/api_chat_filters.cpp b/Telegram/SourceFiles/api/api_chat_filters.cpp index 7efcaf01a..837af84d9 100644 --- a/Telegram/SourceFiles/api/api_chat_filters.cpp +++ b/Telegram/SourceFiles/api/api_chat_filters.cpp @@ -7,12 +7,331 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "api/api_chat_filters.h" -#include "data/data_session.h" -#include "data/data_chat_filters.h" -#include "main/main_session.h" #include "apiwrap.h" +#include "boxes/peer_list_box.h" +#include "core/application.h" +#include "data/data_chat_filters.h" +#include "data/data_peer.h" +#include "data/data_session.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "ui/boxes/confirm_box.h" +#include "ui/toasts/common_toasts.h" +#include "ui/widgets/buttons.h" +#include "window/window_session_controller.h" +#include "styles/style_layers.h" +#include "styles/style_settings.h" namespace Api { +namespace { + +enum class ToggleAction { + Adding, + Removing, +}; + +enum class HeaderType { + AddingFilter, + AddingChats, + AllAdded, + Removing, +}; + +struct HeaderDescriptor { + base::required type; + base::required title; + int badge = 0; +}; + +class ToggleChatsController final + : public PeerListController + , public base::has_weak_ptr { +public: + ToggleChatsController( + not_null window, + ToggleAction action, + const QString &slug, + FilterId filterId, + const QString &title, + std::vector> chats); + + void prepare() override; + void rowClicked(not_null row) override; + Main::Session &session() const override; + + [[nodiscard]] auto selectedValue() const + -> rpl::producer>>; + +private: + void setupAboveWidget(); + + const not_null _window; + + ToggleAction _action = ToggleAction::Adding; + QString _slug; + FilterId _filterId = 0; + QString _filterTitle; + std::vector> _chats; + rpl::variable>> _selected; + + base::unique_qptr _menu; + + rpl::lifetime _lifetime; + +}; + +[[nodiscard]] rpl::producer TitleText(HeaderType type) { + // langs + switch (type) { + case HeaderType::AddingFilter: + return rpl::single(u"Add Folder"_q); + case HeaderType::AddingChats: + return rpl::single(u"Add Chats to Folder"_q); + case HeaderType::AllAdded: + return rpl::single(u"Folder Already Added"_q); + case HeaderType::Removing: + return rpl::single(u"Remove Folder"_q); + } + Unexpected("HeaderType in TitleText."); +} + +void FillHeader( + not_null container, + HeaderDescriptor descriptor) { + // langs + const auto description = (descriptor.type == HeaderType::AddingFilter) + ? (u"Do you want to add a new chat folder "_q + + descriptor.title + + u" and join its groups and channels?"_q) + : (descriptor.type == HeaderType::AddingChats) + ? (u"Do you want to join "_q + + QString::number(descriptor.badge) + + u" chats and add them to your folder "_q + + descriptor.title + '?') + : (descriptor.type == HeaderType::AllAdded) + ? (u"You have already added the folder "_q + + descriptor.title + + u" and all its chats."_q) + : (u"Do you want to quit the chats you joined " + "when adding the folder "_q + + descriptor.title + '?'); + container->add( + object_ptr( + container, + description, + st::boxDividerLabel), + st::boxRowPadding); +} + +void ImportInvite( + base::weak_ptr weak, + const QString &slug, + const base::flat_set> &peers) { + Expects(!peers.empty()); + + const auto peer = peers.front(); + const auto api = &peer->session().api(); + const auto callback = [=](const MTPUpdates &result) { + api->applyUpdates(result); + }; + const auto error = [=](const MTP::Error &error) { + if (const auto strong = weak.get()) { + Ui::ShowMultilineToast({ + .parentOverride = Window::Show(strong).toastParent(), + .text = { error.description() }, + }); + } + }; + auto inputs = peers | ranges::views::transform([](auto peer) { + return MTPInputPeer(peer->input); + }) | ranges::to(); + api->request(MTPcommunities_JoinCommunityInvite( + MTP_string(slug), + MTP_vector(std::move(inputs)) + )).done(callback).fail(error).send(); +} + +ToggleChatsController::ToggleChatsController( + not_null window, + ToggleAction action, + const QString &slug, + FilterId filterId, + const QString &title, + std::vector> chats) +: _window(window) +, _action(action) +, _slug(slug) +, _filterId(filterId) +, _filterTitle(title) +, _chats(std::move(chats)) { +} + +void ToggleChatsController::prepare() { + setupAboveWidget(); + auto selected = base::flat_set>(); + for (const auto &peer : _chats) { + auto row = std::make_unique(peer); + const auto raw = row.get(); + delegate()->peerListAppendRow(std::move(row)); + delegate()->peerListSetRowChecked(raw, true); + selected.emplace(peer); + } + delegate()->peerListRefreshRows(); + _selected = std::move(selected); +} + +void ToggleChatsController::rowClicked(not_null row) { + const auto peer = row->peer(); + const auto checked = row->checked(); + auto selected = _selected.current(); + delegate()->peerListSetRowChecked(row, !checked); + if (checked) { + selected.remove(peer); + } else { + selected.emplace(peer); + } + _selected = std::move(selected); +} + +void ToggleChatsController::setupAboveWidget() { + using namespace Settings; + + auto wrap = object_ptr((QWidget*)nullptr); + const auto container = wrap.data(); + + const auto type = !_filterId + ? HeaderType::AddingFilter + : (_action == ToggleAction::Adding) + ? HeaderType::AddingChats + : HeaderType::Removing; + delegate()->peerListSetTitle(TitleText(type)); + FillHeader(container, { + .type = type, + .title = _filterTitle, + .badge = (type == HeaderType::AddingChats) ? int(_chats.size()) : 0, + }); + + delegate()->peerListSetAboveWidget(std::move(wrap)); +} + +Main::Session &ToggleChatsController::session() const { + return _window->session(); +} + +auto ToggleChatsController::selectedValue() const +-> rpl::producer>> { + return _selected.value(); +} + +[[nodiscard]] void AlreadyFilterBox( + not_null box, + const QString &title) { + // langs + box->setTitle(TitleText(HeaderType::AllAdded)); + + FillHeader(box->verticalLayout(), { + .type = HeaderType::AllAdded, + .title = title, + }); + + box->addButton(tr::lng_box_ok(), [=] { box->closeBox(); }); +} + +void ProcessFilterInvite( + base::weak_ptr weak, + const QString &slug, + FilterId filterId, + const QString &title, + std::vector> peers) { + const auto strong = weak.get(); + if (!strong) { + return; + } + Core::App().hideMediaView(); + if (peers.empty()) { + if (filterId) { + strong->show(Box(AlreadyFilterBox, title)); + } else { + Ui::ShowMultilineToast({ + .parentOverride = Window::Show(strong).toastParent(), + .text = { tr::lng_group_invite_bad_link(tr::now) }, + }); + } + return; + } + auto controller = std::make_unique( + strong, + ToggleAction::Adding, + slug, + filterId, + title, + std::move(peers)); + const auto raw = controller.get(); + auto initBox = [=](not_null box) { + box->setStyle(st::filterInviteBox); + raw->selectedValue( + ) | rpl::start_with_next([=]( + base::flat_set> &&peers) { + const auto count = int(peers.size()); + + box->clearButtons(); + auto button = object_ptr( + box, + rpl::single(count + ? u"Add %1 Chats"_q.arg(count) + : u"Don't add chats"_q), + st::defaultActiveButton); + const auto raw = button.data(); + + box->widthValue( + ) | rpl::start_with_next([=](int width) { + const auto &padding = st::filterInviteBox.buttonPadding; + raw->resizeToWidth(width + - padding.left() + - padding.right()); + raw->moveToLeft(padding.left(), padding.top()); + }, raw->lifetime()); + + raw->setClickedCallback([=] { + if (!count) { + box->closeBox(); + //} else if (count + alreadyInFilter() >= ...) { + // #TODO filters + } else { + ImportInvite(weak, slug, peers); + } + }); + + box->addButton(std::move(button)); + }, box->lifetime()); + }; + strong->show( + Box(std::move(controller), std::move(initBox))); +} + +void ProcessFilterInvite( + base::weak_ptr weak, + const QString &slug, + FilterId filterId, + std::vector> peers) { + const auto strong = weak.get(); + if (!strong) { + return; + } + Core::App().hideMediaView(); + const auto &list = strong->session().data().chatsFilters().list(); + const auto it = ranges::find(list, filterId, &Data::ChatFilter::id); + if (it == end(list)) { + Ui::ShowMultilineToast({ + .parentOverride = Window::Show(strong).toastParent(), + .text = { u"Filter not found :shrug:"_q }, + }); + return; + } + ProcessFilterInvite(weak, slug, filterId, it->title(), std::move(peers)); +} + +} // namespace void SaveNewFilterPinned( not_null session, @@ -25,7 +344,70 @@ void SaveNewFilterPinned( MTP_int(filterId), filter.tl() )).send(); +} +void CheckFilterInvite( + not_null controller, + const QString &slug) { + const auto session = &controller->session(); + const auto weak = base::make_weak(controller); + session->api().checkFilterInvite(slug, [=]( + const MTPcommunities_CommunityInvite &result) { + const auto strong = weak.get(); + if (!strong) { + return; + } + auto title = QString(); + auto filterId = FilterId(); + auto peers = std::vector>(); + auto already = std::vector>(); + auto &owner = strong->session().data(); + result.match([&](const auto &data) { + owner.processUsers(data.vusers()); + owner.processChats(data.vchats()); + }); + const auto parseList = [&](const MTPVector &list) { + auto result = std::vector>(); + result.reserve(list.v.size()); + for (const auto &peer : list.v) { + result.push_back(owner.peer(peerFromMTP(peer))); + } + return result; + }; + result.match([&](const MTPDcommunities_communityInvite &data) { + title = qs(data.vtitle()); + peers = parseList(data.vpeers()); + }, [&](const MTPDcommunities_communityInviteAlready &data) { + filterId = data.vfilter_id().v; + peers = parseList(data.vmissing_peers()); + already = parseList(data.valready_peers()); + }); + + const auto &filters = owner.chatsFilters(); + const auto notLoaded = filterId + && !ranges::contains( + owner.chatsFilters().list(), + filterId, + &Data::ChatFilter::id); + if (notLoaded) { + const auto lifetime = std::make_shared(); + owner.chatsFilters().changed( + ) | rpl::start_with_next([=] { + lifetime->destroy(); + ProcessFilterInvite(weak, slug, filterId, std::move(peers)); + }, *lifetime); + owner.chatsFilters().reload(); + } else if (filterId) { + ProcessFilterInvite(weak, slug, filterId, std::move(peers)); + } else { + ProcessFilterInvite(weak, slug, filterId, title, std::move(peers)); + } + }, [=](const MTP::Error &error) { + if (error.code() != 400) { + return; + } + ProcessFilterInvite(weak, slug, FilterId(), QString(), {}); + }); } } // namespace Api diff --git a/Telegram/SourceFiles/api/api_chat_filters.h b/Telegram/SourceFiles/api/api_chat_filters.h index 59c29f72b..99fce06a2 100644 --- a/Telegram/SourceFiles/api/api_chat_filters.h +++ b/Telegram/SourceFiles/api/api_chat_filters.h @@ -11,10 +11,18 @@ namespace Main { class Session; } // namespace Main +namespace Window { +class SessionController; +} // namespace Window + namespace Api { void SaveNewFilterPinned( not_null session, FilterId filterId); +void CheckFilterInvite( + not_null controller, + const QString &slug); + } // namespace Api diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 282683741..f7a097d26 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -380,6 +380,16 @@ void ApiWrap::checkChatInvite( )).done(std::move(done)).fail(std::move(fail)).send(); } +void ApiWrap::checkFilterInvite( + const QString &slug, + FnMut done, + Fn fail) { + request(base::take(_checkFilterInviteRequestId)).cancel(); + _checkFilterInviteRequestId = request( + MTPcommunities_CheckCommunityInvite(MTP_string(slug)) + ).done(std::move(done)).fail(std::move(fail)).send(); +} + void ApiWrap::savePinnedOrder(Data::Folder *folder) { const auto &order = _session->data().pinnedChatsOrder(folder); const auto input = [](Dialogs::Key key) { diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h index ec71c781b..a142e09a9 100644 --- a/Telegram/SourceFiles/apiwrap.h +++ b/Telegram/SourceFiles/apiwrap.h @@ -202,6 +202,10 @@ public: const QString &hash, FnMut done, Fn fail); + void checkFilterInvite( + const QString &slug, + FnMut done, + Fn fail); void processFullPeer( not_null peer, @@ -653,6 +657,7 @@ private: mtpRequestId _termsUpdateRequestId = 0; mtpRequestId _checkInviteRequestId = 0; + mtpRequestId _checkFilterInviteRequestId = 0; struct MigrateCallbacks { FnMut)> done; diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp index 5a6a6fa72..3fa2aeeee 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp @@ -77,6 +77,14 @@ struct InviteLinkAction { } } +void ShowEmptyLinkError(not_null window) { + // langs + Ui::ShowMultilineToast({ + .parentOverride = Window::Show(window).toastParent(), + .text = { u"Link should have at least one chat shared."_q }, + }); +} + void ChatFilterLinkBox( not_null box, Data::ChatFilterLink data) { @@ -336,6 +344,7 @@ public: void showFinished() override; [[nodiscard]] rpl::producer hasChangesValue() const; + [[nodiscard]] base::flat_set> selected() const; private: void setupAboveWidget(); @@ -347,14 +356,13 @@ private: base::flat_set> _filterChats; base::flat_set> _allowed; - rpl::variable _selected = 0; + rpl::variable>> _selected; + base::flat_set> _initial; base::unique_qptr _menu; QString _link; - Ui::RpWidget *_headerWidget = nullptr; - rpl::variable _addedHeight; rpl::variable _hasChanges = false; rpl::event_stream<> _showFinished; @@ -495,7 +503,6 @@ void LinkController::addLinkBlock(not_null container) { void LinkController::prepare() { setupAboveWidget(); - auto selected = 0; for (const auto &history : _data.chats) { const auto peer = history->peer; _allowed.emplace(peer); @@ -503,7 +510,7 @@ void LinkController::prepare() { const auto raw = row.get(); delegate()->peerListAppendRow(std::move(row)); delegate()->peerListSetRowChecked(raw, true); - ++selected; + _initial.emplace(peer); } for (const auto &history : _filterChats) { if (delegate()->peerListFindRow(history->peer->id.value)) { @@ -520,14 +527,23 @@ void LinkController::prepare() { } } delegate()->peerListRefreshRows(); - _selected = selected; + _selected = _initial; } void LinkController::rowClicked(not_null row) { if (_allowed.contains(row->peer())) { + const auto peer = row->peer(); const auto checked = row->checked(); + auto selected = _selected.current(); delegate()->peerListSetRowChecked(row, !checked); - _selected = _selected.current() + (checked ? -1 : 1); + if (checked) { + selected.remove(peer); + } else { + selected.emplace(peer); + } + const auto has = (_initial != selected); + _selected = std::move(selected); + _hasChanges = has; } } @@ -546,9 +562,16 @@ void LinkController::setupAboveWidget() { addLinkBlock(container); } + // langs + auto subtitle = _selected.value( + ) | rpl::map([](const base::flat_set> &selected) { + return selected.empty() + ? u"No chats selected"_q + : (QString::number(selected.size()) + u" chats selected"_q); + }); Settings::AddSubsectionTitle( container, - rpl::single(u"3 chats selected"_q)); + std::move(subtitle)); delegate()->peerListSetAboveWidget(std::move(wrap)); } @@ -561,6 +584,10 @@ rpl::producer LinkController::hasChangesValue() const { return _hasChanges.value(); } +base::flat_set> LinkController::selected() const { + return _selected.current(); +} + LinksController::LinksController( not_null window, rpl::producer> content, @@ -804,7 +831,7 @@ void ExportFilterLink( ) | ranges::to(); session->api().request(MTPcommunities_ExportCommunityInvite( MTP_inputCommunityDialogFilter(MTP_int(id)), - MTP_string(), + MTP_string(), // title MTP_vector(std::move(mtpPeers)) )).done([=](const MTPcommunities_ExportedCommunityInvite &result) { const auto &data = result.data(); @@ -821,6 +848,34 @@ void ExportFilterLink( }).send(); } +void EditLinkChats( + const Data::ChatFilterLink &link, + base::flat_set> peers) { + Expects(!peers.empty()); + Expects(link.id != 0); + Expects(!link.url.isEmpty()); + + const auto id = link.id; + 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_EditExportedInvite( + MTP_flags(MTPcommunities_EditExportedInvite::Flag::f_peers), + MTP_inputCommunityDialogFilter(MTP_int(link.id)), + MTP_string(link.url), + MTPstring(), // title + MTP_vector(std::move(mtpPeers)) + )).done([=](const MTPExportedCommunityInvite &result) { + const auto &data = result.data(); + const auto link = session->data().chatsFilters().add(id, result); + //done(link); + }).fail([=](const MTP::Error &error) { + //done({ .id = id }); + }).send(); +} + object_ptr ShowLinkBox( not_null window, const Data::ChatFilter &filter, @@ -837,7 +892,12 @@ object_ptr ShowLinkBox( box->clearButtons(); if (has) { box->addButton(tr::lng_settings_save(), [=] { - + const auto chosen = raw->selected(); + if (chosen.empty()) { + ShowEmptyLinkError(window); + } else { + EditLinkChats(link, chosen); + } }); box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); } else { diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp index 19fce2ad3..cf6673c44 100644 --- a/Telegram/SourceFiles/core/local_url_handlers.cpp +++ b/Telegram/SourceFiles/core/local_url_handlers.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_authorizations.h" #include "api/api_confirm_phone.h" #include "api/api_text_entities.h" +#include "api/api_chat_filters.h" #include "api/api_chat_invite.h" #include "base/qthelp_regex.h" #include "base/qthelp_url.h" @@ -78,6 +79,18 @@ bool JoinGroupByHash( return true; } +bool JoinFilterBySlug( + Window::SessionController *controller, + const Match &match, + const QVariant &context) { + if (!controller) { + return false; + } + Api::CheckFilterInvite(controller, match->captured(1)); + controller->window().activate(); + return true; +} + bool ShowStickerSet( Window::SessionController *controller, const Match &match, @@ -829,6 +842,10 @@ const std::vector &LocalUrlHandlers() { u"^join/?\\?invite=([a-zA-Z0-9\\.\\_\\-]+)(&|$)"_q, JoinGroupByHash }, + { + u"^list/?\\?slug=([a-zA-Z0-9\\.\\_\\-]+)(&|$)"_q, + JoinFilterBySlug + }, { u"^(addstickers|addemoji)/?\\?set=([a-zA-Z0-9\\.\\_]+)(&|$)"_q, ShowStickerSet @@ -953,6 +970,8 @@ QString TryConvertUrlToLocal(QString url) { return u"tg://resolve?phone="_q + phoneMatch->captured(1) + (params.isEmpty() ? QString() : '&' + params); } else if (const auto joinChatMatch = regex_match(u"^(joinchat/|\\+|\\%20)([a-zA-Z0-9\\.\\_\\-]+)(\\?|$)"_q, query, matchOptions)) { return u"tg://join?invite="_q + url_encode(joinChatMatch->captured(2)); + } else if (const auto joinFilterMatch = regex_match(u"^(list/)([a-zA-Z0-9\\.\\_\\-]+)(\\?|$)"_q, query, matchOptions)) { + return u"tg://list?slug="_q + url_encode(joinFilterMatch->captured(2)); } else if (const auto stickerSetMatch = regex_match(u"^(addstickers|addemoji)/([a-zA-Z0-9\\.\\_]+)(\\?|$)"_q, query, matchOptions)) { return u"tg://"_q + stickerSetMatch->captured(1) + "?set=" + url_encode(stickerSetMatch->captured(2)); } else if (const auto themeMatch = regex_match(u"^addtheme/([a-zA-Z0-9\\.\\_]+)(\\?|$)"_q, query, matchOptions)) { diff --git a/Telegram/SourceFiles/data/data_chat_filters.cpp b/Telegram/SourceFiles/data/data_chat_filters.cpp index 7f255f4a6..1404c5fdd 100644 --- a/Telegram/SourceFiles/data/data_chat_filters.cpp +++ b/Telegram/SourceFiles/data/data_chat_filters.cpp @@ -329,6 +329,11 @@ void ChatFilters::load() { load(false); } +void ChatFilters::reload() { + _reloading = true; + load(); +} + void ChatFilters::load(bool force) { if (_loadRequestId && !force) { return; @@ -341,6 +346,10 @@ void ChatFilters::load(bool force) { _loadRequestId = 0; }).fail([=] { _loadRequestId = 0; + if (_reloading) { + _reloading = false; + _listChanged.fire({}); + } }).send(); } @@ -372,8 +381,9 @@ void ChatFilters::received(const QVector &list) { if (!ranges::contains(begin(_list), end(_list), 0, &ChatFilter::id)) { _list.insert(begin(_list), ChatFilter()); } - if (changed || !_loaded) { + if (changed || !_loaded || _reloading) { _loaded = true; + _reloading = false; _listChanged.fire({}); } } diff --git a/Telegram/SourceFiles/data/data_chat_filters.h b/Telegram/SourceFiles/data/data_chat_filters.h index ff5e697a4..69999a124 100644 --- a/Telegram/SourceFiles/data/data_chat_filters.h +++ b/Telegram/SourceFiles/data/data_chat_filters.h @@ -112,6 +112,7 @@ public: void setPreloaded(const QVector &result); void load(); + void reload(); void apply(const MTPUpdate &update); void set(ChatFilter filter); void remove(FilterId id); @@ -175,6 +176,7 @@ private: mtpRequestId _saveOrderRequestId = 0; mtpRequestId _saveOrderAfterId = 0; bool _loaded = false; + bool _reloading = false; mtpRequestId _suggestedRequestId = 0; std::vector _suggested; diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style index 85a982fb6..b5b304ec7 100644 --- a/Telegram/SourceFiles/settings/settings.style +++ b/Telegram/SourceFiles/settings/settings.style @@ -538,3 +538,13 @@ powerSavingButtonNoIcon: SettingsButton(powerSavingButton) { padding: margins(22px, 8px, 22px, 8px); } powerSavingSubtitlePadding: margins(0px, 4px, 0px, -2px); + +filterInviteBox: Box(defaultBox) { + buttonPadding: margins(12px, 12px, 12px, 12px); + buttonHeight: 44px; + button: RoundButton(defaultActiveButton) { + height: 44px; + textTop: 12px; + font: font(13px semibold); + } +} From fb16375a195dc9f52cb476ab93810683eeb84aa2 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 28 Mar 2023 20:24:54 +0400 Subject: [PATCH 004/676] Delete filter links, edit filter link names. --- .../boxes/filters/edit_filter_box.cpp | 1 + .../boxes/filters/edit_filter_links.cpp | 110 +++++++++++++----- .../SourceFiles/data/data_chat_filters.cpp | 23 +++- Telegram/SourceFiles/data/data_chat_filters.h | 2 +- .../SourceFiles/settings/settings_folders.cpp | 20 +++- 5 files changed, 117 insertions(+), 39 deletions(-) diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp index 26a0a6d31..c52d16202 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp @@ -692,6 +692,7 @@ void EditFilterBox( rules.never()); 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)); diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp index 3fa2aeeee..30298fb72 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp @@ -87,6 +87,7 @@ void ShowEmptyLinkError(not_null window) { void ChatFilterLinkBox( not_null box, + not_null session, Data::ChatFilterLink data) { using namespace rpl::mappers; @@ -139,10 +140,20 @@ void ChatFilterLinkBox( labelField->setMaxLength(kMaxLinkTitleLength); Settings::AddDivider(container); + box->setFocusCallback([=] { + labelField->setFocusFast(); + }); + const auto &saveLabel = link.isEmpty() ? tr::lng_formatting_link_create : tr::lng_settings_save; - box->addButton(saveLabel(), [=] {}); + box->addButton(saveLabel(), [=] { + session->data().chatsFilters().edit( + data.id, + data.url, + labelField->getLastText().trimmed()); + box->closeBox(); + }); box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); } @@ -420,6 +431,19 @@ void LinkController::addHeader(not_null container) { }, divider->lifetime()); } +object_ptr DeleteLinkBox( + not_null window, + const InviteLinkData &link) { + const auto sure = [=](Fn &&close) { + window->session().data().chatsFilters().destroy(link.id, link.url); + close(); + }; + return Ui::MakeConfirmBox({ + u"Are you sure you want to delete this link?"_q, // langs + sure, + }); +} + void LinkController::addLinkBlock(not_null container) { using namespace Settings; @@ -439,14 +463,14 @@ void LinkController::addLinkBlock(not_null container) { Ui::LayerOption::KeepOther); }); const auto editLink = crl::guard(weak, [=] { - //delegate()->peerListShowBox( - // EditLinkBox(_window, _data.current()), - // Ui::LayerOption::KeepOther); + delegate()->peerListShowBox( + Box(ChatFilterLinkBox, &_window->session(), _data), + Ui::LayerOption::KeepOther); }); const auto deleteLink = crl::guard(weak, [=] { - //delegate()->peerListShowBox( - // DeleteLinkBox(_window, _data.current()), - // Ui::LayerOption::KeepOther); + delegate()->peerListShowBox( + DeleteLinkBox(_window, _data), + Ui::LayerOption::KeepOther); }); const auto createMenu = [=] { @@ -466,7 +490,7 @@ void LinkController::addLinkBlock(not_null container) { getLinkQr, &st::menuIconQrCode); result->addAction( - tr::lng_group_invite_context_edit(tr::now), + u"Name Link"_q, // langs editLink, &st::menuIconEdit); result->addAction( @@ -502,6 +526,8 @@ void LinkController::addLinkBlock(not_null container) { } void LinkController::prepare() { + Expects(!_data.url.isEmpty() || _data.chats.empty()); + setupAboveWidget(); for (const auto &history : _data.chats) { const auto peer = history->peer; @@ -522,7 +548,7 @@ void LinkController::prepare() { delegate()->peerListAppendRow(std::move(row)); if (const auto error = ErrorForSharing(history)) { raw->setCustomStatus(*error); - } else { + } else if (!_data.url.isEmpty()) { _allowed.emplace(peer); } } @@ -663,32 +689,52 @@ base::unique_qptr LinksController::createRowContextMenu( const auto real = static_cast(row.get()); const auto data = real->data(); const auto link = data.url; + const auto copyLink = [=] { + CopyInviteLink(delegate()->peerListToastParent(), link); + }; + const auto shareLink = [=] { + delegate()->peerListShowBox( + ShareInviteLinkBox(&_window->session(), link), + Ui::LayerOption::KeepOther); + }; + const auto getLinkQr = [=] { + delegate()->peerListShowBox( + InviteLinkQrBox(link), + Ui::LayerOption::KeepOther); + }; + const auto editLink = [=] { + delegate()->peerListShowBox( + Box(ChatFilterLinkBox, &_window->session(), data), + Ui::LayerOption::KeepOther); + }; + const auto deleteLink = [=] { + delegate()->peerListShowBox( + DeleteLinkBox(_window, data), + Ui::LayerOption::KeepOther); + }; 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); + 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( + u"Name Link"_q, // langs + editLink, + &st::menuIconEdit); + result->addAction( + tr::lng_group_invite_context_delete(tr::now), + deleteLink, + &st::menuIconDelete); return result; } diff --git a/Telegram/SourceFiles/data/data_chat_filters.cpp b/Telegram/SourceFiles/data/data_chat_filters.cpp index 1404c5fdd..8cf286943 100644 --- a/Telegram/SourceFiles/data/data_chat_filters.cpp +++ b/Telegram/SourceFiles/data/data_chat_filters.cpp @@ -454,15 +454,36 @@ void ChatFilters::edit( if (i != end(links)) { i->title = title; _communityLinksUpdated.fire_copy(id); + + _owner->session().api().request(MTPcommunities_EditExportedInvite( + MTP_flags(MTPcommunities_EditExportedInvite::Flag::f_title), + MTP_inputCommunityDialogFilter(MTP_int(id)), + MTP_string(url), + MTP_string(title), + MTPVector() // peers + )).done([=](const MTPExportedCommunityInvite &result) { + //const auto &data = result.data(); + //const auto link = _owner->chatsFilters().add(id, result); + //done(link); + }).fail([=](const MTP::Error &error) { + //done({ .id = id }); + }).send(); } } -void ChatFilters::remove(FilterId id, const QString &url) { +void ChatFilters::destroy(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); + + const auto api = &_owner->session().api(); + api->request(_linksRequestId).cancel(); + _linksRequestId = api->request(MTPcommunities_DeleteExportedInvite( + MTP_inputCommunityDialogFilter(MTP_int(id)), + MTP_string(url) + )).send(); } } diff --git a/Telegram/SourceFiles/data/data_chat_filters.h b/Telegram/SourceFiles/data/data_chat_filters.h index 69999a124..83875fd43 100644 --- a/Telegram/SourceFiles/data/data_chat_filters.h +++ b/Telegram/SourceFiles/data/data_chat_filters.h @@ -154,7 +154,7 @@ public: FilterId id, const QString &url, const QString &title); - void remove(FilterId id, const QString &url); + void destroy(FilterId id, const QString &url); rpl::producer> communityLinks( FilterId id) const; void reloadCommunityLinks(FilterId id); diff --git a/Telegram/SourceFiles/settings/settings_folders.cpp b/Telegram/SourceFiles/settings/settings_folders.cpp index 4a8dc7e70..a34a584aa 100644 --- a/Telegram/SourceFiles/settings/settings_folders.cpp +++ b/Telegram/SourceFiles/settings/settings_folders.cpp @@ -629,6 +629,15 @@ void FilterRowButton::paintEvent(QPaintEvent *e) { ] { const auto api = &session->api(); const auto filters = &session->data().chatsFilters(); + const auto ids = std::make_shared< + base::flat_set + >(); + const auto checkFinished = [=] { + if (ids->empty() && next) { + Assert(updated.id() != 0); + next(updated); + } + }; for (const auto &update : updates) { filters->apply(update); } @@ -639,15 +648,16 @@ void FilterRowButton::paintEvent(QPaintEvent *e) { for (auto &request : requests) { previousId = api->request( std::move(request) - ).afterRequest(previousId).send(); + ).done([=](const auto &, mtpRequestId id) { + ids->remove(id); + checkFinished(); + }).afterRequest(previousId).send(); + ids->emplace(previousId); } if (!order.empty() && !addRequests.empty()) { filters->saveOrder(order, previousId); } - if (next) { - Assert(updated.id() != 0); - next(updated); - } + checkFinished(); }); }; return [copy = state->save] { From 86f9875662705c5b4d6d852b3736efed24f66702 Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 29 Mar 2023 15:11:06 +0400 Subject: [PATCH 005/676] Add most of the phrases to the langpack. --- Telegram/Resources/langs/lang.strings | 49 +++++++- Telegram/SourceFiles/api/api_chat_filters.cpp | 87 ++++++++----- .../boxes/filters/edit_filter_box.cpp | 13 +- .../boxes/filters/edit_filter_links.cpp | 118 +++++++++++++----- 4 files changed, 197 insertions(+), 70 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index e694cc160..b41c7d502 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -232,6 +232,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_file_size_limit1" = "The document can't be sent, because it is larger than {size}."; "lng_file_size_limit2" = "You can double this limit to {size} per document by subscribing to **Telegram Premium**."; +"lng_filter_links_limit_title" = "Limit Reached"; +"lng_filter_links_limit1#one" = "Sorry, you can't create more than **{count}** invite link."; +"lng_filter_links_limit1#other" = "Sorry, you can't create more than **{count}** invite links."; +"lng_filter_links_limit2#one" = "You can increase the limit to **{count}** link by subscribing to **Telegram Premium**."; +"lng_filter_links_limit2#other" = "You can increase the limit to **{count}** links by subscribing to **Telegram Premium**."; + +"lng_filter_shared_limit_title" = "Limit Reached"; +"lng_filter_shared_limit1#one" = "Sorry, you can't add more than **{count}** shareable folders."; +"lng_filter_shared_limit1#other" = "Sorry, you can't add more than **{count}** shareable folders."; +"lng_filter_shared_limit2#one" = "You can increase the limit up to **{count}** folder by subscribing to **Telegram Premium**."; +"lng_filter_shared_limit2#other" = "You can increase the limit up to **{count}** folders by subscribing to **Telegram Premium**."; + "lng_limits_increase" = "Increase Limit"; "lng_sticker_premium_text" = "This pack contains premium stickers like this one."; @@ -3541,22 +3553,46 @@ 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" = "Invite links"; +"lng_filters_link_badge" = "New"; "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_cant" = "No way to share folders with chat types or excluded chats."; "lng_filters_link_about" = "Share access to some of this folder's groups and channels with others."; +"lng_filters_link_about_many" = "Create more links to set up different access levels for different people."; "lng_filters_link_title" = "Share Folder"; -"lng_filters_link_select" = "Select the chats you want to share with the link."; +"lng_filters_link_share_about" = "Anyone with this link can add the {folder} folder and the chats selected below."; +"lng_filters_link_subtitle" = "Invite link"; +"lng_filters_link_chats_none" = "No chats selected"; +"lng_filters_link_chats#one" = "{count} chat selected"; +"lng_filters_link_chats#other" = "{count} chats selected"; +"lng_filters_link_bot_status" = "you can't share chats with bots"; +"lng_filters_link_bot_error" = "Chat's with bots can't be shared You can't share chats with bots"; +"lng_filters_link_private_status" = "you can't share private chats"; +"lng_filters_link_private_error" = "You can't share private chats"; +"lng_filters_link_noadmin_status" = "you can't invite others here"; +"lng_filters_link_noadmin_group_error" = "You don't have the admin rights to share invite links to this group chat."; +"lng_filters_link_noadmin_channel_error" = "You don't have the admin rights to share invite links to this channel."; +"lng_filters_link_chats_about" = "Select groups and channels that you want everyone who adds the folder via invite link to join."; +"lng_filters_link_no_about" = "There are no chats in this folder that you can share with others."; +"lng_filters_link_chats_no" = "These chats cannot be shared"; +"lng_filters_link_chats_no_about" = "You can only share groups and channels in which you are allowed to create invite links."; +"lng_filters_link_name_it" = "Name Link"; +"lng_filters_link_delete_sure" = "Are you sure you want to delete this 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_add_button" = "Add {folder}"; +"lng_filters_by_link_add_no" = "Do not add this 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_join_no" = "Do not join any chats"; +"lng_filters_by_link_already" = "Folder already added"; +"lng_filters_by_link_already_about" = "You have already added the folder {folder} and all its chats."; +"lng_filters_by_link_in#one" = "{count} chat in this folder"; +"lng_filters_by_link_in#other" = "{count} chats in this folder"; "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"; @@ -3565,6 +3601,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "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_filters_bar_you_can#one" = "You can join {count} new chat"; +"lng_filters_bar_you_can#other" = "You can join {count} new chats"; +"lng_filters_bar_view" = "Click here to view them"; "lng_chat_theme_change" = "Change colors"; "lng_chat_theme_none" = "No\nTheme"; diff --git a/Telegram/SourceFiles/api/api_chat_filters.cpp b/Telegram/SourceFiles/api/api_chat_filters.cpp index 837af84d9..5396f14f7 100644 --- a/Telegram/SourceFiles/api/api_chat_filters.cpp +++ b/Telegram/SourceFiles/api/api_chat_filters.cpp @@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "main/main_session.h" #include "ui/boxes/confirm_box.h" +#include "ui/text/text_utilities.h" #include "ui/toasts/common_toasts.h" #include "ui/widgets/buttons.h" #include "window/window_session_controller.h" @@ -64,6 +65,7 @@ public: private: void setupAboveWidget(); + void setupBelowWidget(); const not_null _window; @@ -81,16 +83,15 @@ private: }; [[nodiscard]] rpl::producer TitleText(HeaderType type) { - // langs switch (type) { case HeaderType::AddingFilter: - return rpl::single(u"Add Folder"_q); + return tr::lng_filters_by_link_title(); case HeaderType::AddingChats: - return rpl::single(u"Add Chats to Folder"_q); + return tr::lng_filters_by_link_more(); case HeaderType::AllAdded: - return rpl::single(u"Folder Already Added"_q); + return tr::lng_filters_by_link_already(); case HeaderType::Removing: - return rpl::single(u"Remove Folder"_q); + return tr::lng_filters_by_link_remove(); } Unexpected("HeaderType in TitleText."); } @@ -98,27 +99,44 @@ private: void FillHeader( not_null container, HeaderDescriptor descriptor) { - // langs - const auto description = (descriptor.type == HeaderType::AddingFilter) - ? (u"Do you want to add a new chat folder "_q - + descriptor.title - + u" and join its groups and channels?"_q) + const auto phrase = (descriptor.type == HeaderType::AddingFilter) + ? tr::lng_filters_by_link_sure : (descriptor.type == HeaderType::AddingChats) - ? (u"Do you want to join "_q - + QString::number(descriptor.badge) - + u" chats and add them to your folder "_q - + descriptor.title + '?') + ? tr::lng_filters_by_link_more_sure : (descriptor.type == HeaderType::AllAdded) - ? (u"You have already added the folder "_q - + descriptor.title - + u" and all its chats."_q) - : (u"Do you want to quit the chats you joined " - "when adding the folder "_q - + descriptor.title + '?'); + ? tr::lng_filters_by_link_already_about + : tr::lng_filters_by_link_remove_sure; + auto boldTitle = Ui::Text::Bold(descriptor.title); + auto description = (descriptor.type == HeaderType::AddingFilter) + ? tr::lng_filters_by_link_sure( + tr::now, + lt_folder, + std::move(boldTitle), + Ui::Text::WithEntities) + : (descriptor.type == HeaderType::AddingChats) + ? tr::lng_filters_by_link_more_sure( + tr::now, + lt_folder, + std::move(boldTitle), + Ui::Text::WithEntities) + : (descriptor.type == HeaderType::AllAdded) + ? tr::lng_filters_by_link_already_about( + tr::now, + lt_folder, + std::move(boldTitle), + Ui::Text::WithEntities) + : tr::lng_filters_by_link_remove_sure( + tr::now, + lt_folder, + std::move(boldTitle), + Ui::Text::WithEntities); container->add( object_ptr( container, - description, + phrase( + lt_folder, + rpl::single(Ui::Text::Bold(descriptor.title)), + Ui::Text::WithEntities), st::boxDividerLabel), st::boxRowPadding); } @@ -152,12 +170,12 @@ void ImportInvite( } ToggleChatsController::ToggleChatsController( - not_null window, - ToggleAction action, - const QString &slug, - FilterId filterId, - const QString &title, - std::vector> chats) + not_null window, + ToggleAction action, + const QString &slug, + FilterId filterId, + const QString &title, + std::vector> chats) : _window(window) , _action(action) , _slug(slug) @@ -168,6 +186,7 @@ ToggleChatsController::ToggleChatsController( void ToggleChatsController::prepare() { setupAboveWidget(); + setupBelowWidget(); auto selected = base::flat_set>(); for (const auto &peer : _chats) { auto row = std::make_unique(peer); @@ -211,9 +230,22 @@ void ToggleChatsController::setupAboveWidget() { .badge = (type == HeaderType::AddingChats) ? int(_chats.size()) : 0, }); + // lng_filters_by_link_join; // langs + delegate()->peerListSetAboveWidget(std::move(wrap)); } +void ToggleChatsController::setupBelowWidget() { + delegate()->peerListSetBelowWidget( + object_ptr( + (QWidget*)nullptr, + object_ptr( + (QWidget*)nullptr, + tr::lng_filters_by_link_about(), + st::boxDividerLabel), + st::settingsDividerLabelPadding)); +} + Main::Session &ToggleChatsController::session() const { return _window->session(); } @@ -226,7 +258,6 @@ auto ToggleChatsController::selectedValue() const [[nodiscard]] void AlreadyFilterBox( not_null box, const QString &title) { - // langs box->setTitle(TitleText(HeaderType::AllAdded)); FillHeader(box->verticalLayout(), { diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp index c52d16202..003efe022 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp @@ -744,11 +744,7 @@ void EditFilterBox( } const auto shared = CollectFilterLinkChats(*result); if (shared.empty()) { - // langs - Ui::ShowMultilineToast({ - .parentOverride = Window::Show(window).toastParent(), - .text = { tr::lng_filters_link_cant(tr::now) }, - }); + window->show(ShowLinkBox(window, *result, {})); return; } saveAnd(*result, crl::guard(box, [=](Data::ChatFilter updated) { @@ -767,7 +763,12 @@ void EditFilterBox( }, createLink->lifetime()); AddSkip(content); - AddDividerText(content, tr::lng_filters_link_about()); + AddDividerText( + content, + rpl::conditional( + state->hasLinks.value(), + tr::lng_filters_link_about_many(), + tr::lng_filters_link_about())); const auto show = std::make_shared(box); const auto refreshPreviews = [=] { diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp index 30298fb72..a181d3dac 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/peers/edit_peer_invite_link.h" // InviteLinkQrBox. #include "boxes/peer_list_box.h" #include "data/data_channel.h" +#include "data/data_chat.h" #include "data/data_chat_filters.h" #include "data/data_session.h" #include "data/data_user.h" @@ -22,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/boxes/confirm_box.h" #include "ui/controls/invite_link_buttons.h" #include "ui/controls/invite_link_label.h" +#include "ui/text/text_utilities.h" #include "ui/toasts/common_toasts.h" #include "ui/widgets/input_fields.h" #include "ui/widgets/popup_menu.h" @@ -60,28 +62,49 @@ struct InviteLinkAction { Type type = Type::Copy; }; -[[nodiscard]] std::optional ErrorForSharing( +struct Errors { + QString status; + QString toast; +}; + +[[nodiscard]] std::optional ErrorForSharing( not_null history) { + const auto result = [](const QString &status, const QString &toast) { + return Errors{ status, toast }; + }; const auto peer = history->peer; - if (const auto user = peer->asUser()) { // langs + if (const auto user = peer->asUser()) { 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; + ? result( + tr::lng_filters_link_bot_status(tr::now), + tr::lng_filters_link_bot_error(tr::now)) + : result( + tr::lng_filters_link_private_status(tr::now), + tr::lng_filters_link_private_error(tr::now)); + } else if (const auto chat = history->peer->asChat()) { + if (!chat->canHaveInviteLink()) { + return result( + tr::lng_filters_link_noadmin_status(tr::now), + tr::lng_filters_link_noadmin_group_error(tr::now)); + } + return std::nullopt; + } else if (const auto channel = history->peer->asChannel()) { + if (!channel->canHaveInviteLink()) { + return result( + tr::lng_filters_link_noadmin_status(tr::now), + (channel->isMegagroup() + ? tr::lng_filters_link_noadmin_group_error(tr::now) + : tr::lng_filters_link_noadmin_channel_error(tr::now))); } return std::nullopt; - } else { - return u"you can't share this :("_q; } + Unexpected("Peer type in ErrorForSharing."); } void ShowEmptyLinkError(not_null window) { - // langs Ui::ShowMultilineToast({ .parentOverride = Window::Show(window).toastParent(), - .text = { u"Link should have at least one chat shared."_q }, + .text = { tr::lng_filters_empty(tr::now) }, }); } @@ -359,14 +382,16 @@ public: private: void setupAboveWidget(); + void setupBelowWidget(); void addHeader(not_null container); void addLinkBlock(not_null container); const not_null _window; InviteLinkData _data; + QString _filterTitle; base::flat_set> _filterChats; - base::flat_set> _allowed; + base::flat_map, QString> _denied; rpl::variable>> _selected; base::flat_set> _initial; @@ -387,6 +412,7 @@ LinkController::LinkController( const Data::ChatFilter &filter, InviteLinkData data) : _window(window) +, _filterTitle(filter.title()) , _filterChats(filter.always()) { _data = std::move(data); _link = _data.url; @@ -421,7 +447,12 @@ void LinkController::addHeader(not_null container) { verticalLayout, object_ptr( verticalLayout, - tr::lng_filters_about(), // langs + (_data.url.isEmpty() + ? tr::lng_filters_link_no_about(Ui::Text::WithEntities) + : tr::lng_filters_link_share_about( + lt_folder, + rpl::single(Ui::Text::Bold(_filterTitle)), + Ui::Text::WithEntities)), st::settingsFilterDividerLabel)), st::settingsFilterDividerLabelPadding); @@ -439,8 +470,9 @@ object_ptr DeleteLinkBox( close(); }; return Ui::MakeConfirmBox({ - u"Are you sure you want to delete this link?"_q, // langs - sure, + .text = tr::lng_filters_link_delete_sure(tr::now), + .confirmed = sure, + .confirmText = tr::lng_box_delete(tr::now), }); } @@ -490,7 +522,7 @@ void LinkController::addLinkBlock(not_null container) { getLinkQr, &st::menuIconQrCode); result->addAction( - u"Name Link"_q, // langs + tr::lng_filters_link_name_it(tr::now), editLink, &st::menuIconEdit); result->addAction( @@ -499,8 +531,7 @@ void LinkController::addLinkBlock(not_null container) { &st::menuIconDelete); return result; }; - - AddSubsectionTitle(container, tr::lng_manage_peer_link_invite()); + AddSubsectionTitle(container, tr::lng_filters_link_subtitle()); const auto prefix = u"https://"_q; const auto label = container->lifetime().make_state( @@ -529,9 +560,9 @@ void LinkController::prepare() { Expects(!_data.url.isEmpty() || _data.chats.empty()); setupAboveWidget(); + setupBelowWidget(); 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)); @@ -547,9 +578,10 @@ void LinkController::prepare() { const auto raw = row.get(); delegate()->peerListAppendRow(std::move(row)); if (const auto error = ErrorForSharing(history)) { - raw->setCustomStatus(*error); - } else if (!_data.url.isEmpty()) { - _allowed.emplace(peer); + raw->setCustomStatus(error->status); + _denied.emplace(peer, error->toast); + } else if (_data.url.isEmpty()) { + _denied.emplace(peer); } } delegate()->peerListRefreshRows(); @@ -557,8 +589,15 @@ void LinkController::prepare() { } void LinkController::rowClicked(not_null row) { - if (_allowed.contains(row->peer())) { - const auto peer = row->peer(); + const auto peer = row->peer(); + if (const auto i = _denied.find(peer); i != end(_denied)) { + if (!i->second.isEmpty()) { + Ui::ShowMultilineToast({ + .parentOverride = delegate()->peerListToastParent(), + .text = { i->second }, + }); + } + } else { const auto checked = row->checked(); auto selected = _selected.current(); delegate()->peerListSetRowChecked(row, !checked); @@ -588,12 +627,16 @@ void LinkController::setupAboveWidget() { addLinkBlock(container); } - // langs auto subtitle = _selected.value( - ) | rpl::map([](const base::flat_set> &selected) { - return selected.empty() - ? u"No chats selected"_q - : (QString::number(selected.size()) + u" chats selected"_q); + ) | rpl::map([=](const base::flat_set> &selected) { + return _data.url.isEmpty() + ? tr::lng_filters_link_chats_no(tr::now) + : selected.empty() + ? tr::lng_filters_link_chats_none(tr::now) + : tr::lng_filters_link_chats( + tr::now, + lt_count, + float64(selected.size())); }); Settings::AddSubsectionTitle( container, @@ -602,6 +645,19 @@ void LinkController::setupAboveWidget() { delegate()->peerListSetAboveWidget(std::move(wrap)); } +void LinkController::setupBelowWidget() { + delegate()->peerListSetBelowWidget( + object_ptr( + (QWidget*)nullptr, + object_ptr( + (QWidget*)nullptr, + (_data.url.isEmpty() + ? tr::lng_filters_link_chats_no_about() + : tr::lng_filters_link_chats_about()), + st::boxDividerLabel), + st::settingsDividerLabelPadding)); +} + Main::Session &LinkController::session() const { return _window->session(); } @@ -728,7 +784,7 @@ base::unique_qptr LinksController::createRowContextMenu( getLinkQr, &st::menuIconQrCode); result->addAction( - u"Name Link"_q, // langs + tr::lng_filters_link_name_it(tr::now), editLink, &st::menuIconEdit); result->addAction( @@ -931,7 +987,7 @@ object_ptr ShowLinkBox( auto initBox = [=](not_null box) { box->setTitle(!link.title.isEmpty() ? rpl::single(link.title) - : tr::lng_manage_peer_link_invite()); + : tr::lng_filters_link_title()); raw->hasChangesValue( ) | rpl::start_with_next([=](bool has) { From 7684dbc7011e9e6c5ec7700f3be348b7b16da7c4 Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 29 Mar 2023 17:11:04 +0400 Subject: [PATCH 006/676] Allow "Nobody" in group/channel invites privacy. --- .../SourceFiles/settings/settings_privacy_controllers.cpp | 4 ---- Telegram/SourceFiles/settings/settings_privacy_controllers.h | 1 - 2 files changed, 5 deletions(-) diff --git a/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp b/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp index f983f856c..f28b44e96 100644 --- a/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp +++ b/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp @@ -686,10 +686,6 @@ rpl::producer GroupsInvitePrivacyController::title() const { return tr::lng_edit_privacy_groups_title(); } -bool GroupsInvitePrivacyController::hasOption(Option option) const { - return (option != Option::Nobody); -} - rpl::producer GroupsInvitePrivacyController::optionsTitleKey( ) const { return tr::lng_edit_privacy_groups_header(); diff --git a/Telegram/SourceFiles/settings/settings_privacy_controllers.h b/Telegram/SourceFiles/settings/settings_privacy_controllers.h index 2d7d26ae6..3aa1e36d2 100644 --- a/Telegram/SourceFiles/settings/settings_privacy_controllers.h +++ b/Telegram/SourceFiles/settings/settings_privacy_controllers.h @@ -126,7 +126,6 @@ public: Key key() const override; rpl::producer title() const override; - bool hasOption(Option option) const override; rpl::producer optionsTitleKey() const override; rpl::producer exceptionButtonTextKey( Exception exception) const override; From 0faadc8fa0791d61bdbe3a0320b380446f7f3dcc Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 29 Mar 2023 17:23:21 +0400 Subject: [PATCH 007/676] Implement folder link add / join design. --- Telegram/SourceFiles/api/api_chat_filters.cpp | 260 ++++++---- .../boxes/filters/edit_filter_links.cpp | 8 +- Telegram/SourceFiles/boxes/peer_list_box.cpp | 1 + .../SourceFiles/mtproto/session_private.cpp | 5 + .../platform/platform_overlay_widget.cpp | 17 +- Telegram/SourceFiles/settings/settings.style | 48 +- .../ui/controls/filter_link_header.cpp | 446 ++++++++++++++++++ .../ui/controls/filter_link_header.h | 49 ++ Telegram/cmake/td_ui.cmake | 2 + Telegram/lib_ui | 2 +- 10 files changed, 725 insertions(+), 113 deletions(-) create mode 100644 Telegram/SourceFiles/ui/controls/filter_link_header.cpp create mode 100644 Telegram/SourceFiles/ui/controls/filter_link_header.h diff --git a/Telegram/SourceFiles/api/api_chat_filters.cpp b/Telegram/SourceFiles/api/api_chat_filters.cpp index 5396f14f7..03149b6e3 100644 --- a/Telegram/SourceFiles/api/api_chat_filters.cpp +++ b/Telegram/SourceFiles/api/api_chat_filters.cpp @@ -15,11 +15,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_session.h" #include "lang/lang_keys.h" #include "main/main_session.h" +#include "settings/settings_common.h" #include "ui/boxes/confirm_box.h" +#include "ui/controls/filter_link_header.h" #include "ui/text/text_utilities.h" #include "ui/toasts/common_toasts.h" #include "ui/widgets/buttons.h" #include "window/window_session_controller.h" +#include "styles/style_filter_icons.h" #include "styles/style_layers.h" #include "styles/style_settings.h" @@ -31,19 +34,6 @@ enum class ToggleAction { Removing, }; -enum class HeaderType { - AddingFilter, - AddingChats, - AllAdded, - Removing, -}; - -struct HeaderDescriptor { - base::required type; - base::required title; - int badge = 0; -}; - class ToggleChatsController final : public PeerListController , public base::has_weak_ptr { @@ -63,11 +53,14 @@ public: [[nodiscard]] auto selectedValue() const -> rpl::producer>>; + void setAddedTopHeight(int addedTopHeight); + private: void setupAboveWidget(); void setupBelowWidget(); const not_null _window; + Ui::RpWidget *_addedTopWidget = nullptr; ToggleAction _action = ToggleAction::Adding; QString _slug; @@ -82,44 +75,42 @@ private: }; -[[nodiscard]] rpl::producer TitleText(HeaderType type) { +[[nodiscard]] tr::phrase<> TitleText(Ui::FilterLinkHeaderType type) { + using Type = Ui::FilterLinkHeaderType; switch (type) { - case HeaderType::AddingFilter: - return tr::lng_filters_by_link_title(); - case HeaderType::AddingChats: - return tr::lng_filters_by_link_more(); - case HeaderType::AllAdded: - return tr::lng_filters_by_link_already(); - case HeaderType::Removing: - return tr::lng_filters_by_link_remove(); + case Type::AddingFilter: return tr::lng_filters_by_link_title; + case Type::AddingChats: return tr::lng_filters_by_link_more; + case Type::AllAdded: return tr::lng_filters_by_link_already; + case Type::Removing: return tr::lng_filters_by_link_remove; } - Unexpected("HeaderType in TitleText."); + Unexpected("Ui::FilterLinkHeaderType in TitleText."); } -void FillHeader( - not_null container, - HeaderDescriptor descriptor) { - const auto phrase = (descriptor.type == HeaderType::AddingFilter) +[[nodiscard]] TextWithEntities AboutText( + Ui::FilterLinkHeaderType type, + const QString &title) { + using Type = Ui::FilterLinkHeaderType; + const auto phrase = (type == Type::AddingFilter) ? tr::lng_filters_by_link_sure - : (descriptor.type == HeaderType::AddingChats) + : (type == Type::AddingChats) ? tr::lng_filters_by_link_more_sure - : (descriptor.type == HeaderType::AllAdded) + : (type == Type::AllAdded) ? tr::lng_filters_by_link_already_about : tr::lng_filters_by_link_remove_sure; - auto boldTitle = Ui::Text::Bold(descriptor.title); - auto description = (descriptor.type == HeaderType::AddingFilter) + auto boldTitle = Ui::Text::Bold(title); + return (type == Type::AddingFilter) ? tr::lng_filters_by_link_sure( tr::now, lt_folder, std::move(boldTitle), Ui::Text::WithEntities) - : (descriptor.type == HeaderType::AddingChats) + : (type == Type::AddingChats) ? tr::lng_filters_by_link_more_sure( tr::now, lt_folder, std::move(boldTitle), Ui::Text::WithEntities) - : (descriptor.type == HeaderType::AllAdded) + : (type == Type::AllAdded) ? tr::lng_filters_by_link_already_about( tr::now, lt_folder, @@ -130,35 +121,92 @@ void FillHeader( lt_folder, std::move(boldTitle), Ui::Text::WithEntities); - container->add( - object_ptr( - container, - phrase( - lt_folder, - rpl::single(Ui::Text::Bold(descriptor.title)), - Ui::Text::WithEntities), - st::boxDividerLabel), - st::boxRowPadding); +} + +void InitFilterLinkHeader( + not_null box, + Fn setAddedTopHeight, + Ui::FilterLinkHeaderType type, + const QString &title, + rpl::producer count) { + auto header = Ui::MakeFilterLinkHeader(box, { + .type = type, + .title = TitleText(type)(tr::now), + .about = AboutText(type, title), + .folderTitle = title, + .folderIcon = &st::foldersCustomActive, + .badge = (type == Ui::FilterLinkHeaderType::AddingChats + ? std::move(count) + : rpl::single(0)), + }); + const auto widget = header.widget; + widget->resizeToWidth(st::boxWideWidth); + Ui::SendPendingMoveResizeEvents(widget); + + const auto min = widget->minimumHeight(), max = widget->maximumHeight(); + widget->resize(st::boxWideWidth, max); + + box->setAddedTopScrollSkip(max); + std::move( + header.wheelEvents + ) | rpl::start_with_next([=](not_null e) { + box->sendScrollViewportEvent(e); + }, widget->lifetime()); + + struct State { + bool processing = false; + int addedTopHeight = 0; + }; + const auto state = widget->lifetime().make_state(); + + box->scrolls( + ) | rpl::filter([=] { + return !state->processing; + }) | rpl::start_with_next([=] { + state->processing = true; + const auto guard = gsl::finally([&] { state->processing = false; }); + + const auto top = box->scrollTop(); + const auto height = box->scrollHeight(); + const auto headerHeight = std::max(max - top, min); + const auto addedTopHeight = max - headerHeight; + widget->resize(widget->width(), headerHeight); + if (state->addedTopHeight < addedTopHeight) { + setAddedTopHeight(addedTopHeight); + box->setAddedTopScrollSkip(headerHeight); + } else { + box->setAddedTopScrollSkip(headerHeight); + setAddedTopHeight(addedTopHeight); + } + state->addedTopHeight = addedTopHeight; + box->peerListRefreshRows(); + }, widget->lifetime()); + + box->setNoContentMargin(true); } void ImportInvite( base::weak_ptr weak, const QString &slug, - const base::flat_set> &peers) { + const base::flat_set> &peers, + Fn done, + Fn fail) { Expects(!peers.empty()); const auto peer = peers.front(); const auto api = &peer->session().api(); const auto callback = [=](const MTPUpdates &result) { api->applyUpdates(result); + done(); }; const auto error = [=](const MTP::Error &error) { if (const auto strong = weak.get()) { Ui::ShowMultilineToast({ .parentOverride = Window::Show(strong).toastParent(), - .text = { error.description() }, + .text = { error.type() }, }); } + fail(); }; auto inputs = peers | ranges::views::transform([](auto peer) { return MTPInputPeer(peer->input); @@ -182,6 +230,7 @@ ToggleChatsController::ToggleChatsController( , _filterId(filterId) , _filterTitle(title) , _chats(std::move(chats)) { + setStyleOverrides(&st::filterLinkChatsList); } void ToggleChatsController::prepare() { @@ -218,19 +267,14 @@ void ToggleChatsController::setupAboveWidget() { auto wrap = object_ptr((QWidget*)nullptr); const auto container = wrap.data(); - const auto type = !_filterId - ? HeaderType::AddingFilter - : (_action == ToggleAction::Adding) - ? HeaderType::AddingChats - : HeaderType::Removing; - delegate()->peerListSetTitle(TitleText(type)); - FillHeader(container, { - .type = type, - .title = _filterTitle, - .badge = (type == HeaderType::AddingChats) ? int(_chats.size()) : 0, - }); - - // lng_filters_by_link_join; // langs + _addedTopWidget = container->add(object_ptr(container)); + AddDivider(container); + AddSubsectionTitle( + container, + tr::lng_filters_by_link_join( + lt_count, + rpl::single(float64(_chats.size()))), + st::filterLinkSubsectionTitlePadding); delegate()->peerListSetAboveWidget(std::move(wrap)); } @@ -241,7 +285,7 @@ void ToggleChatsController::setupBelowWidget() { (QWidget*)nullptr, object_ptr( (QWidget*)nullptr, - tr::lng_filters_by_link_about(), + tr::lng_filters_by_link_about(tr::now), st::boxDividerLabel), st::settingsDividerLabelPadding)); } @@ -255,17 +299,10 @@ auto ToggleChatsController::selectedValue() const return _selected.value(); } -[[nodiscard]] void AlreadyFilterBox( - not_null box, - const QString &title) { - box->setTitle(TitleText(HeaderType::AllAdded)); +void ToggleChatsController::setAddedTopHeight(int addedTopHeight) { + Expects(addedTopHeight >= 0); - FillHeader(box->verticalLayout(), { - .type = HeaderType::AllAdded, - .title = title, - }); - - box->addButton(tr::lng_box_ok(), [=] { box->closeBox(); }); + _addedTopWidget->resize(_addedTopWidget->width(), addedTopHeight); } void ProcessFilterInvite( @@ -279,15 +316,11 @@ void ProcessFilterInvite( return; } Core::App().hideMediaView(); - if (peers.empty()) { - if (filterId) { - strong->show(Box(AlreadyFilterBox, title)); - } else { - Ui::ShowMultilineToast({ - .parentOverride = Window::Show(strong).toastParent(), - .text = { tr::lng_group_invite_bad_link(tr::now) }, - }); - } + if (peers.empty() && !filterId) { + Ui::ShowMultilineToast({ + .parentOverride = Window::Show(strong).toastParent(), + .text = { tr::lng_group_invite_bad_link(tr::now) }, + }); return; } auto controller = std::make_unique( @@ -298,42 +331,61 @@ void ProcessFilterInvite( title, std::move(peers)); const auto raw = controller.get(); - auto initBox = [=](not_null box) { + auto initBox = [=](not_null box) { box->setStyle(st::filterInviteBox); + + using Type = Ui::FilterLinkHeaderType; + const auto type = !filterId + ? Type::AddingFilter + : Type::AddingChats; + auto badge = raw->selectedValue( + ) | rpl::map([=](const base::flat_set> &peers) { + return int(peers.size()); + }); + InitFilterLinkHeader(box, [=](int addedTopHeight) { + raw->setAddedTopHeight(addedTopHeight); + }, type, title, rpl::duplicate(badge)); + + auto owned = Ui::FilterLinkProcessButton( + box, + type, + title, + std::move(badge)); + + const auto button = owned.data(); + box->widthValue( + ) | rpl::start_with_next([=](int width) { + const auto &padding = st::filterInviteBox.buttonPadding; + button->resizeToWidth(width + - padding.left() + - padding.right()); + button->moveToLeft(padding.left(), padding.top()); + }, button->lifetime()); + + box->addButton(std::move(owned)); + + struct State { + bool importing = false; + }; + const auto state = box->lifetime().make_state(); + raw->selectedValue( ) | rpl::start_with_next([=]( base::flat_set> &&peers) { - const auto count = int(peers.size()); - - box->clearButtons(); - auto button = object_ptr( - box, - rpl::single(count - ? u"Add %1 Chats"_q.arg(count) - : u"Don't add chats"_q), - st::defaultActiveButton); - const auto raw = button.data(); - - box->widthValue( - ) | rpl::start_with_next([=](int width) { - const auto &padding = st::filterInviteBox.buttonPadding; - raw->resizeToWidth(width - - padding.left() - - padding.right()); - raw->moveToLeft(padding.left(), padding.top()); - }, raw->lifetime()); - - raw->setClickedCallback([=] { - if (!count) { + button->setClickedCallback([=] { + if (peers.empty()) { box->closeBox(); //} else if (count + alreadyInFilter() >= ...) { // #TODO filters - } else { - ImportInvite(weak, slug, peers); + } else if (!state->importing) { + state->importing = true; + ImportInvite(weak, slug, peers, crl::guard(box, [=] { + box->closeBox(); + }), crl::guard(box, [=] { + state->importing = false; + })); } }); - - box->addButton(std::move(button)); }, box->lifetime()); }; strong->show( diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp index a181d3dac..d9f01e55f 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp @@ -531,7 +531,10 @@ void LinkController::addLinkBlock(not_null container) { &st::menuIconDelete); return result; }; - AddSubsectionTitle(container, tr::lng_filters_link_subtitle()); + AddSubsectionTitle( + container, + tr::lng_filters_link_subtitle(), + st::filterLinkSubsectionTitlePadding); const auto prefix = u"https://"_q; const auto label = container->lifetime().make_state( @@ -640,7 +643,8 @@ void LinkController::setupAboveWidget() { }); Settings::AddSubsectionTitle( container, - std::move(subtitle)); + std::move(subtitle), + st::filterLinkSubsectionTitlePadding); delegate()->peerListSetAboveWidget(std::move(wrap)); } diff --git a/Telegram/SourceFiles/boxes/peer_list_box.cpp b/Telegram/SourceFiles/boxes/peer_list_box.cpp index 2dbe7c67a..db5076952 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_box.cpp @@ -140,6 +140,7 @@ void PeerListBox::createMultiSelect() { void PeerListBox::setAddedTopScrollSkip(int skip) { _addedTopScrollSkip = skip; + _scrollBottomFixed = false; updateScrollSkips(); } diff --git a/Telegram/SourceFiles/mtproto/session_private.cpp b/Telegram/SourceFiles/mtproto/session_private.cpp index fe4beb835..729a95ed6 100644 --- a/Telegram/SourceFiles/mtproto/session_private.cpp +++ b/Telegram/SourceFiles/mtproto/session_private.cpp @@ -1630,6 +1630,11 @@ SessionPrivate::HandleResult SessionPrivate::handleOneReceived( _sessionSalt = data.vnew_server_salt().v; correctUnixtimeWithBadLocal(info.serverTime); + if (_bindMsgId) { + LOG(("Message Info: bad_server_salt received while binding temp key, restarting.")); + return HandleResult::RestartConnection; + } + if (setState(ConnectedState, ConnectingState)) { resendAll(); } diff --git a/Telegram/SourceFiles/platform/platform_overlay_widget.cpp b/Telegram/SourceFiles/platform/platform_overlay_widget.cpp index 5707b54f3..90bda5624 100644 --- a/Telegram/SourceFiles/platform/platform_overlay_widget.cpp +++ b/Telegram/SourceFiles/platform/platform_overlay_widget.cpp @@ -224,9 +224,20 @@ rpl::producer<> DefaultOverlayWidgetHelper::controlsActivations() { } rpl::producer DefaultOverlayWidgetHelper::controlsSideRightValue() { - return Ui::Platform::TitleControlsLayoutValue() | rpl::map([=] { - return _controls->controls.geometry().center().x() - > _controls->wrap.geometry().center().x(); + using namespace Ui::Platform; + + return TitleControlsLayoutValue( + ) | rpl::map([=](const TitleControls::Layout &layout) { + // See TitleControls::updateControlsPosition. + if (ranges::contains(layout.left, TitleControl::Close)) { + return false; + } else if (ranges::contains(layout.right, TitleControl::Close)) { + return true; + } else if (layout.left.size() > layout.right.size()) { + return false; + } else { + return true; + } }) | rpl::distinct_until_changed(); } diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style index b5b304ec7..2af9de650 100644 --- a/Telegram/SourceFiles/settings/settings.style +++ b/Telegram/SourceFiles/settings/settings.style @@ -540,11 +540,53 @@ powerSavingButtonNoIcon: SettingsButton(powerSavingButton) { powerSavingSubtitlePadding: margins(0px, 4px, 0px, -2px); filterInviteBox: Box(defaultBox) { - buttonPadding: margins(12px, 12px, 12px, 12px); - buttonHeight: 44px; + buttonPadding: margins(10px, 9px, 10px, 9px); + buttonHeight: 42px; button: RoundButton(defaultActiveButton) { - height: 44px; + height: 42px; textTop: 12px; font: font(13px semibold); } } +filterInviteButtonStyle: TextStyle(defaultTextStyle) { + font: font(13px semibold); + linkFont: font(13px underline); + linkFontOver: font(13px underline); +} +filterInviteButtonBadgeStyle: TextStyle(defaultTextStyle) { + font: font(12px semibold); + linkFont: font(12px underline); + linkFontOver: font(12px underline); +} +filterInviteButtonBadgePadding: margins(5px, 0px, 5px, 2px); +filterInviteButtonBadgeSkip: 5px; +filterLinkTitlePadding: margins(0px, 15px, 0px, 17px); +filterLinkAboutTextStyle: TextStyle(defaultTextStyle) { + font: font(12px); + linkFont: font(12px underline); + linkFontOver: font(12px underline); + lineHeight: 17px; +} +filterLinkAbout: FlatLabel(defaultFlatLabel) { + style: filterLinkAboutTextStyle; + align: align(top); + minWidth: 190px; +} +filterLinkAboutTop: 170px; +filterLinkAboutBottom: 15px; +filterLinkPreview: 96px; +filterLinkPreviewRadius: 13px; +filterLinkPreviewTop: 30px; +filterLinkPreviewColumn: 65px; +filterLinkPreviewAllBottom: 18px; +filterLinkPreviewAllTop: 17px; +filterLinkPreviewMyBottom: 74px; +filterLinkPreviewMyTop: 73px; +filterLinkPreviewChatSize: 36px; +filterLinkPreviewChatSkip: 10px; +filterLinkPreviewBadgeLeft: 40px; +filterLinkPreviewBadgeTop: 38px; +filterLinkSubsectionTitlePadding: margins(0px, 5px, 0px, -4px); +filterLinkChatsList: PeerList(peerListBox) { + padding: margins(0px, 0px, 0px, membersMarginBottom); +} diff --git a/Telegram/SourceFiles/ui/controls/filter_link_header.cpp b/Telegram/SourceFiles/ui/controls/filter_link_header.cpp new file mode 100644 index 000000000..f35ad1a61 --- /dev/null +++ b/Telegram/SourceFiles/ui/controls/filter_link_header.cpp @@ -0,0 +1,446 @@ +/* +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 "ui/controls/filter_link_header.h" + +#include "lang/lang_keys.h" +#include "ui/painter.h" +#include "ui/rp_widget.h" +#include "ui/image/image_prepare.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/labels.h" +#include "styles/style_filter_icons.h" +#include "styles/style_layers.h" +#include "styles/style_settings.h" +#include "styles/style_window.h" + +namespace Ui { +namespace { + +constexpr auto kBodyAnimationPart = 0.90; +constexpr auto kTitleAdditionalScale = 0.05; + +class Widget final : public RpWidget { +public: + Widget( + not_null parent, + FilterLinkHeaderDescriptor &&descriptor); + + void setTitlePosition(int x, int y); + void updateDimensions(int newWidth); + + [[nodiscard]] rpl::producer> wheelEvents() const; + +private: + void resizeEvent(QResizeEvent *e) override; + void paintEvent(QPaintEvent *e) override; + void wheelEvent(QWheelEvent *e) override; + + [[nodiscard]] QRectF previewRect( + float64 topProgress, + float64 sizeProgress) const; + void refreshTitleText(); + + const not_null _about; + QMargins _aboutPadding; + + struct { + float64 top = 0.; + float64 body = 0.; + float64 title = 0.; + float64 scaleTitle = 0.; + } _progress; + + rpl::variable _badge; + QImage _preview; + QRectF _previewRect; + + QString _titleText; + style::font _titleFont; + QMargins _titlePadding; + QPoint _titlePosition; + QPainterPath _titlePath; + + QString _folderTitle; + not_null _folderIcon; + + int _maxHeight = 0; + + rpl::event_stream> _wheelEvents; + +}; + +[[nodiscard]] QImage GeneratePreview( + const QString &title, + not_null icon, + int badge) { + const auto size = st::filterLinkPreview; + const auto ratio = style::DevicePixelRatio(); + const auto radius = st::filterLinkPreviewRadius; + const auto full = QSize(size, size) * ratio; + auto result = QImage(full, QImage::Format_ARGB32_Premultiplied); + result.setDevicePixelRatio(ratio); + result.fill(st::windowBg->c); + + auto p = QPainter(&result); + + const auto column = st::filterLinkPreviewColumn; + p.fillRect(0, 0, column, size, st::sideBarBg); + p.fillRect(column, 0, size - column, size, st::emojiPanCategories); + + const auto &st = st::windowFiltersButton; + const auto skip = st.style.font->spacew; + const auto available = column - 2 * skip; + const auto iconWidth = st::foldersAll.width(); + const auto iconHeight = st::foldersAll.height(); + const auto iconLeft = (column - iconWidth) / 2; + const auto allIconTop = st::filterLinkPreviewAllBottom - iconHeight; + st::foldersAll.paint(p, iconLeft, allIconTop, size); + const auto myIconTop = st::filterLinkPreviewMyBottom - iconHeight; + icon->paint(p, iconLeft, myIconTop, size); + + const auto paintName = [&](const QString &text, int top) { + const auto &font = st.style.font; + p.drawText( + QRect(0, top, column, font->height), + font->elided(text, available), + style::al_top); + }; + p.setFont(st.style.font); + p.setPen(st.textFg); + paintName(tr::lng_filters_all(tr::now), st::filterLinkPreviewAllTop); + p.setPen(st.textFgActive); + paintName(title, st::filterLinkPreviewMyTop); + + auto hq = PainterHighQualityEnabler(p); + + const auto chatSize = st::filterLinkPreviewChatSize; + const auto chatLeft = size + st::lineWidth - (chatSize / 2); + const auto paintChat = [&](int top, const style::color &bg) { + p.setBrush(bg); + p.drawEllipse(chatLeft, top, chatSize, chatSize); + }; + const auto chatSkip = st::filterLinkPreviewChatSkip; + const auto chat1Top = (size - 2 * chatSize - chatSkip) / 2; + const auto chat2Top = size - chat1Top - chatSize; + p.setPen(Qt::NoPen); + paintChat(chat1Top, st::historyPeer4UserpicBg); + paintChat(chat2Top, st::historyPeer8UserpicBg); + + if (badge > 0) { + const auto font = st.badgeStyle.font; + const auto badgeHeight = st.badgeHeight; + const auto countBadgeWidth = [&](const QString &text) { + return std::max( + font->width(text) + 2 * st.badgeSkip, + badgeHeight); + }; + const auto defaultBadgeWidth = countBadgeWidth(u"+3"_q); + const auto badgeText = '+' + QString::number(badge); + const auto badgeWidth = countBadgeWidth(badgeText); + const auto defaultBadgeLeft = st::filterLinkPreviewBadgeLeft; + const auto badgeLeft = defaultBadgeLeft + + (defaultBadgeWidth - badgeWidth) / 2; + const auto badgeTop = st::filterLinkPreviewBadgeTop; + + const auto add = st::lineWidth; + auto pen = st.textBg->p; + pen.setWidthF(add * 2.); + p.setPen(pen); + p.setBrush(st.badgeBg); + const auto radius = (badgeHeight / 2) + add; + const auto rect = QRect(badgeLeft, badgeTop, badgeWidth, badgeHeight) + + QMargins(add, add, add, add); + p.drawRoundedRect(rect, radius, radius); + + p.setPen(st.badgeFg); + p.setFont(st.badgeStyle.font); + p.drawText(rect, badgeText, style::al_center); + } + + auto pen = st::shadowFg->p; + pen.setWidthF(st::lineWidth * 2.); + p.setPen(pen); + p.setBrush(Qt::NoBrush); + p.drawRoundedRect(0, 0, size, size, radius, radius); + p.end(); + + return Images::Round(std::move(result), Images::CornersMask(radius)); +} + +Widget::Widget( + not_null parent, + FilterLinkHeaderDescriptor &&descriptor) +: RpWidget(parent) +, _about(CreateChild( + this, + rpl::single(descriptor.about.value()), + st::filterLinkAbout)) +, _aboutPadding(st::boxRowPadding) +, _badge(std::move(descriptor.badge)) +, _titleText(descriptor.title) +, _titleFont(st::boxTitle.style.font) +, _titlePadding(st::filterLinkTitlePadding) +, _folderTitle(descriptor.folderTitle) +, _folderIcon(descriptor.folderIcon) { + setMinimumHeight(st::boxTitleHeight); + refreshTitleText(); + setTitlePosition(st::boxTitlePosition.x(), st::boxTitlePosition.y()); + + style::PaletteChanged( + ) | rpl::start_with_next([=] { + _preview = QImage(); + }, lifetime()); + + _badge.changes() | rpl::start_with_next([=] { + _preview = QImage(); + update(); + }, lifetime()); +} + +void Widget::refreshTitleText() { + _titlePath = QPainterPath(); + _titlePath.addText(0, _titleFont->ascent, _titleFont, _titleText); + update(); +} + +void Widget::setTitlePosition(int x, int y) { + _titlePosition = { x, y }; +} + +rpl::producer> Widget::wheelEvents() const { + return _wheelEvents.events(); +} + +void Widget::resizeEvent(QResizeEvent *e) { + const auto &padding = _aboutPadding; + const auto availableWidth = width() - padding.left() - padding.right(); + if (availableWidth <= 0) { + return; + } + _about->resizeToWidth(availableWidth); + + const auto minHeight = minimumHeight(); + const auto maxHeight = st::filterLinkAboutTop + + _about->height() + + st::filterLinkAboutBottom; + if (maxHeight <= minHeight) { + return; + } else if (_maxHeight != maxHeight) { + _maxHeight = maxHeight; + setMaximumHeight(maxHeight); + } + + const auto progress = (height() - minHeight) + / float64(_maxHeight - minHeight); + _progress.top = 1. - + std::clamp( + (1. - progress) / kBodyAnimationPart, + 0., + 1.); + _progress.body = _progress.top; + _progress.title = 1. - progress; + _progress.scaleTitle = 1. + kTitleAdditionalScale * progress; + + _previewRect = previewRect(_progress.top, _progress.body); + + const auto titleTop = _previewRect.top() + + _previewRect.height() + + _titlePadding.top(); + const auto titlePathRect = _titlePath.boundingRect(); + const auto aboutTop = titleTop + + titlePathRect.height() + + _titlePadding.bottom(); + _about->moveToLeft(_aboutPadding.left(), aboutTop); + _about->setOpacity(_progress.body); + + update(); +} + +QRectF Widget::previewRect( + float64 topProgress, + float64 sizeProgress) const { + const auto size = st::filterLinkPreview; + return QRectF( + (width() - size) / 2., + st::filterLinkPreviewTop * topProgress, + size, + size); +}; + +void Widget::paintEvent(QPaintEvent *e) { + auto p = QPainter(this); + + p.setOpacity(_progress.body); + p.translate(_previewRect.center()); + p.scale(_progress.body, _progress.body); + p.translate(-_previewRect.center()); + if (_progress.top) { + auto hq = PainterHighQualityEnabler(p); + if (_preview.isNull()) { + _preview = GeneratePreview( + _folderTitle, + _folderIcon, + _badge.current()); + } + p.drawImage(_previewRect, _preview); + } + p.resetTransform(); + + const auto titlePathRect = _titlePath.boundingRect(); + + // Title. + PainterHighQualityEnabler hq(p); + p.setOpacity(1.); + p.setFont(_titleFont); + p.setPen(st::boxTitleFg); + const auto fullPreviewRect = previewRect(1., 1.); + const auto fullTitleTop = fullPreviewRect.top() + + fullPreviewRect.height() + + _titlePadding.top(); + p.translate( + anim::interpolate( + (width() - titlePathRect.width()) / 2, + _titlePosition.x(), + _progress.title), + anim::interpolate(fullTitleTop, _titlePosition.y(), _progress.title)); + + p.translate(titlePathRect.center()); + p.scale(_progress.scaleTitle, _progress.scaleTitle); + p.translate(-titlePathRect.center()); + p.fillPath(_titlePath, st::boxTitleFg); +} + +void Widget::wheelEvent(QWheelEvent *e) { + _wheelEvents.fire(e); +} + +} // namespace + +[[nodiscard]] FilterLinkHeader MakeFilterLinkHeader( + not_null parent, + FilterLinkHeaderDescriptor &&descriptor) { + const auto result = CreateChild( + parent.get(), + std::move(descriptor)); + return { .widget = result, .wheelEvents = result->wheelEvents() }; +} + +object_ptr FilterLinkProcessButton( + not_null parent, + FilterLinkHeaderType type, + const QString &title, + rpl::producer badge) { + const auto st = &st::filterInviteBox.button; + const auto badgeSt = &st::filterInviteButtonBadgeStyle; + auto result = object_ptr(parent, rpl::single(u""_q), *st); + + struct Data { + QString text; + QString badge; + }; + auto data = std::move( + badge + ) | rpl::map([=](int count) { + const auto badge = count ? QString::number(count) : QString(); + const auto with = [&](QString badge) { + return rpl::map([=](QString text) { + return Data{ text, badge }; + }); + }; + switch (type) { + case FilterLinkHeaderType::AddingFilter: + return badge.isEmpty() + ? tr::lng_filters_by_link_add_no() | with(QString()) + : tr::lng_filters_by_link_add_button( + lt_folder, + rpl::single(title) + ) | with(badge); + case FilterLinkHeaderType::AddingChats: + return badge.isEmpty() + ? tr::lng_filters_by_link_join_no() | with(QString()) + : tr::lng_filters_by_link_join_button() | with(badge); + case FilterLinkHeaderType::AllAdded: + return tr::lng_box_ok() | with(QString()); + case FilterLinkHeaderType::Removing: + return badge.isEmpty() + ? tr::lng_filters_by_link_remove_button() | with(QString()) + : tr::lng_filters_by_link_quit_button() | with(badge); + } + Unexpected("Type in FilterLinkProcessButton."); + }) | rpl::flatten_latest(); + + struct Label : RpWidget { + using RpWidget::RpWidget; + + Text::String text; + Text::String badge; + }; + const auto label = result->lifetime().make_state