diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt
index d2c6768fd..f9f3de29c 100644
--- a/Telegram/CMakeLists.txt
+++ b/Telegram/CMakeLists.txt
@@ -265,6 +265,8 @@ PRIVATE
boxes/peers/edit_participant_box.h
boxes/peers/edit_participants_box.cpp
boxes/peers/edit_participants_box.h
+ boxes/peers/edit_peer_color_box.cpp
+ boxes/peers/edit_peer_color_box.h
boxes/peers/edit_peer_common.h
boxes/peers/edit_peer_info_box.cpp
boxes/peers/edit_peer_info_box.h
@@ -718,6 +720,8 @@ PRIVATE
history/view/controls/history_view_compose_controls.h
history/view/controls/history_view_compose_search.cpp
history/view/controls/history_view_compose_search.h
+ history/view/controls/history_view_draft_options.cpp
+ history/view/controls/history_view_draft_options.h
history/view/controls/history_view_forward_panel.cpp
history/view/controls/history_view_forward_panel.h
history/view/controls/history_view_ttl_button.cpp
@@ -726,6 +730,8 @@ PRIVATE
history/view/controls/history_view_voice_record_bar.h
history/view/controls/history_view_voice_record_button.cpp
history/view/controls/history_view_voice_record_button.h
+ history/view/controls/history_view_webpage_processor.cpp
+ history/view/controls/history_view_webpage_processor.h
history/view/media/history_view_call.cpp
history/view/media/history_view_call.h
history/view/media/history_view_contact.cpp
@@ -744,6 +750,8 @@ PRIVATE
history/view/media/history_view_game.h
history/view/media/history_view_gif.cpp
history/view/media/history_view_gif.h
+ history/view/media/history_view_giveaway.cpp
+ history/view/media/history_view_giveaway.h
history/view/media/history_view_invoice.cpp
history/view/media/history_view_invoice.h
history/view/media/history_view_large_emoji.cpp
@@ -883,20 +891,6 @@ PRIVATE
history/history_view_highlight_manager.h
history/history_widget.cpp
history/history_widget.h
- info/info_content_widget.cpp
- info/info_content_widget.h
- info/info_controller.cpp
- info/info_controller.h
- info/info_layer_widget.cpp
- info/info_layer_widget.h
- info/info_memento.cpp
- info/info_memento.h
- info/info_section_widget.cpp
- info/info_section_widget.h
- info/info_top_bar.cpp
- info/info_top_bar.h
- info/info_wrap_widget.cpp
- info/info_wrap_widget.h
info/boosts/info_boosts_inner_widget.cpp
info/boosts/info_boosts_inner_widget.h
info/boosts/info_boosts_widget.cpp
@@ -983,6 +977,20 @@ PRIVATE
info/userpic/info_userpic_emoji_builder_preview.h
info/userpic/info_userpic_emoji_builder_widget.cpp
info/userpic/info_userpic_emoji_builder_widget.h
+ info/info_content_widget.cpp
+ info/info_content_widget.h
+ info/info_controller.cpp
+ info/info_controller.h
+ info/info_layer_widget.cpp
+ info/info_layer_widget.h
+ info/info_memento.cpp
+ info/info_memento.h
+ info/info_section_widget.cpp
+ info/info_section_widget.h
+ info/info_top_bar.cpp
+ info/info_top_bar.h
+ info/info_wrap_widget.cpp
+ info/info_wrap_widget.h
inline_bots/bot_attach_web_view.cpp
inline_bots/bot_attach_web_view.h
inline_bots/inline_bot_layout_internal.cpp
diff --git a/Telegram/Resources/icons/chat/mini_quote.png b/Telegram/Resources/icons/chat/mini_quote.png
index 08a844820..7c021f6ef 100644
Binary files a/Telegram/Resources/icons/chat/mini_quote.png and b/Telegram/Resources/icons/chat/mini_quote.png differ
diff --git a/Telegram/Resources/icons/chat/mini_quote@2x.png b/Telegram/Resources/icons/chat/mini_quote@2x.png
index 8068b2208..f4a9b43f5 100644
Binary files a/Telegram/Resources/icons/chat/mini_quote@2x.png and b/Telegram/Resources/icons/chat/mini_quote@2x.png differ
diff --git a/Telegram/Resources/icons/chat/mini_quote@3x.png b/Telegram/Resources/icons/chat/mini_quote@3x.png
index 2399ff033..2c572bd02 100644
Binary files a/Telegram/Resources/icons/chat/mini_quote@3x.png and b/Telegram/Resources/icons/chat/mini_quote@3x.png differ
diff --git a/Telegram/Resources/icons/menu/boosts.png b/Telegram/Resources/icons/menu/boosts.png
new file mode 100644
index 000000000..64a5ae3b9
Binary files /dev/null and b/Telegram/Resources/icons/menu/boosts.png differ
diff --git a/Telegram/Resources/icons/menu/boosts@2x.png b/Telegram/Resources/icons/menu/boosts@2x.png
new file mode 100644
index 000000000..0478fa660
Binary files /dev/null and b/Telegram/Resources/icons/menu/boosts@2x.png differ
diff --git a/Telegram/Resources/icons/menu/boosts@3x.png b/Telegram/Resources/icons/menu/boosts@3x.png
new file mode 100644
index 000000000..33590248c
Binary files /dev/null and b/Telegram/Resources/icons/menu/boosts@3x.png differ
diff --git a/Telegram/Resources/icons/menu/link_above.png b/Telegram/Resources/icons/menu/link_above.png
new file mode 100644
index 000000000..1f8881dfe
Binary files /dev/null and b/Telegram/Resources/icons/menu/link_above.png differ
diff --git a/Telegram/Resources/icons/menu/link_above@2x.png b/Telegram/Resources/icons/menu/link_above@2x.png
new file mode 100644
index 000000000..61c53d18e
Binary files /dev/null and b/Telegram/Resources/icons/menu/link_above@2x.png differ
diff --git a/Telegram/Resources/icons/menu/link_above@3x.png b/Telegram/Resources/icons/menu/link_above@3x.png
new file mode 100644
index 000000000..be58d0df3
Binary files /dev/null and b/Telegram/Resources/icons/menu/link_above@3x.png differ
diff --git a/Telegram/Resources/icons/menu/link_below.png b/Telegram/Resources/icons/menu/link_below.png
new file mode 100644
index 000000000..315150fa6
Binary files /dev/null and b/Telegram/Resources/icons/menu/link_below.png differ
diff --git a/Telegram/Resources/icons/menu/link_below@2x.png b/Telegram/Resources/icons/menu/link_below@2x.png
new file mode 100644
index 000000000..f30fe99b4
Binary files /dev/null and b/Telegram/Resources/icons/menu/link_below@2x.png differ
diff --git a/Telegram/Resources/icons/menu/link_below@3x.png b/Telegram/Resources/icons/menu/link_below@3x.png
new file mode 100644
index 000000000..39f6fbf1a
Binary files /dev/null and b/Telegram/Resources/icons/menu/link_below@3x.png differ
diff --git a/Telegram/Resources/icons/menu/link_enlarge.png b/Telegram/Resources/icons/menu/link_enlarge.png
new file mode 100644
index 000000000..4750b24d6
Binary files /dev/null and b/Telegram/Resources/icons/menu/link_enlarge.png differ
diff --git a/Telegram/Resources/icons/menu/link_enlarge@2x.png b/Telegram/Resources/icons/menu/link_enlarge@2x.png
new file mode 100644
index 000000000..ff98f6dc4
Binary files /dev/null and b/Telegram/Resources/icons/menu/link_enlarge@2x.png differ
diff --git a/Telegram/Resources/icons/menu/link_enlarge@3x.png b/Telegram/Resources/icons/menu/link_enlarge@3x.png
new file mode 100644
index 000000000..78b3f137b
Binary files /dev/null and b/Telegram/Resources/icons/menu/link_enlarge@3x.png differ
diff --git a/Telegram/Resources/icons/menu/link_shrink.png b/Telegram/Resources/icons/menu/link_shrink.png
new file mode 100644
index 000000000..48bf8f035
Binary files /dev/null and b/Telegram/Resources/icons/menu/link_shrink.png differ
diff --git a/Telegram/Resources/icons/menu/link_shrink@2x.png b/Telegram/Resources/icons/menu/link_shrink@2x.png
new file mode 100644
index 000000000..289e1f69d
Binary files /dev/null and b/Telegram/Resources/icons/menu/link_shrink@2x.png differ
diff --git a/Telegram/Resources/icons/menu/link_shrink@3x.png b/Telegram/Resources/icons/menu/link_shrink@3x.png
new file mode 100644
index 000000000..dfb66a523
Binary files /dev/null and b/Telegram/Resources/icons/menu/link_shrink@3x.png differ
diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 28906fea5..a35291eaa 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -129,6 +129,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_reconnecting#other" = "Reconnect in {count} s...";
"lng_reconnecting_try_now" = "Try now";
+"lng_code_block_header_copy" = "copy";
+
"lng_status_service_notifications" = "service notifications";
"lng_status_support" = "support";
"lng_status_bot" = "bot";
@@ -402,6 +404,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_dlg_search_from" = "From: {user}";
"lng_settings_save" = "Save";
+"lng_settings_apply" = "Apply";
"lng_username_title" = "Username";
"lng_username_description1" = "You can choose a username on Telegram. If you do, other people will be able to find you by this username and contact you without knowing your phone number.";
@@ -770,11 +773,33 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_clear_payment_info_clear" = "Clear";
"lng_clear_payment_info_confirm" = "Delete your shipping info and instruct all payment providers to remove your saved credit cards? Note that Telegram never stores your credit card data.";
+"lng_settings_theme_settings" = "Theme settings";
+"lng_settings_theme_name_color" = "Your name color";
"lng_settings_auto_night_mode" = "Auto-Night mode";
-"lng_settings_auto_night_enabled" = "Match the system settings";
+"lng_settings_auto_night_mode_off" = "Off";
+"lng_settings_auto_night_mode_on" = "System";
"lng_settings_auto_night_warning" = "You have enabled auto-night mode. If you want to change the dark mode settings, you'll need to disable it first.";
"lng_settings_auto_night_disable" = "Disable";
+"lng_settings_color_title" = "Color preview";
+"lng_settings_color_reply" = "Reply to your message";
+"lng_settings_color_reply_channel" = "Reply to your channel message";
+"lng_settings_color_text" = "Your name and replies to your messages will be shown in the selected color.";
+"lng_settings_color_text_channel" = "The name of the channel and replies to its messages will be shown in the selected color.";
+"lng_settings_color_link_name" = "Telegram";
+"lng_settings_color_link_title" = "Link Preview";
+"lng_settings_color_link_description" = "Your selected color will also tint the link preview.";
+"lng_settings_color_about" = "You can choose a color to tint your name, the links you send, and replies to your messages.";
+"lng_settings_color_about_channel" = "You can choose a color to tint your channel's name, the links it sends, and replies to its messages.";
+"lng_settings_color_emoji" = "Add icons to replies";
+"lng_settings_color_emoji_remove" = "Remove icon";
+"lng_settings_color_emoji_off" = "Off";
+"lng_settings_color_emoji_about" = "Make replies to your messages stand out by adding custom patterns to them.";
+"lng_settings_color_emoji_about_channel" = "Make replies to your channel's messages stand out by adding custom patterns to them.";
+"lng_settings_color_subscribe" = "Subscribe to {link} to choose a custom color for your name.";
+"lng_settings_color_changed" = "Your name color has been updated!";
+"lng_settings_color_changed_channel" = "Your channel color has been updated!";
+
"lng_suggest_hide_new_title" = "Hide new chats?";
"lng_suggest_hide_new_about" = "You are receiving lots of new chats from users who are not in your Contact List.\n\nDo you want to have such chats **automatically muted** and **archived**?";
"lng_suggest_hide_new_to_settings" = "Go to Settings";
@@ -802,7 +827,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_background_text1" = "Ah, you kids today with techno music! You should enjoy the classics, like Hasselhoff!";
"lng_background_text2" = "I can't even take you seriously right now.";
"lng_background_bad_link" = "This background link appears to be invalid.";
-"lng_background_apply" = "Apply";
"lng_background_share" = "Share";
"lng_background_link_copied" = "Link copied to clipboard";
"lng_background_blur" = "Blurred";
@@ -1142,6 +1166,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_mute_box_title" = "Mute notifications for...";
"lng_preview_loading" = "Getting Link Info...";
+"lng_preview_cant" = "Could not generate preview for this link.";
"lng_profile_settings_section" = "Settings";
"lng_profile_bot_settings" = "Bot Settings";
@@ -1640,6 +1665,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_action_story_mention_button" = "View Story";
"lng_action_story_mention_me_unavailable" = "The story where you mentioned {user} is no longer available.";
"lng_action_story_mention_unavailable" = "The story where {user} mentioned you is no longer available.";
+"lng_action_giveaway_started" = "{from} just started a giveaway of Telegram Premium subscriptions to its followers.";
"lng_premium_gift_duration_months#one" = "for {count} month";
"lng_premium_gift_duration_months#other" = "for {count} months";
@@ -2034,6 +2060,129 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_boost_now_instead" = "You currently boost {channel}. Do you want to boost {other} instead?";
"lng_boost_now_replace" = "Replace";
+"lng_boost_channel_title_color" = "Enable colors";
+"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_ask" = "Ask your **Premium** subscribers to boost your channel with this link:";
+"lng_boost_channel_ask_button" = "Copy Link";
+"lng_boost_channel_or" = "or";
+"lng_boost_channel_gifting" = "Boost your channel by gifting your subscribers Telegram Premium. {link}";
+"lng_boost_channel_gifting_link" = "Get boosts >";
+
+"lng_giveaway_new_title" = "Boosts via Gifts";
+"lng_giveaway_new_about" = "Get more boosts for your channel by gifting Premium to your subscribers.";
+"lng_giveaway_create_option" = "Create Giveaway";
+"lng_giveaway_create_subtitle" = "winners are chosen randomly";
+"lng_giveaway_award_option" = "Award Specific Users";
+"lng_giveaway_award_subtitle" = "Select recipients >";
+"lng_giveaway_award_chosen#one" = "{count} recipient >";
+"lng_giveaway_award_chosen#other" = "{count} recipients >";
+"lng_giveaway_quantity_title" = "Quantity of prizes / boosts";
+"lng_giveaway_quantity#one" = "{count} Subscription / Boost";
+"lng_giveaway_quantity#other" = "{count} Subscriptions / Boosts";
+"lng_giveaway_quantity_about" = "Choose how many Premium subscriptions to give away and boosts to receive.";
+"lng_giveaway_channels_title" = "Channels included in the giveaway";
+"lng_giveaway_channels_this#one" = "this channel will receive {count} boost";
+"lng_giveaway_channels_this#other" = "this channel will receive {count} boosts";
+"lng_giveaway_channels_add" = "Add Channel";
+"lng_giveaway_channels_about" = "Choose the channels the users need to join to take part in the giveaway.";
+"lng_giveaway_users_title" = "Users eligible for the giveaway";
+"lng_giveaway_users_all" = "All subscribers";
+"lng_giveaway_users_new" = "Only new subscribers";
+"lng_giveaway_users_about" = "Choose if you want to limit the giveaway only to the newly joined subscribers.";
+"lng_giveaway_start" = "Start Giveaway";
+"lng_giveaway_award" = "Gift Premium";
+"lng_giveaway_date_title" = "Date when giveaway ends";
+"lng_giveaway_date" = "Date and Time";
+"lng_giveaway_date_about#one" = "Choose when {count} subscriber of your channel will be randomly selected to receive Telegram Premium.";
+"lng_giveaway_date_about#other" = "Choose when {count} subscribers of your channel will be randomly selected to receive Telegram Premium.";
+"lng_giveaway_duration_title#one" = "Duration of Premium subscription";
+"lng_giveaway_duration_title#other" = "Duration of Premium subscriptions";
+"lng_giveaway_duration_price" = "{price} x {amount}";
+"lng_giveaway_duration_about" = "You can review the list of features and terms of use for Telegram Premium {link}.";
+"lng_giveaway_duration_about_link" = "here";
+"lng_giveaway_date_select" = "Select Date and Time";
+"lng_giveaway_date_confirm" = "Confirm";
+"lng_giveaway_channels_select#one" = "Select up to {count} channel";
+"lng_giveaway_channels_select#other" = "Select up to {count} channels";
+"lng_giveaway_recipients_save" = "Save Recipients";
+"lng_giveaway_recipients_deselect" = "Deselect All";
+
+"lng_prize_title" = "Congratulations!";
+"lng_prize_about" = "You won a prize in a giveaway organized by {channel}.";
+"lng_prize_duration" = "Your prize is a **Telegram Premium** subscription {duration}.";
+"lng_prize_gift_about" = "You've received a gift from {channel}.";
+"lng_prize_gift_duration" = "Your gift is a **Telegram Premium** subscription {duration}.";
+"lng_prize_open" = "Open Gift Link";
+"lng_prize_unclaimed_title" = "Unclaimed Prize";
+"lng_prize_unclaimed_about" = "You have an unclaimed prize from a giveaway by {channel}.";
+"lng_prize_unclaimed_duration" = "This prize is a **Telegram Premium** subscription {duration}.";
+
+"lng_prizes_title#one" = "Giveaway Prize";
+"lng_prizes_title#other" = "Giveaway Prizes";
+"lng_prizes_about#one" = "**{count}** Telegram Premium Subscription {duration}.";
+"lng_prizes_about#other" = "**{count}** Telegram Premium Subscriptions {duration}.";
+"lng_prizes_participants" = "Participants";
+"lng_prizes_participants_all#one" = "All subscribers of the channel:";
+"lng_prizes_participants_all#other" = "All subscribers of the channels:";
+"lng_prizes_participants_new#one" = "All users who joined the channel below after this date:";
+"lng_prizes_participants_new#other" = "All users who joined the channels below after this date:";
+"lng_prizes_countries" = "from {countries}";
+"lng_prizes_countries_and_one" = "{countries}, {country}";
+"lng_prizes_countries_and_last" = "{countries} and {country}";
+"lng_prizes_date" = "Winners Selection Date";
+"lng_prizes_how_works" = "Learn more";
+"lng_prizes_how_title" = "About this giveaway";
+"lng_prizes_end_title" = "Giveaway ended";
+"lng_prizes_how_text" = "This giveaway is sponsored by {admins}.";
+"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_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.";
+"lng_prizes_end_activated#other" = "**{count}** of the winners already used their gift links.";
+"lng_prizes_winners_all_of_one#one" = "{count} random subscribers of {channel}.";
+"lng_prizes_winners_all_of_one#other" = "{count} random subscribers of {channel}.";
+"lng_prizes_winners_all_of_many#one" = "{count} random subscribers of {channel} and other listed channels.";
+"lng_prizes_winners_all_of_many#other" = "{count} random subscribers of {channel} and other listed channels.";
+"lng_prizes_winners_new_of_one#one" = "{count} random user that joined {channel} after {start_date}";
+"lng_prizes_winners_new_of_one#other" = "{count} random users that joined {channel} after {start_date}";
+"lng_prizes_winners_new_of_many#one" = "{count} random user that joined {channel} and other listed channels after {start_date}";
+"lng_prizes_winners_new_of_many#other" = "{count} random users that joined {channel} and other listed channels after {start_date}";
+"lng_prizes_how_participate_one" = "To take part in this giveaway please join channel {channel} before {date}.";
+"lng_prizes_how_participate_many" = "To take part in this giveaway please join channel {channel} and other listed channels before {date}.";
+"lng_prizes_how_no_admin" = "You are not eligible to participate in this giveaway, because you are an admin of participating channel ({channel}).";
+"lng_prizes_how_no_joined" = "You are not eligible to participate in this giveaway, because you joined this channel on {date}, which is before the contest started.";
+"lng_prizes_how_no_country" = "You are not eligible to participate in this giveaway, because your country is not included in the terms of the giveaway.";
+"lng_prizes_how_yes_joined_one" = "You are participating in this giveaway, because you have joined channel {channel}.";
+"lng_prizes_how_yes_joined_many" = "You are participating in this giveaway, because you have joined channel {channel} (and other listed channels).";
+"lng_prizes_you_won" = "You won a prize in this giveaway {cup}";
+"lng_prizes_view_prize" = "View my prize";
+"lng_prizes_you_didnt" = "You didn't win a prize in this giveaway.";
+"lng_prizes_cancelled" = "The channel cancelled the prizes by reversing the payment for them.";
+"lng_prizes_badge" = "x{amount}";
+
+"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";
+"lng_gift_link_label_to" = "To";
+"lng_gift_link_label_to_unclaimed" = "No recipient";
+"lng_gift_link_label_gift" = "Gift";
+"lng_gift_link_gift_premium" = "Telegram Premium {duration}";
+"lng_gift_link_label_reason" = "Reason";
+"lng_gift_link_reason_giveaway" = "Giveaway";
+"lng_gift_link_reason_unclaimed" = "Incomplete Giveaway";
+"lng_gift_link_reason_chosen" = "You were selected by the channel";
+"lng_gift_link_label_date" = "Date";
+"lng_gift_link_also_send" = "You can also {link} to a friend as a gift.";
+"lng_gift_link_also_send_link" = "send this link";
+"lng_gift_link_use" = "Use Link";
+"lng_gift_link_used_title" = "Used Gift Link";
+"lng_gift_link_used_about" = "This link was used to activate\na **Telegram Premium** subscription.";
+"lng_gift_link_used_footer" = "This link was used on {date}.";
+"lng_gift_link_expired" = "Gift code link expired";
+
"lng_accounts_limit_title" = "Limit Reached";
"lng_accounts_limit1#one" = "You have reached the limit of **{count}** connected accounts.";
"lng_accounts_limit1#other" = "You have reached the limit of **{count}** connected accounts.";
@@ -2213,6 +2362,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_saved_messages" = "Saved Messages";
"lng_saved_short" = "Save";
"lng_saved_forward_here" = "Forward messages here for quick access";
+"lng_saved_quote_here" = "Quote here to save";
"lng_scheduled_messages" = "Scheduled Messages";
"lng_scheduled_messages_empty" = "No scheduled messages here yet...";
@@ -2420,6 +2570,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_context_attached_stickers" = "Attached Stickers";
"lng_context_to_msg" = "Go To Message";
"lng_context_reply_msg" = "Reply";
+"lng_context_quote_and_reply" = "Quote & Reply";
"lng_context_edit_msg" = "Edit";
"lng_context_forward_msg" = "Forward Message";
"lng_context_send_now_msg" = "Send now";
@@ -2524,6 +2675,24 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_inline_switch_choose" = "Choose conversation...";
"lng_inline_switch_cant" = "Sorry, no way to write here :(";
+"lng_reply_in_another_title" = "Reply in...";
+"lng_reply_in_another_chat" = "Reply in Another Chat";
+"lng_reply_show_in_chat" = "Show in Chat";
+"lng_reply_remove" = "Do Not Reply";
+"lng_reply_about_quote" = "You can select specific part to quote.";
+"lng_reply_options_header" = "Reply to Message";
+"lng_reply_options_quote" = "Update Quote";
+"lng_reply_header_short" = "Reply";
+"lng_reply_quote_selected" = "Quote Selected";
+"lng_link_options_header" = "Link Preview Settings";
+"lng_link_header_short" = "Link";
+"lng_link_move_up" = "Move Up";
+"lng_link_move_down" = "Move Down";
+"lng_link_shrink_photo" = "Shrink Photo";
+"lng_link_enlarge_photo" = "Enlarge Photo";
+"lng_link_remove" = "Do Not Preview";
+"lng_link_about_choose" = "Click on a link to generate its preview.";
+
"lng_share_cant" = "Sorry, no way to share here :(";
"lng_reply_cant" = "Sorry, no way to reply to an old message in supergroup :(";
"lng_reply_cant_forward" = "Sorry, you can't reply to a message that was sent before the group was upgraded to a supergroup. Do you wish to forward it and add your comment?";
@@ -2546,6 +2715,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_edit_bot_title" = "Edit bot";
"lng_edit_sign_messages" = "Sign messages";
"lng_edit_group" = "Edit group";
+"lng_edit_channel_color" = "Change name color";
"lng_edit_self_title" = "Edit your name";
"lng_confirm_contact_data" = "New Contact";
"lng_add_contact" = "Create";
@@ -2674,6 +2844,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_menu_formatting_italic" = "Italic";
"lng_menu_formatting_underline" = "Underline";
"lng_menu_formatting_strike_out" = "Strike-through";
+"lng_menu_formatting_blockquote" = "Quote";
"lng_menu_formatting_monospace" = "Monospace";
"lng_menu_formatting_spoiler" = "Spoiler";
"lng_menu_formatting_link_create" = "Create link";
@@ -2686,7 +2857,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_formatting_link_create" = "Create";
"lng_text_copied" = "Text copied to clipboard.";
-"lng_code_copied" = "Code copied to clipboard.";
+"lng_code_copied" = "Block copied to clipboard.";
"lng_spellchecker_submenu" = "Spelling";
"lng_spellchecker_add" = "Add to Dictionary";
@@ -3352,6 +3523,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_admin_log_participant_volume_channel" = "{from} changed live stream volume for {user} to {percent}";
"lng_admin_log_antispam_enabled" = "{from} enabled aggressive anti-spam";
"lng_admin_log_antispam_disabled" = "{from} disabled aggressive anti-spam";
+"lng_admin_log_change_color" = "{from} changed channel color from {previous} to {color}";
+"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_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}";
diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml
index a95a14af9..1422235cb 100644
--- a/Telegram/Resources/uwp/AppX/AppxManifest.xml
+++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml
@@ -10,7 +10,7 @@
+ Version="4.11.1.0" />
Telegram Desktop
Telegram Messenger LLP
diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc
index c68845489..57cea3ebf 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,10,4,0
- PRODUCTVERSION 4,10,4,0
+ FILEVERSION 4,11,1,0
+ PRODUCTVERSION 4,11,1,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
@@ -62,10 +62,10 @@ BEGIN
BEGIN
VALUE "CompanyName", "Radolyn Labs"
VALUE "FileDescription", "AyuGram Desktop"
- VALUE "FileVersion", "4.10.4.0"
+ VALUE "FileVersion", "4.11.1.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2023"
VALUE "ProductName", "AyuGram Desktop"
- VALUE "ProductVersion", "4.10.4.0"
+ VALUE "ProductVersion", "4.11.1.0"
END
END
BLOCK "VarFileInfo"
diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc
index de9ad07c2..82062cc15 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,10,4,0
- PRODUCTVERSION 4,10,4,0
+ FILEVERSION 4,11,1,0
+ PRODUCTVERSION 4,11,1,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
@@ -53,10 +53,10 @@ BEGIN
BEGIN
VALUE "CompanyName", "Radolyn Labs"
VALUE "FileDescription", "AyuGram Desktop Updater"
- VALUE "FileVersion", "4.10.4.0"
+ VALUE "FileVersion", "4.11.1.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2023"
VALUE "ProductName", "AyuGram Desktop"
- VALUE "ProductVersion", "4.10.4.0"
+ VALUE "ProductVersion", "4.11.1.0"
END
END
BLOCK "VarFileInfo"
diff --git a/Telegram/SourceFiles/api/api_bot.cpp b/Telegram/SourceFiles/api/api_bot.cpp
index 65826aa2f..239d2d0ab 100644
--- a/Telegram/SourceFiles/api/api_bot.cpp
+++ b/Telegram/SourceFiles/api/api_bot.cpp
@@ -169,9 +169,7 @@ void SendBotCallbackData(
void HideSingleUseKeyboard(
not_null controller,
not_null item) {
- controller->content()->hideSingleUseKeyboard(
- item->history()->peer,
- item->id);
+ controller->content()->hideSingleUseKeyboard(item->fullId());
}
} // namespace
@@ -312,12 +310,14 @@ void ActivateBotCommand(ClickHandlerContext context, int row, int column) {
case ButtonType::Default: {
// Copy string before passing it to the sending method
// because the original button can be destroyed inside.
- const auto replyTo = item->isRegular() ? item->id : 0;
+ const auto replyTo = item->isRegular()
+ ? item->fullId()
+ : FullMsgId();
controller->content()->sendBotCommand({
.peer = item->history()->peer,
.command = QString(button->text),
.context = item->fullId(),
- .replyTo = replyTo,
+ .replyTo = { replyTo },
});
} break;
@@ -363,7 +363,7 @@ void ActivateBotCommand(ClickHandlerContext context, int row, int column) {
case ButtonType::RequestPhone: {
HideSingleUseKeyboard(controller, item);
- const auto itemId = item->id;
+ const auto itemId = item->fullId();
const auto topicRootId = item->topicRootId();
const auto history = item->history();
controller->show(Ui::MakeConfirmBox({
@@ -376,7 +376,7 @@ void ActivateBotCommand(ClickHandlerContext context, int row, int column) {
auto action = Api::SendAction(history);
action.clearDraft = false;
action.replyTo = {
- .msgId = itemId,
+ .messageId = itemId,
.topicRootId = topicRootId,
};
history->session().api().shareContact(
@@ -397,13 +397,11 @@ void ActivateBotCommand(ClickHandlerContext context, int row, int column) {
chosen |= PollData::Flag::Quiz;
}
}
- const auto replyToId = MsgId(0);
- const auto topicRootId = MsgId(0);
+ const auto replyTo = FullReplyTo();
Window::PeerMenuCreatePoll(
controller,
item->history()->peer,
- replyToId,
- topicRootId,
+ replyTo,
chosen,
disabled);
} break;
diff --git a/Telegram/SourceFiles/api/api_common.cpp b/Telegram/SourceFiles/api/api_common.cpp
index 0a44a916f..cfb1e7220 100644
--- a/Telegram/SourceFiles/api/api_common.cpp
+++ b/Telegram/SourceFiles/api/api_common.cpp
@@ -19,8 +19,8 @@ SendAction::SendAction(
SendOptions options)
: history(thread->owningHistory())
, options(options)
-, replyTo({ .msgId = thread->topicRootId() }) {
- replyTo.topicRootId = replyTo.msgId;
+, replyTo({ .messageId = { history->peer->id, thread->topicRootId() } }) {
+ replyTo.topicRootId = replyTo.messageId.msg;
}
SendOptions DefaultSendWhenOnlineOptions() {
@@ -31,7 +31,7 @@ SendOptions DefaultSendWhenOnlineOptions() {
}
MTPInputReplyTo SendAction::mtpReplyTo() const {
- return Data::ReplyToForMTP(&history->owner(), replyTo);
+ return Data::ReplyToForMTP(history, replyTo);
}
} // namespace Api
diff --git a/Telegram/SourceFiles/api/api_common.h b/Telegram/SourceFiles/api/api_common.h
index 08ee59ca1..c82841142 100644
--- a/Telegram/SourceFiles/api/api_common.h
+++ b/Telegram/SourceFiles/api/api_common.h
@@ -7,6 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
+#include "data/data_drafts.h"
+
class History;
namespace Data {
@@ -22,7 +24,6 @@ struct SendOptions {
TimeId scheduled = 0;
bool silent = false;
bool handleSupportSwitch = false;
- bool removeWebPageId = false;
bool hideViaBot = false;
};
[[nodiscard]] SendOptions DefaultSendWhenOnlineOptions();
@@ -54,7 +55,7 @@ struct MessageToSend {
SendAction action;
TextWithTags textWithTags;
- WebPageId webPageId = 0;
+ Data::WebPageDraft webPage;
};
struct RemoteFileInfo {
diff --git a/Telegram/SourceFiles/api/api_editing.cpp b/Telegram/SourceFiles/api/api_editing.cpp
index 916e27653..005effad9 100644
--- a/Telegram/SourceFiles/api/api_editing.cpp
+++ b/Telegram/SourceFiles/api/api_editing.cpp
@@ -11,8 +11,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_media.h"
#include "api/api_text_entities.h"
#include "ui/boxes/confirm_box.h"
+#include "data/data_histories.h"
#include "data/data_scheduled_messages.h"
#include "data/data_session.h"
+#include "data/data_web_page.h"
#include "history/history.h"
#include "history/history_item.h"
#include "lang/lang_keys.h"
@@ -45,6 +47,7 @@ template
mtpRequestId EditMessage(
not_null item,
const TextWithEntities &textWithEntities,
+ Data::WebPageDraft webpage,
SendOptions options,
DoneCallback &&done,
FailCallback &&fail,
@@ -65,15 +68,21 @@ mtpRequestId EditMessage(
const auto emptyFlag = MTPmessages_EditMessage::Flag(0);
const auto flags = emptyFlag
- | (!text.isEmpty() || media
+ | ((!text.isEmpty() || media)
? MTPmessages_EditMessage::Flag::f_message
: emptyFlag)
| ((media && inputMedia.has_value())
? MTPmessages_EditMessage::Flag::f_media
: emptyFlag)
- | (options.removeWebPageId
+ | (webpage.removed
? MTPmessages_EditMessage::Flag::f_no_webpage
: emptyFlag)
+ | ((!webpage.removed && !webpage.url.isEmpty())
+ ? MTPmessages_EditMessage::Flag::f_media
+ : emptyFlag)
+ | ((!webpage.removed && !webpage.url.isEmpty() && webpage.invert)
+ ? MTPmessages_EditMessage::Flag::f_invert_media
+ : emptyFlag)
| (!sentEntities.v.isEmpty()
? MTPmessages_EditMessage::Flag::f_entities
: emptyFlag)
@@ -89,7 +98,7 @@ mtpRequestId EditMessage(
item->history()->peer->input,
MTP_int(id),
MTP_string(text),
- inputMedia.value_or(MTPInputMedia()),
+ inputMedia.value_or(Data::WebPageForMTP(webpage, text.isEmpty())),
MTPReplyMarkup(),
sentEntities,
MTP_int(options.scheduled)
@@ -133,9 +142,15 @@ mtpRequestId EditMessage(
FailCallback &&fail,
std::optional inputMedia = std::nullopt) {
const auto &text = item->originalText();
+ const auto webpage = (!item->media() || !item->media()->webpage())
+ ? Data::WebPageDraft{ .removed = true }
+ : Data::WebPageDraft{
+ .id = item->media()->webpage()->id,
+ };
return EditMessage(
item,
text,
+ webpage,
options,
std::forward(done),
std::forward(fail),
@@ -216,12 +231,19 @@ mtpRequestId EditCaption(
SendOptions options,
Fn done,
Fn fail) {
- return EditMessage(item, caption, options, done, fail);
+ return EditMessage(
+ item,
+ caption,
+ Data::WebPageDraft(),
+ options,
+ done,
+ fail);
}
mtpRequestId EditTextMessage(
not_null item,
const TextWithEntities &caption,
+ Data::WebPageDraft webpage,
SendOptions options,
Fn done,
Fn fail) {
@@ -229,7 +251,7 @@ mtpRequestId EditTextMessage(
applyUpdates();
done(id);
};
- return EditMessage(item, caption, options, callback, fail);
+ return EditMessage(item, caption, webpage, options, callback, fail);
}
} // namespace Api
diff --git a/Telegram/SourceFiles/api/api_editing.h b/Telegram/SourceFiles/api/api_editing.h
index 7c132124b..86f106f45 100644
--- a/Telegram/SourceFiles/api/api_editing.h
+++ b/Telegram/SourceFiles/api/api_editing.h
@@ -9,6 +9,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
class HistoryItem;
+namespace Data {
+struct WebPageDraft;
+} // namespace Data
+
namespace MTP {
class Error;
} // namespace MTP
@@ -48,6 +52,7 @@ mtpRequestId EditCaption(
mtpRequestId EditTextMessage(
not_null item,
const TextWithEntities &caption,
+ Data::WebPageDraft webpage,
SendOptions options,
Fn done,
Fn fail);
diff --git a/Telegram/SourceFiles/api/api_peer_photo.cpp b/Telegram/SourceFiles/api/api_peer_photo.cpp
index 71512172b..25a706587 100644
--- a/Telegram/SourceFiles/api/api_peer_photo.cpp
+++ b/Telegram/SourceFiles/api/api_peer_photo.cpp
@@ -510,40 +510,57 @@ void PeerPhoto::requestUserPhotos(
_userPhotosRequests.emplace(user, requestId);
}
+auto PeerPhoto::emojiList(EmojiListType type) -> EmojiListData & {
+ switch (type) {
+ case EmojiListType::Profile: return _profileEmojiList;
+ case EmojiListType::Group: return _groupEmojiList;
+ case EmojiListType::Background: return _backgroundEmojiList;
+ }
+ Unexpected("Type in PeerPhoto::emojiList.");
+}
+
+auto PeerPhoto::emojiList(EmojiListType type) const
+-> const EmojiListData & {
+ return const_cast(this)->emojiList(type);
+}
+
void PeerPhoto::requestEmojiList(EmojiListType type) {
- if (_requestIdEmojiList) {
+ auto &list = emojiList(type);
+ if (list.requestId) {
return;
}
- const auto isGroup = (type == EmojiListType::Group);
- const auto d = [=](const MTPEmojiList &result) {
- _requestIdEmojiList = 0;
- result.match([](const MTPDemojiListNotModified &data) {
- }, [&](const MTPDemojiList &data) {
- auto &list = isGroup ? _profileEmojiList : _groupEmojiList;
- list = ranges::views::all(
- data.vdocument_id().v
- ) | ranges::views::transform(&MTPlong::v) | ranges::to_vector;
- });
+ const auto send = [&](auto &&request) {
+ return _api.request(
+ std::move(request)
+ ).done([=](const MTPEmojiList &result) {
+ auto &list = emojiList(type);
+ list.requestId = 0;
+ result.match([](const MTPDemojiListNotModified &data) {
+ }, [&](const MTPDemojiList &data) {
+ list.list = ranges::views::all(
+ data.vdocument_id().v
+ ) | ranges::views::transform(
+ &MTPlong::v
+ ) | ranges::to_vector;
+ });
+ }).fail([=] {
+ emojiList(type).requestId = 0;
+ }).send();
};
- const auto f = [=] { _requestIdEmojiList = 0; };
- _requestIdEmojiList = isGroup
- ? _api.request(
- MTPaccount_GetDefaultGroupPhotoEmojis()
- ).done(d).fail(f).send()
- : _api.request(
- MTPaccount_GetDefaultProfilePhotoEmojis()
- ).done(d).fail(f).send();
+ list.requestId = (type == EmojiListType::Profile)
+ ? send(MTPaccount_GetDefaultProfilePhotoEmojis())
+ : (type == EmojiListType::Group)
+ ? send(MTPaccount_GetDefaultGroupPhotoEmojis())
+ : send(MTPaccount_GetDefaultBackgroundEmojis());
}
rpl::producer PeerPhoto::emojiListValue(
EmojiListType type) {
- auto &list = (type == EmojiListType::Group)
- ? _profileEmojiList
- : _groupEmojiList;
- if (list.current().empty() && !_requestIdEmojiList) {
+ auto &list = emojiList(type);
+ if (list.list.current().empty() && !list.requestId) {
requestEmojiList(type);
}
- return list.value();
+ return list.list.value();
}
// Non-personal photo in case a personal photo is set.
diff --git a/Telegram/SourceFiles/api/api_peer_photo.h b/Telegram/SourceFiles/api/api_peer_photo.h
index 03920d3ef..1d4bd1071 100644
--- a/Telegram/SourceFiles/api/api_peer_photo.h
+++ b/Telegram/SourceFiles/api/api_peer_photo.h
@@ -31,6 +31,7 @@ public:
enum class EmojiListType {
Profile,
Group,
+ Background,
};
struct UserPhoto {
@@ -73,6 +74,10 @@ private:
Suggestion,
Fallback,
};
+ struct EmojiListData {
+ rpl::variable list;
+ mtpRequestId requestId = 0;
+ };
void ready(
const FullMsgId &msgId,
@@ -84,6 +89,9 @@ private:
UploadType type,
Fn done);
+ [[nodiscard]] EmojiListData &emojiList(EmojiListType type);
+ [[nodiscard]] const EmojiListData &emojiList(EmojiListType type) const;
+
const not_null _session;
MTP::Sender _api;
@@ -101,9 +109,9 @@ private:
not_null,
not_null> _nonPersonalPhotos;
- mtpRequestId _requestIdEmojiList = 0;
- rpl::variable _profileEmojiList;
- rpl::variable _groupEmojiList;
+ EmojiListData _profileEmojiList;
+ EmojiListData _groupEmojiList;
+ EmojiListData _backgroundEmojiList;
};
diff --git a/Telegram/SourceFiles/api/api_polls.cpp b/Telegram/SourceFiles/api/api_polls.cpp
index 71d44119d..9f1aef8d0 100644
--- a/Telegram/SourceFiles/api/api_polls.cpp
+++ b/Telegram/SourceFiles/api/api_polls.cpp
@@ -47,7 +47,7 @@ void Polls::create(
const auto history = action.history;
const auto peer = history->peer;
- const auto topicRootId = action.replyTo.msgId
+ const auto topicRootId = action.replyTo.messageId
? action.replyTo.topicRootId
: 0;
auto sendFlags = MTPmessages_SendMedia::Flags(0);
diff --git a/Telegram/SourceFiles/api/api_premium.cpp b/Telegram/SourceFiles/api/api_premium.cpp
index 975d36a65..320f3f94a 100644
--- a/Telegram/SourceFiles/api/api_premium.cpp
+++ b/Telegram/SourceFiles/api/api_premium.cpp
@@ -17,6 +17,21 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "apiwrap.h"
namespace Api {
+namespace {
+
+[[nodiscard]] GiftCode Parse(const MTPDpayments_checkedGiftCode &data) {
+ return {
+ .from = peerFromMTP(data.vfrom_id()),
+ .to = data.vto_id() ? peerFromUser(*data.vto_id()) : PeerId(),
+ .giveawayId = data.vgiveaway_msg_id().value_or_empty(),
+ .date = data.vdate().v,
+ .used = data.vused_date().value_or_empty(),
+ .months = data.vmonths().v,
+ .giveaway = data.is_via_giveaway(),
+ };
+}
+
+} // namespace
Premium::Premium(not_null api)
: _session(&api->session())
@@ -183,6 +198,115 @@ void Premium::reloadCloudSet() {
}).send();
}
+void Premium::checkGiftCode(
+ const QString &slug,
+ Fn done) {
+ if (_giftCodeRequestId) {
+ if (_giftCodeSlug == slug) {
+ return;
+ }
+ _api.request(_giftCodeRequestId).cancel();
+ }
+ _giftCodeSlug = slug;
+ _giftCodeRequestId = _api.request(MTPpayments_CheckGiftCode(
+ MTP_string(slug)
+ )).done([=](const MTPpayments_CheckedGiftCode &result) {
+ _giftCodeRequestId = 0;
+
+ const auto &data = result.data();
+ _session->data().processUsers(data.vusers());
+ _session->data().processChats(data.vchats());
+ done(updateGiftCode(slug, Parse(data)));
+ }).fail([=](const MTP::Error &error) {
+ _giftCodeRequestId = 0;
+
+ done(updateGiftCode(slug, {}));
+ }).send();
+}
+
+GiftCode Premium::updateGiftCode(
+ const QString &slug,
+ const GiftCode &code) {
+ auto &now = _giftCodes[slug];
+ if (now != code) {
+ now = code;
+ _giftCodeUpdated.fire_copy(slug);
+ }
+ return code;
+}
+
+rpl::producer Premium::giftCodeValue(const QString &slug) const {
+ return _giftCodeUpdated.events_starting_with_copy(
+ slug
+ ) | rpl::filter(rpl::mappers::_1 == slug) | rpl::map([=] {
+ const auto i = _giftCodes.find(slug);
+ return (i != end(_giftCodes)) ? i->second : GiftCode();
+ });
+}
+
+void Premium::applyGiftCode(const QString &slug, Fn done) {
+ _api.request(MTPpayments_ApplyGiftCode(
+ MTP_string(slug)
+ )).done([=](const MTPUpdates &result) {
+ _session->api().applyUpdates(result);
+ done({});
+ }).fail([=](const MTP::Error &error) {
+ done(error.type());
+ }).send();
+}
+
+void Premium::resolveGiveawayInfo(
+ not_null peer,
+ MsgId messageId,
+ Fn done) {
+ Expects(done != nullptr);
+
+ _giveawayInfoDone = std::move(done);
+ if (_giveawayInfoRequestId) {
+ if (_giveawayInfoPeer == peer
+ && _giveawayInfoMessageId == messageId) {
+ return;
+ }
+ _api.request(_giveawayInfoRequestId).cancel();
+ }
+ _giveawayInfoPeer = peer;
+ _giveawayInfoMessageId = messageId;
+ _giveawayInfoRequestId = _api.request(MTPpayments_GetGiveawayInfo(
+ _giveawayInfoPeer->input,
+ MTP_int(_giveawayInfoMessageId.bare)
+ )).done([=](const MTPpayments_GiveawayInfo &result) {
+ _giveawayInfoRequestId = 0;
+
+ auto info = GiveawayInfo();
+ result.match([&](const MTPDpayments_giveawayInfo &data) {
+ info.participating = data.is_participating();
+ info.state = data.is_preparing_results()
+ ? GiveawayState::Preparing
+ : GiveawayState::Running;
+ info.adminChannelId = data.vadmin_disallowed_chat_id()
+ ? ChannelId(*data.vadmin_disallowed_chat_id())
+ : ChannelId();
+ info.disallowedCountry = qs(
+ data.vdisallowed_country().value_or_empty());
+ info.tooEarlyDate
+ = data.vjoined_too_early_date().value_or_empty();
+ info.startDate = data.vstart_date().v;
+ }, [&](const MTPDpayments_giveawayInfoResults &data) {
+ info.state = data.is_refunded()
+ ? GiveawayState::Refunded
+ : GiveawayState::Finished;
+ info.giftCode = qs(data.vgift_code_slug().value_or_empty());
+ info.activatedCount = data.vactivated_count().v;
+ info.finishDate = data.vfinish_date().v;
+ info.startDate = data.vstart_date().v;
+ });
+ _giveawayInfoDone(std::move(info));
+ }).fail([=] {
+ _giveawayInfoRequestId = 0;
+ _giveawayInfoDone({});
+ }).send();
+}
+
const Data::SubscriptionOptions &Premium::subscriptionOptions() const {
return _subscriptionOptions;
}
diff --git a/Telegram/SourceFiles/api/api_premium.h b/Telegram/SourceFiles/api/api_premium.h
index 358283360..01ed83cc7 100644
--- a/Telegram/SourceFiles/api/api_premium.h
+++ b/Telegram/SourceFiles/api/api_premium.h
@@ -18,6 +18,49 @@ class Session;
namespace Api {
+struct GiftCode {
+ PeerId from = 0;
+ PeerId to = 0;
+ MsgId giveawayId = 0;
+ TimeId date = 0;
+ TimeId used = 0; // 0 if not used.
+ int months = 0;
+ bool giveaway = false;
+
+ explicit operator bool() const {
+ return months != 0;
+ }
+
+ friend inline bool operator==(
+ const GiftCode&,
+ const GiftCode&) = default;
+};
+
+enum class GiveawayState {
+ Invalid,
+ Running,
+ Preparing,
+ Finished,
+ Refunded,
+};
+
+struct GiveawayInfo {
+ QString giftCode;
+ QString disallowedCountry;
+ ChannelId adminChannelId = 0;
+ GiveawayState state = GiveawayState::Invalid;
+ TimeId tooEarlyDate = 0;
+ TimeId finishDate = 0;
+ TimeId startDate = 0;
+ int winnersCount = 0;
+ int activatedCount = 0;
+ bool participating = false;
+
+ explicit operator bool() const {
+ return state != GiveawayState::Invalid;
+ }
+};
+
class Premium final {
public:
explicit Premium(not_null api);
@@ -40,6 +83,19 @@ public:
[[nodiscard]] int64 monthlyAmount() const;
[[nodiscard]] QString monthlyCurrency() const;
+ void checkGiftCode(
+ const QString &slug,
+ Fn done);
+ GiftCode updateGiftCode(const QString &slug, const GiftCode &code);
+ [[nodiscard]] rpl::producer giftCodeValue(
+ const QString &slug) const;
+ void applyGiftCode(const QString &slug, Fn done);
+
+ void resolveGiveawayInfo(
+ not_null peer,
+ MsgId messageId,
+ Fn done);
+
[[nodiscard]] auto subscriptionOptions() const
-> const Data::SubscriptionOptions &;
@@ -71,6 +127,16 @@ private:
int64 _monthlyAmount = 0;
QString _monthlyCurrency;
+ mtpRequestId _giftCodeRequestId = 0;
+ QString _giftCodeSlug;
+ base::flat_map _giftCodes;
+ rpl::event_stream _giftCodeUpdated;
+
+ mtpRequestId _giveawayInfoRequestId = 0;
+ PeerData *_giveawayInfoPeer = nullptr;
+ MsgId _giveawayInfoMessageId = 0;
+ Fn _giveawayInfoDone;
+
Data::SubscriptionOptions _subscriptionOptions;
};
diff --git a/Telegram/SourceFiles/api/api_sending.cpp b/Telegram/SourceFiles/api/api_sending.cpp
index 291a2675a..09001ae35 100644
--- a/Telegram/SourceFiles/api/api_sending.cpp
+++ b/Telegram/SourceFiles/api/api_sending.cpp
@@ -368,9 +368,9 @@ void SendConfirmedFile(
if (!isEditing) {
const auto histories = &session->data().histories();
- file->to.replyTo.msgId = histories->convertTopicReplyToId(
+ file->to.replyTo.messageId = histories->convertTopicReplyToId(
history,
- file->to.replyTo.msgId);
+ file->to.replyTo.messageId);
file->to.replyTo.topicRootId = histories->convertTopicReplyToId(
history,
file->to.replyTo.topicRootId);
diff --git a/Telegram/SourceFiles/api/api_statistics.cpp b/Telegram/SourceFiles/api/api_statistics.cpp
index 1bd5eb314..c3286f667 100644
--- a/Telegram/SourceFiles/api/api_statistics.cpp
+++ b/Telegram/SourceFiles/api/api_statistics.cpp
@@ -434,19 +434,42 @@ void MessageStatistics::request(Fn done) {
const auto requestPrivateForwards = [=](
const Data::StatisticalGraph &messageGraph) {
- _api.request(MTPstats_GetBroadcastStats(
- MTP_flags(MTPstats_GetBroadcastStats::Flags(0)),
- _channel->inputChannel
- )).done([=](const MTPstats_BroadcastStats &result) {
- const auto channelStats = ChannelStatisticsFromTL(result.data());
- auto info = Data::StatisticsMessageInteractionInfo();
- for (const auto &r : channelStats.recentMessageInteractions) {
- if (r.messageId == _fullId.msg) {
- info = r;
- break;
- }
- }
- requestFirstPublicForwards(messageGraph, info);
+ _api.request(MTPchannels_GetMessages(
+ _channel->inputChannel,
+ MTP_vector(
+ 1,
+ MTP_inputMessageID(MTP_int(_fullId.msg))))
+ ).done([=](const MTPmessages_Messages &result) {
+ const auto process = [&](const MTPVector &messages) {
+ const auto &message = messages.v.front();
+ return message.match([&](const MTPDmessage &data) {
+ return Data::StatisticsMessageInteractionInfo{
+ .messageId = IdFromMessage(message),
+ .viewsCount = data.vviews()
+ ? data.vviews()->v
+ : 0,
+ .forwardsCount = data.vforwards()
+ ? data.vforwards()->v
+ : 0,
+ };
+ }, [](const MTPDmessageEmpty &) {
+ return Data::StatisticsMessageInteractionInfo();
+ }, [](const MTPDmessageService &) {
+ return Data::StatisticsMessageInteractionInfo();
+ });
+ };
+
+ auto info = result.match([&](const MTPDmessages_messages &data) {
+ return process(data.vmessages());
+ }, [&](const MTPDmessages_messagesSlice &data) {
+ return process(data.vmessages());
+ }, [&](const MTPDmessages_channelMessages &data) {
+ return process(data.vmessages());
+ }, [](const MTPDmessages_messagesNotModified &) {
+ return Data::StatisticsMessageInteractionInfo();
+ });
+
+ requestFirstPublicForwards(messageGraph, std::move(info));
}).fail([=](const MTP::Error &error) {
requestFirstPublicForwards(messageGraph, {});
}).send();
@@ -478,9 +501,9 @@ rpl::producer Boosts::request() {
return lifetime;
}
- _api.request(MTPstories_GetBoostsStatus(
+ _api.request(MTPpremium_GetBoostsStatus(
_peer->input
- )).done([=](const MTPstories_BoostsStatus &result) {
+ )).done([=](const MTPpremium_BoostsStatus &result) {
const auto &data = result.data();
const auto hasPremium = !!data.vpremium_audience();
const auto premiumMemberCount = hasPremium
@@ -530,28 +553,29 @@ void Boosts::requestBoosts(
}
constexpr auto kTlFirstSlice = tl::make_int(kFirstSlice);
constexpr auto kTlLimit = tl::make_int(kLimit);
- _requestId = _api.request(MTPstories_GetBoostersList(
+ _requestId = _api.request(MTPpremium_GetBoostsList(
+ MTP_flags(0),
_peer->input,
MTP_string(token.next),
token.next.isEmpty() ? kTlFirstSlice : kTlLimit
- )).done([=](const MTPstories_BoostersList &result) {
+ )).done([=](const MTPpremium_BoostsList &result) {
_requestId = 0;
const auto &data = result.data();
_peer->owner().processUsers(data.vusers());
auto list = std::vector();
- list.reserve(data.vboosters().v.size());
- for (const auto &boost : data.vboosters().v) {
+ list.reserve(data.vboosts().v.size());
+ for (const auto &boost : data.vboosts().v) {
list.push_back({
- boost.data().vuser_id().v,
+ boost.data().vuser_id().value_or_empty(),
QDateTime::fromSecsSinceEpoch(boost.data().vexpires().v),
});
}
done(Data::BoostsListSlice{
.list = std::move(list),
.total = data.vcount().v,
- .allLoaded = (data.vcount().v == data.vboosters().v.size()),
+ .allLoaded = (data.vcount().v == data.vboosts().v.size()),
.token = Data::BoostsListSlice::OffsetToken{
data.vnext_offset()
? qs(*data.vnext_offset())
diff --git a/Telegram/SourceFiles/api/api_text_entities.cpp b/Telegram/SourceFiles/api/api_text_entities.cpp
index 1bfe1142f..6c434bc9b 100644
--- a/Telegram/SourceFiles/api/api_text_entities.cpp
+++ b/Telegram/SourceFiles/api/api_text_entities.cpp
@@ -114,6 +114,7 @@ EntitiesInText EntitiesFromMTP(
case mtpc_messageEntityStrike: { auto &d = entity.c_messageEntityStrike(); result.push_back({ EntityType::StrikeOut, d.voffset().v, d.vlength().v }); } break;
case mtpc_messageEntityCode: { auto &d = entity.c_messageEntityCode(); result.push_back({ EntityType::Code, d.voffset().v, d.vlength().v }); } break;
case mtpc_messageEntityPre: { auto &d = entity.c_messageEntityPre(); result.push_back({ EntityType::Pre, d.voffset().v, d.vlength().v, qs(d.vlanguage()) }); } break;
+ case mtpc_messageEntityBlockquote: { auto &d = entity.c_messageEntityBlockquote(); result.push_back({ EntityType::Blockquote, d.voffset().v, d.vlength().v }); } break;
case mtpc_messageEntityBankCard: break; // Skipping cards. // #TODO entities
case mtpc_messageEntitySpoiler: { auto &d = entity.c_messageEntitySpoiler(); result.push_back({ EntityType::Spoiler, d.voffset().v, d.vlength().v }); } break;
case mtpc_messageEntityCustomEmoji: {
@@ -142,6 +143,7 @@ MTPVector EntitiesToMTP(
&& entity.type() != EntityType::StrikeOut
&& entity.type() != EntityType::Code // #TODO entities
&& entity.type() != EntityType::Pre
+ && entity.type() != EntityType::Blockquote
&& entity.type() != EntityType::Spoiler
&& entity.type() != EntityType::MentionName
&& entity.type() != EntityType::CustomUrl
@@ -170,6 +172,7 @@ MTPVector EntitiesToMTP(
case EntityType::StrikeOut: v.push_back(MTP_messageEntityStrike(offset, length)); break;
case EntityType::Code: v.push_back(MTP_messageEntityCode(offset, length)); break; // #TODO entities
case EntityType::Pre: v.push_back(MTP_messageEntityPre(offset, length, MTP_string(entity.data()))); break;
+ case EntityType::Blockquote: v.push_back(MTP_messageEntityBlockquote(offset, length)); break;
case EntityType::Spoiler: v.push_back(MTP_messageEntitySpoiler(offset, length)); break;
case EntityType::CustomEmoji: {
if (const auto valid = CustomEmojiEntity(offset, length, entity.data())) {
diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp
index 5bd77251d..0303700ad 100644
--- a/Telegram/SourceFiles/api/api_updates.cpp
+++ b/Telegram/SourceFiles/api/api_updates.cpp
@@ -2103,7 +2103,10 @@ void Updates::feedUpdate(const MTPUpdate &update) {
windows.front()->window().show(Ui::MakeInformBox(text));
}
} else {
- session().data().serviceNotification(text, d.vmedia());
+ session().data().serviceNotification(
+ text,
+ d.vmedia(),
+ d.is_invert_media());
session().api().authorizations().reload();
}
} break;
diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp
index 3c3270595..ad6d137b9 100644
--- a/Telegram/SourceFiles/apiwrap.cpp
+++ b/Telegram/SourceFiles/apiwrap.cpp
@@ -2150,14 +2150,13 @@ void ApiWrap::saveDraftsToCloud() {
auto flags = MTPmessages_SaveDraft::Flags(0);
auto &textWithTags = cloudDraft->textWithTags;
- if (cloudDraft->previewState != Data::PreviewState::Allowed) {
+ if (cloudDraft->webpage.removed) {
flags |= MTPmessages_SaveDraft::Flag::f_no_webpage;
+ } else if (!cloudDraft->webpage.url.isEmpty()) {
+ flags |= MTPmessages_SaveDraft::Flag::f_media;
}
- if (cloudDraft->msgId) {
- flags |= MTPmessages_SaveDraft::Flag::f_reply_to_msg_id;
- }
- if (cloudDraft->topicRootId) {
- flags |= MTPmessages_SaveDraft::Flag::f_top_msg_id;
+ if (cloudDraft->reply.messageId || cloudDraft->reply.topicRootId) {
+ flags |= MTPmessages_SaveDraft::Flag::f_reply_to;
}
if (!textWithTags.tags.isEmpty()) {
flags |= MTPmessages_SaveDraft::Flag::f_entities;
@@ -2170,11 +2169,13 @@ void ApiWrap::saveDraftsToCloud() {
history->startSavingCloudDraft(topicRootId);
cloudDraft->saveRequestId = request(MTPmessages_SaveDraft(
MTP_flags(flags),
- MTP_int(cloudDraft->msgId),
- MTP_int(cloudDraft->topicRootId),
+ ReplyToForMTP(history, cloudDraft->reply),
history->peer->input,
MTP_string(textWithTags.text),
- entities
+ entities,
+ Data::WebPageForMTP(
+ cloudDraft->webpage,
+ textWithTags.text.isEmpty())
)).done([=](const MTPBool &result, const MTP::Response &response) {
const auto requestId = response.requestId;
history->finishSavingCloudDraft(
@@ -2261,7 +2262,7 @@ void ApiWrap::gotStickerSet(
}
void ApiWrap::requestWebPageDelayed(not_null page) {
- if (page->pendingTill <= 0) {
+ if (page->failed || !page->pendingTill) {
return;
}
_webPagesPending.emplace(page, 0);
@@ -2566,7 +2567,8 @@ void ApiWrap::gotWebPages(ChannelData *channel, const MTPmessages_Messages &resu
for (auto i = _webPagesPending.begin(); i != _webPagesPending.cend();) {
if (i->second == req) {
if (i->first->pendingTill > 0) {
- i->first->pendingTill = -1;
+ i->first->pendingTill = 0;
+ i->first->failed = 1;
_session->data().notifyWebPageUpdateDelayed(i->first);
}
i = _webPagesPending.erase(i);
@@ -3612,9 +3614,8 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
action.generateLocal = true;
sendAction(action);
- const auto replyToId = action.replyTo.msgId;
- const auto replyTo = replyToId
- ? peer->owner().message(peer, replyToId)
+ const auto replyTo = action.replyTo.messageId
+ ? peer->owner().message(action.replyTo.messageId)
: nullptr;
const auto topicRootId = replyTo
? replyTo->topicRootId()
@@ -3642,7 +3643,13 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
auto &histories = history->owner().histories();
- while (TextUtilities::CutPart(sending, left, MaxMessageSize)) {
+ const auto exactWebPage = !message.webPage.url.isEmpty();
+ auto isFirst = true;
+ while (TextUtilities::CutPart(sending, left, MaxMessageSize)
+ || (isFirst && exactWebPage)) {
+ TextUtilities::Trim(left);
+ const auto isLast = left.empty();
+
auto newId = FullMsgId(
peer->id,
_session->data().nextLocalMessageId());
@@ -3656,26 +3663,52 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
MTPstring msgText(MTP_string(sending.text));
auto flags = NewMessageFlags(peer);
auto sendFlags = MTPmessages_SendMessage::Flags(0);
+ auto mediaFlags = MTPmessages_SendMedia::Flags(0);
if (action.replyTo) {
flags |= MessageFlag::HasReplyInfo;
sendFlags |= MTPmessages_SendMessage::Flag::f_reply_to;
+ mediaFlags |= MTPmessages_SendMedia::Flag::f_reply_to;
}
+ const auto ignoreWebPage = message.webPage.removed
+ || (exactWebPage && !isLast);
+ const auto manualWebPage = exactWebPage
+ && !ignoreWebPage
+ && (message.webPage.manual || (isLast && !isFirst));
const auto replyHeader = NewMessageReplyHeader(action);
MTPMessageMedia media = MTP_messageMediaEmpty();
- if (message.webPageId == CancelledWebPageId) {
+ if (ignoreWebPage) {
sendFlags |= MTPmessages_SendMessage::Flag::f_no_webpage;
- } else if (message.webPageId) {
- auto page = _session->data().webpage(message.webPageId);
+ } else if (exactWebPage) {
+ using PageFlag = MTPDmessageMediaWebPage::Flag;
+ using PendingFlag = MTPDwebPagePending::Flag;
+ const auto &fields = message.webPage;
+ const auto page = _session->data().webpage(fields.id);
media = MTP_messageMediaWebPage(
+ MTP_flags(PageFlag()
+ | (manualWebPage ? PageFlag::f_manual : PageFlag())
+ | (fields.forceLargeMedia
+ ? PageFlag::f_force_large_media
+ : PageFlag())
+ | (fields.forceSmallMedia
+ ? PageFlag::f_force_small_media
+ : PageFlag())),
MTP_webPagePending(
- MTP_long(page->id),
+ MTP_flags(PendingFlag::f_url),
+ MTP_long(fields.id),
+ MTP_string(fields.url),
MTP_int(page->pendingTill)));
}
const auto anonymousPost = peer->amAnonymous();
const auto silentPost = ShouldSendSilent(peer, action.options);
FillMessagePostFlags(action, peer, flags);
+ if (exactWebPage && !ignoreWebPage && message.webPage.invert) {
+ flags |= MessageFlag::InvertMedia;
+ sendFlags |= MTPmessages_SendMessage::Flag::f_invert_media;
+ mediaFlags |= MTPmessages_SendMedia::Flag::f_invert_media;
+ }
if (silentPost) {
sendFlags |= MTPmessages_SendMessage::Flag::f_silent;
+ mediaFlags |= MTPmessages_SendMedia::Flag::f_silent;
}
const auto sentEntities = Api::EntitiesToMTP(
_session,
@@ -3683,11 +3716,13 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
Api::ConvertOption::SkipLocal);
if (!sentEntities.v.isEmpty()) {
sendFlags |= MTPmessages_SendMessage::Flag::f_entities;
+ mediaFlags |= MTPmessages_SendMedia::Flag::f_entities;
}
const auto clearCloudDraft = action.clearDraft;
const auto topicRootId = action.replyTo.topicRootId;
if (clearCloudDraft) {
sendFlags |= MTPmessages_SendMessage::Flag::f_clear_draft;
+ mediaFlags |= MTPmessages_SendMedia::Flag::f_clear_draft;
history->clearCloudDraft(topicRootId);
history->startSavingCloudDraft(topicRootId);
}
@@ -3699,6 +3734,7 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
: _session->userPeerId();
if (sendAs) {
sendFlags |= MTPmessages_SendMessage::Flag::f_send_as;
+ mediaFlags |= MTPmessages_SendMedia::Flag::f_send_as;
}
const auto messagePostAuthor = peer->isBroadcast()
? _session->user()->name()
@@ -3706,6 +3742,7 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
if (action.options.scheduled) {
flags |= MessageFlag::IsOrWasScheduled;
sendFlags |= MTPmessages_SendMessage::Flag::f_schedule_date;
+ mediaFlags |= MTPmessages_SendMedia::Flag::f_schedule_date;
}
const auto viaBotId = UserId();
lastMessage = history->addNewLocalMessage(
@@ -3719,27 +3756,18 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
sending,
media,
HistoryMessageMarkupData());
- histories.sendPreparedMessage(
- history,
- action.replyTo,
- randomId,
- Data::Histories::PrepareMessage(
- MTP_flags(sendFlags),
- peer->input,
- Data::Histories::ReplyToPlaceholder(),
- msgText,
- MTP_long(randomId),
- MTPReplyMarkup(),
- sentEntities,
- MTP_int(action.options.scheduled),
- (sendAs ? sendAs->input : MTP_inputPeerEmpty())
- ), [=](const MTPUpdates &result, const MTP::Response &response) {
+ const auto done = [=](
+ const MTPUpdates &result,
+ const MTP::Response &response) {
if (clearCloudDraft) {
history->finishSavingCloudDraft(
topicRootId,
UnixtimeFromMsgId(response.outerMsgId));
}
- }, [=](const MTP::Error &error, const MTP::Response &response) {
+ };
+ const auto fail = [=](
+ const MTP::Error &error,
+ const MTP::Response &response) {
if (error.type() == u"MESSAGE_EMPTY"_q) {
lastMessage->destroy();
} else {
@@ -3750,7 +3778,44 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
topicRootId,
UnixtimeFromMsgId(response.outerMsgId));
}
- });
+ };
+ if (exactWebPage
+ && !ignoreWebPage
+ && (manualWebPage || sending.empty())) {
+ histories.sendPreparedMessage(
+ history,
+ action.replyTo,
+ randomId,
+ Data::Histories::PrepareMessage(
+ MTP_flags(mediaFlags),
+ peer->input,
+ Data::Histories::ReplyToPlaceholder(),
+ Data::WebPageForMTP(message.webPage, true),
+ msgText,
+ MTP_long(randomId),
+ MTPReplyMarkup(),
+ sentEntities,
+ MTP_int(message.action.options.scheduled),
+ (sendAs ? sendAs->input : MTP_inputPeerEmpty())
+ ), done, fail);
+ } else {
+ histories.sendPreparedMessage(
+ history,
+ action.replyTo,
+ randomId,
+ Data::Histories::PrepareMessage(
+ MTP_flags(sendFlags),
+ peer->input,
+ Data::Histories::ReplyToPlaceholder(),
+ msgText,
+ MTP_long(randomId),
+ MTPReplyMarkup(),
+ sentEntities,
+ MTP_int(action.options.scheduled),
+ (sendAs ? sendAs->input : MTP_inputPeerEmpty())
+ ), done, fail);
+ }
+ isFirst = false;
}
finishForwarding(action);
@@ -3815,7 +3880,7 @@ void ApiWrap::sendInlineResult(
? (*localMessageId)
: _session->data().nextLocalMessageId());
const auto randomId = base::RandomValue();
- const auto topicRootId = action.replyTo.msgId
+ const auto topicRootId = action.replyTo.messageId
? action.replyTo.topicRootId
: 0;
diff --git a/Telegram/SourceFiles/boxes/background_preview_box.cpp b/Telegram/SourceFiles/boxes/background_preview_box.cpp
index c83af953d..a920aa76a 100644
--- a/Telegram/SourceFiles/boxes/background_preview_box.cpp
+++ b/Telegram/SourceFiles/boxes/background_preview_box.cpp
@@ -175,7 +175,8 @@ BackgroundPreviewBox::BackgroundPreviewBox(
, _controller(controller)
, _forPeer(args.forPeer)
, _fromMessageId(args.fromMessageId)
-, _chatStyle(std::make_unique())
+, _chatStyle(std::make_unique(
+ controller->session().colorIndicesValue()))
, _serviceHistory(_controller->session().data().history(
PeerData::kServiceNotificationsId))
, _service(nullptr)
@@ -434,7 +435,7 @@ void BackgroundPreviewBox::rebuildButtons(bool dark) {
clearButtons();
addButton(_forPeer
? tr::lng_background_apply_button()
- : tr::lng_background_apply(), [=] { apply(); });
+ : tr::lng_settings_apply(), [=] { apply(); });
addButton(tr::lng_cancel(), [=] { closeBox(); });
if (!_forPeer && _paper.hasShareUrl()) {
addLeftButton(tr::lng_background_share(), [=] { share(); });
diff --git a/Telegram/SourceFiles/boxes/gift_premium_box.cpp b/Telegram/SourceFiles/boxes/gift_premium_box.cpp
index 6720339c2..c7b59f44b 100644
--- a/Telegram/SourceFiles/boxes/gift_premium_box.cpp
+++ b/Telegram/SourceFiles/boxes/gift_premium_box.cpp
@@ -8,26 +8,36 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/gift_premium_box.h"
#include "apiwrap.h"
+#include "api/api_premium.h"
#include "api/api_premium_option.h"
+#include "base/unixtime.h"
#include "base/weak_ptr.h"
+#include "boxes/peers/prepare_short_info_box.h"
#include "data/data_changes.h"
+#include "data/data_channel.h"
+#include "data/data_media_types.h" // Data::Giveaway
#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 "lang/lang_keys.h"
#include "main/main_session.h"
+#include "mainwidget.h"
#include "settings/settings_premium.h"
#include "ui/basic_click_handlers.h" // UrlClickHandler::Open.
+#include "ui/boxes/boost_box.h" // StartFireworks.
#include "ui/controls/userpic_button.h"
#include "ui/effects/premium_graphics.h"
#include "ui/effects/premium_stars_colored.h"
+#include "ui/effects/premium_top_bar.h"
#include "ui/layers/generic_box.h"
#include "ui/text/format_values.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/table_layout.h"
+#include "window/window_peer_menu.h" // ShowChooseRecipientBox.
#include "window/window_session_controller.h"
#include "styles/style_boxes.h"
#include "styles/style_layers.h"
@@ -35,6 +45,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/style_info.h"
#include "styles/style_premium.h"
+#include
+
namespace {
constexpr auto kDiscountDivider = 5.;
@@ -225,6 +237,131 @@ void GiftBox(
}, box->lifetime());
}
+struct GiftCodeLink {
+ QString text;
+ QString link;
+};
+[[nodiscard]] GiftCodeLink MakeGiftCodeLink(
+ not_null session,
+ const QString &slug) {
+ const auto path = u"giftcode/"_q + slug;
+ return {
+ session->createInternalLink(path),
+ session->createInternalLinkFull(path),
+ };
+}
+
+[[nodiscard]] object_ptr MakeLinkCopyIcon(
+ not_null parent) {
+ auto result = object_ptr(parent);
+ const auto raw = result.data();
+
+ raw->paintRequest() | rpl::start_with_next([=] {
+ auto p = QPainter(raw);
+ const auto &icon = st::giveawayGiftCodeLinkCopy;
+ const auto left = (raw->width() - icon.width()) / 2;
+ const auto top = (raw->height() - icon.height()) / 2;
+ icon.paint(p, left, top, raw->width());
+ }, raw->lifetime());
+
+ raw->resize(
+ st::giveawayGiftCodeLinkCopyWidth,
+ st::giveawayGiftCodeLinkHeight);
+
+ raw->setAttribute(Qt::WA_TransparentForMouseEvents);
+
+ return result;
+}
+
+[[nodiscard]] tr::phrase GiftDurationPhrase(int months) {
+ return (months < 12)
+ ? tr::lng_premium_gift_duration_months
+ : tr::lng_premium_gift_duration_years;
+}
+
+[[nodiscard]] object_ptr MakePeerTableValue(
+ not_null parent,
+ not_null controller,
+ PeerId id) {
+ auto result = object_ptr(parent);
+ const auto raw = result.data();
+
+ const auto &st = st::giveawayGiftCodeUserpic;
+ raw->resize(raw->width(), st.photoSize);
+
+ const auto peer = controller->session().data().peer(id);
+ const auto userpic = Ui::CreateChild(raw, peer, st);
+ const auto label = Ui::CreateChild(
+ raw,
+ peer->name(),
+ st::giveawayGiftCodeValue);
+ raw->widthValue(
+ ) | rpl::start_with_next([=](int width) {
+ const auto position = st::giveawayGiftCodeNamePosition;
+ label->resizeToNaturalWidth(width - position.x());
+ label->moveToLeft(position.x(), position.y(), width);
+ const auto top = (raw->height() - userpic->height()) / 2;
+ userpic->moveToLeft(0, top, width);
+ }, label->lifetime());
+
+ userpic->setAttribute(Qt::WA_TransparentForMouseEvents);
+ label->setAttribute(Qt::WA_TransparentForMouseEvents);
+ label->setTextColorOverride(st::windowActiveTextFg->c);
+
+ raw->setClickedCallback([=] {
+ controller->show(PrepareShortInfoBox(peer, controller));
+ });
+
+ return result;
+}
+
+void AddTableRow(
+ not_null table,
+ rpl::producer label,
+ object_ptr value,
+ style::margins valueMargins) {
+ table->addRow(
+ object_ptr(
+ table,
+ std::move(label),
+ st::giveawayGiftCodeLabel),
+ std::move(value),
+ st::giveawayGiftCodeLabelMargin,
+ valueMargins);
+}
+
+not_null AddTableRow(
+ not_null table,
+ rpl::producer label,
+ rpl::producer value) {
+ auto widget = object_ptr(
+ table,
+ std::move(value),
+ st::giveawayGiftCodeValue);
+ const auto result = widget.data();
+ AddTableRow(
+ table,
+ std::move(label),
+ std::move(widget),
+ st::giveawayGiftCodeValueMargin);
+ return result;
+}
+
+void AddTableRow(
+ not_null table,
+ rpl::producer label,
+ not_null controller,
+ PeerId id) {
+ if (!id) {
+ return;
+ }
+ AddTableRow(
+ table,
+ std::move(label),
+ MakePeerTableValue(table, controller, id),
+ st::giveawayGiftCodePeerMargin);
+}
+
} // namespace
GiftPremiumValidator::GiftPremiumValidator(
@@ -263,3 +400,405 @@ void GiftPremiumValidator::showBox(not_null user) {
_requestId = 0;
}).send();
}
+
+rpl::producer GiftDurationValue(int months) {
+ return GiftDurationPhrase(months)(
+ lt_count,
+ rpl::single(float64((months < 12) ? months : (months / 12))));
+}
+
+QString GiftDuration(int months) {
+ return GiftDurationPhrase(months)(
+ tr::now,
+ lt_count,
+ (months < 12) ? months : (months / 12));
+}
+
+void GiftCodeBox(
+ not_null box,
+ not_null controller,
+ const QString &slug) {
+ struct State {
+ rpl::variable data;
+ rpl::variable used;
+ bool sent = false;
+ };
+ const auto session = &controller->session();
+ const auto state = box->lifetime().make_state(State{});
+ state->data = session->api().premium().giftCodeValue(slug);
+ state->used = state->data.value(
+ ) | rpl::map([=](const Api::GiftCode &data) {
+ return data.used;
+ });
+
+ box->setWidth(st::boxWideWidth);
+ box->setStyle(st::giveawayGiftCodeBox);
+ box->setNoContentMargin(true);
+
+ const auto bar = box->setPinnedToTopContent(
+ object_ptr(
+ box,
+ st::giveawayGiftCodeCover,
+ nullptr,
+ rpl::conditional(
+ state->used.value(),
+ tr::lng_gift_link_used_title(),
+ tr::lng_gift_link_title()),
+ rpl::conditional(
+ state->used.value(),
+ tr::lng_gift_link_used_about(Ui::Text::RichLangValue),
+ tr::lng_gift_link_about(Ui::Text::RichLangValue)),
+ true));
+
+ const auto max = st::giveawayGiftCodeTopHeight;
+ bar->setMaximumHeight(max);
+ bar->setMinimumHeight(st::infoLayerTopBarHeight);
+
+ bar->resize(bar->width(), bar->maximumHeight());
+
+ const auto link = MakeGiftCodeLink(&controller->session(), slug);
+ box->addRow(
+ Ui::MakeLinkLabel(
+ box,
+ rpl::single(link.text),
+ rpl::single(link.link),
+ box->uiShow(),
+ MakeLinkCopyIcon(box)),
+ st::giveawayGiftCodeLinkMargin);
+
+ auto table = box->addRow(
+ object_ptr(
+ box,
+ st::giveawayGiftCodeTable),
+ st::giveawayGiftCodeTableMargin);
+ const auto current = state->data.current();
+ AddTableRow(
+ table,
+ tr::lng_gift_link_label_from(),
+ controller,
+ current.from);
+ if (current.to) {
+ AddTableRow(
+ table,
+ tr::lng_gift_link_label_to(),
+ controller,
+ current.to);
+ } else {
+ AddTableRow(
+ table,
+ tr::lng_gift_link_label_to(),
+ tr::lng_gift_link_label_to_unclaimed(Ui::Text::WithEntities));
+ }
+ AddTableRow(
+ table,
+ tr::lng_gift_link_label_gift(),
+ tr::lng_gift_link_gift_premium(
+ lt_duration,
+ GiftDurationValue(current.months) | Ui::Text::ToWithEntities(),
+ Ui::Text::WithEntities));
+ const auto reason = AddTableRow(
+ table,
+ tr::lng_gift_link_label_reason(),
+ (current.giveawayId
+ ? ((current.to
+ ? tr::lng_gift_link_reason_giveaway
+ : tr::lng_gift_link_reason_unclaimed)(
+ ) | Ui::Text::ToLink())
+ : current.giveaway
+ ? ((current.to
+ ? tr::lng_gift_link_reason_giveaway
+ : tr::lng_gift_link_reason_unclaimed)(
+ Ui::Text::WithEntities
+ ) | rpl::type_erased())
+ : tr::lng_gift_link_reason_chosen(Ui::Text::WithEntities)));
+ reason->setClickHandlerFilter([=](const auto &...) {
+ controller->showPeerHistory(
+ current.from,
+ Window::SectionShow::Way::Forward,
+ current.giveawayId);
+ return false;
+ });
+ if (current.date) {
+ AddTableRow(
+ table,
+ tr::lng_gift_link_label_date(),
+ rpl::single(Ui::Text::WithEntities(
+ langDateTime(base::unixtime::parse(current.date)))));
+ }
+
+ auto shareLink = tr::lng_gift_link_also_send_link(
+ ) | rpl::map([](const QString &text) {
+ return Ui::Text::Link(text);
+ });
+ auto richDate = [](const Api::GiftCode &data) {
+ return TextWithEntities{
+ langDateTime(base::unixtime::parse(data.used)),
+ };
+ };
+ const auto footer = box->addRow(
+ object_ptr(
+ box,
+ rpl::conditional(
+ state->used.value(),
+ tr::lng_gift_link_used_footer(
+ lt_date,
+ state->data.value() | rpl::map(richDate),
+ Ui::Text::WithEntities),
+ tr::lng_gift_link_also_send(
+ lt_link,
+ std::move(shareLink),
+ Ui::Text::WithEntities)),
+ st::giveawayGiftCodeFooter),
+ st::giveawayGiftCodeFooterMargin);
+ footer->setClickHandlerFilter([=](const auto &...) {
+ const auto chosen = [=](not_null thread) {
+ const auto content = controller->content();
+ return content->shareUrl(
+ thread,
+ MakeGiftCodeLink(&controller->session(), slug).link,
+ QString());
+ };
+ Window::ShowChooseRecipientBox(controller, chosen);
+ return false;
+ });
+
+ const auto close = Ui::CreateChild(
+ box.get(),
+ st::boxTitleClose);
+ close->setClickedCallback([=] {
+ box->closeBox();
+ });
+ box->widthValue(
+ ) | rpl::start_with_next([=](int width) {
+ close->moveToRight(0, 0);
+ }, box->lifetime());
+
+ const auto button = box->addButton(rpl::conditional(
+ state->used.value(),
+ tr::lng_box_ok(),
+ tr::lng_gift_link_use()
+ ), [=] {
+ if (state->used.current()) {
+ box->closeBox();
+ } else if (!state->sent) {
+ state->sent = true;
+ const auto done = crl::guard(box, [=](const QString &error) {
+ if (error.isEmpty()) {
+ auto copy = state->data.current();
+ copy.used = base::unixtime::now();
+ state->data = std::move(copy);
+
+ Ui::StartFireworks(box->parentWidget());
+ } else {
+ box->uiShow()->showToast(error);
+ }
+ });
+ controller->session().api().premium().applyGiftCode(slug, done);
+ }
+ });
+ const auto buttonPadding = st::giveawayGiftCodeBox.buttonPadding;
+ const auto buttonWidth = st::boxWideWidth
+ - buttonPadding.left()
+ - buttonPadding.right();
+ button->widthValue() | rpl::filter([=] {
+ return (button->widthNoMargins() != buttonWidth);
+ }) | rpl::start_with_next([=] {
+ button->resizeToWidth(buttonWidth);
+ }, button->lifetime());
+}
+
+void ResolveGiftCode(
+ not_null controller,
+ const QString &slug) {
+ const auto done = [=](Api::GiftCode code) {
+ if (!code) {
+ controller->showToast(tr::lng_gift_link_expired(tr::now));
+ } else {
+ controller->show(Box(GiftCodeBox, controller, slug));
+ }
+ };
+ controller->session().api().premium().checkGiftCode(
+ slug,
+ crl::guard(controller, done));
+}
+
+void GiveawayInfoBox(
+ not_null box,
+ not_null controller,
+ Data::Giveaway giveaway,
+ Api::GiveawayInfo info) {
+ using State = Api::GiveawayState;
+ const auto finished = (info.state == State::Finished)
+ || (info.state == State::Refunded);
+
+ box->setTitle((finished
+ ? tr::lng_prizes_end_title
+ : tr::lng_prizes_how_title)());
+
+ const auto first = !giveaway.channels.empty()
+ ? giveaway.channels.front()->name()
+ : u"channel"_q;
+ auto text = (finished
+ ? tr::lng_prizes_end_text
+ : tr::lng_prizes_how_text)(
+ tr::now,
+ lt_admins,
+ tr::lng_prizes_admins(
+ tr::now,
+ lt_count,
+ giveaway.quantity,
+ lt_channel,
+ Ui::Text::Bold(first),
+ lt_duration,
+ TextWithEntities{ GiftDuration(giveaway.months) },
+ Ui::Text::RichLangValue),
+ Ui::Text::RichLangValue);
+ const auto many = (giveaway.channels.size() > 1);
+ const auto count = info.winnersCount
+ ? info.winnersCount
+ : giveaway.quantity;
+ auto winners = giveaway.all
+ ? (many
+ ? tr::lng_prizes_winners_all_of_many
+ : tr::lng_prizes_winners_all_of_one)(
+ tr::now,
+ lt_count,
+ count,
+ lt_channel,
+ Ui::Text::Bold(first),
+ Ui::Text::RichLangValue)
+ : (many
+ ? tr::lng_prizes_winners_new_of_many
+ : tr::lng_prizes_winners_new_of_one)(
+ tr::now,
+ lt_count,
+ count,
+ lt_channel,
+ Ui::Text::Bold(first),
+ lt_start_date,
+ Ui::Text::Bold(
+ langDateTime(base::unixtime::parse(info.startDate))),
+ Ui::Text::RichLangValue);
+ 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())),
+ lt_winners,
+ winners,
+ Ui::Text::RichLangValue));
+ if (info.activatedCount > 0) {
+ text.append(' ').append(tr::lng_prizes_end_activated(
+ tr::now,
+ lt_count,
+ info.activatedCount));
+ }
+ 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) {
+
+ } else if (info.state != State::Refunded) {
+ if (info.adminChannelId) {
+ const auto channel = controller->session().data().channel(
+ info.adminChannelId);
+ text.append("\n\n").append(tr::lng_prizes_how_no_admin(
+ tr::now,
+ lt_channel,
+ Ui::Text::Bold(channel->name()),
+ Ui::Text::RichLangValue));
+ } else if (info.tooEarlyDate) {
+ text.append("\n\n").append(tr::lng_prizes_how_no_joined(
+ tr::now,
+ lt_date,
+ Ui::Text::Bold(
+ langDateTime(base::unixtime::parse(info.tooEarlyDate))),
+ Ui::Text::RichLangValue));
+ } else if (!info.disallowedCountry.isEmpty()) {
+ text.append("\n\n").append(tr::lng_prizes_how_no_country(
+ tr::now,
+ Ui::Text::RichLangValue));
+ } else if (info.participating) {
+ text.append("\n\n").append((many
+ ? tr::lng_prizes_how_yes_joined_many
+ : tr::lng_prizes_how_yes_joined_one)(
+ tr::now,
+ lt_channel,
+ Ui::Text::Bold(first),
+ Ui::Text::RichLangValue));
+ } else {
+ text.append("\n\n").append((many
+ ? tr::lng_prizes_how_participate_many
+ : tr::lng_prizes_how_participate_one)(
+ tr::now,
+ lt_channel,
+ Ui::Text::Bold(first),
+ lt_date,
+ Ui::Text::Bold(langDayOfMonthFull(
+ base::unixtime::parse(giveaway.untilDate).date())),
+ Ui::Text::RichLangValue));
+ }
+ }
+ const auto padding = st::boxPadding;
+ box->addRow(
+ object_ptr(
+ box.get(),
+ rpl::single(std::move(text)),
+ st::boxLabel),
+ { padding.left(), 0, padding.right(), padding.bottom() });
+
+ if (info.state == State::Refunded) {
+ const auto wrap = box->addRow(
+ object_ptr>(
+ box.get(),
+ object_ptr(
+ box.get(),
+ tr::lng_prizes_cancelled(),
+ st::giveawayRefundedLabel),
+ st::giveawayRefundedPadding),
+ { padding.left(), 0, padding.right(), padding.bottom() });
+ const auto bg = wrap->lifetime().make_state(
+ st::boxRadius,
+ st::attentionBoxButton.textBgOver);
+ wrap->paintRequest() | rpl::start_with_next([=] {
+ auto p = QPainter(wrap);
+ bg->paint(p, wrap->rect());
+ }, wrap->lifetime());
+ }
+ if (const auto slug = info.giftCode; !slug.isEmpty()) {
+ box->addButton(tr::lng_prizes_view_prize(), [=] {
+ ResolveGiftCode(controller, slug);
+ });
+ box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
+ } else {
+ box->addButton(tr::lng_close(), [=] { box->closeBox(); });
+ }
+}
+
+void ResolveGiveawayInfo(
+ not_null controller,
+ not_null peer,
+ MsgId messageId,
+ Data::Giveaway giveaway) {
+ const auto show = [=](Api::GiveawayInfo info) {
+ if (!info) {
+ controller->showToast(
+ tr::lng_confirm_phone_link_invalid(tr::now));
+ } else {
+ controller->show(
+ Box(GiveawayInfoBox, controller, giveaway, info));
+ }
+ };
+ controller->session().api().premium().resolveGiveawayInfo(
+ peer,
+ messageId,
+ crl::guard(controller, show));
+}
diff --git a/Telegram/SourceFiles/boxes/gift_premium_box.h b/Telegram/SourceFiles/boxes/gift_premium_box.h
index cc11b2388..3f6d273bc 100644
--- a/Telegram/SourceFiles/boxes/gift_premium_box.h
+++ b/Telegram/SourceFiles/boxes/gift_premium_box.h
@@ -11,6 +11,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
class UserData;
+namespace Api {
+struct GiftCode;
+} // namespace Api
+
+namespace Data {
+struct Giveaway;
+} // namespace Data
+
+namespace Ui {
+class GenericBox;
+} // namespace Ui
+
namespace Window {
class SessionController;
} // namespace Window
@@ -29,3 +41,20 @@ private:
mtpRequestId _requestId = 0;
};
+
+[[nodiscard]] rpl::producer GiftDurationValue(int months);
+[[nodiscard]] QString GiftDuration(int months);
+
+void GiftCodeBox(
+ not_null box,
+ not_null controller,
+ const QString &slug);
+void ResolveGiftCode(
+ not_null controller,
+ const QString &slug);
+
+void ResolveGiveawayInfo(
+ not_null controller,
+ not_null peer,
+ MsgId messageId,
+ Data::Giveaway giveaway);
diff --git a/Telegram/SourceFiles/boxes/peer_list_box.cpp b/Telegram/SourceFiles/boxes/peer_list_box.cpp
index 28286f377..2c1e361fb 100644
--- a/Telegram/SourceFiles/boxes/peer_list_box.cpp
+++ b/Telegram/SourceFiles/boxes/peer_list_box.cpp
@@ -456,7 +456,7 @@ int PeerListController::descriptionTopSkipMin() const {
void PeerListBox::addSelectItem(
not_null peer,
anim::type animated) {
- const auto respect = _controller->respectSavedMessagesChat();
+ const auto respect = !_controller->savedMessagesChatStatus().isEmpty();
const auto text = (respect && peer->isSelf())
? tr::lng_saved_short(tr::now)
: (respect && peer->isRepliesChat())
@@ -579,8 +579,8 @@ void PeerListRow::refreshStatus() {
_statusType = StatusType::LastSeen;
_statusValidTill = 0;
if (auto user = peer()->asUser()) {
- if (_isSavedMessagesChat) {
- setStatusText(tr::lng_saved_forward_here(tr::now));
+ if (!_savedMessagesStatus.isEmpty()) {
+ setStatusText(_savedMessagesStatus);
} else {
auto time = base::unixtime::now();
setStatusText(Data::OnlineText(user, time));
@@ -613,7 +613,7 @@ void PeerListRow::refreshName(const style::PeerListItem &st) {
if (!_initialized) {
return;
}
- const auto text = _isSavedMessagesChat
+ const auto text = !_savedMessagesStatus.isEmpty()
? tr::lng_saved_messages(tr::now)
: _isRepliesMessagesChat
? tr::lng_replies_messages(tr::now)
@@ -683,7 +683,7 @@ QString PeerListRow::generateName() {
}
QString PeerListRow::generateShortName() {
- return _isSavedMessagesChat
+ return !_savedMessagesStatus.isEmpty()
? tr::lng_saved_short(tr::now)
: _isRepliesMessagesChat
? tr::lng_replies_messages(tr::now)
@@ -699,7 +699,7 @@ Ui::PeerUserpicView &PeerListRow::ensureUserpicView() {
PaintRoundImageCallback PeerListRow::generatePaintUserpicCallback(
bool forceRound) {
- const auto saved = _isSavedMessagesChat;
+ const auto saved = !_savedMessagesStatus.isEmpty();
const auto replies = _isRepliesMessagesChat;
const auto peer = this->peer();
auto userpic = saved ? Ui::PeerUserpicView() : ensureUserpicView();
@@ -745,7 +745,9 @@ int PeerListRow::paintNameIconGetWidth(
int availableWidth,
int outerWidth,
bool selected) {
- if (special() || _isSavedMessagesChat || _isRepliesMessagesChat) {
+ if (special()
+ || !_savedMessagesStatus.isEmpty()
+ || _isRepliesMessagesChat) {
return 0;
}
return _bagde.drawGetWidth(
@@ -855,7 +857,7 @@ void PeerListRow::paintDisabledCheckUserpic(
auto iconBorderPen = st.checkbox.check.border->p;
iconBorderPen.setWidth(st.checkbox.selectWidth);
- if (_isSavedMessagesChat) {
+ if (!_savedMessagesStatus.isEmpty()) {
Ui::EmptyUserpic::PaintSavedMessages(p, userpicLeft, userpicTop, outerWidth, userpicRadius * 2);
} else if (_isRepliesMessagesChat) {
Ui::EmptyUserpic::PaintRepliesMessages(p, userpicLeft, userpicTop, outerWidth, userpicRadius * 2);
@@ -1046,9 +1048,10 @@ void PeerListContent::setRowHidden(not_null row, bool hidden) {
}
void PeerListContent::addRowEntry(not_null row) {
- if (_controller->respectSavedMessagesChat() && !row->special()) {
+ const auto savedMessagesStatus = _controller->savedMessagesChatStatus();
+ if (!savedMessagesStatus.isEmpty() && !row->special()) {
if (row->peer()->isSelf()) {
- row->setIsSavedMessagesChat(true);
+ row->setSavedMessagesChatStatus(savedMessagesStatus);
} else if (row->peer()->isRepliesChat()) {
row->setIsRepliesMessagesChat(true);
}
diff --git a/Telegram/SourceFiles/boxes/peer_list_box.h b/Telegram/SourceFiles/boxes/peer_list_box.h
index 837bb6e2c..6dfce4630 100644
--- a/Telegram/SourceFiles/boxes/peer_list_box.h
+++ b/Telegram/SourceFiles/boxes/peer_list_box.h
@@ -185,8 +185,8 @@ public:
void setIsSearchResult(bool isSearchResult) {
_isSearchResult = isSearchResult;
}
- void setIsSavedMessagesChat(bool isSavedMessagesChat) {
- _isSavedMessagesChat = isSavedMessagesChat;
+ void setSavedMessagesChatStatus(QString savedMessagesStatus) {
+ _savedMessagesStatus = savedMessagesStatus;
}
void setIsRepliesMessagesChat(bool isRepliesMessagesChat) {
_isRepliesMessagesChat = isRepliesMessagesChat;
@@ -278,12 +278,12 @@ private:
StatusType _statusType = StatusType::Online;
crl::time _statusValidTill = 0;
base::flat_set _nameFirstLetters;
+ QString _savedMessagesStatus;
int _absoluteIndex = -1;
State _disabledState = State::Active;
bool _hidden : 1 = false;
bool _initialized : 1 = false;
bool _isSearchResult : 1 = false;
- bool _isSavedMessagesChat : 1 = false;
bool _isRepliesMessagesChat : 1 = false;
};
@@ -517,8 +517,8 @@ public:
void peerListSearchAddRow(PeerListRowId id) override;
void peerListSearchRefreshRows() override;
- [[nodiscard]] virtual bool respectSavedMessagesChat() const {
- return false;
+ [[nodiscard]] virtual QString savedMessagesChatStatus() const {
+ return QString();
}
[[nodiscard]] virtual int customRowHeight() {
Unexpected("PeerListController::customRowHeight.");
diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
index 3e2c3abcd..df1a24fba 100644
--- a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
+++ b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp
@@ -313,7 +313,7 @@ void ChatsListBoxController::rebuildRows() {
return count;
};
auto added = 0;
- if (respectSavedMessagesChat()) {
+ if (!savedMessagesChatStatus().isEmpty()) {
if (appendRow(session().data().history(session().user()))) {
++added;
}
@@ -330,7 +330,7 @@ void ChatsListBoxController::rebuildRows() {
const auto history = static_cast(a).history();
return history->inChatList();
});
- if (respectSavedMessagesChat()) {
+ if (!savedMessagesChatStatus().isEmpty()) {
delegate()->peerListPartitionRows([](const PeerListRow &a) {
return a.peer()->isSelf();
});
@@ -696,6 +696,10 @@ void ChooseRecipientBoxController::rowClicked(not_null row) {
}
}
+QString ChooseRecipientBoxController::savedMessagesChatStatus() const {
+ return tr::lng_saved_forward_here(tr::now);
+}
+
auto ChooseRecipientBoxController::createRow(
not_null history) -> std::unique_ptr {
const auto peer = history->peer;
diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.h b/Telegram/SourceFiles/boxes/peer_list_controllers.h
index 35b4e0f15..5d8ef334a 100644
--- a/Telegram/SourceFiles/boxes/peer_list_controllers.h
+++ b/Telegram/SourceFiles/boxes/peer_list_controllers.h
@@ -218,9 +218,7 @@ public:
Main::Session &session() const override;
void rowClicked(not_null row) override;
- bool respectSavedMessagesChat() const override {
- return true;
- }
+ QString savedMessagesChatStatus() const override;
protected:
void prepareViewHook() override;
diff --git a/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp b/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp
index 60031e0ce..767b0c915 100644
--- a/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp
+++ b/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp
@@ -45,8 +45,8 @@ public:
Main::Session &session() const override;
void rowClicked(not_null row) override;
- bool respectSavedMessagesChat() const override {
- return true;
+ QString savedMessagesChatStatus() const override {
+ return tr::lng_saved_forward_here(tr::now);
}
private:
diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp
new file mode 100644
index 000000000..1eede85e3
--- /dev/null
+++ b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp
@@ -0,0 +1,936 @@
+/*
+This file is part of Telegram Desktop,
+the official desktop application for the Telegram messaging service.
+
+For license and copyright information please follow this link:
+https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
+*/
+#include "boxes/peers/edit_peer_color_box.h"
+
+#include "apiwrap.h"
+#include "base/unixtime.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_peer.h"
+#include "data/data_session.h"
+#include "data/data_web_page.h"
+#include "history/view/history_view_element.h"
+#include "history/history.h"
+#include "history/history_item.h"
+#include "info/boosts/info_boosts_widget.h"
+#include "info/profile/info_profile_emoji_status_panel.h"
+#include "info/info_memento.h"
+#include "lang/lang_keys.h"
+#include "main/main_account.h"
+#include "main/main_app_config.h"
+#include "main/main_session.h"
+#include "settings/settings_common.h"
+#include "settings/settings_premium.h"
+#include "ui/boxes/boost_box.h"
+#include "ui/chat/chat_style.h"
+#include "ui/chat/chat_theme.h"
+#include "ui/effects/path_shift_gradient.h"
+#include "ui/layers/generic_box.h"
+#include "ui/text/text_utilities.h"
+#include "ui/widgets/buttons.h"
+#include "ui/painter.h"
+#include "window/themes/window_theme.h"
+#include "window/section_widget.h"
+#include "window/window_session_controller.h"
+#include "styles/style_chat.h"
+#include "styles/style_layers.h"
+#include "styles/style_menu_icons.h"
+#include "styles/style_settings.h"
+#include "styles/style_widgets.h"
+
+namespace {
+
+using namespace Settings;
+
+constexpr auto kFakeChannelId = ChannelId(0xFFFFFFF000ULL);
+constexpr auto kFakeWebPageId = WebPageId(0xFFFFFFFF00000000ULL);
+constexpr auto kSelectAnimationDuration = crl::time(150);
+
+class ColorSample final : public Ui::AbstractButton {
+public:
+ ColorSample(
+ not_null parent,
+ std::shared_ptr style,
+ rpl::producer colorIndex,
+ const QString &name);
+ ColorSample(
+ not_null parent,
+ std::shared_ptr style,
+ uint8 colorIndex,
+ bool selected);
+
+ [[nodiscard]] uint8 index() const;
+ int naturalWidth() const override;
+
+ void setSelected(bool selected);
+
+private:
+ void paintEvent(QPaintEvent *e) override;
+
+ std::shared_ptr _style;
+ Ui::Text::String _name;
+ uint8 _index = 0;
+ Ui::Animations::Simple _selectAnimation;
+ bool _selected = false;
+ bool _simple = false;
+
+};
+
+class PreviewDelegate final : public HistoryView::DefaultElementDelegate {
+public:
+ PreviewDelegate(
+ not_null parent,
+ not_null st,
+ Fn update);
+
+ bool elementAnimationsPaused() override;
+ not_null elementPathShiftGradient() override;
+ HistoryView::Context elementContext() override;
+
+private:
+ const not_null _parent;
+ const std::unique_ptr _pathGradient;
+
+};
+
+class PreviewWrap final : public Ui::RpWidget {
+public:
+ PreviewWrap(
+ not_null box,
+ std::shared_ptr style,
+ std::shared_ptr theme,
+ not_null peer,
+ rpl::producer colorIndexValue,
+ rpl::producer backgroundEmojiId);
+ ~PreviewWrap();
+
+private:
+ using Element = HistoryView::Element;
+
+ void paintEvent(QPaintEvent *e) override;
+
+ void initElements();
+
+ const not_null _box;
+ const not_null _peer;
+ const not_null _fake;
+ const not_null _history;
+ const not_null _webpage;
+ const std::shared_ptr _theme;
+ const std::shared_ptr _style;
+ const std::unique_ptr _delegate;
+ const not_null _replyToItem;
+ const not_null _replyItem;
+ std::unique_ptr _element;
+ Ui::PeerUserpicView _userpic;
+ QPoint _position;
+
+};
+
+ColorSample::ColorSample(
+ not_null parent,
+ std::shared_ptr style,
+ rpl::producer colorIndex,
+ const QString &name)
+: AbstractButton(parent)
+, _style(style)
+, _name(st::semiboldTextStyle, name) {
+ std::move(
+ colorIndex
+ ) | rpl::start_with_next([=](uint8 index) {
+ _index = index;
+ update();
+ }, lifetime());
+}
+
+ColorSample::ColorSample(
+ not_null parent,
+ std::shared_ptr style,
+ uint8 colorIndex,
+ bool selected)
+: AbstractButton(parent)
+, _style(style)
+, _index(colorIndex)
+, _selected(selected)
+, _simple(true) {
+}
+
+void ColorSample::setSelected(bool selected) {
+ if (_selected == selected) {
+ return;
+ }
+ _selected = selected;
+ _selectAnimation.start(
+ [=] { update(); },
+ _selected ? 0. : 1.,
+ _selected ? 1. : 0.,
+ kSelectAnimationDuration);
+}
+
+void ColorSample::paintEvent(QPaintEvent *e) {
+ auto p = Painter(this);
+ auto hq = PainterHighQualityEnabler(p);
+ const auto colors = _style->coloredValues(false, _index);
+ if (!_simple && !colors.outlines[1].alpha()) {
+ const auto radius = height() / 2;
+ p.setPen(Qt::NoPen);
+ p.setBrush(colors.bg);
+ p.drawRoundedRect(rect(), radius, radius);
+
+ const auto padding = st::settingsColorSamplePadding;
+ p.setPen(colors.name);
+ p.setBrush(Qt::NoBrush);
+ p.setFont(st::semiboldFont);
+ _name.drawLeftElided(
+ p,
+ padding.left(),
+ padding.top(),
+ width() - padding.left() - padding.right(),
+ width(),
+ 1,
+ style::al_top);
+ } else {
+ const auto size = float64(width());
+ const auto half = size / 2.;
+ const auto full = QRectF(-half, -half, size, size);
+ p.translate(size / 2., size / 2.);
+ p.setPen(Qt::NoPen);
+ if (colors.outlines[1].alpha()) {
+ p.rotate(-45.);
+ p.setClipRect(-size, 0, 3 * size, size);
+ p.setBrush(colors.outlines[1]);
+ p.drawEllipse(full);
+ p.setClipRect(-size, -size, 3 * size, size);
+ }
+ p.setBrush(colors.outlines[0]);
+ p.drawEllipse(full);
+ p.setClipping(false);
+ if (colors.outlines[2].alpha()) {
+ const auto multiplier = size / st::settingsColorSampleSize;
+ const auto center = st::settingsColorSampleCenter * multiplier;
+ const auto radius = st::settingsColorSampleCenterRadius
+ * multiplier;
+ p.setBrush(colors.outlines[2]);
+ p.drawRoundedRect(
+ QRectF(-center / 2., -center / 2., center, center),
+ radius,
+ radius);
+ }
+ const auto selected = _selectAnimation.value(_selected ? 1. : 0.);
+ if (selected > 0) {
+ const auto line = st::settingsColorRadioStroke * 1.;
+ const auto thickness = selected * line;
+ auto pen = st::boxBg->p;
+ pen.setWidthF(thickness);
+ p.setBrush(Qt::NoBrush);
+ p.setPen(pen);
+ const auto skip = 1.5 * line;
+ p.drawEllipse(full.marginsRemoved({ skip, skip, skip, skip }));
+ }
+ }
+}
+
+uint8 ColorSample::index() const {
+ return _index;
+}
+
+int ColorSample::naturalWidth() const {
+ if (_name.isEmpty() || _style->colorPatternIndex(_index)) {
+ return st::settingsColorSampleSize;
+ }
+ const auto padding = st::settingsColorSamplePadding;
+ return std::max(
+ padding.left() + _name.maxWidth() + padding.right(),
+ padding.top() + st::semiboldFont->height + padding.bottom());
+}
+
+PreviewWrap::PreviewWrap(
+ not_null box,
+ std::shared_ptr style,
+ std::shared_ptr theme,
+ not_null peer,
+ rpl::producer colorIndexValue,
+ rpl::producer backgroundEmojiId)
+: RpWidget(box)
+, _box(box)
+, _peer(peer)
+, _fake(_peer->owner().channel(kFakeChannelId))
+, _history(_fake->owner().history(_fake))
+, _webpage(_peer->owner().webpage(
+ kFakeWebPageId,
+ WebPageType::Article,
+ u"internal:peer-color-webpage-preview"_q,
+ u"internal:peer-color-webpage-preview"_q,
+ tr::lng_settings_color_link_name(tr::now),
+ tr::lng_settings_color_link_title(tr::now),
+ { tr::lng_settings_color_link_description(tr::now) },
+ nullptr, // photo
+ nullptr, // document
+ WebPageCollage(),
+ 0, // duration
+ QString(), // author
+ false, // hasLargeMedia
+ 0)) // pendingTill
+, _theme(theme)
+, _style(style)
+, _delegate(std::make_unique(box, _style.get(), [=] {
+ update();
+}))
+, _replyToItem(_history->addNewLocalMessage(
+ _history->nextNonHistoryEntryId(),
+ (MessageFlag::FakeHistoryItem
+ | MessageFlag::HasFromId
+ | MessageFlag::Post),
+ UserId(), // via
+ FullReplyTo(),
+ base::unixtime::now(), // date
+ _fake->id,
+ QString(), // postAuthor
+ TextWithEntities{ _peer->isSelf()
+ ? tr::lng_settings_color_reply(tr::now)
+ : tr::lng_settings_color_reply_channel(tr::now),
+ },
+ MTP_messageMediaEmpty(),
+ HistoryMessageMarkupData(),
+ uint64(0)))
+, _replyItem(_history->addNewLocalMessage(
+ _history->nextNonHistoryEntryId(),
+ (MessageFlag::FakeHistoryItem
+ | MessageFlag::HasFromId
+ | MessageFlag::HasReplyInfo
+ | MessageFlag::Post),
+ UserId(), // via
+ FullReplyTo{ .messageId = _replyToItem->fullId() },
+ base::unixtime::now(), // date
+ _fake->id,
+ QString(), // postAuthor
+ TextWithEntities{ _peer->isSelf()
+ ? tr::lng_settings_color_text(tr::now)
+ : tr::lng_settings_color_text_channel(tr::now),
+ },
+ MTP_messageMediaWebPage(
+ MTP_flags(0),
+ MTP_webPagePending(
+ MTP_flags(0),
+ MTP_long(_webpage->id),
+ MTPstring(),
+ MTP_int(0))),
+ HistoryMessageMarkupData(),
+ uint64(0)))
+, _element(_replyItem->createView(_delegate.get()))
+, _position(0, st::msgMargin.bottom()) {
+ _style->apply(_theme.get());
+
+ _fake->setName(peer->name(), QString());
+ std::move(colorIndexValue) | rpl::start_with_next([=](uint8 index) {
+ _fake->changeColorIndex(index);
+ update();
+ }, lifetime());
+ std::move(backgroundEmojiId) | rpl::start_with_next([=](DocumentId id) {
+ _fake->changeBackgroundEmojiId(id);
+ update();
+ }, lifetime());
+
+ const auto session = &_history->session();
+ session->data().viewRepaintRequest(
+ ) | rpl::start_with_next([=](not_null view) {
+ if (view == _element.get()) {
+ update();
+ }
+ }, lifetime());
+
+ initElements();
+}
+
+PreviewWrap::~PreviewWrap() {
+ _element = nullptr;
+ _replyItem->destroy();
+ _replyToItem->destroy();
+}
+
+void PreviewWrap::paintEvent(QPaintEvent *e) {
+ auto p = Painter(this);
+ const auto clip = e->rect();
+
+ p.setClipRect(clip);
+ Window::SectionWidget::PaintBackground(
+ p,
+ _theme.get(),
+ QSize(_box->width(), _box->window()->height()),
+ clip);
+
+ auto context = _theme->preparePaintContext(
+ _style.get(),
+ rect(),
+ clip,
+ !window()->isActiveWindow());
+
+ p.translate(_position);
+ _element->draw(p, context);
+
+ if (_element->displayFromPhoto()) {
+ auto userpicBottom = height()
+ - _element->marginBottom()
+ - _element->marginTop();
+ const auto userpicTop = userpicBottom - st::msgPhotoSize;
+ _peer->paintUserpicLeft(
+ p,
+ _userpic,
+ st::historyPhotoLeft,
+ userpicTop,
+ width(),
+ st::msgPhotoSize);
+ }
+}
+
+void PreviewWrap::initElements() {
+ _element->initDimensions();
+
+ widthValue(
+ ) | rpl::filter([=](int width) {
+ return width > st::msgMinWidth;
+ }) | rpl::start_with_next([=](int width) {
+ const auto height = _position.y()
+ + _element->resizeGetHeight(width)
+ + st::msgMargin.top();
+ resize(width, height);
+ }, lifetime());
+}
+
+PreviewDelegate::PreviewDelegate(
+ not_null parent,
+ not_null st,
+ Fn update)
+: _parent(parent)
+, _pathGradient(HistoryView::MakePathShiftGradient(st, update)) {
+}
+
+bool PreviewDelegate::elementAnimationsPaused() {
+ return _parent->window()->isActiveWindow();
+}
+
+auto PreviewDelegate::elementPathShiftGradient()
+-> not_null {
+ return _pathGradient.get();
+}
+
+HistoryView::Context PreviewDelegate::elementContext() {
+ return HistoryView::Context::AdminLog;
+}
+
+void Set(
+ std::shared_ptr show,
+ not_null peer,
+ uint8 colorIndex,
+ DocumentId backgroundEmojiId) {
+ const auto wasIndex = peer->colorIndex();
+ const auto wasEmojiId = peer->backgroundEmojiId();
+
+ const auto setLocal = [=](uint8 index, DocumentId emojiId) {
+ using UpdateFlag = Data::PeerUpdate::Flag;
+ peer->changeColorIndex(index);
+ peer->changeBackgroundEmojiId(emojiId);
+ peer->session().changes().peerUpdated(
+ peer,
+ UpdateFlag::Color | UpdateFlag::BackgroundEmoji);
+ };
+ setLocal(colorIndex, backgroundEmojiId);
+
+ const auto done = [=] {
+ show->showToast(peer->isSelf()
+ ? tr::lng_settings_color_changed(tr::now)
+ : tr::lng_settings_color_changed_channel(tr::now));
+ };
+ const auto fail = [=](const MTP::Error &error) {
+ setLocal(wasIndex, wasEmojiId);
+ show->showToast(error.type());
+ };
+ const auto send = [&](auto &&request) {
+ peer->session().api().request(
+ std::move(request)
+ ).done(done).fail(fail).send();
+ };
+ if (peer->isSelf()) {
+ send(MTPaccount_UpdateColor(
+ MTP_flags(
+ MTPaccount_UpdateColor::Flag::f_background_emoji_id),
+ MTP_int(colorIndex),
+ MTP_long(backgroundEmojiId)));
+ } else if (const auto channel = peer->asChannel()) {
+ send(MTPchannels_UpdateColor(
+ MTP_flags(
+ MTPchannels_UpdateColor::Flag::f_background_emoji_id),
+ channel->inputChannel,
+ MTP_int(colorIndex),
+ MTP_long(backgroundEmojiId)));
+ } else {
+ Unexpected("Invalid peer type in Set(colorIndex).");
+ }
+}
+
+void Apply(
+ std::shared_ptr show,
+ not_null peer,
+ uint8 colorIndex,
+ DocumentId backgroundEmojiId,
+ Fn close,
+ Fn cancel) {
+ const auto session = &peer->session();
+ if (peer->colorIndex() == colorIndex
+ && peer->backgroundEmojiId() == backgroundEmojiId) {
+ close();
+ } else if (peer->isSelf() && !session->premium()) {
+ Settings::ShowPremiumPromoToast(
+ show,
+ tr::lng_settings_color_subscribe(
+ tr::now,
+ lt_link,
+ Ui::Text::Link(
+ Ui::Text::Bold(
+ tr::lng_send_as_premium_required_link(tr::now))),
+ Ui::Text::WithEntities),
+ u"name_color"_q);
+ cancel();
+ } else if (peer->isSelf()) {
+ Set(show, peer, colorIndex, backgroundEmojiId);
+ 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);
+ close();
+ return;
+ }
+ const auto next = data.vnext_level_boosts().value_or_empty();
+ const auto openStatistics = [=] {
+ if (const auto controller = show->resolveWindow(
+ ChatHelpers::WindowUsage::PremiumPromo)) {
+ controller->showSection(Info::Boosts::Make(peer));
+ }
+ };
+ show->show(Box(Ui::AskBoostBox, Ui::AskBoostBoxData{
+ .link = qs(data.vboost_url()),
+ .boost = {
+ .level = data.vlevel().v,
+ .boosts = data.vboosts().v,
+ .thisLevelBoosts = data.vcurrent_level_boosts().v,
+ .nextLevelBoosts = next,
+ },
+ .requiredLevel = required,
+ }, openStatistics, nullptr));
+ cancel();
+ }).fail([=](const MTP::Error &error) {
+ show->showToast(error.type());
+ cancel();
+ }).send();
+ }
+}
+
+class ColorSelector final : public Ui::RpWidget {
+public:
+ ColorSelector(
+ not_null box,
+ std::shared_ptr style,
+ rpl::producer> indices,
+ uint8 index,
+ Fn callback);
+
+private:
+ void fillFrom(std::vector indices);
+
+ int resizeGetHeight(int newWidth) override;
+
+ const std::shared_ptr _style;
+ std::vector> _samples;
+ const Fn _callback;
+ uint8 _index = 0;
+
+};
+
+ColorSelector::ColorSelector(
+ not_null box,
+ std::shared_ptr style,
+ rpl::producer> indices,
+ uint8 index,
+ Fn callback)
+: RpWidget(box)
+, _style(style)
+, _callback(std::move(callback))
+, _index(index) {
+ std::move(
+ indices
+ ) | rpl::start_with_next([=](std::vector indices) {
+ fillFrom(std::move(indices));
+ }, lifetime());
+}
+
+void ColorSelector::fillFrom(std::vector indices) {
+ auto samples = std::vector>();
+ const auto add = [&](uint8 index) {
+ auto i = ranges::find(_samples, index, &ColorSample::index);
+ if (i != end(_samples)) {
+ samples.push_back(std::move(*i));
+ _samples.erase(i);
+ } else {
+ samples.push_back(std::make_unique(
+ this,
+ _style,
+ index,
+ index == _index));
+ samples.back()->show();
+ samples.back()->setClickedCallback([=] {
+ if (_index != index) {
+ _callback(index);
+
+ ranges::find(
+ _samples,
+ _index,
+ &ColorSample::index
+ )->get()->setSelected(false);
+ _index = index;
+ ranges::find(
+ _samples,
+ _index,
+ &ColorSample::index
+ )->get()->setSelected(true);
+ }
+ });
+ }
+ };
+ for (const auto index : indices) {
+ add(index);
+ }
+ if (!ranges::contains(indices, _index)) {
+ add(_index);
+ }
+ _samples = std::move(samples);
+ if (width() > 0) {
+ resizeToWidth(width());
+ }
+}
+
+int ColorSelector::resizeGetHeight(int newWidth) {
+ if (newWidth <= 0) {
+ return 0;
+ }
+ const auto count = int(_samples.size());
+ const auto columns = Ui::kSimpleColorIndexCount;
+ const auto skip = st::settingsColorRadioSkip;
+ const auto size = (newWidth - skip * (columns - 1)) / float64(columns);
+ const auto isize = int(base::SafeRound(size));
+ auto top = 0;
+ auto left = 0.;
+ for (auto i = 0; i != count; ++i) {
+ _samples[i]->resize(isize, isize);
+ _samples[i]->move(int(base::SafeRound(left)), top);
+ left += size + skip;
+ if (!((i + 1) % columns)) {
+ top += isize + skip;
+ left = 0.;
+ }
+ }
+ return (top - skip) + ((count % columns) ? (isize + skip) : 0);
+}
+
+[[nodiscard]] object_ptr CreateEmojiIconButton(
+ not_null parent,
+ std::shared_ptr show,
+ std::shared_ptr style,
+ rpl::producer colorIndexValue,
+ rpl::producer emojiIdValue,
+ Fn emojiIdChosen) {
+ 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 = CreateButton(
+ parent,
+ tr::lng_settings_color_emoji(),
+ *st,
+ {});
+ const auto raw = result.data();
+
+ const auto right = Ui::CreateChild(raw);
+ right->show();
+
+ struct State {
+ Info::Profile::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());
+
+ std::move(colorIndexValue) | rpl::start_with_next([=](uint8 index) {
+ state->index = index;
+ if (state->emoji) {
+ right->update();
+ }
+ }, right->lifetime());
+
+ const auto session = &show->session();
+ std::move(emojiIdValue) | rpl::start_with_next([=](DocumentId emojiId) {
+ state->emojiId = emojiId;
+ state->emoji = emojiId
+ ? session->data().customEmojiManager().create(
+ emojiId,
+ [=] { right->update(); })
+ : nullptr;
+ right->resize(
+ (emojiId ? 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) {
+ const auto colors = style->coloredValues(false, state->index);
+ state->emoji->paint(p, {
+ .textColor = colors.name,
+ .position = QPoint(added, (height - emojiSize) / 2),
+ .internal = {
+ .forceFirstFrame = true,
+ },
+ });
+ } else {
+ const auto &font = st::normalFont;
+ p.setFont(font);
+ p.setPen(style->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 customTextColor = [=] {
+ return style->coloredValues(false, state->index).name;
+ };
+ const auto controller = show->resolveWindow(
+ ChatHelpers::WindowUsage::PremiumPromo);
+ if (controller) {
+ state->panel.show({
+ .controller = controller,
+ .button = right,
+ .currentBackgroundEmojiId = state->emojiId,
+ .customTextColor = customTextColor,
+ .backgroundEmojiMode = true,
+ });
+ }
+ });
+
+ return result;
+}
+
+} // namespace
+
+void EditPeerColorBox(
+ not_null box,
+ std::shared_ptr show,
+ not_null peer,
+ std::shared_ptr style,
+ std::shared_ptr theme) {
+ box->setTitle(tr::lng_settings_color_title());
+ box->setWidth(st::boxWideWidth);
+
+ struct State {
+ rpl::variable index;
+ rpl::variable emojiId;
+ bool changing = false;
+ bool applying = false;
+ };
+ const auto state = box->lifetime().make_state();
+ state->index = peer->colorIndex();
+ state->emojiId = peer->backgroundEmojiId();
+
+ box->addRow(object_ptr(
+ box,
+ style,
+ theme,
+ peer,
+ state->index.value(),
+ state->emojiId.value()
+ ), {});
+
+ const auto appConfig = &peer->session().account().appConfig();
+ auto indices = rpl::single(
+ rpl::empty
+ ) | rpl::then(
+ appConfig->refreshed()
+ ) | rpl::map([=] {
+ const auto list = appConfig->get>(
+ "peer_colors_available",
+ { 0, 1, 2, 3, 4, 5, 6 });
+ return list | ranges::views::transform([](int i) {
+ return uint8(i);
+ }) | ranges::to_vector;
+ });
+ const auto margin = st::settingsColorRadioMargin;
+ const auto skip = st::settingsColorRadioSkip;
+ box->addRow(
+ object_ptr(
+ box,
+ style,
+ std::move(indices),
+ state->index.current(),
+ [=](uint8 index) { state->index = index; }),
+ { margin, skip, margin, skip });
+
+ const auto container = box->verticalLayout();
+ AddDividerText(container, peer->isSelf()
+ ? tr::lng_settings_color_about()
+ : tr::lng_settings_color_about_channel());
+
+ AddSkip(container, st::settingsColorSampleSkip);
+
+ container->add(CreateEmojiIconButton(
+ container,
+ show,
+ style,
+ state->index.value(),
+ state->emojiId.value(),
+ [=](DocumentId id) { state->emojiId = id; }));
+
+ AddSkip(container, st::settingsColorSampleSkip);
+ AddDividerText(container, peer->isSelf()
+ ? tr::lng_settings_color_emoji_about()
+ : tr::lng_settings_color_emoji_about_channel());
+
+ 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, [=] {
+ box->closeBox();
+ }), crl::guard(box, [=] {
+ state->applying = false;
+ }));
+ });
+ box->addButton(tr::lng_cancel(), [=] {
+ box->closeBox();
+ });
+}
+
+void AddPeerColorButton(
+ not_null container,
+ std::shared_ptr show,
+ not_null peer) {
+ const auto button = AddButton(
+ container,
+ (peer->isSelf()
+ ? tr::lng_settings_theme_name_color()
+ : tr::lng_edit_channel_color()),
+ st::settingsColorButton,
+ { &st::menuIconChangeColors });
+
+ auto colorIndexValue = peer->session().changes().peerFlagsValue(
+ peer,
+ Data::PeerUpdate::Flag::Color
+ ) | rpl::map([=] {
+ return peer->colorIndex();
+ });
+ const auto name = peer->shortName();
+
+ const auto style = std::make_shared(
+ peer->session().colorIndicesValue());
+ const auto theme = std::shared_ptr(
+ Window::Theme::DefaultChatThemeOn(button->lifetime()));
+ style->apply(theme.get());
+
+ const auto sample = Ui::CreateChild(
+ button.get(),
+ style,
+ rpl::duplicate(colorIndexValue),
+ name);
+ sample->show();
+
+ rpl::combine(
+ button->widthValue(),
+ tr::lng_settings_theme_name_color(),
+ rpl::duplicate(colorIndexValue)
+ ) | rpl::start_with_next([=](
+ int width,
+ const QString &button,
+ int colorIndex) {
+ const auto sampleSize = st::settingsColorSampleSize;
+ const auto available = width
+ - st::settingsButton.padding.left()
+ - (st::settingsColorButton.padding.right() - sampleSize)
+ - st::settingsButton.style.font->width(button)
+ - st::settingsButtonRightSkip;
+ if (style->colorPatternIndex(colorIndex)) {
+ sample->resize(sampleSize, sampleSize);
+ } else {
+ const auto padding = st::settingsColorSamplePadding;
+ const auto wantedHeight = padding.top()
+ + st::semiboldFont->height
+ + padding.bottom();
+ const auto wantedWidth = sample->naturalWidth();
+ sample->resize(std::min(wantedWidth, available), wantedHeight);
+ }
+ sample->update();
+ }, sample->lifetime());
+
+ rpl::combine(
+ button->sizeValue(),
+ sample->sizeValue(),
+ std::move(colorIndexValue)
+ ) | rpl::start_with_next([=](QSize outer, QSize inner, int colorIndex) {
+ const auto right = st::settingsColorButton.padding.right()
+ - st::settingsColorSampleSkip
+ - st::settingsColorSampleSize
+ - (style->colorPatternIndex(colorIndex)
+ ? 0
+ : st::settingsColorSamplePadding.right());
+ sample->move(
+ outer.width() - right - inner.width(),
+ (outer.height() - inner.height()) / 2);
+ }, sample->lifetime());
+
+ sample->setAttribute(Qt::WA_TransparentForMouseEvents);
+
+ button->setClickedCallback([=] {
+ show->show(Box(EditPeerColorBox, show, peer, style, theme));
+ });
+}
diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.h b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.h
new file mode 100644
index 000000000..68aecaab2
--- /dev/null
+++ b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.h
@@ -0,0 +1,31 @@
+/*
+This file is part of Telegram Desktop,
+the official desktop application for the Telegram messaging service.
+
+For license and copyright information please follow this link:
+https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
+*/
+#pragma once
+
+namespace ChatHelpers {
+class Show;
+} // namespace ChatHelpers
+
+namespace Ui {
+class GenericBox;
+class ChatStyle;
+class ChatTheme;
+class VerticalLayout;
+} // namespace Ui
+
+void EditPeerColorBox(
+ not_null box,
+ std::shared_ptr show,
+ not_null peer,
+ std::shared_ptr style = nullptr,
+ std::shared_ptr theme = nullptr);
+
+void AddPeerColorButton(
+ not_null container,
+ std::shared_ptr show,
+ not_null peer);
diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp
index 3e95dcc1f..0012d8a0f 100644
--- a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp
+++ b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp
@@ -13,8 +13,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "main/main_session.h"
#include "boxes/add_contact_box.h"
#include "ui/boxes/confirm_box.h"
-#include "boxes/peer_list_controllers.h"
#include "boxes/peers/edit_participants_box.h"
+#include "boxes/peers/edit_peer_color_box.h"
#include "boxes/peers/edit_peer_common.h"
#include "boxes/peers/edit_peer_type_box.h"
#include "boxes/peers/edit_peer_history_visibility_box.h"
@@ -23,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/peers/edit_linked_chat_box.h"
#include "boxes/peers/edit_peer_requests_box.h"
#include "boxes/peers/edit_peer_reactions.h"
+#include "boxes/peer_list_controllers.h"
#include "boxes/stickers_box.h"
#include "boxes/username_box.h"
#include "ui/boxes/single_choice_box.h"
@@ -302,6 +303,7 @@ private:
void fillLinkedChatButton();
//void fillInviteLinkButton();
void fillForumButton();
+ void fillColorIndexButton();
void fillSignaturesButton();
void fillHistoryVisibilityButton();
void fillManageSection();
@@ -905,6 +907,13 @@ void Controller::refreshForumToggleLocked() {
_controls.forumToggle->setToggleLocked(locked);
}
+void Controller::fillColorIndexButton() {
+ Expects(_controls.buttonsLayout != nullptr);
+
+ const auto show = _navigation->uiShow();
+ AddPeerColorButton(_controls.buttonsLayout, show, _peer);
+}
+
void Controller::fillSignaturesButton() {
Expects(_controls.buttonsLayout != nullptr);
@@ -1024,74 +1033,42 @@ void Controller::fillManageSection() {
return;
}
- const auto canEditType = [&] {
- return isChannel
- ? channel->amCreator()
- : chat->amCreator();
- }();
- const auto canEditSignatures = [&] {
- return isChannel
- ? (channel->canEditSignatures() && !channel->isMegagroup())
- : false;
- }();
- const auto canEditPreHistoryHidden = [&] {
- return isChannel
- ? channel->canEditPreHistoryHidden()
- : chat->canEditPreHistoryHidden();
- }();
+ const auto canEditType = isChannel
+ ? channel->amCreator()
+ : chat->amCreator();
+ const auto canEditSignatures = isChannel
+ && channel->canEditSignatures()
+ && !channel->isMegagroup();
+ const auto canEditPreHistoryHidden = isChannel
+ ? channel->canEditPreHistoryHidden()
+ : chat->canEditPreHistoryHidden();
const auto canEditForum = isChannel
? (channel->isMegagroup() && channel->amCreator())
: chat->amCreator();
-
- const auto canEditPermissions = [&] {
- return isChannel
- ? channel->canEditPermissions()
- : chat->canEditPermissions();
- }();
- const auto canEditInviteLinks = [&] {
- return isChannel
- ? channel->canHaveInviteLink()
- : chat->canHaveInviteLink();
- }();
- const auto canViewAdmins = [&] {
- return isChannel
- ? channel->canViewAdmins()
- : chat->amIn();
- }();
- const auto canViewMembers = [&] {
- return isChannel
- ? channel->canViewMembers()
- : chat->amIn();
- }();
- const auto canViewKicked = [&] {
- return isChannel
- ? (channel->isBroadcast() || channel->isGigagroup())
- : false;
- }();
- const auto hasRecentActions = [&] {
- return isChannel
- ? (channel->hasAdminRights() || channel->amCreator())
- : false;
- }();
-
- const auto canEditStickers = [&] {
- return isChannel
- ? channel->canEditStickers()
- : false;
- }();
- const auto canDeleteChannel = [&] {
- return isChannel
- ? channel->canDelete()
- : false;
- }();
-
- const auto canViewOrEditLinkedChat = [&] {
- return !isChannel
- ? false
- : channel->linkedChat()
- ? true
- : (channel->isBroadcast() && channel->canEditInformation());
- }();
+ const auto canEditPermissions = isChannel
+ ? channel->canEditPermissions()
+ : chat->canEditPermissions();
+ const auto canEditInviteLinks = isChannel
+ ? channel->canHaveInviteLink()
+ : chat->canHaveInviteLink();
+ const auto canViewAdmins = isChannel
+ ? channel->canViewAdmins()
+ : chat->amIn();
+ const auto canViewMembers = isChannel
+ ? channel->canViewMembers()
+ : chat->amIn();
+ const auto canViewKicked = isChannel
+ && (channel->isBroadcast() || channel->isGigagroup());
+ const auto hasRecentActions = isChannel
+ && (channel->hasAdminRights() || channel->amCreator());
+ const auto canEditStickers = isChannel && channel->canEditStickers();
+ const auto canDeleteChannel = isChannel && channel->canDelete();
+ const auto canEditColorIndex = isChannel
+ && !channel->isMegagroup()
+ && channel->canEditInformation();
+ const auto canViewOrEditLinkedChat = isChannel
+ && (channel->linkedChat()
+ || (channel->isBroadcast() && channel->canEditInformation()));
AddSkip(_controls.buttonsLayout, 0);
@@ -1109,11 +1086,15 @@ void Controller::fillManageSection() {
if (canEditForum) {
fillForumButton();
}
+ if (canEditColorIndex) {
+ fillColorIndexButton();
+ }
if (canEditSignatures) {
fillSignaturesButton();
}
if (canEditPreHistoryHidden
|| canEditForum
+ || canEditColorIndex
|| canEditSignatures
//|| canEditInviteLinks
|| canViewOrEditLinkedChat
diff --git a/Telegram/SourceFiles/boxes/reactions_settings_box.cpp b/Telegram/SourceFiles/boxes/reactions_settings_box.cpp
index f0282306b..2bbb1b4a7 100644
--- a/Telegram/SourceFiles/boxes/reactions_settings_box.cpp
+++ b/Telegram/SourceFiles/boxes/reactions_settings_box.cpp
@@ -63,7 +63,9 @@ PeerId GenerateUser(not_null history, const QString &name) {
MTPstring(), // lang code
MTPEmojiStatus(),
MTPVector(),
- MTPint())); // stories_max_id
+ MTPint(), // stories_max_id
+ MTP_int(0), // color
+ MTPlong())); // background_emoji_id
return peerId;
}
@@ -71,7 +73,7 @@ AdminLog::OwnedItem GenerateItem(
not_null delegate,
not_null history,
PeerId from,
- MsgId replyTo,
+ FullMsgId replyTo,
const QString &text) {
Expects(history->peer->isUser());
@@ -81,7 +83,7 @@ AdminLog::OwnedItem GenerateItem(
| MessageFlag::HasFromId
| MessageFlag::HasReplyInfo),
UserId(), // via
- FullReplyTo{ .msgId = replyTo },
+ FullReplyTo{ .messageId = replyTo },
base::unixtime::now(), // date
from,
QString(), // postAuthor
@@ -131,7 +133,8 @@ void AddMessage(
state->delegate = std::make_unique(
controller,
crl::guard(widget, [=] { widget->update(); }));
- state->style = std::make_unique();
+ state->style = std::make_unique(
+ controller->session().colorIndicesValue());
state->style->apply(controller->defaultChatTheme().get());
state->icons.lifetimes = std::vector(2);
@@ -143,13 +146,13 @@ void AddMessage(
GenerateUser(
history,
tr::lng_settings_chat_message_reply_from(tr::now)),
- 0,
+ FullMsgId(),
tr::lng_settings_chat_message_reply(tr::now));
auto message = GenerateItem(
state->delegate.get(),
history,
history->peer->id,
- state->reply->data()->fullId().msg,
+ state->reply->data()->fullId(),
tr::lng_settings_chat_message(tr::now));
const auto view = message.get();
state->item = std::move(message);
diff --git a/Telegram/SourceFiles/calls/calls_userpic.cpp b/Telegram/SourceFiles/calls/calls_userpic.cpp
index f296ef0c6..38f467170 100644
--- a/Telegram/SourceFiles/calls/calls_userpic.cpp
+++ b/Telegram/SourceFiles/calls/calls_userpic.cpp
@@ -202,8 +202,7 @@ void Userpic::createCache(Image *image) {
{
auto p = QPainter(&filled);
Ui::EmptyUserpic(
- Ui::EmptyUserpic::UserpicColor(
- Data::PeerColorIndex(_peer->id)),
+ Ui::EmptyUserpic::UserpicColor(_peer->colorIndex()),
_peer->name()
).paintCircle(p, 0, 0, size, size);
}
diff --git a/Telegram/SourceFiles/chat_helpers/bot_command.h b/Telegram/SourceFiles/chat_helpers/bot_command.h
index d5e0c9511..b91ef6903 100644
--- a/Telegram/SourceFiles/chat_helpers/bot_command.h
+++ b/Telegram/SourceFiles/chat_helpers/bot_command.h
@@ -16,7 +16,7 @@ struct SendCommandRequest {
not_null peer;
QString command;
FullMsgId context;
- MsgId replyTo = 0;
+ FullReplyTo replyTo;
};
[[nodiscard]] QString WrapCommandInChat(
diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style
index 81ff92cf0..d100850ee 100644
--- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style
+++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style
@@ -662,6 +662,9 @@ statusEmojiPan: EmojiPan(defaultEmojiPan) {
fadeLeft: icon {{ "fade_horizontal-flip_horizontal", windowBg }};
fadeRight: icon {{ "fade_horizontal", windowBg }};
}
+backgroundEmojiPan: EmojiPan(defaultEmojiPan) {
+ padding: margins(7px, 7px, 4px, 0px);
+}
inlineBotsScroll: ScrollArea(defaultSolidScroll) {
deltat: stickerPanPadding;
diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp
index cb3b034ce..2a51048b5 100644
--- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp
+++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp
@@ -467,6 +467,7 @@ EmojiListWidget::EmojiListWidget(
, _localSetsManager(
std::make_unique(&session()))
, _customRecentFactory(std::move(descriptor.customRecentFactory))
+, _customTextColor(std::move(descriptor.customTextColor))
, _overBg(st::emojiPanRadius, st().overBg)
, _collapsedBg(st::emojiPanExpand.height / 2, st().headerFg)
, _picker(this, st())
@@ -476,7 +477,7 @@ EmojiListWidget::EmojiListWidget(
setAttribute(Qt::WA_OpaquePaintEvent);
}
- if (_mode != Mode::RecentReactions) {
+ if (_mode != Mode::RecentReactions && _mode != Mode::BackgroundEmoji) {
setupSearch();
}
@@ -791,10 +792,12 @@ object_ptr EmojiListWidget::createFooter() {
};
auto result = object_ptr(FooterDescriptor{
.session = &session(),
+ .customTextColor = _customTextColor,
.paused = footerPaused,
.parent = this,
.st = &st(),
.features = { .stickersSettings = false },
+ .forceFirstFrame = (_mode == Mode::BackgroundEmoji),
});
_footer = result;
@@ -1027,6 +1030,14 @@ 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) {
+ const auto fakeId = DocumentId(5246772116543512028ULL);
+ const auto no = QString::fromUtf8("\xe2\x9b\x94\xef\xb8\x8f");
+ _recent.push_back({
+ .custom = resolveCustomRecent(fakeId),
+ .id = { Ui::Emoji::Find(no) },
+ });
+ _recentCustomIds.emplace(fakeId);
} else {
_recent.push_back({
.custom = resolveCustomRecent(id),
@@ -1188,7 +1199,9 @@ void EmojiListWidget::paintEvent(QPaintEvent *e) {
void EmojiListWidget::validateEmojiPaintContext(
const ExpandingContext &context) {
auto value = Ui::Text::CustomEmojiPaintContext{
- .textColor = (_mode == Mode::EmojiStatus
+ .textColor = (_customTextColor
+ ? _customTextColor()
+ : (_mode == Mode::EmojiStatus)
? anim::color(
st::stickerPanPremium1,
st::stickerPanPremium2,
@@ -1199,6 +1212,7 @@ void EmojiListWidget::validateEmojiPaintContext(
.scale = context.progress,
.paused = On(powerSavingFlag()) || paused(),
.scaled = context.expanding,
+ .internal = { .forceFirstFrame = (_mode == Mode::BackgroundEmoji) },
};
if (!_emojiPaintContext) {
_emojiPaintContext = std::make_unique<
@@ -1629,6 +1643,9 @@ void EmojiListWidget::mouseReleaseEvent(QMouseEvent *e) {
case Mode::TopicIcon:
Settings::ShowPremium(resolved, u"forum_topic_icon"_q);
break;
+ case Mode::BackgroundEmoji:
+ Settings::ShowPremium(resolved, u"name_color"_q);
+ break;
}
}
}
@@ -1995,7 +2012,10 @@ void EmojiListWidget::refreshCustom() {
const auto &sets = owner->stickers().sets();
const auto push = [&](uint64 setId, bool installed) {
auto it = sets.find(setId);
- if (it == sets.cend() || it->second->stickers.isEmpty()) {
+ if (it == sets.cend()
+ || it->second->stickers.isEmpty()
+ || (_mode == Mode::BackgroundEmoji
+ && !it->second->textColor())) {
return;
}
const auto canRemove = !!(it->second->flags
diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h
index 5cc197995..5735d7a55 100644
--- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h
+++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h
@@ -74,11 +74,13 @@ enum class EmojiListMode {
FullReactions,
RecentReactions,
UserpicBuilder,
+ BackgroundEmoji,
};
struct EmojiListDescriptor {
std::shared_ptr show;
EmojiListMode mode = EmojiListMode::Full;
+ Fn customTextColor;
Fn paused;
std::vector customRecentList;
Fn(
@@ -386,6 +388,7 @@ private:
base::flat_map<
DocumentId,
std::unique_ptr> _customRecent;
+ Fn _customTextColor;
int _customSingleSize = 0;
bool _allowWithoutPremium = false;
Ui::RoundRect _overBg;
diff --git a/Telegram/SourceFiles/chat_helpers/message_field.cpp b/Telegram/SourceFiles/chat_helpers/message_field.cpp
index b988a9b3f..9c5b37751 100644
--- a/Telegram/SourceFiles/chat_helpers/message_field.cpp
+++ b/Telegram/SourceFiles/chat_helpers/message_field.cpp
@@ -601,6 +601,12 @@ MessageLinksParser::MessageLinksParser(not_null field)
_lifetime = _field->changes(
) | rpl::start_with_next([=] {
const auto length = _field->getTextWithTags().text.size();
+ if (!length) {
+ _lastLength = 0;
+ _timer.cancel();
+ parse();
+ return;
+ }
const auto timeout = (std::abs(length - _lastLength) > 2)
? 0
: kParseLinksTimeout;
@@ -642,16 +648,13 @@ bool MessageLinksParser::eventFilter(QObject *object, QEvent *event) {
return QObject::eventFilter(object, event);
}
-const rpl::variable &MessageLinksParser::list() const {
- return _list;
-}
-
void MessageLinksParser::parse() {
const auto &textWithTags = _field->getTextWithTags();
const auto &text = textWithTags.text;
const auto &tags = textWithTags.tags;
const auto &markdownTags = _field->getMarkdownTags();
if (_disabled || text.isEmpty()) {
+ _ranges = {};
_list = QStringList();
return;
}
@@ -663,7 +666,7 @@ void MessageLinksParser::parse() {
|| (tag == Ui::InputField::kTagSpoiler);
};
- auto ranges = QVector();
+ _ranges.clear();
auto tag = tags.begin();
const auto tagsEnd = tags.end();
@@ -672,7 +675,7 @@ void MessageLinksParser::parse() {
if (Ui::InputField::IsValidMarkdownLink(tag->id)
&& !TextUtilities::IsMentionLink(tag->id)) {
- ranges.push_back({ tag->offset, tag->length, tag->id });
+ _ranges.push_back({ tag->offset, tag->length, tag->id });
}
++tag;
};
@@ -774,7 +777,7 @@ void MessageLinksParser::parse() {
continue;
}
}
- const auto range = LinkRange {
+ const auto range = MessageLinkRange{
int(domainOffset),
static_cast(p - start - domainOffset),
QString()
@@ -782,22 +785,20 @@ void MessageLinksParser::parse() {
processTagsBefore(domainOffset);
if (!hasTagsIntersection(range.start + range.length)) {
if (markdownTagsAllow(range.start, range.length)) {
- ranges.push_back(range);
+ _ranges.push_back(range);
}
}
offset = matchOffset = p - start;
}
processTagsBefore(Ui::kQFixedMax);
- apply(text, ranges);
+ applyRanges(text);
}
-void MessageLinksParser::apply(
- const QString &text,
- const QVector &ranges) {
- const auto count = int(ranges.size());
+void MessageLinksParser::applyRanges(const QString &text) {
+ const auto count = int(_ranges.size());
const auto current = _list.current();
- const auto computeLink = [&](const LinkRange &range) {
+ const auto computeLink = [&](const MessageLinkRange &range) {
return range.custom.isEmpty()
? base::StringViewMid(text, range.start, range.length)
: QStringView(range.custom);
@@ -807,7 +808,7 @@ void MessageLinksParser::apply(
return true;
}
for (auto i = 0; i != count; ++i) {
- if (computeLink(ranges[i]) != current[i]) {
+ if (computeLink(_ranges[i]) != current[i]) {
return true;
}
}
@@ -818,7 +819,7 @@ void MessageLinksParser::apply(
}
auto parsed = QStringList();
parsed.reserve(count);
- for (const auto &range : ranges) {
+ for (const auto &range : _ranges) {
parsed.push_back(computeLink(range).toString());
}
_list = std::move(parsed);
diff --git a/Telegram/SourceFiles/chat_helpers/message_field.h b/Telegram/SourceFiles/chat_helpers/message_field.h
index 9c4ba031f..a78abd94c 100644
--- a/Telegram/SourceFiles/chat_helpers/message_field.h
+++ b/Telegram/SourceFiles/chat_helpers/message_field.h
@@ -7,9 +7,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
-#include "ui/widgets/fields/input_field.h"
+#include "base/qt/qt_compare.h"
#include "base/timer.h"
#include "chat_helpers/compose/compose_features.h"
+#include "ui/widgets/fields/input_field.h"
#ifndef TDESKTOP_DISABLE_SPELLCHECK
#include "boxes/dictionaries_manager.h"
@@ -96,38 +97,42 @@ AutocompleteQuery ParseMentionHashtagBotCommandQuery(
not_null field,
ChatHelpers::ComposeFeatures features);
-class MessageLinksParser : private QObject {
+struct MessageLinkRange {
+ int start = 0;
+ int length = 0;
+ QString custom;
+
+ friend inline auto operator<=>(
+ const MessageLinkRange&,
+ const MessageLinkRange&) = default;
+ friend inline bool operator==(
+ const MessageLinkRange&,
+ const MessageLinkRange&) = default;
+};
+
+class MessageLinksParser final : private QObject {
public:
MessageLinksParser(not_null field);
void parseNow();
void setDisabled(bool disabled);
- [[nodiscard]] const rpl::variable &list() const;
-
-protected:
- bool eventFilter(QObject *object, QEvent *event) override;
+ [[nodiscard]] const rpl::variable &list() const {
+ return _list;
+ }
+ [[nodiscard]] const std::vector &ranges() const {
+ return _ranges;
+ }
private:
- struct LinkRange {
- int start;
- int length;
- QString custom;
- };
- friend inline bool operator==(const LinkRange &a, const LinkRange &b) {
- return (a.start == b.start)
- && (a.length == b.length)
- && (a.custom == b.custom);
- }
- friend inline bool operator!=(const LinkRange &a, const LinkRange &b) {
- return !(a == b);
- }
+ bool eventFilter(QObject *object, QEvent *event) override;
void parse();
- void apply(const QString &text, const QVector &ranges);
+ void applyRanges(const QString &text);
not_null _field;
rpl::variable _list;
+ std::vector _ranges;
int _lastLength = 0;
bool _disabled = false;
base::Timer _timer;
diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_footer.cpp b/Telegram/SourceFiles/chat_helpers/stickers_list_footer.cpp
index 0c09b6dd3..66784f774 100644
--- a/Telegram/SourceFiles/chat_helpers/stickers_list_footer.cpp
+++ b/Telegram/SourceFiles/chat_helpers/stickers_list_footer.cpp
@@ -291,12 +291,14 @@ StickersListFooter::StickersListFooter(Descriptor &&descriptor)
descriptor.parent,
descriptor.st ? *descriptor.st : st::defaultEmojiPan)
, _session(descriptor.session)
-, _paused(descriptor.paused)
+, _customTextColor(std::move(descriptor.customTextColor))
+, _paused(std::move(descriptor.paused))
, _features(descriptor.features)
, _iconState([=] { update(); })
, _subiconState([=] { update(); })
, _selectionBg(st::emojiPanRadius, st().categoriesBgOver)
-, _subselectionBg(st().iconArea / 2, st().categoriesBgOver) {
+, _subselectionBg(st().iconArea / 2, st().categoriesBgOver)
+, _forceFirstFrame(descriptor.forceFirstFrame) {
setMouseTracking(true);
_iconsLeft = st().iconSkip
@@ -1345,13 +1347,16 @@ void StickersListFooter::paintSetIconToCache(
const auto y = (st().footer - icon.pixh) / 2;
if (icon.custom) {
icon.custom->paint(p, Ui::Text::CustomEmoji::Context{
- .textColor = st().textFg->c,
+ .textColor = (_customTextColor
+ ? _customTextColor()
+ : st().textFg->c),
.size = QSize(icon.pixw, icon.pixh),
.now = now,
.scale = context.progress,
.position = { x, y },
.paused = paused,
.scaled = context.expanding,
+ .internal = { .forceFirstFrame = _forceFirstFrame },
});
} else if (icon.lottie && icon.lottie->ready()) {
const auto frame = icon.lottie->frame();
@@ -1428,11 +1433,13 @@ void StickersListFooter::paintSetIconToCache(
return icons[index];
};
const auto paintOne = [&](int left, const style::icon *icon) {
- icon->paint(
- p,
- left + (_singleWidth - icon->width()) / 2,
- (st().footer - icon->height()) / 2,
- width());
+ left += (_singleWidth - icon->width()) / 2;
+ const auto top = (st().footer - icon->height()) / 2;
+ if (_customTextColor) {
+ icon->paint(p, left, top, width(), _customTextColor());
+ } else {
+ icon->paint(p, left, top, width());
+ }
};
if (_icons[info.index].setId == AllEmojiSectionSetId()
&& info.width > _singleWidth) {
diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_footer.h b/Telegram/SourceFiles/chat_helpers/stickers_list_footer.h
index f7a0afc1d..5dbdd30f9 100644
--- a/Telegram/SourceFiles/chat_helpers/stickers_list_footer.h
+++ b/Telegram/SourceFiles/chat_helpers/stickers_list_footer.h
@@ -115,10 +115,12 @@ class StickersListFooter final : public TabbedSelector::InnerFooter {
public:
struct Descriptor {
not_null