diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml
index 5ded5a0400b18..6ac646b3c6ede 100644
--- a/.github/workflows/linux.yml
+++ b/.github/workflows/linux.yml
@@ -94,6 +94,7 @@ jobs:
-D CMAKE_CXX_FLAGS="-Werror" \
-D CMAKE_EXE_LINKER_FLAGS="-s" \
-D TDESKTOP_API_TEST=ON \
+ -D DESKTOP_APP_DISABLE_AUTOUPDATE=OFF \
-D DESKTOP_APP_DISABLE_CRASH_REPORTS=OFF \
$DEFINE
@@ -115,7 +116,7 @@ jobs:
run: |
cd $REPO_NAME/out/Debug
mkdir artifact
- mv Telegram artifact/
+ mv {Telegram,Updater} artifact/
- uses: actions/upload-artifact@master
if: env.UPLOAD_ARTIFACT == 'true'
name: Upload artifact.
diff --git a/.github/workflows/mac.yml b/.github/workflows/mac.yml
index 96bfe6e1322f0..2a570aef7fa69 100644
--- a/.github/workflows/mac.yml
+++ b/.github/workflows/mac.yml
@@ -115,6 +115,7 @@ jobs:
-D CMAKE_CXX_FLAGS="-Werror" \
-D CMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED=NO \
-D TDESKTOP_API_TEST=ON \
+ -D DESKTOP_APP_DISABLE_AUTOUPDATE=OFF \
-D DESKTOP_APP_DISABLE_CRASH_REPORTS=OFF \
$DEFINE
diff --git a/.github/workflows/win.yml b/.github/workflows/win.yml
index 53b517939548b..b42906c7cbe50 100644
--- a/.github/workflows/win.yml
+++ b/.github/workflows/win.yml
@@ -169,6 +169,7 @@ jobs:
%TDESKTOP_BUILD_GENERATOR% ^
%TDESKTOP_BUILD_ARCH% ^
%TDESKTOP_BUILD_API% ^
+ -D DESKTOP_APP_DISABLE_AUTOUPDATE=OFF ^
-D DESKTOP_APP_DISABLE_CRASH_REPORTS=OFF ^
-D DESKTOP_APP_NO_PDB=ON ^
%TDESKTOP_BUILD_DEFINE%
@@ -178,8 +179,10 @@ jobs:
- name: Move artifact.
if: (env.UPLOAD_ARTIFACT == 'true') || (github.ref == 'refs/heads/nightly')
run: |
+ set OUT=%TBUILD%\%REPO_NAME%\out\Debug
mkdir artifact
- move %TBUILD%\%REPO_NAME%\out\Debug\Telegram.exe artifact/
+ move %OUT%\Telegram.exe artifact/
+ move %OUT%\Updater.exe artifact/
- uses: actions/upload-artifact@master
name: Upload artifact.
if: (env.UPLOAD_ARTIFACT == 'true') || (github.ref == 'refs/heads/nightly')
diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt
index acf322fc23396..4f9db79398dba 100644
--- a/Telegram/CMakeLists.txt
+++ b/Telegram/CMakeLists.txt
@@ -550,6 +550,10 @@ PRIVATE
data/data_replies_list.h
data/data_reply_preview.cpp
data/data_reply_preview.h
+ data/data_saved_messages.cpp
+ data/data_saved_messages.h
+ data/data_saved_sublist.cpp
+ data/data_saved_sublist.h
data/data_search_controller.cpp
data/data_search_controller.h
data/data_send_action.cpp
@@ -796,6 +800,8 @@ PRIVATE
history/view/history_view_sponsored_click_handler.h
history/view/history_view_sticker_toast.cpp
history/view/history_view_sticker_toast.h
+ history/view/history_view_sublist_section.cpp
+ history/view/history_view_sublist_section.h
history/view/history_view_transcribe_button.cpp
history/view/history_view_transcribe_button.h
history/view/history_view_translate_bar.cpp
@@ -897,6 +903,8 @@ PRIVATE
info/profile/info_profile_values.h
info/profile/info_profile_widget.cpp
info/profile/info_profile_widget.h
+ info/saved/info_saved_sublists_widget.cpp
+ info/saved/info_saved_sublists_widget.h
info/settings/info_settings_widget.cpp
info/settings/info_settings_widget.h
info/similar_channels/info_similar_channels_widget.cpp
diff --git a/Telegram/Resources/animations/voice_ttl_idle.tgs b/Telegram/Resources/animations/voice_ttl_idle.tgs
new file mode 100644
index 0000000000000..01661129813aa
Binary files /dev/null and b/Telegram/Resources/animations/voice_ttl_idle.tgs differ
diff --git a/Telegram/Resources/animations/voice_ttl_start.tgs b/Telegram/Resources/animations/voice_ttl_start.tgs
new file mode 100644
index 0000000000000..835556dd0b363
Binary files /dev/null and b/Telegram/Resources/animations/voice_ttl_start.tgs differ
diff --git a/Telegram/Resources/art/winners.tgs b/Telegram/Resources/art/winners.tgs
new file mode 100644
index 0000000000000..d1112ecbe732e
Binary files /dev/null and b/Telegram/Resources/art/winners.tgs differ
diff --git a/Telegram/Resources/icons/hidden_author_userpic.png b/Telegram/Resources/icons/hidden_author_userpic.png
new file mode 100644
index 0000000000000..37acae5ab95ae
Binary files /dev/null and b/Telegram/Resources/icons/hidden_author_userpic.png differ
diff --git a/Telegram/Resources/icons/hidden_author_userpic@2x.png b/Telegram/Resources/icons/hidden_author_userpic@2x.png
new file mode 100644
index 0000000000000..31c29045e918b
Binary files /dev/null and b/Telegram/Resources/icons/hidden_author_userpic@2x.png differ
diff --git a/Telegram/Resources/icons/hidden_author_userpic@3x.png b/Telegram/Resources/icons/hidden_author_userpic@3x.png
new file mode 100644
index 0000000000000..b60d52b54c1c4
Binary files /dev/null and b/Telegram/Resources/icons/hidden_author_userpic@3x.png differ
diff --git a/Telegram/Resources/icons/info/info_media_saved.png b/Telegram/Resources/icons/info/info_media_saved.png
new file mode 100644
index 0000000000000..0b3ebc5dde0a7
Binary files /dev/null and b/Telegram/Resources/icons/info/info_media_saved.png differ
diff --git a/Telegram/Resources/icons/info/info_media_saved@2x.png b/Telegram/Resources/icons/info/info_media_saved@2x.png
new file mode 100644
index 0000000000000..aa91e6fb81eb6
Binary files /dev/null and b/Telegram/Resources/icons/info/info_media_saved@2x.png differ
diff --git a/Telegram/Resources/icons/info/info_media_saved@3x.png b/Telegram/Resources/icons/info/info_media_saved@3x.png
new file mode 100644
index 0000000000000..6c9814fe12eae
Binary files /dev/null and b/Telegram/Resources/icons/info/info_media_saved@3x.png differ
diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index c7160a2a39521..acebc453088b5 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -395,6 +395,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_dlg_new_bot_name" = "Bot name";
"lng_no_chats" = "Your chats will be here";
"lng_no_chats_filter" = "No chats currently belong to this folder.";
+"lng_no_saved_sublists" = "You can save messages from other chats here.";
"lng_contacts_loading" = "Loading...";
"lng_contacts_not_found" = "No contacts found";
"lng_topics_not_found" = "No topics found.";
@@ -591,6 +592,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_settings_section_background" = "Chat background";
"lng_settings_bg_from_gallery" = "Choose from gallery";
"lng_settings_bg_from_file" = "Choose from file";
+"lng_settings_bg_remove" = "Remove wallpaper";
"lng_settings_bg_theme_edit" = "Edit theme";
"lng_settings_bg_theme_create" = "Create new theme";
"lng_settings_bg_cloud_themes" = "Custom themes";
@@ -813,6 +815,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_settings_manage_enabled_dictionary" = "Dictionary is enabled";
"lng_settings_manage_remove_dictionary" = "Remove Dictionary";
+"lng_settings_gift_premium" = "Premium Gifting";
+"lng_settings_gift_premium_users_confirm" = "Proceed";
+"lng_settings_gift_premium_users_error#one" = "You can select maximum {count} user.";
+"lng_settings_gift_premium_users_error#other" = "You can select maximum {count} users.";
+
"lng_backgrounds_header" = "Choose Wallpaper";
"lng_theme_sure_keep" = "Keep this theme?";
"lng_theme_reverting#one" = "Reverting to the old theme in {count} second.";
@@ -833,6 +840,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_background_blur" = "Blurred";
"lng_background_sure_delete" = "Are you sure you want to delete this background?";
"lng_background_other_info" = "{user} will be able to apply this wallpaper";
+"lng_background_other_channel" = "All subscribers will see this wallpaper";
"lng_background_apply1" = "Apply the wallpaper in this chat.";
"lng_background_apply2" = "Enjoy the view.";
"lng_background_apply_button" = "Apply For This Chat";
@@ -841,6 +849,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_background_reset_default" = "Reset";
"lng_background_apply_me" = "Apply for me";
"lng_background_apply_both" = "Apply for me and {user}";
+"lng_background_apply_channel" = "Apply For Channel";
"lng_download_path_ask" = "Ask download path for each file";
"lng_download_path" = "Download path";
@@ -1179,6 +1188,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_profile_common_groups#other" = "{count} groups in common";
"lng_profile_similar_channels#one" = "{count} similar channel";
"lng_profile_similar_channels#other" = "{count} similar channels";
+"lng_profile_saved_messages#one" = "{count} saved message";
+"lng_profile_saved_messages#other" = "{count} saved messages";
"lng_profile_participants_section" = "Members";
"lng_profile_subscribers_section" = "Subscribers";
"lng_profile_add_contact" = "Add Contact";
@@ -1682,7 +1693,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_action_giveaway_started" = "{from} just started a giveaway of Telegram Premium subscriptions to its followers.";
"lng_action_giveaway_results#one" = "{count} winner of the giveaway was randomly selected by Telegram and received private messages with giftcodes.";
"lng_action_giveaway_results#other" = "{count} winners of the giveaway were randomly selected by Telegram and received private messages with giftcodes.";
-"lng_action_giveaway_results_some" = "Some winners of the giveaway was randomly selected by Telegram and received private messages with giftcodes.";
+"lng_action_giveaway_results_some" = "Some winners of the giveaway were randomly selected by Telegram and received private messages with giftcodes.";
"lng_action_giveaway_results_none" = "No winners of the giveaway could be selected.";
"lng_similar_channels_title" = "Similar channels";
@@ -1704,6 +1715,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_ttl_video_received" = "{from} sent you a self-destructing video. Please view it on your mobile.";
"lng_ttl_video_sent" = "You sent a self-destructing video.";
"lng_ttl_video_expired" = "Video has expired";
+"lng_ttl_voice_sent" = "You sent a self-destructing voice messsage.";
+"lng_ttl_voice_expired" = "Voice message expired";
+"lng_ttl_round_sent" = "You sent a self-destructing video message.";
+"lng_ttl_round_expired" = "Round message expired";
"lng_profile_add_more_after_create" = "You will be able to add more members after you create the group.";
"lng_profile_camera_title" = "Capture yourself";
@@ -1836,6 +1851,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_sponsored_message_title" = "Sponsored";
"lng_recommended_message_title" = "Recommended";
"lng_edited" = "edited";
+"lng_commented" = "commented";
"lng_edited_date" = "Edited: {date}";
"lng_sent_date" = "Sent: {date}";
"lng_views_tooltip#one" = "Views: {count}";
@@ -2055,6 +2071,22 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_premium_gift_per" = "{cost} / month";
"lng_premium_gift_terms" = "You can review the list of features and terms of use for Telegram Premium {link}.";
"lng_premium_gift_terms_link" = "here";
+"lng_premium_gifts_about_user1" = "Give **{user}** access to exclusive features.";
+"lng_premium_gifts_about_user2" = "Give **{user}** and **{second_user}** access to exclusive features.";
+"lng_premium_gifts_about_user3" = "Give **{user}**, **{second_user}** and **{name}** access to exclusive features.";
+"lng_premium_gifts_about_user_more#one" = "Give **{user}**, **{second_user}**, **{name}** and **{count}** more friend access to exclusive features.";
+"lng_premium_gifts_about_user_more#other" = "Give **{user}**, **{second_user}**, **{name}** and **{count}** more friends access to exclusive features.";
+"lng_premium_gifts_about_reward#one" = "You will receive {emoji}**{count}** boost.";
+"lng_premium_gifts_about_reward#other" = "You will receive {emoji}**{count}** boosts.";
+"lng_premium_gifts_about_paid_title" = "Gifts Sent!";
+"lng_premium_gifts_about_paid1" = "**{user}** has been notified about the gifts you purchased.";
+"lng_premium_gifts_about_paid2" = "**{user}** and **{second_user}** have been notified about the gifts you purchased.";
+"lng_premium_gifts_about_paid3" = "**{user}**, **{second_user}** and **{name}** have been notified about the gifts you purchased.";
+"lng_premium_gifts_about_paid_more#one" = "**{user}**, **{second_user}**, **{name}** and **{count}** other have been notified about the gifts you purchased.";
+"lng_premium_gifts_about_paid_more#other" = "**{user}**, **{second_user}**, **{name}** and **{count}** others have been notified about the gifts you purchased.";
+"lng_premium_gifts_about_paid_below#one" = "They now have access to additional features.";
+"lng_premium_gifts_about_paid_below#other" = "They now have access to additional features.";
+"lng_premium_gifts_summary_subtitle" = "What's Included";
"lng_boost_channel_button" = "Boost Channel";
"lng_boost_again_button" = "Boost Again";
@@ -2111,6 +2143,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_boost_channel_needs_level_color#one" = "Your channel needs to reach **Level {count}** to change channel color.";
"lng_boost_channel_needs_level_color#other" = "Your channel needs to reach **Level {count}** to change channel color.";
+"lng_boost_channel_title_wallpaper" = "Enable wallpapers";
+"lng_boost_channel_needs_level_wallpaper#one" = "Your channel needs to reach **Level {count}** to change channel wallpaper.";
+"lng_boost_channel_needs_level_wallpaper#other" = "Your channel needs to reach **Level {count}** to change channel wallpaper.";
+
+"lng_boost_channel_title_status" = "Enable emoji status";
+"lng_boost_channel_needs_level_status#one" = "Your channel needs to reach **Level {count}** to set emoji status.";
+"lng_boost_channel_needs_level_status#other" = "Your channel needs to reach **Level {count}** to set emoji status.";
+
"lng_boost_channel_title_reactions" = "Custom reactions";
"lng_boost_channel_needs_level_reactions#one" = "Your channel needs to reach **Level {count}** to add **{same_count}** custom emoji as a reaction.";
"lng_boost_channel_needs_level_reactions#other" = "Your channel needs to reach **Level {count}** to add **{same_count}** custom emoji as reactions.";
@@ -2170,6 +2210,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_giveaway_maximum_users_error#other" = "You can select maximum {count} users.";
"lng_giveaway_channels_confirm_title" = "Channel is Private";
"lng_giveaway_channels_confirm_about" = "Are you sure you want to add a private channel? Users won't be able to join it without an invite link.";
+"lng_giveaway_additional_prizes" = "Additional prizes";
+"lng_giveaway_additional_about" = "Turn this on if you want to give the winners your own prizes in addition to Premium subscriptions.";
+"lng_giveaway_additional_prizes_ph" = "Enter your prize";
+"lng_giveaway_prizes_just_premium#one" = "All prizes: **{count}** Telegram Premium subscription {duration}.";
+"lng_giveaway_prizes_just_premium#other" = "All prizes: **{count}** Telegram Premium subscriptions {duration}.";
+"lng_giveaway_prizes_additional#one" = "All prizes: **{count}** {prize} with Telegram Premium subscription {duration}.";
+"lng_giveaway_prizes_additional#other" = "All prizes: **{count}** {prize} with Telegram Premium subscriptions {duration}.";
+"lng_giveaway_show_winners" = "Show winners";
+"lng_giveaway_show_winners_about" = "Choose whether to make the list of winners public when the giveaway ends.";
"lng_giveaway_created_title" = "Giveaway created";
"lng_giveaway_created_body" = "Check your channels' {link} to see how this giveaway boosted your channel.";
@@ -2189,6 +2238,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_prizes_title#one" = "Giveaway Prize";
"lng_prizes_title#other" = "Giveaway Prizes";
+"lng_prizes_additional#one" = "**{count}** {prize}";
+"lng_prizes_additional#other" = "**{count}** {prize}";
+"lng_prizes_additional_with" = "with";
"lng_prizes_about#one" = "**{count}** Telegram Premium Subscription {duration}.";
"lng_prizes_about#other" = "**{count}** Telegram Premium Subscriptions {duration}.";
"lng_prizes_participants" = "Participants";
@@ -2207,6 +2259,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_prizes_end_text" = "This giveaway was sponsored by {admins}.";
"lng_prizes_admins#one" = "the admins of {channel}, who aquired **{count} Telegram Premium** subscription {duration} for its followers";
"lng_prizes_admins#other" = "the admins of {channel}, who aquired **{count} Telegram Premium** subscriptions {duration} for its followers.";
+"lng_prizes_additional_added#one" = "{channel} also included **{count} {prize}** in the prize. Admins of the channel are responsible for delivering this prize.";
+"lng_prizes_additional_added#other" = "{channel} also included **{count} {prize}** in the prizes. Admins of the channel are responsible for delivering these prizes.";
"lng_prizes_how_when_finish" = "On {date}, Telegram will automatically select {winners}.";
"lng_prizes_end_when_finish" = "On {date}, Telegram automatically selected {winners}.";
"lng_prizes_end_activated#one" = "**{count}** of the winners already used their gift link.";
@@ -2232,6 +2286,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_prizes_cancelled" = "The channel cancelled the prizes by reversing the payment for them.";
"lng_prizes_badge" = "x{amount}";
+"lng_prizes_results_title" = "Winners Selected!";
+"lng_prizes_results_about#one" = "**{count}** winner of the {link} was randomly selected by Telegram.";
+"lng_prizes_results_about#other" = "**{count}** winners of the {link} were randomly selected by Telegram.";
+"lng_prizes_results_link" = "Giveaway";
+"lng_prizes_results_winners" = "Winners";
+"lng_prizes_results_more#one" = "and {count} more!";
+"lng_prizes_results_more#other" = "and {count} more!";
+"lng_prizes_results_all" = "All winners received gift links in private messages.";
+"lng_prizes_results_some" = "Some winners couldn't be selected.";
+
"lng_gift_link_title" = "Gift Link";
"lng_gift_link_about" = "This link allows you to activate\na **Telegram Premium** subscription.";
"lng_gift_link_label_from" = "From";
@@ -2435,6 +2499,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_saved_short" = "Save";
"lng_saved_forward_here" = "Forward messages here for quick access";
"lng_saved_quote_here" = "Quote here to save";
+"lng_saved_open_chat" = "Open Chat";
+"lng_saved_open_channel" = "Open Channel";
+"lng_saved_open_group" = "Open Group";
+"lng_saved_about_hidden" = "Senders of this messages restricted to link their name when forwarding.";
"lng_scheduled_messages" = "Scheduled Messages";
"lng_scheduled_messages_empty" = "No scheduled messages here yet...";
@@ -2461,6 +2529,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_comments_open_none" = "Leave a comment";
"lng_replies_view_original" = "View in chat";
"lng_replies_messages" = "Replies";
+"lng_hidden_author_messages" = "Author Hidden";
"lng_replies_discussion_started" = "Discussion started";
"lng_replies_no_comments" = "No comments here yet...";
@@ -2801,6 +2870,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_edit_sign_messages" = "Sign messages";
"lng_edit_group" = "Edit group";
"lng_edit_channel_color" = "Change name color";
+"lng_edit_channel_level_min" = "Level 1+";
+"lng_edit_channel_wallpaper" = "Channel wallpaper";
+"lng_edit_channel_wallpaper_about" = "Set a wallpaper that will be visible for everyone reading your channel.";
+"lng_edit_channel_status" = "Channel emoji status";
+"lng_edit_channel_status_about" = "Choose a status that will be shown next to the channel's name.";
"lng_edit_self_title" = "Edit your name";
"lng_confirm_contact_data" = "New Contact";
"lng_add_contact" = "Create";
@@ -3615,6 +3689,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_admin_log_set_background_emoji" = "{from} set channel background emoji to {emoji}";
"lng_admin_log_change_background_emoji" = "{from} changed channel background emoji from {previous} to {emoji}";
"lng_admin_log_removed_background_emoji" = "{from} removed channel background emoji {emoji}";
+"lng_admin_log_change_profile_color" = "{from} changed channel profile color from {previous} to {color}";
+"lng_admin_log_set_profile_background_emoji" = "{from} set channel profile background emoji to {emoji}";
+"lng_admin_log_change_profile_background_emoji" = "{from} changed channel profile background emoji from {previous} to {emoji}";
+"lng_admin_log_removed_profile_background_emoji" = "{from} removed channel profile background emoji {emoji}";
+"lng_admin_log_change_wallpaper" = "{from} changed channel wallpaper";
+"lng_admin_log_set_status" = "{from} set channel emoji status to {emoji}";
+"lng_admin_log_change_status" = "{from} changed channel emoji status from {previous} to {emoji}";
+"lng_admin_log_removed_status" = "{from} removed channel emoji status {emoji}";
+"lng_admin_log_set_status_until" = "{from} set channel emoji status to {emoji} until {date}";
+"lng_admin_log_change_status_until" = "{from} changed channel emoji status from {previous} to {emoji} until {date}";
"lng_admin_log_user_with_username" = "{name} ({mention})";
"lng_admin_log_messages_ttl_set" = "{from} enabled messages auto-delete after {duration}";
"lng_admin_log_messages_ttl_changed" = "{from} changed messages auto-delete period from {previous} to {duration}";
@@ -4207,6 +4291,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_request_peer_confirm_rights" = "This will also add {bot} to {chat} with the following rights: {rights}.";
"lng_request_peer_confirm_send" = "Send";
"lng_request_user_title" = "Choose User";
+"lng_request_users_title" = "Choose Users";
"lng_request_user_premium_yes" = "The user should have a Premium subscription.";
"lng_request_user_premium_no" = "The user shouldn't have a Premium subscription.";
"lng_request_user_no" = "No such users";
diff --git a/Telegram/Resources/qrc/telegram/animations.qrc b/Telegram/Resources/qrc/telegram/animations.qrc
index 705b508a246af..76ea1ef4a3c29 100644
--- a/Telegram/Resources/qrc/telegram/animations.qrc
+++ b/Telegram/Resources/qrc/telegram/animations.qrc
@@ -11,5 +11,7 @@
../../animations/ttl.tgs
../../animations/discussion.tgs
../../animations/stats.tgs
+ ../../animations/voice_ttl_idle.tgs
+ ../../animations/voice_ttl_start.tgs
diff --git a/Telegram/Resources/qrc/telegram/telegram.qrc b/Telegram/Resources/qrc/telegram/telegram.qrc
index 0e43acb03d343..57afe388252d5 100644
--- a/Telegram/Resources/qrc/telegram/telegram.qrc
+++ b/Telegram/Resources/qrc/telegram/telegram.qrc
@@ -15,6 +15,7 @@
../../art/slot_2_idle.tgs
../../art/slot_back.tgs
../../art/slot_pull.tgs
+ ../../art/winners.tgs
../../day-blue.tdesktop-theme
../../night.tdesktop-theme
../../night-green.tdesktop-theme
diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml
index f983e858561a2..d8a1b5191a47d 100644
--- a/Telegram/Resources/uwp/AppX/AppxManifest.xml
+++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml
@@ -10,7 +10,7 @@
+ Version="4.14.0.0" />
Telegram Desktop
Telegram Messenger LLP
diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc
index b2f5c49e7fedb..4f8a48a2472b7 100644
--- a/Telegram/Resources/winrc/Telegram.rc
+++ b/Telegram/Resources/winrc/Telegram.rc
@@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico"
//
VS_VERSION_INFO VERSIONINFO
- FILEVERSION 4,12,2,0
- PRODUCTVERSION 4,12,2,0
+ FILEVERSION 4,14,0,0
+ PRODUCTVERSION 4,14,0,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
@@ -62,10 +62,10 @@ BEGIN
BEGIN
VALUE "CompanyName", "Telegram FZ-LLC"
VALUE "FileDescription", "Telegram Desktop"
- VALUE "FileVersion", "4.12.2.0"
+ VALUE "FileVersion", "4.14.0.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2023"
VALUE "ProductName", "Telegram Desktop"
- VALUE "ProductVersion", "4.12.2.0"
+ VALUE "ProductVersion", "4.14.0.0"
END
END
BLOCK "VarFileInfo"
diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc
index 95e556c9ab811..351bde377dcf4 100644
--- a/Telegram/Resources/winrc/Updater.rc
+++ b/Telegram/Resources/winrc/Updater.rc
@@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
//
VS_VERSION_INFO VERSIONINFO
- FILEVERSION 4,12,2,0
- PRODUCTVERSION 4,12,2,0
+ FILEVERSION 4,14,0,0
+ PRODUCTVERSION 4,14,0,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
@@ -53,10 +53,10 @@ BEGIN
BEGIN
VALUE "CompanyName", "Telegram FZ-LLC"
VALUE "FileDescription", "Telegram Desktop Updater"
- VALUE "FileVersion", "4.12.2.0"
+ VALUE "FileVersion", "4.14.0.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2023"
VALUE "ProductName", "Telegram Desktop"
- VALUE "ProductVersion", "4.12.2.0"
+ VALUE "ProductVersion", "4.14.0.0"
END
END
BLOCK "VarFileInfo"
diff --git a/Telegram/SourceFiles/_other/startup_task_win.cpp b/Telegram/SourceFiles/_other/startup_task_win.cpp
index 8e780848d6650..a54b941ee7aec 100644
--- a/Telegram/SourceFiles/_other/startup_task_win.cpp
+++ b/Telegram/SourceFiles/_other/startup_task_win.cpp
@@ -6,6 +6,7 @@ For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include
+#include
#include
#include
diff --git a/Telegram/SourceFiles/_other/updater_win.cpp b/Telegram/SourceFiles/_other/updater_win.cpp
index 1873815ce8a4f..0b9b247853279 100644
--- a/Telegram/SourceFiles/_other/updater_win.cpp
+++ b/Telegram/SourceFiles/_other/updater_win.cpp
@@ -537,11 +537,12 @@ HANDLE _generateDumpFileAtPath(const WCHAR *path) {
GetLocalTime(&stLocalTime);
- wsprintf(szFileName, L"%s%s-%s-%04d%02d%02d-%02d%02d%02d-%ld-%ld.dmp",
- szPath, szExeName, updaterVersionStr,
- stLocalTime.wYear, stLocalTime.wMonth, stLocalTime.wDay,
- stLocalTime.wHour, stLocalTime.wMinute, stLocalTime.wSecond,
- GetCurrentProcessId(), GetCurrentThreadId());
+ wsprintf(
+ szFileName, L"%s%s-%s-%04d%02d%02d-%02d%02d%02d-%ld-%ld.dmp",
+ szPath, szExeName, updaterVersionStr,
+ stLocalTime.wYear, stLocalTime.wMonth, stLocalTime.wDay,
+ stLocalTime.wHour, stLocalTime.wMinute, stLocalTime.wSecond,
+ GetCurrentProcessId(), GetCurrentThreadId());
return CreateFile(szFileName, GENERIC_READ|GENERIC_WRITE, FILE_SHARE_WRITE|FILE_SHARE_READ, 0, CREATE_ALWAYS, 0, 0);
}
@@ -562,7 +563,7 @@ void _generateDump(EXCEPTION_POINTERS* pExceptionPointers) {
DWORD len = GetModuleFileName(GetModuleHandle(0), szPath, maxFileLen);
if (!len) return;
- WCHAR *pathEnd = szPath + len;
+ WCHAR *pathEnd = szPath + len;
if (!_wcsicmp(pathEnd - wcslen(_exeName), _exeName)) {
wsprintf(pathEnd - wcslen(_exeName), L"");
diff --git a/Telegram/SourceFiles/api/api_bot.cpp b/Telegram/SourceFiles/api/api_bot.cpp
index 239d2d0ab89a4..d49f1bbf78472 100644
--- a/Telegram/SourceFiles/api/api_bot.cpp
+++ b/Telegram/SourceFiles/api/api_bot.cpp
@@ -415,12 +415,16 @@ void ActivateBotCommand(ClickHandlerContext context, int row, int column) {
const auto peer = item->history()->peer;
const auto itemId = item->id;
const auto id = int32(button->buttonId);
- const auto chosen = [=](not_null result) {
+ const auto chosen = [=](std::vector> result) {
peer->session().api().request(MTPmessages_SendBotRequestedPeer(
peer->input,
MTP_int(itemId),
MTP_int(id),
- result->input
+ MTP_vector_from_range(
+ result
+ | ranges::views::transform([](
+ not_null peer) {
+ return MTPInputPeer(peer->input); }))
)).done([=](const MTPUpdates &result) {
peer->session().api().applyUpdates(result);
}).send();
diff --git a/Telegram/SourceFiles/api/api_messages_search.cpp b/Telegram/SourceFiles/api/api_messages_search.cpp
index 09f9d6a2159ea..15119b9068ee1 100644
--- a/Telegram/SourceFiles/api/api_messages_search.cpp
+++ b/Telegram/SourceFiles/api/api_messages_search.cpp
@@ -90,6 +90,7 @@ void MessagesSearch::searchRequest() {
(_from
? _from->input
: MTP_inputPeerEmpty()),
+ MTPInputPeer(), // saved_peer_id
MTPint(), // top_msg_id
MTP_inputMessagesFilterEmpty(),
MTP_int(0), // min_date
diff --git a/Telegram/SourceFiles/api/api_peer_colors.cpp b/Telegram/SourceFiles/api/api_peer_colors.cpp
index 85d60d814c8d7..50c2382fdcc64 100644
--- a/Telegram/SourceFiles/api/api_peer_colors.cpp
+++ b/Telegram/SourceFiles/api/api_peer_colors.cpp
@@ -8,6 +8,7 @@ For license and copyright information please follow this link:
#include "api/api_peer_colors.h"
#include "apiwrap.h"
+#include "data/data_peer.h"
#include "ui/chat/chat_style.h"
namespace Api {
@@ -62,6 +63,16 @@ auto PeerColors::indicesValue() const
}));
}
+int PeerColors::requiredLevelFor(PeerId channel, uint8 index) const {
+ if (Data::DecideColorIndex(channel) == index) {
+ return 0;
+ } else if (const auto i = _requiredLevels.find(index)
+ ; i != end(_requiredLevels)) {
+ return i->second;
+ }
+ return 1;
+}
+
void PeerColors::apply(const MTPDhelp_peerColors &data) {
auto suggested = std::vector();
auto colors = std::make_shared<
@@ -89,6 +100,7 @@ void PeerColors::apply(const MTPDhelp_peerColors &data) {
};
const auto &list = data.vcolors().v;
+ _requiredLevels.clear();
suggested.reserve(list.size());
for (const auto &color : list) {
const auto &data = color.data();
@@ -98,6 +110,9 @@ void PeerColors::apply(const MTPDhelp_peerColors &data) {
continue;
}
const auto colorIndex = uint8(colorIndexBare);
+ if (const auto min = data.vchannel_min_level()) {
+ _requiredLevels[colorIndex] = min->v;
+ }
if (!data.is_hidden()) {
suggested.push_back(colorIndex);
}
diff --git a/Telegram/SourceFiles/api/api_peer_colors.h b/Telegram/SourceFiles/api/api_peer_colors.h
index de06d42211d6f..0ad1a63c7ea8e 100644
--- a/Telegram/SourceFiles/api/api_peer_colors.h
+++ b/Telegram/SourceFiles/api/api_peer_colors.h
@@ -28,6 +28,10 @@ class PeerColors final {
[[nodiscard]] auto indicesValue() const
-> rpl::producer;
+ [[nodiscard]] int requiredLevelFor(
+ PeerId channel,
+ uint8 index) const;
+
private:
void request();
void apply(const MTPDhelp_peerColors &data);
@@ -38,6 +42,7 @@ class PeerColors final {
mtpRequestId _requestId = 0;
base::Timer _timer;
rpl::variable> _suggested;
+ base::flat_map _requiredLevels;
rpl::event_stream<> _colorIndicesChanged;
std::unique_ptr _colorIndicesCurrent;
diff --git a/Telegram/SourceFiles/api/api_peer_photo.cpp b/Telegram/SourceFiles/api/api_peer_photo.cpp
index 25a7065873279..c70ef4491f82e 100644
--- a/Telegram/SourceFiles/api/api_peer_photo.cpp
+++ b/Telegram/SourceFiles/api/api_peer_photo.cpp
@@ -515,6 +515,7 @@ auto PeerPhoto::emojiList(EmojiListType type) -> EmojiListData & {
case EmojiListType::Profile: return _profileEmojiList;
case EmojiListType::Group: return _groupEmojiList;
case EmojiListType::Background: return _backgroundEmojiList;
+ case EmojiListType::NoChannelStatus: return _noChannelStatusEmojiList;
}
Unexpected("Type in PeerPhoto::emojiList.");
}
@@ -551,6 +552,8 @@ void PeerPhoto::requestEmojiList(EmojiListType type) {
? send(MTPaccount_GetDefaultProfilePhotoEmojis())
: (type == EmojiListType::Group)
? send(MTPaccount_GetDefaultGroupPhotoEmojis())
+ : (type == EmojiListType::NoChannelStatus)
+ ? send(MTPaccount_GetChannelRestrictedStatusEmojis())
: send(MTPaccount_GetDefaultBackgroundEmojis());
}
diff --git a/Telegram/SourceFiles/api/api_peer_photo.h b/Telegram/SourceFiles/api/api_peer_photo.h
index 1d4bd1071ee5e..71d340a031f6b 100644
--- a/Telegram/SourceFiles/api/api_peer_photo.h
+++ b/Telegram/SourceFiles/api/api_peer_photo.h
@@ -32,6 +32,7 @@ class PeerPhoto final {
Profile,
Group,
Background,
+ NoChannelStatus,
};
struct UserPhoto {
@@ -112,6 +113,7 @@ class PeerPhoto final {
EmojiListData _profileEmojiList;
EmojiListData _groupEmojiList;
EmojiListData _backgroundEmojiList;
+ EmojiListData _noChannelStatusEmojiList;
};
diff --git a/Telegram/SourceFiles/api/api_premium.cpp b/Telegram/SourceFiles/api/api_premium.cpp
index 58950f6f4b910..088c3cf876ac4 100644
--- a/Telegram/SourceFiles/api/api_premium.cpp
+++ b/Telegram/SourceFiles/api/api_premium.cpp
@@ -26,7 +26,7 @@ namespace {
[[nodiscard]] GiftCode Parse(const MTPDpayments_checkedGiftCode &data) {
return {
- .from = peerFromMTP(data.vfrom_id()),
+ .from = data.vfrom_id() ? peerFromMTP(*data.vfrom_id()) : PeerId(),
.to = data.vto_id() ? peerFromUser(*data.vto_id()) : PeerId(),
.giveawayId = data.vgiveaway_msg_id().value_or_empty(),
.date = data.vdate().v,
@@ -342,15 +342,12 @@ PremiumGiftCodeOptions::PremiumGiftCodeOptions(not_null peer)
rpl::producer PremiumGiftCodeOptions::request() {
return [=](auto consumer) {
auto lifetime = rpl::lifetime();
- const auto channel = _peer->asChannel();
- if (!channel) {
- return lifetime;
- }
using TLOption = MTPPremiumGiftCodeOption;
_api.request(MTPpayments_GetPremiumGiftCodeOptions(
- MTP_flags(
- MTPpayments_GetPremiumGiftCodeOptions::Flag::f_boost_peer),
+ MTP_flags(_peer->isChannel()
+ ? MTPpayments_GetPremiumGiftCodeOptions::Flag::f_boost_peer
+ : MTPpayments_GetPremiumGiftCodeOptions::Flag(0)),
_peer->input
)).done([=](const MTPVector &result) {
auto tlMapOptions = base::flat_map>();
@@ -420,6 +417,8 @@ const std::vector &PremiumGiftCodeOptions::availablePresets() const {
}
[[nodiscard]] int PremiumGiftCodeOptions::monthsFromPreset(int monthsIndex) {
+ Expects(monthsIndex >= 0 && monthsIndex < _availablePresets.size());
+
return _optionsForOnePerson.months[monthsIndex];
}
diff --git a/Telegram/SourceFiles/api/api_statistics.cpp b/Telegram/SourceFiles/api/api_statistics.cpp
index 6655c618ed56d..f24e156d0894b 100644
--- a/Telegram/SourceFiles/api/api_statistics.cpp
+++ b/Telegram/SourceFiles/api/api_statistics.cpp
@@ -10,6 +10,8 @@ For license and copyright information please follow this link:
#include "apiwrap.h"
#include "data/data_channel.h"
#include "data/data_session.h"
+#include "data/data_stories.h"
+#include "data/data_story.h"
#include "history/history.h"
#include "main/main_session.h"
#include "statistics/statistics_data_deserialize.h"
@@ -359,161 +361,54 @@ PublicForwards::PublicForwards(
void PublicForwards::request(
const Data::PublicForwardsSlice::OffsetToken &token,
Fn done) {
- if (!_requestId) {
- if (_fullId.messageId) {
- requestMessage(token, std::move(done));
- } else if (_fullId.storyId) {
- requestStory(token, std::move(done));
- }
+ if (_requestId) {
+ return;
}
-}
-
-void PublicForwards::requestMessage(
- const Data::PublicForwardsSlice::OffsetToken &token,
- Fn done) {
- Expects(_fullId.messageId);
-
- const auto offsetPeer = channel()->owner().peer(token.fullId.peer);
- const auto tlOffsetPeer = offsetPeer
- ? offsetPeer->input
- : MTP_inputPeerEmpty();
- constexpr auto kLimit = tl::make_int(100);
- _requestId = makeRequest(MTPstats_GetMessagePublicForwards(
- channel()->inputChannel,
- MTP_int(_fullId.messageId.msg),
- MTP_int(token.rate),
- tlOffsetPeer,
- MTP_int(token.fullId.msg),
- kLimit
- )).done([=, channel = channel()](const MTPmessages_Messages &result) {
+ const auto channel = StatisticsRequestSender::channel();
+ const auto processResult = [=](const MTPstats_PublicForwards &tl) {
using Messages = QVector;
_requestId = 0;
- auto nextToken = Data::PublicForwardsSlice::OffsetToken();
- const auto process = [&](const MTPVector &messages) {
- auto result = Messages();
- for (const auto &message : messages.v) {
- const auto msgId = IdFromMessage(message);
- const auto peerId = PeerFromMessage(message);
- const auto lastDate = DateFromMessage(message);
- if (const auto peer = channel->owner().peerLoaded(peerId)) {
- if (lastDate) {
- channel->owner().addNewMessage(
- message,
- MessageFlags(),
- NewMessageType::Existing);
- nextToken.fullId = { peerId, msgId };
- result.push_back({ .messageId = nextToken.fullId });
- }
- }
- }
- return result;
- };
+ const auto &data = tl.data();
+ auto &owner = channel->owner();
- auto allLoaded = false;
- auto fullCount = 0;
- auto messages = result.match([&](const MTPDmessages_messages &data) {
- channel->owner().processUsers(data.vusers());
- channel->owner().processChats(data.vchats());
- auto list = process(data.vmessages());
- allLoaded = true;
- fullCount = list.size();
- return list;
- }, [&](const MTPDmessages_messagesSlice &data) {
- channel->owner().processUsers(data.vusers());
- channel->owner().processChats(data.vchats());
- auto list = process(data.vmessages());
-
- if (const auto nextRate = data.vnext_rate()) {
- const auto rateUpdated = (nextRate->v != token.rate);
- if (rateUpdated) {
- nextToken.rate = nextRate->v;
- } else {
- allLoaded = true;
- }
- }
- fullCount = data.vcount().v;
- return list;
- }, [&](const MTPDmessages_channelMessages &data) {
- channel->owner().processUsers(data.vusers());
- channel->owner().processChats(data.vchats());
- auto list = process(data.vmessages());
- allLoaded = true;
- fullCount = data.vcount().v;
- return list;
- }, [&](const MTPDmessages_messagesNotModified &) {
- allLoaded = true;
- return Messages();
- });
+ owner.processUsers(data.vusers());
+ owner.processChats(data.vchats());
- _lastTotal = std::max(_lastTotal, fullCount);
- done({
- .list = std::move(messages),
- .total = _lastTotal,
- .allLoaded = allLoaded,
- .token = nextToken,
- });
- }).fail([=] {
- _requestId = 0;
- }).send();
-}
-
-void PublicForwards::requestStory(
- const Data::PublicForwardsSlice::OffsetToken &token,
- Fn done) {
- Expects(_fullId.storyId);
-
- constexpr auto kLimit = tl::make_int(100);
- _requestId = makeRequest(MTPstats_GetStoryPublicForwards(
- channel()->input,
- MTP_int(_fullId.storyId.story),
- MTP_string(token.storyOffset),
- kLimit
- )).done([=, channel = channel()](
- const MTPstats_PublicForwards &tlForwards) {
- using Messages = QVector;
- _requestId = 0;
-
- const auto &data = tlForwards.data();
+ const auto nextToken = data.vnext_offset()
+ ? qs(*data.vnext_offset())
+ : Data::PublicForwardsSlice::OffsetToken();
- channel->owner().processUsers(data.vusers());
- channel->owner().processChats(data.vchats());
-
- const auto nextToken = Data::PublicForwardsSlice::OffsetToken({
- .storyOffset = data.vnext_offset().value_or_empty(),
- });
-
- const auto allLoaded = nextToken.storyOffset.isEmpty()
- || (nextToken.storyOffset == token.storyOffset);
const auto fullCount = data.vcount().v;
- auto recentList = Messages();
+ auto recentList = Messages(data.vforwards().v.size());
for (const auto &tlForward : data.vforwards().v) {
tlForward.match([&](const MTPDpublicForwardMessage &data) {
const auto &message = data.vmessage();
const auto msgId = IdFromMessage(message);
const auto peerId = PeerFromMessage(message);
const auto lastDate = DateFromMessage(message);
- if (const auto peer = channel->owner().peerLoaded(peerId)) {
+ if (const auto peer = owner.peerLoaded(peerId)) {
if (!lastDate) {
return;
}
- channel->owner().addNewMessage(
+ owner.addNewMessage(
message,
MessageFlags(),
NewMessageType::Existing);
recentList.push_back({ .messageId = { peerId, msgId } });
}
}, [&](const MTPDpublicForwardStory &data) {
- data.vstory().match([&](const MTPDstoryItem &d) {
- recentList.push_back({
- .storyId = { peerFromMTP(data.vpeer()), d.vid().v }
- });
- }, [](const auto &) {
- });
+ const auto story = owner.stories().applySingle(
+ peerFromMTP(data.vpeer()),
+ data.vstory());
+ if (story) {
+ recentList.push_back({ .storyId = story->fullId() });
+ }
});
}
+ const auto allLoaded = nextToken.isEmpty() || (nextToken == token);
_lastTotal = std::max(_lastTotal, fullCount);
done({
.list = std::move(recentList),
@@ -521,9 +416,24 @@ void PublicForwards::requestStory(
.allLoaded = allLoaded,
.token = nextToken,
});
- }).fail([=] {
- _requestId = 0;
- }).send();
+ };
+
+ constexpr auto kLimit = tl::make_int(100);
+ if (_fullId.messageId) {
+ _requestId = makeRequest(MTPstats_GetMessagePublicForwards(
+ channel->inputChannel,
+ MTP_int(_fullId.messageId.msg),
+ MTP_string(token),
+ kLimit
+ )).done(processResult).fail([=] { _requestId = 0; }).send();
+ } else if (_fullId.storyId) {
+ _requestId = makeRequest(MTPstats_GetStoryPublicForwards(
+ channel->input,
+ MTP_int(_fullId.storyId.story),
+ MTP_string(token),
+ kLimit
+ )).done(processResult).fail([=] { _requestId = 0; }).send();
+ }
}
MessageStatistics::MessageStatistics(
@@ -702,6 +612,7 @@ rpl::producer Boosts::request() {
_peer->input
)).done([=](const MTPpremium_BoostsStatus &result) {
const auto &data = result.data();
+ channel->updateLevelHint(data.vlevel().v);
const auto hasPremium = !!data.vpremium_audience();
const auto premiumMemberCount = hasPremium
? std::max(0, int(data.vpremium_audience()->data().vpart().v))
diff --git a/Telegram/SourceFiles/api/api_statistics.h b/Telegram/SourceFiles/api/api_statistics.h
index d1b5b6624e44e..f5360adb21600 100644
--- a/Telegram/SourceFiles/api/api_statistics.h
+++ b/Telegram/SourceFiles/api/api_statistics.h
@@ -77,13 +77,6 @@ class PublicForwards final : public StatisticsRequestSender {
Fn done);
private:
- void requestMessage(
- const Data::PublicForwardsSlice::OffsetToken &token,
- Fn done);
- void requestStory(
- const Data::PublicForwardsSlice::OffsetToken &token,
- Fn done);
-
const Data::RecentPostId _fullId;
mtpRequestId _requestId = 0;
int _lastTotal = 0;
diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp
index 5f903a67d9d4b..c9714d67ec637 100644
--- a/Telegram/SourceFiles/api/api_updates.cpp
+++ b/Telegram/SourceFiles/api/api_updates.cpp
@@ -22,6 +22,7 @@ For license and copyright information please follow this link:
#include "mtproto/mtproto_dc_options.h"
#include "data/notify/data_notify_settings.h"
#include "data/stickers/data_stickers.h"
+#include "data/data_saved_messages.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "data/data_chat.h"
@@ -44,6 +45,7 @@ For license and copyright information please follow this link:
#include "lang/lang_cloud_manager.h"
#include "history/history.h"
#include "history/history_item.h"
+#include "history/history_item_helpers.h"
#include "history/history_unread_things.h"
#include "core/application.h"
#include "storage/storage_account.h"
@@ -1111,6 +1113,7 @@ void Updates::applyUpdatesNoPtsCheck(const MTPUpdates &updates) {
? peerToMTP(_session->userPeerId())
: MTP_peerUser(d.vuser_id())),
MTP_peerUser(d.vuser_id()),
+ MTPPeer(), // saved_peer_id
d.vfwd_from() ? *d.vfwd_from() : MTPMessageFwdHeader(),
MTP_long(d.vvia_bot_id().value_or_empty()),
d.vreply_to() ? *d.vreply_to() : MTPMessageReplyHeader(),
@@ -1142,6 +1145,7 @@ void Updates::applyUpdatesNoPtsCheck(const MTPUpdates &updates) {
d.vid(),
MTP_peerUser(d.vfrom_id()),
MTP_peerChat(d.vchat_id()),
+ MTPPeer(), // saved_peer_id
d.vfwd_from() ? *d.vfwd_from() : MTPMessageFwdHeader(),
MTP_long(d.vvia_bot_id().value_or_empty()),
d.vreply_to() ? *d.vreply_to() : MTPMessageReplyHeader(),
@@ -1202,11 +1206,12 @@ void Updates::applyUpdateNoPtsCheck(const MTPUpdate &update) {
item->markMediaAndMentionRead();
_session->data().requestItemRepaint(item);
- if (item->out()
- && item->history()->peer->isUser()
- && !requestingDifference()) {
- item->history()->peer->asUser()->madeAction(
- base::unixtime::now());
+ if (item->out()) {
+ const auto user = item->history()->peer->asUser();
+ if (user && !requestingDifference()) {
+ user->madeAction(base::unixtime::now());
+ }
+ ClearMediaAsExpired(item);
}
}
} else {
@@ -2204,6 +2209,16 @@ void Updates::feedUpdate(const MTPUpdate &update) {
}
} break;
+ case mtpc_updatePinnedSavedDialogs: {
+ session().data().savedMessages().apply(
+ update.c_updatePinnedSavedDialogs());
+ } break;
+
+ case mtpc_updateSavedDialogPinned: {
+ session().data().savedMessages().apply(
+ update.c_updateSavedDialogPinned());
+ } break;
+
case mtpc_updateChannel: {
auto &d = update.c_updateChannel();
if (const auto channel = session().data().channelLoaded(d.vchannel_id())) {
diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp
index 3b12875988a1a..a247ad1964c6d 100644
--- a/Telegram/SourceFiles/apiwrap.cpp
+++ b/Telegram/SourceFiles/apiwrap.cpp
@@ -39,6 +39,7 @@ For license and copyright information please follow this link:
#include "data/data_folder.h"
#include "data/data_forum_topic.h"
#include "data/data_forum.h"
+#include "data/data_saved_sublist.h"
#include "data/data_search_controller.h"
#include "data/data_scheduled_messages.h"
#include "data/data_session.h"
@@ -221,11 +222,11 @@ void ApiWrap::setupSupportMode() {
void ApiWrap::requestChangelog(
const QString &sinceVersion,
Fn callback) {
- request(MTPhelp_GetAppChangelog(
- MTP_string(sinceVersion)
- )).done(
- callback
- ).send();
+ //request(MTPhelp_GetAppChangelog(
+ // MTP_string(sinceVersion)
+ //)).done(
+ // callback
+ //).send();
}
void ApiWrap::refreshTopPromotion() {
@@ -440,6 +441,26 @@ void ApiWrap::savePinnedOrder(not_null forum) {
}).send();
}
+void ApiWrap::savePinnedOrder(not_null saved) {
+ const auto &order = _session->data().pinnedChatsOrder(saved);
+ const auto input = [](Dialogs::Key key) {
+ if (const auto sublist = key.sublist()) {
+ return MTP_inputDialogPeer(sublist->peer()->input);
+ }
+ Unexpected("Key type in pinnedDialogsOrder().");
+ };
+ auto peers = QVector();
+ peers.reserve(order.size());
+ ranges::transform(
+ order,
+ ranges::back_inserter(peers),
+ input);
+ request(MTPmessages_ReorderPinnedSavedDialogs(
+ MTP_flags(MTPmessages_ReorderPinnedSavedDialogs::Flag::f_force),
+ MTP_vector(peers)
+ )).send();
+}
+
void ApiWrap::toggleHistoryArchived(
not_null history,
bool archived,
diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h
index 70ed1affa9ecb..7e44d460c3641 100644
--- a/Telegram/SourceFiles/apiwrap.h
+++ b/Telegram/SourceFiles/apiwrap.h
@@ -34,6 +34,7 @@ class Forum;
class ForumTopic;
class Thread;
class Story;
+class SavedMessages;
} // namespace Data
namespace InlineBots {
@@ -152,6 +153,7 @@ class ApiWrap final : public MTP::Sender {
void savePinnedOrder(Data::Folder *folder);
void savePinnedOrder(not_null forum);
+ void savePinnedOrder(not_null saved);
void toggleHistoryArchived(
not_null history,
bool archived,
diff --git a/Telegram/SourceFiles/boxes/background_box.cpp b/Telegram/SourceFiles/boxes/background_box.cpp
index 0ae3023e0a99b..877cf556e6562 100644
--- a/Telegram/SourceFiles/boxes/background_box.cpp
+++ b/Telegram/SourceFiles/boxes/background_box.cpp
@@ -129,6 +129,8 @@ class BackgroundBox::Inner final : public Ui::RpWidget {
int row) const;
void validatePaperThumbnail(const Paper &paper) const;
+ [[nodiscard]] bool forChannel() const;
+
const not_null _session;
PeerData * const _forPeer = nullptr;
@@ -176,6 +178,23 @@ void BackgroundBox::prepare() {
st::infoIconMediaPhoto,
st::infoSharedMediaButtonIconPosition);
+ if (forChannel() && _forPeer->wallPaper()) {
+ const auto remove = container->add(object_ptr(
+ container,
+ tr::lng_settings_bg_remove(),
+ st::infoBlockButton));
+ object_ptr(
+ remove,
+ st::infoIconDeleteRed,
+ st::infoSharedMediaButtonIconPosition);
+
+ remove->setClickedCallback([=] {
+ if (const auto resolved = _inner->resolveResetCustomPaper()) {
+ chosen(*resolved);
+ }
+ });
+ }
+
button->setClickedCallback([=] {
chooseFromFile();
});
@@ -290,6 +309,23 @@ void BackgroundBox::chosen(const Data::WallPaper &paper) {
closeBox();
}
return;
+ } else if (forChannel()) {
+ if (_forPeer->wallPaper() && _forPeer->wallPaper()->equals(paper)) {
+ closeBox();
+ return;
+ }
+ const auto &themes = _forPeer->owner().cloudThemes();
+ for (const auto &theme : themes.chatThemes()) {
+ for (const auto &[type, themed] : theme.settings) {
+ if (themed.paper && themed.paper->equals(paper)) {
+ _controller->show(Box(
+ _controller,
+ Data::WallPaper::FromEmojiId(theme.emoticon),
+ BackgroundPreviewArgs{ _forPeer }));
+ return;
+ }
+ }
+ }
}
_controller->show(Box(
_controller,
@@ -316,6 +352,10 @@ void BackgroundBox::resetForPeer() {
}
}
+bool BackgroundBox::forChannel() const {
+ return _forPeer && _forPeer->isChannel();
+}
+
void BackgroundBox::removePaper(const Data::WallPaper &paper) {
const auto session = &_controller->session();
const auto remove = [=, weak = Ui::MakeWeak(this)](Fn &&close) {
@@ -345,9 +385,16 @@ BackgroundBox::Inner::Inner(
, _session(session)
, _forPeer(forPeer)
, _api(&_session->mtp())
-, _check(std::make_unique(st::overviewCheck, [=] { update(); })) {
+, _check(
+ std::make_unique(
+ st::overviewCheck,
+ [=] { update(); })) {
_check->setChecked(true, anim::type::instant);
- resize(st::boxWideWidth, 2 * (st::backgroundSize.height() + st::backgroundPadding) + st::backgroundPadding);
+ resize(
+ st::boxWideWidth,
+ (2 * (st::backgroundSize.height() + st::backgroundPadding)
+ + st::backgroundPadding));
+
Window::Theme::IsNightModeValue(
) | rpl::start_with_next([=] {
updatePapers();
@@ -364,21 +411,31 @@ BackgroundBox::Inner::Inner(
_check->invalidateCache();
}, lifetime());
- using Update = Window::Theme::BackgroundUpdate;
- Window::Theme::Background()->updates(
- ) | rpl::start_with_next([=](const Update &update) {
- if (update.type == Update::Type::New) {
- sortPapers();
- requestPapers();
- this->update();
- }
- }, lifetime());
-
+ if (forChannel()) {
+ _session->data().cloudThemes().chatThemesUpdated(
+ ) | rpl::start_with_next([=] {
+ updatePapers();
+ }, lifetime());
+ } else {
+ using Update = Window::Theme::BackgroundUpdate;
+ Window::Theme::Background()->updates(
+ ) | rpl::start_with_next([=](const Update &update) {
+ if (update.type == Update::Type::New) {
+ sortPapers();
+ requestPapers();
+ this->update();
+ }
+ }, lifetime());
+ }
setMouseTracking(true);
}
void BackgroundBox::Inner::requestPapers() {
+ if (forChannel()) {
+ _session->data().cloudThemes().refreshChatThemes();
+ return;
+ }
_api.request(MTPaccount_GetWallPapers(
MTP_long(_session->data().wallpapersHash())
)).done([=](const MTPaccount_WallPapers &result) {
@@ -395,7 +452,7 @@ auto BackgroundBox::Inner::resolveResetCustomPaper() const
}
const auto nonCustom = Window::Theme::Background()->paper();
const auto themeEmoji = _forPeer->themeEmoji();
- if (themeEmoji.isEmpty()) {
+ if (forChannel() || themeEmoji.isEmpty()) {
return nonCustom;
}
const auto &themes = _forPeer->owner().cloudThemes();
@@ -443,6 +500,8 @@ void BackgroundBox::Inner::pushCustomPapers() {
}
void BackgroundBox::Inner::sortPapers() {
+ Expects(!forChannel());
+
const auto currentCustom = _forPeer ? _forPeer->wallPaper() : nullptr;
_currentId = currentCustom
? currentCustom->id()
@@ -472,23 +531,60 @@ void BackgroundBox::Inner::sortPapers() {
}
void BackgroundBox::Inner::updatePapers() {
- if (_session->data().wallpapers().empty()) {
- return;
+ if (forChannel()) {
+ if (_session->data().cloudThemes().chatThemes().empty()) {
+ return;
+ }
+ } else {
+ if (_session->data().wallpapers().empty()) {
+ return;
+ }
}
_over = _overDown = Selection();
- _papers = _session->data().wallpapers(
- ) | ranges::views::filter([&](const Data::WallPaper &paper) {
- return (!paper.isPattern() || !paper.backgroundColors().empty())
- && (!_forPeer
- || (!Data::IsDefaultWallPaper(paper)
- && (Data::IsCloudWallPaper(paper)
- || Data::IsCustomWallPaper(paper))));
- }) | ranges::views::transform([](const Data::WallPaper &paper) {
- return Paper{ paper };
- }) | ranges::to_vector;
- pushCustomPapers();
- sortPapers();
+ const auto was = base::take(_papers);
+ if (forChannel()) {
+ const auto now = _forPeer->wallPaper();
+ const auto &list = _session->data().cloudThemes().chatThemes();
+ if (list.empty()) {
+ return;
+ }
+ using Type = Data::CloudThemeType;
+ const auto type = Window::Theme::IsNightMode()
+ ? Type::Dark
+ : Type::Light;
+ _papers.reserve(list.size() + 1);
+ const auto nowEmojiId = now ? now->emojiId() : QString();
+ if (!now || !now->emojiId().isEmpty()) {
+ _papers.push_back({ Window::Theme::Background()->paper() });
+ _currentId = _papers.back().data.id();
+ } else {
+ _papers.push_back({ *now });
+ _currentId = now->id();
+ }
+ for (const auto &theme : list) {
+ const auto i = theme.settings.find(type);
+ if (i != end(theme.settings) && i->second.paper) {
+ _papers.push_back({ *i->second.paper });
+ if (nowEmojiId == theme.emoticon) {
+ _currentId = _papers.back().data.id();
+ }
+ }
+ }
+ } else {
+ _papers = _session->data().wallpapers(
+ ) | ranges::views::filter([&](const Data::WallPaper &paper) {
+ return (!paper.isPattern() || !paper.backgroundColors().empty())
+ && (!_forPeer
+ || (!Data::IsDefaultWallPaper(paper)
+ && (Data::IsCloudWallPaper(paper)
+ || Data::IsCustomWallPaper(paper))));
+ }) | ranges::views::transform([](const Data::WallPaper &paper) {
+ return Paper{ paper };
+ }) | ranges::to_vector;
+ pushCustomPapers();
+ sortPapers();
+ }
resizeToContentAndPreload();
}
@@ -587,6 +683,10 @@ void BackgroundBox::Inner::validatePaperThumbnail(
paper.thumbnail.setDevicePixelRatio(cRetinaFactor());
}
+bool BackgroundBox::Inner::forChannel() const {
+ return _forPeer && _forPeer->isChannel();
+}
+
void BackgroundBox::Inner::paintPaper(
QPainter &p,
const Paper &paper,
@@ -604,7 +704,8 @@ void BackgroundBox::Inner::paintPaper(
const auto checkLeft = x + st::backgroundSize.width() - st::overviewCheckSkip - st::overviewCheck.size;
const auto checkTop = y + st::backgroundSize.height() - st::overviewCheckSkip - st::overviewCheck.size;
_check->paint(p, checkLeft, checkTop, width());
- } else if (Data::IsCloudWallPaper(paper.data)
+ } else if (!forChannel()
+ && Data::IsCloudWallPaper(paper.data)
&& !Data::IsDefaultWallPaper(paper.data)
&& !Data::IsLegacy2DefaultWallPaper(paper.data)
&& !Data::IsLegacy3DefaultWallPaper(paper.data)
@@ -642,7 +743,8 @@ void BackgroundBox::Inner::mouseMoveEvent(QMouseEvent *e) {
- st::stickerPanDeleteIconBg.width();
const auto deleteBottom = row * (height + skip) + skip
+ st::stickerPanDeleteIconBg.height();
- const auto inDelete = (x >= deleteLeft)
+ const auto inDelete = !forChannel()
+ && (x >= deleteLeft)
&& (y < deleteBottom)
&& Data::IsCloudWallPaper(data)
&& !Data::IsDefaultWallPaper(data)
diff --git a/Telegram/SourceFiles/boxes/background_box.h b/Telegram/SourceFiles/boxes/background_box.h
index 3fe16d60217d2..8aa63ff9164ab 100644
--- a/Telegram/SourceFiles/boxes/background_box.h
+++ b/Telegram/SourceFiles/boxes/background_box.h
@@ -38,6 +38,7 @@ class BackgroundBox : public Ui::BoxContent {
const Data::WallPaper &paper) const;
void removePaper(const Data::WallPaper &paper);
void resetForPeer();
+ [[nodiscard]] bool forChannel() const;
void chooseFromFile();
diff --git a/Telegram/SourceFiles/boxes/background_preview_box.cpp b/Telegram/SourceFiles/boxes/background_preview_box.cpp
index c54f581ced90d..db72d5690fb87 100644
--- a/Telegram/SourceFiles/boxes/background_preview_box.cpp
+++ b/Telegram/SourceFiles/boxes/background_preview_box.cpp
@@ -8,11 +8,13 @@ For license and copyright information please follow this link:
#include "boxes/background_preview_box.h"
#include "base/unixtime.h"
+#include "boxes/peers/edit_peer_color_box.h"
#include "boxes/premium_preview_box.h"
#include "lang/lang_keys.h"
#include "mainwidget.h"
#include "window/themes/window_theme.h"
#include "ui/boxes/confirm_box.h"
+#include "ui/boxes/boost_box.h"
#include "ui/controls/chat_service_checkbox.h"
#include "ui/chat/chat_theme.h"
#include "ui/chat/chat_style.h"
@@ -29,6 +31,8 @@ For license and copyright information please follow this link:
#include "history/history_item.h"
#include "history/history_item_helpers.h"
#include "history/view/history_view_message.h"
+#include "main/main_account.h"
+#include "main/main_app_config.h"
#include "main/main_session.h"
#include "apiwrap.h"
#include "data/data_session.h"
@@ -53,7 +57,6 @@ For license and copyright information please follow this link:
namespace {
constexpr auto kMaxWallPaperSlugLength = 255;
-constexpr auto kDefaultDimming = 50;
[[nodiscard]] bool IsValidWallPaperSlug(const QString &slug) {
if (slug.isEmpty() || slug.size() > kMaxWallPaperSlugLength) {
@@ -159,6 +162,25 @@ constexpr auto kDefaultDimming = 50;
return result;
}
+[[nodiscard]] Data::WallPaper Resolve(
+ not_null session,
+ const Data::WallPaper &paper,
+ bool dark) {
+ if (paper.emojiId().isEmpty()) {
+ return paper;
+ }
+ const auto &themes = session->data().cloudThemes();
+ if (const auto theme = themes.themeForEmoji(paper.emojiId())) {
+ using Type = Data::CloudThemeType;
+ const auto type = dark ? Type::Dark : Type::Light;
+ const auto i = theme->settings.find(type);
+ if (i != end(theme->settings) && i->second.paper) {
+ return *i->second.paper;
+ }
+ }
+ return paper;
+}
+
} // namespace
struct BackgroundPreviewBox::OverridenStyle {
@@ -196,15 +218,17 @@ BackgroundPreviewBox::BackgroundPreviewBox(
? tr::lng_background_apply2(tr::now)
: tr::lng_background_text2(tr::now)),
true))
-, _paper(paper)
+, _paperEmojiId(paper.emojiId())
+, _paper(
+ Resolve(&controller->session(), paper, Window::Theme::IsNightMode()))
, _media(_paper.document() ? _paper.document()->createMediaView() : nullptr)
, _radial([=](crl::time now) { radialAnimationCallback(now); })
, _appNightMode(Window::Theme::IsNightModeValue())
, _boxDarkMode(_appNightMode.current())
-, _dimmingIntensity(std::clamp(paper.patternIntensity(), 0, 100))
+, _dimmingIntensity(std::clamp(_paper.patternIntensity(), 0, 100))
, _dimmed(_forPeer
- && (paper.document() || paper.localThumbnail())
- && !paper.isPattern()) {
+ && (_paper.document() || _paper.localThumbnail())
+ && !_paper.isPattern()) {
if (_media) {
_media->thumbnailWanted(_paper.fileOrigin());
}
@@ -244,7 +268,36 @@ BackgroundPreviewBox::BackgroundPreviewBox(
BackgroundPreviewBox::~BackgroundPreviewBox() = default;
+void BackgroundPreviewBox::recreate(bool dark) {
+ _paper = Resolve(
+ &_controller->session(),
+ Data::WallPaper::FromEmojiId(_paperEmojiId),
+ dark);
+ _media = _paper.document()
+ ? _paper.document()->createMediaView()
+ : nullptr;
+ if (_media) {
+ _media->thumbnailWanted(_paper.fileOrigin());
+ }
+ _full = QImage();
+ _generated = _scaled = _blurred = _fadeOutThumbnail = QPixmap();
+ _generating = {};
+ generateBackground();
+ _paper.loadDocument();
+ if (const auto document = _paper.document()) {
+ if (document->loading()) {
+ _radial.start(_media->progress());
+ }
+ }
+ checkLoadedDocument();
+ updateServiceBg(_paper.backgroundColors());
+ update();
+}
+
void BackgroundPreviewBox::applyDarkMode(bool dark) {
+ if (!_paperEmojiId.isEmpty()) {
+ recreate(dark);
+ }
const auto equals = (dark == Window::Theme::IsNightMode());
const auto &palette = (dark ? _darkPalette : _lightPalette);
if (!equals && !palette) {
@@ -410,6 +463,10 @@ auto BackgroundPreviewBox::prepareOverridenStyle(bool dark)
return result;
}
+bool BackgroundPreviewBox::forChannel() const {
+ return _forPeer && _forPeer->isChannel();
+}
+
void BackgroundPreviewBox::generateBackground() {
if (_paper.backgroundColors().empty()) {
return;
@@ -435,7 +492,9 @@ void BackgroundPreviewBox::resetTitle() {
void BackgroundPreviewBox::rebuildButtons(bool dark) {
clearButtons();
- addButton(_forPeer
+ addButton(forChannel()
+ ? tr::lng_background_apply_channel()
+ : _forPeer
? tr::lng_background_apply_button()
: tr::lng_settings_apply(), [=] { apply(); });
addButton(tr::lng_cancel(), [=] { closeBox(); });
@@ -624,6 +683,36 @@ void BackgroundPreviewBox::setExistingForPeer(
_controller->finishChatThemeEdit(_forPeer);
}
+void BackgroundPreviewBox::checkLevelForChannel() {
+ Expects(forChannel());
+
+ const auto show = _controller->uiShow();
+ _forPeerLevelCheck = true;
+ const auto weak = Ui::MakeWeak(this);
+ CheckBoostLevel(show, _forPeer, [=](int level) {
+ if (!weak) {
+ return std::optional();
+ }
+ const auto appConfig = &_forPeer->session().account().appConfig();
+ const auto defaultRequired = appConfig->get(
+ "channel_wallpaper_level_min",
+ 9);
+ const auto customRequired = appConfig->get(
+ "channel_custom_wallpaper_level_min",
+ 10);
+ const auto required = _paperEmojiId.isEmpty()
+ ? customRequired
+ : defaultRequired;
+ if (level >= required) {
+ applyForPeer(false);
+ return std::optional();
+ }
+ return std::make_optional(Ui::AskBoostReason{
+ Ui::AskBoostWallpaper{ required }
+ });
+ }, [=] { _forPeerLevelCheck = false; });
+}
+
void BackgroundPreviewBox::applyForPeer() {
Expects(_forPeer != nullptr);
@@ -636,105 +725,110 @@ void BackgroundPreviewBox::applyForPeer() {
}
}
- if (!_fromMessageId && _forPeer->session().premiumPossible()) {
- if (_forBothOverlay) {
- return;
- }
- const auto size = this->size() * style::DevicePixelRatio();
- const auto bg = Images::DitherImage(
- Images::BlurLargeImage(
- Ui::GrabWidgetToImage(this).scaled(
- size / style::ConvertScale(4),
- Qt::IgnoreAspectRatio,
- Qt::SmoothTransformation),
- 24).scaled(
- size,
- Qt::IgnoreAspectRatio,
- Qt::SmoothTransformation));
-
- _forBothOverlay = std::make_unique>(
- this,
- object_ptr(this));
- const auto overlay = _forBothOverlay->entity();
-
- sizeValue() | rpl::start_with_next([=](QSize size) {
- _forBothOverlay->setGeometry({ QPoint(), size });
- overlay->setGeometry({ QPoint(), size });
- }, _forBothOverlay->lifetime());
-
- overlay->paintRequest(
- ) | rpl::start_with_next([=](QRect clip) {
- auto p = QPainter(overlay);
- p.drawImage(0, 0, bg);
- p.fillRect(clip, QColor(0, 0, 0, 64));
- }, overlay->lifetime());
-
- using namespace Ui;
- const auto forMe = CreateChild(
- overlay,
- tr::lng_background_apply_me(),
- st::backgroundConfirm);
- forMe->setClickedCallback([=] {
- applyForPeer(false);
- });
- using namespace rpl::mappers;
- const auto forBoth = ::Settings::CreateLockedButton(
- overlay,
- tr::lng_background_apply_both(
- lt_user,
- rpl::single(_forPeer->shortName())),
- st::backgroundConfirm,
- Data::AmPremiumValue(&_forPeer->session()) | rpl::map(!_1));
- forBoth->setClickedCallback([=] {
- if (_forPeer->session().premium()) {
- applyForPeer(true);
- } else {
- ShowPremiumPreviewBox(
- _controller->uiShow(),
- PremiumPreview::Wallpapers);
- }
- });
- const auto cancel = CreateChild(
- overlay,
- tr::lng_cancel(),
- st::backgroundConfirmCancel);
- cancel->setClickedCallback([=] {
- const auto raw = _forBothOverlay.release();
- raw->shownValue() | rpl::filter(
- !rpl::mappers::_1
- ) | rpl::take(1) | rpl::start_with_next(crl::guard(raw, [=] {
- delete raw;
- }), raw->lifetime());
- raw->toggle(false, anim::type::normal);
- });
- forMe->setTextTransform(RoundButton::TextTransform::NoTransform);
- forBoth->setTextTransform(RoundButton::TextTransform::NoTransform);
- cancel->setTextTransform(RoundButton::TextTransform::NoTransform);
-
- overlay->sizeValue(
- ) | rpl::start_with_next([=](QSize size) {
- const auto padding = st::backgroundConfirmPadding;
- const auto width = size.width()
- - padding.left()
- - padding.right();
- const auto height = cancel->height();
- auto top = size.height() - padding.bottom() - height;
- cancel->setGeometry(padding.left(), top, width, height);
- top -= height + padding.top();
- forBoth->setGeometry(padding.left(), top, width, height);
- top -= height + padding.top();
- forMe->setGeometry(padding.left(), top, width, height);
- }, _forBothOverlay->lifetime());
-
- _forBothOverlay->hide(anim::type::instant);
- _forBothOverlay->show(anim::type::normal);
- } else {
+ if (forChannel()) {
+ checkLevelForChannel();
+ return;
+ } else if (_fromMessageId || !_forPeer->session().premiumPossible()) {
applyForPeer(false);
+ return;
+ } else if (_forBothOverlay) {
+ return;
}
+ const auto size = this->size() * style::DevicePixelRatio();
+ const auto bg = Images::DitherImage(
+ Images::BlurLargeImage(
+ Ui::GrabWidgetToImage(this).scaled(
+ size / style::ConvertScale(4),
+ Qt::IgnoreAspectRatio,
+ Qt::SmoothTransformation),
+ 24).scaled(
+ size,
+ Qt::IgnoreAspectRatio,
+ Qt::SmoothTransformation));
+
+ _forBothOverlay = std::make_unique>(
+ this,
+ object_ptr(this));
+ const auto overlay = _forBothOverlay->entity();
+
+ sizeValue() | rpl::start_with_next([=](QSize size) {
+ _forBothOverlay->setGeometry({ QPoint(), size });
+ overlay->setGeometry({ QPoint(), size });
+ }, _forBothOverlay->lifetime());
+
+ overlay->paintRequest(
+ ) | rpl::start_with_next([=](QRect clip) {
+ auto p = QPainter(overlay);
+ p.drawImage(0, 0, bg);
+ p.fillRect(clip, QColor(0, 0, 0, 64));
+ }, overlay->lifetime());
+
+ using namespace Ui;
+ const auto forMe = CreateChild(
+ overlay,
+ tr::lng_background_apply_me(),
+ st::backgroundConfirm);
+ forMe->setClickedCallback([=] {
+ applyForPeer(false);
+ });
+ using namespace rpl::mappers;
+ const auto forBoth = ::Settings::CreateLockedButton(
+ overlay,
+ tr::lng_background_apply_both(
+ lt_user,
+ rpl::single(_forPeer->shortName())),
+ st::backgroundConfirm,
+ Data::AmPremiumValue(&_forPeer->session()) | rpl::map(!_1));
+ forBoth->setClickedCallback([=] {
+ if (_forPeer->session().premium()) {
+ applyForPeer(true);
+ } else {
+ ShowPremiumPreviewBox(
+ _controller->uiShow(),
+ PremiumPreview::Wallpapers);
+ }
+ });
+ const auto cancel = CreateChild(
+ overlay,
+ tr::lng_cancel(),
+ st::backgroundConfirmCancel);
+ cancel->setClickedCallback([=] {
+ const auto raw = _forBothOverlay.release();
+ raw->shownValue() | rpl::filter(
+ !rpl::mappers::_1
+ ) | rpl::take(1) | rpl::start_with_next(crl::guard(raw, [=] {
+ delete raw;
+ }), raw->lifetime());
+ raw->toggle(false, anim::type::normal);
+ });
+ forMe->setTextTransform(RoundButton::TextTransform::NoTransform);
+ forBoth->setTextTransform(RoundButton::TextTransform::NoTransform);
+ cancel->setTextTransform(RoundButton::TextTransform::NoTransform);
+
+ overlay->sizeValue(
+ ) | rpl::start_with_next([=](QSize size) {
+ const auto padding = st::backgroundConfirmPadding;
+ const auto width = size.width()
+ - padding.left()
+ - padding.right();
+ const auto height = cancel->height();
+ auto top = size.height() - padding.bottom() - height;
+ cancel->setGeometry(padding.left(), top, width, height);
+ top -= height + padding.top();
+ forBoth->setGeometry(padding.left(), top, width, height);
+ top -= height + padding.top();
+ forMe->setGeometry(padding.left(), top, width, height);
+ }, _forBothOverlay->lifetime());
+
+ _forBothOverlay->hide(anim::type::instant);
+ _forBothOverlay->show(anim::type::normal);
}
void BackgroundPreviewBox::applyForPeer(bool both) {
- if (Data::IsCustomWallPaper(_paper)) {
+ using namespace Data;
+ if (forChannel() && !_paperEmojiId.isEmpty()) {
+ setExistingForPeer(WallPaper::FromEmojiId(_paperEmojiId), both);
+ } else if (IsCustomWallPaper(_paper)) {
uploadForPeer(both);
} else {
setExistingForPeer(_paper, both);
@@ -855,7 +949,7 @@ int BackgroundPreviewBox::textsTop() const {
- st::historyPaddingBottom
- (_service ? _service->height() : 0)
- _text1->height()
- - _text2->height();
+ - (forChannel() ? _text2->height() : 0);
}
QRect BackgroundPreviewBox::radialRect() const {
@@ -885,10 +979,11 @@ void BackgroundPreviewBox::paintTexts(Painter &p, crl::time ms) {
context.outbg = _text1->hasOutLayout();
_text1->draw(p, context);
p.translate(0, height1);
-
- context.outbg = _text2->hasOutLayout();
- _text2->draw(p, context);
- p.translate(0, height2);
+ if (!forChannel()) {
+ context.outbg = _text2->hasOutLayout();
+ _text2->draw(p, context);
+ p.translate(0, height2);
+ }
}
void BackgroundPreviewBox::radialAnimationCallback(crl::time now) {
@@ -988,7 +1083,9 @@ void BackgroundPreviewBox::updateServiceBg(const std::vector &bg) {
_service = GenerateServiceItem(
delegate(),
_serviceHistory,
- ((_forPeer && !_fromMessageId)
+ (forChannel()
+ ? tr::lng_background_other_channel(tr::now)
+ : (_forPeer && !_fromMessageId)
? tr::lng_background_other_info(
tr::now,
lt_user,
diff --git a/Telegram/SourceFiles/boxes/background_preview_box.h b/Telegram/SourceFiles/boxes/background_preview_box.h
index 8c1bd43462e0f..3eb06d1a502f5 100644
--- a/Telegram/SourceFiles/boxes/background_preview_box.h
+++ b/Telegram/SourceFiles/boxes/background_preview_box.h
@@ -94,18 +94,24 @@ class BackgroundPreviewBox
void applyDarkMode(bool dark);
[[nodiscard]] OverridenStyle prepareOverridenStyle(bool dark);
+ [[nodiscard]] bool forChannel() const;
+ void checkLevelForChannel();
+
+ void recreate(bool dark);
void resetTitle();
void rebuildButtons(bool dark);
void createDimmingSlider(bool dark);
const not_null _controller;
PeerData * const _forPeer = nullptr;
+ bool _forPeerLevelCheck = false;
FullMsgId _fromMessageId;
std::unique_ptr _chatStyle;
const not_null _serviceHistory;
AdminLog::OwnedItem _service;
AdminLog::OwnedItem _text1;
AdminLog::OwnedItem _text2;
+ QString _paperEmojiId;
Data::WallPaper _paper;
std::shared_ptr _media;
QImage _full;
diff --git a/Telegram/SourceFiles/boxes/connection_box.cpp b/Telegram/SourceFiles/boxes/connection_box.cpp
index 144582a533fce..3bdaa932d9685 100644
--- a/Telegram/SourceFiles/boxes/connection_box.cpp
+++ b/Telegram/SourceFiles/boxes/connection_box.cpp
@@ -113,7 +113,8 @@ Base64UrlInput::Base64UrlInput(
rpl::producer placeholder,
const QString &val)
: MaskedInputField(parent, st, std::move(placeholder), val) {
- if (!QRegularExpression("^[a-zA-Z0-9_\\-]+$").match(val).hasMatch()) {
+ static const auto RegExp = QRegularExpression("^[a-zA-Z0-9_\\-]+$");
+ if (!RegExp.match(val).hasMatch()) {
setText(QString());
}
}
@@ -831,8 +832,9 @@ void ProxyBox::prepare() {
connect(_host.data(), &HostInput::changed, [=] {
Ui::PostponeCall(_host, [=] {
const auto host = _host->getLastText().trimmed();
- static const auto mask = u"^\\d+\\.\\d+\\.\\d+\\.\\d+:(\\d*)$"_q;
- const auto match = QRegularExpression(mask).match(host);
+ static const auto mask = QRegularExpression(
+ u"^\\d+\\.\\d+\\.\\d+\\.\\d+:(\\d*)$"_q);
+ const auto match = mask.match(host);
if (_host->cursorPosition() == host.size()
&& match.hasMatch()) {
const auto port = match.captured(1);
@@ -1107,6 +1109,10 @@ void ProxiesBoxController::ShowApplyConfirmation(
proxy.password = fields.value(u"secret"_q);
}
if (proxy) {
+ static const auto UrlStartRegExp = QRegularExpression(
+ "^https://",
+ QRegularExpression::CaseInsensitiveOption);
+ static const auto UrlEndRegExp = QRegularExpression("/$");
const auto displayed = "https://" + server + "/";
const auto parsed = QUrl::fromUserInput(displayed);
const auto displayUrl = !UrlClickHandler::IsSuspicious(displayed)
@@ -1117,11 +1123,9 @@ void ProxiesBoxController::ShowApplyConfirmation(
const auto displayServer = QString(
displayUrl
).replace(
- QRegularExpression(
- "^https://",
- QRegularExpression::CaseInsensitiveOption),
+ UrlStartRegExp,
QString()
- ).replace(QRegularExpression("/$"), QString());
+ ).replace(UrlEndRegExp, QString());
const auto text = tr::lng_sure_enable_socks(
tr::now,
lt_server,
diff --git a/Telegram/SourceFiles/boxes/gift_premium_box.cpp b/Telegram/SourceFiles/boxes/gift_premium_box.cpp
index 1244964d2e8c3..63603f6d21b2b 100644
--- a/Telegram/SourceFiles/boxes/gift_premium_box.cpp
+++ b/Telegram/SourceFiles/boxes/gift_premium_box.cpp
@@ -12,18 +12,26 @@ For license and copyright information please follow this link:
#include "apiwrap.h"
#include "base/unixtime.h"
#include "base/weak_ptr.h"
+#include "boxes/peer_list_controllers.h" // ContactsBoxController.
#include "boxes/peers/prepare_short_info_box.h"
+#include "boxes/peers/replace_boost_box.h" // BoostsForGift.
+#include "boxes/premium_preview_box.h" // ShowPremiumPreviewBox.
+#include "core/ui_integration.h" // Core::MarkedTextContext.
#include "data/data_boosts.h"
#include "data/data_changes.h"
#include "data/data_channel.h"
-#include "data/data_media_types.h" // Data::Giveaway
+#include "data/data_media_types.h" // Data::GiveawayStart.
#include "data/data_peer_values.h" // Data::PeerPremiumValue.
#include "data/data_session.h"
#include "data/data_subscription_option.h"
#include "data/data_user.h"
+#include "data/stickers/data_custom_emoji.h"
+#include "info/boosts/giveaway/boost_badge.h" // InfiniteRadialAnimationWidget.
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "mainwidget.h"
+#include "payments/payments_checkout_process.h"
+#include "payments/payments_form.h"
#include "settings/settings_premium.h"
#include "ui/basic_click_handlers.h" // UrlClickHandler::Open.
#include "ui/boxes/boost_box.h" // StartFireworks.
@@ -33,11 +41,14 @@ For license and copyright information please follow this link:
#include "ui/effects/premium_top_bar.h"
#include "ui/effects/spoiler_mess.h"
#include "ui/layers/generic_box.h"
+#include "ui/painter.h"
#include "ui/rect.h"
+#include "ui/vertical_list.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/gradient_round_button.h"
#include "ui/wrap/padding_wrap.h"
+#include "ui/wrap/slide_wrap.h"
#include "ui/wrap/table_layout.h"
#include "window/window_peer_menu.h" // ShowChooseRecipientBox.
#include "window/window_session_controller.h"
@@ -51,7 +62,7 @@ For license and copyright information please follow this link:
namespace {
-constexpr auto kDiscountDivider = 5.;
+constexpr auto kUserpicsMax = size_t(3);
using GiftOption = Data::SubscriptionOption;
using GiftOptions = Data::SubscriptionOptions;
@@ -72,6 +83,137 @@ GiftOptions GiftOptionFromTL(const MTPDuserFull &data) {
return result;
}
+using TagUser1 = lngtag_user;
+using TagUser2 = lngtag_second_user;
+using TagUser3 = lngtag_name;
+[[nodiscard]] rpl::producer ComplexAboutLabel(
+ const std::vector> &users,
+ tr::phrase phrase1,
+ tr::phrase phrase2,
+ tr::phrase phrase3,
+ tr::phrase phraseMore) {
+ Expects(!users.empty());
+
+ const auto count = users.size();
+ const auto nameValue = [&](not_null user) {
+ return user->session().changes().peerFlagsValue(
+ user,
+ Data::PeerUpdate::Flag::Name
+ ) | rpl::map([=] { return TextWithEntities{ user->firstName }; });
+ };
+ if (count == 1) {
+ return phrase1(
+ lt_user,
+ nameValue(users.front()),
+ Ui::Text::RichLangValue);
+ } else if (count == 2) {
+ return phrase2(
+ lt_user,
+ nameValue(users.front()),
+ lt_second_user,
+ nameValue(users[1]),
+ Ui::Text::RichLangValue);
+ } else if (count == 3) {
+ return phrase3(
+ lt_user,
+ nameValue(users.front()),
+ lt_second_user,
+ nameValue(users[1]),
+ lt_name,
+ nameValue(users[2]),
+ Ui::Text::RichLangValue);
+ } else {
+ return phraseMore(
+ lt_count,
+ rpl::single(count - kUserpicsMax) | tr::to_count(),
+ lt_user,
+ nameValue(users.front()),
+ lt_second_user,
+ nameValue(users[1]),
+ lt_name,
+ nameValue(users[2]),
+ Ui::Text::RichLangValue);
+ }
+}
+
+[[nodiscard]] not_null CircleBadge(
+ not_null parent,
+ const QString &text) {
+ const auto widget = Ui::CreateChild(parent.get());
+
+ const auto full = Rect(st::premiumGiftsUserpicBadgeSize);
+ const auto inner = full - Margins(st::premiumGiftsUserpicBadgeInner);
+ auto gradient = QLinearGradient(
+ QPointF(0, full.height()),
+ QPointF(full.width(), 0));
+ gradient.setStops(Ui::Premium::GiftGradientStops());
+
+ widget->paintRequest(
+ ) | rpl::start_with_next([=] {
+ auto p = QPainter(widget);
+ auto hq = PainterHighQualityEnabler(p);
+ p.setPen(Qt::NoPen);
+ p.setBrush(st::boxBg);
+ p.drawEllipse(full);
+ p.setPen(Qt::NoPen);
+ p.setBrush(gradient);
+ p.drawEllipse(inner);
+ p.setFont(st::premiumGiftsUserpicBadgeFont);
+ p.setPen(st::premiumButtonFg);
+ p.drawText(full, text, style::al_center);
+ }, widget->lifetime());
+ widget->resize(full.size());
+ return widget;
+}
+
+[[nodiscard]] not_null UserpicsContainer(
+ not_null parent,
+ std::vector> users) {
+ Expects(!users.empty());
+
+ if (users.size() == 1) {
+ const auto userpic = Ui::CreateChild(
+ parent.get(),
+ users.front(),
+ st::defaultUserpicButton);
+ userpic->setAttribute(Qt::WA_TransparentForMouseEvents);
+ return userpic;
+ }
+
+ const auto &singleSize = st::defaultUserpicButton.size;
+
+ const auto container = Ui::CreateChild(parent.get());
+ const auto single = singleSize.width();
+ const auto shift = single - st::boostReplaceUserpicsShift;
+ const auto maxWidth = users.size() * (single - shift) + shift;
+ container->resize(maxWidth, singleSize.height());
+ container->setAttribute(Qt::WA_TransparentForMouseEvents);
+
+ const auto diff = (single - st::premiumGiftsUserpicButton.size.width())
+ / 2;
+ for (auto i = 0; i < users.size(); i++) {
+ const auto bg = Ui::CreateChild(container);
+ bg->resize(singleSize);
+ bg->paintRequest(
+ ) | rpl::start_with_next([=] {
+ auto p = QPainter(bg);
+ auto hq = PainterHighQualityEnabler(p);
+ p.setPen(Qt::NoPen);
+ p.setBrush(st::boxBg);
+ p.drawEllipse(bg->rect());
+ }, bg->lifetime());
+ bg->moveToLeft(std::max(0, i * (single - shift)), 0);
+
+ const auto userpic = Ui::CreateChild(
+ bg,
+ users[i],
+ st::premiumGiftsUserpicButton);
+ userpic->moveToLeft(diff, diff);
+ }
+
+ return container;
+}
+
void GiftBox(
not_null box,
not_null controller,
@@ -95,12 +237,11 @@ void GiftBox(
+ st::defaultUserpicButton.size.height()));
using ColoredMiniStars = Ui::Premium::ColoredMiniStars;
- const auto stars = box->lifetime().make_state(top, true);
-
- const auto userpic = Ui::CreateChild(
+ const auto stars = box->lifetime().make_state(
top,
- user,
- st::defaultUserpicButton);
+ true);
+
+ const auto userpic = UserpicsContainer(top, { user });
userpic->setAttribute(Qt::WA_TransparentForMouseEvents);
top->widthValue(
) | rpl::start_with_next([=](int width) {
@@ -211,7 +352,7 @@ void GiftBox(
auto raw = Settings::CreateSubscribeButton({
controller,
box,
- [] { return QString("gift"); },
+ [] { return u"gift"_q; },
state->buttonText.events(),
Ui::Premium::GiftGradientStops(),
[=] {
@@ -222,10 +363,8 @@ void GiftBox(
},
});
auto button = object_ptr::fromRaw(raw);
- button->resizeToWidth(boxWidth
- - stButton.buttonPadding.left()
- - stButton.buttonPadding.right());
- box->setShowFinishedCallback([raw = button.data()]{
+ button->resizeToWidth(boxWidth - rect::m::sum::h(stButton.buttonPadding));
+ box->setShowFinishedCallback([raw = button.data()] {
raw->startGlareAnimation();
});
box->addButton(std::move(button));
@@ -239,6 +378,302 @@ void GiftBox(
}, box->lifetime());
}
+void GiftsBox(
+ not_null box,
+ not_null controller,
+ std::vector> users,
+ not_null api) {
+ Expects(!users.empty());
+
+ const auto boxWidth = st::boxWideWidth;
+ box->setWidth(boxWidth);
+ box->setNoContentMargin(true);
+ const auto buttonsParent = box->verticalLayout().get();
+ const auto session = &users.front()->session();
+
+ struct State {
+ rpl::event_stream buttonText;
+ rpl::variable confirmButtonBusy = false;
+ rpl::variable isPaymentComplete = false;
+ };
+ const auto state = box->lifetime().make_state();
+
+ const auto userpicPadding = st::premiumGiftUserpicPadding;
+ const auto top = box->addRow(object_ptr(
+ buttonsParent,
+ userpicPadding.top()
+ + userpicPadding.bottom()
+ + st::defaultUserpicButton.size.height()));
+
+ using ColoredMiniStars = Ui::Premium::ColoredMiniStars;
+ const auto stars = box->lifetime().make_state(
+ top,
+ true);
+
+ const auto maxWithUserpic = std::min(users.size(), kUserpicsMax);
+ const auto userpics = UserpicsContainer(
+ top,
+ { users.begin(), users.begin() + maxWithUserpic });
+ top->widthValue(
+ ) | rpl::start_with_next([=](int width) {
+ userpics->moveToLeft(
+ (width - userpics->width()) / 2,
+ userpicPadding.top());
+
+ const auto center = top->rect().center();
+ const auto size = QSize(
+ userpics->width() * Ui::Premium::MiniStars::kSizeFactor,
+ userpics->height());
+ const auto ministarsRect = QRect(
+ QPoint(center.x() - size.width(), center.y() - size.height()),
+ QPoint(center.x() + size.width(), center.y() + size.height()));
+ stars->setPosition(ministarsRect.topLeft());
+ stars->setSize(ministarsRect.size());
+ }, userpics->lifetime());
+ if (const auto rest = users.size() - maxWithUserpic; rest > 0) {
+ const auto badge = CircleBadge(
+ userpics,
+ QChar('+') + QString::number(rest));
+ badge->moveToRight(0, userpics->height() - badge->height());
+ }
+
+ top->paintRequest(
+ ) | rpl::start_with_next([=](const QRect &r) {
+ auto p = QPainter(top);
+
+ p.fillRect(r, Qt::transparent);
+ stars->paint(p);
+ }, top->lifetime());
+
+ const auto close = Ui::CreateChild(
+ buttonsParent,
+ st::infoTopBarClose);
+ close->setClickedCallback([=] { box->closeBox(); });
+
+ buttonsParent->widthValue(
+ ) | rpl::start_with_next([=](int width) {
+ close->moveToRight(0, 0, width);
+ }, close->lifetime());
+
+ // Header.
+ const auto &padding = st::premiumGiftAboutPadding;
+ const auto available = boxWidth - padding.left() - padding.right();
+ const auto &stTitle = st::premiumPreviewAboutTitle;
+ auto titleLabel = object_ptr(
+ box,
+ rpl::conditional(
+ state->isPaymentComplete.value(),
+ tr::lng_premium_gifts_about_paid_title(),
+ tr::lng_premium_gift_title()),
+ stTitle);
+ titleLabel->resizeToWidth(available);
+ box->addRow(
+ object_ptr>(
+ box,
+ std::move(titleLabel)),
+ st::premiumGiftTitlePadding);
+
+ // About.
+ {
+ const auto emoji = Ui::Text::SingleCustomEmoji(
+ session->data().customEmojiManager().registerInternalEmoji(
+ st::premiumGiftsBoostIcon,
+ QMargins(0, st::premiumGiftsUserpicBadgeInner, 0, 0),
+ false));
+ auto text = rpl::conditional(
+ state->isPaymentComplete.value(),
+ ComplexAboutLabel(
+ users,
+ tr::lng_premium_gifts_about_paid1,
+ tr::lng_premium_gifts_about_paid2,
+ tr::lng_premium_gifts_about_paid3,
+ tr::lng_premium_gifts_about_paid_more
+ ) | rpl::map([count = users.size()](TextWithEntities text) {
+ text.append('\n');
+ text.append('\n');
+ text.append(tr::lng_premium_gifts_about_paid_below(
+ tr::now,
+ lt_count,
+ float64(count),
+ Ui::Text::RichLangValue));
+ return text;
+ }),
+ ComplexAboutLabel(
+ users,
+ tr::lng_premium_gifts_about_user1,
+ tr::lng_premium_gifts_about_user2,
+ tr::lng_premium_gifts_about_user3,
+ tr::lng_premium_gifts_about_user_more
+ ) | rpl::map([=, count = users.size()](TextWithEntities text) {
+ text.append('\n');
+ text.append('\n');
+ text.append(tr::lng_premium_gifts_about_reward(
+ tr::now,
+ lt_count,
+ count * BoostsForGift(session),
+ lt_emoji,
+ emoji,
+ Ui::Text::RichLangValue));
+ return text;
+ })
+ );
+ const auto label = box->addRow(
+ object_ptr>(
+ box,
+ object_ptr(box, st::premiumPreviewAbout)),
+ padding)->entity();
+ std::move(
+ text
+ ) | rpl::start_with_next([=](const TextWithEntities &t) {
+ using namespace Core;
+ label->setMarkedText(t, MarkedTextContext{ .session = session });
+ }, label->lifetime());
+ label->setTextColorOverride(stTitle.textFg->c);
+ label->resizeToWidth(available);
+ }
+
+ // List.
+ const auto optionsContainer = buttonsParent->add(
+ object_ptr>(
+ buttonsParent,
+ object_ptr(buttonsParent)));
+ const auto options = api->options(users.size());
+ const auto group = std::make_shared();
+ const auto groupValueChangedCallback = [=](int value) {
+ Expects(value < options.size() && value >= 0);
+ auto text = tr::lng_premium_gift_button(
+ tr::now,
+ lt_cost,
+ options[value].costTotal);
+ state->buttonText.fire(std::move(text));
+ };
+ group->setChangedCallback(groupValueChangedCallback);
+ Ui::Premium::AddGiftOptions(
+ optionsContainer->entity(),
+ group,
+ options,
+ st::premiumGiftOption);
+ optionsContainer->toggleOn(
+ state->isPaymentComplete.value() | rpl::map(!rpl::mappers::_1),
+ anim::type::instant);
+
+ // Summary.
+ {
+ {
+ // Will be hidden after payment.
+ const auto content = optionsContainer->entity();
+ Ui::AddSkip(content);
+ Ui::AddDivider(content);
+ Ui::AddSkip(content);
+ Ui::AddSubsectionTitle(
+ content,
+ tr::lng_premium_gifts_summary_subtitle());
+ }
+ const auto content = box->addRow(
+ object_ptr(box),
+ {});
+ auto buttonCallback = [=](PremiumPreview section) {
+ stars->setPaused(true);
+ const auto previewBoxShown = [=](
+ not_null previewBox) {
+ previewBox->boxClosing(
+ ) | rpl::start_with_next(crl::guard(box, [=] {
+ stars->setPaused(false);
+ }), previewBox->lifetime());
+ };
+
+ ShowPremiumPreviewBox(
+ controller->uiShow(),
+ section,
+ previewBoxShown,
+ true);
+ };
+ Settings::AddSummaryPremium(
+ content,
+ controller,
+ u"gift"_q,
+ std::move(buttonCallback));
+ }
+
+ // Footer.
+ {
+ box->addRow(
+ object_ptr(
+ box,
+ object_ptr(
+ box,
+ session->api().premium().statusTextValue(), // TODO.
+ st::premiumGiftTerms),
+ st::defaultBoxDividerLabelPadding),
+ {});
+ }
+
+ // Button.
+ const auto &stButton = st::premiumGiftBox;
+ box->setStyle(stButton);
+ auto raw = Settings::CreateSubscribeButton({
+ controller,
+ box,
+ [] { return u"gift"_q; },
+ rpl::combine(
+ state->buttonText.events(),
+ state->confirmButtonBusy.value(),
+ state->isPaymentComplete.value()
+ ) | rpl::map([](const QString &text, bool busy, bool paid) {
+ return busy
+ ? QString()
+ : paid
+ ? tr::lng_close(tr::now)
+ : text;
+ }),
+ Ui::Premium::GiftGradientStops(),
+ });
+ raw->setClickedCallback([=] {
+ if (state->confirmButtonBusy.current()) {
+ return;
+ }
+ if (state->isPaymentComplete.current()) {
+ return box->closeBox();
+ }
+ auto invoice = api->invoice(
+ users.size(),
+ api->monthsFromPreset(group->value()));
+ invoice.purpose = Payments::InvoicePremiumGiftCodeUsers{ users };
+
+ state->confirmButtonBusy = true;
+ const auto show = box->uiShow();
+ const auto weak = Ui::MakeWeak(box.get());
+ const auto done = [=](Payments::CheckoutResult result) {
+ if (const auto strong = weak.data()) {
+ strong->window()->setFocus();
+ state->confirmButtonBusy = false;
+ if (result == Payments::CheckoutResult::Paid) {
+ state->isPaymentComplete = true;
+ Ui::StartFireworks(box->parentWidget());
+ }
+ }
+ };
+
+ Payments::CheckoutProcess::Start(std::move(invoice), done);
+ });
+ {
+ using namespace Info::Statistics;
+ const auto loadingAnimation = InfiniteRadialAnimationWidget(
+ raw,
+ raw->height() / 2);
+ AddChildToWidgetCenter(raw, loadingAnimation);
+ loadingAnimation->showOn(state->confirmButtonBusy.value());
+ }
+ auto button = object_ptr::fromRaw(raw);
+ button->resizeToWidth(boxWidth - rect::m::sum::h(stButton.buttonPadding));
+ box->setShowFinishedCallback([raw = button.data()] {
+ raw->startGlareAnimation();
+ });
+ box->addButton(std::move(button));
+
+ groupValueChangedCallback(0);
+}
+
[[nodiscard]] Data::GiftCodeLink MakeGiftCodeLink(
not_null session,
const QString &slug) {
@@ -370,18 +805,20 @@ void AddTable(
container,
st::giveawayGiftCodeTable),
st::giveawayGiftCodeTableMargin);
- AddTableRow(
- table,
- tr::lng_gift_link_label_from(),
- controller,
- current.from);
- if (current.to) {
+ if (current.from) {
+ AddTableRow(
+ table,
+ tr::lng_gift_link_label_from(),
+ controller,
+ current.from);
+ }
+ if (current.from && current.to) {
AddTableRow(
table,
tr::lng_gift_link_label_to(),
controller,
current.to);
- } else {
+ } else if (current.from) {
AddTableRow(
table,
tr::lng_gift_link_label_to(),
@@ -394,7 +831,7 @@ void AddTable(
lt_duration,
GiftDurationValue(current.months) | Ui::Text::ToWithEntities(),
Ui::Text::WithEntities));
- if (!skipReason) {
+ if (!skipReason && current.from) {
const auto reason = AddTableRow(
table,
tr::lng_gift_link_label_reason(),
@@ -439,6 +876,112 @@ void GiftPremiumValidator::cancel() {
_requestId = 0;
}
+void GiftPremiumValidator::showChoosePeerBox() {
+ if (_manyGiftsLifetime) {
+ return;
+ }
+ using namespace Api;
+ const auto api = _manyGiftsLifetime.make_state(
+ _controller->session().user());
+ const auto show = _controller->uiShow();
+ api->request(
+ ) | rpl::start_with_error_done([=](const QString &error) {
+ show->showToast(error);
+ }, [=] {
+ const auto maxAmount = *ranges::max_element(api->availablePresets());
+
+ class Controller final : public ContactsBoxController {
+ public:
+ Controller(
+ not_null session,
+ Fn checkErrorCallback)
+ : ContactsBoxController(session)
+ , _checkErrorCallback(std::move(checkErrorCallback)) {
+ }
+
+ protected:
+ std::unique_ptr createRow(
+ not_null user) override {
+ return !user->isSelf()
+ ? ContactsBoxController::createRow(user)
+ : nullptr;
+ }
+
+ void rowClicked(not_null row) override {
+ const auto checked = !row->checked();
+ if (checked
+ && _checkErrorCallback
+ && _checkErrorCallback(
+ delegate()->peerListSelectedRowsCount())) {
+ return;
+ }
+ delegate()->peerListSetRowChecked(row, checked);
+ }
+
+ private:
+ const Fn _checkErrorCallback;
+
+ };
+ auto initBox = [=](not_null peersBox) {
+ const auto ignoreClose = peersBox->lifetime().make_state(0);
+
+ auto process = [=] {
+ const auto selected = peersBox->collectSelectedRows();
+ const auto users = ranges::views::all(
+ selected
+ ) | ranges::views::transform([](not_null p) {
+ return p->asUser();
+ }) | ranges::views::filter([](UserData *u) -> bool {
+ return u;
+ }) | ranges::to>>();
+ if (!users.empty()) {
+ const auto giftBox = show->show(
+ Box(GiftsBox, _controller, users, api));
+ giftBox->boxClosing(
+ ) | rpl::start_with_next([=] {
+ _manyGiftsLifetime.destroy();
+ }, giftBox->lifetime());
+ }
+ (*ignoreClose) = true;
+ peersBox->closeBox();
+ };
+
+ peersBox->setTitle(tr::lng_premium_gift_title());
+ peersBox->addButton(
+ tr::lng_settings_gift_premium_users_confirm(),
+ std::move(process));
+ peersBox->addButton(tr::lng_cancel(), [=] {
+ peersBox->closeBox();
+ });
+ peersBox->boxClosing(
+ ) | rpl::start_with_next([=] {
+ if (!(*ignoreClose)) {
+ _manyGiftsLifetime.destroy();
+ }
+ }, peersBox->lifetime());
+ };
+
+ auto listController = std::make_unique(
+ &_controller->session(),
+ [=](int count) {
+ if (count <= maxAmount) {
+ return false;
+ }
+ show->showToast(tr::lng_settings_gift_premium_users_error(
+ tr::now,
+ lt_count,
+ maxAmount));
+ return true;
+ });
+ show->showBox(
+ Box(
+ std::move(listController),
+ std::move(initBox)),
+ Ui::LayerOption::KeepOther);
+
+ }, _manyGiftsLifetime);
+}
+
void GiftPremiumValidator::showBox(not_null user) {
if (_requestId) {
return;
@@ -723,10 +1266,22 @@ void GiftCodePendingBox(
void ResolveGiftCode(
not_null controller,
- const QString &slug) {
+ const QString &slug,
+ PeerId fromId,
+ PeerId toId) {
const auto done = [=](Api::GiftCode code) {
+ const auto session = &controller->session();
+ const auto selfId = session->userPeerId();
if (!code) {
controller->showToast(tr::lng_gift_link_expired(tr::now));
+ } else if (!code.from && fromId == selfId) {
+ code.from = fromId;
+ code.to = toId;
+ const auto self = (fromId == selfId);
+ const auto peer = session->data().peer(self ? toId : fromId);
+ const auto months = code.months;
+ const auto parent = controller->parentController();
+ Settings::ShowGiftPremium(parent, peer, months, self);
} else {
controller->uiShow()->showBox(Box(GiftCodeBox, controller, slug));
}
@@ -739,8 +1294,11 @@ void ResolveGiftCode(
void GiveawayInfoBox(
not_null box,
not_null controller,
- Data::Giveaway giveaway,
+ std::optional start,
+ std::optional results,
Api::GiveawayInfo info) {
+ Expects(start || results);
+
using State = Api::GiveawayState;
const auto finished = (info.state == State::Finished)
|| (info.state == State::Refunded);
@@ -749,10 +1307,31 @@ void GiveawayInfoBox(
? tr::lng_prizes_end_title
: tr::lng_prizes_how_title)());
- const auto first = !giveaway.channels.empty()
- ? giveaway.channels.front()->name()
+ const auto first = results
+ ? results->channel->name()
+ : !start->channels.empty()
+ ? start->channels.front()->name()
: u"channel"_q;
- auto text = (finished
+ auto text = TextWithEntities();
+
+ if (!info.giftCode.isEmpty()) {
+ text.append("\n\n");
+ text.append(Ui::Text::Bold(tr::lng_prizes_you_won(
+ tr::now,
+ lt_cup,
+ QString::fromUtf8("\xf0\x9f\x8f\x86"))));
+ text.append("\n\n");
+ } else if (info.state == State::Finished) {
+ text.append("\n\n");
+ text.append(Ui::Text::Bold(tr::lng_prizes_you_didnt(tr::now)));
+ text.append("\n\n");
+ }
+
+ const auto quantity = start
+ ? start->quantity
+ : (results->winnersCount + results->unclaimedCount);
+ const auto months = start ? start->months : results->months;
+ text.append((finished
? tr::lng_prizes_end_text
: tr::lng_prizes_how_text)(
tr::now,
@@ -760,18 +1339,21 @@ void GiveawayInfoBox(
tr::lng_prizes_admins(
tr::now,
lt_count,
- giveaway.quantity,
+ quantity,
lt_channel,
Ui::Text::Bold(first),
lt_duration,
- TextWithEntities{ GiftDuration(giveaway.months) },
+ TextWithEntities{ GiftDuration(months) },
Ui::Text::RichLangValue),
- Ui::Text::RichLangValue);
- const auto many = (giveaway.channels.size() > 1);
+ Ui::Text::RichLangValue));
+ const auto many = start
+ ? (start->channels.size() > 1)
+ : (results->additionalPeersCount > 0);
const auto count = info.winnersCount
? info.winnersCount
- : giveaway.quantity;
- auto winners = giveaway.all
+ : quantity;
+ const auto all = start ? start->all : results->all;
+ auto winners = all
? (many
? tr::lng_prizes_winners_all_of_many
: tr::lng_prizes_winners_all_of_one)(
@@ -793,13 +1375,30 @@ void GiveawayInfoBox(
Ui::Text::Bold(
langDateTime(base::unixtime::parse(info.startDate))),
Ui::Text::RichLangValue);
+ const auto additionalPrize = results
+ ? results->additionalPrize
+ : start->additionalPrize;
+ if (!additionalPrize.isEmpty()) {
+ text.append("\n\n").append(tr::lng_prizes_additional_added(
+ tr::now,
+ lt_count,
+ count,
+ lt_channel,
+ Ui::Text::Bold(first),
+ lt_prize,
+ TextWithEntities{ additionalPrize },
+ Ui::Text::RichLangValue));
+ }
+ const auto untilDate = start
+ ? start->untilDate
+ : results->untilDate;
text.append("\n\n").append((finished
? tr::lng_prizes_end_when_finish
: tr::lng_prizes_how_when_finish)(
tr::now,
lt_date,
Ui::Text::Bold(langDayOfMonthFull(
- base::unixtime::parse(giveaway.untilDate).date())),
+ base::unixtime::parse(untilDate).date())),
lt_winners,
winners,
Ui::Text::RichLangValue));
@@ -810,17 +1409,9 @@ void GiveawayInfoBox(
info.activatedCount,
Ui::Text::RichLangValue));
}
- if (!info.giftCode.isEmpty()) {
- text.append("\n\n");
- text.append(tr::lng_prizes_you_won(
- tr::now,
- lt_cup,
- QString::fromUtf8("\xf0\x9f\x8f\x86")));
- } else if (info.state == State::Finished) {
- text.append("\n\n");
- text.append(tr::lng_prizes_you_didnt(tr::now));
- } else if (info.state == State::Preparing) {
-
+ if (!info.giftCode.isEmpty()
+ || info.state == State::Finished
+ || info.state == State::Preparing) {
} else if (info.state != State::Refunded) {
if (info.adminChannelId) {
const auto channel = controller->session().data().channel(
@@ -858,7 +1449,7 @@ void GiveawayInfoBox(
Ui::Text::Bold(first),
lt_date,
Ui::Text::Bold(langDayOfMonthFull(
- base::unixtime::parse(giveaway.untilDate).date())),
+ base::unixtime::parse(untilDate).date())),
Ui::Text::RichLangValue));
}
}
@@ -902,14 +1493,15 @@ void ResolveGiveawayInfo(
not_null controller,
not_null peer,
MsgId messageId,
- Data::Giveaway giveaway) {
+ std::optional start,
+ std::optional results) {
const auto show = [=](Api::GiveawayInfo info) {
if (!info) {
controller->showToast(
tr::lng_confirm_phone_link_invalid(tr::now));
} else {
controller->uiShow()->showBox(
- Box(GiveawayInfoBox, controller, giveaway, info));
+ Box(GiveawayInfoBox, controller, start, results, info));
}
};
controller->session().api().premium().resolveGiveawayInfo(
diff --git a/Telegram/SourceFiles/boxes/gift_premium_box.h b/Telegram/SourceFiles/boxes/gift_premium_box.h
index 1891ce5ff43fa..5a0bfb65967a6 100644
--- a/Telegram/SourceFiles/boxes/gift_premium_box.h
+++ b/Telegram/SourceFiles/boxes/gift_premium_box.h
@@ -16,7 +16,8 @@ struct GiftCode;
} // namespace Api
namespace Data {
-struct Giveaway;
+struct GiveawayStart;
+struct GiveawayResults;
} // namespace Data
namespace Ui {
@@ -33,6 +34,7 @@ class GiftPremiumValidator final {
GiftPremiumValidator(not_null controller);
void showBox(not_null user);
+ void showChoosePeerBox();
void cancel();
private:
@@ -41,6 +43,8 @@ class GiftPremiumValidator final {
mtpRequestId _requestId = 0;
+ rpl::lifetime _manyGiftsLifetime;
+
};
[[nodiscard]] rpl::producer GiftDurationValue(int months);
@@ -56,10 +60,13 @@ void GiftCodePendingBox(
const Api::GiftCode &data);
void ResolveGiftCode(
not_null controller,
- const QString &slug);
+ const QString &slug,
+ PeerId fromId = 0,
+ PeerId toId = 0);
void ResolveGiveawayInfo(
not_null controller,
not_null peer,
MsgId messageId,
- Data::Giveaway giveaway);
+ std::optional start,
+ std::optional results);
diff --git a/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp b/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp
index 6ccc564fc5e0d..7d4d744530a35 100644
--- a/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp
+++ b/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp
@@ -40,11 +40,14 @@ class ChoosePeerBoxController final
not_null navigation,
not_null bot,
RequestPeerQuery query,
- Fn)> callback);
+ Fn>)> callback);
Main::Session &session() const override;
void rowClicked(not_null row) override;
+ [[nodiscard]] rpl::producer selectedCountValue() const;
+ void submit();
+
QString savedMessagesChatStatus() const override {
return tr::lng_saved_forward_here(tr::now);
}
@@ -60,7 +63,9 @@ class ChoosePeerBoxController final
not_null _bot;
RequestPeerQuery _query;
base::flat_set> _commonGroups;
- Fn)> _callback;
+ base::flat_set> _selected;
+ rpl::variable _selectedCount;
+ Fn>)> _callback;
};
@@ -253,10 +258,10 @@ object_ptr CreatePeerByQueryBox(
not_null navigation,
not_null bot,
RequestPeerQuery query,
- Fn)> done) {
+ Fn>)> done) {
const auto weak = std::make_shared>();
auto callback = [=](not_null peer) {
- done(peer);
+ done({ peer });
if (const auto strong = weak->data()) {
strong->closeBox();
}
@@ -332,7 +337,7 @@ ChoosePeerBoxController::ChoosePeerBoxController(
not_null navigation,
not_null bot,
RequestPeerQuery query,
- Fn)> callback)
+ Fn>)> callback)
: ChatsListBoxController(&navigation->session())
, _navigation(navigation)
, _bot(bot)
@@ -415,6 +420,8 @@ void ChoosePeerBoxController::prepareViewHook() {
switch (_query.type) {
case Type::User: return (_query.userIsBot == Restriction::Yes)
? tr::lng_request_bot_title()
+ : (_query.maxQuantity > 1)
+ ? tr::lng_request_users_title()
: tr::lng_request_user_title();
case Type::Group: return tr::lng_request_group_title();
case Type::Broadcast: return tr::lng_request_channel_title();
@@ -425,10 +432,24 @@ void ChoosePeerBoxController::prepareViewHook() {
}
void ChoosePeerBoxController::rowClicked(not_null row) {
+ const auto limit = _query.maxQuantity;
+ const auto multiselect = (limit > 1);
const auto peer = row->peer();
+ if (multiselect) {
+ if (_selected.contains(peer) || _selected.size() < limit) {
+ delegate()->peerListSetRowChecked(row, !row->checked());
+ if (row->checked()) {
+ _selected.emplace(peer);
+ } else {
+ _selected.remove(peer);
+ }
+ _selectedCount = int(_selected.size());
+ }
+ return;
+ }
const auto done = [callback = _callback, peer] {
const auto onstack = callback;
- onstack(peer);
+ onstack({ peer });
};
if (const auto user = peer->asUser()) {
done();
@@ -438,6 +459,15 @@ void ChoosePeerBoxController::rowClicked(not_null row) {
}
}
+rpl::producer ChoosePeerBoxController::selectedCountValue() const {
+ return _selectedCount.value();
+}
+
+void ChoosePeerBoxController::submit() {
+ const auto onstack = _callback;
+ onstack(ranges::to_vector(_selected));
+}
+
auto ChoosePeerBoxController::createRow(not_null history)
-> std::unique_ptr {
return FilterPeerByQuery(history->peer, _query, _commonGroups)
@@ -474,7 +504,7 @@ void ShowChoosePeerBox(
not_null navigation,
not_null bot,
RequestPeerQuery query,
- Fn)> chosen) {
+ Fn>)> chosen) {
const auto needCommonGroups = query.isBotParticipant
&& (query.type == RequestPeerQuery::Type::Group)
&& !query.myRights;
@@ -488,22 +518,39 @@ void ShowChoosePeerBox(
return;
}
const auto weak = std::make_shared>();
- auto initBox = [=](not_null box) {
- box->addButton(tr::lng_cancel(), [box] {
- box->closeBox();
- });
- };
- auto callback = [=, done = std::move(chosen)](not_null peer) {
- done(peer);
+ auto callback = [=, done = std::move(chosen)](
+ std::vector> peers) {
+ done(std::move(peers));
if (const auto strong = weak->data()) {
strong->closeBox();
}
};
+ const auto limit = query.maxQuantity;
+ auto controller = std::make_unique(
+ navigation,
+ bot,
+ query,
+ std::move(callback));
+ auto initBox = [=, ptr = controller.get()](not_null box) {
+ ptr->selectedCountValue() | rpl::start_with_next([=](int count) {
+ box->clearButtons();
+ if (limit > 1) {
+ box->setAdditionalTitle(rpl::single(u"%1 / %2"_q.arg(count).arg(limit)));
+ }
+ if (count > 0) {
+ box->addButton(tr::lng_intro_submit(), [=] {
+ ptr->submit();
+ if (*weak) {
+ (*weak)->closeBox();
+ }
+ });
+ }
+ box->addButton(tr::lng_cancel(), [box] {
+ box->closeBox();
+ });
+ }, box->lifetime());
+ };
*weak = navigation->parentController()->show(Box(
- std::make_unique(
- navigation,
- bot,
- query,
- std::move(callback)),
+ std::move(controller),
std::move(initBox)));
}
diff --git a/Telegram/SourceFiles/boxes/peers/choose_peer_box.h b/Telegram/SourceFiles/boxes/peers/choose_peer_box.h
index 2c9ab7a1d1f6d..982ec4784c9a2 100644
--- a/Telegram/SourceFiles/boxes/peers/choose_peer_box.h
+++ b/Telegram/SourceFiles/boxes/peers/choose_peer_box.h
@@ -21,4 +21,4 @@ void ShowChoosePeerBox(
not_null navigation,
not_null bot,
RequestPeerQuery query,
- Fn)> chosen);
+ Fn>)> chosen);
diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp
index fd3458c1daadf..38f064cb2d03e 100644
--- a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp
+++ b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp
@@ -9,12 +9,15 @@ For license and copyright information please follow this link:
#include "apiwrap.h"
#include "api/api_peer_colors.h"
+#include "api/api_peer_photo.h"
#include "base/unixtime.h"
#include "boxes/peers/replace_boost_box.h"
+#include "boxes/background_box.h"
#include "chat_helpers/compose/compose_show.h"
#include "data/data_changes.h"
#include "data/data_channel.h"
#include "data/stickers/data_custom_emoji.h"
+#include "data/data_emoji_statuses.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "data/data_web_page.h"
@@ -428,11 +431,17 @@ HistoryView::Context PreviewDelegate::elementContext() {
return HistoryView::Context::AdminLog;
}
+struct SetValues {
+ uint8 colorIndex = 0;
+ DocumentId backgroundEmojiId = 0;
+ DocumentId statusId = 0;
+ TimeId statusUntil = 0;
+ bool statusChanged = false;
+};
void Set(
std::shared_ptr show,
not_null peer,
- uint8 colorIndex,
- DocumentId backgroundEmojiId) {
+ SetValues values) {
const auto wasIndex = peer->colorIndex();
const auto wasEmojiId = peer->backgroundEmojiId();
@@ -444,7 +453,7 @@ void Set(
peer,
UpdateFlag::Color | UpdateFlag::BackgroundEmoji);
};
- setLocal(colorIndex, backgroundEmojiId);
+ setLocal(values.colorIndex, values.backgroundEmojiId);
const auto done = [=] {
show->showToast(peer->isSelf()
@@ -452,8 +461,11 @@ void Set(
: tr::lng_settings_color_changed_channel(tr::now));
};
const auto fail = [=](const MTP::Error &error) {
- setLocal(wasIndex, wasEmojiId);
- show->showToast(error.type());
+ const auto type = error.type();
+ if (type != u"CHAT_NOT_MODIFIED"_q) {
+ setLocal(wasIndex, wasEmojiId);
+ show->showToast(type);
+ }
};
const auto send = [&](auto &&request) {
peer->session().api().request(
@@ -464,15 +476,23 @@ void Set(
using Flag = MTPaccount_UpdateColor::Flag;
send(MTPaccount_UpdateColor(
MTP_flags(Flag::f_color | Flag::f_background_emoji_id),
- MTP_int(colorIndex),
- MTP_long(backgroundEmojiId)));
+ MTP_int(values.colorIndex),
+ MTP_long(values.backgroundEmojiId)));
} else if (const auto channel = peer->asChannel()) {
using Flag = MTPchannels_UpdateColor::Flag;
send(MTPchannels_UpdateColor(
- MTP_flags(Flag::f_background_emoji_id),
+ MTP_flags(Flag::f_color | Flag::f_background_emoji_id),
channel->inputChannel,
- MTP_int(colorIndex),
- MTP_long(backgroundEmojiId)));
+ MTP_int(values.colorIndex),
+ MTP_long(values.backgroundEmojiId)));
+
+ if (values.statusChanged
+ && (values.statusId || peer->emojiStatusId())) {
+ peer->owner().emojiStatuses().set(
+ channel,
+ values.statusId,
+ values.statusUntil);
+ }
} else {
Unexpected("Invalid peer type in Set(colorIndex).");
}
@@ -481,13 +501,13 @@ void Set(
void Apply(
std::shared_ptr show,
not_null peer,
- uint8 colorIndex,
- DocumentId backgroundEmojiId,
+ SetValues values,
Fn close,
Fn cancel) {
const auto session = &peer->session();
- if (peer->colorIndex() == colorIndex
- && peer->backgroundEmojiId() == backgroundEmojiId) {
+ if (peer->colorIndex() == values.colorIndex
+ && peer->backgroundEmojiId() == values.backgroundEmojiId
+ && !values.statusChanged) {
close();
} else if (peer->isSelf() && !session->premium()) {
Settings::ShowPremiumPromoToast(
@@ -502,39 +522,45 @@ void Apply(
u"name_color"_q);
cancel();
} else if (peer->isSelf()) {
- Set(show, peer, colorIndex, backgroundEmojiId);
+ Set(show, peer, values);
close();
} else {
- session->api().request(MTPpremium_GetBoostsStatus(
- peer->input
- )).done([=](const MTPpremium_BoostsStatus &result) {
- const auto &data = result.data();
- const auto required = session->account().appConfig().get(
- "channel_color_level_min",
- 5);
- if (data.vlevel().v >= required) {
- Set(show, peer, colorIndex, backgroundEmojiId);
+ CheckBoostLevel(show, peer, [=](int level) {
+ const auto peerColors = &peer->session().api().peerColors();
+ const auto colorRequired = peerColors->requiredLevelFor(
+ peer->id,
+ values.colorIndex);
+ const auto iconRequired = values.backgroundEmojiId
+ ? session->account().appConfig().get(
+ "channel_bg_icon_level_min",
+ 5)
+ : 0;
+ const auto statusRequired = (values.statusChanged
+ && values.statusId)
+ ? session->account().appConfig().get(
+ "channel_emoji_status_level_min",
+ 8)
+ : 0;
+ const auto required = std::max({
+ colorRequired,
+ iconRequired,
+ statusRequired,
+ });
+ if (level >= required) {
+ Set(show, peer, values);
close();
- return;
+ return std::optional();
}
- const auto openStatistics = [=] {
- if (const auto controller = show->resolveWindow(
- ChatHelpers::WindowUsage::PremiumPromo)) {
- controller->showSection(Info::Boosts::Make(peer));
+ const auto reason = [&]() -> Ui::AskBoostReason {
+ if (level < statusRequired) {
+ return { Ui::AskBoostEmojiStatus{ statusRequired } };
+ } else if (level < iconRequired) {
+ return { Ui::AskBoostChannelColor{ iconRequired } };
}
- };
- auto counters = ParseBoostCounters(result);
- counters.mine = 0; // Don't show current level as just-reached.
- show->show(Box(Ui::AskBoostBox, Ui::AskBoostBoxData{
- .link = qs(data.vboost_url()),
- .boost = counters,
- .reason = { Ui::AskBoostChannelColor{ required } },
- }, openStatistics, nullptr));
- cancel();
- }).fail([=](const MTP::Error &error) {
- show->showToast(error.type());
- cancel();
- }).send();
+ return { Ui::AskBoostChannelColor{ colorRequired } };
+ }();
+ return std::make_optional(reason);
+ }, cancel);
}
}
@@ -672,15 +698,18 @@ int ColorSelector::resizeGetHeight(int newWidth) {
const auto right = Ui::CreateChild(raw);
right->show();
+ using namespace Info::Profile;
struct State {
- Info::Profile::EmojiStatusPanel panel;
+ EmojiStatusPanel panel;
std::unique_ptr emoji;
DocumentId emojiId = 0;
uint8 index = 0;
};
const auto state = right->lifetime().make_state();
- state->panel.backgroundEmojiChosen(
- ) | rpl::start_with_next(emojiIdChosen, raw->lifetime());
+ state->panel.someCustomChosen(
+ ) | rpl::start_with_next([=](EmojiStatusPanel::CustomChosen chosen) {
+ emojiIdChosen(chosen.id);
+ }, raw->lifetime());
std::move(colorIndexValue) | rpl::start_with_next([=](uint8 index) {
state->index = index;
@@ -748,7 +777,7 @@ int ColorSelector::resizeGetHeight(int newWidth) {
state->panel.show({
.controller = controller,
.button = right,
- .currentBackgroundEmojiId = state->emojiId,
+ .ensureAddedEmojiId = state->emojiId,
.customTextColor = customTextColor,
.backgroundEmojiMode = true,
});
@@ -758,6 +787,108 @@ int ColorSelector::resizeGetHeight(int newWidth) {
return result;
}
+[[nodiscard]] object_ptr CreateEmojiStatusButton(
+ not_null parent,
+ std::shared_ptr show,
+ rpl::producer statusIdValue,
+ Fn statusIdChosen) {
+ const auto &basicSt = st::settingsButtonNoIcon;
+ const auto ratio = style::DevicePixelRatio();
+ const auto added = st::normalFont->spacew;
+ const auto emojiSize = Data::FrameSizeFromTag({}) / ratio;
+ const auto noneWidth = added
+ + st::normalFont->width(tr::lng_settings_color_emoji_off(tr::now));
+ const auto emojiWidth = added + emojiSize;
+ const auto rightPadding = std::max(noneWidth, emojiWidth)
+ + basicSt.padding.right();
+ const auto st = parent->lifetime().make_state(
+ basicSt);
+ st->padding.setRight(rightPadding);
+ auto result = object_ptr(
+ parent,
+ tr::lng_edit_channel_status(),
+ *st);
+ const auto raw = result.data();
+
+ const auto right = Ui::CreateChild(raw);
+ right->show();
+
+ using namespace Info::Profile;
+ struct State {
+ EmojiStatusPanel panel;
+ std::unique_ptr emoji;
+ DocumentId statusId = 0;
+ };
+ const auto state = right->lifetime().make_state();
+ state->panel.someCustomChosen(
+ ) | rpl::start_with_next([=](EmojiStatusPanel::CustomChosen chosen) {
+ statusIdChosen(chosen.id, chosen.until);
+ }, raw->lifetime());
+
+ const auto session = &show->session();
+ std::move(statusIdValue) | rpl::start_with_next([=](DocumentId id) {
+ state->statusId = id;
+ state->emoji = id
+ ? session->data().customEmojiManager().create(
+ id,
+ [=] { right->update(); })
+ : nullptr;
+ right->resize(
+ (id ? emojiWidth : noneWidth) + added,
+ right->height());
+ right->update();
+ }, right->lifetime());
+
+ rpl::combine(
+ raw->sizeValue(),
+ right->widthValue()
+ ) | rpl::start_with_next([=](QSize outer, int width) {
+ right->resize(width, outer.height());
+ const auto skip = st::settingsButton.padding.right();
+ right->moveToRight(skip - added, 0, outer.width());
+ }, right->lifetime());
+
+ right->paintRequest(
+ ) | rpl::start_with_next([=] {
+ if (state->panel.paintBadgeFrame(right)) {
+ return;
+ }
+ auto p = QPainter(right);
+ const auto height = right->height();
+ if (state->emoji) {
+ state->emoji->paint(p, {
+ .textColor = anim::color(
+ st::stickerPanPremium1,
+ st::stickerPanPremium2,
+ 0.5),
+ .position = QPoint(added, (height - emojiSize) / 2),
+ });
+ } else {
+ const auto &font = st::normalFont;
+ p.setFont(font);
+ p.setPen(st::windowActiveTextFg);
+ p.drawText(
+ QPoint(added, (height - font->height) / 2 + font->ascent),
+ tr::lng_settings_color_emoji_off(tr::now));
+ }
+ }, right->lifetime());
+
+ raw->setClickedCallback([=] {
+ const auto controller = show->resolveWindow(
+ ChatHelpers::WindowUsage::PremiumPromo);
+ if (controller) {
+ state->panel.show({
+ .controller = controller,
+ .button = right,
+ .ensureAddedEmojiId = state->statusId,
+ .channelStatusMode = true,
+ });
+ }
+ });
+
+ return result;
+}
+
} // namespace
void EditPeerColorBox(
@@ -772,12 +903,16 @@ void EditPeerColorBox(
struct State {
rpl::variable index;
rpl::variable emojiId;
+ rpl::variable statusId;
+ TimeId statusUntil = 0;
+ bool statusChanged = false;
bool changing = false;
bool applying = false;
};
const auto state = box->lifetime().make_state();
state->index = peer->colorIndex();
state->emojiId = peer->backgroundEmojiId();
+ state->statusId = peer->emojiStatusId();
box->addRow(object_ptr(
box,
@@ -820,14 +955,61 @@ void EditPeerColorBox(
? tr::lng_settings_color_emoji_about()
: tr::lng_settings_color_emoji_about_channel());
+ if (const auto channel = peer->asChannel()) {
+ Ui::AddSkip(container, st::settingsColorSampleSkip);
+ container->add(object_ptr(
+ container,
+ tr::lng_edit_channel_wallpaper(),
+ st::settingsButtonNoIcon)
+ )->setClickedCallback([=] {
+ const auto usage = ChatHelpers::WindowUsage::PremiumPromo;
+ if (const auto strong = show->resolveWindow(usage)) {
+ show->show(Box(strong, channel));
+ }
+ });
+
+ Ui::AddSkip(container, st::settingsColorSampleSkip);
+ Ui::AddDividerText(
+ container,
+ tr::lng_edit_channel_wallpaper_about());
+
+ // Preload exceptions list.
+ const auto peerPhoto = &channel->session().api().peerPhoto();
+ [[maybe_unused]] auto list = peerPhoto->emojiListValue(
+ Api::PeerPhoto::EmojiListType::NoChannelStatus
+ );
+
+ const auto statuses = &channel->owner().emojiStatuses();
+ statuses->refreshChannelDefault();
+ statuses->refreshChannelColored();
+
+ Ui::AddSkip(container, st::settingsColorSampleSkip);
+ container->add(CreateEmojiStatusButton(
+ container,
+ show,
+ state->statusId.value(),
+ [=](DocumentId id, TimeId until) {
+ state->statusId = id;
+ state->statusUntil = until;
+ state->statusChanged = true;
+ }));
+
+ Ui::AddSkip(container, st::settingsColorSampleSkip);
+ Ui::AddDividerText(container, tr::lng_edit_channel_status_about());
+ }
+
box->addButton(tr::lng_settings_apply(), [=] {
if (state->applying) {
return;
}
state->applying = true;
- const auto index = state->index.current();
- const auto emojiId = state->emojiId.current();
- Apply(show, peer, index, emojiId, crl::guard(box, [=] {
+ Apply(show, peer, {
+ state->index.current(),
+ state->emojiId.current(),
+ state->statusId.current(),
+ state->statusUntil,
+ state->statusChanged,
+ }, crl::guard(box, [=] {
box->closeBox();
}), crl::guard(box, [=] {
state->applying = false;
@@ -842,11 +1024,12 @@ void AddPeerColorButton(
not_null container,
std::shared_ptr show,
not_null peer) {
+ auto label = peer->isSelf()
+ ? tr::lng_settings_theme_name_color()
+ : tr::lng_edit_channel_color();
const auto button = AddButtonWithIcon(
container,
- (peer->isSelf()
- ? tr::lng_settings_theme_name_color()
- : tr::lng_edit_channel_color()),
+ rpl::duplicate(label),
st::settingsColorButton,
{ &st::menuIconChangeColors });
@@ -873,7 +1056,7 @@ void AddPeerColorButton(
rpl::combine(
button->widthValue(),
- tr::lng_settings_theme_name_color(),
+ rpl::duplicate(label),
rpl::duplicate(colorIndexValue)
) | rpl::start_with_next([=](
int width,
@@ -920,3 +1103,39 @@ void AddPeerColorButton(
show->show(Box(EditPeerColorBox, show, peer, style, theme));
});
}
+
+void CheckBoostLevel(
+ std::shared_ptr show,
+ not_null peer,
+ Fn(int level)> askMore,
+ Fn cancel) {
+ peer->session().api().request(MTPpremium_GetBoostsStatus(
+ peer->input
+ )).done([=](const MTPpremium_BoostsStatus &result) {
+ const auto &data = result.data();
+ if (const auto channel = peer->asChannel()) {
+ channel->updateLevelHint(data.vlevel().v);
+ }
+ const auto reason = askMore(data.vlevel().v);
+ if (!reason) {
+ return;
+ }
+ const auto openStatistics = [=] {
+ if (const auto controller = show->resolveWindow(
+ ChatHelpers::WindowUsage::PremiumPromo)) {
+ controller->showSection(Info::Boosts::Make(peer));
+ }
+ };
+ auto counters = ParseBoostCounters(result);
+ counters.mine = 0; // Don't show current level as just-reached.
+ show->show(Box(Ui::AskBoostBox, Ui::AskBoostBoxData{
+ .link = qs(data.vboost_url()),
+ .boost = counters,
+ .reason = *reason,
+ }, openStatistics, nullptr));
+ cancel();
+ }).fail([=](const MTP::Error &error) {
+ show->showToast(error.type());
+ cancel();
+ }).send();
+}
diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.h b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.h
index 68aecaab25684..6b72844494822 100644
--- a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.h
+++ b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.h
@@ -16,6 +16,7 @@ class GenericBox;
class ChatStyle;
class ChatTheme;
class VerticalLayout;
+struct AskBoostReason;
} // namespace Ui
void EditPeerColorBox(
@@ -29,3 +30,9 @@ void AddPeerColorButton(
not_null container,
std::shared_ptr show,
not_null peer);
+
+void CheckBoostLevel(
+ std::shared_ptr show,
+ not_null peer,
+ Fn(int level)> askMore,
+ Fn cancel);
diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp
index 7b1010272e6ad..fba0c73034105 100644
--- a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp
+++ b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp
@@ -1294,6 +1294,9 @@ void Controller::editReactions() {
_peer->input
)).done([=](const MTPpremium_BoostsStatus &result) {
_controls.levelRequested = false;
+ if (const auto channel = _peer->asChannel()) {
+ channel->updateLevelHint(result.data().vlevel().v);
+ }
const auto link = qs(result.data().vboost_url());
const auto weak = base::make_weak(_navigation->parentController());
auto counters = ParseBoostCounters(result);
diff --git a/Telegram/SourceFiles/boxes/premium_limits_box.cpp b/Telegram/SourceFiles/boxes/premium_limits_box.cpp
index 64452a906e599..60eb5ecde3686 100644
--- a/Telegram/SourceFiles/boxes/premium_limits_box.cpp
+++ b/Telegram/SourceFiles/boxes/premium_limits_box.cpp
@@ -24,6 +24,7 @@ For license and copyright information please follow this link:
#include "data/data_user.h"
#include "data/data_channel.h"
#include "data/data_forum.h"
+#include "data/data_saved_messages.h"
#include "data/data_session.h"
#include "data/data_folder.h"
#include "data/data_premium_limits.h"
@@ -882,6 +883,18 @@ void PinsLimitBox(
limits.dialogsPinnedPremium(),
PinsCount(session->data().chatsList()));
}
+void SublistsPinsLimitBox(
+ not_null box,
+ not_null session) {
+ const auto limits = Data::PremiumLimits(session);
+ SimplePinsLimitBox(
+ box,
+ session,
+ "saved_dialog_pinned",
+ limits.savedSublistsPinnedDefault(),
+ limits.savedSublistsPinnedPremium(),
+ PinsCount(session->data().savedMessages().chatsList()));
+}
void ForumPinsLimitBox(
not_null box,
diff --git a/Telegram/SourceFiles/boxes/premium_limits_box.h b/Telegram/SourceFiles/boxes/premium_limits_box.h
index 5a4fa8a0a88b1..8640dc235bf8b 100644
--- a/Telegram/SourceFiles/boxes/premium_limits_box.h
+++ b/Telegram/SourceFiles/boxes/premium_limits_box.h
@@ -60,6 +60,9 @@ void PinsLimitBox(
void ForumPinsLimitBox(
not_null box,
not_null forum);
+void SublistsPinsLimitBox(
+ not_null box,
+ not_null session);
void CaptionLimitBox(
not_null box,
not_null session,
diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.cpp b/Telegram/SourceFiles/boxes/premium_preview_box.cpp
index 6cdb9a10aa671..06835d60372d8 100644
--- a/Telegram/SourceFiles/boxes/premium_preview_box.cpp
+++ b/Telegram/SourceFiles/boxes/premium_preview_box.cpp
@@ -51,8 +51,6 @@ For license and copyright information please follow this link:
namespace {
constexpr auto kPremiumShift = 21. / 240;
-constexpr auto kReactionsPerRow = 5;
-constexpr auto kDisabledOpacity = 0.5;
constexpr auto kToggleStickerTimeout = 2 * crl::time(1000);
constexpr auto kStarOpacityOff = 0.1;
constexpr auto kStarOpacityOn = 1.;
@@ -66,6 +64,7 @@ struct Descriptor {
bool fromSettings = false;
Fn hiddenCallback;
Fn)> shownCallback;
+ bool hideSubscriptionButton = false;
};
bool operator==(const Descriptor &a, const Descriptor &b) {
@@ -1025,7 +1024,8 @@ void PreviewBox(
state->preload();
}
};
- if (descriptor.fromSettings && show->session().premium()) {
+ if ((descriptor.fromSettings && show->session().premium())
+ || descriptor.hideSubscriptionButton) {
box->setShowFinishedCallback(showFinished);
box->addButton(tr::lng_close(), [=] { box->closeBox(); });
} else {
@@ -1151,7 +1151,7 @@ void DecorateListPromoBox(
box->boxClosing() | rpl::start_with_next(hidden, box->lifetime());
}
- if (session->premium()) {
+ if (session->premium() || descriptor.hideSubscriptionButton) {
box->addButton(tr::lng_close(), [=] {
box->closeBox();
});
@@ -1286,10 +1286,12 @@ void ShowPremiumPreviewBox(
void ShowPremiumPreviewBox(
std::shared_ptr show,
PremiumPreview section,
- Fn)> shown) {
+ Fn)> shown,
+ bool hideSubscriptionButton) {
Show(std::move(show), Descriptor{
.section = section,
.shownCallback = std::move(shown),
+ .hideSubscriptionButton = hideSubscriptionButton,
});
}
diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.h b/Telegram/SourceFiles/boxes/premium_preview_box.h
index 35c05571778b2..60c5699ca1ac0 100644
--- a/Telegram/SourceFiles/boxes/premium_preview_box.h
+++ b/Telegram/SourceFiles/boxes/premium_preview_box.h
@@ -73,7 +73,8 @@ void ShowPremiumPreviewBox(
void ShowPremiumPreviewBox(
std::shared_ptr show,
PremiumPreview section,
- Fn)> shown = nullptr);
+ Fn)> shown = nullptr,
+ bool hideSubscriptionButton = false);
void ShowPremiumPreviewToBuy(
not_null controller,
diff --git a/Telegram/SourceFiles/boxes/sessions_box.cpp b/Telegram/SourceFiles/boxes/sessions_box.cpp
index ec428042af606..4328fc4431d4f 100644
--- a/Telegram/SourceFiles/boxes/sessions_box.cpp
+++ b/Telegram/SourceFiles/boxes/sessions_box.cpp
@@ -211,7 +211,7 @@ void RenameBox(not_null box) {
return Type::Other;
} else if (const auto browser = detectBrowser()) {
return *browser;
- } else if (device.contains("iphone")) {
+ } else if (device.contains("iphone")) {
return Type::iPhone;
} else if (device.contains("ipad")) {
return Type::iPad;
@@ -221,9 +221,9 @@ void RenameBox(not_null box) {
return *desktop;
} else if (platform.contains("android") || system.contains("android")) {
return Type::Android;
- } else if (platform.contains("ios") || system.contains("ios")) {
+ } else if (platform.contains("ios") || system.contains("ios")) {
return Type::iPhone;
- }
+ }
return Type::Other;
}
diff --git a/Telegram/SourceFiles/boxes/share_box.cpp b/Telegram/SourceFiles/boxes/share_box.cpp
index 1ffcc1ddfdb89..d1b3dbb3775d2 100644
--- a/Telegram/SourceFiles/boxes/share_box.cpp
+++ b/Telegram/SourceFiles/boxes/share_box.cpp
@@ -848,7 +848,7 @@ void ShareBox::Inner::loadProfilePhotos(int yFrom) {
if (!_chatsIndexed->empty()) {
const auto index = yFrom / _rowHeight;
auto i = _chatsIndexed->begin()
- + std::min(index, _chatsIndexed->size());;
+ + std::min(index, _chatsIndexed->size());
for (auto end = _chatsIndexed->cend(); i != end; ++i) {
if (((*i)->index() * _rowHeight) >= yTo) {
break;
diff --git a/Telegram/SourceFiles/boxes/translate_box.cpp b/Telegram/SourceFiles/boxes/translate_box.cpp
index e0a760350d413..39380bd3ed500 100644
--- a/Telegram/SourceFiles/boxes/translate_box.cpp
+++ b/Telegram/SourceFiles/boxes/translate_box.cpp
@@ -305,7 +305,7 @@ bool SkipTranslate(TextWithEntities textWithEntities) {
const auto skip = Core::App().settings().skipTranslationLanguages();
return result.known() && ranges::contains(skip, result);
#else
- return false;
+ return false;
#endif
}
diff --git a/Telegram/SourceFiles/calls/calls_box_controller.cpp b/Telegram/SourceFiles/calls/calls_box_controller.cpp
index 66a4e01f5b4a2..9eb53580f6235 100644
--- a/Telegram/SourceFiles/calls/calls_box_controller.cpp
+++ b/Telegram/SourceFiles/calls/calls_box_controller.cpp
@@ -520,6 +520,7 @@ void BoxController::loadMoreRows() {
MTP_inputPeerEmpty(),
MTP_string(), // q
MTP_inputPeerEmpty(),
+ MTPInputPeer(), // saved_peer_id
MTPint(), // top_msg_id
MTP_inputMessagesFilterPhoneCalls(MTP_flags(0)),
MTP_int(0), // min_date
diff --git a/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.cpp b/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.cpp
index 26a9cbed0df16..946c6dca8e9d3 100644
--- a/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.cpp
+++ b/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.cpp
@@ -321,9 +321,6 @@ void Viewport::RendererGL::init(
_frameBuffer->bind();
_frameBuffer->allocate(kValues * sizeof(GLfloat));
_downscaleProgram.yuv420.emplace();
- const auto downscaleVertexSource = VertexShader({
- VertexPassTextureCoord(),
- });
_downscaleVertexShader = LinkProgram(
&*_downscaleProgram.yuv420,
VertexShader({
diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp
index 9ae411d24b03e..2d27f1db47474 100644
--- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp
+++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp
@@ -7,6 +7,8 @@ For license and copyright information please follow this link:
*/
#include "chat_helpers/emoji_list_widget.h"
+#include "api/api_peer_photo.h"
+#include "apiwrap.h"
#include "base/unixtime.h"
#include "ui/boxes/confirm_box.h"
#include "ui/controls/tabbed_search.h"
@@ -477,10 +479,23 @@ EmojiListWidget::EmojiListWidget(
setAttribute(Qt::WA_OpaquePaintEvent);
}
- if (_mode != Mode::RecentReactions && _mode != Mode::BackgroundEmoji) {
+ if (_mode != Mode::RecentReactions
+ && _mode != Mode::BackgroundEmoji
+ && _mode != Mode::ChannelStatus) {
setupSearch();
}
+ if (_mode == Mode::ChannelStatus) {
+ session().api().peerPhoto().emojiListValue(
+ Api::PeerPhoto::EmojiListType::NoChannelStatus
+ ) | rpl::start_with_next([=](const std::vector &list) {
+ _restrictedCustomList = { begin(list), end(list) };
+ if (!_custom.empty()) {
+ refreshCustom();
+ }
+ }, lifetime());
+ }
+
_customSingleSize = Data::FrameSizeFromTag(
Data::CustomEmojiManager::SizeTag::Large
) / style::DevicePixelRatio();
@@ -1034,7 +1049,9 @@ void EmojiListWidget::fillRecentFrom(const std::vector &list) {
if (!id && _mode == Mode::EmojiStatus) {
const auto star = QString::fromUtf8("\xe2\xad\x90\xef\xb8\x8f");
_recent.push_back({ .id = { Ui::Emoji::Find(star) } });
- } else if (!id && _mode == Mode::BackgroundEmoji) {
+ } else if (!id
+ && (_mode == Mode::BackgroundEmoji
+ || _mode == Mode::ChannelStatus)) {
const auto fakeId = DocumentId(5246772116543512028ULL);
const auto no = QString::fromUtf8("\xe2\x9b\x94\xef\xb8\x8f");
_recent.push_back({
@@ -1070,7 +1087,7 @@ base::unique_qptr EmojiListWidget::fillContextMenu(
: st::defaultPopupMenu));
if (_mode == Mode::Full) {
fillRecentMenu(menu, section, index);
- } else if (_mode == Mode::EmojiStatus) {
+ } else if (_mode == Mode::EmojiStatus || _mode == Mode::ChannelStatus) {
fillEmojiStatusMenu(menu, section, index);
}
if (menu->empty()) {
@@ -1205,7 +1222,7 @@ void EmojiListWidget::validateEmojiPaintContext(
auto value = Ui::Text::CustomEmojiPaintContext{
.textColor = (_customTextColor
? _customTextColor()
- : (_mode == Mode::EmojiStatus)
+ : (_mode == Mode::EmojiStatus || _mode == Mode::ChannelStatus)
? anim::color(
st::stickerPanPremium1,
st::stickerPanPremium2,
@@ -1402,6 +1419,10 @@ void EmojiListWidget::drawRecent(
_emojiPaintContext->position = position
+ _innerPosition
+ _customPosition;
+ if (_mode == Mode::ChannelStatus) {
+ _emojiPaintContext->internal.forceFirstFrame
+ = (recent.id == _recent.front().id);
+ }
custom->paint(p, *_emojiPaintContext);
} else if (const auto emoji = std::get_if(&recent.id.data)) {
if (_mode == Mode::EmojiStatus) {
@@ -1642,6 +1663,7 @@ void EmojiListWidget::mouseReleaseEvent(QMouseEvent *e) {
Settings::ShowPremium(resolved, u"infinite_reactions"_q);
break;
case Mode::EmojiStatus:
+ case Mode::ChannelStatus:
Settings::ShowPremium(resolved, u"emoji_status"_q);
break;
case Mode::TopicIcon:
@@ -2018,8 +2040,9 @@ void EmojiListWidget::refreshCustom() {
auto it = sets.find(setId);
if (it == sets.cend()
|| it->second->stickers.isEmpty()
- || (_mode == Mode::BackgroundEmoji
- && !it->second->textColor())) {
+ || (_mode == Mode::BackgroundEmoji && !it->second->textColor())
+ || (_mode == Mode::ChannelStatus
+ && !it->second->channelStatus())) {
return;
}
const auto canRemove = !!(it->second->flags
@@ -2070,7 +2093,9 @@ void EmojiListWidget::refreshCustom() {
auto set = std::vector();
set.reserve(list.size());
for (const auto document : list) {
- if (const auto sticker = document->sticker()) {
+ if (_restrictedCustomList.contains(document->id)) {
+ continue;
+ } else if (const auto sticker = document->sticker()) {
set.push_back({
.custom = resolveCustomEmoji(document, setId),
.document = document,
diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h
index 5735d7a55d381..c2a8ef3ed0287 100644
--- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h
+++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h
@@ -71,6 +71,7 @@ enum class EmojiListMode {
Full,
TopicIcon,
EmojiStatus,
+ ChannelStatus,
FullReactions,
RecentReactions,
UserpicBuilder,
@@ -384,6 +385,7 @@ class EmojiListWidget final
bool _grabbingChosen = false;
QVector