diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt
index 9822ab60ea53c9..bb5ea794041026 100644
--- a/Telegram/CMakeLists.txt
+++ b/Telegram/CMakeLists.txt
@@ -198,6 +198,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
@@ -651,6 +653,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
@@ -659,6 +663,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
@@ -677,6 +683,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
@@ -816,20 +824,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
@@ -916,6 +910,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 08a844820e4623..7c021f6ef96e5b 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 8068b220804b3a..f4a9b43f59f699 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 2399ff033c025b..2c572bd02c85be 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/link_above.png b/Telegram/Resources/icons/menu/link_above.png
new file mode 100644
index 00000000000000..1f8881dfebec24
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 00000000000000..61c53d18e377cb
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 00000000000000..be58d0df3a1f8e
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 00000000000000..315150fa660076
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 00000000000000..f30fe99b492a0f
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 00000000000000..39f6fbf1ac6f86
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 00000000000000..4750b24d631881
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 00000000000000..ff98f6dc4aac6b
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 00000000000000..78b3f137be2fca
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 00000000000000..48bf8f0350b84c
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 00000000000000..289e1f69dce5d2
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 00000000000000..dfb66a523c8929
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 5b58b6e2c9a132..d7c24eeeb999e4 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -129,6 +129,8 @@ https://github.com/rabbitGramDesktop/rabbitGramDesktop/blob/dev/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/rabbitGramDesktop/rabbitGramDesktop/blob/dev/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/rabbitGramDesktop/rabbitGramDesktop/blob/dev/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/rabbitGramDesktop/rabbitGramDesktop/blob/dev/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";
@@ -1640,6 +1664,7 @@ https://github.com/rabbitGramDesktop/rabbitGramDesktop/blob/dev/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 +2059,129 @@ https://github.com/rabbitGramDesktop/rabbitGramDesktop/blob/dev/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.";
@@ -2420,6 +2568,7 @@ https://github.com/rabbitGramDesktop/rabbitGramDesktop/blob/dev/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 +2673,24 @@ https://github.com/rabbitGramDesktop/rabbitGramDesktop/blob/dev/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 +2713,7 @@ https://github.com/rabbitGramDesktop/rabbitGramDesktop/blob/dev/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 +2842,7 @@ https://github.com/rabbitGramDesktop/rabbitGramDesktop/blob/dev/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 +2855,7 @@ https://github.com/rabbitGramDesktop/rabbitGramDesktop/blob/dev/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 +3521,10 @@ https://github.com/rabbitGramDesktop/rabbitGramDesktop/blob/dev/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 d0c302ff13f585..7636fabd4c4b60 100644
--- a/Telegram/Resources/uwp/AppX/AppxManifest.xml
+++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml
@@ -10,7 +10,7 @@
+ Version="4.11.0.0" />
rabbitGram Desktop
xmdnx
diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc
index 33adcb65c46356..687e2438a7517d 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,5,0
- PRODUCTVERSION 4,10,5,0
+ FILEVERSION 4,11,0,0
+ PRODUCTVERSION 4,11,0,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
@@ -62,10 +62,10 @@ BEGIN
BEGIN
VALUE "CompanyName", "xmdnx"
VALUE "FileDescription", "rabbitGram Desktop"
- VALUE "FileVersion", "4.10.5.0"
+ VALUE "FileVersion", "4.11.0.0"
VALUE "LegalCopyright", "Copyright (C) 2023"
VALUE "ProductName", "rabbitGram Desktop"
- VALUE "ProductVersion", "4.10.5.0"
+ VALUE "ProductVersion", "4.11.0.0"
END
END
BLOCK "VarFileInfo"
diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc
index 2b97898a441d27..9f61094222f0bc 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,5,0
- PRODUCTVERSION 4,10,5,0
+ FILEVERSION 4,11,0,0
+ PRODUCTVERSION 4,11,0,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
@@ -53,10 +53,10 @@ BEGIN
BEGIN
VALUE "CompanyName", "xmdnx"
VALUE "FileDescription", "rabbitGram Desktop Updater"
- VALUE "FileVersion", "4.10.5.0"
- VALUE "LegalCopyright", "Copyright (C) 2021-2023"
+ VALUE "FileVersion", "4.11.0.0"
+ VALUE "LegalCopyright", "Copyright (C) 2023"
VALUE "ProductName", "rabbitGram Desktop"
- VALUE "ProductVersion", "4.10.5.0"
+ VALUE "ProductVersion", "4.11.0.0"
END
END
BLOCK "VarFileInfo"
diff --git a/Telegram/SourceFiles/api/api_bot.cpp b/Telegram/SourceFiles/api/api_bot.cpp
index 519683c263803f..aa078e731baf83 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 f1988fe2167ef4..35d8c0b476473f 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 8099e4bf22784c..acb95a466a3794 100644
--- a/Telegram/SourceFiles/api/api_common.h
+++ b/Telegram/SourceFiles/api/api_common.h
@@ -7,6 +7,8 @@ For license and copyright information please follow this link:
*/
#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 fc2f1ca59837d9..da57562380afa8 100644
--- a/Telegram/SourceFiles/api/api_editing.cpp
+++ b/Telegram/SourceFiles/api/api_editing.cpp
@@ -11,8 +11,10 @@ For license and copyright information please follow this link:
#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 de321a30b21aa1..4c694f85d23273 100644
--- a/Telegram/SourceFiles/api/api_editing.h
+++ b/Telegram/SourceFiles/api/api_editing.h
@@ -9,6 +9,10 @@ For license and copyright information please follow this link:
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 51889e549511ea..4471f245ed3326 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 86e8c7fa4e16c2..29b3d8198e884a 100644
--- a/Telegram/SourceFiles/api/api_peer_photo.h
+++ b/Telegram/SourceFiles/api/api_peer_photo.h
@@ -31,6 +31,7 @@ class PeerPhoto final {
enum class EmojiListType {
Profile,
Group,
+ Background,
};
struct UserPhoto {
@@ -73,6 +74,10 @@ class PeerPhoto final {
Suggestion,
Fallback,
};
+ struct EmojiListData {
+ rpl::variable list;
+ mtpRequestId requestId = 0;
+ };
void ready(
const FullMsgId &msgId,
@@ -84,6 +89,9 @@ class PeerPhoto final {
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 @@ class PeerPhoto final {
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 84b0aff8936d41..109a50c26df355 100644
--- a/Telegram/SourceFiles/api/api_polls.cpp
+++ b/Telegram/SourceFiles/api/api_polls.cpp
@@ -43,7 +43,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 179f3f8cd135cd..da5417b181c7ed 100644
--- a/Telegram/SourceFiles/api/api_premium.cpp
+++ b/Telegram/SourceFiles/api/api_premium.cpp
@@ -17,6 +17,21 @@ For license and copyright information please follow this link:
#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 cc0320355118c8..bf3035fa9ba08b 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 @@ class Premium final {
[[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 @@ class Premium final {
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 f8e9aa473eca82..1adbfc404b5256 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 79de3c2cde23ae..c3286f667c777f 100644
--- a/Telegram/SourceFiles/api/api_statistics.cpp
+++ b/Telegram/SourceFiles/api/api_statistics.cpp
@@ -501,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
@@ -553,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 0246f1f8433491..bea7b92129fa89 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 8965c9f7b642c2..a2439d5cc03ad4 100644
--- a/Telegram/SourceFiles/api/api_updates.cpp
+++ b/Telegram/SourceFiles/api/api_updates.cpp
@@ -2071,7 +2071,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 36c2c3809432e8..62d76803ed6425 100644
--- a/Telegram/SourceFiles/apiwrap.cpp
+++ b/Telegram/SourceFiles/apiwrap.cpp
@@ -2135,14 +2135,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;
@@ -2155,11 +2154,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(
@@ -2246,7 +2247,7 @@ void ApiWrap::gotStickerSet(
}
void ApiWrap::requestWebPageDelayed(not_null page) {
- if (page->pendingTill <= 0) {
+ if (page->failed || !page->pendingTill) {
return;
}
_webPagesPending.emplace(page, 0);
@@ -2551,7 +2552,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);
@@ -3578,9 +3580,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()
@@ -3608,7 +3609,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());
@@ -3622,26 +3629,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,
@@ -3654,6 +3687,7 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
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);
}
@@ -3665,6 +3699,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()
@@ -3672,6 +3707,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(
@@ -3685,27 +3721,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 {
@@ -3716,7 +3743,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);
@@ -3781,7 +3845,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 cb406a42d004f6..61eaa8f18b94e5 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 40a10ab7ffbc55..3394d9c5ed5fd0 100644
--- a/Telegram/SourceFiles/boxes/gift_premium_box.cpp
+++ b/Telegram/SourceFiles/boxes/gift_premium_box.cpp
@@ -8,26 +8,36 @@ For license and copyright information please follow this link:
#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 @@ For license and copyright information please follow this link:
#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 ccf32c3acfe9cc..62ea4f2effdc8c 100644
--- a/Telegram/SourceFiles/boxes/gift_premium_box.h
+++ b/Telegram/SourceFiles/boxes/gift_premium_box.h
@@ -11,6 +11,18 @@ For license and copyright information please follow this link:
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 @@ class GiftPremiumValidator final {
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/peers/edit_peer_color_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp
new file mode 100644
index 00000000000000..1eede85e306a73
--- /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 00000000000000..68aecaab256848
--- /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 c75b664802ef6d..e82fccf04e389f 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 @@ For license and copyright information please follow this link:
#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 @@ For license and copyright information please follow this link:
#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 @@ class Controller : public base::has_weak_ptr {
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 6cd5788754d4a4..077a62bd4ef9db 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 f2e510fdea1ebc..735be241ca8ac0 100644
--- a/Telegram/SourceFiles/calls/calls_userpic.cpp
+++ b/Telegram/SourceFiles/calls/calls_userpic.cpp
@@ -204,8 +204,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 fe771d1875c7a7..b3a61f970c7cfb 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 db28df558413dd..e0874ea95e6c62 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 4c51f015bcce81..bb97518b593813 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 db81fc106d9ac7..eef00ab1a0a477 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 @@ class EmojiListWidget final
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 c04ab5933d6249..8b7dfef3e8e9b5 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 873493d455ba98..ab50cffb716c9f 100644
--- a/Telegram/SourceFiles/chat_helpers/message_field.h
+++ b/Telegram/SourceFiles/chat_helpers/message_field.h
@@ -7,9 +7,10 @@ For license and copyright information please follow this link:
*/
#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;
-
-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);
+ [[nodiscard]] const rpl::variable &list() const {
+ return _list;
}
- friend inline bool operator!=(const LinkRange &a, const LinkRange &b) {
- return !(a == b);
+ [[nodiscard]] const std::vector &ranges() const {
+ return _ranges;
}
+private:
+ 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 d97e71d6c8b349..e39817fe655ea9 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 e0e6729cc65004..5a0585db3cec0b 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 session;
+ Fn customTextColor;
Fn paused;
not_null parent;
const style::EmojiPan *st = nullptr;
ComposeFeatures features;
+ bool forceFirstFrame = false;
};
explicit StickersListFooter(Descriptor &&descriptor);
@@ -269,6 +271,7 @@ class StickersListFooter final : public TabbedSelector::InnerFooter {
void clipCallback(Media::Clip::Notification notification, uint64 setId);
const not_null _session;
+ const Fn _customTextColor;
const Fn _paused;
const ComposeFeatures _features;
@@ -303,6 +306,7 @@ class StickersListFooter final : public TabbedSelector::InnerFooter {
int _subiconsWidth = 0;
bool _subiconsExpanded = false;
bool _repaintScheduled = false;
+ bool _forceFirstFrame = false;
rpl::event_stream<> _openSettingsRequests;
rpl::event_stream _setChosen;
diff --git a/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp b/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp
index b7edbf1507209a..266e2afef02464 100644
--- a/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp
+++ b/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp
@@ -331,7 +331,7 @@ TabbedSelector::TabbedSelector(
Mode mode)
: TabbedSelector(parent, {
.show = std::move(show),
- .st = (mode == Mode::EmojiStatus
+ .st = ((mode == Mode::EmojiStatus || mode == Mode::BackgroundEmoji)
? st::statusEmojiPan
: st::defaultEmojiPan),
.level = level,
@@ -347,6 +347,7 @@ TabbedSelector::TabbedSelector(
, _features(descriptor.features)
, _show(std::move(descriptor.show))
, _level(descriptor.level)
+, _customTextColor(std::move(descriptor.customTextColor))
, _mode(descriptor.mode)
, _panelRounding(Ui::PrepareCornerPixmaps(st::emojiPanRadius, _st.bg))
, _categoriesRounding(
@@ -512,7 +513,10 @@ TabbedSelector::Tab TabbedSelector::createTab(SelectorTab type, int index) {
.show = _show,
.mode = (_mode == Mode::EmojiStatus
? EmojiMode::EmojiStatus
+ : _mode == Mode::BackgroundEmoji
+ ? EmojiMode::BackgroundEmoji
: EmojiMode::Full),
+ .customTextColor = _customTextColor,
.paused = paused,
.st = &_st,
.features = _features,
diff --git a/Telegram/SourceFiles/chat_helpers/tabbed_selector.h b/Telegram/SourceFiles/chat_helpers/tabbed_selector.h
index 7c02bac2642470..a598ef56c16739 100644
--- a/Telegram/SourceFiles/chat_helpers/tabbed_selector.h
+++ b/Telegram/SourceFiles/chat_helpers/tabbed_selector.h
@@ -81,6 +81,7 @@ enum class TabbedSelectorMode {
EmojiOnly,
MediaEditor,
EmojiStatus,
+ BackgroundEmoji,
};
struct TabbedSelectorDescriptor {
@@ -88,6 +89,7 @@ struct TabbedSelectorDescriptor {
const style::EmojiPan &st;
PauseReason level = {};
TabbedSelectorMode mode = TabbedSelectorMode::Full;
+ Fn customTextColor;
ComposeFeatures features;
};
@@ -272,6 +274,7 @@ class TabbedSelector : public Ui::RpWidget {
const ComposeFeatures _features;
const std::shared_ptr _show;
const PauseReason _level = {};
+ const Fn _customTextColor;
Mode _mode = Mode::Full;
int _roundRadius = 0;
diff --git a/Telegram/SourceFiles/core/click_handler_types.cpp b/Telegram/SourceFiles/core/click_handler_types.cpp
index eaca879214aceb..c57f744c2bc533 100644
--- a/Telegram/SourceFiles/core/click_handler_types.cpp
+++ b/Telegram/SourceFiles/core/click_handler_types.cpp
@@ -308,7 +308,6 @@ void BotCommandClickHandler::onClick(ClickContext context) const {
.peer = peer,
.command = _cmd,
.context = my.itemId,
- .replyTo = 0,
});
}
}
diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp
index b3cab03f5e2490..be570e70a2a5e0 100644
--- a/Telegram/SourceFiles/core/local_url_handlers.cpp
+++ b/Telegram/SourceFiles/core/local_url_handlers.cpp
@@ -12,6 +12,7 @@ For license and copyright information please follow this link:
#include "api/api_text_entities.h"
#include "api/api_chat_filters.h"
#include "api/api_chat_invite.h"
+#include "api/api_premium.h"
#include "base/qthelp_regex.h"
#include "base/qthelp_url.h"
#include "lang/lang_cloud_manager.h"
@@ -23,6 +24,7 @@ For license and copyright information please follow this link:
#include "ui/boxes/confirm_box.h"
#include "boxes/share_box.h"
#include "boxes/connection_box.h"
+#include "boxes/gift_premium_box.h"
#include "boxes/sticker_set_box.h"
#include "boxes/sessions_box.h"
#include "boxes/language_box.h"
@@ -348,6 +350,11 @@ bool ResolveUsernameOrPhone(
const auto domainParam = params.value(u"domain"_q);
const auto appnameParam = params.value(u"appname"_q);
+ if (domainParam == u"giftcode"_q && !appnameParam.isEmpty()) {
+ ResolveGiftCode(controller, appnameParam);
+ return true;
+ }
+
// Fix t.me/s/username links.
const auto webChannelPreviewLink = (domainParam == u"s"_q)
&& !appnameParam.isEmpty();
@@ -1078,7 +1085,7 @@ QString TryConvertUrlToLocal(QString url) {
"("
"/?\\?|"
"/?$|"
- "/[a-zA-Z0-9\\.\\_]+/?(\\?|$)|"
+ "/[a-zA-Z0-9\\.\\_\\-]+/?(\\?|$)|"
"/\\d+/?(\\?|$)|"
"/s/\\d+/?(\\?|$)|"
"/\\d+/\\d+/?(\\?|$)"
@@ -1103,7 +1110,7 @@ QString TryConvertUrlToLocal(QString url) {
added = u"&post="_q + postMatch->captured(1);
} else if (const auto storyMatch = regex_match(u"^/s/(\\d+)(/?\\?|/?$)"_q, usernameMatch->captured(2))) {
added = u"&story="_q + storyMatch->captured(1);
- } else if (const auto appNameMatch = regex_match(u"^/([a-zA-Z0-9\\.\\_]+)(/?\\?|/?$)"_q, usernameMatch->captured(2))) {
+ } else if (const auto appNameMatch = regex_match(u"^/([a-zA-Z0-9\\.\\_\\-]+)(/?\\?|/?$)"_q, usernameMatch->captured(2))) {
added = u"&appname="_q + appNameMatch->captured(1);
}
return base + added + (params.isEmpty() ? QString() : '&' + params);
diff --git a/Telegram/SourceFiles/core/ui_integration.cpp b/Telegram/SourceFiles/core/ui_integration.cpp
index 7687ece608c503..08d4efc8349273 100644
--- a/Telegram/SourceFiles/core/ui_integration.cpp
+++ b/Telegram/SourceFiles/core/ui_integration.cpp
@@ -348,6 +348,10 @@ QString UiIntegration::phraseFormattingStrikeOut() {
return tr::lng_menu_formatting_strike_out(tr::now);
}
+QString UiIntegration::phraseFormattingBlockquote() {
+ return tr::lng_menu_formatting_blockquote(tr::now);
+}
+
QString UiIntegration::phraseFormattingMonospace() {
return tr::lng_menu_formatting_monospace(tr::now);
}
@@ -404,6 +408,10 @@ QString UiIntegration::phraseBotAllowWriteConfirm() {
return tr::lng_bot_allow_write_confirm(tr::now);
}
+QString UiIntegration::phraseQuoteHeaderCopy() {
+ return tr::lng_code_block_header_copy(tr::now);
+}
+
bool OpenGLLastCheckFailed() {
return QFile::exists(OpenGLCheckFilePath());
}
diff --git a/Telegram/SourceFiles/core/ui_integration.h b/Telegram/SourceFiles/core/ui_integration.h
index 932ae5a8e50a10..b410fa5a1a5893 100644
--- a/Telegram/SourceFiles/core/ui_integration.h
+++ b/Telegram/SourceFiles/core/ui_integration.h
@@ -77,6 +77,7 @@ class UiIntegration final : public Ui::Integration {
QString phraseFormattingItalic() override;
QString phraseFormattingUnderline() override;
QString phraseFormattingStrikeOut() override;
+ QString phraseFormattingBlockquote() override;
QString phraseFormattingMonospace() override;
QString phraseFormattingSpoiler() override;
QString phraseButtonOk() override;
@@ -91,6 +92,7 @@ class UiIntegration final : public Ui::Integration {
QString phraseBotAllowWrite() override;
QString phraseBotAllowWriteTitle() override;
QString phraseBotAllowWriteConfirm() override;
+ QString phraseQuoteHeaderCopy() override;
};
diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h
index 76cbb89cb2b89a..1adc9e5da36751 100644
--- a/Telegram/SourceFiles/core/version.h
+++ b/Telegram/SourceFiles/core/version.h
@@ -22,7 +22,7 @@ constexpr auto AppId = "{4356CE01-4137-4C55-9F8B-FB4EEBB6EC0C}"_cs;
constexpr auto AppNameOld = "rabbitGram Win (Unofficial)"_cs;
constexpr auto AppName = "rabbitGram Desktop"_cs;
constexpr auto AppFile = "rabbitGram"_cs;
-constexpr auto AppVersion = 4010005;
-constexpr auto AppVersionStr = "4.10.5";
+constexpr auto AppVersion = 4011000;
+constexpr auto AppVersionStr = "4.11";
constexpr auto AppBetaVersion = false;
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;
diff --git a/Telegram/SourceFiles/countries/countries_instance.cpp b/Telegram/SourceFiles/countries/countries_instance.cpp
index 24f249758f9cce..2ec82dc5f7bf87 100644
--- a/Telegram/SourceFiles/countries/countries_instance.cpp
+++ b/Telegram/SourceFiles/countries/countries_instance.cpp
@@ -254,7 +254,7 @@ const std::array FallbackList = { {
CountriesInstance::CountriesInstance() {
}
-const std::vector &CountriesInstance::list() {
+const std::vector