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 &CountriesInstance::list() const { if (_list.empty()) { _list = (FallbackList | ranges::to_vector); } @@ -268,7 +268,7 @@ void CountriesInstance::setList(std::vector &&infos) { _updated.fire({}); } -const CountriesInstance::Map &CountriesInstance::byCode() { +const CountriesInstance::Map &CountriesInstance::byCode() const { if (_byCode.empty()) { _byCode.reserve(list().size()); for (const auto &entry : list()) { @@ -280,7 +280,7 @@ const CountriesInstance::Map &CountriesInstance::byCode() { return _byCode; } -const CountriesInstance::Map &CountriesInstance::byISO2() { +const CountriesInstance::Map &CountriesInstance::byISO2() const { if (_byISO2.empty()) { _byISO2.reserve(list().size()); for (const auto &entry : list()) { @@ -290,7 +290,7 @@ const CountriesInstance::Map &CountriesInstance::byISO2() { return _byISO2; } -QString CountriesInstance::validPhoneCode(QString fullCode) { +QString CountriesInstance::validPhoneCode(QString fullCode) const { const auto &listByCode = byCode(); while (fullCode.length()) { const auto i = listByCode.constFind(fullCode); @@ -302,20 +302,34 @@ QString CountriesInstance::validPhoneCode(QString fullCode) { return QString(); } -QString CountriesInstance::countryNameByISO2(const QString &iso) { +QString CountriesInstance::countryNameByISO2(const QString &iso) const { const auto &listByISO2 = byISO2(); const auto i = listByISO2.constFind(iso); return (i != listByISO2.cend()) ? (*i)->name : QString(); } -QString CountriesInstance::countryISO2ByPhone(const QString &phone) { +QString CountriesInstance::countryISO2ByPhone(const QString &phone) const { const auto &listByCode = byCode(); const auto code = validPhoneCode(phone); const auto i = listByCode.find(code); return (i != listByCode.cend()) ? (*i)->iso2 : QString(); } -FormatResult CountriesInstance::format(FormatArgs args) { +QString CountriesInstance::flagEmojiByISO2(const QString &iso) const { + if (iso.size() != 2 + || iso.front() < 'A' + || iso.front() > 'Z' + || iso.back() < 'A' + || iso.back() > 'Z') { + return QString(); + } + auto result = QString(4, QChar(0xD83C)); + result[1] = QChar(iso.front().unicode() - 'A' + 0xDDE6); + result[3] = QChar(iso.back().unicode() - 'A' + 0xDDE6); + return result; +} + +FormatResult CountriesInstance::format(FormatArgs args) const { // Ported from TDLib. if (args.phone.isEmpty()) { return FormatResult(); diff --git a/Telegram/SourceFiles/countries/countries_instance.h b/Telegram/SourceFiles/countries/countries_instance.h index b65aa604b8e6a9..78e6e3c87837dd 100644 --- a/Telegram/SourceFiles/countries/countries_instance.h +++ b/Telegram/SourceFiles/countries/countries_instance.h @@ -43,25 +43,26 @@ class CountriesInstance final { using Map = QHash; CountriesInstance(); - [[nodiscard]] const std::vector &list(); + [[nodiscard]] const std::vector &list() const; void setList(std::vector &&infos); - [[nodiscard]] const Map &byCode(); - [[nodiscard]] const Map &byISO2(); + [[nodiscard]] const Map &byCode() const; + [[nodiscard]] const Map &byISO2() const; - [[nodiscard]] QString validPhoneCode(QString fullCode); - [[nodiscard]] QString countryNameByISO2(const QString &iso); - [[nodiscard]] QString countryISO2ByPhone(const QString &phone); + [[nodiscard]] QString validPhoneCode(QString fullCode) const; + [[nodiscard]] QString countryNameByISO2(const QString &iso) const; + [[nodiscard]] QString countryISO2ByPhone(const QString &phone) const; + [[nodiscard]] QString flagEmojiByISO2(const QString &iso) const; - [[nodiscard]] FormatResult format(FormatArgs args); + [[nodiscard]] FormatResult format(FormatArgs args) const; [[nodiscard]] rpl::producer<> updated() const; private: - std::vector _list; + mutable std::vector _list; - Map _byCode; - Map _byISO2; + mutable Map _byCode; + mutable Map _byISO2; rpl::event_stream<> _updated; diff --git a/Telegram/SourceFiles/data/data_bot_app.h b/Telegram/SourceFiles/data/data_bot_app.h index bbd053340bd42f..ee928130be51ad 100644 --- a/Telegram/SourceFiles/data/data_bot_app.h +++ b/Telegram/SourceFiles/data/data_bot_app.h @@ -24,5 +24,4 @@ struct BotAppData { uint64 accessHash = 0; uint64 hash = 0; - bool hasSettings = false; }; diff --git a/Telegram/SourceFiles/data/data_changes.h b/Telegram/SourceFiles/data/data_changes.h index 06e04a77d55cdb..05d5e55379edce 100644 --- a/Telegram/SourceFiles/data/data_changes.h +++ b/Telegram/SourceFiles/data/data_changes.h @@ -71,41 +71,43 @@ struct PeerUpdate { FullInfo = (1ULL << 11), Usernames = (1ULL << 12), TranslationDisabled = (1ULL << 13), + Color = (1ULL << 14), + BackgroundEmoji = (1ULL << 15), // For users - CanShareContact = (1ULL << 14), - IsContact = (1ULL << 15), - PhoneNumber = (1ULL << 16), - OnlineStatus = (1ULL << 17), - BotCommands = (1ULL << 18), - BotCanBeInvited = (1ULL << 19), - BotStartToken = (1ULL << 20), - CommonChats = (1ULL << 21), - HasCalls = (1ULL << 22), - SupportInfo = (1ULL << 23), - IsBot = (1ULL << 24), - EmojiStatus = (1ULL << 25), - StoriesState = (1ULL << 26), + CanShareContact = (1ULL << 16), + IsContact = (1ULL << 17), + PhoneNumber = (1ULL << 18), + OnlineStatus = (1ULL << 19), + BotCommands = (1ULL << 20), + BotCanBeInvited = (1ULL << 21), + BotStartToken = (1ULL << 22), + CommonChats = (1ULL << 23), + HasCalls = (1ULL << 24), + SupportInfo = (1ULL << 25), + IsBot = (1ULL << 26), + EmojiStatus = (1ULL << 27), + StoriesState = (1ULL << 28), // For chats and channels - InviteLinks = (1ULL << 27), - Members = (1ULL << 28), - Admins = (1ULL << 29), - BannedUsers = (1ULL << 30), - Rights = (1ULL << 31), - PendingRequests = (1ULL << 32), - Reactions = (1ULL << 33), + InviteLinks = (1ULL << 29), + Members = (1ULL << 30), + Admins = (1ULL << 31), + BannedUsers = (1ULL << 32), + Rights = (1ULL << 33), + PendingRequests = (1ULL << 34), + Reactions = (1ULL << 35), // For channels - ChannelAmIn = (1ULL << 34), - StickersSet = (1ULL << 35), - ChannelLinkedChat = (1ULL << 36), - ChannelLocation = (1ULL << 37), - Slowmode = (1ULL << 38), - GroupCall = (1ULL << 39), + ChannelAmIn = (1ULL << 36), + StickersSet = (1ULL << 37), + ChannelLinkedChat = (1ULL << 38), + ChannelLocation = (1ULL << 39), + Slowmode = (1ULL << 40), + GroupCall = (1ULL << 41), // For iteration - LastUsedBit = (1ULL << 39), + LastUsedBit = (1ULL << 41), }; using Flags = base::flags; friend inline constexpr auto is_flag_type(Flag) { return true; } diff --git a/Telegram/SourceFiles/data/data_chat_filters.cpp b/Telegram/SourceFiles/data/data_chat_filters.cpp index 578e7a960b8bc0..0148ff922b3d58 100644 --- a/Telegram/SourceFiles/data/data_chat_filters.cpp +++ b/Telegram/SourceFiles/data/data_chat_filters.cpp @@ -70,22 +70,8 @@ ChatFilter ChatFilter::FromTL( | (data.is_exclude_read() ? Flag::NoRead : Flag(0)) | (data.is_exclude_archived() ? Flag::NoArchived : Flag(0)); auto &&to_histories = ranges::views::transform([&]( - const MTPInputPeer &data) { - const auto peer = data.match([&](const MTPDinputPeerUser &data) { - const auto user = owner->user(data.vuser_id().v); - user->setAccessHash(data.vaccess_hash().v); - return (PeerData*)user; - }, [&](const MTPDinputPeerChat &data) { - return (PeerData*)owner->chat(data.vchat_id().v); - }, [&](const MTPDinputPeerChannel &data) { - const auto channel = owner->channel(data.vchannel_id().v); - channel->setAccessHash(data.vaccess_hash().v); - return (PeerData*)channel; - }, [&](const MTPDinputPeerSelf &data) { - return (PeerData*)owner->session().user(); - }, [&](const auto &data) { - return (PeerData*)nullptr; - }); + const MTPInputPeer &input) { + const auto peer = Data::PeerFromInputMTP(owner, input); return peer ? owner->history(peer).get() : nullptr; }) | ranges::views::filter([](History *history) { return history != nullptr; diff --git a/Telegram/SourceFiles/data/data_drafts.cpp b/Telegram/SourceFiles/data/data_drafts.cpp index ff33efbe0d76d4..137e2015e961f6 100644 --- a/Telegram/SourceFiles/data/data_drafts.cpp +++ b/Telegram/SourceFiles/data/data_drafts.cpp @@ -12,39 +12,57 @@ For license and copyright information please follow this link: #include "chat_helpers/message_field.h" #include "history/history.h" #include "history/history_widget.h" +#include "history/history_item_components.h" #include "main/main_session.h" #include "data/data_session.h" +#include "data/data_web_page.h" #include "mainwidget.h" #include "storage/localstorage.h" namespace Data { +WebPageDraft WebPageDraft::FromItem(not_null item) { + const auto previewMedia = item->media(); + const auto previewPage = previewMedia + ? previewMedia->webpage() + : nullptr; + using PageFlag = MediaWebPageFlag; + const auto previewFlags = previewMedia + ? previewMedia->webpageFlags() + : PageFlag(); + return { + .id = previewPage ? previewPage->id : 0, + .url = previewPage ? previewPage->url : QString(), + .forceLargeMedia = !!(previewFlags & PageFlag::ForceLargeMedia), + .forceSmallMedia = !!(previewFlags & PageFlag::ForceSmallMedia), + .invert = item->invertMedia(), + .manual = !!(previewFlags & PageFlag::Manual), + .removed = !previewPage, + }; +} + Draft::Draft( const TextWithTags &textWithTags, - MsgId msgId, - MsgId topicRootId, + FullReplyTo reply, const MessageCursor &cursor, - PreviewState previewState, + WebPageDraft webpage, mtpRequestId saveRequestId) : textWithTags(textWithTags) -, msgId(msgId) -, topicRootId(topicRootId) +, reply(std::move(reply)) , cursor(cursor) -, previewState(previewState) +, webpage(webpage) , saveRequestId(saveRequestId) { } Draft::Draft( not_null field, - MsgId msgId, - MsgId topicRootId, - PreviewState previewState, + FullReplyTo reply, + WebPageDraft webpage, mtpRequestId saveRequestId) : textWithTags(field->getTextWithTags()) -, msgId(msgId) -, topicRootId(topicRootId) +, reply(std::move(reply)) , cursor(field) -, previewState(previewState) { +, webpage(webpage) { } void ApplyPeerCloudDraft( @@ -64,15 +82,32 @@ void ApplyPeerCloudDraft( session, draft.ventities().value_or_empty())) }; - const auto replyTo = draft.vreply_to_msg_id().value_or_empty(); + auto replyTo = draft.vreply_to() + ? ReplyToFromMTP(history, *draft.vreply_to()) + : FullReplyTo(); + replyTo.topicRootId = topicRootId; + auto webpage = WebPageDraft{ + .invert = draft.is_invert_media(), + .removed = draft.is_no_webpage(), + }; + if (const auto media = draft.vmedia()) { + media->match([&](const MTPDmessageMediaWebPage &data) { + const auto parsed = session->data().processWebpage( + data.vwebpage()); + if (!parsed->failed) { + webpage.forceLargeMedia = data.is_force_large_media(); + webpage.forceSmallMedia = data.is_force_small_media(); + webpage.manual = data.is_manual(); + webpage.url = parsed->url; + webpage.id = parsed->id; + } + }, [](const auto &) {}); + } auto cloudDraft = std::make_unique( textWithTags, replyTo, - topicRootId, MessageCursor(Ui::kQFixedMax, Ui::kQFixedMax, Ui::kQFixedMax), - (draft.is_no_webpage() - ? Data::PreviewState::Cancelled - : Data::PreviewState::Allowed)); + std::move(webpage)); cloudDraft->date = date; history->setCloudDraft(std::move(cloudDraft)); diff --git a/Telegram/SourceFiles/data/data_drafts.h b/Telegram/SourceFiles/data/data_drafts.h index 2fafab47d52091..fb1810b93f83e9 100644 --- a/Telegram/SourceFiles/data/data_drafts.h +++ b/Telegram/SourceFiles/data/data_drafts.h @@ -7,6 +7,8 @@ For license and copyright information please follow this link: */ #pragma once +#include "data/data_msg_id.h" + namespace Ui { class InputField; } // namespace Ui @@ -28,34 +30,40 @@ void ClearPeerCloudDraft( MsgId topicRootId, TimeId date); -enum class PreviewState : char { - Allowed, - Cancelled, - EmptyOnEdit, +struct WebPageDraft { + [[nodiscard]] static WebPageDraft FromItem(not_null item); + + WebPageId id = 0; + QString url; + bool forceLargeMedia : 1 = false; + bool forceSmallMedia : 1 = false; + bool invert : 1 = false; + bool manual : 1 = false; + bool removed : 1 = false; + + friend inline bool operator==(const WebPageDraft&, const WebPageDraft&) + = default; }; struct Draft { Draft() = default; Draft( const TextWithTags &textWithTags, - MsgId msgId, - MsgId topicRootId, + FullReplyTo reply, const MessageCursor &cursor, - PreviewState previewState, + WebPageDraft webpage, mtpRequestId saveRequestId = 0); Draft( not_null field, - MsgId msgId, - MsgId topicRootId, - PreviewState previewState, + FullReplyTo reply, + WebPageDraft webpage, mtpRequestId saveRequestId = 0); TimeId date = 0; TextWithTags textWithTags; - MsgId msgId = 0; // replyToId for message draft, editMsgId for edit draft - MsgId topicRootId = 0; + FullReplyTo reply; // reply.messageId.msg is editMsgId for edit draft. MessageCursor cursor; - PreviewState previewState = PreviewState::Allowed; + WebPageDraft webpage; mtpRequestId saveRequestId = 0; }; @@ -167,7 +175,8 @@ using HistoryDrafts = base::flat_map>; [[nodiscard]] inline bool DraftIsNull(const Draft *draft) { return !draft - || (!draft->msgId && DraftStringIsEmpty(draft->textWithTags.text)); + || (!draft->reply.messageId + && DraftStringIsEmpty(draft->textWithTags.text)); } [[nodiscard]] inline bool DraftsAreEqual(const Draft *a, const Draft *b) { @@ -179,8 +188,8 @@ using HistoryDrafts = base::flat_map>; return false; } return (a->textWithTags == b->textWithTags) - && (a->msgId == b->msgId) - && (a->previewState == b->previewState); + && (a->reply == b->reply) + && (a->webpage == b->webpage); } } // namespace Data diff --git a/Telegram/SourceFiles/data/data_group_call.cpp b/Telegram/SourceFiles/data/data_group_call.cpp index 6bdb7bb77489ac..8af0bdf0a5529a 100644 --- a/Telegram/SourceFiles/data/data_group_call.cpp +++ b/Telegram/SourceFiles/data/data_group_call.cpp @@ -123,6 +123,7 @@ void GroupCall::requestParticipants() { return; } } + api().request(base::take(_participantsRequestId)).cancel(); _participantsRequestId = api().request(MTPphone_GetGroupParticipants( input(), MTP_vector(), // ids @@ -132,8 +133,8 @@ void GroupCall::requestParticipants() { : _nextOffset), MTP_int(kRequestPerPage) )).done([=](const MTPphone_GroupParticipants &result) { + _participantsRequestId = 0; result.match([&](const MTPDphone_groupParticipants &data) { - _participantsRequestId = 0; const auto reloaded = processSavedFullCall(); _nextOffset = qs(data.vnext_offset()); _peer->owner().processUsers(data.vusers()); @@ -168,7 +169,7 @@ bool GroupCall::processSavedFullCall() { if (!_savedFull) { return false; } - _reloadRequestId = 0; + api().request(base::take(_reloadRequestId)).cancel(); _reloadLastFinished = crl::now(); processFullCallFields(*base::take(_savedFull)); return true; @@ -511,10 +512,8 @@ void GroupCall::reloadIfStale() { void GroupCall::reload() { if (_reloadRequestId || _applyingQueuedUpdates) { return; - } else if (_participantsRequestId) { - api().request(_participantsRequestId).cancel(); - _participantsRequestId = 0; } + api().request(base::take(_participantsRequestId)).cancel(); DEBUG_LOG(("Group Call Participants: " "Reloading with queued: %1" diff --git a/Telegram/SourceFiles/data/data_histories.cpp b/Telegram/SourceFiles/data/data_histories.cpp index 0783fc722f8746..62f5f77c4674c1 100644 --- a/Telegram/SourceFiles/data/data_histories.cpp +++ b/Telegram/SourceFiles/data/data_histories.cpp @@ -7,6 +7,7 @@ For license and copyright information please follow this link: */ #include "data/data_histories.h" +#include "api/api_text_entities.h" #include "data/data_session.h" #include "data/data_channel.h" #include "data/data_chat.h" @@ -33,8 +34,9 @@ constexpr auto kReadRequestTimeout = 3 * crl::time(1000); } // namespace MTPInputReplyTo ReplyToForMTP( - not_null owner, + not_null history, FullReplyTo replyTo) { + const auto owner = &history->owner(); if (replyTo.storyId) { if (const auto peer = owner->peerLoaded(replyTo.storyId.peer)) { if (const auto user = peer->asUser()) { @@ -43,18 +45,45 @@ MTPInputReplyTo ReplyToForMTP( MTP_int(replyTo.storyId.story)); } } - } else if (replyTo.msgId || replyTo.topicRootId) { + } else if (replyTo.messageId || replyTo.topicRootId) { + const auto external = replyTo.messageId + && (replyTo.messageId.peer != history->peer->id); + const auto quoteEntities = Api::EntitiesToMTP( + &history->session(), + replyTo.quote.entities, + Api::ConvertOption::SkipLocal); using Flag = MTPDinputReplyToMessage::Flag; return MTP_inputReplyToMessage( - (replyTo.topicRootId - ? MTP_flags(Flag::f_top_msg_id) - : MTP_flags(0)), - MTP_int(replyTo.msgId ? replyTo.msgId : replyTo.topicRootId), - MTP_int(replyTo.topicRootId)); + MTP_flags((replyTo.topicRootId ? Flag::f_top_msg_id : Flag()) + | (external ? Flag::f_reply_to_peer_id : Flag()) + | (replyTo.quote.text.isEmpty() + ? Flag() + : Flag::f_quote_text) + | (quoteEntities.v.isEmpty() + ? Flag() + : Flag::f_quote_entities)), + MTP_int(replyTo.messageId ? replyTo.messageId.msg : 0), + MTP_int(replyTo.topicRootId), + (external + ? owner->peer(replyTo.messageId.peer)->input + : MTPInputPeer()), + MTP_string(replyTo.quote.text), + quoteEntities); } return MTPInputReplyTo(); } +MTPInputMedia WebPageForMTP( + const Data::WebPageDraft &draft, + bool required) { + using Flag = MTPDinputMediaWebPage::Flag; + return MTP_inputMediaWebPage( + MTP_flags((required ? Flag() : Flag::f_optional) + | (draft.forceLargeMedia ? Flag::f_force_large_media : Flag()) + | (draft.forceSmallMedia ? Flag::f_force_small_media : Flag())), + MTP_string(draft.url)); +} + Histories::Histories(not_null owner) : _owner(owner) , _readRequestsTimer([=] { sendReadRequests(); }) { @@ -911,7 +940,7 @@ int Histories::sendPreparedMessage( not_null history, FullReplyTo replyTo, uint64 randomId, - Fn, FullReplyTo)> message, + Fn, FullReplyTo)> message, Fn done, Fn fail) { if (isCreatingTopic(history, replyTo.topicRootId)) { @@ -926,7 +955,7 @@ int Histories::sendPreparedMessage( } i->second.push_back({ .randomId = randomId, - .replyTo = replyTo.msgId, + .replyTo = replyTo.messageId, .message = std::move(message), .done = std::move(done), .fail = std::move(fail), @@ -936,11 +965,12 @@ int Histories::sendPreparedMessage( return id; } const auto realReplyTo = FullReplyTo{ - .msgId = convertTopicReplyToId(history, replyTo.msgId), - .topicRootId = convertTopicReplyToId(history, replyTo.topicRootId), + .messageId = convertTopicReplyToId(history, replyTo.messageId), + .quote = replyTo.quote, .storyId = replyTo.storyId, + .topicRootId = convertTopicReplyToId(history, replyTo.topicRootId), }; - return v::match(message(_owner, realReplyTo), [&](const auto &request) { + return v::match(message(history, realReplyTo), [&](const auto &request) { const auto type = RequestType::Send; return sendRequest(history, type, [=](Fn finish) { const auto session = &_owner->session(); @@ -984,7 +1014,7 @@ void Histories::checkTopicCreated(FullMsgId rootId, MsgId realRoot) { sendPreparedMessage( history, FullReplyTo{ - .msgId = entry.replyTo, + .messageId = entry.replyTo, .topicRootId = realRoot, }, entry.randomId, @@ -1006,6 +1036,15 @@ void Histories::checkTopicCreated(FullMsgId rootId, MsgId realRoot) { } } +FullMsgId Histories::convertTopicReplyToId( + not_null history, + FullMsgId replyToId) const { + const auto id = (history->peer->id == replyToId.peer) + ? convertTopicReplyToId(history, replyToId.msg) + : replyToId.msg; + return { replyToId.peer, id }; +} + MsgId Histories::convertTopicReplyToId( not_null history, MsgId replyToId) const { diff --git a/Telegram/SourceFiles/data/data_histories.h b/Telegram/SourceFiles/data/data_histories.h index 9d75dc2f5bb931..7fa5e5b07d23b2 100644 --- a/Telegram/SourceFiles/data/data_histories.h +++ b/Telegram/SourceFiles/data/data_histories.h @@ -25,10 +25,14 @@ namespace Data { class Session; class Folder; +struct WebPageDraft; [[nodiscard]] MTPInputReplyTo ReplyToForMTP( - not_null owner, + not_null history, FullReplyTo replyTo); +[[nodiscard]] MTPInputMedia WebPageForMTP( + const Data::WebPageDraft &draft, + bool required = false); class Histories final { public: @@ -108,7 +112,7 @@ class Histories final { not_null history, FullReplyTo replyTo, uint64 randomId, - Fn, FullReplyTo)> message, + Fn, FullReplyTo)> message, Fn done, Fn fail); @@ -116,14 +120,17 @@ class Histories final { }; template static auto PrepareMessage(const Args &...args) - -> Fn, FullReplyTo)> { - return [=](not_null owner, FullReplyTo replyTo) + -> Fn, FullReplyTo)> { + return [=](not_null history, FullReplyTo replyTo) -> RequestType { - return { ReplaceReplyIds(owner, args, replyTo)... }; + return { ReplaceReplyIds(history, args, replyTo)... }; }; } void checkTopicCreated(FullMsgId rootId, MsgId realRoot); + [[nodiscard]] FullMsgId convertTopicReplyToId( + not_null history, + FullMsgId replyToId) const; [[nodiscard]] MsgId convertTopicReplyToId( not_null history, MsgId replyToId) const; @@ -152,8 +159,8 @@ class Histories final { }; struct DelayedByTopicMessage { uint64 randomId = 0; - MsgId replyTo = 0; - Fn, FullReplyTo)> message; + FullMsgId replyTo; + Fn, FullReplyTo)> message; Fn done; Fn fail; int requestId = 0; @@ -169,11 +176,11 @@ class Histories final { template static auto ReplaceReplyIds( - not_null owner, + not_null history, Arg arg, FullReplyTo replyTo) { if constexpr (std::is_same_v) { - return ReplyToForMTP(owner, replyTo); + return ReplyToForMTP(history, replyTo); } else { return arg; } diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp index 1ab3b1c02767a9..45f31c58aa2cbc 100644 --- a/Telegram/SourceFiles/data/data_media_types.cpp +++ b/Telegram/SourceFiles/data/data_media_types.cpp @@ -20,6 +20,7 @@ For license and copyright information please follow this link: #include "history/view/media/history_view_contact.h" #include "history/view/media/history_view_location.h" #include "history/view/media/history_view_game.h" +#include "history/view/media/history_view_giveaway.h" #include "history/view/media/history_view_invoice.h" #include "history/view/media/history_view_call.h" #include "history/view/media/history_view_web_page.h" @@ -361,6 +362,28 @@ Call ComputeCallData(const MTPDmessageActionPhoneCall &call) { return result; } +Giveaway ComputeGiveawayData( + not_null item, + const MTPDmessageMediaGiveaway &data) { + auto result = Giveaway{ + .untilDate = data.vuntil_date().v, + .quantity = data.vquantity().v, + .months = data.vmonths().v, + }; + result.channels.reserve(data.vchannels().v.size()); + const auto owner = &item->history()->owner(); + for (const auto &id : data.vchannels().v) { + result.channels.push_back(owner->channel(ChannelId(id))); + } + if (const auto countries = data.vcountries_iso2()) { + result.countries.reserve(countries->v.size()); + for (const auto &country : countries->v) { + result.countries.push_back(qs(country)); + } + } + return result; +} + Media::Media(not_null parent) : _parent(parent) { } @@ -380,6 +403,10 @@ WebPageData *Media::webpage() const { return nullptr; } +MediaWebPageFlags Media::webpageFlags() const { + return {}; +} + const SharedContact *Media::sharedContact() const { return nullptr; } @@ -420,6 +447,10 @@ bool Media::storyMention() const { return false; } +const Giveaway *Media::giveaway() const { + return nullptr; +} + bool Media::uploading() const { return false; } @@ -1406,9 +1437,11 @@ QString MediaCall::Text( MediaWebPage::MediaWebPage( not_null parent, - not_null page) + not_null page, + MediaWebPageFlags flags) : Media(parent) -, _page(page) { +, _page(page) +, _flags(flags) { parent->history()->owner().registerWebPageItem(_page, parent); } @@ -1417,7 +1450,7 @@ MediaWebPage::~MediaWebPage() { } std::unique_ptr MediaWebPage::clone(not_null parent) { - return std::make_unique(parent, _page); + return std::make_unique(parent, _page, _flags); } DocumentData *MediaWebPage::document() const { @@ -1432,6 +1465,10 @@ WebPageData *MediaWebPage::webpage() const { return _page; } +MediaWebPageFlags MediaWebPage::webpageFlags() const { + return _flags; +} + bool MediaWebPage::hasReplyPreview() const { if (const auto document = MediaWebPage::document()) { return document->hasThumbnail() @@ -1462,10 +1499,13 @@ bool MediaWebPage::replyPreviewLoaded() const { } ItemPreview MediaWebPage::toPreview(ToPreviewOptions options) const { - return { .text = options.translated + auto text = options.translated ? parent()->translatedText() - : parent()->originalText() - }; + : parent()->originalText(); + if (text.empty()) { + text = Ui::Text::Colorized(_page->url); + } + return { .text = text }; } TextWithEntities MediaWebPage::notificationText() const { @@ -1496,7 +1536,7 @@ std::unique_ptr MediaWebPage::createView( not_null message, not_null realParent, HistoryView::Element *replacing) { - return std::make_unique(message, _page); + return std::make_unique(message, _page, _flags); } MediaGame::MediaGame( @@ -1887,21 +1927,28 @@ MediaGiftBox::MediaGiftBox( not_null parent, not_null from, int months) +: MediaGiftBox(parent, from, GiftCode{ .months = months }) { +} + +MediaGiftBox::MediaGiftBox( + not_null parent, + not_null from, + GiftCode data) : Media(parent) , _from(from) -, _months(months) { +, _data(std::move(data)) { } std::unique_ptr MediaGiftBox::clone(not_null parent) { - return std::make_unique(parent, _from, _months); + return std::make_unique(parent, _from, _data); } not_null MediaGiftBox::from() const { return _from; } -int MediaGiftBox::months() const { - return _months; +const GiftCode &MediaGiftBox::data() const { + return _data; } TextWithEntities MediaGiftBox::notificationText() const { @@ -1933,14 +1980,6 @@ std::unique_ptr MediaGiftBox::createView( std::make_unique(message, this)); } -bool MediaGiftBox::activated() const { - return _activated; -} - -void MediaGiftBox::setActivated(bool activated) { - _activated = activated; -} - MediaWallPaper::MediaWallPaper( not_null parent, const WallPaper &paper) @@ -2144,4 +2183,50 @@ std::unique_ptr MediaStory::createView( } } +MediaGiveaway::MediaGiveaway( + not_null parent, + const Giveaway &data) +: Media(parent) +, _giveaway(data) { +} + +std::unique_ptr MediaGiveaway::clone(not_null parent) { + return std::make_unique(parent, _giveaway); +} + +const Giveaway *MediaGiveaway::giveaway() const { + return &_giveaway; +} + +TextWithEntities MediaGiveaway::notificationText() const { + return { + .text = tr::lng_prizes_title(tr::now, lt_count, _giveaway.quantity), + }; +} + +QString MediaGiveaway::pinnedTextSubstring() const { + return QString::fromUtf8("\xC2\xAB") + + notificationText().text + + QString::fromUtf8("\xC2\xBB"); +} + +TextForMimeData MediaGiveaway::clipboardText() const { + return TextForMimeData(); +} + +bool MediaGiveaway::updateInlineResultMedia(const MTPMessageMedia &media) { + return true; +} + +bool MediaGiveaway::updateSentMedia(const MTPMessageMedia &media) { + return true; +} + +std::unique_ptr MediaGiveaway::createView( + not_null message, + not_null realParent, + HistoryView::Element *replacing) { + return std::make_unique(message, &_giveaway); +} + } // namespace Data diff --git a/Telegram/SourceFiles/data/data_media_types.h b/Telegram/SourceFiles/data/data_media_types.h index aeb85049599848..532e795f4e093f 100644 --- a/Telegram/SourceFiles/data/data_media_types.h +++ b/Telegram/SourceFiles/data/data_media_types.h @@ -90,6 +90,23 @@ struct Invoice { bool isTest = false; }; +struct Giveaway { + std::vector> channels; + std::vector countries; + TimeId untilDate = 0; + int quantity = 0; + int months = 0; + bool all = false; +}; + +struct GiftCode { + QString slug; + ChannelData *channel = nullptr; + int months = 0; + bool viaGiveaway = false; + bool unclaimed = false; +}; + class Media { public: Media(not_null parent); @@ -106,6 +123,7 @@ class Media { virtual DocumentData *document() const; virtual PhotoData *photo() const; virtual WebPageData *webpage() const; + virtual MediaWebPageFlags webpageFlags() const; virtual const SharedContact *sharedContact() const; virtual const Call *call() const; virtual GameData *game() const; @@ -116,6 +134,7 @@ class Media { virtual FullStoryId storyId() const; virtual bool storyExpired(bool revalidate = false); virtual bool storyMention() const; + virtual const Giveaway *giveaway() const; virtual bool uploading() const; virtual Storage::SharedMediaTypesMask sharedMediaTypes() const; @@ -355,7 +374,8 @@ class MediaWebPage final : public Media { public: MediaWebPage( not_null parent, - not_null page); + not_null page, + MediaWebPageFlags flags); ~MediaWebPage(); std::unique_ptr clone(not_null parent) override; @@ -363,6 +383,7 @@ class MediaWebPage final : public Media { DocumentData *document() const override; PhotoData *photo() const override; WebPageData *webpage() const override; + MediaWebPageFlags webpageFlags() const override; bool hasReplyPreview() const override; Image *replyPreview() const override; @@ -381,7 +402,8 @@ class MediaWebPage final : public Media { HistoryView::Element *replacing = nullptr) override; private: - not_null _page; + const not_null _page; + const MediaWebPageFlags _flags; }; @@ -517,14 +539,15 @@ class MediaGiftBox final : public Media { not_null parent, not_null from, int months); + MediaGiftBox( + not_null parent, + not_null from, + GiftCode data); std::unique_ptr clone(not_null parent) override; [[nodiscard]] not_null from() const; - [[nodiscard]] int months() const; - - [[nodiscard]] bool activated() const; - void setActivated(bool activated); + [[nodiscard]] const GiftCode &data() const; TextWithEntities notificationText() const override; QString pinnedTextSubstring() const override; @@ -539,8 +562,7 @@ class MediaGiftBox final : public Media { private: not_null _from; - int _months = 0; - bool _activated = false; + GiftCode _data; }; @@ -605,6 +627,32 @@ class MediaStory final : public Media, public base::has_weak_ptr { }; +class MediaGiveaway final : public Media { +public: + MediaGiveaway( + not_null parent, + const Giveaway &data); + + std::unique_ptr clone(not_null parent) override; + + const Giveaway *giveaway() const override; + + TextWithEntities notificationText() const override; + QString pinnedTextSubstring() const override; + TextForMimeData clipboardText() const override; + + bool updateInlineResultMedia(const MTPMessageMedia &media) override; + bool updateSentMedia(const MTPMessageMedia &media) override; + std::unique_ptr createView( + not_null message, + not_null realParent, + HistoryView::Element *replacing = nullptr) override; + +private: + Giveaway _giveaway; + +}; + [[nodiscard]] TextForMimeData WithCaptionClipboardText( const QString &attachType, TextForMimeData &&caption); @@ -615,4 +663,8 @@ class MediaStory final : public Media, public base::has_weak_ptr { [[nodiscard]] Call ComputeCallData(const MTPDmessageActionPhoneCall &call); +[[nodiscard]] Giveaway ComputeGiveawayData( + not_null item, + const MTPDmessageMediaGiveaway &data); + } // namespace Data diff --git a/Telegram/SourceFiles/data/data_msg_id.h b/Telegram/SourceFiles/data/data_msg_id.h index 70379be1095e5d..e98ae30d4fd729 100644 --- a/Telegram/SourceFiles/data/data_msg_id.h +++ b/Telegram/SourceFiles/data/data_msg_id.h @@ -7,7 +7,9 @@ For license and copyright information please follow this link: */ #pragma once +#include "base/qt/qt_compare.h" #include "data/data_peer_id.h" +#include "ui/text/text_entity.h" struct MsgId { constexpr MsgId() noexcept = default; @@ -67,21 +69,6 @@ struct FullStoryId { friend inline bool operator==(FullStoryId, FullStoryId) = default; }; -struct FullReplyTo { - MsgId msgId = 0; - MsgId topicRootId = 0; - FullStoryId storyId; - - [[nodiscard]] bool valid() const { - return msgId || (storyId && peerIsUser(storyId.peer)); - } - explicit operator bool() const { - return valid(); - } - friend inline auto operator<=>(FullReplyTo, FullReplyTo) = default; - friend inline bool operator==(FullReplyTo, FullReplyTo) = default; -}; - constexpr auto StartClientMsgId = MsgId(0x01 - (1LL << 58)); constexpr auto ClientMsgIds = (1LL << 31); constexpr auto EndClientMsgId = MsgId(StartClientMsgId.bare + ClientMsgIds); @@ -169,6 +156,22 @@ struct FullMsgId { Q_DECLARE_METATYPE(FullMsgId); +struct FullReplyTo { + FullMsgId messageId; + TextWithEntities quote; + FullStoryId storyId; + MsgId topicRootId = 0; + + [[nodiscard]] bool valid() const { + return messageId || (storyId && peerIsUser(storyId.peer)); + } + explicit operator bool() const { + return valid(); + } + friend inline auto operator<=>(FullReplyTo, FullReplyTo) = default; + friend inline bool operator==(FullReplyTo, FullReplyTo) = default; +}; + struct GlobalMsgId { FullMsgId itemId; uint64 sessionUniqueId = 0; diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp index 9e81f5760207b5..4a8f15f5d9f601 100644 --- a/Telegram/SourceFiles/data/data_peer.cpp +++ b/Telegram/SourceFiles/data/data_peer.cpp @@ -38,6 +38,7 @@ For license and copyright information please follow this link: #include "window/window_session_controller.h" #include "window/main_window.h" // Window::LogoNoMargin. #include "ui/image/image.h" +#include "ui/chat/chat_style.h" #include "ui/empty_userpic.h" #include "ui/text/text_options.h" #include "ui/painter.h" @@ -60,8 +61,8 @@ using UpdateFlag = Data::PeerUpdate::Flag; namespace Data { -int PeerColorIndex(PeerId peerId) { - return Ui::EmptyUserpic::ColorIndex(peerId.value & PeerId::kChatTypeMask); +uint8 DecideColorIndex(PeerId peerId) { + return Ui::DecideColorIndex(peerId.value & PeerId::kChatTypeMask); } PeerId FakePeerIdForJustName(const QString &name) { @@ -126,6 +127,40 @@ AllowedReactions Parse(const MTPChatReactions &value) { }); } +PeerData *PeerFromInputMTP( + not_null owner, + const MTPInputPeer &input) { + return input.match([&](const MTPDinputPeerUser &data) { + const auto user = owner->user(data.vuser_id().v); + user->setAccessHash(data.vaccess_hash().v); + return (PeerData*)user; + }, [&](const MTPDinputPeerChat &data) { + return (PeerData*)owner->chat(data.vchat_id().v); + }, [&](const MTPDinputPeerChannel &data) { + const auto channel = owner->channel(data.vchannel_id().v); + channel->setAccessHash(data.vaccess_hash().v); + return (PeerData*)channel; + }, [&](const MTPDinputPeerSelf &data) { + return (PeerData*)owner->session().user(); + }, [&](const auto &data) { + return (PeerData*)nullptr; + }); +} + +UserData *UserFromInputMTP( + not_null owner, + const MTPInputUser &input) { + return input.match([&](const MTPDinputUser &data) { + const auto user = owner->user(data.vuser_id().v); + user->setAccessHash(data.vaccess_hash().v); + return user.get(); + }, [&](const MTPDinputUserSelf &data) { + return owner->session().user().get(); + }, [](const auto &data) { + return (UserData*)nullptr; + }); +} + } // namespace Data PeerClickHandler::PeerClickHandler(not_null peer) @@ -158,7 +193,8 @@ void PeerClickHandler::onClick(ClickContext context) const { PeerData::PeerData(not_null owner, PeerId id) : id(id) -, _owner(owner) { +, _owner(owner) +, _colorIndex(Data::DecideColorIndex(id)) { } Data::Session &PeerData::owner() const { @@ -231,7 +267,7 @@ not_null PeerData::ensureEmptyUserpic() const { if (!_userpicEmpty) { const auto user = asUser(); _userpicEmpty = std::make_unique( - Ui::EmptyUserpic::UserpicColor(Data::PeerColorIndex(id)), + Ui::EmptyUserpic::UserpicColor(colorIndex()), ((user && user->isInaccessible()) ? Ui::EmptyUserpic::InaccessibleName() : name())); @@ -252,7 +288,7 @@ void PeerData::setUserpic( const ImageLocation &location, bool hasVideo) { _userpicPhotoId = photoId; - _userpicHasVideo = hasVideo; + _userpicHasVideo = hasVideo ? 1 : 0; _userpic.set(&session(), ImageWithLocation{ .location = location }); } @@ -416,6 +452,22 @@ QImage PeerData::generateUserpicImage( return result; } +ImageLocation PeerData::userpicLocation() const { + return _userpic.location(); +} + +bool PeerData::userpicPhotoUnknown() const { + return (_userpicPhotoId == kUnknownPhotoId); +} + +PhotoId PeerData::userpicPhotoId() const { + return userpicPhotoUnknown() ? 0 : _userpicPhotoId; +} + +bool PeerData::userpicHasVideo() const { + return _userpicHasVideo != 0; +} + Data::FileOrigin PeerData::userpicOrigin() const { return Data::FileOriginPeerPhoto(id); } @@ -455,7 +507,7 @@ void PeerData::setUserpicChecked( bool hasVideo) { if (_userpicPhotoId != photoId || _userpic.location() != location - || _userpicHasVideo != hasVideo) { + || _userpicHasVideo != (hasVideo ? 1 : 0)) { const auto known = !userpicPhotoUnknown(); setUserpic(photoId, location, hasVideo); session().changes().peerUpdated(this, UpdateFlag::Photo); @@ -629,6 +681,20 @@ void PeerData::setSettings(const MTPPeerSettings &data) { }); } +bool PeerData::changeColorIndex( + const tl::conditional &cloudColorIndex) { + return cloudColorIndex + ? changeColorIndex(cloudColorIndex->v) + : clearColorIndex(); +} + +bool PeerData::changeBackgroundEmojiId( + const tl::conditional &cloudBackgroundEmoji) { + return changeBackgroundEmojiId(cloudBackgroundEmoji + ? cloudBackgroundEmoji->v + : DocumentId()); +} + void PeerData::fillNames() { _nameWords.clear(); _nameFirstLetters.clear(); @@ -845,6 +911,36 @@ QString PeerData::userName() const { return QString(); } +bool PeerData::changeColorIndex(uint8 index) { + index %= Ui::kColorIndexCount; + if (_colorIndexCloud && _colorIndex == index) { + return false; + } + _colorIndexCloud = 1; + _colorIndex = index; + return true; +} + +bool PeerData::clearColorIndex() { + if (!_colorIndexCloud) { + return false; + } + _colorIndexCloud = 0; + _colorIndex = Data::DecideColorIndex(id); + return true; +} + +DocumentId PeerData::backgroundEmojiId() const { + return _backgroundEmojiId; +} + +bool PeerData::changeBackgroundEmojiId(DocumentId id) { + if (_backgroundEmojiId == id) { + return false; + } + _backgroundEmojiId = id; + return true; +} bool PeerData::isSelf() const { if (const auto user = asUser()) { return (user->flags() & UserDataFlag::Self); diff --git a/Telegram/SourceFiles/data/data_peer.h b/Telegram/SourceFiles/data/data_peer.h index 3ef564e7a14ea9..2a4ae3fe532c79 100644 --- a/Telegram/SourceFiles/data/data_peer.h +++ b/Telegram/SourceFiles/data/data_peer.h @@ -39,7 +39,7 @@ class GroupCall; struct ReactionId; class WallPaper; -[[nodiscard]] int PeerColorIndex(PeerId peerId); +[[nodiscard]] uint8 DecideColorIndex(PeerId peerId); // Must be used only for PeerColor-s. [[nodiscard]] PeerId FakePeerIdForJustName(const QString &name); @@ -116,6 +116,12 @@ bool operator<(const AllowedReactions &a, const AllowedReactions &b); bool operator==(const AllowedReactions &a, const AllowedReactions &b); [[nodiscard]] AllowedReactions Parse(const MTPChatReactions &value); +[[nodiscard]] PeerData *PeerFromInputMTP( + not_null owner, + const MTPInputPeer &input); +[[nodiscard]] UserData *UserFromInputMTP( + not_null owner, + const MTPInputUser &input); } // namespace Data @@ -164,6 +170,14 @@ class PeerData { [[nodiscard]] Main::Session &session() const; [[nodiscard]] Main::Account &account() const; + [[nodiscard]] uint8 colorIndex() const { + return _colorIndex; + } + bool changeColorIndex(uint8 index); + bool clearColorIndex(); + [[nodiscard]] DocumentId backgroundEmojiId() const; + bool changeBackgroundEmojiId(DocumentId id); + [[nodiscard]] bool isUser() const { return peerIsUser(id); } @@ -285,20 +299,12 @@ class PeerData { Ui::PeerUserpicView &view, int size, std::optional radius = {}) const; - [[nodiscard]] ImageLocation userpicLocation() const { - return _userpic.location(); - } + [[nodiscard]] ImageLocation userpicLocation() const; static constexpr auto kUnknownPhotoId = PhotoId(0xFFFFFFFFFFFFFFFFULL); - [[nodiscard]] bool userpicPhotoUnknown() const { - return (_userpicPhotoId == kUnknownPhotoId); - } - [[nodiscard]] PhotoId userpicPhotoId() const { - return userpicPhotoUnknown() ? 0 : _userpicPhotoId; - } - [[nodiscard]] bool userpicHasVideo() const { - return _userpicHasVideo; - } + [[nodiscard]] bool userpicPhotoUnknown() const; + [[nodiscard]] PhotoId userpicPhotoId() const; + [[nodiscard]] bool userpicHasVideo() const; [[nodiscard]] Data::FileOrigin userpicOrigin() const; [[nodiscard]] Data::FileOrigin userpicPhotoOrigin() const; @@ -361,6 +367,9 @@ class PeerData { void saveTranslationDisabled(bool disabled); void setSettings(const MTPPeerSettings &data); + bool changeColorIndex(const tl::conditional &cloudColorIndex); + bool changeBackgroundEmojiId( + const tl::conditional &cloudBackgroundEmoji); enum class BlockStatus : char { Unknown, @@ -453,6 +462,7 @@ class PeerData { base::flat_set _nameWords; // for filtering base::flat_set _nameFirstLetters; + uint64 _backgroundEmojiId = 0; crl::time _lastFullUpdate = 0; QString _name; @@ -460,14 +470,16 @@ class PeerData { TimeId _ttlPeriod = 0; + QString _requestChatTitle; + TimeId _requestChatDate = 0; + Settings _settings = PeerSettings(PeerSetting::Unknown); BlockStatus _blockStatus = BlockStatus::Unknown; LoadedStatus _loadedStatus = LoadedStatus::Not; TranslationFlag _translationFlag = TranslationFlag::Unknown; - bool _userpicHasVideo = false; - - QString _requestChatTitle; - TimeId _requestChatDate = 0; + uint8 _colorIndex : 6 = 0; + uint8 _colorIndexCloud : 1 = 0; + uint8 _userpicHasVideo : 1 = 0; QString _about; QString _themeEmoticon; diff --git a/Telegram/SourceFiles/data/data_reply_preview.cpp b/Telegram/SourceFiles/data/data_reply_preview.cpp index c57fc183533477..a6a02e81ad4898 100644 --- a/Telegram/SourceFiles/data/data_reply_preview.cpp +++ b/Telegram/SourceFiles/data/data_reply_preview.cpp @@ -40,14 +40,14 @@ void ReplyPreview::prepare( if (h <= 0) h = 1; auto thumbSize = (w > h) ? QSize( - w * st::msgReplyBarSize.height() / h, - st::msgReplyBarSize.height()) + w * st::historyReplyPreview / h, + st::historyReplyPreview) : QSize( - st::msgReplyBarSize.height(), - h * st::msgReplyBarSize.height() / w); + st::historyReplyPreview, + h * st::historyReplyPreview / w); thumbSize *= style::DevicePixelRatio(); options |= Option::TransparentBackground; - auto outerSize = st::msgReplyBarSize.height(); + auto outerSize = st::historyReplyPreview; auto original = spoiler ? image->original().scaled( { 40, 40 }, diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 013c1c912847c2..efb5edf69436e5 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -705,6 +705,19 @@ not_null Session::processUser(const MTPUser &data) { if (canShareThisContact != result->canShareThisContactFast()) { flags |= UpdateFlag::CanShareContact; } + + auto decorationsUpdated = false; + if (result->changeColorIndex(data.vcolor())) { + flags |= UpdateFlag::Color; + decorationsUpdated = true; + } + if (result->changeBackgroundEmojiId(data.vbackground_emoji_id())) { + flags |= UpdateFlag::BackgroundEmoji; + decorationsUpdated = true; + } + if (decorationsUpdated && result->isMinimalLoaded()) { + _peerDecorationsUpdated.fire_copy(result); + } }); if (minimal) { @@ -979,6 +992,18 @@ not_null Session::processChat(const MTPChat &data) { if (wasCallNotEmpty != Data::ChannelHasActiveCall(channel)) { flags |= UpdateFlag::GroupCall; } + auto decorationsUpdated = false; + if (result->changeColorIndex(data.vcolor())) { + flags |= UpdateFlag::Color; + decorationsUpdated = true; + } + if (result->changeBackgroundEmojiId(data.vbackground_emoji_id())) { + flags |= UpdateFlag::BackgroundEmoji; + decorationsUpdated = true; + } + if (decorationsUpdated && result->isMinimalLoaded()) { + _peerDecorationsUpdated.fire_copy(result); + } }, [&](const MTPDchannelForbidden &data) { const auto channel = result->asChannel(); @@ -3254,8 +3279,10 @@ not_null Session::processWebpage(const MTPWebPage &data) { return processWebpage(data.c_webPage()); case mtpc_webPageEmpty: { const auto result = webpage(data.c_webPageEmpty().vid().v); + result->type = WebPageType::None; if (result->pendingTill > 0) { - result->pendingTill = -1; // failed + result->pendingTill = 0; + result->failed = 1; notifyWebPageUpdateDelayed(result); } return result; @@ -3276,12 +3303,13 @@ not_null Session::processWebpage(const MTPDwebPage &data) { return result; } -not_null Session::processWebpage(const MTPDwebPagePending &data) { +not_null Session::processWebpage( + const MTPDwebPagePending &data) { constexpr auto kDefaultPendingTimeout = 60; const auto result = webpage(data.vid().v); webpageApplyFields( result, - WebPageType::Article, + WebPageType::None, QString(), QString(), QString(), @@ -3293,6 +3321,7 @@ not_null Session::processWebpage(const MTPDwebPagePending &data) { WebPageCollage(), 0, QString(), + false, data.vdate().v ? data.vdate().v : (base::unixtime::now() + kDefaultPendingTimeout)); @@ -3316,6 +3345,7 @@ not_null Session::webpage( WebPageCollage(), 0, QString(), + false, TimeId(0)); } @@ -3332,6 +3362,7 @@ not_null Session::webpage( WebPageCollage &&collage, int duration, const QString &author, + bool hasLargeMedia, TimeId pendingTill) { const auto result = webpage(id); webpageApplyFields( @@ -3348,6 +3379,7 @@ not_null Session::webpage( std::move(collage), duration, author, + hasLargeMedia, pendingTill); return result; } @@ -3447,6 +3479,7 @@ void Session::webpageApplyFields( WebPageCollage(this, data), data.vduration().value_or_empty(), qs(data.vauthor().value_or_empty()), + data.is_has_large_media(), pendingTill); } @@ -3464,6 +3497,7 @@ void Session::webpageApplyFields( WebPageCollage &&collage, int duration, const QString &author, + bool hasLargeMedia, TimeId pendingTill) { const auto requestPending = (!page->pendingTill && pendingTill > 0); const auto changed = page->applyChanges( @@ -3479,6 +3513,7 @@ void Session::webpageApplyFields( std::move(collage), duration, author, + hasLargeMedia, pendingTill); if (requestPending) { _session->api().requestWebPageDelayed(page); @@ -4312,7 +4347,8 @@ auto Session::dialogsRowReplacements() const void Session::serviceNotification( const TextWithEntities &message, - const MTPMessageMedia &media) { + const MTPMessageMedia &media, + bool invertMedia) { const auto date = base::unixtime::now(); if (!peerLoaded(PeerData::kServiceNotificationsId)) { processUser(MTP_user( @@ -4335,25 +4371,32 @@ void Session::serviceNotification( MTPstring(), // lang_code MTPEmojiStatus(), MTPVector(), - MTPint())); // stories_max_id + MTPint(), // stories_max_id + MTP_int(0), // color + MTPlong())); // background_emoji_id } const auto history = this->history(PeerData::kServiceNotificationsId); + const auto insert = [=] { + insertCheckedServiceNotification(message, media, date, invertMedia); + }; if (!history->folderKnown()) { - histories().requestDialogEntry(history, [=] { - insertCheckedServiceNotification(message, media, date); - }); + histories().requestDialogEntry(history, insert); } else { - insertCheckedServiceNotification(message, media, date); + insert(); } } void Session::insertCheckedServiceNotification( const TextWithEntities &message, const MTPMessageMedia &media, - TimeId date) { + TimeId date, + bool invertMedia) { const auto flags = MTPDmessage::Flag::f_entities | MTPDmessage::Flag::f_from_id - | MTPDmessage::Flag::f_media; + | MTPDmessage::Flag::f_media + | (invertMedia + ? MTPDmessage::Flag::f_invert_media + : MTPDmessage::Flag()); const auto localFlags = MessageFlag::ClientSideUnread | MessageFlag::Local; auto sending = TextWithEntities(), left = message; @@ -4497,6 +4540,10 @@ auto Session::webViewResultSent() const -> rpl::producer { return _webViewResultSent.events(); } +rpl::producer> Session::peerDecorationsUpdated() const { + return _peerDecorationsUpdated.events(); +} + void Session::clearLocalStorage() { _cache->close(); _cache->clear(); diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index 0b8b3f51b2b4b9..0e99494bc1b5ff 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -20,7 +20,7 @@ For license and copyright information please follow this link: class Image; class HistoryItem; struct WebPageCollage; -enum class WebPageType; +enum class WebPageType : uint8; enum class NewMessageType; namespace HistoryView { @@ -562,6 +562,7 @@ class Session final { WebPageCollage &&collage, int duration, const QString &author, + bool hasLargeMedia, TimeId pendingTill); [[nodiscard]] not_null game(GameId id); @@ -704,7 +705,8 @@ class Session final { void serviceNotification( const TextWithEntities &message, - const MTPMessageMedia &media = MTP_messageMediaEmpty()); + const MTPMessageMedia &media = MTP_messageMediaEmpty(), + bool invertMedia = false); void setMimeForwardIds(MessageIdsList &&list); MessageIdsList takeMimeForwardIds(); @@ -725,6 +727,9 @@ class Session final { void webViewResultSent(WebViewResultSent &&sent); [[nodiscard]] rpl::producer webViewResultSent() const; + [[nodiscard]] auto peerDecorationsUpdated() const + -> rpl::producer>; + void clearLocalStorage(); private: @@ -824,6 +829,7 @@ class Session final { WebPageCollage &&collage, int duration, const QString &author, + bool hasLargeMedia, TimeId pendingTill); void gameApplyFields( @@ -846,7 +852,8 @@ class Session final { void insertCheckedServiceNotification( const TextWithEntities &message, const MTPMessageMedia &media, - TimeId date); + TimeId date, + bool invertMedia); void setWallpapers(const QVector &data, uint64 hash); void highlightProcessDone(uint64 processId); @@ -1009,6 +1016,8 @@ class Session final { rpl::event_stream _webViewResultSent; + rpl::event_stream> _peerDecorationsUpdated; + Groups _groups; const std::unique_ptr _chatsFilters; std::unique_ptr _scheduledMessages; diff --git a/Telegram/SourceFiles/data/data_types.h b/Telegram/SourceFiles/data/data_types.h index a60776ee294a29..d04b9f49a5d71a 100644 --- a/Telegram/SourceFiles/data/data_types.h +++ b/Telegram/SourceFiles/data/data_types.h @@ -246,65 +246,78 @@ enum class MessageFlag : uint64 { MentionsMe = (1ULL << 15), IsOrWasScheduled = (1ULL << 16), NoForwards = (1ULL << 17), + InvertMedia = (1ULL << 18), // Needs to return back to inline mode. - HasSwitchInlineButton = (1ULL << 18), + HasSwitchInlineButton = (1ULL << 19), // For "shared links" indexing. - HasTextLinks = (1ULL << 19), + HasTextLinks = (1ULL << 20), // Group / channel create or migrate service message. - IsGroupEssential = (1ULL << 20), + IsGroupEssential = (1ULL << 21), // Edited media is generated on the client // and should not update media from server. - IsLocalUpdateMedia = (1ULL << 21), + IsLocalUpdateMedia = (1ULL << 22), // Sent from inline bot, need to re-set media when sent. - FromInlineBot = (1ULL << 22), + FromInlineBot = (1ULL << 23), // Generated on the client side and should be unread. - ClientSideUnread = (1ULL << 23), + ClientSideUnread = (1ULL << 24), // In a supergroup. - HasAdminBadge = (1ULL << 24), + HasAdminBadge = (1ULL << 25), // Outgoing message that is being sent. - BeingSent = (1ULL << 25), + BeingSent = (1ULL << 26), // Outgoing message and failed to be sent. - SendingFailed = (1ULL << 26), + SendingFailed = (1ULL << 27), // No media and only a several emoji or an only custom emoji text. - SpecialOnlyEmoji = (1ULL << 27), + SpecialOnlyEmoji = (1ULL << 28), // Message existing in the message history. - HistoryEntry = (1ULL << 28), + HistoryEntry = (1ULL << 29), // Local message, not existing on the server. - Local = (1ULL << 29), + Local = (1ULL << 30), // Fake message for some UI element. - FakeHistoryItem = (1ULL << 30), + FakeHistoryItem = (1ULL << 31), // Contact sign-up message, notification should be skipped for Silent. - IsContactSignUp = (1ULL << 31), + IsContactSignUp = (1ULL << 32), // Optimization for item text custom emoji repainting. - CustomEmojiRepainting = (1ULL << 32), + CustomEmojiRepainting = (1ULL << 33), // Profile photo suggestion, views have special media type. - IsUserpicSuggestion = (1ULL << 33), + IsUserpicSuggestion = (1ULL << 34), - OnlyEmojiAndSpaces = (1ULL << 34), - OnlyEmojiAndSpacesSet = (1ULL << 35), + OnlyEmojiAndSpaces = (1ULL << 35), + OnlyEmojiAndSpacesSet = (1ULL << 36), // Fake message with bot cover and information. - FakeBotAbout = (1ULL << 36), + FakeBotAbout = (1ULL << 37), - StoryItem = (1ULL << 37), + StoryItem = (1ULL << 38), - InHighlightProcess = (1ULL << 38), + InHighlightProcess = (1ULL << 39), + + // If not set then we need to refresh _displayFrom value. + DisplayFromChecked = (1ULL << 40), }; inline constexpr bool is_flag_type(MessageFlag) { return true; } using MessageFlags = base::flags; + +enum class MediaWebPageFlag : uint8 { + ForceLargeMedia = (1 << 0), + ForceSmallMedia = (1 << 1), + Manual = (1 << 2), + Safe = (1 << 3), +}; +inline constexpr bool is_flag_type(MediaWebPageFlag) { return true; } +using MediaWebPageFlags = base::flags; diff --git a/Telegram/SourceFiles/data/data_web_page.cpp b/Telegram/SourceFiles/data/data_web_page.cpp index 1c150902ed3a41..f4cadf4572c823 100644 --- a/Telegram/SourceFiles/data/data_web_page.cpp +++ b/Telegram/SourceFiles/data/data_web_page.cpp @@ -14,6 +14,7 @@ For license and copyright information please follow this link: #include "data/data_photo.h" #include "data/data_channel.h" #include "data/data_document.h" +#include "lang/lang_keys.h" #include "ui/image/image.h" #include "ui/text/text_entity.h" @@ -224,9 +225,10 @@ bool WebPageData::applyChanges( WebPageCollage &&newCollage, int newDuration, const QString &newAuthor, + bool newHasLargeMedia, int newPendingTill) { if (newPendingTill != 0 - && (!url.isEmpty() || pendingTill < 0) + && (!url.isEmpty() || failed) && (!pendingTill || pendingTill == newPendingTill || newPendingTill < -1)) { @@ -265,6 +267,7 @@ bool WebPageData::applyChanges( && collage.items == newCollage.items && duration == newDuration && author == resultAuthor + && hasLargeMedia == (newHasLargeMedia ? 1 : 0) && pendingTill == newPendingTill) { return false; } @@ -272,6 +275,7 @@ bool WebPageData::applyChanges( _owner->session().api().clearWebPageRequest(this); } type = newType; + hasLargeMedia = newHasLargeMedia ? 1 : 0; url = resultUrl; displayUrl = resultDisplayUrl; siteName = resultSiteName; @@ -343,3 +347,38 @@ void WebPageData::ApplyChanges( } session->data().sendWebPageGamePollNotifications(); } + +QString WebPageData::displayedSiteName() const { + return (document && document->isWallPaper()) + ? tr::lng_media_chat_background(tr::now) + : (document && document->isTheme()) + ? tr::lng_media_color_theme(tr::now) + : siteName; +} + +bool WebPageData::computeDefaultSmallMedia() const { + if (!collage.items.empty()) { + return false; + } else if (siteName.isEmpty() + && title.isEmpty() + && description.empty() + && author.isEmpty()) { + return false; + } else if (!document + && photo + && type != WebPageType::Photo + && type != WebPageType::Document + && type != WebPageType::Story + && type != WebPageType::Video) { + if (type == WebPageType::Profile) { + return true; + } else if (siteName == u"Twitter"_q + || siteName == u"Facebook"_q + || type == WebPageType::ArticleWithIV) { + return false; + } else { + return true; + } + } + return false; +} diff --git a/Telegram/SourceFiles/data/data_web_page.h b/Telegram/SourceFiles/data/data_web_page.h index 44d8d7f2e56d47..4e8a38de5e3e84 100644 --- a/Telegram/SourceFiles/data/data_web_page.h +++ b/Telegram/SourceFiles/data/data_web_page.h @@ -7,6 +7,7 @@ For license and copyright information please follow this link: */ #pragma once +#include "base/flags.h" #include "data/data_photo.h" #include "data/data_document.h" @@ -16,7 +17,9 @@ namespace Data { class Session; } // namespace Data -enum class WebPageType { +enum class WebPageType : uint8 { + None, + Message, Group, @@ -44,8 +47,7 @@ enum class WebPageType { VoiceChat, Livestream, }; - -WebPageType ParseWebPageType(const MTPDwebPage &type); +[[nodiscard]] WebPageType ParseWebPageType(const MTPDwebPage &type); struct WebPageCollage { using Item = std::variant; @@ -78,6 +80,7 @@ struct WebPageData { WebPageCollage &&newCollage, int newDuration, const QString &newAuthor, + bool newHasLargeMedia, int newPendingTill); static void ApplyChanges( @@ -85,21 +88,26 @@ struct WebPageData { ChannelData *channel, const MTPmessages_Messages &result); - WebPageId id = 0; - WebPageType type = WebPageType::Article; + [[nodiscard]] QString displayedSiteName() const; + [[nodiscard]] bool computeDefaultSmallMedia() const; + + const WebPageId id = 0; + WebPageType type = WebPageType::None; QString url; QString displayUrl; QString siteName; QString title; TextWithEntities description; FullStoryId storyId; - int duration = 0; QString author; PhotoData *photo = nullptr; DocumentData *document = nullptr; WebPageCollage collage; - int pendingTill = 0; - int version = 0; + int duration = 0; + TimeId pendingTill = 0; + uint32 version : 30 = 0; + uint32 hasLargeMedia : 1 = 0; + uint32 failed : 1 = 0; private: void replaceDocumentGoodThumbnail(); diff --git a/Telegram/SourceFiles/data/stickers/data_stickers_set.cpp b/Telegram/SourceFiles/data/stickers/data_stickers_set.cpp index b0c35860c46b50..2801770eec83ba 100644 --- a/Telegram/SourceFiles/data/stickers/data_stickers_set.cpp +++ b/Telegram/SourceFiles/data/stickers/data_stickers_set.cpp @@ -53,7 +53,8 @@ StickersSetFlags ParseStickersSetFlags(const MTPDstickerSet &data) { | (data.is_masks() ? Flag::Masks : Flag()) | (data.is_emojis() ? Flag::Emoji : Flag()) | (data.vinstalled_date() ? Flag::Installed : Flag()) - | (data.is_videos() ? Flag::Webm : Flag()); + | (data.is_videos() ? Flag::Webm : Flag()) + | (data.is_text_color() ? Flag::TextColor : Flag()); } StickersSet::StickersSet( @@ -108,6 +109,10 @@ StickersType StickersSet::type() const { : StickersType::Stickers; } +bool StickersSet::textColor() const { + return flags & StickersSetFlag::TextColor; +} + void StickersSet::setThumbnail(const ImageWithLocation &data) { Data::UpdateCloudFile( _thumbnail, diff --git a/Telegram/SourceFiles/data/stickers/data_stickers_set.h b/Telegram/SourceFiles/data/stickers/data_stickers_set.h index a67713430e4230..b8769ae4cef5b4 100644 --- a/Telegram/SourceFiles/data/stickers/data_stickers_set.h +++ b/Telegram/SourceFiles/data/stickers/data_stickers_set.h @@ -57,6 +57,7 @@ enum class StickersSetFlag { Special = (1 << 7), Webm = (1 << 8), Emoji = (1 << 9), + TextColor = (1 << 10), }; inline constexpr bool is_flag_type(StickersSetFlag) { return true; }; using StickersSetFlags = base::flags; @@ -84,6 +85,7 @@ class StickersSet final { [[nodiscard]] MTPInputStickerSet mtpInput() const; [[nodiscard]] StickerSetIdentifier identifier() const; [[nodiscard]] StickersType type() const; + [[nodiscard]] bool textColor() const; void setThumbnail(const ImageWithLocation &data); diff --git a/Telegram/SourceFiles/dialogs/dialogs_key.h b/Telegram/SourceFiles/dialogs/dialogs_key.h index 3cadbc3cbb6435..1aac41def6ac72 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_key.h +++ b/Telegram/SourceFiles/dialogs/dialogs_key.h @@ -108,10 +108,9 @@ struct EntryState { Key key; Section section = Section::History; FilterId filterId = 0; - MsgId rootId = 0; - MsgId currentReplyToId = 0; + FullReplyTo currentReplyTo; - friend inline constexpr auto operator<=>(EntryState, EntryState) noexcept + friend inline auto operator<=>(EntryState, EntryState) noexcept = default; }; diff --git a/Telegram/SourceFiles/export/data/export_data_types.cpp b/Telegram/SourceFiles/export/data/export_data_types.cpp index e82d9db8a737cc..0d8e3ffa294a6f 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.cpp +++ b/Telegram/SourceFiles/export/data/export_data_types.cpp @@ -54,17 +54,16 @@ QString PrepareStoryFileName( } // namespace -int PeerColorIndex(BareId bareId) { - const auto index = bareId % 7; - const int map[] = { 0, 7, 4, 1, 6, 3, 5 }; - return map[index]; +uint8 PeerColorIndex(BareId bareId) { + const uint8 map[] = { 0, 7, 4, 1, 6, 3, 5 }; + return map[bareId % base::array_size(map)]; } BareId PeerToBareId(PeerId peerId) { return (peerId.value & PeerId::kChatTypeMask); } -int PeerColorIndex(PeerId peerId) { +uint8 PeerColorIndex(PeerId peerId) { return PeerColorIndex(PeerToBareId(peerId)); } @@ -78,7 +77,7 @@ BareId StringBarePeerId(const Utf8String &data) { return result; } -int ApplicationColorIndex(int applicationId) { +uint8 ApplicationColorIndex(int applicationId) { static const auto official = std::map { { 1, 0 }, // iOS { 7, 0 }, // iOS X @@ -576,6 +575,18 @@ Poll ParsePoll(const MTPDmessageMediaPoll &data) { return result; } +Giveaway ParseGiveaway(const MTPDmessageMediaGiveaway &data) { + auto result = Giveaway{ + .untilDate = data.vuntil_date().v, + .quantity = data.vquantity().v, + .months = data.vmonths().v, + }; + for (const auto &id : data.vchannels().v) { + result.channels.push_back(ChannelId(id)); + } + return result; +} + UserpicsSlice ParseUserpicsSlice( const MTPVector &data, int baseIndex) { @@ -755,6 +766,8 @@ ContactInfo ParseContactInfo(const MTPUser &data) { auto result = ContactInfo(); data.match([&](const MTPDuser &data) { result.userId = data.vid().v; + result.colorIndex = data.vcolor().value_or( + PeerColorIndex(result.userId)); if (const auto firstName = data.vfirst_name()) { result.firstName = ParseString(*firstName); } @@ -766,15 +779,13 @@ ContactInfo ParseContactInfo(const MTPUser &data) { } }, [&](const MTPDuserEmpty &data) { result.userId = data.vid().v; + result.colorIndex = PeerColorIndex(result.userId); }); return result; } -int ContactColorIndex(const ContactInfo &data) { - if (data.userId != 0) { - return PeerColorIndex(data.userId.bare); - } - return PeerColorIndex(StringBarePeerId(data.phoneNumber)); +uint8 ContactColorIndex(const ContactInfo &data) { + return data.colorIndex; } PeerId User::id() const { @@ -786,6 +797,8 @@ User ParseUser(const MTPUser &data) { result.info = ParseContactInfo(data); data.match([&](const MTPDuser &data) { result.bareId = data.vid().v; + result.colorIndex = data.vcolor().value_or( + PeerColorIndex(result.bareId)); if (const auto username = data.vusername()) { result.username = ParseString(*username); } @@ -840,6 +853,8 @@ Chat ParseChat(const MTPChat &data) { result.input = MTP_inputPeerChat(MTP_long(result.bareId)); }, [&](const MTPDchannel &data) { result.bareId = data.vid().v; + result.colorIndex = data.vcolor().value_or( + PeerColorIndex(result.bareId)); result.isBroadcast = data.is_broadcast(); result.isSupergroup = data.is_megagroup(); result.title = ParseString(data.vtitle()); @@ -923,6 +938,15 @@ MTPInputPeer Peer::input() const { Unexpected("Variant in Peer::id."); } +uint8 Peer::colorIndex() const { + if (const auto user = this->user()) { + return user->colorIndex; + } else if (const auto chat = this->chat()) { + return chat->colorIndex; + } + Unexpected("Variant in Peer::colorIndex."); +} + std::map ParsePeersLists( const MTPVector &users, const MTPVector &chats) { @@ -1057,7 +1081,9 @@ Media ParseMedia( }, [](const MTPDmessageMediaDice &data) { // #TODO dice }, [](const MTPDmessageMediaStory &data) { - // #TODO stories export + // #TODO export stories + }, [&](const MTPDmessageMediaGiveaway &data) { + result.content = ParseGiveaway(data); }, [](const MTPDmessageMediaEmpty &data) {}); return result; } @@ -1298,6 +1324,19 @@ ServiceAction ParseServiceAction( content.peerId = ParsePeerId(data.vpeer()); content.buttonId = data.vbutton_id().v; result.content = content; + }, [&](const MTPDmessageActionGiftCode &data) { + auto content = ActionGiftCode(); + content.boostPeerId = data.vboost_peer() + ? peerFromMTP(*data.vboost_peer()) + : PeerId(); + content.viaGiveaway = data.is_via_giveaway(); + content.unclaimed = data.is_unclaimed(); + content.months = data.vmonths().v; + content.code = data.vslug().v; + result.content = content; + }, [&](const MTPDmessageActionGiveawayLaunch &) { + auto content = ActionGiveawayLaunch(); + result.content = content; }, [](const MTPDmessageActionEmpty &data) {}); return result; } @@ -1358,15 +1397,19 @@ Message ParseMessage( } if (const auto replyTo = data.vreply_to()) { replyTo->match([&](const MTPDmessageReplyHeader &data) { - result.replyToMsgId = data.vreply_to_msg_id().v; - result.replyToPeerId = data.vreply_to_peer_id() - ? ParsePeerId(*data.vreply_to_peer_id()) - : 0; - if (result.replyToPeerId == result.peerId) { - result.replyToPeerId = 0; + if (const auto replyToMsg = data.vreply_to_msg_id()) { + result.replyToMsgId = replyToMsg->v; + result.replyToPeerId = data.vreply_to_peer_id() + ? ParsePeerId(*data.vreply_to_peer_id()) + : 0; + if (result.replyToPeerId == result.peerId) { + result.replyToPeerId = 0; + } + } else { + // #TODO export replies } }, [&](const MTPDmessageReplyStoryHeader &data) { - // #TODO stories export + // #TODO export stories }); } } @@ -1410,12 +1453,16 @@ Message ParseMessage( } if (const auto replyTo = data.vreply_to()) { replyTo->match([&](const MTPDmessageReplyHeader &data) { - result.replyToMsgId = data.vreply_to_msg_id().v; - result.replyToPeerId = data.vreply_to_peer_id() - ? ParsePeerId(*data.vreply_to_peer_id()) - : PeerId(0); + if (const auto replyToMsg = data.vreply_to_msg_id()) { + result.replyToMsgId = replyToMsg->v; + result.replyToPeerId = data.vreply_to_peer_id() + ? ParsePeerId(*data.vreply_to_peer_id()) + : PeerId(0); + } else { + // #TODO export replies + } }, [&](const MTPDmessageReplyStoryHeader &data) { - // #TODO stories export + // #TODO export stories }); } if (const auto viaBotId = data.vvia_bot_id()) { @@ -1507,6 +1554,8 @@ ContactsList ParseContactsList(const MTPVector &data) { info.lastName = ParseString(data.vlast_name()); info.phoneNumber = ParseString(data.vphone()); info.date = data.vdate().v; + info.colorIndex = PeerColorIndex( + StringBarePeerId(info.phoneNumber)); return info; }); result.list.push_back(std::move(info)); @@ -1724,6 +1773,7 @@ DialogsInfo ParseDialogsInfo(const MTPmessages_Dialogs &data) { info.lastName = peer.user() ? peer.user()->info.lastName : Utf8String(); + info.colorIndex = peer.colorIndex(); info.input = peer.input(); info.migratedToChannelId = peer.chat() ? peer.chat()->migratedToChannelId diff --git a/Telegram/SourceFiles/export/data/export_data_types.h b/Telegram/SourceFiles/export/data/export_data_types.h index 9a00392d0d8fc7..4d8bd620f60136 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.h +++ b/Telegram/SourceFiles/export/data/export_data_types.h @@ -24,10 +24,10 @@ namespace Data { using Utf8String = QByteArray; -int PeerColorIndex(BareId bareId); +uint8 PeerColorIndex(BareId bareId); BareId PeerToBareId(PeerId peerId); -int PeerColorIndex(PeerId peerId); -int ApplicationColorIndex(int applicationId); +uint8 PeerColorIndex(PeerId peerId); +uint8 ApplicationColorIndex(int applicationId); int DomainApplicationId(const Utf8String &data); Utf8String ParseString(const MTPstring &data); @@ -108,12 +108,13 @@ struct ContactInfo { Utf8String lastName; Utf8String phoneNumber; TimeId date = 0; + uint8 colorIndex = 0; Utf8String name() const; }; ContactInfo ParseContactInfo(const MTPUser &data); -int ContactColorIndex(const ContactInfo &data); +uint8 ContactColorIndex(const ContactInfo &data); struct Photo { uint64 id = 0; @@ -196,6 +197,13 @@ struct Poll { bool closed = false; }; +struct Giveaway { + std::vector channels; + TimeId untilDate = 0; + int quantity = 0; + int months = 0; +}; + struct UserpicsSlice { std::vector list; }; @@ -210,6 +218,7 @@ struct User { BareId bareId = 0; ContactInfo info; Utf8String username; + uint8 colorIndex = 0; bool isBot = false; bool isSelf = false; bool isReplies = false; @@ -229,6 +238,7 @@ struct Chat { ChannelId migratedToChannelId = 0; Utf8String title; Utf8String username; + uint8 colorIndex = 0; bool isBroadcast = false; bool isSupergroup = false; @@ -242,6 +252,7 @@ struct Peer { PeerId id() const; Utf8String name() const; MTPInputPeer input() const; + uint8 colorIndex() const; const User *user() const; const Chat *chat() const; @@ -325,6 +336,7 @@ struct Media { Game, Invoice, Poll, + Giveaway, UnsupportedMedia> content; TimeId ttl = 0; @@ -527,11 +539,22 @@ struct ActionSetChatWallPaper { struct ActionSetSameChatWallPaper { }; +struct ActionGiftCode { + QByteArray code; + PeerId boostPeerId = 0; + int months = 0; + bool viaGiveaway = false; + bool unclaimed = false; +}; + struct ActionRequestedPeer { PeerId peerId = 0; int buttonId = 0; }; +struct ActionGiveawayLaunch { +}; + struct ServiceAction { std::variant< v::null_t, @@ -570,7 +593,9 @@ struct ServiceAction { ActionSuggestProfilePhoto, ActionRequestedPeer, ActionSetChatWallPaper, - ActionSetSameChatWallPaper> content; + ActionSetSameChatWallPaper, + ActionGiftCode, + ActionGiveawayLaunch> content; }; ServiceAction ParseServiceAction( @@ -726,6 +751,7 @@ struct DialogInfo { int32 topMessageId = 0; TimeId topMessageDate = 0; PeerId peerId = 0; + uint8 colorIndex = 0; MTPInputPeer migratedFromInput = MTP_inputPeerEmpty(); ChannelId migratedToChannelId = 0; diff --git a/Telegram/SourceFiles/export/output/export_output_html.cpp b/Telegram/SourceFiles/export/output/export_output_html.cpp index bd3b175d15a41e..2c91d6c8404443 100644 --- a/Telegram/SourceFiles/export/output/export_output_html.cpp +++ b/Telegram/SourceFiles/export/output/export_output_html.cpp @@ -25,7 +25,7 @@ constexpr auto kPersonalUserpicSize = 90; constexpr auto kEntryUserpicSize = 48; constexpr auto kServiceMessagePhotoSize = 60; constexpr auto kHistoryUserpicSize = 42; -constexpr auto kSavedMessagesColorIndex = 3; +constexpr auto kSavedMessagesColorIndex = uint8(3); constexpr auto kJoinWithinSeconds = 900; constexpr auto kPhotoMaxWidth = 520; constexpr auto kPhotoMaxHeight = 520; @@ -351,7 +351,7 @@ QByteArray FormatTimeText(TimeId date) { namespace details { struct UserpicData { - int colorIndex = 0; + uint8 colorIndex = 0; int pixelSize = 0; QString imageLink; QString largeLink; @@ -611,6 +611,9 @@ class HtmlWriter::Wrap { const Data::Photo &data, const QString &basePath); [[nodiscard]] QByteArray pushPoll(const Data::Poll &data); + [[nodiscard]] QByteArray pushGiveaway( + const PeersMap &peers, + const Data::Giveaway &data); File _file; QByteArray _composedStart; @@ -988,7 +991,7 @@ QByteArray HtmlWriter::Wrap::pushServiceMessage( result.append(popTag()); if (photo) { auto userpic = UserpicData(); - userpic.colorIndex = Data::PeerColorIndex(dialog.peerId); + userpic.colorIndex = dialog.colorIndex; userpic.firstName = dialog.name; userpic.lastName = dialog.lastName; userpic.pixelSize = kServiceMessagePhotoSize; @@ -1276,6 +1279,24 @@ auto HtmlWriter::Wrap::pushMessage( + " set " + wrapReplyToLink("the same background") + " for this chat"; + }, [&](const ActionGiftCode &data) { + return data.unclaimed + ? ("This is an unclaimed Telegram Premium for " + + NumberToString(data.months) + + (data.months > 1 ? " months" : "month") + + " prize in a giveaway organized by a channel.") + : data.viaGiveaway + ? ("You won a Telegram Premium for " + + NumberToString(data.months) + + (data.months > 1 ? " months" : "month") + + " prize in a giveaway organized by a channel.") + : ("You've received a Telegram Premium for " + + NumberToString(data.months) + + (data.months > 1 ? " months" : "month") + + " gift from a channel."); + }, [&](const ActionGiveawayLaunch &data) { + return serviceFrom + " just started a giveaway " + "of Telegram Premium subscriptions to its followers."; }, [](v::null_t) { return QByteArray(); }); if (!serviceText.isEmpty()) { @@ -1459,8 +1480,9 @@ QByteArray HtmlWriter::Wrap::pushMedia( if (!data.classes.isEmpty()) { return pushGenericMedia(data); } + using namespace Data; const auto &content = message.media.content; - if (const auto document = std::get_if(&content)) { + if (const auto document = std::get_if(&content)) { Assert(!message.media.ttl); if (document->isSticker) { return pushStickerMedia(*document, basePath); @@ -1470,11 +1492,13 @@ QByteArray HtmlWriter::Wrap::pushMedia( return pushVideoFileMedia(*document, basePath); } Unexpected("Non generic document in HtmlWriter::Wrap::pushMedia."); - } else if (const auto photo = std::get_if(&content)) { + } else if (const auto photo = std::get_if(&content)) { Assert(!message.media.ttl); return pushPhotoMedia(*photo, basePath); - } else if (const auto poll = std::get_if(&content)) { + } else if (const auto poll = std::get_if(&content)) { return pushPoll(*poll); + } else if (const auto giveaway = std::get_if(&content)) { + return pushGiveaway(peers, *giveaway); } Assert(v::is_null(content)); return QByteArray(); @@ -1796,6 +1820,52 @@ QByteArray HtmlWriter::Wrap::pushPoll(const Data::Poll &data) { return result; } +QByteArray HtmlWriter::Wrap::pushGiveaway( + const PeersMap &peers, + const Data::Giveaway &data) { + auto result = pushDiv("media_wrap clearfix"); + result.append(pushDiv("media_giveaway")); + + result.append(pushDiv("section_title bold")); + result.append(SerializeString("Giveaway Prizes")); + result.append(popTag()); + result.append(pushDiv("section_body")); + result.append("" + + Data::NumberToString(data.quantity) + + " " + + SerializeString((data.quantity > 1) + ? "Telegram Premium Subscriptions" + : "Telegram Premium Subscription") + + " for " + Data::NumberToString(data.months) + " " + + (data.months > 1 ? "months." : "month.")); + result.append(popTag()); + + result.append(pushDiv("section_title bold")); + result.append(SerializeString("Participants")); + result.append(popTag()); + result.append(pushDiv("section_body")); + auto channels = QByteArrayList(); + for (const auto &channel : data.channels) { + channels.append("" + peers.wrapPeerName(channel) + ""); + } + result.append(SerializeString((channels.size() > 1) + ? "All subscribers of those channels: " + : "All subscribers of the channel: ") + + channels.join(", ")); + result.append(popTag()); + + result.append(pushDiv("section_title bold")); + result.append(SerializeString("Winners Selection Date")); + result.append(popTag()); + result.append(pushDiv("section_body")); + result.append(Data::FormatDateTime(data.untilDate)); + result.append(popTag()); + + result.append(popTag()); + result.append(popTag()); + return result; +} + MediaData HtmlWriter::Wrap::prepareMediaData( const Data::Message &message, const QString &basePath, @@ -1954,6 +2024,7 @@ MediaData HtmlWriter::Wrap::prepareMediaData( result.description = data.description; result.status = Data::FormatMoneyAmount(data.amount, data.currency); }, [](const Poll &data) { + }, [](const Giveaway &data) { }, [](const UnsupportedMedia &data) { Unexpected("Unsupported message."); }, [](v::null_t) {}); @@ -2104,7 +2175,7 @@ Result HtmlWriter::start( Result HtmlWriter::writePersonal(const Data::PersonalInfo &data) { Expects(_summary != nullptr); - _selfColorIndex = Data::PeerColorIndex(data.user.info.userId); + _selfColorIndex = data.user.info.colorIndex; if (_settings.types & Settings::Type::Userpics) { _delayedPersonalInfo = std::make_unique(data); return Result::Success(); diff --git a/Telegram/SourceFiles/export/output/export_output_html.h b/Telegram/SourceFiles/export/output/export_output_html.h index 4044fca8a8bae1..f2c32787b42526 100644 --- a/Telegram/SourceFiles/export/output/export_output_html.h +++ b/Telegram/SourceFiles/export/output/export_output_html.h @@ -150,7 +150,7 @@ class HtmlWriter : public AbstractWriter { bool _summaryNeedDivider = false; bool _haveSections = false; - int _selfColorIndex = 0; + uint8 _selfColorIndex = 0; std::unique_ptr _delayedPersonalInfo; int _userpicsCount = 0; diff --git a/Telegram/SourceFiles/export/output/export_output_json.cpp b/Telegram/SourceFiles/export/output/export_output_json.cpp index 7de9cde17fc4d4..18caaffe868063 100644 --- a/Telegram/SourceFiles/export/output/export_output_json.cpp +++ b/Telegram/SourceFiles/export/output/export_output_json.cpp @@ -287,11 +287,12 @@ QByteArray SerializeMessage( } const auto push = [&](const QByteArray &key, const auto &value) { - if constexpr (std::is_arithmetic_v>) { + using V = std::decay_t; + if constexpr (std::is_same_v) { + pushBare(key, value ? "true" : "false"); + } else if constexpr (std::is_arithmetic_v) { pushBare(key, Data::NumberToString(value)); - } else if constexpr (std::is_same_v< - std::decay_t, - PeerId>) { + } else if constexpr (std::is_same_v) { if (const auto chat = peerToChat(value)) { pushBare( key, @@ -592,6 +593,17 @@ QByteArray SerializeMessage( pushAction("requested_peer"); push("button_id", data.buttonId); push("peer_id", data.peerId.value); + }, [&](const ActionGiftCode &data) { + pushAction("gift_code_prize"); + push("gift_code", data.code); + if (data.boostPeerId) { + push("boost_peer_id", data.boostPeerId); + } + push("months", data.months); + push("unclaimed", data.unclaimed); + push("via_giveaway", data.viaGiveaway); + }, [&](const ActionGiveawayLaunch &data) { + pushAction("giveaway_launch"); }, [&](const ActionSetChatWallPaper &data) { pushActor(); pushAction("set_chat_wallpaper"); @@ -738,6 +750,22 @@ QByteArray SerializeMessage( { "total_voters", NumberToString(data.totalVotes) }, { "answers", serialized } })); + }, [&](const Giveaway &data) { + context.nesting.push_back(Context::kObject); + const auto channels = ranges::views::all( + data.channels + ) | ranges::views::transform([&](ChannelId id) { + return NumberToString(id.bare); + }) | ranges::to_vector; + const auto serialized = SerializeArray(context, channels); + context.nesting.pop_back(); + + push("giveaway_information", SerializeObject(context, { + { "quantity", NumberToString(data.quantity) }, + { "months", NumberToString(data.months) }, + { "until_date", SerializeDate(data.untilDate) }, + { "channels", serialized }, + })); }, [](const UnsupportedMedia &data) { Unexpected("Unsupported message."); }, [](v::null_t) {}); diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp index b362da656e2276..9d8825fee5ad80 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp @@ -659,7 +659,7 @@ not_null InnerWidget::elementPathShiftGradient() { return _pathGradient.get(); } -void InnerWidget::elementReplyTo(const FullMsgId &to) { +void InnerWidget::elementReplyTo(const FullReplyTo &to) { } void InnerWidget::elementStartInteraction(not_null view) { diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h index a9b119f00ec4e0..29db0b42704a36 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h @@ -127,7 +127,7 @@ class InnerWidget final void elementHandleViaClick(not_null bot) override; bool elementIsChatWide() override; not_null elementPathShiftGradient() override; - void elementReplyTo(const FullMsgId &to) override; + void elementReplyTo(const FullReplyTo &to) override; void elementStartInteraction( not_null view) override; void elementStartPremium( diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp index 96ab128bf35296..81a94cc35097db 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp @@ -767,6 +767,8 @@ void GenerateItems( using LogDeleteTopic = MTPDchannelAdminLogEventActionDeleteTopic; using LogPinTopic = MTPDchannelAdminLogEventActionPinTopic; using LogToggleAntiSpam = MTPDchannelAdminLogEventActionToggleAntiSpam; + using LogChangeColor = MTPDchannelAdminLogEventActionChangeColor; + using LogChangeBackgroundEmoji = MTPDchannelAdminLogEventActionChangeBackgroundEmoji; const auto session = &history->session(); const auto id = event.vid().v; @@ -1815,6 +1817,54 @@ void GenerateItems( addSimpleServiceMessage(text); }; + const auto createChangeColor = [&](const LogChangeColor &data) { + const auto text = tr::lng_admin_log_change_color( + tr::now, + lt_from, + fromLinkText, + lt_previous, + { '#' + QString::number(data.vprev_value().v + 1) }, + lt_color, + { '#' + QString::number(data.vnew_value().v + 1) }, + Ui::Text::WithEntities); + addSimpleServiceMessage(text); + }; + + const auto createChangeBackgroundEmoji = [&](const LogChangeBackgroundEmoji &data) { + const auto was = data.vprev_value().v; + const auto now = data.vnew_value().v; + const auto text = !was + ? tr::lng_admin_log_set_background_emoji( + tr::now, + lt_from, + fromLinkText, + lt_emoji, + Ui::Text::SingleCustomEmoji( + Data::SerializeCustomEmojiId(now)), + Ui::Text::WithEntities) + : !now + ? tr::lng_admin_log_removed_background_emoji( + tr::now, + lt_from, + fromLinkText, + lt_emoji, + Ui::Text::SingleCustomEmoji( + Data::SerializeCustomEmojiId(was)), + Ui::Text::WithEntities) + : tr::lng_admin_log_change_background_emoji( + tr::now, + lt_from, + fromLinkText, + lt_previous, + Ui::Text::SingleCustomEmoji( + Data::SerializeCustomEmojiId(was)), + lt_emoji, + Ui::Text::SingleCustomEmoji( + Data::SerializeCustomEmojiId(now)), + Ui::Text::WithEntities); + addSimpleServiceMessage(text); + }; + action.match( createChangeTitle, createChangeAbout, @@ -1858,7 +1908,9 @@ void GenerateItems( createEditTopic, createDeleteTopic, createPinTopic, - createToggleAntiSpam); + createToggleAntiSpam, + createChangeColor, + createChangeBackgroundEmoji); } } // namespace AdminLog diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index 5880bfd495874b..da6c32f60af829 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -177,8 +177,7 @@ void History::takeLocalDraft(not_null from) { && !_drafts.contains(Data::DraftKey::Local(topicRootId))) { // Edit and reply to drafts can't migrate. // Cloud drafts do not migrate automatically. - draft->msgId = 0; - + draft->reply = FullReplyTo(); setLocalDraft(std::move(draft)); } from->clearLocalDraft(topicRootId); @@ -194,6 +193,7 @@ void History::createLocalDraftFromCloud(MsgId topicRootId) { return; } + draft->reply.topicRootId = topicRootId; auto existing = localDraft(topicRootId); if (Data::DraftIsNull(existing) || !existing->date @@ -201,17 +201,15 @@ void History::createLocalDraftFromCloud(MsgId topicRootId) { if (!existing) { setLocalDraft(std::make_unique( draft->textWithTags, - draft->msgId, - topicRootId, + draft->reply, draft->cursor, - draft->previewState)); + draft->webpage)); existing = localDraft(topicRootId); } else if (existing != draft) { existing->textWithTags = draft->textWithTags; - existing->msgId = draft->msgId; - existing->topicRootId = draft->topicRootId; + existing->reply = draft->reply; existing->cursor = draft->cursor; - existing->previewState = draft->previewState; + existing->webpage = draft->webpage; } existing->date = draft->date; } @@ -277,28 +275,29 @@ Data::Draft *History::createCloudDraft( if (Data::DraftIsNull(fromDraft)) { setCloudDraft(std::make_unique( TextWithTags(), - 0, - topicRootId, + FullReplyTo{ .topicRootId = topicRootId }, MessageCursor(), - Data::PreviewState::Allowed)); + Data::WebPageDraft())); cloudDraft(topicRootId)->date = TimeId(0); } else { auto existing = cloudDraft(topicRootId); if (!existing) { + auto reply = fromDraft->reply; + reply.topicRootId = topicRootId; setCloudDraft(std::make_unique( fromDraft->textWithTags, - fromDraft->msgId, - topicRootId, + reply, fromDraft->cursor, - fromDraft->previewState)); + fromDraft->webpage)); existing = cloudDraft(topicRootId); } else if (existing != fromDraft) { existing->textWithTags = fromDraft->textWithTags; - existing->msgId = fromDraft->msgId; + existing->reply = fromDraft->reply; existing->cursor = fromDraft->cursor; - existing->previewState = fromDraft->previewState; + existing->webpage = fromDraft->webpage; } existing->date = base::unixtime::now(); + existing->reply.topicRootId = topicRootId; } if (const auto thread = threadFor(topicRootId)) { @@ -1127,8 +1126,8 @@ void History::applyServiceChanges( }, [&](const MTPDmessageActionPinMessage &data) { if (replyTo) { replyTo->match([&](const MTPDmessageReplyHeader &data) { - const auto id = data.vreply_to_msg_id().v; - if (item) { + const auto id = data.vreply_to_msg_id().value_or_empty(); + if (id && item) { session().storage().add(Storage::SharedMediaAddSlice( peer->id, MsgId(0), diff --git a/Telegram/SourceFiles/history/history.h b/Telegram/SourceFiles/history/history.h index a08615db1f0e8b..7a126cce1f280a 100644 --- a/Telegram/SourceFiles/history/history.h +++ b/Telegram/SourceFiles/history/history.h @@ -329,17 +329,17 @@ class History final : public Data::Thread { } void setLocalDraft(std::unique_ptr &&draft) { setDraft( - Data::DraftKey::Local(draft->topicRootId), + Data::DraftKey::Local(draft->reply.topicRootId), std::move(draft)); } void setLocalEditDraft(std::unique_ptr &&draft) { setDraft( - Data::DraftKey::LocalEdit(draft->topicRootId), + Data::DraftKey::LocalEdit(draft->reply.topicRootId), std::move(draft)); } void setCloudDraft(std::unique_ptr &&draft) { setDraft( - Data::DraftKey::Cloud(draft->topicRootId), + Data::DraftKey::Cloud(draft->reply.topicRootId), std::move(draft)); } void clearLocalDraft(MsgId topicRootId) { diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 4bc0724add37e2..337f8eeacc07f6 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -13,6 +13,8 @@ For license and copyright information please follow this link: #include "history/admin_log/history_admin_log_item.h" #include "history/history_item.h" #include "history/history_item_helpers.h" +#include "history/view/controls/history_view_forward_panel.h" +#include "history/view/controls/history_view_draft_options.h" #include "history/view/media/history_view_media.h" #include "history/view/media/history_view_sticker.h" #include "history/view/media/history_view_web_page.h" @@ -96,6 +98,10 @@ For license and copyright information please follow this link: #include "styles/style_chat.h" #include "styles/style_menu_icons.h" +#include "payments/payments_checkout_process.h" +#include "payments/payments_form.h" +#include "base/random.h" + #include #include #include @@ -283,7 +289,7 @@ class HistoryMainElementDelegate final return _widget->elementPathShiftGradient(); } - void elementReplyTo(const FullMsgId &to) override { + void elementReplyTo(const FullReplyTo &to) override { if (_widget) { _widget->elementReplyTo(to); } @@ -449,6 +455,43 @@ HistoryInner::HistoryInner( _migrated->translateTo(_history->translatedTo()); } +#if 0 + if (const auto channel = _history->peer->asBroadcast()) { + if (channel->amCreator()) { + const auto weak = base::make_weak(_controller); + channel->session().api().request(MTPpayments_GetPremiumGiftCodeOptions( + MTP_flags(MTPpayments_GetPremiumGiftCodeOptions::Flag::f_boost_peer), + channel->input + )).done(crl::guard(weak, [=](const MTPVector &result) { + if (result.v.isEmpty()) { + return; + } + const auto &data = result.v.front().data(); + const auto randomId = base::RandomValue(); + Payments::CheckoutProcess::Start( + Payments::InvoicePremiumGiftCode{ + .purpose = Payments::InvoicePremiumGiftCodeGiveaway{ + .boostPeer = channel, + //.additionalChannels = , + .untilDate = (base::unixtime::now() + 300), + .onlyNewSubscribers = true, + }, + .randomId = randomId, + .currency = qs(data.vcurrency()), + .amount = data.vamount().v, + .storeProduct = qs(data.vstore_product().value_or_empty()), + .storeQuantity = data.vstore_quantity().value_or_empty(), + .users = data.vusers().v, + .months = data.vmonths().v, + }, + crl::guard(weak, [=](auto) { weak->window().activate(); })); + })).fail(crl::guard(weak, [=](const MTP::Error &error) { + weak.get()->showToast(error.type()); + })).send(); + } + } +#endif + Window::ChatThemeValueFromPeer( controller, _peer @@ -502,6 +545,10 @@ HistoryInner::HistoryInner( PremiumPreview::InfiniteReactions); }, lifetime()); + session().data().peerDecorationsUpdated( + ) | rpl::start_with_next([=] { + update(); + }, lifetime()); session().data().itemRemoved( ) | rpl::start_with_next( [this](auto item) { itemRemoved(item); }, @@ -2059,6 +2106,20 @@ void HistoryInner::toggleFavoriteReaction(not_null view) const { item->toggleReaction(favorite, HistoryItem::ReactionSource::Quick); } +TextWithEntities HistoryInner::selectedQuote( + not_null item) const { + if (_selected.size() != 1 + || _selected.begin()->first != item + || _selected.begin()->second == FullSelection) { + return {}; + } + const auto view = item->mainView(); + if (!view) { + return {}; + } + return view->selectedQuote(_selected.begin()->second); +} + void HistoryInner::contextMenuEvent(QContextMenuEvent *e) { showContextMenu(e); } @@ -2155,19 +2216,6 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { return; } const auto itemId = item->fullId(); - const auto canReply = [&] { - const auto peer = item->history()->peer; - const auto topic = item->topic(); - return topic - ? Data::CanSendAnything(topic) - : (Data::CanSendAnything(peer) - && (!peer->isChannel() || peer->asChannel()->amIn())); - }(); - if (canReply) { - _menu->addAction(tr::lng_context_reply_msg(tr::now), [=] { - _widget->replyToMessage(itemId); - }, &st::menuIconReply); - } const auto repliesCount = item->repliesCount(); const auto withReplies = (repliesCount > 0); const auto topicRootId = item->history()->isForum() @@ -2331,6 +2379,46 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { } }; + const auto addReplyAction = [&](HistoryItem *item) { + if (!item) { + return; + } + const auto canSendReply = [&] { + const auto peer = item->history()->peer; + const auto topic = item->topic(); + return topic + ? Data::CanSendAnything(topic) + : (Data::CanSendAnything(peer) + && (!peer->isChannel() || peer->asChannel()->amIn())); + }(); + const auto canReply = canSendReply || [&] { + const auto peer = item->history()->peer; + if (const auto chat = peer->asChat()) { + return !chat->isForbidden(); + } else if (const auto channel = peer->asChannel()) { + return !channel->isForbidden(); + } + return true; + }(); + if (canReply) { + const auto itemId = item->fullId(); + const auto quote = selectedQuote(item); + auto text = quote.empty() + ? tr::lng_context_reply_msg(tr::now) + : tr::lng_context_quote_and_reply(tr::now); + text.replace('&', u"&&"_q); + _menu->addAction(text, [=] { + if (canSendReply) { + _widget->replyToMessage({ itemId, quote }); + } else { + HistoryView::Controls::ShowReplyToChatBox( + controller->uiShow(), + { itemId, quote }); + } + }, &st::menuIconReply); + } + }; + const auto lnkPhoto = link ? reinterpret_cast( link->property(kPhotoLinkMediaProperty).toULongLong()) @@ -2342,6 +2430,8 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { if (lnkPhoto || lnkDocument) { const auto item = _dragStateItem; const auto itemId = item ? item->fullId() : FullMsgId(); + addReplyAction(item); + if (isUponSelected > 0) { const auto selectedText = getSelectedText(); if (!hasCopyRestrictionForSelected() @@ -2452,6 +2542,7 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { : QString(); if (isUponSelected > 0) { + addReplyAction(item); const auto selectedText = getSelectedText(); if (!hasCopyRestrictionForSelected() && !selectedText.empty()) { _menu->addAction( @@ -2474,6 +2565,7 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { } addItemActions(item, item); } else { + addReplyAction(item); addItemActions(item, albumPartItem); if (item && !isUponSelected) { const auto media = (view ? view->media() : nullptr); @@ -3440,7 +3532,7 @@ not_null HistoryInner::elementPathShiftGradient() { return _pathGradient.get(); } -void HistoryInner::elementReplyTo(const FullMsgId &to) { +void HistoryInner::elementReplyTo(const FullReplyTo &to) { return _widget->replyToMessage(to); } diff --git a/Telegram/SourceFiles/history/history_inner_widget.h b/Telegram/SourceFiles/history/history_inner_widget.h index 3ec2127e2be470..d47f27968bd05e 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.h +++ b/Telegram/SourceFiles/history/history_inner_widget.h @@ -159,7 +159,7 @@ class HistoryInner void elementHandleViaClick(not_null bot); bool elementIsChatWide(); not_null elementPathShiftGradient(); - void elementReplyTo(const FullMsgId &to); + void elementReplyTo(const FullReplyTo &to); void elementStartInteraction(not_null view); void elementStartPremium( not_null view, @@ -314,6 +314,8 @@ class HistoryInner QPoint mapPointToItem(QPoint p, const Element *view) const; QPoint mapPointToItem(QPoint p, const HistoryItem *item) const; + [[nodiscard]] TextWithEntities selectedQuote( + not_null item) const; void showContextMenu(QContextMenuEvent *e, bool showFromTouch = false); void cancelContextDownload(not_null document); diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index a77af48b6ae772..91ac32518fa3b8 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -125,23 +125,23 @@ void HistoryItem::HistoryItem::Destroyer::operator()(HistoryItem *value) { } struct HistoryItem::CreateConfig { - PeerId replyToPeer = 0; - MsgId replyTo = 0; - MsgId replyToTop = 0; - StoryId replyToStory = 0; - bool replyIsTopicPost = false; + ReplyFields reply; + UserId viaBotId = 0; int viewsCount = -1; int forwardsCount = -1; - QString author; - PeerId senderOriginal = 0; - QString senderNameOriginal; - QString forwardPsaType; + QString postAuthor; + MsgId originalId = 0; + TimeId originalDate = 0; + PeerId originalSenderId = 0; + QString originalSenderName; + QString originalPostAuthor; + + QString forwardPsaType; PeerId savedFromPeer = 0; MsgId savedFromMsgId = 0; - QString authorOriginal; - TimeId originalDate = 0; + TimeId editDate = 0; HistoryMessageMarkupData markup; HistoryMessageRepliesData replies; @@ -154,14 +154,14 @@ struct HistoryItem::CreateConfig { void HistoryItem::FillForwardedInfo( CreateConfig &config, const MTPDmessageFwdHeader &data) { + config.originalId = data.vchannel_post().value_or_empty(); + config.originalDate = data.vdate().v; if (const auto fromId = data.vfrom_id()) { - config.senderOriginal = peerFromMTP(*fromId); + config.originalSenderId = peerFromMTP(*fromId); } - config.originalDate = data.vdate().v; - config.senderNameOriginal = qs(data.vfrom_name().value_or_empty()); + config.originalSenderName = qs(data.vfrom_name().value_or_empty()); + config.originalPostAuthor = qs(data.vpost_author().value_or_empty()); config.forwardPsaType = qs(data.vpsa_type().value_or_empty()); - config.originalId = data.vchannel_post().value_or_empty(); - config.authorOriginal = qs(data.vpost_author().value_or_empty()); const auto savedFromPeer = data.vsaved_from_peer(); const auto savedFromMsgId = data.vsaved_from_msg_id(); if (savedFromPeer && savedFromMsgId) { @@ -252,16 +252,28 @@ std::unique_ptr HistoryItem::CreateMedia( return nullptr; }); }, [&](const MTPDmessageMediaWebPage &media) { + using Flag = MediaWebPageFlag; + const auto flags = Flag() + | (media.is_force_large_media() + ? Flag::ForceLargeMedia + : Flag()) + | (media.is_force_small_media() + ? Flag::ForceSmallMedia + : Flag()) + | (media.is_manual() ? Flag::Manual : Flag()) + | (media.is_safe() ? Flag::Safe : Flag()); return media.vwebpage().match([](const MTPDwebPageEmpty &) -> Result { return nullptr; }, [&](const MTPDwebPagePending &webpage) -> Result { return std::make_unique( item, - item->history()->owner().processWebpage(webpage)); + item->history()->owner().processWebpage(webpage), + flags); }, [&](const MTPDwebPage &webpage) -> Result { return std::make_unique( item, - item->history()->owner().processWebpage(webpage)); + item->history()->owner().processWebpage(webpage), + flags); }, [](const MTPDwebPageNotModified &) -> Result { LOG(("API Error: " "webPageNotModified is unexpected in message media.")); @@ -291,6 +303,10 @@ std::unique_ptr HistoryItem::CreateMedia( peerFromMTP(media.vpeer()), media.vid().v, }, media.is_via_mention()); + }, [&](const MTPDmessageMediaGiveaway &media) -> Result { + return std::make_unique( + item, + Data::ComputeGiveawayData(item, media)); }, [](const MTPDmessageMediaEmpty &) -> Result { return nullptr; }, [](const MTPDmessageMediaUnsupported &) -> Result { @@ -431,21 +447,21 @@ HistoryItem::HistoryItem( const auto originalMedia = original->media(); const auto dropForwardInfo = original->computeDropForwardedInfo(); - config.replyTo = config.replyToTop = topicRootId; - config.replyIsTopicPost = (topicRootId != 0); + config.reply.messageId = config.reply.topMessageId = topicRootId; + config.reply.topicPost = (topicRootId != 0); if (!dropForwardInfo) { - config.originalDate = original->dateOriginal(); + config.originalDate = original->originalDate(); if (const auto info = original->hiddenSenderInfo()) { - config.senderNameOriginal = info->name; - } else if (const auto senderOriginal = original->senderOriginal()) { - config.senderOriginal = senderOriginal->id; - if (senderOriginal->isChannel()) { - config.originalId = original->idOriginal(); + config.originalSenderName = info->name; + } else if (const auto originalSender = original->originalSender()) { + config.originalSenderId = originalSender->id; + if (originalSender->isChannel()) { + config.originalId = original->originalId(); } } else { Unexpected("Corrupt forwarded information in message."); } - config.authorOriginal = original->authorOriginal(); + config.originalPostAuthor = original->originalPostAuthor(); } if (peer->isSelf()) { // @@ -461,12 +477,12 @@ HistoryItem::HistoryItem( //} } if (flags & MessageFlag::HasPostAuthor) { - config.author = postAuthor; + config.postAuthor = postAuthor; } if (const auto fwdViaBot = original->viaBot()) { config.viaBotId = peerToUser(fwdViaBot->id); } else if (originalMedia && originalMedia->game()) { - if (const auto sender = original->senderOriginal()) { + if (const auto sender = original->originalSender()) { if (const auto user = sender->asUser()) { if (user->isBot()) { config.viaBotId = peerToUser(user->id); @@ -478,8 +494,8 @@ HistoryItem::HistoryItem( if (fwdViewsCount > 0) { config.viewsCount = fwdViewsCount; } else if ((isPost() && !isScheduled()) - || (original->senderOriginal() - && original->senderOriginal()->isChannel())) { + || (original->originalSender() + && original->originalSender()->isChannel())) { config.viewsCount = 1; } @@ -499,6 +515,9 @@ HistoryItem::HistoryItem( }; if (mediaOriginal && !ignoreMedia()) { _media = mediaOriginal->clone(this); + if (original->invertMedia()) { + _flags |= MessageFlag::InvertMedia; + } } const auto dropCustomEmoji = dropForwardInfo @@ -1022,7 +1041,7 @@ void HistoryItem::setCommentsItemId(FullMsgId id) { if (id.peer == _history->peer->id) { if (id.msg != this->id) { if (const auto reply = Get()) { - reply->replyToMsgTop = id.msg; + reply->setTopMessageId(id.msg); } } } else if (const auto views = Get()) { @@ -1107,7 +1126,7 @@ HistoryItem *HistoryItem::lookupDiscussionPostOriginal() const { forwarded->savedFromMsgId); } -PeerData *HistoryItem::displayFrom() const { +PeerData *HistoryItem::computeDisplayFrom() const { if (const auto sender = discussionPostOriginalSender()) { return sender; } else if (const auto sponsored = Get()) { @@ -1115,13 +1134,32 @@ PeerData *HistoryItem::displayFrom() const { return nullptr; } } else if (const auto forwarded = Get()) { - if (_history->peer->isSelf() || _history->peer->isRepliesChat() || forwarded->imported) { + if (_history->peer->isSelf() + || _history->peer->isRepliesChat() + || forwarded->imported) { return forwarded->originalSender; } } return author().get(); } +PeerData *HistoryItem::displayFrom() const { + if (!(_flags & MessageFlag::DisplayFromChecked)) { + _flags |= MessageFlag::DisplayFromChecked; + _displayFrom = computeDisplayFrom(); + } + return _displayFrom; +} + +uint8 HistoryItem::colorIndex() const { + if (const auto from = displayFrom()) { + return from->colorIndex(); + } else if (const auto info = hiddenSenderInfo()) { + return info->colorIndex; + } + Unexpected("No displayFrom and no hiddenSenderInfo."); +} + std::unique_ptr HistoryItem::createView( not_null delegate, HistoryView::Element *replacing) { @@ -1481,6 +1519,11 @@ void HistoryItem::applyEdition(HistoryMessageEdition &&edition) { } else { _flags &= ~MessageFlag::HideEdited; } + if (edition.invertMedia) { + _flags |= MessageFlag::InvertMedia; + } else { + _flags &= ~MessageFlag::InvertMedia; + } if (edition.editDate != -1) { //_flags |= MTPDmessage::Flag::f_edit_date; @@ -1582,6 +1625,7 @@ void HistoryItem::applyEdition(const MTPDmessageService &message) { createServiceFromMtp(message); applyServiceDateEdition(message); finishEditionToEmpty(); + _flags &= ~MessageFlag::DisplayFromChecked; } else if (isService()) { if (const auto reply = Get()) { reply->clearData(this); @@ -1591,6 +1635,7 @@ void HistoryItem::applyEdition(const MTPDmessageService &message) { createServiceFromMtp(message); applyServiceDateEdition(message); finishEdition(-1); + _flags &= ~MessageFlag::DisplayFromChecked; } } @@ -1621,11 +1666,18 @@ void HistoryItem::applySentMessage(const MTPDmessage &data) { setForwardsCount(data.vforwards().value_or(-1)); if (const auto reply = data.vreply_to()) { reply->match([&](const MTPDmessageReplyHeader &data) { - setReplyFields( - data.vreply_to_msg_id().v, - data.vreply_to_top_id().value_or( - data.vreply_to_msg_id().v), - data.is_forum_topic()); + // #TODO replies + const auto replyToPeer = data.vreply_to_peer_id() + ? peerFromMTP(*data.vreply_to_peer_id()) + : PeerId(); + if (!replyToPeer || replyToPeer == history()->peer->id) { + if (const auto replyToId = data.vreply_to_msg_id()) { + setReplyFields( + replyToId->v, + data.vreply_to_top_id().value_or(replyToId->v), + data.is_forum_topic()); + } + } }, [](const MTPDmessageReplyStoryHeader &data) { }); } @@ -1793,8 +1845,10 @@ void HistoryItem::destroyHistoryEntry() { topic->unreadReactions().erase(id); } } - if (const auto reply = Get()) { - changeReplyToTopCounter(reply, -1); + if (isRegular() && _history->peer->isMegagroup()) { + if (const auto reply = Get()) { + changeReplyToTopCounter(reply, -1); + } } } @@ -1853,22 +1907,20 @@ void HistoryItem::removeFromSharedMediaIndex() { } void HistoryItem::incrementReplyToTopCounter() { - if (const auto reply = Get()) { - changeReplyToTopCounter(reply, 1); + if (isRegular() && _history->peer->isMegagroup()) { + _history->session().changes().messageUpdated( + this, + Data::MessageUpdate::Flag::ReplyToTopAdded); + if (const auto reply = Get()) { + changeReplyToTopCounter(reply, 1); + } } } void HistoryItem::changeReplyToTopCounter( not_null reply, int delta) { - if (!isRegular() || !_history->peer->isMegagroup()) { - return; - } else if (delta > 0) { - _history->session().changes().messageUpdated( - this, - Data::MessageUpdate::Flag::ReplyToTopAdded); - } - const auto topId = reply->replyToTop(); + const auto topId = reply->topMessageId(); if (!topId) { return; } @@ -1920,10 +1972,10 @@ void HistoryItem::setRealId(MsgId newId) { _history->owner().requestItemResize(this); if (const auto reply = Get()) { - if (reply->replyToLink()) { - reply->setReplyToLinkFrom(this); + if (reply->link()) { + reply->setLinkFrom(this); } - changeReplyToTopCounter(reply, 1); + incrementReplyToTopCounter(); } } @@ -2370,14 +2422,14 @@ not_null HistoryItem::author() const { return (isPost() && !isSponsored()) ? _history->peer : from(); } -TimeId HistoryItem::dateOriginal() const { +TimeId HistoryItem::originalDate() const { if (const auto forwarded = Get()) { return forwarded->originalDate; } return date(); } -PeerData *HistoryItem::senderOriginal() const { +PeerData *HistoryItem::originalSender() const { if (const auto forwarded = Get()) { return forwarded->originalSender; } @@ -2405,18 +2457,18 @@ not_null HistoryItem::fromOriginal() const { return from(); } -QString HistoryItem::authorOriginal() const { +QString HistoryItem::originalPostAuthor() const { if (const auto forwarded = Get()) { - return forwarded->originalAuthor; + return forwarded->originalPostAuthor; } else if (const auto msgsigned = Get()) { if (!msgsigned->isAnonymousRank) { - return msgsigned->author; + return msgsigned->postAuthor; } } return QString(); } -MsgId HistoryItem::idOriginal() const { +MsgId HistoryItem::originalId() const { if (const auto forwarded = Get()) { return forwarded->originalId; } @@ -2480,9 +2532,9 @@ void HistoryItem::setForwardsCount(int count) { history()->owner().notifyItemDataChange(this); } -void HistoryItem::setPostAuthor(const QString &author) { +void HistoryItem::setPostAuthor(const QString &postAuthor) { auto msgsigned = Get(); - if (author.isEmpty()) { + if (postAuthor.isEmpty()) { if (!msgsigned) { return; } @@ -2493,10 +2545,10 @@ void HistoryItem::setPostAuthor(const QString &author) { if (!msgsigned) { AddComponents(HistoryMessageSigned::Bit()); msgsigned = Get(); - } else if (msgsigned->author == author) { + } else if (msgsigned->postAuthor == postAuthor) { return; } - msgsigned->author = author; + msgsigned->postAuthor = postAuthor; msgsigned->isAnonymousRank = !isDiscussionPost() && this->author()->isMegagroup(); history()->owner().requestItemResize(this); @@ -2632,21 +2684,11 @@ void HistoryItem::setReplyFields( } } } else if (const auto reply = Get()) { - reply->topicPost = isForumPost; - if ((reply->replyToMsgId != replyTo) - && !IsServerMsgId(reply->replyToMsgId)) { - reply->replyToMsgId = replyTo; - if (!reply->updateData(this)) { - RequestDependentMessageItem( - this, - reply->replyToPeerId, - reply->replyToMsgId); - } - } - if ((reply->replyToMsgTop != replyToTop) - && !IsServerMsgId(reply->replyToMsgTop)) { - reply->replyToMsgTop = replyToTop; - changeReplyToTopCounter(reply, 1); + const auto increment = (reply->topMessageId() != replyToTop) + && !IsServerMsgId(reply->topMessageId()); + reply->updateFields(this, replyTo, replyToTop, isForumPost); + if (increment) { + incrementReplyToTopCounter(); } } if (const auto topic = this->topic()) { @@ -2808,14 +2850,22 @@ bool HistoryItem::unread(not_null thread) const { MsgId HistoryItem::replyToId() const { if (const auto reply = Get()) { - return reply->replyToId(); + return reply->messageId(); } return 0; } +FullMsgId HistoryItem::replyToFullId() const { + if (const auto reply = Get()) { + const auto peer = reply->externalPeerId(); + return { peer ? peer : history()->peer->id, reply->messageId() }; + } + return {}; +} + MsgId HistoryItem::replyToTop() const { if (const auto reply = Get()) { - return reply->replyToTop(); + return reply->topMessageId(); } else if (const auto data = GetServiceDependentData()) { return data->topId; } @@ -2824,8 +2874,8 @@ MsgId HistoryItem::replyToTop() const { MsgId HistoryItem::topicRootId() const { if (const auto reply = Get() - ; reply && reply->topicPost) { - return reply->replyToTop(); + ; reply && reply->topicPost()) { + return reply->topMessageId(); } else if (const auto data = GetServiceDependentData() ; data && data->topicPost && data->topId) { return data->topId; @@ -2839,11 +2889,11 @@ MsgId HistoryItem::topicRootId() const { FullStoryId HistoryItem::replyToStory() const { if (const auto reply = Get()) { - if (reply->replyToStoryId) { - const auto peerId = reply->replyToPeerId - ? reply->replyToPeerId + if (reply->storyId()) { + const auto peerId = reply->externalPeerId() + ? reply->externalPeerId() : _history->peer->id; - return { .peer = peerId, .story = reply->replyToStoryId }; + return { .peer = peerId, .story = reply->storyId() }; } } return {}; @@ -2851,9 +2901,9 @@ FullStoryId HistoryItem::replyToStory() const { FullReplyTo HistoryItem::replyTo() const { return { - .msgId = replyToId(), - .topicRootId = topicRootId(), + .messageId = replyToFullId(), .storyId = replyToStory(), + .topicRootId = topicRootId(), }; } @@ -3038,7 +3088,10 @@ const std::vector &HistoryItem::customTextLinks() const { void HistoryItem::createComponents(CreateConfig &&config) { uint64 mask = 0; - if (config.replyTo || config.replyToStory) { + if (config.reply.messageId + || config.reply.externalSenderId + || !config.reply.externalSenderName.isEmpty() + || config.reply.storyId) { mask |= HistoryMessageReply::Bit(); } if (config.viaBotId) { @@ -3047,18 +3100,18 @@ void HistoryItem::createComponents(CreateConfig &&config) { if (config.viewsCount >= 0 || !config.replies.isNull) { mask |= HistoryMessageViews::Bit(); } - if (!config.author.isEmpty()) { + if (!config.postAuthor.isEmpty()) { mask |= HistoryMessageSigned::Bit(); } else if (_history->peer->isMegagroup() // Discussion posts signatures. && config.savedFromPeer - && !config.authorOriginal.isEmpty()) { + && !config.originalPostAuthor.isEmpty()) { const auto savedFrom = _history->owner().peerLoaded( config.savedFromPeer); if (savedFrom && savedFrom->isChannel()) { mask |= HistoryMessageSigned::Bit(); } } else if ((_history->peer->isSelf() || _history->peer->isRepliesChat()) - && !config.authorOriginal.isEmpty()) { + && !config.originalPostAuthor.isEmpty()) { mask |= HistoryMessageSigned::Bit(); } if (config.editDate != TimeId(0)) { @@ -3076,23 +3129,18 @@ void HistoryItem::createComponents(CreateConfig &&config) { UpdateComponents(mask); if (const auto reply = Get()) { - reply->replyToPeerId = config.replyToPeer; - reply->replyToMsgId = config.replyTo; - reply->replyToMsgTop = isScheduled() ? 0 : config.replyToTop; - reply->replyToStoryId = config.replyToStory; - reply->storyReply = (config.replyToStory != 0); - reply->topicPost = config.replyIsTopicPost; + reply->set(config.reply); if (!reply->updateData(this)) { - if (reply->replyToMsgId) { + if (const auto messageId = reply->messageId()) { RequestDependentMessageItem( this, - reply->replyToPeerId, - reply->replyToMsgId); - } else if (reply->replyToStoryId) { + reply->externalPeerId(), + reply->messageId()); + } else if (reply->storyId()) { RequestDependentMessageStory( this, - reply->replyToPeerId, - reply->replyToStoryId); + reply->externalPeerId(), + reply->storyId()); } } } @@ -3118,9 +3166,9 @@ void HistoryItem::createComponents(CreateConfig &&config) { edited->date = config.editDate; } if (const auto msgsigned = Get()) { - msgsigned->author = config.author.isEmpty() - ? config.authorOriginal - : config.author; + msgsigned->postAuthor = config.postAuthor.isEmpty() + ? config.originalPostAuthor + : config.postAuthor; msgsigned->isAnonymousRank = !isDiscussionPost() && author()->isMegagroup(); } @@ -3156,9 +3204,9 @@ void HistoryItem::setupForwardedComponent(const CreateConfig &config) { return; } forwarded->originalDate = config.originalDate; - const auto originalSender = config.senderOriginal - ? config.senderOriginal - : !config.senderNameOriginal.isEmpty() + const auto originalSender = config.originalSenderId + ? config.originalSenderId + : !config.originalSenderName.isEmpty() ? PeerId() : from()->id; forwarded->originalSender = originalSender @@ -3166,11 +3214,11 @@ void HistoryItem::setupForwardedComponent(const CreateConfig &config) { : nullptr; if (!forwarded->originalSender) { forwarded->hiddenSenderInfo = std::make_unique( - config.senderNameOriginal, + config.originalSenderName, config.imported); } forwarded->originalId = config.originalId; - forwarded->originalAuthor = config.authorOriginal; + forwarded->originalPostAuthor = config.originalPostAuthor; forwarded->psaType = config.forwardPsaType; forwarded->savedFromPeer = _history->owner().peerLoaded( config.savedFromPeer); @@ -3222,7 +3270,7 @@ TextWithEntities HistoryItem::withLocalEntities( : nullptr; if (document) { if (const auto duration = DurationForTimestampLinks(document)) { - const auto context = reply->replyToMsg->fullId(); + const auto context = reply->resolvedMessage->fullId(); return AddTimestampLinks( textWithEntities, duration, @@ -3230,7 +3278,7 @@ TextWithEntities HistoryItem::withLocalEntities( } } else if (webpage) { if (const auto duration = DurationForTimestampLinks(webpage)) { - const auto context = reply->replyToMsg->fullId(); + const auto context = reply->resolvedMessage->fullId(); return AddTimestampLinks( textWithEntities, duration, @@ -3246,7 +3294,8 @@ void HistoryItem::setSponsoredFrom(const Data::SponsoredFrom &from) { const auto sponsored = Get(); sponsored->sender = std::make_unique( from.title, - false); + false, + from.peer ? from.peer->colorIndex() : std::optional()); sponsored->recommended = from.isRecommended; sponsored->isForceUserpicDisplay = from.isForceUserpicDisplay; if (from.userpic.location.valid()) { @@ -3279,19 +3328,29 @@ void HistoryItem::createComponentsHelper( auto config = CreateConfig(); config.viaBotId = viaBotId; if (flags & MessageFlag::HasReplyInfo) { - config.replyTo = replyTo.msgId; - config.replyToStory = replyTo.storyId.story; - config.replyToPeer = replyTo.storyId ? replyTo.storyId.peer : 0; - const auto to = LookupReplyTo(_history, replyTo.msgId); - const auto replyToTop = LookupReplyToTop(to); - config.replyToTop = replyToTop ? replyToTop : replyTo.msgId; + config.reply.messageId = replyTo.messageId.msg; + config.reply.storyId = replyTo.storyId.story; + config.reply.externalPeerId = replyTo.storyId + ? replyTo.storyId.peer + : (replyTo.messageId && replyTo.messageId.peer + != history()->peer->id) + ? replyTo.messageId.peer + : PeerId(); + const auto to = LookupReplyTo(_history, replyTo.messageId); + const auto replyToTop = LookupReplyToTop(_history, to); + config.reply.topMessageId = replyToTop + ? replyToTop + : (replyTo.messageId.peer == history()->peer->id) + ? replyTo.messageId.msg + : MsgId(); const auto forum = _history->asForum(); - config.replyIsTopicPost = LookupReplyIsTopicPost(to) + config.reply.topicPost = LookupReplyIsTopicPost(to) || (to && to->Has()) - || (forum && forum->creating(config.replyToTop)); + || (forum && forum->creating(config.reply.topMessageId)); + config.reply.quote = std::move(replyTo.quote); } config.markup = std::move(markup); - if (flags & MessageFlag::HasPostAuthor) config.author = postAuthor; + if (flags & MessageFlag::HasPostAuthor) config.postAuthor = postAuthor; if (flags & MessageFlag::HasViews) config.viewsCount = 1; createComponents(std::move(config)); @@ -3381,23 +3440,7 @@ void HistoryItem::createComponents(const MTPDmessage &data) { }); } if (const auto reply = data.vreply_to()) { - reply->match([&](const MTPDmessageReplyHeader &data) { - if (const auto peer = data.vreply_to_peer_id()) { - config.replyToPeer = peerFromMTP(*peer); - if (config.replyToPeer == _history->peer->id) { - config.replyToPeer = 0; - } - } - const auto id = data.vreply_to_msg_id().v; - config.replyTo = data.is_reply_to_scheduled() - ? _history->owner().scheduledMessages().localMessageId(id) - : id; - config.replyToTop = data.vreply_to_top_id().value_or(id); - config.replyIsTopicPost = data.is_forum_topic(); - }, [&](const MTPDmessageReplyStoryHeader &data) { - config.replyToPeer = peerFromUser(data.vuser_id()); - config.replyToStory = data.vstory_id().v; - }); + config.reply = ReplyFieldsFromMTP(history(), *reply); } config.viaBotId = data.vvia_bot_id().value_or_empty(); config.viewsCount = data.vviews().value_or(-1); @@ -3407,7 +3450,7 @@ void HistoryItem::createComponents(const MTPDmessage &data) { : HistoryMessageRepliesData(data.vreplies()); config.markup = HistoryMessageMarkupData(data.vreply_markup()); config.editDate = data.vedit_date().value_or_empty(); - config.author = qs(data.vpost_author().value_or_empty()); + config.postAuthor = qs(data.vpost_author().value_or_empty()); createComponents(std::move(config)); } @@ -3631,21 +3674,23 @@ void HistoryItem::createServiceFromMtp(const MTPDmessageService &message) { ? peerFromMTP(*data.vreply_to_peer_id()) : _history->peer->id; if (const auto dependent = GetServiceDependentData()) { - dependent->peerId = (peerId != _history->peer->id) - ? peerId - : 0; - const auto id = data.vreply_to_msg_id().v; - dependent->msgId = id; - dependent->topId = data.vreply_to_top_id().value_or(id); - dependent->topicPost = data.is_forum_topic() - || Has(); - if (!updateServiceDependent()) { - RequestDependentMessageItem( - this, - (dependent->peerId - ? dependent->peerId - : _history->peer->id), - dependent->msgId); + const auto id = data.vreply_to_msg_id().value_or_empty(); + if (id) { + dependent->peerId = (peerId != _history->peer->id) + ? peerId + : 0; + dependent->msgId = id; + dependent->topId = data.vreply_to_top_id().value_or(id); + dependent->topicPost = data.is_forum_topic() + || Has(); + if (!updateServiceDependent()) { + RequestDependentMessageItem( + this, + (dependent->peerId + ? dependent->peerId + : _history->peer->id), + dependent->msgId); + } } } }, [](const MTPDmessageReplyStoryHeader &data) { @@ -4426,6 +4471,36 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { return result; }; + auto prepareGiftCode = [&](const MTPDmessageActionGiftCode &action) { + auto result = PreparedServiceText(); + _history->session().giftBoxStickersPacks().load(); + result.text = { + (action.is_unclaimed() + ? tr::lng_prize_unclaimed_about + : action.is_via_giveaway() + ? tr::lng_prize_about + : tr::lng_prize_gift_about)( + tr::now, + lt_channel, + (action.vboost_peer() + ? _from->owner().peer( + peerFromMTP(*action.vboost_peer()))->name() + : "a channel")), + }; + return result; + }; + + auto prepareGiveawayLaunch = [&](const MTPDmessageActionGiveawayLaunch &action) { + auto result = PreparedServiceText(); + result.links.push_back(fromLink()); + result.text = tr::lng_action_giveaway_started( + tr::now, + lt_from, + fromLinkText(), // Link 1. + Ui::Text::WithEntities); + return result; + }; + setServiceText(action.match([&]( const MTPDmessageActionChatAddUser &data) { return prepareChatAddUserText(data); @@ -4506,6 +4581,10 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { return prepareSetChatWallPaper(data); }, [&](const MTPDmessageActionSetSameChatWallPaper &data) { return prepareSetSameChatWallPaper(data); + }, [&](const MTPDmessageActionGiftCode &data) { + return prepareGiftCode(data); + }, [&](const MTPDmessageActionGiveawayLaunch &data) { + return prepareGiveawayLaunch(data); }, [](const MTPDmessageActionEmpty &) { return PreparedServiceText{ { tr::lng_message_empty(tr::now) } }; })); @@ -4575,6 +4654,22 @@ void HistoryItem::applyAction(const MTPMessageAction &action) { if (const auto paper = Data::WallPaper::Create(session, attached)) { _media = std::make_unique(this, *paper); } + }, [&](const MTPDmessageActionGiftCode &data) { + const auto boostedId = data.vboost_peer() + ? peerToChannel(peerFromMTP(*data.vboost_peer())) + : ChannelId(); + _media = std::make_unique( + this, + _from, + Data::GiftCode{ + .slug = qs(data.vslug()), + .channel = (peerIsChannel(boostedId) + ? history()->owner().channel(boostedId).get() + : nullptr), + .months = data.vmonths().v, + .viaGiveaway = data.is_via_giveaway(), + .unclaimed = data.is_unclaimed(), + }); }, [](const auto &) { }); } diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index fdbf438899128f..488456aec208f2 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -240,6 +240,9 @@ class HistoryItem final : public RuntimeComposer { [[nodiscard]] bool isPinned() const { return _flags & MessageFlag::Pinned; } + [[nodiscard]] bool invertMedia() const { + return _flags & MessageFlag::InvertMedia; + } [[nodiscard]] bool unread(not_null thread) const; [[nodiscard]] bool showNotification() const; void markClientSideAsRead(); @@ -465,6 +468,7 @@ class HistoryItem final : public RuntimeComposer { void setText(const TextWithEntities &textWithEntities); [[nodiscard]] MsgId replyToId() const; + [[nodiscard]] FullMsgId replyToFullId() const; [[nodiscard]] MsgId replyToTop() const; [[nodiscard]] MsgId topicRootId() const; [[nodiscard]] FullStoryId replyToStory() const; @@ -473,12 +477,12 @@ class HistoryItem final : public RuntimeComposer { [[nodiscard]] not_null author() const; - [[nodiscard]] TimeId dateOriginal() const; - [[nodiscard]] PeerData *senderOriginal() const; + [[nodiscard]] TimeId originalDate() const; + [[nodiscard]] PeerData *originalSender() const; [[nodiscard]] const HiddenSenderInfo *hiddenSenderInfo() const; [[nodiscard]] not_null fromOriginal() const; - [[nodiscard]] QString authorOriginal() const; - [[nodiscard]] MsgId idOriginal() const; + [[nodiscard]] QString originalPostAuthor() const; + [[nodiscard]] MsgId originalId() const; [[nodiscard]] bool isEmpty() const; @@ -497,6 +501,7 @@ class HistoryItem final : public RuntimeComposer { [[nodiscard]] bool isDiscussionPost() const; [[nodiscard]] HistoryItem *lookupDiscussionPostOriginal() const; [[nodiscard]] PeerData *displayFrom() const; + [[nodiscard]] uint8 colorIndex() const; [[nodiscard]] std::unique_ptr createView( not_null delegate, @@ -579,8 +584,8 @@ class HistoryItem final : public RuntimeComposer { bool used); void setSelfDestruct(HistorySelfDestructType type, MTPint mtpTTLvalue); - TextWithEntities fromLinkText() const; - ClickHandlerPtr fromLink() const; + [[nodiscard]] TextWithEntities fromLinkText() const; + [[nodiscard]] ClickHandlerPtr fromLink() const; void setGroupId(MessageGroupId groupId); @@ -619,9 +624,12 @@ class HistoryItem final : public RuntimeComposer { [[nodiscard]] PreparedServiceText prepareCallScheduledText( TimeId scheduleDate); + [[nodiscard]] PeerData *computeDisplayFrom() const; + const not_null _history; const not_null _from; - MessageFlags _flags = 0; + mutable PeerData *_displayFrom = nullptr; + mutable MessageFlags _flags = 0; TextWithEntities _text; diff --git a/Telegram/SourceFiles/history/history_item_components.cpp b/Telegram/SourceFiles/history/history_item_components.cpp index 400dc011999fbf..52234fb67fc68b 100644 --- a/Telegram/SourceFiles/history/history_item_components.cpp +++ b/Telegram/SourceFiles/history/history_item_components.cpp @@ -7,6 +7,7 @@ For license and copyright information please follow this link: */ #include "history/history_item_components.h" +#include "api/api_text_entities.h" #include "base/qt/qt_key_modifiers.h" #include "lang/lang_keys.h" #include "ui/effects/ripple_animation.h" @@ -31,6 +32,7 @@ For license and copyright information please follow this link: #include "mainwindow.h" #include "media/audio/media_audio.h" #include "media/player/media_player_instance.h" +#include "data/stickers/data_custom_emoji.h" #include "data/data_media_types.h" #include "data/data_session.h" #include "data/data_user.h" @@ -38,6 +40,8 @@ For license and copyright information please follow this link: #include "data/data_document.h" #include "data/data_web_page.h" #include "data/data_file_click_handler.h" +#include "data/data_scheduled_messages.h" +#include "data/data_session.h" #include "data/data_stories.h" #include "main/main_session.h" #include "window/window_session_controller.h" @@ -52,6 +56,127 @@ namespace { const auto kPsaForwardedPrefix = "cloud_lng_forwarded_psa_"; +void ValidateBackgroundEmoji( + DocumentId backgroundEmojiId, + not_null data, + not_null cache, + not_null quote, + not_null holder) { + if (data->firstFrameMask.isNull()) { + if (!cache->frames[0].isNull()) { + for (auto &frame : cache->frames) { + frame = QImage(); + } + } + const auto tag = Data::CustomEmojiSizeTag::Isolated; + if (!data->emoji) { + const auto owner = &holder->history()->owner(); + const auto repaint = crl::guard(holder, [=] { + holder->history()->owner().requestViewRepaint(holder); + }); + data->emoji = owner->customEmojiManager().create( + backgroundEmojiId, + repaint, + tag); + } + if (!data->emoji->ready()) { + return; + } + const auto size = Data::FrameSizeFromTag(tag); + data->firstFrameMask = QImage( + QSize(size, size), + QImage::Format_ARGB32_Premultiplied); + data->firstFrameMask.fill(Qt::transparent); + data->firstFrameMask.setDevicePixelRatio(style::DevicePixelRatio()); + auto p = Painter(&data->firstFrameMask); + data->emoji->paint(p, { + .textColor = QColor(255, 255, 255), + .position = QPoint(0, 0), + .internal = { + .forceFirstFrame = true, + }, + }); + p.end(); + + data->emoji = nullptr; + } + if (!cache->frames[0].isNull() && cache->color == quote->icon) { + return; + } + cache->color = quote->icon; + const auto ratio = style::DevicePixelRatio(); + auto colorized = QImage( + data->firstFrameMask.size(), + QImage::Format_ARGB32_Premultiplied); + colorized.setDevicePixelRatio(ratio); + style::colorizeImage( + data->firstFrameMask, + cache->color, + &colorized, + QRect(), // src + QPoint(), // dst + true); // use alpha + const auto make = [&](int size) { + size = style::ConvertScale(size) * ratio; + auto result = colorized.scaled( + size, + size, + Qt::IgnoreAspectRatio, + Qt::SmoothTransformation); + result.setDevicePixelRatio(ratio); + return result; + }; + + constexpr auto kSize1 = 12; + constexpr auto kSize2 = 16; + constexpr auto kSize3 = 20; + cache->frames[0] = make(kSize1); + cache->frames[1] = make(kSize2); + cache->frames[2] = make(kSize3); +} + +void FillBackgroundEmoji( + Painter &p, + const QRect &rect, + bool quote, + const Ui::BackgroundEmojiCache &cache) { + p.setClipRect(rect); + + const auto &frames = cache.frames; + const auto right = rect.x() + rect.width(); + const auto paint = [&](int x, int y, int index, float64 opacity) { + y = style::ConvertScale(y); + if (y >= rect.height()) { + return; + } + p.setOpacity(opacity); + p.drawImage( + right - style::ConvertScale(x + (quote ? 12 : 0)), + rect.y() + y, + frames[index]); + }; + + paint(28, 4, 2, 0.32); + paint(51, 15, 1, 0.32); + paint(64, -2, 0, 0.28); + paint(87, 11, 1, 0.24); + paint(125, -2, 2, 0.16); + + paint(28, 31, 1, 0.24); + paint(72, 33, 2, 0.2); + + paint(46, 52, 1, 0.24); + paint(24, 55, 2, 0.18); + + if (quote) { + paint(4, 23, 1, 0.28); + paint(0, 48, 0, 0.24); + } + + p.setClipping(false); + p.setOpacity(1.); +} + } // namespace void HistoryMessageVia::create( @@ -64,7 +189,7 @@ void HistoryMessageVia::create( lt_inline_bot, '@' + bot->username())); link = std::make_shared([bot = this->bot]( - ClickContext context) { + ClickContext context) { const auto my = context.other.value(); if (const auto controller = my.sessionWindow.get()) { if (base::IsCtrlPressed()) { @@ -105,11 +230,15 @@ void HistoryMessageVia::resize(int32 availw) const { } } -HiddenSenderInfo::HiddenSenderInfo(const QString &name, bool external) +HiddenSenderInfo::HiddenSenderInfo( + const QString &name, + bool external, + std::optional colorIndex) : name(name) -, colorPeerId(Data::FakePeerIdForJustName(name)) +, colorIndex(colorIndex.value_or( + Data::DecideColorIndex(Data::FakePeerIdForJustName(name)))) , emptyUserpic( - Ui::EmptyUserpic::UserpicColor(Data::PeerColorIndex(colorPeerId)), + Ui::EmptyUserpic::UserpicColor(this->colorIndex), (external ? Ui::EmptyUserpic::ExternalName() : name)) { @@ -183,13 +312,13 @@ void HistoryMessageForwarded::create(const HistoryMessageVia *via) const { ? originalSender->name() : hiddenSenderInfo->name) }; - if (!originalAuthor.isEmpty()) { + if (!originalPostAuthor.isEmpty()) { phrase = tr::lng_forwarded_signed( tr::now, lt_channel, name, lt_user, - { .text = originalAuthor }, + { .text = originalPostAuthor }, Ui::Text::WithEntities); } else { phrase = name; @@ -261,6 +390,87 @@ void HistoryMessageForwarded::create(const HistoryMessageVia *via) const { } } +ReplyFields ReplyFieldsFromMTP( + not_null history, + const MTPMessageReplyHeader &reply) { + return reply.match([&](const MTPDmessageReplyHeader &data) { + auto result = ReplyFields(); + if (const auto peer = data.vreply_to_peer_id()) { + result.externalPeerId = peerFromMTP(*peer); + if (result.externalPeerId == history->peer->id) { + result.externalPeerId = 0; + } + } + const auto owner = &history->owner(); + if (const auto id = data.vreply_to_msg_id().value_or_empty()) { + result.messageId = data.is_reply_to_scheduled() + ? owner->scheduledMessages().localMessageId(id) + : id; + result.topMessageId + = data.vreply_to_top_id().value_or(id); + result.topicPost = data.is_forum_topic(); + } + if (const auto header = data.vreply_from()) { + const auto &data = header->data(); + result.externalPostAuthor + = qs(data.vpost_author().value_or_empty()); + result.externalSenderId = data.vfrom_id() + ? peerFromMTP(*data.vfrom_id()) + : PeerId(); + result.externalSenderName + = qs(data.vfrom_name().value_or_empty()); + } + result.quote = TextWithEntities{ + qs(data.vquote_text().value_or_empty()), + Api::EntitiesFromMTP( + &owner->session(), + data.vquote_entities().value_or_empty()), + }; + return result; + }, [&](const MTPDmessageReplyStoryHeader &data) { + return ReplyFields{ + .externalPeerId = peerFromUser(data.vuser_id()), + .storyId = data.vstory_id().v, + }; + }); +} + +FullReplyTo ReplyToFromMTP( + not_null history, + const MTPInputReplyTo &reply) { + return reply.match([&](const MTPDinputReplyToMessage &data) { + auto result = FullReplyTo{ + .messageId = { history->peer->id, data.vreply_to_msg_id().v }, + }; + if (const auto peer = data.vreply_to_peer_id()) { + const auto parsed = Data::PeerFromInputMTP( + &history->owner(), + *peer); + if (!parsed) { + return FullReplyTo(); + } + result.messageId.peer = parsed->id; + } + result.topicRootId = data.vtop_msg_id().value_or_empty(); + result.quote = TextWithEntities{ + qs(data.vquote_text().value_or_empty()), + Api::EntitiesFromMTP( + &history->session(), + data.vquote_entities().value_or_empty()), + }; + return result; + }, [&](const MTPDinputReplyToStory &data) { + if (const auto parsed = Data::UserFromInputMTP( + &history->owner(), + data.vuser_id())) { + return FullReplyTo{ + .storyId = { parsed->id, data.vstory_id().v }, + }; + } + return FullReplyTo(); + }); +} + HistoryMessageReply::HistoryMessageReply() = default; HistoryMessageReply &HistoryMessageReply::operator=( @@ -268,8 +478,8 @@ HistoryMessageReply &HistoryMessageReply::operator=( HistoryMessageReply::~HistoryMessageReply() { // clearData() should be called by holder. - Expects(replyToMsg.empty()); - Expects(replyToVia == nullptr); + Expects(resolvedMessage.empty()); + Expects(originalVia == nullptr); } bool HistoryMessageReply::updateData( @@ -277,166 +487,222 @@ bool HistoryMessageReply::updateData( bool force) { const auto guard = gsl::finally([&] { refreshReplyToMedia(); }); if (!force) { - if ((replyToMsg || !replyToMsgId) - && (replyToStory || !replyToStoryId)) { + if (resolvedMessage || resolvedStory || _unavailable) { return true; } } - const auto peerId = replyToPeerId - ? replyToPeerId + const auto peerId = _fields.externalPeerId + ? _fields.externalPeerId : holder->history()->peer->id; - if (!replyToMsg && replyToMsgId) { - replyToMsg = holder->history()->owner().message( + if (!resolvedMessage && _fields.messageId) { + resolvedMessage = holder->history()->owner().message( peerId, - replyToMsgId); - if (replyToMsg) { - if (replyToMsg->isEmpty()) { + _fields.messageId); + if (resolvedMessage) { + if (resolvedMessage->isEmpty()) { // Really it is deleted. - replyToMsg = nullptr; + resolvedMessage = nullptr; force = true; } else { holder->history()->owner().registerDependentMessage( holder, - replyToMsg.get()); + resolvedMessage.get()); } } } - if (!replyToStory && replyToStoryId) { + if (!resolvedStory && _fields.storyId) { const auto maybe = holder->history()->owner().stories().lookup({ peerId, - replyToStoryId, + _fields.storyId, }); if (maybe) { - replyToStory = *maybe; + resolvedStory = *maybe; holder->history()->owner().stories().registerDependentMessage( holder, - replyToStory.get()); + resolvedStory.get()); } else if (maybe.error() == Data::NoStory::Deleted) { force = true; } } - if (replyToMsg || replyToStory) { + const auto external = _fields.externalSenderId + || !_fields.externalSenderName.isEmpty(); + if (resolvedMessage + || resolvedStory + || (external && (!_fields.messageId || force))) { const auto repaint = [=] { holder->customEmojiRepaint(); }; const auto context = Core::MarkedTextContext{ .session = &holder->history()->session(), .customEmojiRepaint = repaint, }; - replyToText.setMarkedText( + const auto text = !_fields.quote.empty() + ? _fields.quote + : resolvedMessage + ? resolvedMessage->inReplyText() + : resolvedStory + ? resolvedStory->inReplyText() + : TextWithEntities{ u"..."_q }; + _text.setMarkedText( st::defaultTextStyle, - (replyToMsg - ? replyToMsg->inReplyText() - : replyToStory->inReplyText()), + text, Ui::DialogTextOptions(), context); updateName(holder); - - setReplyToLinkFrom(holder); - if (replyToMsg && !replyToMsg->Has()) { - if (auto bot = replyToMsg->viaBot()) { - replyToVia = std::make_unique(); - replyToVia->create( + setLinkFrom(holder); + if (resolvedMessage + && !resolvedMessage->Has()) { + if (const auto bot = resolvedMessage->viaBot()) { + originalVia = std::make_unique(); + originalVia->create( &holder->history()->owner(), peerToUser(bot->id)); } } - if (replyToMsg) { - const auto peer = replyToMsg->history()->peer; - replyToColorKey = (!holder->out() - && (peer->isMegagroup() || peer->isChat()) - && replyToMsg->from()->isUser()) - ? replyToMsg->from()->id - : PeerId(0); - } else { - replyToColorKey = PeerId(0); + if (!resolvedMessage && !resolvedStory) { + _unavailable = 1; } - const auto media = replyToMsg ? replyToMsg->media() : nullptr; + const auto media = resolvedMessage + ? resolvedMessage->media() + : nullptr; if (!media || !media->hasReplyPreview() || !media->hasSpoiler()) { spoiler = nullptr; } else if (!spoiler) { spoiler = std::make_unique(repaint); } } else if (force) { - replyToMsgId = 0; - replyToStoryId = 0; - replyToColorKey = PeerId(0); + if (_fields.messageId || _fields.storyId) { + _unavailable = 1; + } spoiler = nullptr; } if (force) { holder->history()->owner().requestItemResize(holder); } - return (replyToMsg || !replyToMsgId) - && (replyToStory || !replyToStoryId); + return resolvedMessage + || resolvedStory + || (external && !_fields.messageId) + || _unavailable; +} + +void HistoryMessageReply::set(ReplyFields fields) { + _fields = std::move(fields); +} + +void HistoryMessageReply::updateFields( + not_null holder, + MsgId messageId, + MsgId topMessageId, + bool topicPost) { + _fields.topicPost = topicPost; + if ((_fields.messageId != messageId) + && !IsServerMsgId(_fields.messageId)) { + _fields.messageId = messageId; + if (!updateData(holder)) { + RequestDependentMessageItem( + holder, + _fields.externalPeerId, + _fields.messageId); + } + } + if ((_fields.topMessageId != topMessageId) + && !IsServerMsgId(_fields.topMessageId)) { + _fields.topMessageId = topMessageId; + } } -void HistoryMessageReply::setReplyToLinkFrom( +void HistoryMessageReply::setLinkFrom( not_null holder) { - replyToLnk = replyToMsg - ? JumpToMessageClickHandler(replyToMsg.get(), holder->fullId()) - : replyToStory - ? JumpToStoryClickHandler(replyToStory.get()) + const auto externalPeerId = _fields.externalSenderId; + const auto external = externalPeerId + || !_fields.externalSenderName.isEmpty(); + const auto externalLink = [=](ClickContext context) { + const auto my = context.other.value(); + if (const auto controller = my.sessionWindow.get()) { + if (externalPeerId) { + controller->showPeerInfo( + controller->session().data().peer(externalPeerId)); + } else { + controller->showToast(u"External reply"_q); + } + } + }; + _link = resolvedMessage + ? JumpToMessageClickHandler(resolvedMessage.get(), holder->fullId()) + : resolvedStory + ? JumpToStoryClickHandler(resolvedStory.get()) + : (external && !_fields.messageId) + ? std::make_shared(externalLink) : nullptr; } +void HistoryMessageReply::setTopMessageId(MsgId topMessageId) { + _fields.topMessageId = topMessageId; +} + void HistoryMessageReply::clearData(not_null holder) { - replyToVia = nullptr; - if (replyToMsg) { + originalVia = nullptr; + if (resolvedMessage) { holder->history()->owner().unregisterDependentMessage( holder, - replyToMsg.get()); - replyToMsg = nullptr; + resolvedMessage.get()); + resolvedMessage = nullptr; } - if (replyToStory) { + if (resolvedStory) { holder->history()->owner().stories().unregisterDependentMessage( holder, - replyToStory.get()); - replyToStory = nullptr; + resolvedStory.get()); + resolvedStory = nullptr; } - replyToMsgId = 0; - replyToStoryId = 0; + _unavailable = 1; refreshReplyToMedia(); } -PeerData *HistoryMessageReply::replyToFrom( - not_null holder) const { - if (!replyToMsg) { - return nullptr; +PeerData *HistoryMessageReply::sender(not_null holder) const { + if (resolvedStory) { + return resolvedStory->peer(); + } else if (!resolvedMessage) { + if (!_externalSender && _fields.externalSenderId) { + _externalSender = holder->history()->owner().peer( + _fields.externalSenderId); + } + return _externalSender; } else if (holder->Has()) { - if (const auto fwd = replyToMsg->Get()) { - return fwd->originalSender; + // Forward of a reply. Show reply-to original sender. + const auto forwarded + = resolvedMessage->Get(); + if (forwarded) { + return forwarded->originalSender; } } - if (const auto from = replyToMsg->displayFrom()) { + if (const auto from = resolvedMessage->displayFrom()) { return from; } - return replyToMsg->author().get(); + return resolvedMessage->author().get(); } -QString HistoryMessageReply::replyToFromName( +QString HistoryMessageReply::senderName( not_null holder) const { - if (replyToStory) { - return replyToFromName(replyToStory->peer()); - } else if (!replyToMsg) { - return QString(); + if (const auto peer = sender(holder)) { + return senderName(peer); + } else if (!resolvedMessage) { + return _fields.externalSenderName; } else if (holder->Has()) { - if (const auto fwd = replyToMsg->Get()) { - return fwd->originalSender - ? replyToFromName(fwd->originalSender) - : fwd->hiddenSenderInfo->name; + // Forward of a reply. Show reply-to original sender. + const auto forwarded + = resolvedMessage->Get(); + if (forwarded) { + Assert(forwarded->hiddenSenderInfo != nullptr); + return forwarded->hiddenSenderInfo->name; } } - if (const auto from = replyToMsg->displayFrom()) { - return replyToFromName(from); - } - return replyToFromName(replyToMsg->author()); + return QString(); } -QString HistoryMessageReply::replyToFromName( - not_null peer) const { - if (const auto user = replyToVia ? peer->asUser() : nullptr) { +QString HistoryMessageReply::senderName(not_null peer) const { + if (const auto user = originalVia ? peer->asUser() : nullptr) { return user->firstName; } return peer->name(); @@ -444,9 +710,9 @@ QString HistoryMessageReply::replyToFromName( bool HistoryMessageReply::isNameUpdated( not_null holder) const { - if (const auto from = replyToFrom(holder)) { - if (replyToVersion < from->nameVersion()) { - updateName(holder); + if (const auto from = sender(holder)) { + if (_nameVersion < from->nameVersion()) { + updateName(holder, from); return true; } } @@ -454,55 +720,91 @@ bool HistoryMessageReply::isNameUpdated( } void HistoryMessageReply::updateName( - not_null holder) const { - if (const auto name = replyToFromName(holder); !name.isEmpty()) { - replyToName.setText(st::fwdTextStyle, name, Ui::NameTextOptions()); - if (const auto from = replyToFrom(holder)) { - replyToVersion = from->nameVersion(); - } else if (replyToMsg) { - replyToVersion = replyToMsg->author()->nameVersion(); - } else { - replyToVersion = replyToStory->peer()->nameVersion(); - } - bool hasPreview = (replyToStory && replyToStory->hasReplyPreview()) - || (replyToMsg - && replyToMsg->media() - && replyToMsg->media()->hasReplyPreview()); - int32 previewSkip = hasPreview ? (st::msgReplyBarSize.height() + st::msgReplyBarSkip - st::msgReplyBarSize.width() - st::msgReplyBarPos.x()) : 0; - int32 w = replyToName.maxWidth(); - if (replyToVia) { - w += st::msgServiceFont->spacew + replyToVia->maxWidth; + not_null holder, + std::optional resolvedSender) const { + const auto peer = resolvedSender.value_or(sender(holder)); + const auto name = peer ? senderName(peer) : senderName(holder); + const auto hasPreview = (resolvedStory + && resolvedStory->hasReplyPreview()) + || (resolvedMessage + && resolvedMessage->media() + && resolvedMessage->media()->hasReplyPreview()); + const auto textLeft = hasPreview + ? (st::messageQuoteStyle.outline + + st::historyReplyPreviewMargin.left() + + st::historyReplyPreview + + st::historyReplyPreviewMargin.right()) + : st::historyReplyPadding.left(); + if (!name.isEmpty()) { + _name.setText(st::fwdTextStyle, name, Ui::NameTextOptions()); + if (peer) { + _nameVersion = peer->nameVersion(); } - - maxReplyWidth = previewSkip - + std::max( - w, - std::min(replyToText.maxWidth(), st::maxSignatureSize)) - + (storyReply + const auto w = _name.maxWidth() + + (originalVia + ? (st::msgServiceFont->spacew + originalVia->maxWidth) + : 0) + + (_fields.quote.empty() + ? 0 + : st::messageTextStyle.blockquote.icon.width()); + _maxWidth = std::max( + w, + std::min(_text.maxWidth(), st::maxSignatureSize)) + + (_fields.storyId ? (st::dialogsMiniReplyStory.skipText + st::dialogsMiniReplyStory.icon.icon.width()) : 0); } else { - maxReplyWidth = st::msgDateFont->width(statePhrase()); + _maxWidth = st::msgDateFont->width(statePhrase()); + } + _maxWidth = textLeft + + _maxWidth + + st::historyReplyPadding.right(); + _minHeight = st::historyReplyPadding.top() + + st::msgServiceNameFont->height + + st::normalFont->height + + st::historyReplyPadding.bottom(); +} + +int HistoryMessageReply::resizeToWidth(int width) const { + const auto hasPreview = (resolvedStory + && resolvedStory->hasReplyPreview()) + || (resolvedMessage + && resolvedMessage->media() + && resolvedMessage->media()->hasReplyPreview()); + const auto textLeft = hasPreview + ? (st::messageQuoteStyle.outline + + st::historyReplyPreviewMargin.left() + + st::historyReplyPreview + + st::historyReplyPreviewMargin.right()) + : st::historyReplyPadding.left(); + if (originalVia) { + originalVia->resize(width + - textLeft + - st::historyReplyPadding.right() + - _name.maxWidth() + - st::msgServiceFont->spacew); } - maxReplyWidth = st::msgReplyPadding.left() + st::msgReplyBarSkip + maxReplyWidth + st::msgReplyPadding.right(); + if (width >= _maxWidth) { + _height = _minHeight; + return height(); + } + _height = _minHeight; + return height(); } -void HistoryMessageReply::resize(int width) const { - if (replyToVia) { - bool hasPreview = (replyToStory && replyToStory->hasReplyPreview()) - || (replyToMsg - && replyToMsg->media() - && replyToMsg->media()->hasReplyPreview()); - int previewSkip = hasPreview ? (st::msgReplyBarSize.height() + st::msgReplyBarSkip - st::msgReplyBarSize.width() - st::msgReplyBarPos.x()) : 0; - replyToVia->resize(width - st::msgReplyBarSkip - previewSkip - replyToName.maxWidth() - st::msgServiceFont->spacew); - } +int HistoryMessageReply::height() const { + return _height + st::historyReplyTop + st::historyReplyBottom; +} + +QMargins HistoryMessageReply::margins() const { + return QMargins(0, st::historyReplyTop, 0, st::historyReplyBottom); } void HistoryMessageReply::itemRemoved( not_null holder, not_null removed) { - if (replyToMsg.get() == removed) { + if (resolvedMessage.get() == removed) { clearData(holder); holder->history()->owner().requestItemResize(holder); } @@ -511,7 +813,7 @@ void HistoryMessageReply::itemRemoved( void HistoryMessageReply::storyRemoved( not_null holder, not_null removed) { - if (replyToStory.get() == removed) { + if (resolvedStory.get() == removed) { clearData(holder); holder->history()->owner().requestItemResize(holder); } @@ -528,57 +830,108 @@ void HistoryMessageReply::paint( const auto st = context.st; const auto stm = context.messageStyle(); - { - const auto opacity = p.opacity(); - const auto outerWidth = w + 2 * x; - const auto &bar = !inBubble - ? st->msgImgReplyBarColor() - : replyToColorKey - ? HistoryView::FromNameFg(context, replyToColorKey) - : stm->msgReplyBarColor; - const auto rbar = style::rtlrect( - x + st::msgReplyBarPos.x(), - y + st::msgReplyPadding.top() + st::msgReplyBarPos.y(), - st::msgReplyBarSize.width(), - st::msgReplyBarSize.height(), - outerWidth); - - if (ripple.animation) { - const auto colorOverride = &stm->msgWaveformInactive->c; - p.setOpacity(st::historyPollRippleOpacity); - ripple.animation->paint( - p, - x - st::msgReplyPadding.left(), - y, - outerWidth, - colorOverride); - if (ripple.animation->empty()) { - ripple.animation.reset(); - } + y += st::historyReplyTop; + const auto rect = QRect(x, y, w, _height); + const auto hasQuote = !_fields.quote.empty(); + const auto selected = context.selected(); + const auto colorPeer = resolvedMessage + ? resolvedMessage->displayFrom() + : resolvedStory + ? resolvedStory->peer().get() + : _externalSender + ? _externalSender + : nullptr; + const auto backgroundEmojiId = colorPeer + ? colorPeer->backgroundEmojiId() + : DocumentId(); + const auto colorIndexPlusOne = colorPeer + ? (colorPeer->colorIndex() + 1) + : resolvedMessage + ? (resolvedMessage->hiddenSenderInfo()->colorIndex + 1) + : 0; + const auto useColorIndex = colorIndexPlusOne && !context.outbg; + const auto colorPattern = colorIndexPlusOne + ? st->colorPatternIndex(colorIndexPlusOne - 1) + : 0; + const auto cache = !inBubble + ? (hasQuote + ? st->serviceQuoteCache(colorPattern) + : st->serviceReplyCache(colorPattern)).get() + : useColorIndex + ? (hasQuote + ? st->coloredQuoteCache(selected, colorIndexPlusOne - 1) + : st->coloredReplyCache(selected, colorIndexPlusOne - 1)).get() + : (hasQuote + ? stm->quoteCache[colorPattern] + : stm->replyCache[colorPattern]).get(); + const auto "eSt = hasQuote + ? st::messageTextStyle.blockquote + : st::messageQuoteStyle; + const auto backgroundEmoji = backgroundEmojiId + ? st->backgroundEmojiData(backgroundEmojiId).get() + : nullptr; + const auto backgroundEmojiCache = backgroundEmoji + ? &backgroundEmoji->caches[Ui::BackgroundEmojiData::CacheIndex( + selected, + context.outbg, + inBubble, + colorIndexPlusOne)] + : nullptr; + const auto rippleColor = cache->bg; + if (!inBubble) { + cache->bg = QColor(0, 0, 0, 0); + } + Ui::Text::ValidateQuotePaintCache(*cache, quoteSt); + Ui::Text::FillQuotePaint(p, rect, *cache, quoteSt); + if (backgroundEmoji) { + ValidateBackgroundEmoji( + backgroundEmojiId, + backgroundEmoji, + backgroundEmojiCache, + cache, + holder); + if (!backgroundEmojiCache->frames[0].isNull()) { + FillBackgroundEmoji(p, rect, hasQuote, *backgroundEmojiCache); } + } + if (!inBubble) { + cache->bg = rippleColor; + } - p.setOpacity(opacity * kBarAlpha); - p.fillRect(rbar, bar); - p.setOpacity(opacity); + if (ripple.animation) { + ripple.animation->paint(p, x, y, w, &rippleColor); + if (ripple.animation->empty()) { + ripple.animation.reset(); + } } + const auto withPreviewLeft = st::messageQuoteStyle.outline + + st::historyReplyPreviewMargin.left() + + st::historyReplyPreview + + st::historyReplyPreviewMargin.right(); + auto textLeft = st::historyReplyPadding.left(); const auto pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler); - if (w > st::msgReplyBarSkip) { - if (replyToMsg || replyToStory) { - const auto media = replyToMsg ? replyToMsg->media() : nullptr; - auto hasPreview = (replyToStory && replyToStory->hasReplyPreview()) || (media && media->hasReplyPreview()); - if (hasPreview && w < st::msgReplyBarSkip + st::msgReplyBarSize.height()) { + if (w > textLeft) { + if (resolvedMessage || resolvedStory || !_text.isEmpty()) { + const auto media = resolvedMessage ? resolvedMessage->media() : nullptr; + auto hasPreview = (media && media->hasReplyPreview()) + || (resolvedStory && resolvedStory->hasReplyPreview()); + if (hasPreview && w <= withPreviewLeft) { hasPreview = false; } - auto previewSkip = hasPreview ? (st::msgReplyBarSize.height() + st::msgReplyBarSkip - st::msgReplyBarSize.width() - st::msgReplyBarPos.x()) : 0; - if (hasPreview) { + textLeft = withPreviewLeft; const auto image = media ? media->replyPreview() - : replyToStory->replyPreview(); + : resolvedStory->replyPreview(); if (image) { - auto to = style::rtlrect(x + st::msgReplyBarSkip, y + st::msgReplyPadding.top() + st::msgReplyBarPos.y(), st::msgReplyBarSize.height(), st::msgReplyBarSize.height(), w + 2 * x); + auto to = style::rtlrect( + x + st::historyReplyPreviewMargin.left(), + y + st::historyReplyPreviewMargin.top(), + st::historyReplyPreview, + st::historyReplyPreview, + w + 2 * x); const auto preview = image->pixSingle( image->size() / style::DevicePixelRatio(), { @@ -601,42 +954,53 @@ void HistoryMessageReply::paint( } } } - if (w > st::msgReplyBarSkip + previewSkip) { + if (w > textLeft + st::historyReplyPadding.right()) { + w -= textLeft + st::historyReplyPadding.right(); p.setPen(!inBubble - ? st->msgImgReplyBarColor() - : replyToColorKey - ? HistoryView::FromNameFg(context, replyToColorKey) - : stm->msgServiceFg); - replyToName.drawLeftElided(p, x + st::msgReplyBarSkip + previewSkip, y + st::msgReplyPadding.top(), w - st::msgReplyBarSkip - previewSkip, w + 2 * x); - if (replyToVia && w > st::msgReplyBarSkip + previewSkip + replyToName.maxWidth() + st::msgServiceFont->spacew) { + ? st->msgImgReplyBarColor()->c + : useColorIndex + ? FromNameFg(context, colorIndexPlusOne - 1) + : stm->msgServiceFg->c); + _name.drawLeftElided(p, x + textLeft, y + st::historyReplyPadding.top(), w, w + 2 * x + 2 * textLeft); + if (originalVia && w > _name.maxWidth() + st::msgServiceFont->spacew) { p.setFont(st::msgServiceFont); - p.drawText(x + st::msgReplyBarSkip + previewSkip + replyToName.maxWidth() + st::msgServiceFont->spacew, y + st::msgReplyPadding.top() + st::msgServiceFont->ascent, replyToVia->text); + p.drawText(x + textLeft + _name.maxWidth() + st::msgServiceFont->spacew, y + st::historyReplyPadding.top() + st::msgServiceFont->ascent, originalVia->text); } p.setPen(inBubble ? stm->historyTextFg : st->msgImgReplyBarColor()); - holder->prepareCustomEmojiPaint(p, context, replyToText); + holder->prepareCustomEmojiPaint(p, context, _text); auto replyToTextPosition = QPoint( - x + st::msgReplyBarSkip + previewSkip, - y + st::msgReplyPadding.top() + st::msgServiceNameFont->height); - const auto replyToTextPalette = &(inBubble - ? stm->replyTextPalette - : st->imgReplyTextPalette()); - if (storyReply) { + x + textLeft, + y + st::historyReplyPadding.top() + st::msgServiceNameFont->height); + auto replyToTextPalette = &(!inBubble + ? st->imgReplyTextPalette() + : useColorIndex + ? st->coloredTextPalette(selected, colorIndexPlusOne - 1) + : stm->replyTextPalette); + if (_fields.storyId) { st::dialogsMiniReplyStory.icon.icon.paint( p, replyToTextPosition, - w - st::msgReplyBarSkip - previewSkip, + w + 2 * x + 2 * textLeft, replyToTextPalette->linkFg->c); replyToTextPosition += QPoint( st::dialogsMiniReplyStory.skipText + st::dialogsMiniReplyStory.icon.icon.width(), 0); } - replyToText.draw(p, { + auto owned = std::optional(); + auto copy = std::optional(); + if (inBubble && colorIndexPlusOne) { + copy.emplace(*replyToTextPalette); + owned.emplace(cache->icon); + copy->linkFg = owned->color(); + replyToTextPalette = &*copy; + } + _text.draw(p, { .position = replyToTextPosition, - .availableWidth = w - st::msgReplyBarSkip - previewSkip, + .availableWidth = w, .palette = replyToTextPalette, .spoiler = Ui::Text::DefaultSpoilerCache(), .now = context.now, @@ -649,18 +1013,26 @@ void HistoryMessageReply::paint( } } else { p.setFont(st::msgDateFont); - p.setPen(inBubble - ? stm->msgDateFg - : st->msgDateImgFg()); - p.drawTextLeft(x + st::msgReplyBarSkip, y + st::msgReplyPadding.top() + (st::msgReplyBarSize.height() - st::msgDateFont->height) / 2, w + 2 * x, st::msgDateFont->elided(statePhrase(), w - st::msgReplyBarSkip)); + p.setPen(cache->icon); + p.drawTextLeft( + x + textLeft, + (y + (_height - st::msgDateFont->height) / 2), + w + 2 * x + 2 * textLeft, + st::msgDateFont->elided( + statePhrase(), + w - textLeft - st::historyReplyPadding.right())); } } } +void HistoryMessageReply::unloadPersistentAnimation() { + _text.unloadPersistentAnimation(); +} + QString HistoryMessageReply::statePhrase() const { - return (replyToMsgId || replyToStoryId) + return ((_fields.messageId || _fields.storyId) && !_unavailable) ? tr::lng_profile_loading(tr::now) - : storyReply + : _fields.storyId ? tr::lng_deleted_story(tr::now) : tr::lng_deleted_message(tr::now); } @@ -668,7 +1040,7 @@ QString HistoryMessageReply::statePhrase() const { void HistoryMessageReply::refreshReplyToMedia() { replyToDocumentId = 0; replyToWebPageId = 0; - if (const auto media = replyToMsg ? replyToMsg->media() : nullptr) { + if (const auto media = resolvedMessage ? resolvedMessage->media() : nullptr) { if (const auto document = media->document()) { replyToDocumentId = document->id; } else if (const auto webpage = media->webpage()) { diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h index d100b42339e63e..4ff73513b18133 100644 --- a/Telegram/SourceFiles/history/history_item_components.h +++ b/Telegram/SourceFiles/history/history_item_components.h @@ -75,7 +75,7 @@ struct HistoryMessageViews : public RuntimeComponent { - QString author; + QString postAuthor; bool isAnonymousRank = false; }; @@ -85,12 +85,15 @@ struct HistoryMessageEdited : public RuntimeComponent colorIndex = {}); QString name; QString firstName; QString lastName; - PeerId colorPeerId = 0; + uint8 colorIndex = 0; Ui::EmptyUserpic emptyUserpic; mutable Data::CloudImage customUserpic; @@ -123,7 +126,7 @@ struct HistoryMessageForwarded : public RuntimeComponent hiddenSenderInfo; - QString originalAuthor; + QString originalPostAuthor; QString psaType; MsgId originalId = 0; mutable Ui::Text::String text = { 1 }; @@ -226,6 +229,26 @@ class ReplyToStoryPointer final { }; +struct ReplyFields { + TextWithEntities quote; + PeerId externalSenderId = 0; + QString externalSenderName; + QString externalPostAuthor; + PeerId externalPeerId = 0; + MsgId messageId = 0; + MsgId topMessageId = 0; + StoryId storyId = 0; + bool topicPost = false; +}; + +[[nodiscard]] ReplyFields ReplyFieldsFromMTP( + not_null history, + const MTPMessageReplyHeader &reply); + +[[nodiscard]] FullReplyTo ReplyToFromMTP( + not_null history, + const MTPInputReplyTo &reply); + struct HistoryMessageReply : public RuntimeComponent { HistoryMessageReply(); @@ -238,18 +261,28 @@ struct HistoryMessageReply static constexpr auto kBarAlpha = 230. / 255.; + void set(ReplyFields fields); + + void updateFields( + not_null holder, + MsgId messageId, + MsgId topMessageId, + bool topicPost); bool updateData(not_null holder, bool force = false); // Must be called before destructor. void clearData(not_null holder); - [[nodiscard]] PeerData *replyToFrom(not_null holder) const; - [[nodiscard]] QString replyToFromName( - not_null holder) const; - [[nodiscard]] QString replyToFromName(not_null peer) const; + [[nodiscard]] PeerData *sender(not_null holder) const; + [[nodiscard]] QString senderName(not_null holder) const; + [[nodiscard]] QString senderName(not_null peer) const; [[nodiscard]] bool isNameUpdated(not_null holder) const; - void updateName(not_null holder) const; - void resize(int width) const; + void updateName( + not_null holder, + std::optional resolvedSender = std::nullopt) const; + [[nodiscard]] int resizeToWidth(int width) const; + [[nodiscard]] int height() const; + [[nodiscard]] QMargins margins() const; void itemRemoved( not_null holder, not_null removed); @@ -265,52 +298,60 @@ struct HistoryMessageReply int y, int w, bool inBubble) const; + void unloadPersistentAnimation(); - [[nodiscard]] PeerId replyToPeer() const { - return replyToPeerId; + [[nodiscard]] PeerId externalPeerId() const { + return _fields.externalPeerId; + } + [[nodiscard]] MsgId messageId() const { + return _fields.messageId; } - [[nodiscard]] MsgId replyToId() const { - return replyToMsgId; + [[nodiscard]] StoryId storyId() const { + return _fields.storyId; } - [[nodiscard]] MsgId replyToTop() const { - return replyToMsgTop; + [[nodiscard]] MsgId topMessageId() const { + return _fields.topMessageId; } - [[nodiscard]] int replyToWidth() const { - return maxReplyWidth; + [[nodiscard]] int maxWidth() const { + return _maxWidth; } - [[nodiscard]] ClickHandlerPtr replyToLink() const { - return replyToLnk; + [[nodiscard]] ClickHandlerPtr link() const { + return _link; + } + [[nodiscard]] bool topicPost() const { + return _fields.topicPost; } [[nodiscard]] QString statePhrase() const; - void setReplyToLinkFrom(not_null holder); + + void setLinkFrom(not_null holder); + void setTopMessageId(MsgId topMessageId); void refreshReplyToMedia(); - PeerId replyToPeerId = 0; - MsgId replyToMsgId = 0; - MsgId replyToMsgTop = 0; - StoryId replyToStoryId = 0; - using ColorKey = PeerId; - ColorKey replyToColorKey = 0; DocumentId replyToDocumentId = 0; WebPageId replyToWebPageId = 0; - ReplyToMessagePointer replyToMsg; - ReplyToStoryPointer replyToStory; - std::unique_ptr replyToVia; + ReplyToMessagePointer resolvedMessage; + ReplyToStoryPointer resolvedStory; + std::unique_ptr originalVia; std::unique_ptr spoiler; - ClickHandlerPtr replyToLnk; - mutable Ui::Text::String replyToName, replyToText; - mutable int replyToVersion = 0; - mutable int maxReplyWidth = 0; - int toWidth = 0; - bool topicPost = false; - bool storyReply = false; - struct final { + struct { mutable std::unique_ptr animation; QPoint lastPoint; } ripple; +private: + ReplyFields _fields; + ClickHandlerPtr _link; + mutable Ui::Text::String _name; + mutable Ui::Text::String _text; + mutable PeerData *_externalSender = nullptr; + mutable int _maxWidth = 0; + mutable int _minHeight = 0; + mutable int _height = 0; + mutable int _nameVersion = 0; + uint8 _unavailable : 1 = 0; + }; struct HistoryMessageTranslation diff --git a/Telegram/SourceFiles/history/history_item_edition.cpp b/Telegram/SourceFiles/history/history_item_edition.cpp index c918d12cb8a5aa..1dfd04184b67ea 100644 --- a/Telegram/SourceFiles/history/history_item_edition.cpp +++ b/Telegram/SourceFiles/history/history_item_edition.cpp @@ -29,6 +29,7 @@ HistoryMessageEdition::HistoryMessageEdition( if (const auto mtpReplies = message.vreplies()) { replies = HistoryMessageRepliesData(mtpReplies); } + invertMedia = message.is_invert_media(); const auto period = message.vttl_period(); ttl = (period && period->v > 0) ? (message.vdate().v + period->v) : 0; diff --git a/Telegram/SourceFiles/history/history_item_edition.h b/Telegram/SourceFiles/history/history_item_edition.h index a46d2f2eed300a..24e98b95163420 100644 --- a/Telegram/SourceFiles/history/history_item_edition.h +++ b/Telegram/SourceFiles/history/history_item_edition.h @@ -30,6 +30,7 @@ struct HistoryMessageEdition { bool useSameMarkup = false; bool useSameReactions = false; bool savePreviousMedia = false; + bool invertMedia = false; TextWithEntities textWithEntities; HistoryMessageMarkupData replyMarkup; HistoryMessageRepliesData replies; diff --git a/Telegram/SourceFiles/history/history_item_helpers.cpp b/Telegram/SourceFiles/history/history_item_helpers.cpp index 99645ae48dd612..a378ef8a947788 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.cpp +++ b/Telegram/SourceFiles/history/history_item_helpers.cpp @@ -193,13 +193,14 @@ bool ShouldSendSilent( && peer->session().settings().supportAllSilent()); } -HistoryItem *LookupReplyTo(not_null history, MsgId replyToId) { - const auto &owner = history->owner(); - return owner.message(history->peer, replyToId); +HistoryItem *LookupReplyTo(not_null history, FullMsgId replyTo) { + return history->owner().message(replyTo); } -MsgId LookupReplyToTop(HistoryItem *replyTo) { - return replyTo ? replyTo->replyToTop() : 0; +MsgId LookupReplyToTop(not_null history, HistoryItem *replyTo) { + return (replyTo && replyTo->history() == history) + ? replyTo->replyToTop() + : 0; } bool LookupReplyIsTopicPost(HistoryItem *replyTo) { @@ -331,9 +332,12 @@ MessageFlags FlagsFromMTP( | ((flags & MTP::f_from_id) ? Flag::HasFromId : Flag()) | ((flags & MTP::f_reply_to) ? Flag::HasReplyInfo : Flag()) | ((flags & MTP::f_reply_markup) ? Flag::HasReplyMarkup : Flag()) - | ((flags & MTP::f_from_scheduled) ? Flag::IsOrWasScheduled : Flag()) + | ((flags & MTP::f_from_scheduled) + ? Flag::IsOrWasScheduled + : Flag()) | ((flags & MTP::f_views) ? Flag::HasViews : Flag()) - | ((flags & MTP::f_noforwards) ? Flag::NoForwards : Flag()); + | ((flags & MTP::f_noforwards) ? Flag::NoForwards : Flag()) + | ((flags & MTP::f_invert_media) ? Flag::InvertMedia : Flag()); } MessageFlags FlagsFromMTP( @@ -361,23 +365,24 @@ MTPMessageReplyHeader NewMessageReplyHeader(const Api::SendAction &action) { MTP_long(peerToUser(replyTo.storyId.peer).bare), MTP_int(replyTo.storyId.story)); } - const auto to = LookupReplyTo(action.history, replyTo.msgId); - if (const auto replyToTop = LookupReplyToTop(to)) { - using Flag = MTPDmessageReplyHeader::Flag; - return MTP_messageReplyHeader( - MTP_flags(Flag::f_reply_to_top_id - | (LookupReplyIsTopicPost(to) - ? Flag::f_forum_topic - : Flag(0))), - MTP_int(replyTo.msgId), - MTPPeer(), - MTP_int(replyToTop)); - } + using Flag = MTPDmessageReplyHeader::Flag; + const auto historyPeer = action.history->peer->id; + const auto externalPeerId = (replyTo.messageId.peer == historyPeer) + ? PeerId() + : replyTo.messageId.peer; + const auto to = LookupReplyTo(action.history, replyTo.messageId); + const auto replyToTop = LookupReplyToTop(action.history, to); return MTP_messageReplyHeader( - MTP_flags(0), - MTP_int(replyTo.msgId), - MTPPeer(), - MTPint()); + MTP_flags(Flag::f_reply_to_msg_id + | (replyToTop ? Flag::f_reply_to_top_id : Flag()) + | (externalPeerId ? Flag::f_reply_to_peer_id : Flag())), + MTP_int(replyTo.messageId.msg), + peerToMTP(externalPeerId), + MTPMessageFwdHeader(), // reply_from + MTPMessageMedia(), // reply_media + MTP_int(replyToTop), // reply_to_top_id + MTPstring(), // quote_text + MTPVector()); // quote_entities } return MTPMessageReplyHeader(); } @@ -454,6 +459,8 @@ MediaCheckResult CheckMessageMedia(const MTPMessageMedia &media) { return data.is_via_mention() ? Result::HasStoryMention : Result::Good; + }, [](const MTPDmessageMediaGiveaway &) { + return Result::Good; }, [](const MTPDmessageMediaUnsupported &) { return Result::Unsupported; }); diff --git a/Telegram/SourceFiles/history/history_item_helpers.h b/Telegram/SourceFiles/history/history_item_helpers.h index 85d03eb25ad9fa..dcb5b949c97dff 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.h +++ b/Telegram/SourceFiles/history/history_item_helpers.h @@ -85,8 +85,10 @@ void RequestDependentMessageStory( const Api::SendOptions &options); [[nodiscard]] HistoryItem *LookupReplyTo( not_null history, - MsgId replyToId); -[[nodiscard]] MsgId LookupReplyToTop(HistoryItem *replyTo); + FullMsgId replyToId); +[[nodiscard]] MsgId LookupReplyToTop( + not_null history, + HistoryItem *replyTo); [[nodiscard]] bool LookupReplyIsTopicPost(HistoryItem *replyTo); struct SendingErrorRequest { diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 6238fbaf698b92..3c6933ec5d624e 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -84,8 +84,10 @@ For license and copyright information please follow this link: #include "history/history_unread_things.h" #include "history/view/controls/history_view_compose_search.h" #include "history/view/controls/history_view_forward_panel.h" +#include "history/view/controls/history_view_draft_options.h" #include "history/view/controls/history_view_voice_record_bar.h" #include "history/view/controls/history_view_ttl_button.h" +#include "history/view/controls/history_view_webpage_processor.h" #include "history/view/reactions/history_view_reactions_button.h" #include "history/view/history_view_cursor_state.h" #include "history/view/history_view_service_message.h" @@ -207,8 +209,6 @@ HistoryWidget::HistoryWidget( , _api(&controller->session().mtp()) , _updateEditTimeLeftDisplay([=] { updateField(); }) , _fieldBarCancel(this, st::historyReplyCancel) -, _previewTimer([=] { requestPreview(); }) -, _previewState(Data::PreviewState::Allowed) , _topBar(this, controller) , _scroll( this, @@ -442,16 +442,6 @@ HistoryWidget::HistoryWidget( if (_supportAutocomplete) { supportInitAutocomplete(); } - _fieldLinksParser = std::make_unique(_field); - _fieldLinksParser->list().changes( - ) | rpl::start_with_next([=](QStringList &&parsed) { - if (_previewState == Data::PreviewState::EmptyOnEdit - && _parsedLinks != parsed) { - _previewState = Data::PreviewState::Allowed; - } - _parsedLinks = std::move(parsed); - checkPreview(); - }, lifetime()); _field->rawTextEdit()->installEventFilter(this); _field->rawTextEdit()->installEventFilter(_fieldAutocomplete); _field->setMimeDataHook([=]( @@ -519,7 +509,7 @@ HistoryWidget::HistoryWidget( if (!_peer || isRecording()) { return false; } - const auto replyTo = (_replyToId && !_editMsgId) + const auto replyTo = (_replyTo && !_editMsgId) ? _replyEditMsg : 0; const auto topic = replyTo ? replyTo->topic() : nullptr; @@ -580,13 +570,6 @@ HistoryWidget::HistoryWidget( }); }, lifetime()); - session().data().webPageUpdates( - ) | rpl::filter([=](not_null page) { - return (_previewData == page.get()); - }) | rpl::start_with_next([=] { - updatePreview(); - }, lifetime()); - session().data().channelDifferenceTooLong( ) | rpl::filter([=](not_null channel) { return _peer == channel.get(); @@ -737,9 +720,9 @@ HistoryWidget::HistoryWidget( return update.flags; }) | rpl::start_with_next([=](Data::PeerUpdate::Flags flags) { if (flags & PeerUpdateFlag::Rights) { - checkPreview(); updateStickersByEmoji(); updateFieldPlaceholder(); + _preview->checkNow(false); } if (flags & PeerUpdateFlag::Migration) { handlePeerMigration(); @@ -858,9 +841,8 @@ HistoryWidget::HistoryWidget( ) | rpl::filter([=](const Api::SendAction &action) { return (action.history == _history); }) | rpl::start_with_next([=](const Api::SendAction &action) { - const auto lastKeyboardUsed = lastForceReplyReplied(FullMsgId( - action.history->peer->id, - action.replyTo.msgId)); + const auto lastKeyboardUsed = lastForceReplyReplied( + action.replyTo.messageId); if (action.replaceMediaOf) { } else if (action.options.scheduled) { cancelReply(lastKeyboardUsed); @@ -919,7 +901,7 @@ Dialogs::EntryState HistoryWidget::computeDialogsEntryState() const { return Dialogs::EntryState{ .key = _history, .section = Dialogs::EntryState::Section::History, - .currentReplyToId = replyToId(), + .currentReplyTo = replyTo(), }; } @@ -1364,7 +1346,7 @@ void HistoryWidget::insertHashtagOrBotCommand( // Send bot command at once, if it was not inserted by pressing Tab. if (str.at(0) == '/' && method != FieldAutocomplete::ChooseMethod::ByTab) { - sendBotCommand({ _peer, str, FullMsgId(), replyToId() }); + sendBotCommand({ _peer, str, FullMsgId(), replyTo() }); session().api().finishForwarding(prepareSendAction({})); setFieldText(_field->getTextWithTagsPart(_field->textCursor().position())); } else { @@ -1615,7 +1597,6 @@ void HistoryWidget::fieldChanged() { updateSendButtonType(); if (!HasSendText(_field)) { - _previewState = Data::PreviewState::Allowed; _fieldIsEmpty = true; } else if (_fieldIsEmpty) { _fieldIsEmpty = false; @@ -1667,27 +1648,30 @@ void HistoryWidget::saveDraft(bool delayed) { } void HistoryWidget::saveFieldToHistoryLocalDraft() { - if (!_history) return; + if (!_history) { + return; + } const auto topicRootId = MsgId(); if (_editMsgId) { _history->setLocalEditDraft(std::make_unique( _field, - _editMsgId, - topicRootId, - _previewState, + FullReplyTo{ + .messageId = FullMsgId(_history->peer->id, _editMsgId), + .topicRootId = topicRootId, + }, + _preview->draft(), _saveEditMsgRequestId)); } else { - if (_replyToId || !_field->empty()) { + if (_replyTo || !_field->empty()) { _history->setLocalDraft(std::make_unique( _field, - _replyToId, - topicRootId, - _previewState)); + _replyTo, + _preview->draft())); } else { - _history->clearLocalDraft({}); + _history->clearLocalDraft(topicRootId); } - _history->clearLocalEditDraft({}); + _history->clearLocalEditDraft(topicRootId); } } @@ -1789,10 +1773,9 @@ bool HistoryWidget::notify_switchInlineBotButtonReceived( }; _history->setLocalDraft(std::make_unique( textWithTags, - 0, // replyTo - 0, // topicRootId + FullReplyTo(), cursor, - Data::PreviewState::Allowed)); + Data::WebPageDraft())); applyDraft(); return true; } @@ -1895,7 +1878,7 @@ bool HistoryWidget::applyDraft(FieldHistoryAction fieldHistoryAction) { ? _history->localDraft({}) : nullptr; auto fieldAvailable = canWriteMessage(); - const auto editMsgId = editDraft ? editDraft->msgId : 0; + const auto editMsgId = editDraft ? editDraft->reply.messageId.msg : 0; if (_voiceRecordBar->isActive() || (!_canSendTexts && !editMsgId)) { if (!_canSendTexts) { clearFieldText(0, fieldHistoryAction); @@ -1908,8 +1891,11 @@ bool HistoryWidget::applyDraft(FieldHistoryAction fieldHistoryAction) { clearFieldText(0, fieldHistoryAction); setInnerFocus(); _processingReplyItem = _replyEditMsg = nullptr; - _processingReplyId = _replyToId = 0; + _processingReplyTo = _replyTo = FullReplyTo(); setEditMsgId(0); + if (_preview) { + _preview->apply({ .removed = true }); + } if (fieldWillBeHiddenAfterEdit) { updateControlsVisibility(); updateControlsGeometry(); @@ -1926,7 +1912,7 @@ bool HistoryWidget::applyDraft(FieldHistoryAction fieldHistoryAction) { | TextUpdateEvent::SendTyping; _processingReplyItem = _replyEditMsg = nullptr; - _processingReplyId = _replyToId = 0; + _processingReplyTo = _replyTo = FullReplyTo(); setEditMsgId(editMsgId); updateCmdStartShown(); updateControlsVisibility(); @@ -1939,22 +1925,23 @@ bool HistoryWidget::applyDraft(FieldHistoryAction fieldHistoryAction) { } } else if (!readyToForward()) { const auto draft = _history->localDraft({}); - _processingReplyId = draft ? draft->msgId : MsgId(); + _processingReplyTo = draft ? draft->reply : FullReplyTo(); processReply(); } - // Save links from _field to _parsedLinks without generating preview. - _previewState = Data::PreviewState::Cancelled; - if (_editMsgId) { - _fieldLinksParser->setDisabled(!_replyEditMsg - || (_replyEditMsg->media() - && !_replyEditMsg->media()->webpage())); + if (_preview) { + _preview->setDisabled(_editMsgId + && _replyEditMsg + && _replyEditMsg->media() + && !_replyEditMsg->media()->webpage()); + if (!_editMsgId) { + _preview->apply(draft->webpage, true); + } else if (!_replyEditMsg + || !_replyEditMsg->media() + || _replyEditMsg->media()->webpage()) { + _preview->apply(draft->webpage, false); + } } - _fieldLinksParser->parseNow(); - _parsedLinks = _fieldLinksParser->list().current(); - _previewState = draft->previewState; - checkPreview(); - return true; } @@ -2157,12 +2144,11 @@ void HistoryWidget::showHistory( _saveEditMsgRequestId = 0; _processingReplyItem = _replyEditMsg = nullptr; - _processingReplyId = _editMsgId = _replyToId = 0; + _processingReplyTo = _replyTo = FullReplyTo(); + _editMsgId = MsgId(); _canReplaceMedia = false; _photoEditMedia = nullptr; updateReplaceMediaButton(); - _previewData = nullptr; - _previewCache.clear(); _fieldBarCancel->hide(); _membersDropdownShowTimer.cancel(); @@ -2420,10 +2406,45 @@ void HistoryWidget::setHistory(History *history) { _history = history; _migrated = _history ? _history->migrateFrom() : nullptr; registerDraftSource(); + if (_history) { + setupPreview(); + } else { + _previewDrawPreview = nullptr; + _preview = nullptr; + } } refreshAttachBotsMenu(); } +void HistoryWidget::setupPreview() { + Expects(_history != nullptr); + + using namespace HistoryView::Controls; + _preview = std::make_unique(_history, _field); + _preview->repaintRequests() | rpl::start_with_next([=] { + updateField(); + }, _preview->lifetime()); + + _preview->parsedValue( + ) | rpl::start_with_next([=](WebpageParsed value) { + _previewTitle.setText( + st::msgNameStyle, + value.title, + Ui::NameTextOptions()); + _previewDescription.setText( + st::defaultTextStyle, + value.description, + Ui::DialogTextOptions()); + const auto changed = (!_previewDrawPreview != !value.drawPreview); + _previewDrawPreview = value.drawPreview; + if (changed) { + updateControlsGeometry(); + updateControlsVisibility(); + } + updateField(); + }, _preview->lifetime()); +} + void HistoryWidget::injectSponsoredMessages() const { session().data().sponsoredMessages().inject( _history, @@ -2471,12 +2492,15 @@ void HistoryWidget::registerDraftSource() { if (!_history) { return; } + const auto peerId = _history->peer->id; const auto editMsgId = _editMsgId; const auto draft = [=] { return Storage::MessageDraft{ - editMsgId ? editMsgId : _replyToId, + (editMsgId + ? FullReplyTo{ FullMsgId(peerId, editMsgId) } + : _replyTo), _field->getTextWithTags(), - _previewState, + _preview->draft(), }; }; auto draftSource = Storage::MessageDraftSource{ @@ -2494,11 +2518,11 @@ void HistoryWidget::registerDraftSource() { void HistoryWidget::setEditMsgId(MsgId msgId) { unregisterDraftSources(); _editMsgId = msgId; - if (_fieldLinksParser && !_editMsgId) { - _fieldLinksParser->setDisabled(false); - } if (!msgId) { _canReplaceMedia = false; + if (_preview) { + _preview->setDisabled(false); + } } if (_history) { refreshSendAsToggle(); @@ -2911,7 +2935,11 @@ void HistoryWidget::updateControlsVisibility() { } updateFieldPlaceholder(); - if (_editMsgId || _replyToId || readyToForward() || (_previewData && _previewData->pendingTill >= 0) || _kbReplyTo) { + if (_editMsgId + || _replyTo + || readyToForward() + || _previewDrawPreview + || _kbReplyTo) { if (_fieldBarCancel->isHidden()) { _fieldBarCancel->show(); updateControlsGeometry(); @@ -2948,7 +2976,7 @@ void HistoryWidget::updateControlsVisibility() { _botMenuButton->hide(); } _kbScroll->hide(); - if (_replyToId || readyToForward() || _kbReplyTo) { + if (_replyTo || readyToForward() || _kbReplyTo) { if (_fieldBarCancel->isHidden()) { _fieldBarCancel->show(); updateControlsGeometry(); @@ -3767,12 +3795,7 @@ void HistoryWidget::saveEditMsg() { cancelEdit(); return; } - const auto webPageId = (_previewState != Data::PreviewState::Allowed) - ? CancelledWebPageId - : ((_previewData && _previewData->pendingTill >= 0) - ? _previewData->id - : WebPageId(0)); - + const auto webPageDraft = _preview->draft(); const auto textWithTags = _field->getTextWithAppliedMarkdown(); const auto prepareFlags = Ui::ItemTextOptions( _history, @@ -3783,8 +3806,12 @@ void HistoryWidget::saveEditMsg() { TextUtilities::ConvertTextTagsToEntities(textWithTags.tags) }; TextUtilities::PrepareForSending(left, prepareFlags); + const auto media = item->media(); if (!TextUtilities::CutPart(sending, left, MaxMessageSize) - && (!item->media() || !item->media()->allowsEditCaption())) { + && (webPageDraft.removed + || webPageDraft.url.isEmpty() + || !webPageDraft.manual) + && (!media || !media->allowsEditCaption())) { const auto suggestModerateActions = false; controller()->show( Box(item, suggestModerateActions)); @@ -3839,10 +3866,10 @@ void HistoryWidget::saveEditMsg() { }; auto options = Api::SendOptions(); - options.removeWebPageId = (webPageId == CancelledWebPageId); _saveEditMsgRequestId = Api::EditTextMessage( item, sending, + webPageDraft, options, done, fail); @@ -3899,7 +3926,7 @@ void HistoryWidget::hideSelectorControlsAnimated() { Api::SendAction HistoryWidget::prepareSendAction( Api::SendOptions options) const { auto result = Api::SendAction(_history, options); - result.replyTo = { .msgId = replyToId() }; + result.replyTo = replyTo(); result.options.sendAs = _sendAs ? _history->session().sendAsPeers().resolveChosen( _history->peer).get() @@ -3926,15 +3953,9 @@ void HistoryWidget::send(Api::SendOptions options) { _cornerButtons.clearReplyReturns(); } - const auto webPageId = (_previewState != Data::PreviewState::Allowed) - ? CancelledWebPageId - : ((_previewData && _previewData->pendingTill >= 0) - ? _previewData->id - : WebPageId(0)); - auto message = Api::MessageToSend(prepareSendAction(options)); message.textWithTags = _field->getTextWithAppliedMarkdown(); - message.webPageId = webPageId; + message.webPage = _preview->draft(); const auto ignoreSlowmodeCountdown = (options.scheduled != 0); if (showSendMessageError( @@ -3945,13 +3966,15 @@ void HistoryWidget::send(Api::SendOptions options) { session().api().sendMessage(std::move(message)); clearFieldText(); + if (_preview) { + _preview->apply({ .removed = true }); + } _saveDraftText = true; _saveDraftStart = crl::now(); saveDraft(); hideSelectorControlsAnimated(); - if (_previewData && _previewData->pendingTill) previewCancel(); setInnerFocus(); if (!_keyboard->hasMarkup() && _keyboard->forceReply() && !_kbReplyTo) { @@ -4342,21 +4365,23 @@ void HistoryWidget::mouseMoveEvent(QMouseEvent *e) { void HistoryWidget::updateOverStates(QPoint pos) { const auto isReadyToForward = readyToForward(); const auto skip = isReadyToForward ? 0 : st::historyReplySkip; - const auto replyEditForwardInfoRect = QRect( + const auto detailsRect = QRect( skip, _field->y() - st::historySendPadding - st::historyReplyHeight, width() - skip - _fieldBarCancel->width(), st::historyReplyHeight); - auto inReplyEditForward = (_editMsgId || replyToId() || isReadyToForward) - && replyEditForwardInfoRect.contains(pos); - auto inPhotoEdit = inReplyEditForward + const auto hasWebPage = !!_previewDrawPreview; + const auto inDetails = detailsRect.contains(pos) + && (_editMsgId || replyTo() || isReadyToForward || hasWebPage); + const auto inPhotoEdit = inDetails && _photoEditMedia && QRect( - replyEditForwardInfoRect.x(), - replyEditForwardInfoRect.y() + st::msgReplyPadding.top(), - st::msgReplyBarSize.height(), - st::msgReplyBarSize.height()).contains(pos); - auto inClickable = inReplyEditForward; + detailsRect.x(), + (detailsRect.y() + + (detailsRect.height() - st::historyReplyPreview) / 2), + st::historyReplyPreview, + st::historyReplyPreview).contains(pos); + const auto inClickable = inDetails; if (_inPhotoEdit != inPhotoEdit) { _inPhotoEdit = inPhotoEdit; if (_photoEditMedia) { @@ -4369,7 +4394,7 @@ void HistoryWidget::updateOverStates(QPoint pos) { _inPhotoEditOver.stop(); } } - _inReplyEditForward = inReplyEditForward && !inPhotoEdit; + _inDetails = inDetails && !inPhotoEdit; if (inClickable != _inClickable) { _inClickable = inClickable; setCursor(_inClickable ? style::cur_pointer : style::cur_default); @@ -4397,9 +4422,9 @@ void HistoryWidget::sendBotCommand(const Bot::SendCommandRequest &request) { return; } - const auto lastKeyboardUsed = (_keyboard->forMsgId() - == FullMsgId(_peer->id, _history->lastKeyboardId)) - && (_keyboard->forMsgId() == FullMsgId(_peer->id, request.replyTo)); + const auto forMsgId = _keyboard->forMsgId(); + const auto lastKeyboardUsed = (forMsgId == request.replyTo.messageId) + && (forMsgId == FullMsgId(_peer->id, _history->lastKeyboardId)); // 'bot' may be nullptr in case of sending from FieldAutocomplete. const auto toSend = (request.replyTo/* || !bot*/) @@ -4408,14 +4433,14 @@ void HistoryWidget::sendBotCommand(const Bot::SendCommandRequest &request) { auto message = Api::MessageToSend(prepareSendAction({})); message.textWithTags = { toSend, TextWithTags::Tags() }; - message.action.replyTo.msgId = request.replyTo + message.action.replyTo = request.replyTo ? ((!_peer->isUser()/* && (botStatus == 0 || botStatus == 2)*/) ? request.replyTo - : replyToId()) - : 0; + : replyTo()) + : FullReplyTo(); session().api().sendMessage(std::move(message)); if (request.replyTo) { - if (_replyToId == request.replyTo) { + if (_replyTo == request.replyTo) { cancelReply(); saveCloudDraft(); } @@ -4428,18 +4453,25 @@ void HistoryWidget::sendBotCommand(const Bot::SendCommandRequest &request) { setInnerFocus(); } -void HistoryWidget::hideSingleUseKeyboard(PeerData *peer, MsgId replyTo) { - if (!_peer || _peer != peer) return; +void HistoryWidget::hideSingleUseKeyboard(FullMsgId replyToId) { + if (!_peer || _peer->id != replyToId.peer) { + return; + } - bool lastKeyboardUsed = (_keyboard->forMsgId() == FullMsgId(_peer->id, _history->lastKeyboardId)) - && (_keyboard->forMsgId() == FullMsgId(_peer->id, replyTo)); - if (replyTo) { - if (_replyToId == replyTo) { + bool lastKeyboardUsed = (_keyboard->forMsgId() == replyToId) + && (_keyboard->forMsgId() + == FullMsgId(_peer->id, _history->lastKeyboardId)); + if (replyToId) { + if (_replyTo.messageId == replyToId) { cancelReply(); saveCloudDraft(); } - if (_keyboard->singleUse() && _keyboard->hasMarkup() && lastKeyboardUsed) { - if (_kbShown) toggleKeyboard(false); + if (_keyboard->singleUse() + && _keyboard->hasMarkup() + && lastKeyboardUsed) { + if (_kbShown) { + toggleKeyboard(false); + } _history->lastKeyboardUsed = true; } } @@ -4600,6 +4632,7 @@ bool HistoryWidget::showRecordButton() const { && !_voiceRecordBar->isListenState() && !_voiceRecordBar->isRecordingByAnotherBar() && !HasSendText(_field) + && !_previewDrawPreview && !readyToForward() && !_editMsgId; } @@ -4782,7 +4815,10 @@ void HistoryWidget::toggleKeyboard(bool manual) { _field->setMaxHeight(computeMaxFieldHeight()); _kbReplyTo = nullptr; - if (!readyToForward() && (!_previewData || _previewData->pendingTill < 0) && !_editMsgId && !_replyToId) { + if (!readyToForward() + && !_previewDrawPreview + && !_editMsgId + && !_replyTo) { _fieldBarCancel->hide(); updateMouseTracking(); } @@ -4807,7 +4843,7 @@ void HistoryWidget::toggleKeyboard(bool manual) { _kbReplyTo = (_peer->isChat() || _peer->isChannel() || _keyboard->forceReply()) ? session().data().message(_keyboard->forMsgId()) : nullptr; - if (_kbReplyTo && !_editMsgId && !_replyToId && fieldEnabled) { + if (_kbReplyTo && !_editMsgId && !_replyTo && fieldEnabled) { updateReplyToName(); updateReplyEditText(_kbReplyTo); } @@ -4827,7 +4863,7 @@ void HistoryWidget::toggleKeyboard(bool manual) { _kbReplyTo = (_peer->isChat() || _peer->isChannel() || _keyboard->forceReply()) ? session().data().message(_keyboard->forMsgId()) : nullptr; - if (_kbReplyTo && !_editMsgId && !_replyToId) { + if (_kbReplyTo && !_editMsgId && !_replyTo) { updateReplyToName(); updateReplyEditText(_kbReplyTo); } @@ -5599,11 +5635,11 @@ void HistoryWidget::itemRemoved(not_null item) { if (item == _replyEditMsg && _editMsgId) { cancelEdit(); } - if (item == _replyEditMsg && _replyToId) { + if (item == _replyEditMsg && _replyTo) { cancelReply(); } if (item == _processingReplyItem) { - _processingReplyId = 0; + _processingReplyTo = {}; _processingReplyItem = nullptr; } if (_kbReplyTo && item == _kbReplyTo) { @@ -5627,8 +5663,12 @@ void HistoryWidget::itemEdited(not_null item) { } } -MsgId HistoryWidget::replyToId() const { - return _replyToId ? _replyToId : (_kbReplyTo ? _kbReplyTo->id : 0); +FullReplyTo HistoryWidget::replyTo() const { + return _replyTo + ? _replyTo + : _kbReplyTo + ? FullReplyTo{ _kbReplyTo->fullId() } + : FullReplyTo(); } bool HistoryWidget::hasSavedScroll() const { @@ -5763,7 +5803,10 @@ void HistoryWidget::updateHistoryGeometry( } else if (writeRestriction().has_value()) { newScrollHeight -= _unblock->height(); } - if (_editMsgId || replyToId() || readyToForward() || (_previewData && _previewData->pendingTill >= 0)) { + if (_editMsgId + || replyTo() + || readyToForward() + || _previewDrawPreview) { newScrollHeight -= st::historyReplyHeight; } if (_kbShown) { @@ -6001,9 +6044,9 @@ void HistoryWidget::updateBotKeyboard(History *h, bool force) { const auto wasVisible = _kbShown || _kbReplyTo; const auto wasMsgId = _keyboard->forMsgId(); auto changed = false; - if ((_replyToId && !_replyEditMsg) || _editMsgId || !_history) { + if ((_replyTo && !_replyEditMsg) || _editMsgId || !_history) { changed = _keyboard->updateMarkup(nullptr, force); - } else if (_replyToId && _replyEditMsg) { + } else if (_replyTo && _replyEditMsg) { changed = _keyboard->updateMarkup(_replyEditMsg, force); } else { const auto keyboardItem = _history->lastKeyboardId @@ -6020,7 +6063,7 @@ void HistoryWidget::updateBotKeyboard(History *h, bool force) { _kbScroll->scrollTo({ 0, 0 }); } - bool hasMarkup = _keyboard->hasMarkup(), forceReply = _keyboard->forceReply() && (!_replyToId || !_replyEditMsg); + bool hasMarkup = _keyboard->hasMarkup(), forceReply = _keyboard->forceReply() && (!_replyTo || !_replyEditMsg); if (hasMarkup || forceReply) { if (_keyboard->singleUse() && _keyboard->hasMarkup() @@ -6029,7 +6072,7 @@ void HistoryWidget::updateBotKeyboard(History *h, bool force) { && _history->lastKeyboardUsed) { _history->lastKeyboardHiddenId = _history->lastKeyboardId; } - if (!isSearching() && !isBotStart() && !isBlocked() && _canSendMessages && (wasVisible || (_replyToId && _replyEditMsg) || (!HasSendText(_field) && !kbWasHidden()))) { + if (!isSearching() && !isBotStart() && !isBlocked() && _canSendMessages && (wasVisible || (_replyTo && _replyEditMsg) || (!HasSendText(_field) && !kbWasHidden()))) { if (!_showAnimation) { if (hasMarkup) { _kbScroll->show(); @@ -6050,7 +6093,7 @@ void HistoryWidget::updateBotKeyboard(History *h, bool force) { _kbReplyTo = (_peer->isChat() || _peer->isChannel() || _keyboard->forceReply()) ? session().data().message(_keyboard->forMsgId()) : nullptr; - if (_kbReplyTo && !_replyToId) { + if (_kbReplyTo && !_replyTo) { updateReplyToName(); updateReplyEditText(_kbReplyTo); } @@ -6065,7 +6108,9 @@ void HistoryWidget::updateBotKeyboard(History *h, bool force) { _field->setMaxHeight(computeMaxFieldHeight()); _kbShown = false; _kbReplyTo = nullptr; - if (!readyToForward() && (!_previewData || _previewData->pendingTill < 0) && !_replyToId) { + if (!readyToForward() + && !_previewDrawPreview + && !_replyTo) { _fieldBarCancel->hide(); updateMouseTracking(); } @@ -6081,7 +6126,10 @@ void HistoryWidget::updateBotKeyboard(History *h, bool force) { _field->setMaxHeight(computeMaxFieldHeight()); _kbShown = false; _kbReplyTo = nullptr; - if (!readyToForward() && (!_previewData || _previewData->pendingTill < 0) && !_replyToId && !_editMsgId) { + if (!readyToForward() + && !_previewDrawPreview + && !_replyTo + && !_editMsgId) { _fieldBarCancel->hide(); updateMouseTracking(); } @@ -6103,7 +6151,7 @@ void HistoryWidget::botCallbackSent(not_null item) { session().data().requestItemRepaint(item); - if (_replyToId == item->id) { + if (_replyTo.messageId == item->fullId()) { cancelReply(); } if (_keyboard->singleUse() @@ -6124,9 +6172,9 @@ int HistoryWidget::computeMaxFieldHeight() const { - (_groupCallBar ? _groupCallBar->height() : 0) - (_requestsBar ? _requestsBar->height() : 0) - ((_editMsgId - || replyToId() + || replyTo() || readyToForward() - || (_previewData && _previewData->pendingTill >= 0)) + || _previewDrawPreview) ? st::historyReplyHeight : 0) - (2 * st::historySendPadding) @@ -6185,7 +6233,7 @@ bool HistoryWidget::cornerButtonsHas(HistoryView::CornerButtonType type) { void HistoryWidget::mousePressEvent(QMouseEvent *e) { const auto isReadyToForward = readyToForward(); const auto hasSecondLayer = (_editMsgId - || _replyToId + || _replyTo || isReadyToForward || _kbReplyTo); _replyForwardPressed = hasSecondLayer && QRect( @@ -6204,22 +6252,70 @@ void HistoryWidget::mousePressEvent(QMouseEvent *e) { { _history->peer->id, _editMsgId }, _field->getTextWithTags(), crl::guard(_list, [=] { cancelEdit(); })); - } else if (_inReplyEditForward) { - if (isReadyToForward) { - if (e->button() != Qt::LeftButton) { - _forwardPanel->editToNextOption(); - } else { - _forwardPanel->editOptions(controller()->uiShow()); - } + } else if (!_inDetails) { + return; + } else if (_previewDrawPreview) { + editDraftOptions(); + } else if (_editMsgId) { + controller()->showPeerHistory( + _peer, + Window::SectionShow::Way::Forward, + _editMsgId); + } else if (isReadyToForward) { + if (e->button() != Qt::LeftButton) { + _forwardPanel->editToNextOption(); } else { - controller()->showPeerHistory( - _peer, - Window::SectionShow::Way::Forward, - _editMsgId ? _editMsgId : replyToId()); + _forwardPanel->editOptions(controller()->uiShow()); } + } else if (_replyTo) { + editDraftOptions(); + } else if (_kbReplyTo) { + controller()->showPeerHistory( + _kbReplyTo->history()->peer->id, + Window::SectionShow::Way::Forward, + _kbReplyTo->id); } } +void HistoryWidget::editDraftOptions() { + Expects(_history != nullptr); + + const auto history = _history; + const auto reply = _replyTo; + const auto webpage = _preview->draft(); + + const auto done = [=]( + FullReplyTo replyTo, + Data::WebPageDraft webpage) { + if (replyTo) { + replyToMessage(replyTo); + } else { + cancelReply(); + } + _preview->apply(webpage); + }; + const auto replyToId = reply.messageId; + const auto highlight = [=] { + controller()->showPeerHistory( + replyToId.peer, + Window::SectionShow::Way::Forward, + replyToId.msg); + }; + + using namespace HistoryView::Controls; + EditDraftOptions({ + .show = controller()->uiShow(), + .history = history, + .draft = Data::Draft(_field, reply, _preview->draft()), + .usedLink = _preview->link(), + .links = _preview->links(), + .resolver = _preview->resolver(), + .done = done, + .highlight = highlight, + .clearOldDraft = [=] { ClearDraftReplyTo(history, replyToId); }, + }); +} + void HistoryWidget::keyPressEvent(QKeyEvent *e) { if (!_history) return; @@ -6245,7 +6341,7 @@ void HistoryWidget::keyPressEvent(QKeyEvent *e) { if (item && _field->empty() && !_editMsgId - && !_replyToId) { + && !_replyTo) { editMessage(item); return; } @@ -6313,14 +6409,17 @@ void HistoryWidget::handlePeerMigration() { } bool HistoryWidget::replyToPreviousMessage() { - if (!_history || _editMsgId || _history->isForum()) { + if (!_history + || _editMsgId + || _history->isForum() + || (_replyTo && _replyTo.messageId.peer != _history->peer->id)) { return false; } const auto fullId = FullMsgId( _history->peer->id, - _field->isVisible() - ? _replyToId - : _highlighter.latestSingleHighlightedMsgId()); + (_field->isVisible() + ? _replyTo.messageId.msg + : _highlighter.latestSingleHighlightedMsgId())); if (const auto item = session().data().message(fullId)) { if (const auto view = item->mainView()) { if (const auto previousView = view->previousDisplayedInBlocks()) { @@ -6344,14 +6443,17 @@ bool HistoryWidget::replyToPreviousMessage() { } bool HistoryWidget::replyToNextMessage() { - if (!_history || _editMsgId || _history->isForum()) { + if (!_history + || _editMsgId + || _history->isForum() + || (_replyTo && _replyTo.messageId.peer != _history->peer->id)) { return false; } const auto fullId = FullMsgId( _history->peer->id, - _field->isVisible() - ? _replyToId - : _highlighter.latestSingleHighlightedMsgId()); + (_field->isVisible() + ? _replyTo.messageId.msg + : _highlighter.latestSingleHighlightedMsgId())); if (const auto item = session().data().message(fullId)) { if (const auto view = item->mainView()) { if (const auto nextView = view->nextDisplayedInBlocks()) { @@ -7024,8 +7126,9 @@ void HistoryWidget::setFieldText( _textUpdateEvents = TextUpdateEvent::SaveDraft | TextUpdateEvent::SendTyping; - previewCancel(); - _previewState = Data::PreviewState::Allowed; + if (_preview) { + _preview->checkNow(false); + } } void HistoryWidget::clearFieldText( @@ -7034,17 +7137,19 @@ void HistoryWidget::clearFieldText( setFieldText(TextWithTags(), events, fieldHistoryAction); } -void HistoryWidget::replyToMessage(FullMsgId itemId) { - if (const auto item = session().data().message(itemId)) { - replyToMessage(item); +void HistoryWidget::replyToMessage(FullReplyTo id) { + if (const auto item = session().data().message(id.messageId)) { + replyToMessage(item, id.quote); } } -void HistoryWidget::replyToMessage(not_null item) { +void HistoryWidget::replyToMessage( + not_null item, + TextWithEntities quote) { if (isJoinChannel()) { return; } - _processingReplyId = item->id; + _processingReplyTo = { .messageId = item->fullId(), .quote = quote}; _processingReplyItem = item; processReply(); } @@ -7052,14 +7157,13 @@ void HistoryWidget::replyToMessage(not_null item) { void HistoryWidget::processReply() { const auto processContinue = [=] { return crl::guard(_list, [=] { - if (!_peer || !_processingReplyId) { + if (!_peer || !_processingReplyTo) { return; } else if (!_processingReplyItem) { _processingReplyItem = _peer->owner().message( - _peer, - _processingReplyId); + _processingReplyTo.messageId); if (!_processingReplyItem) { - _processingReplyId = 0; + _processingReplyTo = {}; } else { processReply(); } @@ -7067,18 +7171,19 @@ void HistoryWidget::processReply() { }); }; const auto processCancel = [=] { - _processingReplyId = 0; + _processingReplyTo = {}; _processingReplyItem = nullptr; }; - if (!_peer || !_processingReplyId) { + if (!_peer || !_processingReplyTo) { return processCancel(); } else if (!_processingReplyItem) { session().api().requestMessageData( - _peer, - _processingReplyId, + session().data().peer(_processingReplyTo.messageId.peer), + _processingReplyTo.messageId.msg, processContinue()); return; +#if 0 // Now we can "reply" to old legacy group messages. } else if (_processingReplyItem->history() == _migrated) { if (_processingReplyItem->isService()) { controller()->showToast(tr::lng_reply_cant(tr::now)); @@ -7096,10 +7201,11 @@ void HistoryWidget::processReply() { })); } return processCancel(); - } else if (_processingReplyItem->history() != _history - || !_processingReplyItem->isRegular()) { +#endif + } else if (!_processingReplyItem->isRegular()) { return processCancel(); - } else if (const auto forum = _peer->forum()) { + } else if (const auto forum = _peer->forum() + ; forum && _processingReplyItem->history() == _history) { const auto topicRootId = _processingReplyItem->topicRootId(); if (forum->topicDeleted(topicRootId)) { return processCancel(); @@ -7117,7 +7223,7 @@ void HistoryWidget::processReply() { } void HistoryWidget::setReplyFieldsFromProcessing() { - if (!_processingReplyId || !_processingReplyItem) { + if (!_processingReplyTo || !_processingReplyItem) { return; } @@ -7126,22 +7232,21 @@ void HistoryWidget::setReplyFieldsFromProcessing() { _composeSearch->hideAnimated(); } - const auto id = base::take(_processingReplyId); + const auto id = base::take(_processingReplyTo); const auto item = base::take(_processingReplyItem); if (_editMsgId) { if (const auto localDraft = _history->localDraft({})) { - localDraft->msgId = id; + localDraft->reply = id; } else { _history->setLocalDraft(std::make_unique( TextWithTags(), id, - MsgId(), MessageCursor(), - Data::PreviewState::Allowed)); + Data::WebPageDraft())); } } else { _replyEditMsg = item; - _replyToId = id; + _replyTo = id; updateReplyEditText(_replyEditMsg); updateCanSendMessage(); updateBotKeyboard(); @@ -7180,12 +7285,11 @@ void HistoryWidget::editMessage(not_null item) { _send->clearState(); } if (!_editMsgId) { - if (_replyToId || !_field->empty()) { + if (_replyTo || !_field->empty()) { _history->setLocalDraft(std::make_unique( _field, - _replyToId, - MsgId(), // topicRootId - _previewState)); + _replyTo, + _preview->draft())); } else { _history->clearLocalDraft({}); } @@ -7197,28 +7301,14 @@ void HistoryWidget::editMessage(not_null item) { int(editData.text.size()), Ui::kQFixedMax }; - const auto previewPage = [&]() -> WebPageData* { - if (const auto media = item->media()) { - return media->webpage(); - } - return nullptr; - }(); - const auto previewState = previewPage - ? Data::PreviewState::Allowed - : Data::PreviewState::EmptyOnEdit; + const auto previewDraft = Data::WebPageDraft::FromItem(item); _history->setLocalEditDraft(std::make_unique( editData, - item->id, - MsgId(), // topicRootId + FullReplyTo{ item->fullId() }, cursor, - previewState)); + previewDraft)); applyDraft(); - _previewData = previewPage; - if (_previewData) { - updatePreview(); - } - updateBotKeyboard(); if (fieldOrDisabledShown()) { @@ -7271,19 +7361,22 @@ bool HistoryWidget::lastForceReplyReplied(const FullMsgId &replyTo) const { bool HistoryWidget::lastForceReplyReplied() const { return _peer && _keyboard->forceReply() - && _keyboard->forMsgId() == FullMsgId(_peer->id, _history->lastKeyboardId) - && _keyboard->forMsgId().msg == replyToId(); + && _keyboard->forMsgId() == replyTo().messageId + && (_keyboard->forMsgId() + == FullMsgId(_peer->id, _history->lastKeyboardId)); } bool HistoryWidget::cancelReply(bool lastKeyboardUsed) { bool wasReply = false; - if (_replyToId) { + if (_replyTo) { wasReply = true; _processingReplyItem = _replyEditMsg = nullptr; - _processingReplyId = _replyToId = 0; + _processingReplyTo = _replyTo = FullReplyTo(); mouseMoveEvent(0); - if (!readyToForward() && (!_previewData || _previewData->pendingTill < 0) && !_kbReplyTo) { + if (!readyToForward() + && !_previewDrawPreview + && !_kbReplyTo) { _fieldBarCancel->hide(); updateMouseTracking(); } @@ -7294,11 +7387,11 @@ bool HistoryWidget::cancelReply(bool lastKeyboardUsed) { updateControlsGeometry(); update(); } else if (const auto localDraft = (_history ? _history->localDraft({}) : nullptr)) { - if (localDraft->msgId) { + if (localDraft->reply) { if (localDraft->textWithTags.text.isEmpty()) { _history->clearLocalDraft({}); } else { - localDraft->msgId = 0; + localDraft->reply = {}; } } } @@ -7354,7 +7447,9 @@ void HistoryWidget::cancelEdit() { saveDraft(); mouseMoveEvent(nullptr); - if (!readyToForward() && (!_previewData || _previewData->pendingTill < 0) && !replyToId()) { + if (!readyToForward() + && !_previewDrawPreview + && !replyTo()) { _fieldBarCancel->hide(); updateMouseTracking(); } @@ -7375,161 +7470,19 @@ void HistoryWidget::cancelEdit() { void HistoryWidget::cancelFieldAreaState() { controller()->hideLayer(); _replyForwardPressed = false; - if (_previewData && _previewData->pendingTill >= 0) { - _previewState = Data::PreviewState::Cancelled; - previewCancel(); - - _saveDraftText = true; - _saveDraftStart = crl::now(); - saveDraft(); + if (_previewDrawPreview) { + _preview->apply({ .removed = true }); } else if (_editMsgId) { cancelEdit(); } else if (readyToForward()) { _history->setForwardDraft(MsgId(), {}); - } else if (_replyToId) { + } else if (_replyTo) { cancelReply(); } else if (_kbReplyTo) { toggleKeyboard(); } } -void HistoryWidget::previewCancel() { - _api.request(base::take(_previewRequest)).cancel(); - _previewData = nullptr; - _previewLinks.clear(); - updatePreview(); -} - -void HistoryWidget::checkPreview() { - const auto previewRestricted = [&] { - return _peer && _peer->amRestricted(ChatRestriction::EmbedLinks); - }(); - if (_previewState != Data::PreviewState::Allowed || previewRestricted) { - previewCancel(); - return; - } - const auto links = _parsedLinks.join(' '); - if (_previewLinks != links) { - _api.request(base::take(_previewRequest)).cancel(); - _previewLinks = links; - if (_previewLinks.isEmpty()) { - if (_previewData && _previewData->pendingTill >= 0) { - previewCancel(); - } - } else { - const auto i = _previewCache.constFind(links); - if (i == _previewCache.cend()) { - _previewRequest = _api.request(MTPmessages_GetWebPagePreview( - MTP_flags(0), - MTP_string(links), - MTPVector() - )).done([=](const MTPMessageMedia &result, mtpRequestId requestId) { - gotPreview(links, result, requestId); - }).send(); - } else if (i.value()) { - _previewData = session().data().webpage(i.value()); - updatePreview(); - } else if (_previewData && _previewData->pendingTill >= 0) { - previewCancel(); - } - } - } -} - -void HistoryWidget::requestPreview() { - if (!_previewData - || (_previewData->pendingTill <= 0) - || _previewLinks.isEmpty()) { - return; - } - const auto links = _previewLinks; - _previewRequest = _api.request(MTPmessages_GetWebPagePreview( - MTP_flags(0), - MTP_string(links), - MTPVector() - )).done([=](const MTPMessageMedia &result, mtpRequestId requestId) { - gotPreview(links, result, requestId); - }).send(); -} - -void HistoryWidget::gotPreview( - QString links, - const MTPMessageMedia &result, - mtpRequestId req) { - if (req == _previewRequest) { - _previewRequest = 0; - } - if (result.type() == mtpc_messageMediaWebPage) { - const auto &data = result.c_messageMediaWebPage().vwebpage(); - const auto page = session().data().processWebpage(data); - _previewCache.insert(links, page->id); - if (page->pendingTill > 0 - && page->pendingTill <= base::unixtime::now()) { - page->pendingTill = -1; - } - if (links == _previewLinks - && _previewState == Data::PreviewState::Allowed) { - _previewData = (page->id && page->pendingTill >= 0) - ? page.get() - : nullptr; - updatePreview(); - } - session().data().sendWebPageGamePollNotifications(); - } else if (result.type() == mtpc_messageMediaEmpty) { - _previewCache.insert(links, 0); - if (links == _previewLinks - && _previewState == Data::PreviewState::Allowed) { - _previewData = nullptr; - updatePreview(); - } - } -} - -void HistoryWidget::updatePreview() { - _previewTimer.cancel(); - if (_previewData && _previewData->pendingTill >= 0) { - _fieldBarCancel->show(); - updateMouseTracking(); - if (_previewData->pendingTill) { - _previewTitle.setText( - st::msgNameStyle, - tr::lng_preview_loading(tr::now), - Ui::NameTextOptions()); - auto linkText = QStringView(_previewLinks).split(' ').at(0).toString(); - _previewDescription.setText( - st::defaultTextStyle, - linkText, - Ui::DialogTextOptions()); - - const auto timeout = (_previewData->pendingTill - base::unixtime::now()); - _previewTimer.callOnce(std::max(timeout, 0) * crl::time(1000)); - } else { - auto preview = - HistoryView::TitleAndDescriptionFromWebPage(_previewData); - if (preview.title.isEmpty()) { - if (_previewData->document) { - preview.title = tr::lng_attach_file(tr::now); - } else if (_previewData->photo) { - preview.title = tr::lng_attach_photo(tr::now); - } - } - _previewTitle.setText( - st::msgNameStyle, - preview.title, - Ui::NameTextOptions()); - _previewDescription.setText( - st::defaultTextStyle, - preview.description, - Ui::DialogTextOptions()); - } - } else if (!readyToForward() && !replyToId() && !_editMsgId) { - _fieldBarCancel->hide(); - updateMouseTracking(); - } - updateControlsGeometry(); - update(); -} - void HistoryWidget::fullInfoUpdated() { auto refresh = false; if (_list) { @@ -7589,7 +7542,10 @@ bool HistoryWidget::updateCanSendMessage() { if (!_peer) { return false; } - const auto replyTo = (_replyToId && !_editMsgId) ? _replyEditMsg : 0; + const auto checkTopicFromReplyTo = _replyTo + && !_editMsgId + && (_replyTo.messageId.peer == _peer->id); + const auto replyTo = checkTopicFromReplyTo ? _replyEditMsg : 0; const auto topic = replyTo ? replyTo->topic() : nullptr; const auto allWithoutPolls = Data::AllSendRestrictions() & ~ChatRestriction::SendPolls; @@ -7667,7 +7623,7 @@ void HistoryWidget::escape() { } } else if (!_fieldAutocomplete->isHidden()) { _fieldAutocomplete->hideAnimated(); - } else if (_replyToId && _field->getTextWithTags().text.isEmpty()) { + } else if (_replyTo && _field->getTextWithTags().text.isEmpty()) { cancelReply(); } else if (auto &voice = _voiceRecordBar; voice->isActive()) { voice->showDiscardBox(nullptr, anim::type::normal); @@ -7762,8 +7718,15 @@ void HistoryWidget::messageDataReceived( MsgId msgId) { if (!_peer || _peer != peer || !msgId) { return; - } else if (_editMsgId == msgId || _replyToId == msgId) { + } else if (_editMsgId == msgId + || (_replyTo.messageId == FullMsgId(peer->id, msgId))) { updateReplyEditTexts(true); + if (_editMsgId == msgId) { + _preview->setDisabled(_editMsgId + && _replyEditMsg + && _replyEditMsg->media() + && !_replyEditMsg->media()->webpage()); + } } } @@ -7774,7 +7737,9 @@ void HistoryWidget::updateReplyEditText(not_null item) { }; _replyEditMsgText.setMarkedText( st::defaultTextStyle, - item->inReplyText(), + ((_editMsgId || _replyTo.quote.empty()) + ? item->inReplyText() + : _replyTo.quote), Ui::DialogTextOptions(), context); if (fieldOrDisabledShown() || isRecording()) { @@ -7785,14 +7750,14 @@ void HistoryWidget::updateReplyEditText(not_null item) { void HistoryWidget::updateReplyEditTexts(bool force) { if (!force) { - if (_replyEditMsg || (!_editMsgId && !_replyToId)) { + if (_replyEditMsg || (!_editMsgId && !_replyTo)) { return; } } if (!_replyEditMsg && _peer) { _replyEditMsg = session().data().message( - _peer->id, - _editMsgId ? _editMsgId : _replyToId); + _editMsgId ? _peer->id : _replyTo.messageId.peer, + _editMsgId ? _editMsgId : _replyTo.messageId.msg); } if (_replyEditMsg) { const auto media = _replyEditMsg->media(); @@ -7838,7 +7803,7 @@ void HistoryWidget::updateForwarding() { void HistoryWidget::updateReplyToName() { if (_editMsgId) { return; - } else if (!_replyEditMsg && (_replyToId || !_kbReplyTo)) { + } else if (!_replyEditMsg && (_replyTo || !_kbReplyTo)) { return; } const auto from = [&] { @@ -7872,8 +7837,8 @@ void HistoryWidget::drawField(Painter &p, const QRect &rect) { auto backy = _field->y() - st::historySendPadding; auto backh = fieldHeight() + 2 * st::historySendPadding; auto hasForward = readyToForward(); - auto drawMsgText = (_editMsgId || _replyToId) ? _replyEditMsg : _kbReplyTo; - if (_editMsgId || _replyToId || (!hasForward && _kbReplyTo)) { + auto drawMsgText = (_editMsgId || _replyTo) ? _replyEditMsg : _kbReplyTo; + if (_editMsgId || _replyTo || (!hasForward && _kbReplyTo)) { if (!_editMsgId && drawMsgText && (_replyToNameVersion @@ -7885,11 +7850,11 @@ void HistoryWidget::drawField(Painter &p, const QRect &rect) { } else if (hasForward) { backy -= st::historyReplyHeight; backh += st::historyReplyHeight; - } else if (_previewData && _previewData->pendingTill >= 0) { + } else if (_previewDrawPreview) { backy -= st::historyReplyHeight; backh += st::historyReplyHeight; } - auto drawWebPagePreview = (_previewData && _previewData->pendingTill >= 0) && !_replyForwardPressed; + auto drawWebPagePreview = _previewDrawPreview && !_replyForwardPressed; p.setInactive( controller()->isGifPausedAtLeastFor(Window::GifPauseReason::Any)); p.fillRect(myrtlrect(0, backy, width(), backh), st::historyReplyBg); @@ -7908,7 +7873,7 @@ void HistoryWidget::drawField(Painter &p, const QRect &rect) { }); } - if (_editMsgId || _replyToId || (!hasForward && _kbReplyTo)) { + if (_editMsgId || _replyTo || (!hasForward && _kbReplyTo)) { const auto now = crl::now(); const auto paused = p.inactive(); const auto pausedSpoiler = paused || On(PowerSaving::kChatSpoiler); @@ -7921,7 +7886,11 @@ void HistoryWidget::drawField(Painter &p, const QRect &rect) { const auto overEdit = _photoEditMedia ? _inPhotoEditOver.value(_inPhotoEdit ? 1. : 0.) : 0.; - auto to = QRect(replyLeft, backy + st::msgReplyPadding.top(), st::msgReplyBarSize.height(), st::msgReplyBarSize.height()); + auto to = QRect( + replyLeft, + backy + (st::historyReplyHeight - st::historyReplyPreview) / 2, + st::historyReplyPreview, + st::historyReplyPreview); p.drawPixmap(to.x(), to.y(), preview->pixSingle( preview->size() / style::DevicePixelRatio(), { @@ -7945,7 +7914,7 @@ void HistoryWidget::drawField(Painter &p, const QRect &rect) { p.setOpacity(1.); } } - replyLeft += st::msgReplyBarSize.height() + st::msgReplyBarSkip - st::msgReplyBarSize.width() - st::msgReplyBarPos.x(); + replyLeft += st::historyReplyPreview + st::msgReplyBarSkip; } p.setPen(st::historyReplyNameFg); if (_editMsgId) { @@ -7969,7 +7938,7 @@ void HistoryWidget::drawField(Painter &p, const QRect &rect) { } else { p.setFont(st::msgDateFont); p.setPen(st::historyComposeAreaFgService); - p.drawText(replyLeft, backy + st::msgReplyPadding.top() + (st::msgReplyBarSize.height() - st::msgDateFont->height) / 2 + st::msgDateFont->ascent, st::msgDateFont->elided(tr::lng_profile_loading(tr::now), width() - replyLeft - _fieldBarCancel->width() - st::msgReplyPadding.right())); + p.drawText(replyLeft, backy + (st::historyReplyHeight - st::msgDateFont->height) / 2 + st::msgDateFont->ascent, st::msgDateFont->elided(tr::lng_profile_loading(tr::now), width() - replyLeft - _fieldBarCancel->width() - st::msgReplyPadding.right())); } } } else if (hasForward) { @@ -7985,24 +7954,15 @@ void HistoryWidget::drawField(Painter &p, const QRect &rect) { } if (drawWebPagePreview) { const auto textTop = backy + st::msgReplyPadding.top(); - auto previewLeft = st::historyReplySkip + st::webPageLeft; - p.fillRect( - st::historyReplySkip, - textTop, - st::webPageBar, - st::msgReplyBarSize.height(), - st::msgInReplyBarColor); + auto previewLeft = st::historyReplySkip; const auto to = QRect( previewLeft, - textTop, - st::msgReplyBarSize.height(), - st::msgReplyBarSize.height()); - if (HistoryView::DrawWebPageDataPreview(p, _previewData, _peer, to)) { - previewLeft += st::msgReplyBarSize.height() - + st::msgReplyBarSkip - - st::msgReplyBarSize.width() - - st::msgReplyBarPos.x(); + backy + (st::historyReplyHeight - st::historyReplyPreview) / 2, + st::historyReplyPreview, + st::historyReplyPreview); + if (_previewDrawPreview(p, to)) { + previewLeft += st::historyReplyPreview + st::msgReplyBarSkip; } p.setPen(st::historyReplyNameFg); const auto elidedWidth = width() @@ -8107,7 +8067,7 @@ void HistoryWidget::paintEvent(QPaintEvent *e) { const auto restrictionHidden = fieldOrDisabledShown() || isRecording(); if (restrictionHidden - || replyToId() + || replyTo() || readyToForward() || _kbShown) { drawField(p, clip); diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index b0905b6b6dfd92..eb3a6ede576494 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -14,6 +14,7 @@ For license and copyright information please follow this link: #include "history/history.h" #include "chat_helpers/bot_command.h" #include "chat_helpers/field_autocomplete.h" +#include "data/data_drafts.h" #include "window/section_widget.h" #include "ui/widgets/fields/input_field.h" #include "mtproto/sender.h" @@ -30,7 +31,6 @@ class Error; } // namespace MTP namespace Data { -enum class PreviewState : char; class PhotoMedia; } // namespace Data @@ -94,13 +94,15 @@ class Element; class PinnedTracker; class TranslateBar; class ComposeSearch; -namespace Controls { +} // namespace HistoryView + +namespace HistoryView::Controls { class RecordLock; class VoiceRecordBar; class ForwardPanel; class TTLButton; -} // namespace Controls -} // namespace HistoryView +class WebpageProcessor; +} // namespace HistoryView::Controls class BotKeyboard; class HistoryInner; @@ -182,12 +184,14 @@ class HistoryWidget final MessageIdsList getSelectedItems() const; void itemEdited(not_null item); - void replyToMessage(FullMsgId itemId); - void replyToMessage(not_null item); + void replyToMessage(FullReplyTo id); + void replyToMessage( + not_null item, + TextWithEntities quote = {}); void editMessage(FullMsgId itemId); void editMessage(not_null item); - MsgId replyToId() const; + [[nodiscard]] FullReplyTo replyTo() const; bool lastForceReplyReplied(const FullMsgId &replyTo) const; bool lastForceReplyReplied() const; bool cancelReply(bool lastKeyboardUsed = false); @@ -198,13 +202,10 @@ class HistoryWidget final [[nodiscard]] QVector replyReturns() const; void setReplyReturns(PeerId peer, QVector replyReturns); - void updatePreview(); - void previewCancel(); - void escape(); void sendBotCommand(const Bot::SendCommandRequest &request); - void hideSingleUseKeyboard(PeerData *peer, MsgId replyTo); + void hideSingleUseKeyboard(FullMsgId replyToId); bool insertBotCommand(const QString &cmd); bool eventFilter(QObject *obj, QEvent *e) override; @@ -537,9 +538,9 @@ class HistoryWidget final void saveEditMsg(); - void checkPreview(); - void requestPreview(); - void gotPreview(QString links, const MTPMessageMedia &media, mtpRequestId req); + void setupPreview(); + void editDraftOptions(); + void messagesReceived(not_null peer, const MTPmessages_Messages &messages, int requestId); void messagesFailed(const MTP::Error &error, int requestId); void addMessagesToFront(not_null peer, const QVector &messages); @@ -633,11 +634,11 @@ class HistoryWidget final void searchInChat(); MTP::Sender _api; - MsgId _replyToId = 0; + FullReplyTo _replyTo; Ui::Text::String _replyToName; int _replyToNameVersion = 0; - MsgId _processingReplyId = 0; + FullReplyTo _processingReplyTo; HistoryItem *_processingReplyItem = nullptr; MsgId _editMsgId = 0; @@ -671,16 +672,10 @@ class HistoryWidget final mtpRequestId _saveEditMsgRequestId = 0; - QStringList _parsedLinks; - QString _previewLinks; - WebPageData *_previewData = nullptr; - typedef QMap PreviewCache; - PreviewCache _previewCache; - mtpRequestId _previewRequest = 0; + std::unique_ptr _preview; + Fn _previewDrawPreview; Ui::Text::String _previewTitle; Ui::Text::String _previewDescription; - base::Timer _previewTimer; - Data::PreviewState _previewState = Data::PreviewState(); bool _replyForwardPressed = false; @@ -722,7 +717,6 @@ class HistoryWidget final const object_ptr _fieldAutocomplete; object_ptr _supportAutocomplete; - std::unique_ptr _fieldLinksParser; UserData *_inlineBot = nullptr; QString _inlineBotUsername; @@ -757,7 +751,7 @@ class HistoryWidget final object_ptr _field; base::unique_qptr _fieldDisabled; Ui::Animations::Simple _inPhotoEditOver; - bool _inReplyEditForward = false; + bool _inDetails = false; bool _inPhotoEdit = false; bool _inClickable = false; diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp index 6a752add3e8a85..ce7aebe841d0c2 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -46,9 +46,11 @@ For license and copyright information please follow this link: #include "ui/power_saving.h" #include "history/history.h" #include "history/history_item.h" +#include "history/view/controls/history_view_forward_panel.h" +#include "history/view/controls/history_view_draft_options.h" #include "history/view/controls/history_view_voice_record_bar.h" #include "history/view/controls/history_view_ttl_button.h" -#include "history/view/controls/history_view_forward_panel.h" +#include "history/view/controls/history_view_webpage_processor.h" #include "history/view/history_view_webpage_preview.h" #include "inline_bots/bot_attach_web_view.h" #include "inline_bots/inline_results_widget.h" @@ -101,248 +103,8 @@ using SetHistoryArgs = ComposeControls::SetHistoryArgs; using VoiceRecordBar = Controls::VoiceRecordBar; using ForwardPanel = Controls::ForwardPanel; -[[nodiscard]] auto ShowWebPagePreview(WebPageData *page) { - return page && (page->pendingTill >= 0); -} - -WebPageText ProcessWebPageData(WebPageData *page) { - auto previewText = HistoryView::TitleAndDescriptionFromWebPage(page); - if (previewText.title.isEmpty()) { - if (page->document) { - previewText.title = tr::lng_attach_file(tr::now); - } else if (page->photo) { - previewText.title = tr::lng_attach_photo(tr::now); - } - } - return previewText; -} - } // namespace -class WebpageProcessor final { -public: - WebpageProcessor( - not_null history, - not_null field); - - void cancel(); - void checkPreview(); - - [[nodiscard]] Data::PreviewState state() const; - void setState(Data::PreviewState value); - void refreshState(Data::PreviewState value, bool disable); - - [[nodiscard]] rpl::producer<> paintRequests() const; - [[nodiscard]] rpl::producer titleChanges() const; - [[nodiscard]] rpl::producer descriptionChanges() const; - [[nodiscard]] rpl::producer pageDataChanges() const; - -private: - void updatePreview(); - void getWebPagePreview(); - - const not_null _history; - MTP::Sender _api; - MessageLinksParser _fieldLinksParser; - - Data::PreviewState _previewState = Data::PreviewState(); - - QStringList _parsedLinks; - QString _previewLinks; - - WebPageData *_previewData = nullptr; - std::map _previewCache; - - mtpRequestId _previewRequest = 0; - - rpl::event_stream<> _paintRequests; - rpl::event_stream _titleChanges; - rpl::event_stream _descriptionChanges; - rpl::event_stream _pageDataChanges; - - base::Timer _timer; - - rpl::lifetime _lifetime; - -}; - -WebpageProcessor::WebpageProcessor( - not_null history, - not_null field) -: _history(history) -, _api(&history->session().mtp()) -, _fieldLinksParser(field) -, _previewState(Data::PreviewState::Allowed) -, _timer([=] { - if (!ShowWebPagePreview(_previewData) - || _previewLinks.isEmpty()) { - return; - } - getWebPagePreview(); -}) { - - _history->session().downloaderTaskFinished( - ) | rpl::filter([=] { - return _previewData - && (_previewData->document || _previewData->photo); - }) | rpl::start_with_next([=] { - _paintRequests.fire({}); - }, _lifetime); - - _history->owner().webPageUpdates( - ) | rpl::filter([=](not_null page) { - return (_previewData == page.get()); - }) | rpl::start_with_next([=] { - updatePreview(); - }, _lifetime); - - _fieldLinksParser.list().changes( - ) | rpl::start_with_next([=](QStringList &&parsed) { - if (_previewState == Data::PreviewState::EmptyOnEdit - && _parsedLinks != parsed) { - _previewState = Data::PreviewState::Allowed; - } - _parsedLinks = std::move(parsed); - - checkPreview(); - }, _lifetime); -} - -rpl::producer<> WebpageProcessor::paintRequests() const { - return _paintRequests.events(); -} - -Data::PreviewState WebpageProcessor::state() const { - return _previewState; -} - -void WebpageProcessor::setState(Data::PreviewState value) { - _previewState = value; -} - -void WebpageProcessor::refreshState( - Data::PreviewState value, - bool disable) { - // Save links from _field to _parsedLinks without generating preview. - _previewState = Data::PreviewState::Cancelled; - _fieldLinksParser.setDisabled(disable); - _fieldLinksParser.parseNow(); - _parsedLinks = _fieldLinksParser.list().current(); - _previewState = value; - checkPreview(); -} - -void WebpageProcessor::cancel() { - _api.request(base::take(_previewRequest)).cancel(); - _previewData = nullptr; - _previewLinks.clear(); - updatePreview(); -} - -void WebpageProcessor::updatePreview() { - _timer.cancel(); - auto t = QString(); - auto d = QString(); - if (ShowWebPagePreview(_previewData)) { - if (const auto till = _previewData->pendingTill) { - t = tr::lng_preview_loading(tr::now); - d = QStringView(_previewLinks).split(' ').at(0).toString(); - - const auto timeout = till - base::unixtime::now(); - _timer.callOnce( - std::max(timeout, 0) * crl::time(1000)); - } else { - const auto preview = ProcessWebPageData(_previewData); - t = preview.title; - d = preview.description; - } - } - _titleChanges.fire_copy(t); - _descriptionChanges.fire_copy(d); - _pageDataChanges.fire_copy(_previewData); - _paintRequests.fire({}); -} - -void WebpageProcessor::getWebPagePreview() { - const auto links = _previewLinks; - _previewRequest = _api.request( - MTPmessages_GetWebPagePreview( - MTP_flags(0), - MTP_string(links), - MTPVector() - )).done([=](const MTPMessageMedia &result) { - _previewRequest = 0; - result.match([=](const MTPDmessageMediaWebPage &d) { - const auto page = _history->owner().processWebpage(d.vwebpage()); - _previewCache.insert({ links, page->id }); - auto &till = page->pendingTill; - if (till > 0 && till <= base::unixtime::now()) { - till = -1; - } - if (links == _previewLinks - && _previewState == Data::PreviewState::Allowed) { - _previewData = (page->id && page->pendingTill >= 0) - ? page.get() - : nullptr; - updatePreview(); - } - }, [=](const MTPDmessageMediaEmpty &d) { - _previewCache.insert({ links, 0 }); - if (links == _previewLinks - && _previewState == Data::PreviewState::Allowed) { - _previewData = nullptr; - updatePreview(); - } - }, [](const auto &d) { - }); - }).fail([=] { - _previewRequest = 0; - }).send(); -} - -void WebpageProcessor::checkPreview() { - const auto previewRestricted = _history->peer - && _history->peer->amRestricted(ChatRestriction::EmbedLinks); - if (_previewState != Data::PreviewState::Allowed - || previewRestricted) { - cancel(); - return; - } - const auto newLinks = _parsedLinks.join(' '); - if (_previewLinks == newLinks) { - return; - } - _api.request(base::take(_previewRequest)).cancel(); - _previewLinks = newLinks; - if (_previewLinks.isEmpty()) { - if (ShowWebPagePreview(_previewData)) { - cancel(); - } - } else { - const auto i = _previewCache.find(_previewLinks); - if (i == _previewCache.end()) { - getWebPagePreview(); - } else if (i->second) { - _previewData = _history->owner().webpage(i->second); - updatePreview(); - } else if (ShowWebPagePreview(_previewData)) { - cancel(); - } - } -} - -rpl::producer WebpageProcessor::titleChanges() const { - return _titleChanges.events(); -} - -rpl::producer WebpageProcessor::descriptionChanges() const { - return _descriptionChanges.events(); -} - -rpl::producer WebpageProcessor::pageDataChanges() const { - return _pageDataChanges.events(); -} - class FieldHeader final : public Ui::RpWidget { public: FieldHeader( @@ -353,29 +115,26 @@ class FieldHeader final : public Ui::RpWidget { void init(); void editMessage(FullMsgId id, bool photoEditAllowed = false); - void replyToMessage(FullMsgId id); + void replyToMessage(FullReplyTo id); void updateForwarding( Data::Thread *thread, Data::ResolvedForwardDraft items); - void previewRequested( - rpl::producer title, - rpl::producer description, - rpl::producer page); + void previewReady(rpl::producer parsed); void previewUnregister(); [[nodiscard]] bool isDisplayed() const; [[nodiscard]] bool isEditingMessage() const; [[nodiscard]] bool readyToForward() const; [[nodiscard]] const HistoryItemsList &forwardItems() const; - [[nodiscard]] FullMsgId replyingToMessage() const; + [[nodiscard]] FullReplyTo replyingToMessage() const; [[nodiscard]] FullMsgId editMsgId() const; [[nodiscard]] rpl::producer editMsgIdValue() const; [[nodiscard]] rpl::producer scrollToItemRequests() const; [[nodiscard]] rpl::producer<> editPhotoRequests() const; + [[nodiscard]] rpl::producer<> editOptionsRequests() const; [[nodiscard]] MessageToEdit queryToEdit(); - [[nodiscard]] WebPageId webPageId() const; - [[nodiscard]] MsgId getDraftMessageId() const; + [[nodiscard]] FullReplyTo getDraftReply() const; [[nodiscard]] rpl::producer<> editCancelled() const { return _editCancelled.events(); } @@ -406,16 +165,14 @@ class FieldHeader final : public Ui::RpWidget { bool hasPreview() const; struct Preview { - WebPageData *data = nullptr; + Controls::WebpageParsed parsed; Ui::Text::String title; Ui::Text::String description; - bool cancelled = false; }; const std::shared_ptr _show; History *_history = nullptr; - rpl::variable _title; - rpl::variable _description; + MsgId _topicRootId = 0; Preview _preview; rpl::event_stream<> _editCancelled; @@ -425,7 +182,7 @@ class FieldHeader final : public Ui::RpWidget { rpl::lifetime _previewLifetime; rpl::variable _editMsgId; - rpl::variable _replyToId; + rpl::variable _replyTo; std::unique_ptr _forwardPanel; rpl::producer<> _toForwardUpdated; @@ -449,6 +206,7 @@ class FieldHeader final : public Ui::RpWidget { rpl::event_stream _visibleChanged; rpl::event_stream _scrollToItemRequests; + rpl::event_stream<> _editOptionsRequests; rpl::event_stream<> _editPhotoRequests; }; @@ -468,6 +226,7 @@ FieldHeader::FieldHeader( void FieldHeader::setHistory(const SetHistoryArgs &args) { _history = *args.history; + _topicRootId = args.topicRootId; } void FieldHeader::init() { @@ -497,7 +256,7 @@ void FieldHeader::init() { st::historyReplyIcon.paint(p, position, width()); } - (ShowWebPagePreview(_preview.data) && !*leftIconPressed) + (_preview.parsed && !*leftIconPressed) ? paintWebPage( p, _history ? _history->peer : _data->session().user()) @@ -508,14 +267,14 @@ void FieldHeader::init() { _editMsgId.value( ) | rpl::start_with_next([=](FullMsgId value) { - const auto shown = value ? value : _replyToId.current(); + const auto shown = value ? value : _replyTo.current().messageId; setShownMessage(_data->message(shown)); }, lifetime()); - _replyToId.value( - ) | rpl::start_with_next([=](FullMsgId value) { + _replyTo.value( + ) | rpl::start_with_next([=](const FullReplyTo &value) { if (!_editMsgId.current()) { - setShownMessage(_data->message(value)); + setShownMessage(_data->message(value.messageId)); } }, lifetime()); @@ -529,7 +288,7 @@ void FieldHeader::init() { if (_editMsgId.current() == update.item->fullId()) { _editCancelled.fire({}); } - if (_replyToId.current() == update.item->fullId()) { + if (_replyTo.current().messageId == update.item->fullId()) { _replyCancelled.fire({}); } } else { @@ -545,29 +304,13 @@ void FieldHeader::init() { _editCancelled.fire({}); } else if (readyToForward()) { _forwardCancelled.fire({}); - } else if (_replyToId.current()) { + } else if (_replyTo.current()) { _replyCancelled.fire({}); } updateVisible(); update(); }); - _title.value( - ) | rpl::start_with_next([=](const auto &t) { - _preview.title.setText( - st::msgNameStyle, - t, - Ui::NameTextOptions()); - }, lifetime()); - - _description.value( - ) | rpl::start_with_next([=](const auto &d) { - _preview.description.setText( - st::messageTextStyle, - d, - Ui::DialogTextOptions()); - }, lifetime()); - setMouseTracking(true); events( ) | rpl::filter([=](not_null event) { @@ -576,7 +319,8 @@ void FieldHeader::init() { return (ranges::contains(kMouseEvents, type) || leaving) && (isEditingMessage() || readyToForward() - || replyingToMessage()); + || replyingToMessage() + || _preview.parsed); }) | rpl::start_with_next([=](not_null event) { const auto updateOver = [&](bool inClickable, bool inPhotoEdit) { if (_inClickable != inClickable) { @@ -619,13 +363,15 @@ void FieldHeader::init() { } else if (isLeftButton && inPhotoEdit) { _editPhotoRequests.fire({}); } else if (isLeftButton && inPreviewRect) { - if (!isEditingMessage() && readyToForward()) { + const auto reply = replyingToMessage(); + if (_preview.parsed) { + _editOptionsRequests.fire({}); + } else if (isEditingMessage()) { + _scrollToItemRequests.fire(_editMsgId.current()); + } else if (readyToForward()) { _forwardPanel->editOptions(_show); - } else { - auto id = isEditingMessage() - ? _editMsgId.current() - : replyingToMessage(); - _scrollToItemRequests.fire(std::move(id)); + } else if (reply) { + _editOptionsRequests.fire({}); } } } else if (type == QEvent::MouseButtonRelease) { @@ -644,9 +390,12 @@ void FieldHeader::updateShownMessageText() { .session = &_data->session(), .customEmojiRepaint = [=] { customEmojiRepaint(); }, }; + const auto reply = replyingToMessage(); _shownMessageText.setMarkedText( st::messageTextStyle, - _shownMessage->inReplyText(), + ((isEditingMessage() || reply.quote.empty()) + ? _shownMessage->inReplyText() + : reply.quote), Ui::DialogTextOptions(), context); } @@ -663,17 +412,6 @@ void FieldHeader::setShownMessage(HistoryItem *item) { _shownMessage = item; if (item) { updateShownMessageText(); - if (item->fullId() == _editMsgId.current()) { - _preview = {}; - if (const auto media = item->media()) { - if (const auto page = media->webpage()) { - const auto preview = ProcessWebPageData(page); - _title = preview.title; - _description = preview.description; - _preview.data = page; - } - } - } } else { _shownMessageText.clear(); resolveMessageData(); @@ -692,16 +430,18 @@ void FieldHeader::setShownMessage(HistoryItem *item) { } void FieldHeader::resolveMessageData() { - const auto id = (isEditingMessage() ? _editMsgId : _replyToId).current(); + const auto id = isEditingMessage() + ? _editMsgId.current() + : _replyTo.current().messageId; if (!id) { return; } const auto peer = _data->peer(id.peer); const auto itemId = id.msg; const auto callback = crl::guard(this, [=] { - const auto now = (isEditingMessage() - ? _editMsgId - : _replyToId).current(); + const auto now = isEditingMessage() + ? _editMsgId.current() + : _replyTo.current().messageId; if (now == id && !_shownMessage) { if (const auto message = _data->message(peer, itemId)) { setShownMessage(message); @@ -715,34 +455,22 @@ void FieldHeader::resolveMessageData() { _data->session().api().requestMessageData(peer, itemId, callback); } -void FieldHeader::previewRequested( - rpl::producer title, - rpl::producer description, - rpl::producer page) { +void FieldHeader::previewReady( + rpl::producer parsed) { _previewLifetime.destroy(); std::move( - title - ) | rpl::filter([=] { - return !_preview.cancelled; - }) | rpl::start_with_next([=](const QString &t) { - _title = t; - }, _previewLifetime); - - std::move( - description - ) | rpl::filter([=] { - return !_preview.cancelled; - }) | rpl::start_with_next([=](const QString &d) { - _description = d; - }, _previewLifetime); - - std::move( - page - ) | rpl::filter([=] { - return !_preview.cancelled; - }) | rpl::start_with_next([=](WebPageData *p) { - _preview.data = p; + parsed + ) | rpl::start_with_next([=](Controls::WebpageParsed parsed) { + _preview.parsed = std::move(parsed); + _preview.title.setText( + st::msgNameStyle, + _preview.parsed.title, + Ui::NameTextOptions()); + _preview.description.setText( + st::messageTextStyle, + _preview.parsed.description, + Ui::DialogTextOptions()); updateVisible(); }, _previewLifetime); } @@ -752,27 +480,18 @@ void FieldHeader::previewUnregister() { } void FieldHeader::paintWebPage(Painter &p, not_null context) { - Expects(ShowWebPagePreview(_preview.data)); + Expects(!!_preview.parsed); const auto textTop = st::msgReplyPadding.top(); - auto previewLeft = st::historyReplySkip + st::webPageLeft; - p.fillRect( - st::historyReplySkip, - textTop, - st::webPageBar, - st::msgReplyBarSize.height(), - st::msgInReplyBarColor); + auto previewLeft = st::historyReplySkip; const QRect to( previewLeft, - textTop, - st::msgReplyBarSize.height(), - st::msgReplyBarSize.height()); - if (HistoryView::DrawWebPageDataPreview(p, _preview.data, context, to)) { - previewLeft += st::msgReplyBarSize.height() - + st::msgReplyBarSkip - - st::msgReplyBarSize.width() - - st::msgReplyBarPos.x(); + (st::historyReplyHeight - st::historyReplyPreview) / 2, + st::historyReplyPreview, + st::historyReplyPreview); + if (_preview.parsed.drawPreview(p, to)) { + previewLeft += st::historyReplyPreview + st::msgReplyBarSkip; } const auto elidedWidth = width() - previewLeft @@ -806,8 +525,7 @@ void FieldHeader::paintEditOrReplyToMessage(Painter &p) { if (!_shownMessage) { p.setFont(st::msgDateFont); p.setPen(st::historyComposeAreaFgService); - const auto top = (st::msgReplyPadding.top() - + (st::msgReplyBarSize.height() - st::msgDateFont->height) / 2); + const auto top = (st::historyReplyHeight - st::msgDateFont->height) / 2; p.drawText( replySkip, top + st::msgDateFont->ascent, @@ -843,10 +561,8 @@ void FieldHeader::paintEditOrReplyToMessage(Painter &p) { update(); }); } - const auto previewSkipValue = st::msgReplyBarSize.height() - + st::msgReplyBarSkip - - st::msgReplyBarSize.width() - - st::msgReplyBarPos.x(); + const auto previewSkipValue = st::historyReplyPreview + + st::msgReplyBarSkip; const auto previewSkip = _shownMessageHasPreview ? previewSkipValue : 0; const auto textLeft = replySkip + previewSkip; const auto textAvailableWidth = availableWidth - previewSkip; @@ -856,9 +572,9 @@ void FieldHeader::paintEditOrReplyToMessage(Painter &p) { : 0.; const auto to = QRect( replySkip, - st::msgReplyPadding.top(), - st::msgReplyBarSize.height(), - st::msgReplyBarSize.height()); + (st::historyReplyHeight - st::historyReplyPreview) / 2, + st::historyReplyPreview, + st::historyReplyPreview); p.drawPixmap(to.x(), to.y(), preview->pixSingle( preview->size() / style::DevicePixelRatio(), { @@ -950,20 +666,18 @@ const HistoryItemsList &FieldHeader::forwardItems() const { return _forwardPanel->items(); } -FullMsgId FieldHeader::replyingToMessage() const { - return _replyToId.current(); +FullReplyTo FieldHeader::replyingToMessage() const { + return _replyTo.current(); } bool FieldHeader::hasPreview() const { - return ShowWebPagePreview(_preview.data); + return !!_preview.parsed; } -WebPageId FieldHeader::webPageId() const { - return hasPreview() ? _preview.data->id : CancelledWebPageId; -} - -MsgId FieldHeader::getDraftMessageId() const { - return (isEditingMessage() ? _editMsgId : _replyToId).current().msg; +FullReplyTo FieldHeader::getDraftReply() const { + return isEditingMessage() + ? FullReplyTo{ _editMsgId.current() } + : _replyTo.current(); } void FieldHeader::updateControlsGeometry(QSize size) { @@ -977,9 +691,9 @@ void FieldHeader::updateControlsGeometry(QSize size) { height()); _shownMessagePreviewRect = QRect( st::historyReplySkip, - st::msgReplyPadding.top(), - st::msgReplyBarSize.height(), - st::msgReplyBarSize.height()); + (st::historyReplyHeight - st::historyReplyPreview) / 2, + st::historyReplyPreview, + st::historyReplyPreview); } void FieldHeader::editMessage(FullMsgId id, bool photoEditAllowed) { @@ -992,8 +706,8 @@ void FieldHeader::editMessage(FullMsgId id, bool photoEditAllowed) { update(); } -void FieldHeader::replyToMessage(FullMsgId id) { - _replyToId = id; +void FieldHeader::replyToMessage(FullReplyTo id) { + _replyTo = id; } void FieldHeader::updateForwarding( @@ -1018,6 +732,10 @@ rpl::producer<> FieldHeader::editPhotoRequests() const { return _editPhotoRequests.events(); } +rpl::producer<> FieldHeader::editOptionsRequests() const { + return _editOptionsRequests.events(); +} + MessageToEdit FieldHeader::queryToEdit() { const auto item = _data->message(_editMsgId.current()); if (!isEditingMessage() || !item) { @@ -1025,10 +743,7 @@ MessageToEdit FieldHeader::queryToEdit() { } return { .fullId = item->fullId(), - .options = { - .scheduled = item->isScheduled() ? item->date() : 0, - .removeWebPageId = !hasPreview(), - }, + .options = { .scheduled = item->isScheduled() ? item->date() : 0 }, }; } @@ -1156,6 +871,7 @@ void ComposeControls::setHistory(SetHistoryArgs &&args) { } unregisterDraftSources(); _history = history; + _topicRootId = args.topicRootId; _historyLifetime.destroy(); _header->setHistory(args); registerDraftSource(); @@ -1193,8 +909,10 @@ void ComposeControls::setHistory(SetHistoryArgs &&args) { orderControls(); } -void ComposeControls::setCurrentDialogsEntryState(Dialogs::EntryState state) { +void ComposeControls::setCurrentDialogsEntryState( + Dialogs::EntryState state) { unregisterDraftSources(); + state.currentReplyTo.topicRootId = _topicRootId; _currentDialogsEntryState = state; updateForwarding(); registerDraftSource(); @@ -1443,6 +1161,9 @@ void ComposeControls::clear() { {}, saveTextDraft ? TextUpdateEvent::SaveDraft : TextUpdateEvent()); cancelReplyMessage(); + if (_preview) { + _preview->apply({ .removed = true }); + } } void ComposeControls::setText(const TextWithTags &textWithTags) { @@ -1462,8 +1183,7 @@ void ComposeControls::setFieldText( | TextUpdateEvent::SendTyping; if (_preview) { - _preview->cancel(); - _preview->setState(Data::PreviewState::Allowed); + _preview->checkNow(false); } } @@ -1472,16 +1192,12 @@ void ComposeControls::saveFieldToHistoryLocalDraft() { if (!_history || !key) { return; } - const auto id = _header->getDraftMessageId(); + const auto id = _header->getDraftReply(); if (_preview && (id || !_field->empty())) { const auto key = draftKeyCurrent(); _history->setDraft( key, - std::make_unique( - _field, - _header->getDraftMessageId(), - key.topicRootId(), - _preview->state())); + std::make_unique(_field, id, _preview->draft())); } else { _history->clearDraft(draftKeyCurrent()); } @@ -1613,10 +1329,45 @@ void ComposeControls::init() { crl::guard(_wrap.get(), [=] { cancelEditMessage(); })); }, _wrap->lifetime()); + _header->editOptionsRequests( + ) | rpl::start_with_next([=] { + const auto history = _history; + const auto reply = _header->replyingToMessage(); + const auto webpage = _preview->draft(); + + const auto done = [=]( + FullReplyTo replyTo, + Data::WebPageDraft webpage) { + if (replyTo) { + replyToMessage(replyTo); + } else { + cancelReplyMessage(); + } + _preview->apply(webpage); + }; + const auto replyToId = reply.messageId; + const auto highlight = crl::guard(_wrap.get(), [=] { + _scrollToItemRequests.fire_copy(replyToId); + }); + + using namespace HistoryView::Controls; + EditDraftOptions({ + .show = _show, + .history = history, + .draft = Data::Draft(_field, reply, _preview->draft()), + .usedLink = _preview->link(), + .links = _preview->links(), + .resolver = _preview->resolver(), + .done = done, + .highlight = highlight, + .clearOldDraft = [=] { ClearDraftReplyTo(history, replyToId); }, + }); + }, _wrap->lifetime()); + _header->previewCancelled( ) | rpl::start_with_next([=] { if (_preview) { - _preview->setState(Data::PreviewState::Cancelled); + _preview->apply({ .removed = true }); } _saveDraftText = true; _saveDraftStart = crl::now(); @@ -1757,7 +1508,7 @@ void ComposeControls::initKeyHandler() { } } _replyNextRequests.fire({ - .replyId = replyingToMessage(), + .replyId = replyingToMessage().messageId, .direction = (isDown ? ReplyNextRequest::Direction::Next : ReplyNextRequest::Direction::Previous) @@ -1999,9 +1750,6 @@ void ComposeControls::fieldChanged() { && (_textUpdateEvents & TextUpdateEvent::SendTyping)); updateSendButtonType(); _hasSendText = HasSendText(_field); - if (!_hasSendText.current() && _preview) { - _preview->setState(Data::PreviewState::Allowed); - } if (updateBotCommandShown() || updateLikeShown()) { updateControlsVisibility(); updateControlsGeometry(_wrap->size()); @@ -2037,8 +1785,8 @@ Data::DraftKey ComposeControls::draftKey(DraftType type) const { case Section::History: case Section::Replies: return (type == DraftType::Edit) - ? Key::LocalEdit(_currentDialogsEntryState.rootId) - : Key::Local(_currentDialogsEntryState.rootId); + ? Key::LocalEdit(_topicRootId) + : Key::Local(_topicRootId); case Section::Scheduled: return (type == DraftType::Edit) ? Key::ScheduledEdit() @@ -2102,9 +1850,9 @@ void ComposeControls::registerDraftSource() { if (key != Data::DraftKey::None()) { const auto draft = [=] { return Storage::MessageDraft{ - _header->getDraftMessageId(), + _header->getDraftReply(), _field->getTextWithTags(), - _preview->state(), + _preview->draft(), }; }; auto draftSource = Storage::MessageDraftSource{ @@ -2150,8 +1898,8 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) { const auto draft = editDraft ? editDraft : _history->draft(draftKey(DraftType::Normal)); - const auto editingId = (draft == editDraft) - ? FullMsgId{ _history->peer->id, draft ? draft->msgId : 0 } + const auto editingId = (draft && draft == editDraft) + ? draft->reply.messageId : FullMsgId(); InvokeQueued(_autocomplete.get(), [=] { updateStickersByEmoji(); }); @@ -2170,7 +1918,9 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) { } _header->editMessage({}); _header->replyToMessage({}); - _preview->refreshState(Data::PreviewState::Allowed, false); + if (_preview) { + _preview->apply({ .removed = true }); + } _canReplaceMedia = false; _photoEditMedia = nullptr; return; @@ -2184,15 +1934,13 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) { draft->cursor.applyTo(_field); _textUpdateEvents = TextUpdateEvent::SaveDraft | TextUpdateEvent::SendTyping; if (_preview) { - const auto disablePreview = (editDraft != nullptr); - _preview->refreshState(draft->previewState, disablePreview); + _preview->apply(draft->webpage, draft != editDraft); } if (draft == editDraft) { const auto resolve = [=] { if (const auto item = _history->owner().message(editingId)) { const auto media = item->media(); - const auto disablePreview = media && !media->webpage(); _canReplaceMedia = media && media->allowsEditMedia(); _photoEditMedia = (_canReplaceMedia && _regularWindow @@ -2206,7 +1954,11 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) { item->fullId()); } _header->editMessage(editingId, _photoEditMedia != nullptr); - _preview->refreshState(_preview->state(), disablePreview); + if (_preview) { + _preview->apply( + Data::WebPageDraft::FromItem(item), + false); + } return true; } _canReplaceMedia = false; @@ -2232,7 +1984,7 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) { } else { _canReplaceMedia = false; _photoEditMedia = nullptr; - _header->replyToMessage({ _history->peer->id, draft->msgId }); + _header->replyToMessage(draft->reply); if (_header->replyingToMessage()) { cancelForward(); } @@ -2241,9 +1993,7 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) { } void ComposeControls::cancelForward() { - _history->setForwardDraft( - _currentDialogsEntryState.rootId, - {}); + _history->setForwardDraft(_topicRootId, {}); updateForwarding(); } @@ -2840,8 +2590,7 @@ void ComposeControls::toggleTabbedSelectorMode() { && !_regularWindow->adaptive().isOneColumn()) { Core::App().settings().setTabbedSelectorSectionEnabled(true); Core::App().saveSettingsDelayed(); - const auto topic = _history->peer->forumTopicFor( - _currentDialogsEntryState.rootId); + const auto topic = _history->peer->forumTopicFor(_topicRootId); pushTabbedSelectorToThirdSection( (topic ? topic : (Data::Thread*)_history), Window::SectionShow::Way::ClearStack); @@ -2886,24 +2635,17 @@ void ComposeControls::editMessage(not_null item) { int(editData.text.size()), Ui::kQFixedMax }; - const auto previewPage = [&]() -> WebPageData* { - if (const auto media = item->media()) { - return media->webpage(); - } - return nullptr; - }(); - const auto previewState = previewPage - ? Data::PreviewState::Allowed - : Data::PreviewState::EmptyOnEdit; const auto key = draftKey(DraftType::Edit); _history->setDraft( key, std::make_unique( editData, - item->id, - key.topicRootId(), + FullReplyTo{ + .messageId = item->fullId(), + .topicRootId = key.topicRootId(), + }, cursor, - previewState)); + Data::WebPageDraft::FromItem(item))); applyDraft(); if (updateReplaceMediaButton()) { updateControlsVisibility(); @@ -2968,27 +2710,28 @@ void ComposeControls::maybeCancelEditMessage() { } } -void ComposeControls::replyToMessage(FullMsgId id) { +void ComposeControls::replyToMessage(FullReplyTo id) { Expects(_history != nullptr); Expects(draftKeyCurrent() != Data::DraftKey::None()); + id.topicRootId = _topicRootId; if (!id) { cancelReplyMessage(); return; } if (isEditingMessage()) { const auto key = draftKey(DraftType::Normal); + Assert(key.topicRootId() == id.topicRootId); if (const auto localDraft = _history->draft(key)) { - localDraft->msgId = id.msg; + localDraft->reply = id; } else { _history->setDraft( key, std::make_unique( TextWithTags(), - id.msg, - key.topicRootId(), + id, MessageCursor(), - Data::PreviewState::Allowed)); + Data::WebPageDraft())); } } else { _header->replyToMessage(id); @@ -3008,11 +2751,11 @@ void ComposeControls::cancelReplyMessage() { if (_history) { const auto key = draftKey(DraftType::Normal); if (const auto localDraft = _history->draft(key)) { - if (localDraft->msgId) { + if (localDraft->reply.messageId) { if (localDraft->textWithTags.text.isEmpty()) { _history->clearDraft(key); } else { - localDraft->msgId = 0; + localDraft->reply = {}; } } } @@ -3025,7 +2768,7 @@ void ComposeControls::cancelReplyMessage() { } void ComposeControls::updateForwarding() { - const auto rootId = _currentDialogsEntryState.rootId; + const auto rootId = _topicRootId; const auto thread = (_history && rootId) ? _history->peer->forumTopicFor(rootId) : (Data::Thread*)_history; @@ -3069,9 +2812,11 @@ void ComposeControls::initWebpageProcess() { return; } - _preview = std::make_unique(_history, _field); + _preview = std::make_unique( + _history, + _field); - _preview->paintRequests( + _preview->repaintRequests( ) | rpl::start_with_next(crl::guard(_header.get(), [=] { _header->update(); }), _historyLifetime); @@ -3087,7 +2832,7 @@ void ComposeControls::initWebpageProcess() { return update.flags; }) | rpl::start_with_next([=](Data::PeerUpdate::Flags flags) { if (flags & Data::PeerUpdate::Flag::Rights) { - _preview->checkPreview(); + _preview->checkNow(false); updateStickersByEmoji(); updateFieldPlaceholder(); } @@ -3105,10 +2850,7 @@ void ComposeControls::initWebpageProcess() { } }, _historyLifetime); - _header->previewRequested( - _preview->titleChanges(), - _preview->descriptionChanges(), - _preview->pageDataChanges()); + _header->previewReady(_preview->parsedValue()); } void ComposeControls::initForwardProcess() { @@ -3118,7 +2860,7 @@ void ComposeControls::initForwardProcess() { ) | rpl::start_with_next([=](const Data::EntryUpdate &update) { if (const auto topic = update.entry->asTopic()) { if (topic->history() == _history - && topic->rootId() == _currentDialogsEntryState.rootId) { + && topic->rootId() == _topicRootId) { updateForwarding(); } } @@ -3127,26 +2869,30 @@ void ComposeControls::initForwardProcess() { updateForwarding(); } -WebPageId ComposeControls::webPageId() const { - return _header->webPageId(); +Data::WebPageDraft ComposeControls::webPageDraft() const { + return _preview ? _preview->draft() : Data::WebPageDraft(); } rpl::producer ComposeControls::scrollRequests() const { - return _header->scrollToItemRequests( - ) | rpl::map([=](FullMsgId id) -> Data::MessagePosition { - if (const auto item = session().data().message(id)) { - return item->position(); - } - return {}; - }); + return rpl::merge( + _header->scrollToItemRequests(), + _scrollToItemRequests.events() + ) | rpl::map([=](FullMsgId id) -> Data::MessagePosition { + if (const auto item = session().data().message(id)) { + return item->position(); + } + return {}; + }); } bool ComposeControls::isEditingMessage() const { return _header->isEditingMessage(); } -FullMsgId ComposeControls::replyingToMessage() const { - return _header->replyingToMessage(); +FullReplyTo ComposeControls::replyingToMessage() const { + auto result = _header->replyingToMessage(); + result.topicRootId = _topicRootId; + return result; } bool ComposeControls::readyToForward() const { diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h index 6ed07917533770..e467640b8d7eb9 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h @@ -43,8 +43,8 @@ namespace Data { struct MessagePosition; struct Draft; class DraftKey; -enum class PreviewState : char; class PhotoMedia; +struct WebPageDraft; } // namespace Data namespace InlineBots { @@ -79,15 +79,15 @@ namespace Api { enum class SendProgressType; } // namespace Api -namespace HistoryView { - -namespace Controls { +namespace HistoryView::Controls { class VoiceRecordBar; class TTLButton; -} // namespace Controls +class WebpageProcessor; +} // namespace HistoryView::Controls + +namespace HistoryView { class FieldHeader; -class WebpageProcessor; enum class ComposeControlsMode { Normal, @@ -185,7 +185,7 @@ class ComposeControls final { [[nodiscard]] bool isEditingMessage() const; [[nodiscard]] bool readyToForward() const; [[nodiscard]] const HistoryItemsList &forwardItems() const; - [[nodiscard]] FullMsgId replyingToMessage() const; + [[nodiscard]] FullReplyTo replyingToMessage() const; [[nodiscard]] bool preventsClose(Fn &&continueCallback) const; @@ -198,7 +198,7 @@ class ComposeControls final { void cancelEditMessage(); void maybeCancelEditMessage(); // Confirm if changed and cancel. - void replyToMessage(FullMsgId id); + void replyToMessage(FullReplyTo id); void cancelReplyMessage(); void updateForwarding(); @@ -208,7 +208,7 @@ class ComposeControls final { void tryProcessKeyInput(not_null e); [[nodiscard]] TextWithTags getTextWithAppliedMarkdown() const; - [[nodiscard]] WebPageId webPageId() const; + [[nodiscard]] Data::WebPageDraft webPageDraft() const; void setText(const TextWithTags &text); void clear(); void hidePanelsAnimated(); @@ -345,6 +345,7 @@ class ComposeControls final { rpl::event_stream _stickerOrEmojiChosen; History *_history = nullptr; + MsgId _topicRootId = 0; Fn _showSlowmodeError; Fn _sendActionFactory; rpl::variable _slowmodeSecondsLeft; @@ -356,6 +357,7 @@ class ComposeControls final { const std::unique_ptr _wrap; const std::unique_ptr _writeRestricted; + rpl::event_stream _scrollToItemRequests; std::optional _backgroundRect; @@ -421,7 +423,7 @@ class ComposeControls final { std::shared_ptr _photoEditMedia; bool _canReplaceMedia = false; - std::unique_ptr _preview; + std::unique_ptr _preview; Fn _raiseEmojiSuggestions; diff --git a/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp b/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp new file mode 100644 index 00000000000000..ac6dd6fc33bd7a --- /dev/null +++ b/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp @@ -0,0 +1,949 @@ +/* +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 "history/view/controls/history_view_draft_options.h" + +#include "base/unixtime.h" +#include "boxes/peer_list_box.h" +#include "boxes/peer_list_controllers.h" +#include "chat_helpers/compose/compose_show.h" +#include "data/data_changes.h" +#include "data/data_drafts.h" +#include "data/data_file_origin.h" +#include "data/data_session.h" +#include "data/data_thread.h" +#include "data/data_user.h" +#include "data/data_web_page.h" +#include "history/view/controls/history_view_webpage_processor.h" +#include "history/view/history_view_element.h" +#include "history/view/history_view_cursor_state.h" +#include "history/history.h" +#include "history/history_item.h" +#include "history/history_item_components.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "settings/settings_common.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/widgets/buttons.h" +#include "ui/widgets/discrete_sliders.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 +#include + +namespace HistoryView::Controls { +namespace { + +enum class Section { + Reply, + Link, +}; + +class PreviewDelegate final : public DefaultElementDelegate { +public: + PreviewDelegate( + not_null parent, + not_null st, + Fn update); + + bool elementAnimationsPaused() override; + not_null elementPathShiftGradient() override; + Context elementContext() override; + +private: + const not_null _parent; + const std::unique_ptr _pathGradient; + +}; + +[[nodiscard]] TextWithEntities HighlightParsedLinks( + TextWithEntities text, + const std::vector &links) { + auto i = text.entities.begin(); + for (const auto &range : links) { + if (range.custom.isEmpty()) { + while (i != text.entities.end()) { + if (i->offset() > range.start) { + break; + } + ++i; + } + i = text.entities.insert( + i, + EntityInText(EntityType::Url, range.start, range.length)); + ++i; + } + } + return text; +} + +class PreviewWrap final : public Ui::RpWidget { +public: + PreviewWrap( + not_null box, + not_null history); + ~PreviewWrap(); + + [[nodiscard]] rpl::producer showQuoteSelector( + not_null item, + const TextWithEntities "e); + [[nodiscard]] rpl::producer showLinkSelector( + const TextWithTags &message, + Data::WebPageDraft webpage, + const std::vector &links, + const QString &usedLink); + +private: + void paintEvent(QPaintEvent *e) override; + void leaveEventHook(QEvent *e) override; + void mouseMoveEvent(QMouseEvent *e) override; + void mousePressEvent(QMouseEvent *e) override; + void mouseReleaseEvent(QMouseEvent *e) override; + void mouseDoubleClickEvent(QMouseEvent *e) override; + + void initElement(); + void highlightUsedLink( + const TextWithTags &message, + const QString &usedLink, + const std::vector &links); + void startSelection(TextSelectType type); + [[nodiscard]] TextSelection resolveNewSelection() const; + + const not_null _box; + const not_null _history; + const std::unique_ptr _theme; + const std::unique_ptr _style; + const std::unique_ptr _delegate; + + Section _section = Section::Reply; + HistoryItem *_draftItem = nullptr; + std::unique_ptr _element; + rpl::variable _selection; + rpl::event_stream _chosenUrl; + Ui::PeerUserpicView _userpic; + rpl::lifetime _elementLifetime; + + QPoint _position; + + base::Timer _trippleClickTimer; + ClickHandlerPtr _link; + ClickHandlerPtr _pressedLink; + TextSelectType _selectType = TextSelectType::Letters; + uint16 _symbol = 0; + uint16 _selectionStartSymbol = 0; + bool _onlyMessageText = false; + bool _afterSymbol = false; + bool _selectionStartAfterSymbol = false; + bool _over = false; + bool _textCursor = false; + bool _linkCursor = false; + bool _selecting = false; + +}; + +PreviewWrap::PreviewWrap( + not_null box, + not_null history) +: RpWidget(box) +, _box(box) +, _history(history) +, _theme(Window::Theme::DefaultChatThemeOn(lifetime())) +, _style(std::make_unique( + history->session().colorIndicesValue())) +, _delegate(std::make_unique( + box, + _style.get(), + [=] { update(); })) +, _position(0, st::msgMargin.bottom()) { + _style->apply(_theme.get()); + + const auto session = &_history->session(); + session->data().viewRepaintRequest( + ) | rpl::start_with_next([=](not_null view) { + if (view == _element.get()) { + update(); + } + }, lifetime()); + + _selection.changes() | rpl::start_with_next([=] { + update(); + }, lifetime()); + + _box->setAttribute(Qt::WA_OpaquePaintEvent, false); + + _box->paintRequest() | rpl::start_with_next([=](QRect clip) { + const auto geometry = Ui::MapFrom(_box, this, rect()); + const auto fill = geometry.intersected(clip); + if (!fill.isEmpty()) { + auto p = QPainter(_box); + p.setClipRect(fill); + Window::SectionWidget::PaintBackground( + p, + _theme.get(), + QSize(_box->width(), _box->window()->height()), + fill); + } + }, lifetime()); + + setMouseTracking(true); +} + +PreviewWrap::~PreviewWrap() { + _selection.reset(TextSelection()); + _elementLifetime.destroy(); + _element = nullptr; + if (_draftItem) { + _draftItem->destroy(); + } +} + +rpl::producer PreviewWrap::showQuoteSelector( + not_null item, + const TextWithEntities "e) { + _selection.reset(TextSelection()); + + _element = item->createView(_delegate.get()); + _link = _pressedLink = nullptr; + + if (const auto was = base::take(_draftItem)) { + was->destroy(); + } + + const auto media = item->media(); + _onlyMessageText = media + && (media->webpage() + || media->game() + || (!media->photo() && !media->document())); + _section = Section::Reply; + + initElement(); + + _selection = _element->selectionFromQuote(quote); + return _selection.value( + ) | rpl::map([=](TextSelection selection) { + return _element->selectedQuote(selection); + }); +} + +rpl::producer PreviewWrap::showLinkSelector( + const TextWithTags &message, + Data::WebPageDraft webpage, + const std::vector &links, + const QString &usedLink) { + _selection.reset(TextSelection()); + + _element = nullptr; + if (const auto was = base::take(_draftItem)) { + was->destroy(); + } + using Flag = MTPDmessageMediaWebPage::Flag; + _draftItem = _history->addNewLocalMessage( + _history->nextNonHistoryEntryId(), + (MessageFlag::FakeHistoryItem + | MessageFlag::Outgoing + | MessageFlag::HasFromId + | (webpage.invert ? MessageFlag::InvertMedia : MessageFlag())), + UserId(), // via + FullReplyTo(), + base::unixtime::now(), // date + _history->session().userPeerId(), + QString(), // postAuthor + HighlightParsedLinks({ + message.text, + TextUtilities::ConvertTextTagsToEntities(message.tags), + }, links), + MTP_messageMediaWebPage( + MTP_flags(Flag() + | (webpage.forceLargeMedia + ? Flag::f_force_large_media + : Flag()) + | (webpage.forceSmallMedia + ? Flag::f_force_small_media + : Flag())), + MTP_webPagePending( + MTP_flags(webpage.url.isEmpty() + ? MTPDwebPagePending::Flag() + : MTPDwebPagePending::Flag::f_url), + MTP_long(webpage.id), + MTP_string(webpage.url), + MTP_int(0))), + HistoryMessageMarkupData(), + uint64(0)); // groupedId + _element = _draftItem->createView(_delegate.get()); + _selectType = TextSelectType::Letters; + _symbol = _selectionStartSymbol = 0; + _afterSymbol = _selectionStartAfterSymbol = false; + _section = Section::Link; + + initElement(); + highlightUsedLink(message, usedLink, links); + + return _chosenUrl.events(); +} + +void PreviewWrap::highlightUsedLink( + const TextWithTags &message, + const QString &usedLink, + const std::vector &links) { + auto selection = TextSelection(); + const auto view = QStringView(message.text); + for (const auto &range : links) { + auto text = view.mid(range.start, range.length); + if (range.custom == usedLink + || (range.custom.isEmpty() + && range.length == usedLink.size() + && text == usedLink)) { + selection = { + uint16(range.start), + uint16(range.start + range.length), + }; + const auto skip = [](QChar ch) { + return ch.isSpace() || Ui::Text::IsNewline(ch); + }; + while (!text.isEmpty() && skip(text.front())) { + text = text.mid(1); + ++selection.from; + } + while (!text.isEmpty() && skip(text.back())) { + text = text.mid(0, text.size() - 1); + --selection.to; + } + const auto basic = _element->textState(QPoint(0, 0), { + .flags = Ui::Text::StateRequest::Flag::LookupSymbol, + .onlyMessageText = true, + }); + if (basic.symbol > 0) { + selection.from += basic.symbol; + selection.to += basic.symbol; + } + break; + } + } + _selection = selection; +} + +void PreviewWrap::paintEvent(QPaintEvent *e) { + if (!_element) { + return; + } + + auto p = Painter(this); + + auto context = _theme->preparePaintContext( + _style.get(), + rect(), + e->rect(), + !window()->isActiveWindow()); + context.outbg = _element->hasOutLayout(); + context.selection = _selecting + ? resolveNewSelection() + : _selection.current(); + + p.translate(_position); + _element->draw(p, context); + + if (_element->displayFromPhoto()) { + auto userpicBottom = height() + - _element->marginBottom() + - _element->marginTop(); + const auto item = _element->data(); + const auto userpicTop = userpicBottom - st::msgPhotoSize; + if (const auto from = item->displayFrom()) { + from->paintUserpicLeft( + p, + _userpic, + st::historyPhotoLeft, + userpicTop, + width(), + st::msgPhotoSize); + } else if (const auto info = item->hiddenSenderInfo()) { + if (info->customUserpic.empty()) { + info->emptyUserpic.paintCircle( + p, + st::historyPhotoLeft, + userpicTop, + width(), + st::msgPhotoSize); + } else { + const auto valid = info->paintCustomUserpic( + p, + _userpic, + st::historyPhotoLeft, + userpicTop, + width(), + st::msgPhotoSize); + if (!valid) { + info->customUserpic.load( + &item->history()->session(), + item->fullId()); + } + } + } else { + Unexpected("Corrupt forwarded information in message."); + } + } +} + +void PreviewWrap::leaveEventHook(QEvent *e) { + if (!_element || !_over) { + return; + } + _over = false; + _textCursor = false; + _linkCursor = false; + if (!_selecting) { + setCursor(style::cur_default); + } +} + +void PreviewWrap::mouseMoveEvent(QMouseEvent *e) { + if (!_element) { + return; + } + using Flag = Ui::Text::StateRequest::Flag; + auto request = StateRequest{ + .flags = (_section == Section::Reply + ? Flag::LookupSymbol + : Flag::LookupLink), + .onlyMessageText = (_section == Section::Link || _onlyMessageText), + }; + auto resolved = _element->textState( + e->pos() - _position, + request); + _over = true; + const auto text = (_section == Section::Reply) + && (resolved.cursor == CursorState::Text); + _link = (_section == Section::Link && resolved.overMessageText) + ? resolved.link + : nullptr; + const auto link = (_link != nullptr) || (_pressedLink != nullptr); + if (_textCursor != text || _linkCursor != link) { + _textCursor = text; + _linkCursor = link; + setCursor((text || _selecting) + ? style::cur_text + : link + ? style::cur_pointer + : style::cur_default); + } + if (_symbol != resolved.symbol + || _afterSymbol != resolved.afterSymbol) { + _symbol = resolved.symbol; + _afterSymbol = resolved.afterSymbol; + if (_selecting) { + update(); + } + } +} + +void PreviewWrap::mousePressEvent(QMouseEvent *e) { + if (!_over) { + return; + } else if (_section == Section::Reply) { + startSelection(_trippleClickTimer.isActive() + ? TextSelectType::Paragraphs + : TextSelectType::Letters); + } else { + _pressedLink = _link; + } +} + +void PreviewWrap::mouseReleaseEvent(QMouseEvent *e) { + if (_section == Section::Reply) { + if (!_selecting) { + return; + } + const auto result = resolveNewSelection(); + _selecting = false; + _selectType = TextSelectType::Letters; + if (!_textCursor) { + setCursor(style::cur_default); + } + _selection = result; + } else if (base::take(_pressedLink) == _link && _link) { + if (const auto url = _link->url(); !url.isEmpty()) { + _chosenUrl.fire_copy(url); + } + } else if (!_link) { + setCursor(style::cur_default); + } +} + +void PreviewWrap::mouseDoubleClickEvent(QMouseEvent *e) { + if (!_over) { + return; + } else if (_section == Section::Reply) { + startSelection(TextSelectType::Words); + _trippleClickTimer.callOnce(QApplication::doubleClickInterval()); + } +} + +void PreviewWrap::initElement() { + _elementLifetime.destroy(); + + if (!_element) { + return; + } + _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); + }, _elementLifetime); +} + +TextSelection PreviewWrap::resolveNewSelection() const { + if (_section != Section::Reply) { + return TextSelection(); + } + const auto make = [](uint16 symbol, bool afterSymbol) { + return uint16(symbol + (afterSymbol ? 1 : 0)); + }; + const auto first = make(_symbol, _afterSymbol); + const auto second = make( + _selectionStartSymbol, + _selectionStartAfterSymbol); + const auto result = (first <= second) + ? TextSelection{ first, second } + : TextSelection{ second, first }; + return _element->adjustSelection(result, _selectType); +} + +void PreviewWrap::startSelection(TextSelectType type) { + if (_selecting && _selectType >= type) { + return; + } + _selecting = true; + _selectType = type; + _selectionStartSymbol = _symbol; + _selectionStartAfterSymbol = _afterSymbol; + if (!_textCursor) { + setCursor(style::cur_text); + } + update(); +} + +PreviewDelegate::PreviewDelegate( + not_null parent, + not_null st, + Fn update) +: _parent(parent) +, _pathGradient(MakePathShiftGradient(st, update)) { +} + +bool PreviewDelegate::elementAnimationsPaused() { + return _parent->window()->isActiveWindow(); +} + +auto PreviewDelegate::elementPathShiftGradient() +-> not_null { + return _pathGradient.get(); +} + +Context PreviewDelegate::elementContext() { + return Context::Replies; +} + +void AddFilledSkip(not_null container) { + const auto skip = container->add(object_ptr( + container, + st::settingsPrivacySkipTop)); + skip->paintRequest() | rpl::start_with_next([=](QRect clip) { + QPainter(skip).fillRect(clip, st::boxBg); + }, skip->lifetime()); +}; + +void DraftOptionsBox( + not_null box, + EditDraftOptionsArgs &&args, + HistoryItem *replyItem, + WebPageData *previewData) { + box->setWidth(st::boxWideWidth); + + const auto &draft = args.draft; + struct State { + rpl::variable
shown; + rpl::lifetime shownLifetime; + rpl::variable quote; + Data::WebPageDraft webpage; + WebPageData *preview = nullptr; + QString link; + Ui::SettingsSlider *tabs = nullptr; + PreviewWrap *wrap = nullptr; + rpl::lifetime resolveLifetime; + }; + const auto state = box->lifetime().make_state(); + state->quote = draft.reply.quote; + state->webpage = draft.webpage; + state->preview = previewData; + state->shown = previewData ? Section::Link : Section::Reply; + if (replyItem && previewData) { + box->setNoContentMargin(true); + state->tabs = box->setPinnedToTopContent( + object_ptr( + box.get(), + st::defaultTabsSlider)); + state->tabs->resizeToWidth(st::boxWideWidth); + state->tabs->move(0, 0); + state->tabs->setRippleTopRoundRadius(st::boxRadius); + state->tabs->setSections({ + tr::lng_reply_header_short(tr::now), + tr::lng_link_header_short(tr::now), + }); + state->tabs->setActiveSectionFast(1); + state->tabs->sectionActivated( + ) | rpl::start_with_next([=](int section) { + state->shown = section ? Section::Link : Section::Reply; + }, box->lifetime()); + } else { + box->setTitle(previewData + ? tr::lng_link_options_header() + : draft.reply.quote.empty() + ? tr::lng_reply_options_header() + : tr::lng_reply_options_quote()); + } + + const auto bottom = box->setPinnedToBottomContent( + object_ptr(box)); + + const auto &done = args.done; + const auto &show = args.show; + const auto &highlight = args.highlight; + const auto &clearOldDraft = args.clearOldDraft; + const auto resolveReply = [=] { + auto result = draft.reply; + result.quote = state->quote.current(); + return result; + }; + const auto finish = [=]( + FullReplyTo result, + Data::WebPageDraft webpage) { + const auto weak = Ui::MakeWeak(box); + done(std::move(result), std::move(webpage)); + if (const auto strong = weak.data()) { + strong->closeBox(); + } + }; + const auto setupReplyActions = [=] { + AddFilledSkip(bottom); + + Settings::AddButton( + bottom, + tr::lng_reply_in_another_chat(), + st::settingsButton, + { &st::menuIconReplace } + )->setClickedCallback([=] { + ShowReplyToChatBox(show, resolveReply(), clearOldDraft); + }); + + Settings::AddButton( + bottom, + tr::lng_reply_show_in_chat(), + st::settingsButton, + { &st::menuIconShowInChat } + )->setClickedCallback(highlight); + + Settings::AddButton( + bottom, + tr::lng_reply_remove(), + st::settingsAttentionButtonWithIcon, + { &st::menuIconDeleteAttention } + )->setClickedCallback([=] { + finish({}, state->webpage); + }); + + if (!replyItem->originalText().empty()) { + AddFilledSkip(bottom); + Settings::AddDividerText( + bottom, + tr::lng_reply_about_quote()); + } + }; + const auto setupLinkActions = [=] { + AddFilledSkip(bottom); + + if (!draft.textWithTags.empty()) { + Settings::AddButton( + bottom, + (state->webpage.invert + ? tr::lng_link_move_down() + : tr::lng_link_move_up()), + st::settingsButton, + { state->webpage.invert + ? &st::menuIconBelow + : &st::menuIconAbove } + )->setClickedCallback([=] { + state->webpage.invert = !state->webpage.invert; + state->webpage.manual = true; + state->shown.force_assign(Section::Link); + }); + } + + if (state->preview->hasLargeMedia) { + const auto small = state->webpage.forceSmallMedia + || (!state->webpage.forceLargeMedia + && state->preview->computeDefaultSmallMedia()); + Settings::AddButton( + bottom, + (small + ? tr::lng_link_enlarge_photo() + : tr::lng_link_shrink_photo()), + st::settingsButton, + { small ? &st::menuIconEnlarge : &st::menuIconShrink } + )->setClickedCallback([=] { + if (small) { + state->webpage.forceSmallMedia = false; + state->webpage.forceLargeMedia = true; + } else { + state->webpage.forceLargeMedia = false; + state->webpage.forceSmallMedia = true; + } + state->webpage.manual = true; + state->shown.force_assign(Section::Link); + }); + } + + Settings::AddButton( + bottom, + tr::lng_link_remove(), + st::settingsAttentionButtonWithIcon, + { &st::menuIconDeleteAttention } + )->setClickedCallback([=] { + finish(resolveReply(), { .removed = true }); + }); + + if (args.links.size() > 1) { + AddFilledSkip(bottom); + Settings::AddDividerText( + bottom, + tr::lng_link_about_choose()); + } + }; + + const auto &resolver = args.resolver; + const auto performSwitch = [=](const QString &link, WebPageData *page) { + if (page) { + state->preview = page; + state->webpage.id = page->id; + state->webpage.url = page->url; + state->webpage.manual = true; + state->link = link; + state->shown.force_assign(Section::Link); + } else { + show->showToast(u"Could not generate preview for this link."_q); + } + }; + const auto switchTo = [=](const QString &link) { + if (link == state->link) { + return; + } + if (const auto value = resolver->lookup(link)) { + performSwitch(link, *value); + } else { + resolver->request(link); + state->resolveLifetime = resolver->resolved( + ) | rpl::start_with_next([=](const QString &resolved) { + if (resolved == link) { + state->resolveLifetime.destroy(); + performSwitch( + link, + resolver->lookup(link).value_or(nullptr)); + } + }); + } + }; + + state->wrap = box->addRow( + object_ptr(box, args.history), + {}); + const auto &linkRanges = args.links; + state->shown.value() | rpl::start_with_next([=](Section shown) { + bottom->clear(); + state->shownLifetime.destroy(); + if (shown == Section::Reply) { + state->quote = state->wrap->showQuoteSelector( + replyItem, + state->quote.current()); + setupReplyActions(); + } else { + state->wrap->showLinkSelector( + draft.textWithTags, + state->webpage, + linkRanges, + state->link + ) | rpl::start_with_next([=](QString link) { + switchTo(link); + }, state->shownLifetime); + setupLinkActions(); + } + }, box->lifetime()); + + auto save = rpl::combine( + state->quote.value(), + state->shown.value() + ) | rpl::map([=](const TextWithEntities "e, Section shown) { + return (quote.empty() || shown != Section::Reply) + ? tr::lng_settings_save() + : tr::lng_reply_quote_selected(); + }) | rpl::flatten_latest(); + box->addButton(std::move(save), [=] { + finish(resolveReply(), state->webpage); + }); + + box->addButton(tr::lng_cancel(), [=] { + box->closeBox(); + }); + + if (replyItem) { + args.show->session().data().itemRemoved( + ) | rpl::filter([=](not_null removed) { + return removed == replyItem; + }) | rpl::start_with_next([=] { + if (previewData) { + state->tabs = nullptr; + box->setPinnedToTopContent( + object_ptr(nullptr)); + box->setNoContentMargin(false); + box->setTitle(state->quote.current().empty() + ? tr::lng_reply_options_header() + : tr::lng_reply_options_quote()); + state->shown = Section::Link; + } else { + box->closeBox(); + } + }, box->lifetime()); + } +} +} // namespace + +void ShowReplyToChatBox( + std::shared_ptr show, + FullReplyTo reply, + Fn clearOldDraft) { + class Controller final : public ChooseRecipientBoxController { + public: + using Chosen = not_null; + + Controller(not_null session) + : ChooseRecipientBoxController( + session, + [=](Chosen thread) mutable { _singleChosen.fire_copy(thread); }, + nullptr) { + } + + void rowClicked(not_null row) override final { + ChooseRecipientBoxController::rowClicked(row); + } + + [[nodiscard]] rpl::producer singleChosen() const{ + return _singleChosen.events(); + } + + bool respectSavedMessagesChat() const override { + return false; + } + + private: + void prepareViewHook() override { + delegate()->peerListSetTitle(tr::lng_reply_in_another_title()); + } + + rpl::event_stream _singleChosen; + + }; + + struct State { + not_null box; + not_null controller; + base::unique_qptr menu; + }; + const auto session = &show->session(); + const auto state = [&] { + auto controller = std::make_unique(session); + const auto controllerRaw = controller.get(); + auto box = Box(std::move(controller), [=]( + not_null box) { + box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); + }); + const auto boxRaw = box.data(); + show->show(std::move(box)); + auto state = State{ boxRaw, controllerRaw }; + return boxRaw->lifetime().make_state(std::move(state)); + }(); + + auto chosen = [=](not_null thread) mutable { + const auto history = thread->owningHistory(); + const auto topicRootId = thread->topicRootId(); + const auto draft = history->localDraft(topicRootId); + const auto textWithTags = draft + ? draft->textWithTags + : TextWithTags(); + const auto cursor = draft ? draft->cursor : MessageCursor(); + reply.topicRootId = topicRootId; + history->setLocalDraft(std::make_unique( + textWithTags, + reply, + cursor, + Data::WebPageDraft())); + history->clearLocalEditDraft(topicRootId); + history->session().changes().entryUpdated( + thread, + Data::EntryUpdate::Flag::LocalDraftSet); + + if (clearOldDraft) { + crl::on_main(&history->session(), clearOldDraft); + } + return true; + }; + auto callback = [=, chosen = std::move(chosen)]( + Controller::Chosen thread) mutable { + const auto weak = Ui::MakeWeak(state->box); + if (!chosen(thread)) { + return; + } else if (const auto strong = weak.data()) { + strong->closeBox(); + } + }; + state->controller->singleChosen( + ) | rpl::start_with_next(std::move(callback), state->box->lifetime()); +} + +void EditDraftOptions(EditDraftOptionsArgs &&args) { + const auto &draft = args.draft; + const auto session = &args.show->session(); + const auto replyItem = session->data().message(draft.reply.messageId); + const auto previewDataRaw = draft.webpage.id + ? session->data().webpage(draft.webpage.id).get() + : nullptr; + const auto previewData = (previewDataRaw + && !previewDataRaw->pendingTill + && !previewDataRaw->failed) + ? previewDataRaw + : nullptr; + if (!replyItem && !previewData) { + return; + } + args.show->show( + Box(DraftOptionsBox, std::move(args), replyItem, previewData)); +} + +} // namespace HistoryView::Controls diff --git a/Telegram/SourceFiles/history/view/controls/history_view_draft_options.h b/Telegram/SourceFiles/history/view/controls/history_view_draft_options.h new file mode 100644 index 00000000000000..798e1fa0cc2f51 --- /dev/null +++ b/Telegram/SourceFiles/history/view/controls/history_view_draft_options.h @@ -0,0 +1,46 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "data/data_drafts.h" + +class History; +struct MessageLinkRange; + +namespace ChatHelpers { +class Show; +} // namespace ChatHelpers + +namespace Window { +class SessionController; +} // namespace Window + +namespace HistoryView::Controls { + +class WebpageResolver; + +struct EditDraftOptionsArgs { + std::shared_ptr show; + not_null history; + Data::Draft draft; + QString usedLink; + std::vector links; + std::shared_ptr resolver; + Fn done; + Fn highlight; + Fn clearOldDraft; +}; + +void EditDraftOptions(EditDraftOptionsArgs &&args); + +void ShowReplyToChatBox( + std::shared_ptr show, + FullReplyTo reply, + Fn clearOldDraft = nullptr); + +} // namespace HistoryView::Controls diff --git a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp index ae1a6b6f297941..2490c3ac509f64 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp @@ -29,6 +29,14 @@ For license and copyright information please follow this link: #include "styles/style_chat.h" #include "styles/style_chat_helpers.h" +#include "apiwrap.h" +#include "boxes/peer_list_controllers.h" +#include "data/data_changes.h" +#include "settings/settings_common.h" +#include "ui/widgets/buttons.h" +#include "styles/style_menu_icons.h" +#include "styles/style_settings.h" + namespace HistoryView::Controls { namespace { @@ -117,7 +125,7 @@ void ForwardPanel::checkTexts() { : kNameNoCaptionsVersion; if (keepNames) { for (const auto item : _data.items) { - if (const auto from = item->senderOriginal()) { + if (const auto from = item->originalSender()) { version += from->nameVersion(); } else if (const auto info = item->hiddenSenderInfo()) { ++version; @@ -154,7 +162,7 @@ void ForwardPanel::updateTexts() { auto names = std::vector(); names.reserve(_data.items.size()); for (const auto item : _data.items) { - if (const auto from = item->senderOriginal()) { + if (const auto from = item->originalSender()) { if (!insertedPeers.contains(from)) { insertedPeers.emplace(from); names.push_back(from->shortName()); @@ -354,9 +362,9 @@ void ForwardPanel::paint( if (preview) { auto to = QRect( x, - y + st::msgReplyPadding.top(), - st::msgReplyBarSize.height(), - st::msgReplyBarSize.height()); + y + (st::historyReplyHeight - st::historyReplyPreview) / 2, + st::historyReplyPreview, + st::historyReplyPreview); p.drawPixmap(to.x(), to.y(), preview->pixSingle( preview->size() / style::DevicePixelRatio(), { @@ -367,10 +375,7 @@ void ForwardPanel::paint( Ui::FillSpoilerRect(p, to, Ui::DefaultImageSpoiler().frame( _spoiler->index(now, pausedSpoiler))); } - const auto skip = st::msgReplyBarSize.height() - + st::msgReplyBarSkip - - st::msgReplyBarSize.width() - - st::msgReplyBarPos.x(); + const auto skip = st::historyReplyPreview + st::msgReplyBarSkip; x += skip; available -= skip; } @@ -395,4 +400,115 @@ void ForwardPanel::paint( }); } +void ClearDraftReplyTo(not_null thread, FullMsgId equalTo) { + ClearDraftReplyTo( + thread->owningHistory(), + thread->topicRootId(), + equalTo); +} + +void ClearDraftReplyTo( + not_null history, + MsgId topicRootId, + FullMsgId equalTo) { + const auto local = history->localDraft(topicRootId); + if (!local || (equalTo && local->reply.messageId != equalTo)) { + return; + } + auto draft = *local; + draft.reply = { .topicRootId = topicRootId }; + if (Data::DraftIsNull(&draft)) { + history->clearLocalDraft(topicRootId); + } else { + history->setLocalDraft( + std::make_unique(std::move(draft))); + } + if (const auto thread = history->threadFor(topicRootId)) { + history->session().api().saveDraftToCloudDelayed(thread); + } +} + +void EditWebPageOptions( + std::shared_ptr show, + not_null webpage, + Data::WebPageDraft draft, + Fn done) { + show->show(Box([=](not_null box) { + box->setTitle(rpl::single(u"Link Preview"_q)); + + struct State { + rpl::variable result; + Ui::SettingsButton *large = nullptr; + Ui::SettingsButton *small = nullptr; + }; + const auto state = box->lifetime().make_state(State{ + .result = draft, + }); + + state->large = Settings::AddButton( + box->verticalLayout(), + rpl::single(u"Force large media"_q), + st::settingsButton, + { &st::menuIconMakeBig }); + state->large->setClickedCallback([=] { + auto copy = state->result.current(); + copy.forceLargeMedia = true; + copy.forceSmallMedia = false; + state->result = copy; + }); + + state->small = Settings::AddButton( + box->verticalLayout(), + rpl::single(u"Force small media"_q), + st::settingsButton, + { &st::menuIconMakeSmall }); + state->small->setClickedCallback([=] { + auto copy = state->result.current(); + copy.forceSmallMedia = true; + copy.forceLargeMedia = false; + state->result = copy; + }); + + state->result.value( + ) | rpl::start_with_next([=](const Data::WebPageDraft &draft) { + state->large->setColorOverride(draft.forceLargeMedia + ? st::windowActiveTextFg->c + : std::optional()); + state->small->setColorOverride(draft.forceSmallMedia + ? st::windowActiveTextFg->c + : std::optional()); + }, box->lifetime()); + + Settings::AddButton( + box->verticalLayout(), + state->result.value( + ) | rpl::map([=](const Data::WebPageDraft &draft) { + return draft.invert + ? u"Above message"_q + : u"Below message"_q; + }), + st::settingsButton, + { &st::menuIconChangeOrder } + )->setClickedCallback([=] { + auto copy = state->result.current(); + copy.invert = !copy.invert; + state->result = copy; + }); + + box->addButton(tr::lng_settings_save(), [=] { + const auto weak = Ui::MakeWeak(box.get()); + auto result = state->result.current(); + result.manual = true; + done(result); + if (const auto strong = weak.data()) { + strong->closeBox(); + } + }); + box->addButton(tr::lng_cancel(), [=] { + box->closeBox(); + }); + })); + +} + } // namespace HistoryView::Controls diff --git a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.h b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.h index 7f6d23bbae70e7..fa72f04e97f658 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.h @@ -20,6 +20,7 @@ class SpoilerAnimation; namespace Data { class Thread; +struct WebPageDraft; } // namespace Data namespace Window { @@ -71,4 +72,16 @@ class ForwardPanel final : public base::has_weak_ptr { }; +void ClearDraftReplyTo(not_null thread, FullMsgId equalTo); +void ClearDraftReplyTo( + not_null history, + MsgId topicRootId, + FullMsgId equalTo); + +void EditWebPageOptions( + std::shared_ptr show, + not_null webpage, + Data::WebPageDraft draft, + Fn done); + } // namespace HistoryView::Controls diff --git a/Telegram/SourceFiles/history/view/controls/history_view_webpage_processor.cpp b/Telegram/SourceFiles/history/view/controls/history_view_webpage_processor.cpp new file mode 100644 index 00000000000000..50abe57bd51352 --- /dev/null +++ b/Telegram/SourceFiles/history/view/controls/history_view_webpage_processor.cpp @@ -0,0 +1,399 @@ +/* +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 "history/view/controls/history_view_webpage_processor.h" + +#include "base/unixtime.h" +#include "data/data_chat_participant_status.h" +#include "data/data_file_origin.h" +#include "data/data_session.h" +#include "data/data_web_page.h" +#include "history/history.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" + +namespace HistoryView::Controls { + +WebPageText TitleAndDescriptionFromWebPage(not_null d) { + QString resultTitle, resultDescription; + const auto document = d->document; + const auto author = d->author; + const auto siteName = d->siteName; + const auto title = d->title; + const auto description = d->description; + const auto filenameOrUrl = [&] { + return ((document && !document->filename().isEmpty()) + ? document->filename() + : d->url); + }; + const auto authorOrFilename = [&] { + return (author.isEmpty() + ? filenameOrUrl() + : author); + }; + const auto descriptionOrAuthor = [&] { + return (description.text.isEmpty() + ? authorOrFilename() + : description.text); + }; + if (siteName.isEmpty()) { + if (title.isEmpty()) { + if (description.text.isEmpty()) { + resultTitle = author; + resultDescription = filenameOrUrl(); + } else { + resultTitle = description.text; + resultDescription = authorOrFilename(); + } + } else { + resultTitle = title; + resultDescription = descriptionOrAuthor(); + } + } else { + resultTitle = siteName; + resultDescription = title.isEmpty() + ? descriptionOrAuthor() + : title; + } + return { resultTitle, resultDescription }; +} + +bool DrawWebPageDataPreview( + QPainter &p, + not_null webpage, + not_null context, + QRect to) { + const auto document = webpage->document; + const auto photo = webpage->photo; + if ((!photo || photo->isNull()) + && (!document + || !document->hasThumbnail() + || document->isPatternWallPaper())) { + return false; + } + + const auto preview = photo + ? photo->getReplyPreview(Data::FileOrigin(), context, false) + : document->getReplyPreview(Data::FileOrigin(), context, false); + if (preview) { + const auto w = preview->width(); + const auto h = preview->height(); + if (w == h) { + p.drawPixmap(to.x(), to.y(), preview->pix()); + } else { + const auto from = (w > h) + ? QRect((w - h) / 2, 0, h, h) + : QRect(0, (h - w) / 2, w, w); + p.drawPixmap(to, preview->pix(), from); + } + } + return true; +} + +[[nodiscard]] bool ShowWebPagePreview(WebPageData *page) { + return page && !page->failed; +} + +WebPageText ProcessWebPageData(WebPageData *page) { + auto previewText = TitleAndDescriptionFromWebPage(page); + if (previewText.title.isEmpty()) { + if (page->document) { + previewText.title = tr::lng_attach_file(tr::now); + } else if (page->photo) { + previewText.title = tr::lng_attach_photo(tr::now); + } + } + return previewText; +} + +WebpageResolver::WebpageResolver(not_null session) +: _session(session) +, _api(&session->mtp()) { +} + +std::optional WebpageResolver::lookup( + const QString &link) const { + const auto i = _cache.find(link); + return (i == end(_cache)) + ? std::optional() + : (i->second && !i->second->failed) + ? i->second + : nullptr; +} + +QString WebpageResolver::find(not_null page) const { + for (const auto &[link, cached] : _cache) { + if (cached == page) { + return link; + } + } + return QString(); +} + +void WebpageResolver::request(const QString &link) { + if (_requestLink == link) { + return; + } + const auto done = [=](const MTPDmessageMediaWebPage &data) { + const auto page = _session->data().processWebpage(data.vwebpage()); + if (page->pendingTill > 0 + && page->pendingTill < base::unixtime::now()) { + page->pendingTill = 0; + page->failed = true; + } + _cache.emplace(link, page->failed ? nullptr : page.get()); + _resolved.fire_copy(link); + }; + const auto fail = [=] { + _cache.emplace(link, nullptr); + _resolved.fire_copy(link); + }; + _requestLink = link; + _requestId = _api.request( + MTPmessages_GetWebPagePreview( + MTP_flags(0), + MTP_string(link), + MTPVector() + )).done([=](const MTPMessageMedia &result, mtpRequestId requestId) { + if (_requestId == requestId) { + _requestId = 0; + } + result.match([=](const MTPDmessageMediaWebPage &data) { + done(data); + }, [&](const auto &d) { + fail(); + }); + }).fail([=](const MTP::Error &error, mtpRequestId requestId) { + if (_requestId == requestId) { + _requestId = 0; + } + fail(); + }).send(); +} + +void WebpageResolver::cancel(const QString &link) { + if (_requestLink == link) { + _api.request(base::take(_requestId)).cancel(); + } +} + +WebpageProcessor::WebpageProcessor( + not_null history, + not_null field) +: _history(history) +, _resolver(std::make_shared(&history->session())) +, _parser(field) +, _timer([=] { + if (!ShowWebPagePreview(_data) || _link.isEmpty()) { + return; + } + _resolver->request(_link); +}) { + _history->session().downloaderTaskFinished( + ) | rpl::filter([=] { + return _data && (_data->document || _data->photo); + }) | rpl::start_with_next([=] { + _repaintRequests.fire({}); + }, _lifetime); + + _history->owner().webPageUpdates( + ) | rpl::filter([=](not_null page) { + return (_data == page.get()); + }) | rpl::start_with_next([=] { + updateFromData(); + }, _lifetime); + + _parser.list().changes( + ) | rpl::start_with_next([=](QStringList &&parsed) { + _parsedLinks = std::move(parsed); + checkPreview(); + }, _lifetime); + + _resolver->resolved() | rpl::start_with_next([=](QString link) { + if (_link != link + || _draft.removed + || (_draft.manual && _draft.url != link)) { + return; + } + _data = _resolver->lookup(link).value_or(nullptr); + if (_data) { + _draft.id = _data->id; + _draft.url = _data->url; + updateFromData(); + } else { + _links = QStringList(); + checkPreview(); + } + }, _lifetime); +} + +rpl::producer<> WebpageProcessor::repaintRequests() const { + return _repaintRequests.events(); +} + +Data::WebPageDraft WebpageProcessor::draft() const { + return _draft; +} + +std::shared_ptr WebpageProcessor::resolver() const { + return _resolver; +} + +const std::vector &WebpageProcessor::links() const { + return _parser.ranges(); +} + +QString WebpageProcessor::link() const { + return _link; +} + +void WebpageProcessor::apply(Data::WebPageDraft draft, bool reparse) { + const auto was = _link; + if (draft.removed) { + _draft = draft; + if (_parsedLinks.empty()) { + _draft.removed = false; + } + _data = nullptr; + _links = QStringList(); + _link = QString(); + _parsed = WebpageParsed(); + updateFromData(); + } else if (draft.manual && !draft.url.isEmpty()) { + _draft = draft; + _parsedLinks = QStringList(); + _links = QStringList(); + _link = _draft.url; + const auto page = draft.id + ? _history->owner().webpage(draft.id).get() + : nullptr; + if (page && page->url == draft.url) { + _data = page; + if (const auto link = _resolver->find(page); !link.isEmpty()) { + _link = link; + } + updateFromData(); + } else { + _resolver->request(_link); + return; + } + } else if (!draft.manual && !_draft.manual) { + _draft = draft; + checkNow(reparse); + } + if (_link != was) { + _resolver->cancel(was); + } +} + +void WebpageProcessor::updateFromData() { + _timer.cancel(); + auto parsed = WebpageParsed(); + if (ShowWebPagePreview(_data)) { + if (const auto till = _data->pendingTill) { + parsed.drawPreview = [](QPainter &p, QRect to) { + return false; + }; + parsed.title = tr::lng_preview_loading(tr::now); + parsed.description = _link; + + const auto timeout = till - base::unixtime::now(); + _timer.callOnce( + std::max(timeout, 0) * crl::time(1000)); + } else { + const auto webpage = _data; + const auto context = _history->peer; + const auto preview = ProcessWebPageData(_data); + parsed.title = preview.title; + parsed.description = preview.description; + parsed.drawPreview = [=](QPainter &p, QRect to) { + return DrawWebPageDataPreview(p, webpage, context, to); + }; + } + } + _parsed = std::move(parsed); + _repaintRequests.fire({}); +} + +void WebpageProcessor::setDisabled(bool disabled) { + _parser.setDisabled(disabled); + if (disabled) { + apply({ .removed = true }); + } else { + checkNow(false); + } +} + +void WebpageProcessor::checkNow(bool force) { + _parser.parseNow(); + if (force) { + _link = QString(); + _links = QStringList(); + if (_parsedLinks.isEmpty()) { + _data = nullptr; + updateFromData(); + return; + } + } + checkPreview(); +} + +void WebpageProcessor::checkPreview() { + const auto previewRestricted = _history->peer + && _history->peer->amRestricted(ChatRestriction::EmbedLinks); + if (_parsedLinks.empty()) { + _draft.removed = false; + } + if (_draft.removed) { + return; + } else if (previewRestricted) { + apply({ .removed = true }); + _draft.removed = false; + return; + } else if (_draft.manual) { + return; + } else if (_links == _parsedLinks) { + return; + } + _links = _parsedLinks; + + auto page = (WebPageData*)nullptr; + auto chosen = QString(); + for (const auto &link : _links) { + const auto value = _resolver->lookup(link); + if (!value) { + chosen = link; + break; + } else if (*value) { + chosen = link; + page = *value; + break; + } + } + if (_link != chosen) { + _resolver->cancel(_link); + _link = chosen; + if (!page && !_link.isEmpty()) { + _resolver->request(_link); + } + } + if (page) { + _data = page; + _draft.id = _data->id; + _draft.url = _data->url; + } else { + _data = nullptr; + _draft = {}; + } + updateFromData(); +} + +rpl::producer WebpageProcessor::parsedValue() const { + return _parsed.value(); +} + +} // namespace HistoryView::Controls diff --git a/Telegram/SourceFiles/history/view/controls/history_view_webpage_processor.h b/Telegram/SourceFiles/history/view/controls/history_view_webpage_processor.h new file mode 100644 index 00000000000000..f27d1ad6cd42d4 --- /dev/null +++ b/Telegram/SourceFiles/history/view/controls/history_view_webpage_processor.h @@ -0,0 +1,129 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "data/data_drafts.h" +#include "chat_helpers/message_field.h" +#include "mtproto/sender.h" + +class History; + +namespace Main { +class Session; +} // namespace Main + +namespace Ui { +class InputField; +} // namespace Ui + +namespace HistoryView::Controls { + +struct WebPageText { + QString title; + QString description; +}; + +[[nodiscard]] WebPageText TitleAndDescriptionFromWebPage( + not_null data); + +bool DrawWebPageDataPreview( + QPainter &p, + not_null webpage, + not_null context, + QRect to); + +[[nodiscard]] bool ShowWebPagePreview(WebPageData *page); +[[nodiscard]] WebPageText ProcessWebPageData(WebPageData *page); + +struct WebpageParsed { + Fn drawPreview; + QString title; + QString description; + + explicit operator bool() const { + return drawPreview != nullptr; + } +}; + +class WebpageResolver final { +public: + explicit WebpageResolver(not_null session); + + [[nodiscard]] std::optional lookup( + const QString &link) const; + [[nodiscard]] rpl::producer resolved() const { + return _resolved.events(); + } + + [[nodiscard]] QString find(not_null page) const; + + void request(const QString &link); + void cancel(const QString &link); + +private: + const not_null _session; + MTP::Sender _api; + base::flat_map _cache; + rpl::event_stream _resolved; + + QString _requestLink; + mtpRequestId _requestId = 0; + +}; + +class WebpageProcessor final { +public: + WebpageProcessor( + not_null history, + not_null field); + + void setDisabled(bool disabled); + void checkNow(bool force); + + // If editing a message without a preview we don't want to show + // parsed preview until links set is changed in the message. + // + // If writing a new message we want to parse links immediately, + // unless preview was removed in the draft or manual. + void apply(Data::WebPageDraft draft, bool reparse = true); + [[nodiscard]] Data::WebPageDraft draft() const; + [[nodiscard]] std::shared_ptr resolver() const; + [[nodiscard]] const std::vector &links() const; + [[nodiscard]] QString link() const; + + [[nodiscard]] rpl::producer<> repaintRequests() const; + [[nodiscard]] rpl::producer parsedValue() const; + + [[nodiscard]] rpl::lifetime &lifetime() { + return _lifetime; + } + +private: + void updateFromData(); + void checkPreview(); + + const not_null _history; + const std::shared_ptr _resolver; + MessageLinksParser _parser; + + QStringList _parsedLinks; + QStringList _links; + QString _link; + WebPageData *_data = nullptr; + Data::WebPageDraft _draft; + + rpl::event_stream<> _repaintRequests; + rpl::variable _parsed; + + base::Timer _timer; + + rpl::lifetime _lifetime; + +}; + +} // namespace HistoryView::Controls diff --git a/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp b/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp index 329944fe9d84f6..bf44fda9013ea3 100644 --- a/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp +++ b/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp @@ -655,7 +655,7 @@ BottomInfo::Data BottomInfoDataFromMessage(not_null message) { } if (const auto msgsigned = item->Get()) { if (!msgsigned->isAnonymousRank) { - result.author = msgsigned->author; + result.author = msgsigned->postAuthor; } } if (message->displayedEditDate()) { diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index 9b58663d3538a5..c09a731063140f 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -53,6 +53,7 @@ For license and copyright information please follow this link: #include "data/data_stories.h" #include "data/data_groups.h" #include "data/data_channel.h" +#include "data/data_chat.h" #include "data/data_file_click_handler.h" #include "data/data_file_origin.h" #include "data/data_message_reactions.h" @@ -540,9 +541,6 @@ bool AddRescheduleAction( if (!item || !item->isScheduled()) { continue; } - if (!item->media() || !item->media()->webpage()) { - options.removeWebPageId = true; - } Api::RescheduleMessage(item, options); // Increase the scheduled date by 1s to keep the order. options.scheduled += 1; @@ -590,20 +588,37 @@ bool AddReplyToMessageAction( const auto peer = item ? item->history()->peer.get() : nullptr; if (!item || !item->isRegular() - || !(topic - ? Data::CanSendAnything(topic) - : Data::CanSendAnything(peer)) || (context != Context::History && context != Context::Replies)) { return false; } - const auto owner = &item->history()->owner(); + const auto canSendReply = topic + ? Data::CanSendAnything(topic) + : Data::CanSendAnything(peer); + const auto canReply = canSendReply || [&] { + const auto peer = item->history()->peer; + if (const auto chat = peer->asChat()) { + return !chat->isForbidden(); + } else if (const auto channel = peer->asChannel()) { + return !channel->isForbidden(); + } + return true; + }(); + if (!canReply) { + return false; + } + + const auto "e = request.quote; + auto text = quote.empty() + ? tr::lng_context_reply_msg(tr::now) + : tr::lng_context_quote_and_reply(tr::now); + text.replace('&', u"&&"_q); const auto itemId = item->fullId(); - menu->addAction(tr::lng_context_reply_msg(tr::now), [=] { - const auto item = owner->message(itemId); + menu->addAction(text, [=] { if (!item) { return; + } else { + list->replyToMessageRequestNotify({ itemId, quote }); } - list->replyToMessageRequestNotify(item->fullId()); }, &st::menuIconReply); return true; } @@ -927,7 +942,6 @@ void AddTopMessageActions( not_null menu, const ContextMenuRequest &request, not_null list) { - AddReplyToMessageAction(menu, request, list); AddGoToMessageAction(menu, request, list); AddViewRepliesAction(menu, request, list); AddEditMessageAction(menu, request, list); @@ -999,6 +1013,8 @@ base::unique_qptr FillContextMenu( list, st::popupMenuWithIcons); + AddReplyToMessageAction(result, request, list); + if (request.overSelection && !list->hasCopyRestrictionForSelected() && !list->getSelectedText().empty()) { diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.h b/Telegram/SourceFiles/history/view/history_view_context_menu.h index 1dac400e971e4d..8f81ff1f19a45f 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.h +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.h @@ -47,6 +47,7 @@ struct ContextMenuRequest { HistoryItem *item = nullptr; SelectedItems selectedItems; TextForMimeData selectedText; + TextWithEntities quote; bool overSelection = false; PointState pointState = PointState(); }; diff --git a/Telegram/SourceFiles/history/view/history_view_cursor_state.h b/Telegram/SourceFiles/history/view/history_view_cursor_state.h index 6f51d8b08135b7..0fbfc6c5b28430 100644 --- a/Telegram/SourceFiles/history/view/history_view_cursor_state.h +++ b/Telegram/SourceFiles/history/view/history_view_cursor_state.h @@ -49,6 +49,7 @@ struct TextState { FullMsgId itemId; CursorState cursor = CursorState::None; ClickHandlerPtr link; + bool overMessageText = false; bool afterSymbol = false; bool customTooltip = false; uint16 symbol = 0; @@ -63,6 +64,7 @@ struct StateRequest { result.flags = flags; return result; } + bool onlyMessageText = false; }; enum class InfoDisplayType : char { diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index c6126eeb7e63a2..f549a1ba20171d 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -68,10 +68,10 @@ Element *MousedElement/* = nullptr*/; HistoryMessageForwarded *prevForwarded, not_null item, HistoryMessageForwarded *forwarded) { - const auto sender = previous->senderOriginal(); + const auto sender = previous->originalSender(); if ((prevForwarded != nullptr) != (forwarded != nullptr)) { return false; - } else if (sender != item->senderOriginal()) { + } else if (sender != item->originalSender()) { return false; } else if (!prevForwarded || sender) { return true; @@ -178,7 +178,7 @@ bool DefaultElementDelegate::elementIsChatWide() { return false; } -void DefaultElementDelegate::elementReplyTo(const FullMsgId &to) { +void DefaultElementDelegate::elementReplyTo(const FullReplyTo &to) { } void DefaultElementDelegate::elementStartInteraction( @@ -275,8 +275,10 @@ QString DateTooltipText(not_null view) { } if (view->isSignedAuthorElided()) { if (const auto msgsigned = item->Get()) { - dateText += '\n' - + tr::lng_signed_author(tr::now, lt_user, msgsigned->author); + dateText += '\n' + tr::lng_signed_author( + tr::now, + lt_user, + msgsigned->postAuthor); } } return dateText; @@ -338,7 +340,6 @@ void UnreadBar::paint( text); } - void DateBadge::init(const QString &date) { text = date; width = st::msgServiceFont->width(text); @@ -361,6 +362,81 @@ void DateBadge::paint( ServiceMessagePainter::PaintDate(p, st, text, width, y, w, chatWide); } +void ServicePreMessage::init(TextWithEntities string) { + text = Ui::Text::String( + st::serviceTextStyle, + string, + kMarkupTextOptions, + st::msgMinWidth); +} + +int ServicePreMessage::resizeToWidth(int newWidth, bool chatWide) { + width = newWidth; + if (chatWide) { + accumulate_min( + width, + st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left()); + } + auto contentWidth = width; + contentWidth -= st::msgServiceMargin.left() + st::msgServiceMargin.left(); // two small margins + if (contentWidth < st::msgServicePadding.left() + st::msgServicePadding.right() + 1) { + contentWidth = st::msgServicePadding.left() + st::msgServicePadding.right() + 1; + } + + auto maxWidth = text.maxWidth() + + st::msgServicePadding.left() + + st::msgServicePadding.right(); + auto minHeight = text.minHeight(); + + auto nwidth = qMax(contentWidth + - st::msgServicePadding.left() + - st::msgServicePadding.right(), 0); + height = (contentWidth >= maxWidth) + ? minHeight + : text.countHeight(nwidth); + height += st::msgServicePadding.top() + + st::msgServicePadding.bottom() + + st::msgServiceMargin.top() + + st::msgServiceMargin.bottom(); + return height; +} + +void ServicePreMessage::paint( + Painter &p, + const PaintContext &context, + QRect g, + bool chatWide) const { + const auto top = g.top() - height - st::msgMargin.top(); + p.translate(0, top); + + const auto rect = QRect(0, 0, width, height) + - st::msgServiceMargin; + const auto trect = rect - st::msgServicePadding; + + ServiceMessagePainter::PaintComplexBubble( + p, + context.st, + rect.left(), + rect.width(), + text, + trect); + + p.setBrush(Qt::NoBrush); + p.setPen(context.st->msgServiceFg()); + p.setFont(st::msgServiceFont); + text.draw(p, { + .position = trect.topLeft(), + .availableWidth = trect.width(), + .align = style::al_top, + .palette = &context.st->serviceTextPalette(), + .now = context.now, + //.selection = context.selection, + .fullWidthSelection = false, + }); + + p.translate(0, -top); +} + void FakeBotAboutTop::init() { if (!text.isEmpty()) { return; @@ -414,6 +490,10 @@ not_null Element::history() const { return _data->history(); } +uint8 Element::colorIndex() const { + return data()->colorIndex(); +} + QDateTime Element::dateTime() const { return _dateTime; } @@ -481,8 +561,8 @@ void Element::prepareCustomEmojiPaint( } clearCustomEmojiRepaint(); p.setInactive(context.paused); - if (!_heavyCustomEmoji) { - _heavyCustomEmoji = true; + if (!(_flags & Flag::HeavyCustomEmoji)) { + _flags |= Flag::HeavyCustomEmoji; history()->owner().registerHeavyViewPart(const_cast(this)); } } @@ -496,8 +576,8 @@ void Element::prepareCustomEmojiPaint( } clearCustomEmojiRepaint(); p.setInactive(context.paused); - if (!_heavyCustomEmoji) { - _heavyCustomEmoji = true; + if (!(_flags & Flag::HeavyCustomEmoji)) { + _flags |= Flag::HeavyCustomEmoji; history()->owner().registerHeavyViewPart(const_cast(this)); } } @@ -722,7 +802,7 @@ auto Element::contextDependentServiceText() -> TextWithLinks { if (!info) { return {}; } - if (_delegate->elementContext() == Context::Replies) { + if (_context == Context::Replies) { if (info->created()) { return { { tr::lng_action_topic_created_inside(tr::now) } }; } @@ -971,7 +1051,9 @@ bool Element::computeIsAttachToPrevious(not_null previous) { || !item->from()->isChannel()); }; const auto item = data(); - if (!Has() && !Has()) { + if (!Has() + && !Has() + && !Has()) { const auto prev = previous->data(); const auto previousMarkup = prev->inlineReplyMarkup(); const auto possible = (std::abs(prev->date() - item->date()) @@ -1183,6 +1265,18 @@ void Element::setDisplayDate(bool displayDate) { } } +void Element::setServicePreMessage(TextWithEntities text) { + if (!text.empty()) { + AddComponents(ServicePreMessage::Bit()); + const auto service = Get(); + service->init(std::move(text)); + setPendingResize(); + } else if (Has()) { + RemoveComponents(ServicePreMessage::Bit()); + setPendingResize(); + } +} + void Element::setAttachToNext(bool attachToNext, Element *next) { Expects(next || !attachToNext); @@ -1321,7 +1415,7 @@ auto Element::verticalRepaintRange() const -> VerticalRepaintRange { } bool Element::hasHeavyPart() const { - return _heavyCustomEmoji; + return (_flags & Flag::HeavyCustomEmoji); } void Element::checkHeavyPart() { @@ -1355,11 +1449,11 @@ void Element::unloadHeavyPart() { if (_media) { _media->unloadHeavyPart(); } - if (_heavyCustomEmoji) { - _heavyCustomEmoji = false; + if (_flags & Flag::HeavyCustomEmoji) { + _flags &= ~Flag::HeavyCustomEmoji; _text.unloadPersistentAnimation(); if (const auto reply = data()->Get()) { - reply->replyToText.unloadPersistentAnimation(); + reply->unloadPersistentAnimation(); } } } @@ -1533,8 +1627,8 @@ Element::~Element() { // Delete media while owner still exists. clearSpecialOnlyEmoji(); base::take(_media); - if (_heavyCustomEmoji) { - _heavyCustomEmoji = false; + if (_flags & Flag::HeavyCustomEmoji) { + _flags &= ~Flag::HeavyCustomEmoji; _text.unloadPersistentAnimation(); checkHeavyPart(); } diff --git a/Telegram/SourceFiles/history/view/history_view_element.h b/Telegram/SourceFiles/history/view/history_view_element.h index 8eb8bab1759353..f65825b92cf312 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.h +++ b/Telegram/SourceFiles/history/view/history_view_element.h @@ -100,7 +100,7 @@ class ElementDelegate { virtual void elementHandleViaClick(not_null bot) = 0; virtual bool elementIsChatWide() = 0; virtual not_null elementPathShiftGradient() = 0; - virtual void elementReplyTo(const FullMsgId &to) = 0; + virtual void elementReplyTo(const FullReplyTo &to) = 0; virtual void elementStartInteraction(not_null view) = 0; virtual void elementStartPremium( not_null view, @@ -149,7 +149,7 @@ class DefaultElementDelegate : public ElementDelegate { const FullMsgId &context) override; void elementHandleViaClick(not_null bot) override; bool elementIsChatWide() override; - void elementReplyTo(const FullMsgId &to) override; + void elementReplyTo(const FullReplyTo &to) override; void elementStartInteraction(not_null view) override; void elementStartPremium( not_null view, @@ -234,6 +234,26 @@ struct DateBadge : public RuntimeComponent { }; +// Any HistoryView::Element can have this Component for +// displaying some text in layout of a service message above the message. +struct ServicePreMessage + : public RuntimeComponent { + void init(TextWithEntities string); + + int resizeToWidth(int newWidth, bool chatWide); + + void paint( + Painter &p, + const PaintContext &context, + QRect g, + bool chatWide) const; + + Ui::Text::String text; + int width = 0; + int height = 0; + +}; + struct FakeBotAboutTop : public RuntimeComponent { void init(); @@ -257,18 +277,19 @@ class Element , public base::has_weak_ptr { public: enum class Flag : uint16 { - ServiceMessage = 0x0001, - NeedsResize = 0x0002, - AttachedToPrevious = 0x0004, - AttachedToNext = 0x0008, + ServiceMessage = 0x0001, + NeedsResize = 0x0002, + AttachedToPrevious = 0x0004, + AttachedToNext = 0x0008, BubbleAttachedToPrevious = 0x0010, - BubbleAttachedToNext = 0x0020, - HiddenByGroup = 0x0040, - SpecialOnlyEmoji = 0x0080, - CustomEmojiRepainting = 0x0100, - ScheduledUntilOnline = 0x0200, - TopicRootReply = 0x0400, - MediaOverriden = 0x0800, + BubbleAttachedToNext = 0x0020, + HiddenByGroup = 0x0040, + SpecialOnlyEmoji = 0x0080, + CustomEmojiRepainting = 0x0100, + ScheduledUntilOnline = 0x0200, + TopicRootReply = 0x0400, + MediaOverriden = 0x0800, + HeavyCustomEmoji = 0x1000, }; using Flags = base::flags; friend inline constexpr auto is_flag_type(Flag) { return true; } @@ -286,6 +307,7 @@ class Element [[nodiscard]] Context context() const; void refreshDataId(); + [[nodiscard]] uint8 colorIndex() const; [[nodiscard]] QDateTime dateTime() const; [[nodiscard]] int y() const; @@ -339,6 +361,7 @@ class Element // For blocks context this should be called only from recountDisplayDate(). void setDisplayDate(bool displayDate); + void setServicePreMessage(TextWithEntities text); bool computeIsAttachToPrevious(not_null previous); @@ -367,8 +390,16 @@ class Element int bottom, QPoint point, InfoDisplayType type) const; - virtual TextForMimeData selectedText( + virtual TextForMimeData selectedText(TextSelection selection) const = 0; + virtual TextWithEntities selectedQuote(TextSelection selection) const = 0; + virtual TextWithEntities selectedQuote( + const Ui::Text::String &text, TextSelection selection) const = 0; + virtual TextSelection selectionFromQuote( + const TextWithEntities "e) const = 0; + virtual TextSelection selectionFromQuote( + const Ui::Text::String &text, + const TextWithEntities "e) const = 0; [[nodiscard]] virtual TextSelection adjustSelection( TextSelection selection, TextSelectType type) const; @@ -568,9 +599,10 @@ class Element int _indexInBlock = -1; mutable Flags _flags = Flag(0); - mutable bool _heavyCustomEmoji = false; Context _context = Context(); }; +constexpr auto size = sizeof(Element); + } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index 0e9a298b9d07fc..0705d751a9ac18 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -353,6 +353,11 @@ ListWidget::ListWidget( update(); }, lifetime()); + session().data().peerDecorationsUpdated( + ) | rpl::start_with_next([=] { + update(); + }, lifetime()); + session().data().itemRemoved( ) | rpl::start_with_next([=](not_null item) { itemRemoved(item); @@ -1735,7 +1740,7 @@ not_null ListWidget::elementPathShiftGradient() { return _pathGradient.get(); } -void ListWidget::elementReplyTo(const FullMsgId &to) { +void ListWidget::elementReplyTo(const FullReplyTo &to) { replyToMessageRequestNotify(to); } @@ -2474,7 +2479,7 @@ void ListWidget::mouseDoubleClickEvent(QMouseEvent *e) { mouseActionCancel(); switch (CurrentQuickAction()) { case DoubleClickQuickAction::Reply: { - replyToMessageRequestNotify(_overElement->data()->fullId()); + replyToMessageRequestNotify({ _overElement->data()->fullId() }); } break; case DoubleClickQuickAction::React: { toggleFavoriteReaction(_overElement); @@ -2571,6 +2576,7 @@ void ListWidget::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { : _overElement ? _overElement->data().get() : nullptr; + const auto overItemView = viewForItem(overItem); const auto clickedReaction = link ? link->property( kReactionsCountEmojiProperty).value() @@ -2597,6 +2603,9 @@ void ListWidget::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { request.view = _overElement; request.item = overItem; request.pointState = _overState.pointState; + request.quote = (overItemView && _selectedTextItem == overItem) + ? overItemView->selectedQuote(_selectedTextRange) + : TextWithEntities(); request.selectedText = _selectedText; request.selectedItems = collectSelectedItems(); const auto hasSelection = !request.selectedItems.empty() @@ -3855,12 +3864,12 @@ bool ListWidget::lastMessageEditRequestNotify() const { } } -rpl::producer ListWidget::replyToMessageRequested() const { +rpl::producer ListWidget::replyToMessageRequested() const { return _requestedToReplyToMessage.events(); } -void ListWidget::replyToMessageRequestNotify(FullMsgId item) { - _requestedToReplyToMessage.fire(std::move(item)); +void ListWidget::replyToMessageRequestNotify(FullReplyTo id) { + _requestedToReplyToMessage.fire(std::move(id)); } rpl::producer ListWidget::readMessageRequested() const { @@ -3878,10 +3887,10 @@ void ListWidget::replyNextMessage(FullMsgId fullId, bool next) { if (!view->data()->isRegular()) { return replyNextMessage(newFullId, next); } - replyToMessageRequestNotify(newFullId); + replyToMessageRequestNotify({ newFullId }); _requestedToShowMessage.fire_copy(newFullId); } else { - replyToMessageRequestNotify(FullMsgId()); + replyToMessageRequestNotify({}); _highlighter.clear(); } }; diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.h b/Telegram/SourceFiles/history/view/history_view_list_widget.h index a7aef47cee254a..0e49e9b6e62f95 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.h +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.h @@ -277,8 +277,8 @@ class ListWidget final [[nodiscard]] rpl::producer editMessageRequested() const; void editMessageRequestNotify(FullMsgId item) const; [[nodiscard]] bool lastMessageEditRequestNotify() const; - [[nodiscard]] rpl::producer replyToMessageRequested() const; - void replyToMessageRequestNotify(FullMsgId item); + [[nodiscard]] rpl::producer replyToMessageRequested() const; + void replyToMessageRequestNotify(FullReplyTo id); [[nodiscard]] rpl::producer readMessageRequested() const; [[nodiscard]] rpl::producer showMessageRequested() const; void replyNextMessage(FullMsgId fullId, bool next = true); @@ -323,7 +323,7 @@ class ListWidget final void elementHandleViaClick(not_null bot) override; bool elementIsChatWide() override; not_null elementPathShiftGradient() override; - void elementReplyTo(const FullMsgId &to) override; + void elementReplyTo(const FullReplyTo &to) override; void elementStartInteraction(not_null view) override; void elementStartPremium( not_null view, @@ -735,7 +735,7 @@ class ListWidget final base::Timer _touchScrollTimer; rpl::event_stream _requestedToEditMessage; - rpl::event_stream _requestedToReplyToMessage; + rpl::event_stream _requestedToReplyToMessage; rpl::event_stream _requestedToReadMessage; rpl::event_stream _requestedToShowMessage; diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 99fb5208626076..06163e7f26a8a0 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -56,7 +56,7 @@ namespace { constexpr auto kPlayStatusLimit = 2; const auto kPsaTooltipPrefix = "cloud_lng_tooltip_psa_"; -std::optional ExtractController( +[[nodiscard]] std::optional ExtractController( const ClickContext &context) { const auto my = context.other.value(); if (const auto controller = my.sessionWindow.get()) { @@ -65,6 +65,42 @@ std::optional ExtractController( return std::nullopt; } +[[nodiscard]] bool CheckQuoteEntities( + const EntitiesInText "eEntities, + const TextWithEntities &original, + TextSelection selection) { + auto left = quoteEntities; + const auto allowed = std::array{ + EntityType::Bold, + EntityType::Italic, + EntityType::Underline, + EntityType::StrikeOut, + EntityType::Spoiler, + EntityType::CustomEmoji, + }; + for (const auto &entity : original.entities) { + const auto from = entity.offset(); + const auto till = from + entity.length(); + if (till <= selection.from || from >= selection.to) { + continue; + } + const auto quoteFrom = std::max(from, int(selection.from)); + const auto quoteTill = std::min(till, int(selection.to)); + const auto cut = EntityInText( + entity.type(), + quoteFrom - int(selection.from), + quoteTill - quoteFrom, + entity.data()); + const auto i = ranges::find(left, cut); + if (i != left.end()) { + left.erase(i); + } else if (ranges::contains(allowed, cut.type())) { + return false; + } + } + return left.empty(); +}; + class KeyboardStyle : public ReplyKeyboard::Style { public: KeyboardStyle(const style::BotKeyboardButton &st); @@ -366,37 +402,6 @@ QString FastReplyText() { } // namespace -style::color FromNameFg( - const Ui::ChatPaintContext &context, - PeerId peerId) { - const auto st = context.st; - if (context.selected()) { - const style::color colors[] = { - st->historyPeer1NameFgSelected(), - st->historyPeer2NameFgSelected(), - st->historyPeer3NameFgSelected(), - st->historyPeer4NameFgSelected(), - st->historyPeer5NameFgSelected(), - st->historyPeer6NameFgSelected(), - st->historyPeer7NameFgSelected(), - st->historyPeer8NameFgSelected(), - }; - return colors[Data::PeerColorIndex(peerId)]; - } else { - const style::color colors[] = { - st->historyPeer1NameFg(), - st->historyPeer2NameFg(), - st->historyPeer3NameFg(), - st->historyPeer4NameFg(), - st->historyPeer5NameFg(), - st->historyPeer6NameFg(), - st->historyPeer7NameFg(), - st->historyPeer8NameFg(), - }; - return colors[Data::PeerColorIndex(peerId)]; - } -} - struct Message::CommentsButton { std::unique_ptr ripple; std::vector userpics; @@ -436,6 +441,7 @@ Message::Message( not_null data, Element *replacing) : Element(delegate, data, replacing, Flag(0)) +, _invertMedia(data->invertMedia() && !data->emptyText()) , _bottomInfo( &data->history()->owner().reactions(), BottomInfoDataFromMessage(this)) { @@ -477,7 +483,7 @@ void Message::refreshRightBadge() { } else if (data()->author()->isMegagroup()) { if (const auto msgsigned = data()->Get()) { Assert(msgsigned->isAnonymousRank); - return msgsigned->author; + return msgsigned->postAuthor; } } const auto channel = data()->history()->peer->asMegagroup(); @@ -801,9 +807,12 @@ QSize Message::performCountOptimalSize() { accumulate_max(maxWidth, namew); } if (reply) { - auto replyw = st::msgPadding.left() + reply->maxReplyWidth - st::msgReplyPadding.left() - st::msgReplyPadding.right() + st::msgPadding.right(); - if (reply->replyToVia) { - replyw += st::msgServiceFont->spacew + reply->replyToVia->maxWidth; + auto replyw = st::msgPadding.left() + + reply->maxWidth() + + st::msgPadding.right(); + if (reply->originalVia) { + replyw += st::msgServiceFont->spacew + + reply->originalVia->maxWidth; } accumulate_max(maxWidth, replyw); } @@ -874,6 +883,9 @@ int Message::marginTop() const { if (const auto bar = Get()) { result += bar->height(); } + if (const auto service = Get()) { + result += service->height; + } return result; } @@ -911,6 +923,10 @@ void Message::draw(Painter &p, const PaintContext &context) const { } } + if (const auto service = Get()) { + service->paint(p, context, g, delegate()->elementIsChatWide()); + } + if (isHidden()) { return; } @@ -1093,16 +1109,20 @@ void Message::draw(Painter &p, const PaintContext &context) const { trect.setHeight(trect.height() - (_bottomInfo.height() - st::msgDateFont->height)); } - paintText(p, trect, context); - if (mediaDisplayed) { - auto mediaHeight = media->height(); - auto mediaPosition = QPoint( - inner.left(), - trect.y() + trect.height() - mediaHeight); + auto textSelection = context.selection; + const auto mediaHeight = mediaDisplayed ? media->height() : 0; + const auto paintMedia = [&](int top) { + if (!mediaDisplayed) { + return; + } + const auto mediaSelection = _invertMedia + ? context.selection + : skipTextSelection(context.selection); + auto mediaPosition = QPoint(inner.left(), top); p.translate(mediaPosition); media->draw(p, context.translated( -mediaPosition - ).withSelection(skipTextSelection(context.selection))); + ).withSelection(mediaSelection)); if (context.reactionInfo && !displayInfo && !_reactions) { const auto add = QPoint(0, mediaHeight); context.reactionInfo->position = mediaPosition + add; @@ -1111,6 +1131,27 @@ void Message::draw(Painter &p, const PaintContext &context) const { } } p.translate(-mediaPosition); + }; + if (mediaDisplayed && _invertMedia) { + if (!mediaOnTop) { + trect.setY(trect.y() + st::mediaInBubbleSkip); + } + paintMedia(trect.y()); + trect.setY(trect.y() + + mediaHeight + + (mediaOnBottom ? 0 : st::mediaInBubbleSkip)); + textSelection = media->skipSelection(textSelection); + } + paintText(p, trect, context.withSelection(textSelection)); + if (mediaDisplayed && !_invertMedia) { + paintMedia(trect.y() + trect.height() - mediaHeight); + if (context.reactionInfo && !displayInfo && !_reactions) { + context.reactionInfo->position + = QPoint(inner.left(), trect.y() + trect.height()); + if (context.reactionInfo->effectPaint) { + context.reactionInfo->effectOffset -= QPoint(0, mediaHeight); + } + } } if (entry) { auto entryLeft = inner.left(); @@ -1345,13 +1386,9 @@ void Message::paintFromName( const auto from = item->displayFrom(); const auto info = from ? nullptr : item->hiddenSenderInfo(); Assert(from || info); - const auto service = (context.outbg || item->isPost()); - const auto st = context.st; - const auto nameFg = !service - ? FromNameFg(context, from ? from->id : info->colorPeerId) - : item->isSponsored() - ? st->boxTextFgGood() - : stm->msgServiceFg; + const auto nameFg = !context.outbg + ? FromNameFg(context, colorIndex()) + : stm->msgServiceFg->c; const auto nameText = [&] { if (from) { validateFromNameText(from); @@ -1366,11 +1403,8 @@ void Message::paintFromName( const auto x = availableLeft + std::min(availableWidth - statusWidth, nameText->maxWidth()); const auto y = trect.top(); - const auto color = QColor( - nameFg->c.red(), - nameFg->c.green(), - nameFg->c.blue(), - nameFg->c.alpha() * 115 / 255); + auto color = nameFg; + color.setAlpha(115); const auto user = from->asUser(); const auto id = user ? user->emojiStatusId() : 0; if (_fromNameStatus->id != id) { @@ -1576,9 +1610,8 @@ void Message::paintReplyInfo( QRect &trect, const PaintContext &context) const { if (const auto reply = displayedReply()) { - int32 h = st::msgReplyPadding.top() + st::msgReplyBarSize.height() + st::msgReplyPadding.bottom(); reply->paint(p, this, context, trect.x(), trect.y(), trect.width(), true); - trect.setY(trect.y() + h); + trect.setY(trect.y() + reply->height()); } } @@ -1623,7 +1656,7 @@ void Message::paintText( .availableWidth = trect.width(), .palette = &stm->textPalette, .pre = stm->preCache.get(), - .blockquote = stm->blockquoteCache.get(), + .blockquote = context.quoteCache(colorIndex()), .colors = context.st->highlightColors(), .spoiler = Ui::Text::DefaultSpoilerCache(), .now = context.now, @@ -1729,8 +1762,8 @@ void Message::clickHandlerPressedChanged( toggleTopicButtonRipple(pressed); } else if (_viewButton) { _viewButton->checkLink(handler, pressed); - } else if (const auto reply = displayedReply(); - reply && (handler == reply->replyToLink())) { + } else if (const auto reply = displayedReply() + ; reply && (handler == reply->link())) { toggleReplyRipple(pressed); } } @@ -1779,41 +1812,18 @@ void Message::toggleReplyRipple(bool pressed) { if (pressed) { if (!reply->ripple.animation && !unwrapped()) { - const auto smallTop = displayFromName() - || displayedTopicButton() - || displayForwardedFrom(); - const auto rounding = countBubbleRounding(); - - using Corner = Ui::BubbleCornerRounding; - using Radius = Ui::CachedCornerRadius; - const auto &small = Ui::CachedCornersMasks(Radius::ThumbSmall); - const auto &large = Ui::CachedCornersMasks(Radius::ThumbLarge); - const auto corners = std::array{{ - ((smallTop || (rounding.topLeft == Corner::Small)) - ? small - : large)[0], - ((smallTop || (rounding.topRight == Corner::Small)) - ? small - : large)[1], - small[2], - small[3], - }}; - - const auto &padding = st::msgReplyPadding; + const auto &padding = st::msgPadding; const auto geometry = countGeometry(); const auto item = data(); + const auto margins = reply->margins(); const auto size = QSize( - geometry.width() - - padding.left() / 2 - - padding.right(), - st::msgReplyBarSize.height() - + padding.top() - + padding.bottom()); + geometry.width() - padding.left() - padding.right(), + reply->height() - margins.top() - margins.bottom()); reply->ripple.animation = std::make_unique( st::defaultRippleAnimation, - Images::Round( - Ui::RippleAnimation::MaskByDrawer(size, true, nullptr), - corners), + Ui::RippleAnimation::RoundRectMask( + size, + st::messageQuoteStyle.radius), [=] { item->history()->owner().requestItemRepaint(item); }); } if (reply->ripple.animation) { @@ -2014,6 +2024,10 @@ TextState Message::textState( const auto media = this->media(); auto result = TextState(item); + const auto minSymbol = (_invertMedia && request.onlyMessageText) + ? visibleMediaTextLength() + : 0; + result.symbol = minSymbol; auto g = countGeometry(); if (g.width() < 1 || isHidden()) { @@ -2147,26 +2161,45 @@ TextState Message::textState( result = bottomInfoResult; } }; - if (!result.symbol && inBubble) { - if (mediaDisplayed) { - auto mediaHeight = media->height(); - auto mediaLeft = trect.x() - st::msgPadding.left(); - auto mediaTop = (trect.y() + trect.height() - mediaHeight); - - if (point.y() >= mediaTop && point.y() < mediaTop + mediaHeight) { - result = media->textState(point - QPoint(mediaLeft, mediaTop), request); - result.symbol += visibleTextLength(); - } else if (getStateText(point, trect, &result, request)) { - checkBottomInfoState(); - return result; - } else if (point.y() >= trect.y() + trect.height()) { + if (result.symbol <= minSymbol && inBubble) { + const auto mediaHeight = mediaDisplayed ? media->height() : 0; + const auto mediaLeft = trect.x() - st::msgPadding.left(); + const auto mediaTop = (!mediaDisplayed || _invertMedia) + ? (trect.y() + (mediaOnTop ? 0 : st::mediaInBubbleSkip)) + : (trect.y() + trect.height() - mediaHeight); + if (mediaDisplayed && _invertMedia) { + trect.setY(mediaTop + + mediaHeight + + (mediaOnBottom ? 0 : st::mediaInBubbleSkip)); + } + if (point.y() >= mediaTop + && point.y() < mediaTop + mediaHeight) { + result = media->textState( + point - QPoint(mediaLeft, mediaTop), + request); + if (_invertMedia) { + if (request.onlyMessageText) { + result.symbol = minSymbol; + result.afterSymbol = false; + result.cursor = CursorState::None; + } + } else if (request.onlyMessageText) { result.symbol = visibleTextLength(); + result.afterSymbol = false; + result.cursor = CursorState::None; + } else { + result.symbol += visibleTextLength(); } } else if (getStateText(point, trect, &result, request)) { + if (_invertMedia) { + result.symbol += visibleMediaTextLength(); + } + result.overMessageText = true; checkBottomInfoState(); return result; } else if (point.y() >= trect.y() + trect.height()) { - result.symbol = visibleTextLength(); + result.symbol = visibleTextLength() + + visibleMediaTextLength(); } } checkBottomInfoState(); @@ -2189,6 +2222,11 @@ TextState Message::textState( } } else if (media && media->isDisplayed()) { result = media->textState(point - g.topLeft(), request); + if (request.onlyMessageText) { + result.symbol = 0; + result.afterSymbol = false; + result.cursor = CursorState::None; + } result.symbol += visibleTextLength(); } @@ -2201,7 +2239,6 @@ TextState Message::textState( : 0); if (QRect(g.left(), keyboardTop, g.width(), keyboardHeight).contains(point)) { result.link = keyboard->getLink(point - QPoint(g.left(), keyboardTop)); - return result; } } @@ -2454,22 +2491,24 @@ bool Message::getStateReplyInfo( QPoint point, QRect &trect, not_null outResult) const { - if (auto reply = displayedReply()) { - int32 h = st::msgReplyPadding.top() + st::msgReplyBarSize.height() + st::msgReplyPadding.bottom(); - if (point.y() >= trect.top() && point.y() < trect.top() + h) { + if (const auto reply = displayedReply()) { + const auto margins = reply->margins(); + const auto height = reply->height(); + if (point.y() >= trect.top() && point.y() < trect.top() + height) { const auto g = QRect( trect.x(), - trect.y() + st::msgReplyPadding.top(), + trect.y() + margins.top(), trect.width(), - st::msgReplyBarSize.height()); - if ((reply->replyToMsg || reply->replyToStory) - && g.contains(point)) { - outResult->link = reply->replyToLink(); - reply->ripple.lastPoint = point - g.topLeft(); + height - margins.top() - margins.bottom()); + if (g.contains(point)) { + if (const auto link = reply->link()) { + outResult->link = reply->link(); + reply->ripple.lastPoint = point - g.topLeft(); + } } return true; } - trect.setTop(trect.top() + h); + trect.setTop(trect.top() + height); } return false; } @@ -2547,9 +2586,8 @@ void Message::updatePressed(QPoint point) { auto fwdheight = ((forwarded->text.maxWidth() > trect.width()) ? 2 : 1) * st::semiboldFont->height; trect.setTop(trect.top() + fwdheight); } - if (item->Get()) { - auto h = st::msgReplyPadding.top() + st::msgReplyBarSize.height() + st::msgReplyPadding.bottom(); - trect.setTop(trect.top() + h); + if (const auto reply = item->Get()) { + trect.setTop(trect.top() + reply->height()); } if (const auto via = item->Get()) { if (!displayFromName() && !displayForwardedFrom()) { @@ -2575,25 +2613,35 @@ void Message::updatePressed(QPoint point) { TextForMimeData Message::selectedText(TextSelection selection) const { const auto media = this->media(); auto logEntryOriginalResult = TextForMimeData(); + const auto mediaDisplayed = (media && media->isDisplayed()); + const auto mediaBefore = mediaDisplayed && invertMedia(); + const auto textSelection = mediaBefore + ? media->skipSelection(selection) + : selection; + const auto mediaSelection = !invertMedia() + ? skipTextSelection(selection) + : selection; auto textResult = hasVisibleText() - ? text().toTextForMimeData(selection) + ? text().toTextForMimeData(textSelection) : TextForMimeData(); - auto skipped = skipTextSelection(selection); - auto mediaDisplayed = (media && media->isDisplayed()); auto mediaResult = (mediaDisplayed || isHiddenByGroup()) - ? media->selectedText(skipped) + ? media->selectedText(mediaSelection) : TextForMimeData(); if (auto entry = logEntryOriginal()) { - const auto originalSelection = mediaDisplayed - ? media->skipSelection(skipped) - : skipped; + const auto originalSelection = mediaBefore + ? skipTextSelection(textSelection) + : mediaDisplayed + ? media->skipSelection(mediaSelection) + : skipTextSelection(selection); logEntryOriginalResult = entry->selectedText(originalSelection); } - auto result = textResult; + auto &first = mediaBefore ? mediaResult : textResult; + auto &second = mediaBefore ? textResult : mediaResult; + auto result = first; if (result.empty()) { - result = std::move(mediaResult); - } else if (!mediaResult.empty()) { - result.append(u"\n\n"_q).append(std::move(mediaResult)); + result = std::move(second); + } else if (!second.empty()) { + result.append(u"\n\n"_q).append(std::move(second)); } if (result.empty()) { result = std::move(logEntryOriginalResult); @@ -2603,47 +2651,199 @@ TextForMimeData Message::selectedText(TextSelection selection) const { return result; } +TextWithEntities Message::selectedQuote(TextSelection selection) const { + const auto item = data(); + const auto &translated = item->translatedText(); + const auto &original = item->originalText(); + if (&translated != &original + || selection.empty() + || selection == FullSelection) { + return {}; + } else if (hasVisibleText()) { + const auto media = this->media(); + const auto mediaDisplayed = media && media->isDisplayed(); + const auto mediaBefore = mediaDisplayed && invertMedia(); + const auto textSelection = mediaBefore + ? media->skipSelection(selection) + : selection; + return selectedQuote(text(), textSelection); + } else if (const auto media = this->media()) { + if (media->isDisplayed() || isHiddenByGroup()) { + return media->selectedQuote(selection); + } + } + return {}; +} + +TextWithEntities Message::selectedQuote( + const Ui::Text::String &text, + TextSelection selection) const { + if (selection.to > text.length()) { + return {}; + } + auto modified = selection; + for (const auto &modification : text.modifications()) { + if (modification.position >= selection.to) { + break; + } else if (modification.position <= selection.from) { + modified.from += modification.skipped; + if (modification.added + && modification.position < selection.from) { + --modified.from; + } + } + modified.to += modification.skipped; + if (modification.added && modified.to > modified.from) { + --modified.to; + } + } + auto result = data()->originalText(); + if (modified.empty() || modified.to > result.text.size()) { + return {}; + } + result.text = result.text.mid( + modified.from, + modified.to - modified.from); + const auto allowed = std::array{ + EntityType::Bold, + EntityType::Italic, + EntityType::Underline, + EntityType::StrikeOut, + EntityType::Spoiler, + EntityType::CustomEmoji, + }; + for (auto i = result.entities.begin(); i != result.entities.end();) { + const auto offset = i->offset(); + const auto till = offset + i->length(); + if ((till <= modified.from) + || (offset >= modified.to) + || !ranges::contains(allowed, i->type())) { + i = result.entities.erase(i); + } else { + if (till > modified.to) { + i->shrinkFromRight(till - modified.to); + } + i->shiftLeft(modified.from); + ++i; + } + } + return result; +} + +TextSelection Message::selectionFromQuote( + const TextWithEntities "e) const { + const auto item = data(); + const auto &translated = item->translatedText(); + const auto &original = item->originalText(); + if (&translated != &original || quote.empty()) { + return {}; + } else if (hasVisibleText()) { + const auto media = this->media(); + const auto mediaDisplayed = media && media->isDisplayed(); + const auto mediaBefore = mediaDisplayed && invertMedia(); + const auto result = selectionFromQuote(text(), quote); + return mediaBefore ? media->unskipSelection(result) : result; + } else if (const auto media = this->media()) { + if (media->isDisplayed() || isHiddenByGroup()) { + return media->selectionFromQuote(quote); + } + } + return {}; +} + +TextSelection Message::selectionFromQuote( + const Ui::Text::String &text, + const TextWithEntities "e) const { + if (quote.empty()) { + return {}; + } + const auto &original = data()->originalText(); + auto result = TextSelection(); + auto offset = 0; + while (true) { + const auto i = original.text.indexOf(quote.text, offset); + if (i < 0) { + return {}; + } + auto selection = TextSelection{ + uint16(i), + uint16(i + quote.text.size()), + }; + if (CheckQuoteEntities(quote.entities, original, selection)) { + result = selection; + break; + } + offset = i + 1; + } + //for (const auto &modification : text.modifications()) { + // if (modification.position >= selection.to) { + // break; + // } else if (modification.position <= selection.from) { + // modified.from += modification.skipped; + // if (modification.added + // && modification.position < selection.from) { + // --modified.from; + // } + // } + // modified.to += modification.skipped; + // if (modification.added && modified.to > modified.from) { + // --modified.to; + // } + //} + return result; +} + TextSelection Message::adjustSelection( TextSelection selection, TextSelectType type) const { const auto media = this->media(); - - auto result = hasVisibleText() - ? text().adjustSelection(selection, type) + const auto mediaDisplayed = media && media->isDisplayed(); + const auto mediaBefore = mediaDisplayed && invertMedia(); + const auto textSelection = mediaBefore + ? media->skipSelection(selection) : selection; - auto beforeMediaLength = visibleTextLength(); - if (selection.to <= beforeMediaLength) { - return result; - } - auto mediaDisplayed = media && media->isDisplayed(); + auto textAdjusted = hasVisibleText() + ? text().adjustSelection(textSelection, type) + : textSelection; + auto textResult = mediaBefore + ? media->unskipSelection(textAdjusted) + : textAdjusted; + auto mediaResult = TextSelection(); + auto mediaSelection = mediaBefore + ? selection + : skipTextSelection(selection); if (mediaDisplayed) { - auto mediaSelection = unskipTextSelection( - media->adjustSelection(skipTextSelection(selection), type)); - if (selection.from >= beforeMediaLength) { - result = mediaSelection; - } else { - result.to = mediaSelection.to; - } - } - auto beforeEntryLength = beforeMediaLength + visibleMediaTextLength(); - if (selection.to <= beforeEntryLength) { - return result; + auto mediaAdjusted = media->adjustSelection(mediaSelection, type); + mediaResult = mediaBefore + ? mediaAdjusted + : unskipTextSelection(mediaAdjusted); } + auto entryResult = TextSelection(); if (const auto entry = logEntryOriginal()) { - auto entrySelection = mediaDisplayed - ? media->skipSelection(skipTextSelection(selection)) - : skipTextSelection(selection); - auto logEntryOriginalSelection = entry->adjustSelection(entrySelection, type); + auto entrySelection = !mediaDisplayed + ? skipTextSelection(selection) + : mediaBefore + ? skipTextSelection(textSelection) + : media->skipSelection(mediaSelection); + auto entryAdjusted = entry->adjustSelection(entrySelection, type); + entryResult = unskipTextSelection(entryAdjusted); if (mediaDisplayed) { - logEntryOriginalSelection = media->unskipSelection(logEntryOriginalSelection); - } - logEntryOriginalSelection = unskipTextSelection(logEntryOriginalSelection); - if (selection.from >= beforeEntryLength) { - result = logEntryOriginalSelection; - } else { - result.to = logEntryOriginalSelection.to; + entryResult = media->unskipSelection(entryResult); } } + auto result = textResult; + if (!mediaResult.empty()) { + result = result.empty() ? mediaResult : TextSelection{ + std::min(result.from, mediaResult.from), + std::max(result.to, mediaResult.to), + }; + } + if (!entryResult.empty()) { + result = result.empty() ? entryResult : TextSelection{ + std::min(result.from, entryResult.from), + std::max(result.to, entryResult.to), + }; + } return result; } @@ -2947,6 +3147,10 @@ void Message::refreshDataIdHook() { if (base::take(_fastReplyLink)) { _fastReplyLink = fastReplyLink(); } + if (_viewButton) { + _viewButton = nullptr; + updateViewButtonExistence(); + } if (_comments) { _comments->link = nullptr; } @@ -2980,17 +3184,21 @@ void Message::updateViewButtonExistence() { } else if (_viewButton) { return; } - auto repainter = [=] { repaint(); }; - _viewButton = sponsored - ? std::make_unique(sponsored, std::move(repainter)) - : std::make_unique(media, std::move(repainter)); + auto make = [=](auto &&from) { + return std::make_unique( + std::forward(from), + colorIndex(), + [=] { repaint(); }); + }; + _viewButton = sponsored ? make(sponsored) : make(media); } void Message::initLogEntryOriginal() { if (const auto log = data()->Get()) { AddComponents(LogEntryOriginal::Bit()); const auto entry = Get(); - entry->page = std::make_unique(this, log->page); + using Flags = MediaWebPageFlags; + entry->page = std::make_unique(this, log->page, Flags()); } } @@ -3043,6 +3251,9 @@ bool Message::hasFromName() const { if (hasOutLayout() && !item->from()->isChannel()) { return false; } else if (!peer->isUser()) { + if (const auto media = this->media()) { + return !media->hideFromName(); + } return true; } if (const auto forwarded = item->Get()) { @@ -3429,7 +3640,7 @@ ClickHandlerPtr Message::fastReplyLink() const { } const auto itemId = data()->fullId(); _fastReplyLink = std::make_shared([=] { - delegate()->elementReplyTo(itemId); + delegate()->elementReplyTo({ itemId }); }); return _fastReplyLink; } @@ -3447,7 +3658,8 @@ void Message::updateMediaInBubbleState() { } const auto reactionsInBubble = (_reactions && embedReactionsInBubble()); auto mediaHasSomethingBelow = (_viewButton != nullptr) - || reactionsInBubble; + || reactionsInBubble + || (invertMedia() && hasVisibleText()); auto mediaHasSomethingAbove = false; auto getMediaHasSomethingAbove = [&] { return displayFromName() @@ -3485,7 +3697,7 @@ void Message::updateMediaInBubbleState() { if (!entry) { mediaHasSomethingAbove = getMediaHasSomethingAbove(); } - if (hasVisibleText()) { + if (!invertMedia() && hasVisibleText()) { mediaHasSomethingAbove = true; } const auto state = [&] { @@ -3578,13 +3790,9 @@ QRect Message::innerGeometry() const { + st::topicButtonSkip); } // Skip displayForwardedFrom() until there are no animations for it. - if (displayedReply()) { + if (const auto reply = displayedReply()) { // See paintReplyInfo(). - result.translate( - 0, - st::msgReplyPadding.top() - + st::msgReplyBarSize.height() - + st::msgReplyPadding.bottom()); + result.translate(0, reply->height()); } if (!displayFromName() && !displayForwardedFrom()) { // See paintViaBotIdInfo(). @@ -3621,7 +3829,7 @@ QRect Message::countGeometry() const { // contentLeft += st::msgPhotoSkip - (hmaxwidth - hwidth); } accumulate_min(contentWidth, maxWidth()); - accumulate_min(contentWidth, _bubbleWidthLimit); + accumulate_min(contentWidth, int(_bubbleWidthLimit)); if (mediaWidth < contentWidth) { const auto textualWidth = plainMaxWidth(); if (mediaWidth < textualWidth @@ -3703,6 +3911,10 @@ int Message::resizeContentGetHeight(int newWidth) { auto newHeight = minHeight(); + if (const auto service = Get()) { + service->resizeToWidth(newWidth, delegate()->elementIsChatWide()); + } + const auto item = data(); const auto botTop = item->isFakeBotAbout() ? Get() @@ -3724,7 +3936,7 @@ int Message::resizeContentGetHeight(int newWidth) { } accumulate_min(contentWidth, maxWidth()); _bubbleWidthLimit = std::max(st::msgMaxWidth, monospaceMaxWidth()); - accumulate_min(contentWidth, _bubbleWidthLimit); + accumulate_min(contentWidth, int(_bubbleWidthLimit)); if (mediaDisplayed) { media->resizeGetHeight(contentWidth); if (media->width() < contentWidth) { @@ -3832,9 +4044,10 @@ int Message::resizeContentGetHeight(int newWidth) { } if (reply) { - reply->resize(contentWidth - st::msgPadding.left() - st::msgPadding.right()); + newHeight += reply->resizeToWidth(contentWidth + - st::msgPadding.left() + - st::msgPadding.right()); reply->ripple.animation = nullptr; - newHeight += st::msgReplyPadding.top() + st::msgReplyBarSize.height() + st::msgReplyPadding.bottom(); } if (needInfoDisplay()) { newHeight += (bottomInfoHeight - st::msgDateFont->height); @@ -3879,11 +4092,15 @@ bool Message::needInfoDisplay() const { const auto entry = logEntryOriginal(); return entry ? !entry->customInfoLayout() - : (mediaDisplayed + : ((mediaDisplayed && media->isBubbleBottom()) ? !media->customInfoLayout() : true); } +bool Message::invertMedia() const { + return _invertMedia; +} + bool Message::hasVisibleText() const { if (data()->emptyText()) { if (const auto media = data()->media()) { diff --git a/Telegram/SourceFiles/history/view/history_view_message.h b/Telegram/SourceFiles/history/view/history_view_message.h index f2c67348822e91..f20f9d0b0aee12 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.h +++ b/Telegram/SourceFiles/history/view/history_view_message.h @@ -56,10 +56,6 @@ struct BottomRippleMask { int shift = 0; }; -[[nodiscard]] style::color FromNameFg( - const Ui::ChatPaintContext &context, - PeerId peerId); - class Message final : public Element { public: Message( @@ -99,6 +95,15 @@ class Message final : public Element { QPoint point, InfoDisplayType type) const override; TextForMimeData selectedText(TextSelection selection) const override; + TextWithEntities selectedQuote(TextSelection selection) const override; + TextWithEntities selectedQuote( + const Ui::Text::String &text, + TextSelection selection) const override; + TextSelection selectionFromQuote( + const TextWithEntities "e) const override; + TextSelection selectionFromQuote( + const Ui::Text::String &text, + const TextWithEntities "e) const override; TextSelection adjustSelection( TextSelection selection, TextSelectType type) const override; @@ -266,6 +271,7 @@ class Message final : public Element { [[nodiscard]] int visibleTextLength() const; [[nodiscard]] int visibleMediaTextLength() const; [[nodiscard]] bool needInfoDisplay() const; + [[nodiscard]] bool invertMedia() const; [[nodiscard]] bool isPinnedContext() const; @@ -305,7 +311,8 @@ class Message final : public Element { mutable std::unique_ptr _fromNameStatus; Ui::Text::String _rightBadge; mutable int _fromNameVersion = 0; - int _bubbleWidthLimit = 0; + uint32 _bubbleWidthLimit : 31 = 0; + uint32 _invertMedia : 1 = 0; BottomInfo _bottomInfo; diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp index e7c998ed923037..255347a229d46e 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp @@ -8,6 +8,8 @@ For license and copyright information please follow this link: #include "history/view/history_view_replies_section.h" #include "history/view/controls/history_view_compose_controls.h" +#include "history/view/controls/history_view_forward_panel.h" +#include "history/view/controls/history_view_draft_options.h" #include "history/view/history_view_top_bar_widget.h" #include "history/view/history_view_list_widget.h" #include "history/view/history_view_schedule_box.h" @@ -318,10 +320,15 @@ RepliesWidget::RepliesWidget( }, _inner->lifetime()); _inner->replyToMessageRequested( - ) | rpl::filter([=] { - return !_joinGroup; - }) | rpl::start_with_next([=](auto fullId) { - replyToMessage(fullId); + ) | rpl::start_with_next([=](auto fullId) { + const auto canSendReply = _topic + ? Data::CanSendAnything(_topic) + : Data::CanSendAnything(_history->peer); + if (_joinGroup || !canSendReply) { + Controls::ShowReplyToChatBox(controller->uiShow(), { fullId }); + } else { + replyToMessage(fullId); + } }, _inner->lifetime()); _inner->showMessageRequested( @@ -1002,7 +1009,7 @@ void RepliesWidget::sendingFilesConfirmed( album, action); } - if (_composeControls->replyingToMessage().msg == action.replyTo.msgId) { + if (_composeControls->replyingToMessage() == action.replyTo) { _composeControls->cancelReplyMessage(); refreshTopBarActiveChat(); } @@ -1123,9 +1130,9 @@ bool RepliesWidget::showSendingFilesError( } Api::SendAction RepliesWidget::prepareSendAction( - Api::SendOptions options) const { + Api::SendOptions options) const { auto result = Api::SendAction(_history, options); - result.replyTo = { .msgId = replyToId(), .topicRootId = _rootId }; + result.replyTo = replyTo(); result.options.sendAs = _composeControls->sendAsPeer(); return result; } @@ -1164,11 +1171,9 @@ void RepliesWidget::send(Api::SendOptions options) { _cornerButtons.clearReplyReturns(); } - const auto webPageId = _composeControls->webPageId(); - auto message = Api::MessageToSend(prepareSendAction(options)); message.textWithTags = _composeControls->getTextWithAppliedMarkdown(); - message.webPageId = webPageId; + message.webPage = _composeControls->webPageDraft(); const auto error = GetErrorTextForSending( _history->peer, @@ -1207,6 +1212,7 @@ void RepliesWidget::edit( return; } const auto textWithTags = _composeControls->getTextWithAppliedMarkdown(); + const auto webpage = _composeControls->webPageDraft(); const auto prepareFlags = Ui::ItemTextOptions( _history, session().user()).flags; @@ -1268,6 +1274,7 @@ void RepliesWidget::edit( *saveEditMsgRequestId = Api::EditTextMessage( item, sending, + webpage, options, crl::guard(this, done), crl::guard(this, fail)); @@ -1444,28 +1451,29 @@ SendMenu::Type RepliesWidget::sendMenuType() const { : SendMenu::Type::Scheduled; } +FullReplyTo RepliesWidget::replyTo() const { + if (auto custom = _composeControls->replyingToMessage()) { + custom.topicRootId = _rootId; + return custom; + } + return FullReplyTo{ + .messageId = FullMsgId(_history->peer->id, _rootId), + .topicRootId = _rootId, + }; +} + void RepliesWidget::refreshTopBarActiveChat() { using namespace Dialogs; const auto state = EntryState{ .key = (_topic ? Key{ _topic } : Key{ _history }), .section = EntryState::Section::Replies, - .rootId = _rootId, - .currentReplyToId = _composeControls->replyingToMessage().msg, + .currentReplyTo = replyTo(), }; _topBar->setActiveChat(state, _sendAction.get()); _composeControls->setCurrentDialogsEntryState(state); controller()->setCurrentDialogsEntryState(state); } -MsgId RepliesWidget::replyToId() const { - const auto custom = _composeControls->replyingToMessage().msg; - return custom - ? custom - : (_rootId == Data::ForumTopic::kGeneralId) - ? MsgId() - : _rootId; -} - void RepliesWidget::refreshUnreadCountBadge(std::optional count) { if (count.has_value()) { _cornerButtons.updateJumpDownVisibility(count); @@ -2052,8 +2060,8 @@ bool RepliesWidget::confirmSendingFiles( insertTextOnCancel); } -void RepliesWidget::replyToMessage(FullMsgId itemId) { - _composeControls->replyToMessage(itemId); +void RepliesWidget::replyToMessage(FullReplyTo id) { + _composeControls->replyToMessage(std::move(id)); refreshTopBarActiveChat(); } diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.h b/Telegram/SourceFiles/history/view/history_view_replies_section.h index 5c052f3f40558d..b55a7b4638f248 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.h +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.h @@ -243,7 +243,7 @@ class RepliesWidget final mtpRequestId *const saveEditMsgRequestId); void chooseAttach(std::optional overrideSendImagesAsPhotos); [[nodiscard]] SendMenu::Type sendMenuType() const; - [[nodiscard]] MsgId replyToId() const; + [[nodiscard]] FullReplyTo replyTo() const; [[nodiscard]] HistoryItem *lookupRoot() const; [[nodiscard]] Data::ForumTopic *lookupTopic(); [[nodiscard]] bool computeAreComments() const; @@ -252,7 +252,7 @@ class RepliesWidget final void pushReplyReturn(not_null item); void checkReplyReturns(); void recountChatWidth(); - void replyToMessage(FullMsgId itemId); + void replyToMessage(FullReplyTo id); void refreshTopBarActiveChat(); void refreshUnreadCountBadge(std::optional count); diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp index 1a2b4ef0e5eac7..b9d4be5ef071ff 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp @@ -585,11 +585,11 @@ void ScheduledWidget::send() { } void ScheduledWidget::send(Api::SendOptions options) { - const auto webPageId = _composeControls->webPageId(); + const auto webPageDraft = _composeControls->webPageDraft(); auto message = Api::MessageToSend(prepareSendAction(options)); message.textWithTags = _composeControls->getTextWithAppliedMarkdown(); - message.webPageId = webPageId; + message.webPage = webPageDraft; session().api().sendMessage(std::move(message)); @@ -635,6 +635,7 @@ void ScheduledWidget::edit( return; } const auto textWithTags = _composeControls->getTextWithAppliedMarkdown(); + const auto webpage = _composeControls->webPageDraft(); const auto prepareFlags = Ui::ItemTextOptions( _history, session().user()).flags; @@ -696,6 +697,7 @@ void ScheduledWidget::edit( *saveEditMsgRequestId = Api::EditTextMessage( item, sending, + webpage, options, crl::guard(this, done), crl::guard(this, fail)); diff --git a/Telegram/SourceFiles/history/view/history_view_service_message.cpp b/Telegram/SourceFiles/history/view/history_view_service_message.cpp index e367d6e6d55346..4c9695667e74f9 100644 --- a/Telegram/SourceFiles/history/view/history_view_service_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_service_message.cpp @@ -669,6 +669,27 @@ TextForMimeData Service::selectedText(TextSelection selection) const { return text().toTextForMimeData(selection); } +TextWithEntities Service::selectedQuote(TextSelection selection) const { + return {}; +} + +TextWithEntities Service::selectedQuote( + const Ui::Text::String &text, + TextSelection selection) const { + return {}; +} + +TextSelection Service::selectionFromQuote( + const TextWithEntities "e) const { + return {}; +} + +TextSelection Service::selectionFromQuote( + const Ui::Text::String &text, + const TextWithEntities "e) const { + return {}; +} + TextSelection Service::adjustSelection( TextSelection selection, TextSelectType type) const { diff --git a/Telegram/SourceFiles/history/view/history_view_service_message.h b/Telegram/SourceFiles/history/view/history_view_service_message.h index 3bb3b790b3149f..2e36fa5bf782d8 100644 --- a/Telegram/SourceFiles/history/view/history_view_service_message.h +++ b/Telegram/SourceFiles/history/view/history_view_service_message.h @@ -43,6 +43,15 @@ class Service final : public Element { StateRequest request) const override; void updatePressed(QPoint point) override; TextForMimeData selectedText(TextSelection selection) const override; + TextWithEntities selectedQuote(TextSelection selection) const override; + TextWithEntities selectedQuote( + const Ui::Text::String &text, + TextSelection selection) const override; + TextSelection selectionFromQuote( + const TextWithEntities "e) const override; + TextSelection selectionFromQuote( + const Ui::Text::String &text, + const TextWithEntities "e) const override; TextSelection adjustSelection( TextSelection selection, TextSelectType type) const override; diff --git a/Telegram/SourceFiles/history/view/history_view_translate_tracker.cpp b/Telegram/SourceFiles/history/view/history_view_translate_tracker.cpp index 0a1d2f13f74b14..a63203d5e15e71 100644 --- a/Telegram/SourceFiles/history/view/history_view_translate_tracker.cpp +++ b/Telegram/SourceFiles/history/view/history_view_translate_tracker.cpp @@ -112,7 +112,7 @@ bool TranslateTracker::add( } if (!skipDependencies) { if (const auto reply = item->Get()) { - if (const auto to = reply->replyToMsg.get()) { + if (const auto to = reply->resolvedMessage.get()) { add(to, true); } } diff --git a/Telegram/SourceFiles/history/view/history_view_view_button.cpp b/Telegram/SourceFiles/history/view/history_view_view_button.cpp index 95de695967db37..6765fef6bc2cd7 100644 --- a/Telegram/SourceFiles/history/view/history_view_view_button.cpp +++ b/Telegram/SourceFiles/history/view/history_view_view_button.cpp @@ -16,8 +16,9 @@ For license and copyright information please follow this link: #include "data/data_sponsored_messages.h" #include "data/data_user.h" #include "data/data_web_page.h" -#include "history/history_item_components.h" #include "history/view/history_view_cursor_state.h" +#include "history/history_item_components.h" +#include "history/history.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "ui/click_handler.h" @@ -51,60 +52,33 @@ inline auto SponsoredPhrase(SponsoredType type) { return Ui::Text::Upper(phrase(tr::now)); } -inline auto WebPageToPhrase(not_null webpage) { - const auto type = webpage->type; - return Ui::Text::Upper((type == WebPageType::Theme) - ? tr::lng_view_button_theme(tr::now) - : (type == WebPageType::Story) - ? tr::lng_view_button_story(tr::now) - : (type == WebPageType::Message) - ? tr::lng_view_button_message(tr::now) - : (type == WebPageType::Group) - ? tr::lng_view_button_group(tr::now) - : (type == WebPageType::WallPaper) - ? tr::lng_view_button_background(tr::now) - : (type == WebPageType::Channel) - ? tr::lng_view_button_channel(tr::now) - : (type == WebPageType::GroupWithRequest - || type == WebPageType::ChannelWithRequest) - ? tr::lng_view_button_request_join(tr::now) - : (type == WebPageType::ChannelBoost) - ? tr::lng_view_button_boost(tr::now) - : (type == WebPageType::VoiceChat) - ? tr::lng_view_button_voice_chat(tr::now) - : (type == WebPageType::Livestream) - ? tr::lng_view_button_voice_chat_channel(tr::now) - : (type == WebPageType::Bot) - ? tr::lng_view_button_bot(tr::now) - : (type == WebPageType::User) - ? tr::lng_view_button_user(tr::now) - : (type == WebPageType::BotApp) - ? tr::lng_view_button_bot_app(tr::now) - : QString()); -} - -[[nodiscard]] ClickHandlerPtr MakeWebPageButtonClickHandler( +[[nodiscard]] ClickHandlerPtr MakeMediaButtonClickHandler( not_null media) { - Expects(media->webpage() != nullptr); - - const auto url = media->webpage()->url; - const auto type = media->webpage()->type; - return std::make_shared([=](ClickContext context) { + const auto giveaway = media->giveaway(); + Assert(giveaway != nullptr); + const auto peer = media->parent()->history()->peer; + const auto messageId = media->parent()->id; + if (media->parent()->isSending() || media->parent()->hasFailed()) { + return nullptr; + } + const auto info = *giveaway; + return std::make_shared([=]( + ClickContext context) { const auto my = context.other.value(); - if (const auto controller = my.sessionWindow.get()) { - if (type == WebPageType::BotApp) { - // Bot Web Apps always show confirmation on hidden urls. - // - // But from the dedicated "Open App" button we don't want - // to request users confirmation on non-first app opening. - UrlClickHandler::Open(url, context.other); - } else { - HiddenUrlClickHandler::Open(url, context.other); - } + const auto controller = my.sessionWindow.get(); + if (!controller) { + return; } + ResolveGiveawayInfo(controller, peer, messageId, info); }); } +[[nodiscard]] QString MakeMediaButtonText(not_null media) { + const auto giveaway = media->giveaway(); + Assert(giveaway != nullptr); + return Ui::Text::Upper(tr::lng_prizes_how_works(tr::now)); +} + [[nodiscard]] ClickHandlerPtr SponsoredLink( not_null sponsored) { if (!sponsored->externalLink.isEmpty()) { @@ -150,8 +124,12 @@ inline auto WebPageToPhrase(not_null webpage) { struct ViewButton::Inner { Inner( not_null sponsored, + uint8 colorIndex, + Fn updateCallback); + Inner( + not_null media, + uint8 colorIndex, Fn updateCallback); - Inner(not_null media, Fn updateCallback); void updateMask(int height); void toggleRipple(bool pressed); @@ -159,60 +137,41 @@ struct ViewButton::Inner { const style::margins &margins; const ClickHandlerPtr link; const Fn updateCallback; - bool belowInfo = true; - bool externalLink = false; - int lastWidth = 0; + uint32 lastWidth : 24 = 0; + uint32 colorIndex : 6 = 0; + uint32 aboveInfo : 1 = 0; + uint32 externalLink : 1 = 0; QPoint lastPoint; std::unique_ptr ripple; Ui::Text::String text; }; bool ViewButton::MediaHasViewButton(not_null media) { - return media->webpage() - ? MediaHasViewButton(media->webpage()) - : false; -} - -bool ViewButton::MediaHasViewButton( - not_null webpage) { - const auto type = webpage->type; - return (type == WebPageType::Message) - || (type == WebPageType::Group) - || (type == WebPageType::Channel) - || (type == WebPageType::ChannelBoost) - // || (type == WebPageType::Bot) - || (type == WebPageType::User) - || (type == WebPageType::VoiceChat) - || (type == WebPageType::Livestream) - || (type == WebPageType::BotApp) - || ((type == WebPageType::Theme) - && webpage->document - && webpage->document->isTheme()) - || ((type == WebPageType::Story) - && (webpage->photo || webpage->document)) - || ((type == WebPageType::WallPaper) - && webpage->document - && webpage->document->isWallPaper()); + return (media->giveaway() != nullptr); } ViewButton::Inner::Inner( not_null sponsored, + uint8 colorIndex, Fn updateCallback) : margins(st::historyViewButtonMargins) , link(SponsoredLink(sponsored)) , updateCallback(std::move(updateCallback)) -, externalLink(sponsored->type == SponsoredType::ExternalLink) +, colorIndex(colorIndex) +, externalLink((sponsored->type == SponsoredType::ExternalLink) ? 1 : 0) , text(st::historyViewButtonTextStyle, SponsoredPhrase(sponsored->type)) { } ViewButton::Inner::Inner( not_null media, + uint8 colorIndex, Fn updateCallback) : margins(st::historyViewButtonMargins) -, link(MakeWebPageButtonClickHandler(media)) +, link(MakeMediaButtonClickHandler(media)) , updateCallback(std::move(updateCallback)) -, belowInfo(false) -, text(st::historyViewButtonTextStyle, WebPageToPhrase(media->webpage())) { +, colorIndex(colorIndex) +, aboveInfo(1) +, text(st::historyViewButtonTextStyle, MakeMediaButtonText(media)) { } void ViewButton::Inner::updateMask(int height) { @@ -236,14 +195,22 @@ void ViewButton::Inner::toggleRipple(bool pressed) { ViewButton::ViewButton( not_null sponsored, + uint8 colorIndex, Fn updateCallback) -: _inner(std::make_unique(sponsored, std::move(updateCallback))) { +: _inner(std::make_unique( + sponsored, + colorIndex, + std::move(updateCallback))) { } ViewButton::ViewButton( not_null media, + uint8 colorIndex, Fn updateCallback) -: _inner(std::make_unique(media, std::move(updateCallback))) { +: _inner(std::make_unique( + media, + colorIndex, + std::move(updateCallback))) { } ViewButton::~ViewButton() { @@ -258,54 +225,49 @@ int ViewButton::height() const { } bool ViewButton::belowMessageInfo() const { - return _inner->belowInfo; + return !_inner->aboveInfo; } void ViewButton::draw( Painter &p, const QRect &r, const Ui::ChatPaintContext &context) { + const auto st = context.st; const auto stm = context.messageStyle(); + const auto selected = context.selected(); + const auto cache = context.outbg + ? stm->replyCache[st->colorPatternIndex(_inner->colorIndex)].get() + : st->coloredReplyCache(selected, _inner->colorIndex).get(); + const auto radius = st::historyPagePreview.radius; if (_inner->ripple && !_inner->ripple->empty()) { - const auto opacity = p.opacity(); - p.setOpacity(st::historyPollRippleOpacity); - const auto colorOverride = &stm->msgWaveformInactive->c; - _inner->ripple->paint(p, r.left(), r.top(), r.width(), colorOverride); - p.setOpacity(opacity); + _inner->ripple->paint(p, r.left(), r.top(), r.width(), &cache->bg); } - p.save(); - { - PainterHighQualityEnabler hq(p); - auto pen = stm->fwdTextPalette.linkFg->p; - pen.setWidth(st::lineWidth); - p.setPen(pen); - p.setBrush(Qt::NoBrush); - const auto half = st::lineWidth / 2.; - const auto rf = QRectF(r).marginsRemoved({ half, half, half, half }); - p.drawRoundedRect(rf, st::roundRadiusLarge, st::roundRadiusLarge); - - _inner->text.drawElided( + PainterHighQualityEnabler hq(p); + p.setPen(Qt::NoPen); + p.setBrush(cache->bg); + p.drawRoundedRect(r, radius, radius); + + p.setPen(cache->icon); + _inner->text.drawElided( + p, + r.left(), + r.top() + (r.height() - _inner->text.minHeight()) / 2, + r.width(), + 1, + style::al_top); + + if (_inner->externalLink) { + const auto &icon = st::msgBotKbUrlIcon; + const auto padding = st::msgBotKbIconPadding; + icon.paint( p, - r.left(), - r.top() + (r.height() - _inner->text.minHeight()) / 2, + r.left() + r.width() - icon.width() - padding, + r.top() + padding, r.width(), - 1, - style::al_center); - - if (_inner->externalLink) { - const auto &icon = st::msgBotKbUrlIcon; - const auto padding = st::msgBotKbIconPadding; - icon.paint( - p, - r.left() + r.width() - icon.width() - padding, - r.top() + padding, - r.width(), - stm->fwdTextPalette.linkFg->c); - } + cache->icon); } - p.restore(); if (_inner->lastWidth != r.width()) { _inner->lastWidth = r.width(); resized(); diff --git a/Telegram/SourceFiles/history/view/history_view_view_button.h b/Telegram/SourceFiles/history/view/history_view_view_button.h index b4788d4291f41b..4ff3163f179190 100644 --- a/Telegram/SourceFiles/history/view/history_view_view_button.h +++ b/Telegram/SourceFiles/history/view/history_view_view_button.h @@ -25,14 +25,16 @@ class ViewButton { public: ViewButton( not_null sponsored, + uint8 colorIndex, + Fn updateCallback); + ViewButton( + not_null media, + uint8 colorIndex, Fn updateCallback); - ViewButton(not_null media, Fn updateCallback); ~ViewButton(); [[nodiscard]] static bool MediaHasViewButton( not_null media); - [[nodiscard]] static bool MediaHasViewButton( - not_null webpage); [[nodiscard]] int height() const; [[nodiscard]] bool belowMessageInfo() const; diff --git a/Telegram/SourceFiles/history/view/media/history_view_contact.cpp b/Telegram/SourceFiles/history/view/media/history_view_contact.cpp index afadf020bb577d..c4aeb4bd0f5ee3 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_contact.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_contact.cpp @@ -123,7 +123,7 @@ QSize Contact::countOptimalSize() { } else { const auto full = _name.toString(); _photoEmpty = std::make_unique( - Ui::EmptyUserpic::UserpicColor(Data::PeerColorIndex(_userId + Ui::EmptyUserpic::UserpicColor(Data::DecideColorIndex(_userId ? peerFromUser(_userId) : Data::FakePeerIdForJustName(full))), full); diff --git a/Telegram/SourceFiles/history/view/media/history_view_document.cpp b/Telegram/SourceFiles/history/view/media/history_view_document.cpp index 78d72c301b63f6..02e6d7f1b2ccfc 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_document.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_document.cpp @@ -749,7 +749,7 @@ void Document::draw( .availableWidth = captionw, .palette = &stm->textPalette, .pre = stm->preCache.get(), - .blockquote = stm->blockquoteCache.get(), + .blockquote = context.quoteCache(parent()->colorIndex()), .colors = context.st->highlightColors(), .spoiler = Ui::Text::DefaultSpoilerCache(), .now = context.now, @@ -1210,6 +1210,40 @@ TextForMimeData Document::selectedText(TextSelection selection) const { return result; } +TextWithEntities Document::selectedQuote(TextSelection selection) const { + if (const auto voice = Get()) { + const auto length = voice->transcribeText.length(); + if (selection.from < length) { + return {}; + } + selection = HistoryView::UnshiftItemSelection( + selection, + voice->transcribeText); + } + if (const auto captioned = Get()) { + return parent()->selectedQuote(captioned->caption, selection); + } + return {}; +} + +TextSelection Document::selectionFromQuote( + const TextWithEntities "e) const { + if (const auto captioned = Get()) { + const auto result = parent()->selectionFromQuote( + captioned->caption, + quote); + if (result.empty()) { + return {}; + } else if (const auto voice = Get()) { + return HistoryView::ShiftItemSelection( + result, + voice->transcribeText); + } + return result; + } + return {}; +} + bool Document::uploading() const { return _data->uploading(); } diff --git a/Telegram/SourceFiles/history/view/media/history_view_document.h b/Telegram/SourceFiles/history/view/media/history_view_document.h index bf3078867b15f8..225a9b7ef96b05 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_document.h +++ b/Telegram/SourceFiles/history/view/media/history_view_document.h @@ -46,6 +46,9 @@ class Document final bool hasTextForCopy() const override; TextForMimeData selectedText(TextSelection selection) const override; + TextWithEntities selectedQuote(TextSelection selection) const override; + TextSelection selectionFromQuote( + const TextWithEntities "e) const override; bool uploading() const override; diff --git a/Telegram/SourceFiles/history/view/media/history_view_extended_preview.cpp b/Telegram/SourceFiles/history/view/media/history_view_extended_preview.cpp index d343ad334236b4..ffd26817e803bf 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_extended_preview.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_extended_preview.cpp @@ -236,7 +236,7 @@ void ExtendedPreview::draw(Painter &p, const PaintContext &context) const { .availableWidth = captionw, .palette = &stm->textPalette, .pre = stm->preCache.get(), - .blockquote = stm->blockquoteCache.get(), + .blockquote = context.quoteCache(parent()->colorIndex()), .colors = context.st->highlightColors(), .spoiler = Ui::Text::DefaultSpoilerCache(), .now = context.now, diff --git a/Telegram/SourceFiles/history/view/media/history_view_game.cpp b/Telegram/SourceFiles/history/view/media/history_view_game.cpp index cc2ebeef8253dd..88a3915359bd0f 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_game.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_game.cpp @@ -32,9 +32,10 @@ Game::Game( not_null data, const TextWithEntities &consumed) : Media(parent) +, _st(st::historyPagePreview) , _data(data) -, _title(st::msgMinWidth - st::webPageLeft) -, _description(st::msgMinWidth - st::webPageLeft) { +, _title(st::msgMinWidth - _st.padding.left() - _st.padding.right()) +, _description(st::msgMinWidth - _st.padding.left() - _st.padding.right()) { if (!consumed.text.isEmpty()) { const auto context = Core::MarkedTextContext{ .session = &history()->session(), @@ -126,8 +127,8 @@ QSize Game::countOptimalSize() { accumulate_max(maxWidth, maxMediaWidth); minHeight += _attach->minHeight() - bubble.top() - bubble.bottom(); } - maxWidth += st::msgPadding.left() + st::webPageLeft + st::msgPadding.right(); - auto padding = inBubblePadding(); + auto padding = inBubblePadding() + innerMargin(); + maxWidth += padding.left() + padding.right(); minHeight += padding.top() + padding.bottom(); if (!_gameTagWidth) { @@ -147,7 +148,8 @@ void Game::refreshParentId(not_null realParent) { QSize Game::countCurrentSize(int newWidth) { accumulate_min(newWidth, maxWidth()); - auto innerWidth = newWidth - st::msgPadding.left() - st::webPageLeft - st::msgPadding.right(); + const auto padding = inBubblePadding() + innerMargin(); + auto innerWidth = newWidth - padding.left() - padding.right(); // enable any count of lines in game description / message auto linesMax = 4096; @@ -184,11 +186,7 @@ QSize Game::countCurrentSize(int newWidth) { _attach->resizeGetHeight(innerWidth + bubble.left() + bubble.right()); newHeight += _attach->height() - bubble.top() - bubble.bottom(); - if (isBubbleBottom() && _attach->customInfoLayout() && _attach->width() + _parent->skipBlockWidth() > innerWidth + bubble.left() + bubble.right()) { - newHeight += bottomInfoPadding(); - } } - auto padding = inBubblePadding(); newHeight += padding.top() + padding.bottom(); return { newWidth, newHeight }; @@ -205,38 +203,53 @@ TextSelection Game::fromDescriptionSelection( } void Game::draw(Painter &p, const PaintContext &context) const { - if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) return; - auto paintw = width(); + if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) { + return; + } const auto st = context.st; const auto sti = context.imageStyle(); const auto stm = context.messageStyle(); - const auto &barfg = stm->msgReplyBarColor; - const auto &semibold = stm->msgServiceFg; - - QMargins bubble(_attach ? _attach->bubbleMargins() : QMargins()); - auto padding = inBubblePadding(); - auto tshift = padding.top(); - auto bshift = padding.bottom(); - paintw -= padding.left() + padding.right(); - if (isBubbleBottom() && _attach && _attach->customInfoLayout() && _attach->width() + _parent->skipBlockWidth() > paintw + bubble.left() + bubble.right()) { - bshift += bottomInfoPadding(); - } - - QRect bar(style::rtlrect(st::msgPadding.left(), tshift, st::webPageBar, height() - tshift - bshift, width())); - p.fillRect(bar, barfg); + const auto bubble = _attach ? _attach->bubbleMargins() : QMargins(); + const auto full = QRect(0, 0, width(), height()); + auto outer = full.marginsRemoved(inBubblePadding()); + auto inner = outer.marginsRemoved(innerMargin()); + auto tshift = inner.top(); + auto paintw = inner.width(); + + const auto colorIndex = parent()->colorIndex(); + const auto selected = context.selected(); + const auto cache = context.outbg + ? stm->replyCache[st->colorPatternIndex(colorIndex)].get() + : st->coloredReplyCache(selected, colorIndex).get(); + Ui::Text::ValidateQuotePaintCache(*cache, _st); + Ui::Text::FillQuotePaint(p, outer, *cache, _st); auto lineHeight = UnitedLineHeight(); if (_titleLines) { - p.setPen(semibold); - p.setTextPalette(stm->semiboldPalette); + p.setPen(cache->icon); + p.setTextPalette(context.outbg + ? stm->semiboldPalette + : st->coloredTextPalette(selected, colorIndex)); auto endskip = 0; if (_title.hasSkipBlock()) { endskip = _parent->skipBlockWidth(); } - _title.drawLeftElided(p, padding.left(), tshift, paintw, width(), _titleLines, style::al_left, 0, -1, endskip, false, context.selection); + _title.drawLeftElided( + p, + inner.left(), + tshift, + paintw, + width(), + _titleLines, + style::al_left, + 0, + -1, + endskip, + false, + context.selection); tshift += _titleLines * lineHeight; p.setTextPalette(stm->textPalette); @@ -249,7 +262,7 @@ void Game::draw(Painter &p, const PaintContext &context) const { } _parent->prepareCustomEmojiPaint(p, context, _description); _description.draw(p, { - .position = { padding.left(), tshift }, + .position = { inner.left(), tshift }, .outerWidth = width(), .availableWidth = paintw, .spoiler = Ui::Text::DefaultSpoilerCache(), @@ -266,7 +279,7 @@ void Game::draw(Painter &p, const PaintContext &context) const { auto attachAtTop = !_titleLines && !_descriptionLines; if (!attachAtTop) tshift += st::mediaInBubbleSkip; - auto attachLeft = padding.left() - bubble.left(); + auto attachLeft = inner.left() - bubble.left(); auto attachTop = tshift - bubble.top(); if (rtl()) attachLeft = width() - attachLeft - _attach->width(); @@ -301,16 +314,13 @@ TextState Game::textState(QPoint point, StateRequest request) const { if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) { return result; } - auto paintw = width(); - - QMargins bubble(_attach ? _attach->bubbleMargins() : QMargins()); - auto padding = inBubblePadding(); - auto tshift = padding.top(); - auto bshift = padding.bottom(); - if (isBubbleBottom() && _attach && _attach->customInfoLayout() && _attach->width() + _parent->skipBlockWidth() > paintw + bubble.left() + bubble.right()) { - bshift += bottomInfoPadding(); - } - paintw -= padding.left() + padding.right(); + + const auto bubble = _attach ? _attach->bubbleMargins() : QMargins(); + const auto full = QRect(0, 0, width(), height()); + auto outer = full.marginsRemoved(inBubblePadding()); + auto inner = outer.marginsRemoved(innerMargin()); + auto tshift = inner.top(); + auto paintw = inner.width(); auto inThumb = false; auto symbolAdd = 0; @@ -320,7 +330,7 @@ TextState Game::textState(QPoint point, StateRequest request) const { Ui::Text::StateRequestElided titleRequest = request.forText(); titleRequest.lines = _titleLines; result = TextState(_parent, _title.getStateElidedLeft( - point - QPoint(padding.left(), tshift), + point - QPoint(inner.left(), tshift), paintw, width(), titleRequest)); @@ -334,7 +344,7 @@ TextState Game::textState(QPoint point, StateRequest request) const { Ui::Text::StateRequestElided descriptionRequest = request.forText(); descriptionRequest.lines = _descriptionLines; result = TextState(_parent, _description.getStateElidedLeft( - point - QPoint(padding.left(), tshift), + point - QPoint(inner.left(), tshift), paintw, width(), descriptionRequest)); @@ -351,11 +361,11 @@ TextState Game::textState(QPoint point, StateRequest request) const { auto attachAtTop = !_titleLines && !_descriptionLines; if (!attachAtTop) tshift += st::mediaInBubbleSkip; - auto attachLeft = padding.left() - bubble.left(); + auto attachLeft = inner.left() - bubble.left(); auto attachTop = tshift - bubble.top(); if (rtl()) attachLeft = width() - attachLeft - _attach->width(); - if (QRect(attachLeft, tshift, _attach->width(), height() - tshift - bshift).contains(point)) { + if (QRect(attachLeft, tshift, _attach->width(), inner.top() + inner.height() - tshift).contains(point)) { if (_attach->isReadyForOpen()) { if (_parent->data()->isHistoryEntry()) { result.link = _openl; @@ -417,15 +427,24 @@ void Game::playAnimation(bool autoplay) { } QMargins Game::inBubblePadding() const { - auto lshift = st::msgPadding.left() + st::webPageLeft; - auto rshift = st::msgPadding.right(); - auto bshift = isBubbleBottom() ? st::msgPadding.left() : st::mediaInBubbleSkip; - auto tshift = isBubbleTop() ? st::msgPadding.left() : st::mediaInBubbleSkip; - return QMargins(lshift, tshift, rshift, bshift); + return { + st::msgPadding.left(), + isBubbleTop() ? st::msgPadding.left() : st::mediaInBubbleSkip, + st::msgPadding.right(), + (isBubbleBottom() + ? (st::msgPadding.left() + bottomInfoPadding()) + : st::mediaInBubbleSkip), + }; +} + +QMargins Game::innerMargin() const { + return _st.padding; } int Game::bottomInfoPadding() const { - if (!isBubbleBottom()) return 0; + if (!isBubbleBottom()) { + return 0; + } auto result = st::msgDateFont->height; @@ -452,7 +471,9 @@ void Game::parentTextUpdated() { Ui::ItemTextOptions(_parent->data()), context); } else { - _description = Ui::Text::String(st::msgMinWidth - st::webPageLeft); + _description = Ui::Text::String(st::msgMinWidth + - _st.padding.left() + - _st.padding.right()); } history()->owner().requestViewResize(_parent); } diff --git a/Telegram/SourceFiles/history/view/media/history_view_game.h b/Telegram/SourceFiles/history/view/media/history_view_game.h index c45c6640fa5dda..30a53587b4bb85 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_game.h +++ b/Telegram/SourceFiles/history/view/media/history_view_game.h @@ -87,23 +87,28 @@ class Game : public Media { private: void playAnimation(bool autoplay) override; - QSize countOptimalSize() override; - QSize countCurrentSize(int newWidth) override; - - TextSelection toDescriptionSelection(TextSelection selection) const; - TextSelection fromDescriptionSelection(TextSelection selection) const; - QMargins inBubblePadding() const; - int bottomInfoPadding() const; - - not_null _data; + [[nodiscard]] QSize countOptimalSize() override; + [[nodiscard]] QSize countCurrentSize(int newWidth) override; + + [[nodiscard]] TextSelection toDescriptionSelection( + TextSelection selection) const; + [[nodiscard]] TextSelection fromDescriptionSelection( + TextSelection selection) const; + [[nodiscard]] QMargins inBubblePadding() const; + [[nodiscard]] QMargins innerMargin() const; + [[nodiscard]] int bottomInfoPadding() const; + + const style::QuoteStyle &_st; + const not_null _data; std::shared_ptr _openl; std::unique_ptr _attach; - int _titleLines, _descriptionLines; - - Ui::Text::String _title, _description; - int _gameTagWidth = 0; + int _descriptionLines = 0; + int _titleLines = 0; + + Ui::Text::String _title; + Ui::Text::String _description; }; diff --git a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp index 19527833e65dbe..bb0f6fefa6a254 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp @@ -286,7 +286,7 @@ QSize Gif::countCurrentSize(int newWidth) { via->resize(availw); } if (reply) { - reply->resize(availw); + [[maybe_unused]] int height = reply->resizeToWidth(availw); } } } @@ -652,16 +652,21 @@ void Gif::draw(Painter &p, const PaintContext &context) const { if (via || reply || forwarded) { auto rectw = width() - usew - st::msgReplyPadding.left(); auto innerw = rectw - (st::msgReplyPadding.left() + st::msgReplyPadding.right()); - auto recth = st::msgReplyPadding.top() + st::msgReplyPadding.bottom(); + auto recth = 0; auto forwardedHeightReal = forwarded ? forwarded->text.countHeight(innerw) : 0; auto forwardedHeight = qMin(forwardedHeightReal, kMaxGifForwardedBarLines * st::msgServiceNameFont->height); if (forwarded) { - recth += forwardedHeight; + recth += st::msgReplyPadding.top() + forwardedHeight; } else if (via) { - recth += st::msgServiceNameFont->height + (reply ? st::msgReplyPadding.top() : 0); + recth += st::msgReplyPadding.top() + st::msgServiceNameFont->height + (reply ? st::msgReplyPadding.top() : 0); } if (reply) { - recth += st::msgReplyBarSize.height(); + const auto replyMargins = reply->margins(); + recth += reply->height() + - ((forwarded || via) ? 0 : replyMargins.top()) + - replyMargins.bottom(); + } else { + recth += st::msgReplyPadding.bottom(); } int rectx = rightAligned ? 0 : (usew + st::msgReplyPadding.left()); int recty = painty; @@ -669,25 +674,31 @@ void Gif::draw(Painter &p, const PaintContext &context) const { Ui::FillRoundRect(p, rectx, recty, rectw, recth, sti->msgServiceBg, sti->msgServiceBgCornersSmall); p.setPen(st->msgServiceFg()); - rectx += st::msgReplyPadding.left(); - rectw = innerw; + const auto textx = rectx + st::msgReplyPadding.left(); + const auto textw = rectw - st::msgReplyPadding.left() - st::msgReplyPadding.right(); if (forwarded) { p.setTextPalette(st->serviceTextPalette()); auto breakEverywhere = (forwardedHeightReal > forwardedHeight); - forwarded->text.drawElided(p, rectx, recty + st::msgReplyPadding.top(), rectw, kMaxGifForwardedBarLines, style::al_left, 0, -1, 0, breakEverywhere); + forwarded->text.drawElided(p, textx, recty + st::msgReplyPadding.top(), textw, kMaxGifForwardedBarLines, style::al_left, 0, -1, 0, breakEverywhere); p.restoreTextPalette(); const auto skip = std::min( - forwarded->text.countHeight(rectw), + forwarded->text.countHeight(textw), kMaxGifForwardedBarLines * st::msgServiceNameFont->height); recty += skip; } else if (via) { p.setFont(st::msgServiceNameFont); - p.drawTextLeft(rectx, recty + st::msgReplyPadding.top(), 2 * rectx + rectw, via->text); + p.drawTextLeft(textx, recty + st::msgReplyPadding.top(), 2 * textx + textw, via->text); int skip = st::msgServiceNameFont->height + (reply ? st::msgReplyPadding.top() : 0); recty += skip; } if (reply) { + if (forwarded || via) { + recty += st::msgReplyPadding.top(); + recth -= st::msgReplyPadding.top(); + } else { + recty -= reply->margins().top(); + } reply->paint(p, _parent, context, rectx, recty, rectw, false); } } @@ -710,7 +721,7 @@ void Gif::draw(Painter &p, const PaintContext &context) const { .availableWidth = captionw, .palette = &stm->textPalette, .pre = stm->preCache.get(), - .blockquote = stm->blockquoteCache.get(), + .blockquote = context.quoteCache(parent()->colorIndex()), .colors = context.st->highlightColors(), .spoiler = Ui::Text::DefaultSpoilerCache(), .now = context.now, @@ -1019,16 +1030,21 @@ TextState Gif::textState(QPoint point, StateRequest request) const { if (via || reply || forwarded) { auto rectw = paintw - usew - st::msgReplyPadding.left(); auto innerw = rectw - (st::msgReplyPadding.left() + st::msgReplyPadding.right()); - auto recth = st::msgReplyPadding.top() + st::msgReplyPadding.bottom(); + auto recth = 0; auto forwardedHeightReal = forwarded ? forwarded->text.countHeight(innerw) : 0; auto forwardedHeight = qMin(forwardedHeightReal, kMaxGifForwardedBarLines * st::msgServiceNameFont->height); if (forwarded) { - recth += forwardedHeight; + recth += st::msgReplyPadding.top() + forwardedHeight; } else if (via) { - recth += st::msgServiceNameFont->height + (reply ? st::msgReplyPadding.top() : 0); + recth += st::msgReplyPadding.top() + st::msgServiceNameFont->height + (reply ? st::msgReplyPadding.top() : 0); } if (reply) { - recth += st::msgReplyBarSize.height(); + const auto replyMargins = reply->margins(); + recth += reply->height() + - ((forwarded || via) ? 0 : replyMargins.top()) + - replyMargins.bottom(); + } else { + recth += st::msgReplyPadding.bottom(); } auto rectx = rightAligned ? 0 : (usew + st::msgReplyPadding.left()); auto recty = painty; @@ -1067,16 +1083,22 @@ TextState Gif::textState(QPoint point, StateRequest request) const { recth -= skip; } if (reply) { + if (forwarded || via) { + recty += st::msgReplyPadding.top(); + recth -= st::msgReplyPadding.top() + reply->margins().top(); + } else { + recty -= reply->margins().top(); + } const auto replyRect = QRect(rectx, recty, rectw, recth); if (replyRect.contains(point)) { - result.link = reply->replyToLink(); + result.link = reply->link(); reply->ripple.lastPoint = point - replyRect.topLeft(); if (!reply->ripple.animation) { reply->ripple.animation = std::make_unique( st::defaultRippleAnimation, Ui::RippleAnimation::RoundRectMask( replyRect.size(), - st::roundRadiusSmall), + st::messageQuoteStyle.radius), [=] { item->history()->owner().requestItemRepaint(item); }); } return result; @@ -1181,6 +1203,15 @@ TextForMimeData Gif::selectedText(TextSelection selection) const { return _caption.toTextForMimeData(selection); } +TextWithEntities Gif::selectedQuote(TextSelection selection) const { + return parent()->selectedQuote(_caption, selection); +} + +TextSelection Gif::selectionFromQuote( + const TextWithEntities "e) const { + return parent()->selectionFromQuote(_caption, quote); +} + bool Gif::fullFeaturedGrouped(RectParts sides) const { return (sides & RectPart::Left) && (sides & RectPart::Right); } @@ -1737,7 +1768,7 @@ int Gif::additionalWidth(const HistoryMessageVia *via, const HistoryMessageReply accumulate_max(result, st::msgReplyPadding.left() + st::msgReplyPadding.left() + via->maxWidth + st::msgReplyPadding.left()); } if (reply) { - accumulate_max(result, st::msgReplyPadding.left() + reply->replyToWidth()); + accumulate_max(result, st::msgReplyPadding.left() + reply->maxWidth()); } return result; } diff --git a/Telegram/SourceFiles/history/view/media/history_view_gif.h b/Telegram/SourceFiles/history/view/media/history_view_gif.h index 02f0aa8af7094e..dbe3eda9bfc90a 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_gif.h +++ b/Telegram/SourceFiles/history/view/media/history_view_gif.h @@ -68,6 +68,9 @@ class Gif final : public File { } TextForMimeData selectedText(TextSelection selection) const override; + TextWithEntities selectedQuote(TextSelection selection) const override; + TextSelection selectionFromQuote( + const TextWithEntities "e) const override; bool uploading() const override; diff --git a/Telegram/SourceFiles/history/view/media/history_view_giveaway.cpp b/Telegram/SourceFiles/history/view/media/history_view_giveaway.cpp new file mode 100644 index 00000000000000..dceb0cd1d53823 --- /dev/null +++ b/Telegram/SourceFiles/history/view/media/history_view_giveaway.cpp @@ -0,0 +1,530 @@ +/* +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 "history/view/media/history_view_giveaway.h" + +#include "base/unixtime.h" +#include "boxes/gift_premium_box.h" +#include "chat_helpers/stickers_gift_box_pack.h" +#include "countries/countries_instance.h" +#include "data/data_channel.h" +#include "data/data_document.h" +#include "data/data_media_types.h" +#include "data/data_session.h" +#include "dialogs/ui/dialogs_stories_content.h" +#include "dialogs/ui/dialogs_stories_list.h" +#include "history/history.h" +#include "history/history_item.h" +#include "history/history_item_components.h" +#include "history/view/history_view_element.h" +#include "history/view/history_view_cursor_state.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "ui/chat/chat_style.h" +#include "ui/chat/message_bubble.h" +#include "ui/effects/ripple_animation.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/tooltip.h" +#include "ui/painter.h" +#include "ui/round_rect.h" +#include "styles/style_chat.h" + +namespace HistoryView { +namespace { + +[[nodiscard]] QSize CountOptimalTextSize( + const Ui::Text::String &text, + int minWidth, + int maxWidth) { + if (text.maxWidth() <= maxWidth) { + return { text.maxWidth(), text.minHeight() }; + } + const auto height = text.countHeight(maxWidth); + return { Ui::FindNiceTooltipWidth(minWidth, maxWidth, [&](int width) { + return text.countHeight(width); + }), height }; +} + +} // namespace + +Giveaway::Giveaway( + not_null parent, + not_null giveaway) +: Media(parent) +, _prizesTitle(st::msgMinWidth) +, _prizes(st::msgMinWidth) +, _participantsTitle(st::msgMinWidth) +, _participants(st::msgMinWidth) +, _countries(st::msgMinWidth) +, _winnersTitle(st::msgMinWidth) +, _winners(st::msgMinWidth) { + fillFromData(giveaway); +} + +Giveaway::~Giveaway() { + if (hasHeavyPart()) { + unloadHeavyPart(); + _parent->checkHeavyPart(); + } +} + +void Giveaway::fillFromData(not_null giveaway) { + _months = giveaway->months; + _quantity = giveaway->quantity; + + _prizesTitle.setText( + st::semiboldTextStyle, + tr::lng_prizes_title(tr::now, lt_count, _quantity), + kDefaultTextOptions); + + _prizes.setMarkedText( + st::defaultTextStyle, + tr::lng_prizes_about( + tr::now, + lt_count, + _quantity, + lt_duration, + Ui::Text::Bold(GiftDuration(_months)), + Ui::Text::RichLangValue), + kDefaultTextOptions); + _participantsTitle.setText( + st::semiboldTextStyle, + tr::lng_prizes_participants(tr::now), + kDefaultTextOptions); + + for (const auto &channel : giveaway->channels) { + _channels.push_back({ + .name = Ui::Text::String( + st::semiboldTextStyle, + channel->name(), + kDefaultTextOptions, + st::msgMinWidth), + .thumbnail = Dialogs::Stories::MakeUserpicThumbnail(channel), + .link = channel->openLink(), + }); + } + const auto channels = int(_channels.size()); + + const auto &instance = Countries::Instance(); ; + auto countries = QStringList(); + for (const auto &country : giveaway->countries) { + const auto name = instance.countryNameByISO2(country); + const auto flag = instance.flagEmojiByISO2(country); + countries.push_back(flag + QChar(0xA0) + name); + } + if (const auto count = countries.size()) { + auto united = countries.front(); + for (auto i = 1; i != count; ++i) { + united = ((i + 1 == count) + ? tr::lng_prizes_countries_and_last + : tr::lng_prizes_countries_and_one)( + tr::now, + lt_countries, + united, + lt_country, + countries[i]); + } + _countries.setText( + st::defaultTextStyle, + tr::lng_prizes_countries(tr::now, lt_countries, united), + kDefaultTextOptions); + } else { + _countries.clear(); + } + + _participants.setText( + st::defaultTextStyle, + (giveaway->all + ? tr::lng_prizes_participants_all + : tr::lng_prizes_participants_new)(tr::now, lt_count, channels), + kDefaultTextOptions); + _winnersTitle.setText( + st::semiboldTextStyle, + tr::lng_prizes_date(tr::now), + kDefaultTextOptions); + _winners.setText( + st::defaultTextStyle, + langDateTime(base::unixtime::parse(giveaway->untilDate)), + kDefaultTextOptions); + + ensureStickerCreated(); +} + +QSize Giveaway::countOptimalSize() { + const auto maxWidth = st::chatGiveawayWidth; + const auto padding = inBubblePadding(); + const auto available = maxWidth - padding.left() - padding.right(); + + _stickerTop = st::chatGiveawayStickerTop; + _prizesTitleTop = _stickerTop + + st::msgServiceGiftBoxStickerSize.height() + + st::chatGiveawayPrizesTop; + _prizesTop = _prizesTitleTop + + _prizesTitle.countHeight(available) + + st::chatGiveawayPrizesSkip; + const auto prizesSize = CountOptimalTextSize( + _prizes, + st::msgMinWidth, + available); + _prizesWidth = prizesSize.width(); + _participantsTitleTop = _prizesTop + + prizesSize.height() + + st::chatGiveawayParticipantsTop; + _participantsTop = _participantsTitleTop + + _participantsTitle.countHeight(available) + + st::chatGiveawayParticipantsSkip; + const auto participantsSize = CountOptimalTextSize( + _participants, + st::msgMinWidth, + available); + _participantsWidth = participantsSize.width(); + const auto channelsTop = _participantsTop + + participantsSize.height() + + st::chatGiveawayChannelTop; + const auto channelsBottom = layoutChannels( + padding.left(), + channelsTop, + available); + _countriesTop = channelsBottom; + if (_countries.isEmpty()) { + _winnersTitleTop = _countriesTop + st::chatGiveawayDateTop; + } else { + const auto countriesSize = CountOptimalTextSize( + _countries, + st::msgMinWidth, + available); + _countriesWidth = countriesSize.width(); + _winnersTitleTop = _countriesTop + + _countries.countHeight(available) + + st::chatGiveawayCountriesSkip; + } + _winnersTop = _winnersTitleTop + + _winnersTitle.countHeight(available) + + st::chatGiveawayDateSkip; + const auto height = _winnersTop + + _winners.countHeight(available) + + st::chatGiveawayBottomSkip; + return { maxWidth, height }; +} + +int Giveaway::layoutChannels(int x, int y, int available) { + const auto size = st::chatGiveawayChannelSize; + const auto skip = st::chatGiveawayChannelSkip; + const auto padding = st::chatGiveawayChannelPadding; + auto left = available; + const auto shiftRow = [&](int i, int top, int shift) { + for (auto j = i; j != 0; --j) { + auto &geometry = _channels[j - 1].geometry; + if (geometry.top() != top) { + break; + } + geometry.moveLeft(geometry.x() + shift); + } + }; + const auto count = int(_channels.size()); + for (auto i = 0; i != count; ++i) { + const auto desired = size + + padding.left() + + _channels[i].name.maxWidth() + + padding.right(); + const auto width = std::min(desired, available); + if (left < width) { + shiftRow(i, y, (left + skip) / 2); + left = available; + y += size + skip; + } + _channels[i].geometry = { x + available - left, y, width, size }; + left -= width + skip; + } + shiftRow(count, y, (left + skip) / 2); + return y + size + skip; +} + +QSize Giveaway::countCurrentSize(int newWidth) { + return { maxWidth(), minHeight()}; +} + +void Giveaway::draw(Painter &p, const PaintContext &context) const { + if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) return; + + const auto stm = context.messageStyle(); + + auto padding = inBubblePadding(); + + const auto outer = width(); + const auto paintw = outer - padding.left() - padding.right(); + const auto stickerSize = st::msgServiceGiftBoxStickerSize; + const auto sticker = QRect( + (outer - stickerSize.width()) / 2, + _stickerTop, + stickerSize.width(), + stickerSize.height()); + + if (_sticker) { + _sticker->draw(p, context, sticker); + paintBadge(p, context); + } else { + ensureStickerCreated(); + } + const auto paintText = [&]( + const Ui::Text::String &text, + int top, + int width) { + p.setPen(stm->historyTextFg); + text.draw(p, { + .position = { padding.left() + (paintw - width) / 2, top}, + .outerWidth = outer, + .availableWidth = width, + .align = style::al_top, + .palette = &stm->textPalette, + .now = context.now, + }); + }; + paintText(_prizesTitle, _prizesTitleTop, paintw); + paintText(_prizes, _prizesTop, _prizesWidth); + paintText(_participantsTitle, _participantsTitleTop, paintw); + paintText(_participants, _participantsTop, _participantsWidth); + if (!_countries.isEmpty()) { + paintText(_countries, _countriesTop, _countriesWidth); + } + paintText(_winnersTitle, _winnersTitleTop, paintw); + paintText(_winners, _winnersTop, paintw); + paintChannels(p, context); +} + +void Giveaway::paintBadge(Painter &p, const PaintContext &context) const { + validateBadge(context); + + const auto badge = _badge.size() / _badge.devicePixelRatio(); + const auto left = (width() - badge.width()) / 2; + const auto top = st::chatGiveawayBadgeTop; + const auto rect = QRect(left, top, badge.width(), badge.height()); + const auto paintContent = [&](QPainter &q) { + q.drawImage(rect.topLeft(), _badge); + }; + + { + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + p.setBrush(context.messageStyle()->msgFileBg); + const auto half = st::chatGiveawayBadgeStroke / 2.; + const auto inner = QRectF(rect).marginsRemoved( + { half, half, half, half }); + const auto radius = inner.height() / 2.; + p.drawRoundedRect(inner, radius, radius); + } + + if (!usesBubblePattern(context)) { + paintContent(p); + } else { + Ui::PaintPatternBubblePart( + p, + context.viewport, + context.bubblesPattern->pixmap, + rect, + paintContent, + _badgeCache); + } +} + +void Giveaway::paintChannels( + Painter &p, + const PaintContext &context) const { + if (_channels.empty()) { + return; + } + + const auto size = _channels[0].geometry.height(); + const auto st = context.st; + const auto stm = context.messageStyle(); + const auto selected = context.selected(); + const auto colorIndex = parent()->colorIndex(); + const auto cache = context.outbg + ? stm->replyCache[st->colorPatternIndex(colorIndex)].get() + : st->coloredReplyCache(selected, colorIndex).get(); + if (_channelCorners[0].isNull() || _channelBg != cache->bg) { + _channelBg = cache->bg; + _channelCorners = Images::CornersMask(size / 2); + for (auto &image : _channelCorners) { + style::colorizeImage(image, cache->bg, &image); + } + } + p.setPen(cache->icon); + const auto padding = st::chatGiveawayChannelPadding; + for (const auto &channel : _channels) { + const auto &thumbnail = channel.thumbnail; + const auto &geometry = channel.geometry; + if (!_subscribedToThumbnails) { + thumbnail->subscribeToUpdates([view = parent()] { + view->history()->owner().requestViewRepaint(view); + }); + } + + Ui::DrawRoundedRect(p, geometry, _channelBg, _channelCorners); + if (channel.ripple) { + channel.ripple->paint( + p, + geometry.x(), + geometry.y(), + width(), + &cache->bg); + if (channel.ripple->empty()) { + channel.ripple = nullptr; + } + } + + p.drawImage(geometry.topLeft(), thumbnail->image(size)); + const auto left = size + padding.left(); + const auto top = padding.top(); + const auto available = geometry.width() - left - padding.right(); + channel.name.draw(p, { + .position = { geometry.left() + left, geometry.top() + top }, + .outerWidth = width(), + .availableWidth = available, + .align = style::al_left, + .palette = &stm->textPalette, + .now = context.now, + .elisionOneLine = true, + .elisionBreakEverywhere = true, + }); + } + _subscribedToThumbnails = 1; +} + +void Giveaway::ensureStickerCreated() const { + if (_sticker) { + return; + } + const auto &session = _parent->history()->session(); + auto &packs = session.giftBoxStickersPacks(); + if (const auto document = packs.lookup(_months)) { + if (const auto sticker = document->sticker()) { + const auto skipPremiumEffect = false; + _sticker.emplace(_parent, document, skipPremiumEffect, _parent); + _sticker->setDiceIndex(sticker->alt, 1); + _sticker->setGiftBoxSticker(true); + _sticker->initSize(); + } + } +} + +void Giveaway::validateBadge(const PaintContext &context) const { + const auto stm = context.messageStyle(); + const auto &badgeFg = stm->historyFileRadialFg->c; + const auto &badgeBorder = stm->msgBg->c; + if (!_badge.isNull() + && _badgeFg == badgeFg + && _badgeBorder == badgeBorder) { + return; + } + const auto &font = st::chatGiveawayBadgeFont; + _badgeFg = badgeFg; + _badgeBorder = badgeBorder; + const auto text = tr::lng_prizes_badge( + tr::now, + lt_amount, + QString::number(_quantity)); + const auto width = font->width(text); + const auto inner = QRect(0, 0, width, font->height); + const auto rect = inner.marginsAdded(st::chatGiveawayBadgePadding); + const auto size = rect.size(); + const auto ratio = style::DevicePixelRatio(); + _badge = QImage(size * ratio, QImage::Format_ARGB32_Premultiplied); + _badge.setDevicePixelRatio(ratio); + _badge.fill(Qt::transparent); + + auto p = QPainter(&_badge); + auto hq = PainterHighQualityEnabler(p); + p.setPen(QPen(_badgeBorder, st::chatGiveawayBadgeStroke * 1.)); + p.setBrush(Qt::NoBrush); + const auto half = st::chatGiveawayBadgeStroke / 2.; + const auto smaller = QRectF( + rect.translated(-rect.topLeft()) + ).marginsRemoved({ half, half, half, half }); + const auto radius = smaller.height() / 2.; + p.drawRoundedRect(smaller, radius, radius); + p.setPen(_badgeFg); + p.setFont(font); + p.drawText( + st::chatGiveawayBadgePadding.left(), + st::chatGiveawayBadgePadding.top() + font->ascent, + text); +} + +TextState Giveaway::textState(QPoint point, StateRequest request) const { + auto result = TextState(_parent); + + if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) { + return result; + } + + for (const auto &channel : _channels) { + if (channel.geometry.contains(point)) { + result.link = channel.link; + _lastPoint = point; + return result; + } + } + return result; +} + +void Giveaway::clickHandlerActiveChanged( + const ClickHandlerPtr &p, + bool active) { +} + +void Giveaway::clickHandlerPressedChanged( + const ClickHandlerPtr &p, + bool pressed) { + for (auto &channel : _channels) { + if (channel.link != p) { + continue; + } + if (pressed) { + if (!channel.ripple) { + const auto owner = &parent()->history()->owner(); + channel.ripple = std::make_unique( + st::defaultRippleAnimation, + Ui::RippleAnimation::RoundRectMask( + channel.geometry.size(), + channel.geometry.height() / 2), + [=] { owner->requestViewRepaint(parent()); }); + } + channel.ripple->add(_lastPoint - channel.geometry.topLeft()); + } else if (channel.ripple) { + channel.ripple->lastStop(); + } + break; + } +} + +bool Giveaway::hideFromName() const { + return !parent()->data()->Has(); +} + +bool Giveaway::hasHeavyPart() const { + return _subscribedToThumbnails; +} + +void Giveaway::unloadHeavyPart() { + if (_subscribedToThumbnails) { + _subscribedToThumbnails = 0; + for (const auto &channel : _channels) { + channel.thumbnail->subscribeToUpdates(nullptr); + } + } +} + +QMargins Giveaway::inBubblePadding() const { + auto lshift = st::msgPadding.left(); + auto rshift = st::msgPadding.right(); + auto bshift = isBubbleBottom() ? st::msgPadding.top() : st::mediaInBubbleSkip; + auto tshift = isBubbleTop() ? st::msgPadding.bottom() : st::mediaInBubbleSkip; + return QMargins(lshift, tshift, rshift, bshift); +} + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/media/history_view_giveaway.h b/Telegram/SourceFiles/history/view/media/history_view_giveaway.h new file mode 100644 index 00000000000000..6529ad4971cc9e --- /dev/null +++ b/Telegram/SourceFiles/history/view/media/history_view_giveaway.h @@ -0,0 +1,122 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "history/view/media/history_view_media.h" +#include "history/view/media/history_view_sticker.h" + +namespace Data { +struct Giveaway; +} // namespace Data + +namespace Dialogs::Stories { +class Thumbnail; +} // namespace Dialogs::Stories + +namespace Ui { +class RippleAnimation; +} // namespace Ui + +namespace HistoryView { + +class Giveaway final : public Media { +public: + Giveaway( + not_null parent, + not_null giveaway); + ~Giveaway(); + + void draw(Painter &p, const PaintContext &context) const override; + TextState textState(QPoint point, StateRequest request) const override; + + void clickHandlerActiveChanged( + const ClickHandlerPtr &p, + bool active) override; + void clickHandlerPressedChanged( + const ClickHandlerPtr &p, + bool pressed) override; + + bool needsBubble() const override { + return true; + } + bool customInfoLayout() const override { + return false; + } + + bool toggleSelectionByHandlerClick( + const ClickHandlerPtr &p) const override { + return true; + } + bool dragItemByHandler(const ClickHandlerPtr &p) const override { + return true; + } + + bool hideFromName() const override; + + void unloadHeavyPart() override; + bool hasHeavyPart() const override; + +private: + using Thumbnail = Dialogs::Stories::Thumbnail; + struct Channel { + Ui::Text::String name; + std::shared_ptr thumbnail; + QRect geometry; + ClickHandlerPtr link; + mutable std::unique_ptr ripple; + }; + + void paintBadge(Painter &p, const PaintContext &context) const; + void paintChannels(Painter &p, const PaintContext &context) const; + int layoutChannels(int x, int y, int available); + QSize countOptimalSize() override; + QSize countCurrentSize(int newWidth) override; + + void fillFromData(not_null giveaway); + void ensureStickerCreated() const; + void validateBadge(const PaintContext &context) const; + + [[nodiscard]] QMargins inBubblePadding() const; + + mutable std::optional _sticker; + + Ui::Text::String _prizesTitle; + Ui::Text::String _prizes; + Ui::Text::String _participantsTitle; + Ui::Text::String _participants; + std::vector _channels; + Ui::Text::String _countries; + Ui::Text::String _winnersTitle; + Ui::Text::String _winners; + + mutable QColor _channelBg; + mutable QColor _badgeFg; + mutable QColor _badgeBorder; + mutable std::array _channelCorners; + mutable QImage _badge; + mutable QImage _badgeCache; + + mutable QPoint _lastPoint; + int _months = 0; + int _quantity = 0; + int _stickerTop = 0; + int _prizesTitleTop = 0; + int _prizesTop = 0; + int _prizesWidth = 0; + int _participantsTitleTop = 0; + int _participantsTop = 0; + int _participantsWidth = 0; + int _countriesTop = 0; + int _countriesWidth = 0; + int _winnersTitleTop = 0; + int _winnersTop = 0; + mutable uint8 _subscribedToThumbnails : 1 = 0; + +}; + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/media/history_view_media.h b/Telegram/SourceFiles/history/view/media/history_view_media.h index ef1e812505224f..67ee8cedc55c5d 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media.h +++ b/Telegram/SourceFiles/history/view/media/history_view_media.h @@ -86,7 +86,15 @@ class Media : public Object, public base::has_weak_ptr { [[nodiscard]] virtual TextForMimeData selectedText( TextSelection selection) const { - return TextForMimeData(); + return {}; + } + [[nodiscard]] virtual TextWithEntities selectedQuote( + TextSelection selection) const { + return {}; + } + [[nodiscard]] virtual TextSelection selectionFromQuote( + const TextWithEntities "e) const { + return {}; } [[nodiscard]] virtual bool isDisplayed() const; @@ -101,6 +109,9 @@ class Media : public Object, public base::has_weak_ptr { [[nodiscard]] virtual bool hideServiceText() const { return false; } + [[nodiscard]] virtual bool hideFromName() const { + return false; + } [[nodiscard]] virtual bool allowsFastShare() const { return false; } diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_grouped.cpp b/Telegram/SourceFiles/history/view/media/history_view_media_grouped.cpp index b5d63499d3dc09..9ea9cc8bed808f 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_grouped.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media_grouped.cpp @@ -368,7 +368,7 @@ void GroupedMedia::draw(Painter &p, const PaintContext &context) const { .availableWidth = captionw, .palette = &stm->textPalette, .pre = stm->preCache.get(), - .blockquote = stm->blockquoteCache.get(), + .blockquote = context.quoteCache(parent()->colorIndex()), .colors = context.st->highlightColors(), .spoiler = Ui::Text::DefaultSpoilerCache(), .now = context.now, diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.cpp b/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.cpp index 37e9bb6f722dee..b89e28f7687e84 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.cpp @@ -131,7 +131,7 @@ QSize UnwrappedMedia::countCurrentSize(int newWidth) { via->resize(availw); } if (reply) { - reply->resize(availw); + [[maybe_unused]] int height = reply->resizeToWidth(availw); } } return { newWidth, newHeight }; @@ -219,11 +219,16 @@ UnwrappedMedia::SurroundingInfo UnwrappedMedia::surroundingInfo( panelHeight += st::msgServiceNameFont->height + (reply ? st::msgReplyPadding.top() : 0); } - if (reply) { - panelHeight += st::msgReplyBarSize.height(); - } if (panelHeight) { - panelHeight += st::msgReplyPadding.top() + st::msgReplyPadding.bottom(); + panelHeight += st::msgReplyPadding.top(); + } + if (reply) { + const auto replyMargins = reply->margins(); + panelHeight += reply->height() + - ((forwarded || via) ? 0 : replyMargins.top()) + - replyMargins.bottom(); + } else { + panelHeight += st::msgReplyPadding.bottom(); } const auto total = (topicSize.isEmpty() ? 0 : topicSize.height()) + ((panelHeight || !topicSize.height()) ? st::topicButtonSkip : 0) @@ -311,26 +316,32 @@ void UnwrappedMedia::drawSurrounding( Ui::FillRoundRect(p, rectx, recty, rectw, recth, sti->msgServiceBg, sti->msgServiceBgCornersSmall); p.setPen(st->msgServiceFg()); - rectx += st::msgReplyPadding.left(); - rectw -= st::msgReplyPadding.left() + st::msgReplyPadding.right(); + const auto textx = rectx + st::msgReplyPadding.left(); + const auto textw = rectw - st::msgReplyPadding.left() - st::msgReplyPadding.right(); if (forwarded) { p.setTextPalette(st->serviceTextPalette()); - forwarded->text.drawElided(p, rectx, recty + st::msgReplyPadding.top(), rectw, kMaxForwardedBarLines, style::al_left, 0, -1, 0, surrounding.forwardedBreakEverywhere); + forwarded->text.drawElided(p, textx, recty + st::msgReplyPadding.top(), textw, kMaxForwardedBarLines, style::al_left, 0, -1, 0, surrounding.forwardedBreakEverywhere); p.restoreTextPalette(); const auto skip = std::min( - forwarded->text.countHeight(rectw), + forwarded->text.countHeight(textw), kMaxForwardedBarLines * st::msgServiceNameFont->height); recty += skip; } else if (via) { p.setFont(st::msgDateFont); - p.drawTextLeft(rectx, recty + st::msgReplyPadding.top(), 2 * rectx + rectw, via->text); + p.drawTextLeft(rectx, recty + st::msgReplyPadding.top(), 2 * textx + textw, via->text); const auto skip = st::msgServiceNameFont->height + (reply ? st::msgReplyPadding.top() : 0); recty += skip; } if (reply) { + if (forwarded || via) { + recty += st::msgReplyPadding.top(); + recth -= st::msgReplyPadding.top(); + } else { + recty -= reply->margins().top(); + } reply->paint(p, _parent, context, rectx, recty, rectw, false); } replyRight = rectx + rectw; @@ -338,8 +349,9 @@ void UnwrappedMedia::drawSurrounding( } if (rightActionSize) { const auto position = calculateFastActionPosition( - fullBottom, replyRight, + reply ? reply->height() : 0, + fullBottom, fullRight, *rightActionSize); const auto outer = 2 * inner.x() + inner.width(); @@ -470,16 +482,22 @@ TextState UnwrappedMedia::textState(QPoint point, StateRequest request) const { recth -= skip; } if (reply) { + if (forwarded || via) { + recty += st::msgReplyPadding.top(); + recth -= st::msgReplyPadding.top() + reply->margins().top(); + } else { + recty -= reply->margins().top(); + } const auto replyRect = QRect(rectx, recty, rectw, recth); if (replyRect.contains(point)) { - result.link = reply->replyToLink(); + result.link = reply->link(); reply->ripple.lastPoint = point - replyRect.topLeft(); if (!reply->ripple.animation) { reply->ripple.animation = std::make_unique( st::defaultRippleAnimation, Ui::RippleAnimation::RoundRectMask( replyRect.size(), - st::roundRadiusSmall), + st::messageQuoteStyle.radius), [=] { item->history()->owner().requestItemRepaint(item); }); } return result; @@ -503,8 +521,9 @@ TextState UnwrappedMedia::textState(QPoint point, StateRequest request) const { } if (rightActionSize) { const auto position = calculateFastActionPosition( - fullBottom, replyRight, + reply ? reply->height() : 0, + fullBottom, fullRight, *rightActionSize); if (QRect(position.x(), position.y(), rightActionSize->width(), rightActionSize->height()).contains(point)) { @@ -527,10 +546,9 @@ bool UnwrappedMedia::hasTextForCopy() const { return _content->hasTextForCopy(); } -bool UnwrappedMedia::dragItemByHandler( - const ClickHandlerPtr &p) const { +bool UnwrappedMedia::dragItemByHandler(const ClickHandlerPtr &p) const { const auto reply = _parent->displayedReply(); - return !(reply && (reply->replyToLink() == p)); + return !reply || (reply->link() != p); } QRect UnwrappedMedia::contentRectForReactions() const { @@ -607,17 +625,16 @@ int UnwrappedMedia::calculateFullRight(const QRect &inner) const { } QPoint UnwrappedMedia::calculateFastActionPosition( - int fullBottom, int replyRight, + int replyHeight, + int fullBottom, int fullRight, QSize size) const { const auto fastShareTop = (fullBottom - st::historyFastShareBottom - size.height()); - const auto doesRightActionHitReply = replyRight && (fastShareTop < - st::msgReplyBarSize.height() - + st::msgReplyPadding.top() - + st::msgReplyPadding.bottom()); + const auto doesRightActionHitReply = replyRight + && (fastShareTop < replyHeight); const auto fastShareLeft = ((doesRightActionHitReply ? replyRight : fullRight) + st::historyFastShareLeft); @@ -650,7 +667,7 @@ int UnwrappedMedia::additionalWidth( accumulate_max(result, 2 * st::msgReplyPadding.left() + via->maxWidth + st::msgReplyPadding.right()); } if (reply) { - accumulate_max(result, st::msgReplyPadding.left() + reply->replyToWidth()); + accumulate_max(result, reply->maxWidth()); } return result; } diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.h b/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.h index 9cf92a1f442368..7f6a5d33d2f39f 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.h +++ b/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.h @@ -145,8 +145,9 @@ class UnwrappedMedia final : public Media { int calculateFullRight(const QRect &inner) const; QPoint calculateFastActionPosition( - int fullBottom, int replyRight, + int replyHeight, + int fullBottom, int fullRight, QSize size) const; diff --git a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp index 11cec5c0ea5179..f84af98b24bb4e 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp @@ -406,7 +406,7 @@ void Photo::draw(Painter &p, const PaintContext &context) const { .availableWidth = captionw, .palette = &stm->textPalette, .pre = stm->preCache.get(), - .blockquote = stm->blockquoteCache.get(), + .blockquote = context.quoteCache(parent()->colorIndex()), .colors = context.st->highlightColors(), .spoiler = Ui::Text::DefaultSpoilerCache(), .now = context.now, @@ -1049,6 +1049,15 @@ TextForMimeData Photo::selectedText(TextSelection selection) const { return _caption.toTextForMimeData(selection); } +TextWithEntities Photo::selectedQuote(TextSelection selection) const { + return parent()->selectedQuote(_caption, selection); +} + +TextSelection Photo::selectionFromQuote( + const TextWithEntities "e) const { + return parent()->selectionFromQuote(_caption, quote); +} + void Photo::hideSpoilers() { _caption.setSpoilerRevealed(false, anim::type::instant); if (_spoiler) { diff --git a/Telegram/SourceFiles/history/view/media/history_view_photo.h b/Telegram/SourceFiles/history/view/media/history_view_photo.h index cd960e1c6190cd..dfec77d99c7bc5 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_photo.h +++ b/Telegram/SourceFiles/history/view/media/history_view_photo.h @@ -57,6 +57,9 @@ class Photo final : public File { } TextForMimeData selectedText(TextSelection selection) const override; + TextWithEntities selectedQuote(TextSelection selection) const override; + TextSelection selectionFromQuote( + const TextWithEntities "e) const override; PhotoData *getPhoto() const override { return _data; diff --git a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp index d3bc6b154f6921..901d51daaedb54 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp @@ -7,37 +7,29 @@ For license and copyright information please follow this link: */ #include "history/view/media/history_view_premium_gift.h" +#include "boxes/gift_premium_box.h" // ResolveGiftCode #include "chat_helpers/stickers_gift_box_pack.h" #include "core/click_handler_types.h" // ClickHandlerContext #include "data/data_document.h" +#include "data/data_channel.h" #include "history/history.h" #include "history/history_item.h" #include "history/view/history_view_element.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "settings/settings_premium.h" // Settings::ShowGiftPremium +#include "ui/text/text_utilities.h" #include "window/window_session_controller.h" #include "styles/style_chat.h" namespace HistoryView { -namespace { - -[[nodiscard]] QString FormatGiftMonths(int months) { - return (months < 12) - ? tr::lng_premium_gift_duration_months(tr::now, lt_count, months) - : tr::lng_premium_gift_duration_years( - tr::now, - lt_count, - std::round(months / 12.)); -} - -} // namespace PremiumGift::PremiumGift( not_null parent, not_null gift) : _parent(parent) -, _gift(gift) { +, _gift(gift) +, _data(gift->data()) { } PremiumGift::~PremiumGift() = default; @@ -51,27 +43,62 @@ QSize PremiumGift::size() { } QString PremiumGift::title() { - return tr::lng_premium_summary_title(tr::now); + return _data.slug.isEmpty() + ? tr::lng_premium_summary_title(tr::now) + : _data.unclaimed + ? tr::lng_prize_unclaimed_title(tr::now) + : tr::lng_prize_title(tr::now); } TextWithEntities PremiumGift::subtitle() { - return { FormatGiftMonths(_gift->months()) }; + if (_data.slug.isEmpty()) { + return { GiftDuration(_data.months) }; + } + const auto name = _data.channel ? _data.channel->name() : "channel"; + auto result = (_data.unclaimed + ? tr::lng_prize_unclaimed_about + : _data.viaGiveaway + ? tr::lng_prize_about + : tr::lng_prize_gift_about)( + tr::now, + lt_channel, + Ui::Text::Bold(name), + Ui::Text::RichLangValue); + result.append("\n\n"); + result.append((_data.unclaimed + ? tr::lng_prize_unclaimed_duration + : _data.viaGiveaway + ? tr::lng_prize_duration + : tr::lng_prize_gift_duration)( + tr::now, + lt_duration, + Ui::Text::Bold(GiftDuration(_data.months)), + Ui::Text::RichLangValue)); + return result; } QString PremiumGift::button() { - return tr::lng_sticker_premium_view(tr::now); + return _data.slug.isEmpty() + ? tr::lng_sticker_premium_view(tr::now) + : tr::lng_prize_open(tr::now); } ClickHandlerPtr PremiumGift::createViewLink() { const auto from = _gift->from(); const auto to = _parent->history()->peer; - const auto months = _gift->months(); + const auto data = _gift->data(); return std::make_shared([=](ClickContext context) { const auto my = context.other.value(); if (const auto controller = my.sessionWindow.get()) { - const auto me = (from->id == controller->session().userPeerId()); - const auto peer = me ? to : from; - Settings::ShowGiftPremium(controller, peer, months, me); + if (data.slug.isEmpty()) { + const auto selfId = controller->session().userPeerId(); + const auto self = (from->id == selfId); + const auto peer = self ? to : from; + const auto months = data.months; + Settings::ShowGiftPremium(controller, peer, months, self); + } else { + ResolveGiftCode(controller, data.slug); + } } }); } @@ -91,6 +118,10 @@ void PremiumGift::draw( } } +bool PremiumGift::hideServiceText() { + return !_data.slug.isEmpty(); +} + void PremiumGift::stickerClearLoopPlayed() { if (_sticker) { _sticker->stickerClearLoopPlayed(); @@ -120,8 +151,9 @@ void PremiumGift::ensureStickerCreated() const { return; } const auto &session = _parent->history()->session(); + const auto months = _gift->data().months; auto &packs = session.giftBoxStickersPacks(); - if (const auto document = packs.lookup(_gift->months())) { + if (const auto document = packs.lookup(months)) { if (const auto sticker = document->sticker()) { const auto skipPremiumEffect = false; _sticker.emplace(_parent, document, skipPremiumEffect, _parent); diff --git a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h index d68b4c6683376a..e6288f4a50f74b 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h +++ b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h @@ -12,6 +12,7 @@ For license and copyright information please follow this link: namespace Data { class MediaGiftBox; +struct GiftCode; } // namespace Data namespace HistoryView { @@ -35,10 +36,7 @@ class PremiumGift final : public ServiceBoxContent { const QRect &geometry) override; ClickHandlerPtr createViewLink() override; - bool hideServiceText() override { - return false; - } - + bool hideServiceText() override; void stickerClearLoopPlayed() override; std::unique_ptr stickerTakePlayer( not_null data, @@ -52,6 +50,7 @@ class PremiumGift final : public ServiceBoxContent { const not_null _parent; const not_null _gift; + const Data::GiftCode &_data; mutable std::optional _sticker; }; diff --git a/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp b/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp index fcc83690c28443..6b6761bb1e1112 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp @@ -24,6 +24,7 @@ For license and copyright information please follow this link: #include "ui/text/format_values.h" #include "ui/chat/chat_style.h" #include "ui/cached_round_corners.h" +#include "ui/effects/ripple_animation.h" #include "ui/painter.h" #include "ui/power_saving.h" #include "data/data_session.h" @@ -84,37 +85,113 @@ std::vector> PrepareCollageMedia( return result; } +[[nodiscard]] QString PageToPhrase(not_null webpage) { + const auto type = webpage->type; + return Ui::Text::Upper((type == WebPageType::Theme) + ? tr::lng_view_button_theme(tr::now) + : (type == WebPageType::Story) + ? tr::lng_view_button_story(tr::now) + : (type == WebPageType::Message) + ? tr::lng_view_button_message(tr::now) + : (type == WebPageType::Group) + ? tr::lng_view_button_group(tr::now) + : (type == WebPageType::WallPaper) + ? tr::lng_view_button_background(tr::now) + : (type == WebPageType::Channel) + ? tr::lng_view_button_channel(tr::now) + : (type == WebPageType::GroupWithRequest + || type == WebPageType::ChannelWithRequest) + ? tr::lng_view_button_request_join(tr::now) + : (type == WebPageType::ChannelBoost) + ? tr::lng_view_button_boost(tr::now) + : (type == WebPageType::VoiceChat) + ? tr::lng_view_button_voice_chat(tr::now) + : (type == WebPageType::Livestream) + ? tr::lng_view_button_voice_chat_channel(tr::now) + : (type == WebPageType::Bot) + ? tr::lng_view_button_bot(tr::now) + : (type == WebPageType::User) + ? tr::lng_view_button_user(tr::now) + : (type == WebPageType::BotApp) + ? tr::lng_view_button_bot_app(tr::now) + : QString()); +} + } // namespace WebPage::WebPage( not_null parent, - not_null data) + not_null data, + MediaWebPageFlags flags) : Media(parent) +, _st(st::historyPagePreview) , _data(data) -, _siteName(st::msgMinWidth - st::webPageLeft) -, _title(st::msgMinWidth - st::webPageLeft) -, _description(st::msgMinWidth - st::webPageLeft) { +, _siteName(st::msgMinWidth - _st.padding.left() - _st.padding.right()) +, _title(st::msgMinWidth - _st.padding.left() - _st.padding.right()) +, _description(st::msgMinWidth - _st.padding.left() - _st.padding.right()) +, _flags(flags) { history()->owner().registerWebPageView(_data, _parent); } +bool WebPage::HasButton(not_null webpage) { + const auto type = webpage->type; + return (type == WebPageType::Message) + || (type == WebPageType::Group) + || (type == WebPageType::Channel) + || (type == WebPageType::ChannelBoost) + // || (type == WebPageType::Bot) + || (type == WebPageType::User) + || (type == WebPageType::VoiceChat) + || (type == WebPageType::Livestream) + || (type == WebPageType::BotApp) + || ((type == WebPageType::Theme) + && webpage->document + && webpage->document->isTheme()) + || ((type == WebPageType::Story) + && (webpage->photo || webpage->document)) + || ((type == WebPageType::WallPaper) + && webpage->document + && webpage->document->isWallPaper()); +} + QSize WebPage::countOptimalSize() { if (_data->pendingTill) { return { 0, 0 }; } + + // Detect _openButtonWidth before counting paddings. + _openButton = QString(); + _openButtonWidth = 0; + if (HasButton(_data)) { + _openButton = PageToPhrase(_data); + _openButtonWidth = st::semiboldFont->width(_openButton); + } + + const auto padding = inBubblePadding() + innerMargin(); const auto versionChanged = (_dataVersion != _data->version); if (versionChanged) { _dataVersion = _data->version; _openl = nullptr; _attach = nullptr; _collage = PrepareCollageMedia(_parent->data(), _data->collage); - _siteName = Ui::Text::String(st::msgMinWidth - st::webPageLeft); - _title = Ui::Text::String(st::msgMinWidth - st::webPageLeft); - _description = Ui::Text::String(st::msgMinWidth - st::webPageLeft); + const auto min = st::msgMinWidth + - _st.padding.left() + - _st.padding.right(); + _siteName = Ui::Text::String(min); + _title = Ui::Text::String(min); + _description = Ui::Text::String(min); } auto lineHeight = UnitedLineHeight(); if (!_openl && !_data->url.isEmpty()) { const auto previewOfHiddenUrl = [&] { + if (_data->type == WebPageType::BotApp) { + // Bot Web Apps always show confirmation on hidden urls. + // + // But from the dedicated "Open App" button we don't want + // to request users confirmation on non-first app opening. + return false; + } const auto simplify = [](const QString &url) { auto result = url.toLower(); if (result.endsWith('/')) { @@ -162,31 +239,13 @@ QSize WebPage::countOptimalSize() { auto title = TextUtilities::SingleLine(_data->title.isEmpty() ? _data->author : _data->title); - if (!_collage.empty()) { - _asArticle = false; - } else if (!_data->document - && _data->photo - && _data->type != WebPageType::Photo - && _data->type != WebPageType::Document - && _data->type != WebPageType::Story - && _data->type != WebPageType::Video) { - if (_data->type == WebPageType::Profile) { - _asArticle = true; - } else if (_data->siteName == u"Twitter"_q - || _data->siteName == u"Facebook"_q - || _data->type == WebPageType::ArticleWithIV) { - _asArticle = false; - } else { - _asArticle = true; - } - if (_asArticle - && _data->description.text.isEmpty() - && title.isEmpty() - && _data->siteName.isEmpty()) { - _asArticle = false; - } + using Flag = MediaWebPageFlag; + if (_data->hasLargeMedia && (_flags & Flag::ForceLargeMedia)) { + _asArticle = 0; + } else if (_data->photo && (_flags & Flag::ForceSmallMedia)) { + _asArticle = 1; } else { - _asArticle = false; + _asArticle = _data->computeDefaultSmallMedia(); } // init attach @@ -199,13 +258,6 @@ QSize WebPage::countOptimalSize() { _data->url); } - _hasViewButton = ViewButton::MediaHasViewButton(_data); - - const auto textFloatsAroundInfo = !_asArticle - && !_attach - && isBubbleBottom() - && !_hasViewButton; - // init strings if (_description.isEmpty() && !_data->description.text.isEmpty()) { auto text = _data->description; @@ -213,9 +265,8 @@ QSize WebPage::countOptimalSize() { if (isLogEntryOriginal()) { // Fix layout for small bubbles (narrow media caption edit log entries). _description = Ui::Text::String(st::minPhotoSize - - st::msgPadding.left() - - st::msgPadding.right() - - st::webPageLeft); + - padding.left() + - padding.right()); } using MarkedTextContext = Core::MarkedTextContext; auto context = MarkedTextContext{ @@ -232,17 +283,13 @@ QSize WebPage::countOptimalSize() { text, Ui::WebpageTextDescriptionOptions(), context); - if (textFloatsAroundInfo) { - _description.updateSkipBlock( - _parent->skipBlockWidth(), - _parent->skipBlockHeight()); - } } - if (!displayedSiteName().isEmpty()) { + const auto siteName = _data->displayedSiteName(); + if (!siteName.isEmpty()) { _siteNameLines = 1; _siteName.setMarkedText( st::webPageTitleStyle, - Ui::Text::Link(displayedSiteName(), _data->url), + Ui::Text::Link(siteName, _data->url), Ui::WebpageTextTitleOptions()); } if (_title.isEmpty() && !title.isEmpty()) { @@ -259,11 +306,6 @@ QSize WebPage::countOptimalSize() { title, Ui::WebpageTextTitleOptions()); } - if (textFloatsAroundInfo && _description.isEmpty()) { - _title.updateSkipBlock( - _parent->skipBlockWidth(), - _parent->skipBlockHeight()); - } } // init dimensions @@ -282,11 +324,7 @@ QSize WebPage::countOptimalSize() { } if (!_siteName.isEmpty()) { - if (_title.isEmpty() && _description.isEmpty() && textFloatsAroundInfo) { - accumulate_max(maxWidth, _siteName.maxWidth() + _parent->skipBlockWidth()); - } else { - accumulate_max(maxWidth, _siteName.maxWidth() + articlePhotoMaxWidth); - } + accumulate_max(maxWidth, _siteName.maxWidth() + articlePhotoMaxWidth); minHeight += lineHeight; } if (!_title.isEmpty()) { @@ -309,16 +347,16 @@ QSize WebPage::countOptimalSize() { } accumulate_max(maxWidth, maxMediaWidth); minHeight += _attach->minHeight() - bubble.top() - bubble.bottom(); - if (!_attach->additionalInfoString().isEmpty()) { - minHeight += bottomInfoPadding(); - } } if (_data->type == WebPageType::Video && _data->duration) { _duration = Ui::FormatDurationText(_data->duration); _durationWidth = st::msgDateFont->width(_duration); } - maxWidth += st::msgPadding.left() + st::webPageLeft + st::msgPadding.right(); - auto padding = inBubblePadding(); + if (_openButtonWidth) { + const auto &margins = st::historyPageButtonPadding; + maxWidth += margins.left() + _openButtonWidth + margins.right(); + } + maxWidth += padding.left() + padding.right(); minHeight += padding.top() + padding.bottom(); if (_asArticle) { @@ -332,7 +370,8 @@ QSize WebPage::countCurrentSize(int newWidth) { return { newWidth, minHeight() }; } - auto innerWidth = newWidth - st::msgPadding.left() - st::webPageLeft - st::msgPadding.right(); + auto padding = inBubblePadding() + innerMargin(); + auto innerWidth = newWidth - padding.left() - padding.right(); auto newHeight = 0; auto lineHeight = UnitedLineHeight(); @@ -373,7 +412,6 @@ QSize WebPage::countCurrentSize(int newWidth) { _pixh -= lineHeight; } while (_pixh > lineHeight); - newHeight += bottomInfoPadding(); } else { newHeight = siteNameHeight; @@ -410,16 +448,8 @@ QSize WebPage::countCurrentSize(int newWidth) { _attach->resizeGetHeight(innerWidth + bubble.left() + bubble.right()); newHeight += _attach->height() - bubble.top() - bubble.bottom(); - if (!_attach->additionalInfoString().isEmpty()) { - newHeight += bottomInfoPadding(); - } else if (isBubbleBottom() && _attach->customInfoLayout() && _attach->width() + _parent->skipBlockWidth() > innerWidth + bubble.left() + bubble.right()) { - newHeight += bottomInfoPadding(); - } - } else if (_hasViewButton) { - newHeight += bottomInfoPadding(); } } - auto padding = inBubblePadding(); newHeight += padding.top() + padding.bottom(); return { newWidth, newHeight }; @@ -476,34 +506,35 @@ void WebPage::unloadHeavyPart() { } void WebPage::draw(Painter &p, const PaintContext &context) const { - if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) return; - auto paintw = width(); - + if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) { + return; + } const auto st = context.st; const auto sti = context.imageStyle(); const auto stm = context.messageStyle(); - const auto &barfg = stm->msgReplyBarColor; - const auto &semibold = stm->msgServiceFg; - - QMargins bubble(_attach ? _attach->bubbleMargins() : QMargins()); - auto padding = inBubblePadding(); - auto tshift = padding.top(); - auto bshift = padding.bottom(); - paintw -= padding.left() + padding.right(); + const auto bubble = _attach ? _attach->bubbleMargins() : QMargins(); + const auto full = QRect(0, 0, width(), height()); + auto outer = full.marginsRemoved(inBubblePadding()); + auto inner = outer.marginsRemoved(innerMargin()); + auto tshift = inner.top(); + auto paintw = inner.width(); auto attachAdditionalInfoText = _attach ? _attach->additionalInfoString() : QString(); - if (asArticle()) { - bshift += bottomInfoPadding(); - } else if (!attachAdditionalInfoText.isEmpty()) { - bshift += bottomInfoPadding(); - } else if (isBubbleBottom() && _attach && _attach->customInfoLayout() && _attach->width() + _parent->skipBlockWidth() > paintw + bubble.left() + bubble.right()) { - bshift += bottomInfoPadding(); - } else if (_hasViewButton) { - bshift += bottomInfoPadding(); - } - QRect bar(style::rtlrect(st::msgPadding.left(), tshift, st::webPageBar, height() - tshift - bshift, width())); - p.fillRect(bar, barfg); + const auto selected = context.selected(); + const auto colorIndex = parent()->colorIndex(); + const auto cache = context.outbg + ? stm->replyCache[st->colorPatternIndex(colorIndex)].get() + : st->coloredReplyCache(selected, colorIndex).get(); + Ui::Text::ValidateQuotePaintCache(*cache, _st); + Ui::Text::FillQuotePaint(p, outer, *cache, _st); + + if (_ripple) { + _ripple->paint(p, outer.x(), outer.y(), width(), &cache->bg); + if (_ripple->empty()) { + _ripple = nullptr; + } + } auto lineHeight = UnitedLineHeight(); if (asArticle()) { @@ -535,26 +566,28 @@ void WebPage::draw(Painter &p, const PaintContext &context) const { } else if (const auto blurred = _photoMedia->thumbnailInline()) { pix = blurred->pixSingle(size, args.blurred()); } - p.drawPixmapLeft(padding.left() + paintw - pw, tshift, width(), pix); + p.drawPixmapLeft(inner.left() + paintw - pw, tshift, width(), pix); if (context.selected()) { const auto st = context.st; Ui::FillRoundRect( p, - style::rtlrect(padding.left() + paintw - pw, tshift, pw, _pixh, width()), + style::rtlrect(inner.left() + paintw - pw, tshift, pw, _pixh, width()), st->msgSelectOverlay(), st->msgSelectOverlayCorners(Ui::CachedCornerRadius::Small)); } paintw -= pw + st::webPagePhotoDelta; } if (_siteNameLines) { - p.setPen(semibold); - p.setTextPalette(stm->semiboldPalette); + p.setPen(cache->icon); + p.setTextPalette(context.outbg + ? stm->semiboldPalette + : st->coloredTextPalette(selected, colorIndex)); auto endskip = 0; if (_siteName.hasSkipBlock()) { endskip = _parent->skipBlockWidth(); } - _siteName.drawLeftElided(p, padding.left(), tshift, paintw, width(), _siteNameLines, style::al_left, 0, -1, endskip, false, context.selection); + _siteName.drawLeftElided(p, inner.left(), tshift, paintw, width(), _siteNameLines, style::al_left, 0, -1, endskip, false, context.selection); tshift += lineHeight; p.setTextPalette(stm->textPalette); @@ -565,7 +598,7 @@ void WebPage::draw(Painter &p, const PaintContext &context) const { if (_title.hasSkipBlock()) { endskip = _parent->skipBlockWidth(); } - _title.drawLeftElided(p, padding.left(), tshift, paintw, width(), _titleLines, style::al_left, 0, -1, endskip, false, toTitleSelection(context.selection)); + _title.drawLeftElided(p, inner.left(), tshift, paintw, width(), _titleLines, style::al_left, 0, -1, endskip, false, toTitleSelection(context.selection)); tshift += _titleLines * lineHeight; } if (_descriptionLines) { @@ -575,7 +608,7 @@ void WebPage::draw(Painter &p, const PaintContext &context) const { } _parent->prepareCustomEmojiPaint(p, context, _description); _description.draw(p, { - .position = { padding.left(), tshift }, + .position = { inner.left(), tshift }, .outerWidth = width(), .availableWidth = paintw, .spoiler = Ui::Text::DefaultSpoilerCache(), @@ -596,7 +629,7 @@ void WebPage::draw(Painter &p, const PaintContext &context) const { auto attachAtTop = !_siteNameLines && !_titleLines && !_descriptionLines; if (!attachAtTop) tshift += st::mediaInBubbleSkip; - auto attachLeft = padding.left() - bubble.left(); + auto attachLeft = inner.left() - bubble.left(); auto attachTop = tshift - bubble.top(); if (rtl()) attachLeft = width() - attachLeft - _attach->width(); @@ -641,9 +674,24 @@ void WebPage::draw(Painter &p, const PaintContext &context) const { if (!attachAdditionalInfoText.isEmpty()) { p.setFont(st::msgDateFont); p.setPen(stm->msgDateFg); - p.drawTextLeft(st::msgPadding.left(), bar.y() + bar.height() + st::mediaInBubbleSkip, width(), attachAdditionalInfoText); + p.drawTextLeft(st::msgPadding.left(), outer.y() + outer.height() + st::mediaInBubbleSkip, width(), attachAdditionalInfoText); } } + + if (_openButtonWidth) { + p.setFont(st::semiboldFont); + p.setPen(cache->icon); + const auto end = inner.y() + inner.height() + _st.padding.bottom(); + const auto line = st::historyPageButtonLine; + auto color = cache->icon; + color.setAlphaF(color.alphaF() * 0.3); + p.fillRect(inner.x(), end, inner.width(), line, color); + const auto top = end + st::historyPageButtonPadding.top(); + p.drawText( + inner.x() + (inner.width() - _openButtonWidth) / 2, + top + st::semiboldFont->ascent, + _openButton); + } } bool WebPage::asArticle() const { @@ -656,22 +704,19 @@ TextState WebPage::textState(QPoint point, StateRequest request) const { if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) { return result; } - auto paintw = width(); - - QMargins bubble(_attach ? _attach->bubbleMargins() : QMargins()); - auto padding = inBubblePadding(); - auto tshift = padding.top(); - auto bshift = padding.bottom(); - if (asArticle() || (isBubbleBottom() && _attach && _attach->customInfoLayout() && _attach->width() + _parent->skipBlockWidth() > paintw + bubble.left() + bubble.right())) { - bshift += bottomInfoPadding(); - } - paintw -= padding.left() + padding.right(); + const auto bubble = _attach ? _attach->bubbleMargins() : QMargins(); + const auto full = QRect(0, 0, width(), height()); + auto outer = full.marginsRemoved(inBubblePadding()); + auto inner = outer.marginsRemoved(innerMargin()); + auto tshift = inner.top(); + auto paintw = inner.width(); + auto attachAdditionalInfoText = _attach ? _attach->additionalInfoString() : QString(); auto lineHeight = UnitedLineHeight(); auto inThumb = false; if (asArticle()) { auto pw = qMax(_pixw, lineHeight); - if (style::rtlrect(padding.left() + paintw - pw, tshift, pw, _pixh, width()).contains(point)) { + if (style::rtlrect(inner.left() + paintw - pw, tshift, pw, _pixh, width()).contains(point)) { inThumb = true; } paintw -= pw + st::webPagePhotoDelta; @@ -682,7 +727,7 @@ TextState WebPage::textState(QPoint point, StateRequest request) const { Ui::Text::StateRequestElided siteNameRequest = request.forText(); siteNameRequest.lines = _siteNameLines; result = TextState(_parent, _siteName.getStateElidedLeft( - point - QPoint(padding.left(), tshift), + point - QPoint(inner.left(), tshift), paintw, width(), siteNameRequest)); @@ -696,7 +741,7 @@ TextState WebPage::textState(QPoint point, StateRequest request) const { Ui::Text::StateRequestElided titleRequest = request.forText(); titleRequest.lines = _titleLines; result = TextState(_parent, _title.getStateElidedLeft( - point - QPoint(padding.left(), tshift), + point - QPoint(inner.left(), tshift), paintw, width(), titleRequest)); @@ -712,13 +757,13 @@ TextState WebPage::textState(QPoint point, StateRequest request) const { Ui::Text::StateRequestElided descriptionRequest = request.forText(); descriptionRequest.lines = _descriptionLines; result = TextState(_parent, _description.getStateElidedLeft( - point - QPoint(padding.left(), tshift), + point - QPoint(inner.left(), tshift), paintw, width(), descriptionRequest)); } else { result = TextState(_parent, _description.getStateLeft( - point - QPoint(padding.left(), tshift), + point - QPoint(inner.left(), tshift), paintw, width(), request.forText())); @@ -734,14 +779,18 @@ TextState WebPage::textState(QPoint point, StateRequest request) const { auto attachAtTop = !_siteNameLines && !_titleLines && !_descriptionLines; if (!attachAtTop) tshift += st::mediaInBubbleSkip; - if (QRect(padding.left(), tshift, paintw, height() - tshift - bshift).contains(point)) { - auto attachLeft = padding.left() - bubble.left(); + if (QRect(inner.left(), tshift, paintw, inner.top() + inner.height() - tshift).contains(point)) { + auto attachLeft = inner.left() - bubble.left(); auto attachTop = tshift - bubble.top(); if (rtl()) attachLeft = width() - attachLeft - _attach->width(); result = _attach->textState(point - QPoint(attachLeft, attachTop), request); result.link = replaceAttachLink(result.link); } } + if (!result.link && outer.contains(point)) { + result.link = _openl; + } + _lastPoint = point - outer.topLeft(); result.symbol += symbolAdd; return result; @@ -749,29 +798,13 @@ TextState WebPage::textState(QPoint point, StateRequest request) const { ClickHandlerPtr WebPage::replaceAttachLink( const ClickHandlerPtr &link) const { - if (!link || !_attach->isReadyForOpen() || !_collage.empty()) { + if (!_attach->isReadyForOpen() + || (_siteName.isEmpty() + && _title.isEmpty() + && _description.isEmpty())) { return link; } - if (_data->document) { - if (_data->document->isWallPaper() || _data->document->isTheme()) { - return _openl; - } - } else if (_data->photo) { - if (_data->type == WebPageType::Profile - || _data->type == WebPageType::Video) { - return _openl; - } else if (_data->type == WebPageType::Photo - || _data->type == WebPageType::Document - || _data->siteName == u"Twitter"_q - || _data->siteName == u"Facebook"_q) { - // leave photo link - } else { - return _openl; - } - } else if (ThemeDocument::ParamsFromUrl(_data->url).has_value()) { - return _openl; - } - return link; + return _openl; } TextSelection WebPage::adjustSelection(TextSelection selection, TextSelectType type) const { @@ -797,13 +830,39 @@ TextSelection WebPage::adjustSelection(TextSelection selection, TextSelectType t return { siteNameSelection.from, fromDescriptionSelection(descriptionSelection).to }; } -void WebPage::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) { +uint16 WebPage::fullSelectionLength() const { + return _siteName.length() + _title.length() + _description.length(); +} + +void WebPage::clickHandlerActiveChanged( + const ClickHandlerPtr &p, + bool active) { if (_attach) { _attach->clickHandlerActiveChanged(p, active); } } -void WebPage::clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) { +void WebPage::clickHandlerPressedChanged( + const ClickHandlerPtr &p, + bool pressed) { + if (p == _openl) { + if (pressed) { + if (!_ripple) { + const auto full = QRect(0, 0, width(), height()); + const auto outer = full.marginsRemoved(inBubblePadding()); + const auto owner = &parent()->history()->owner(); + _ripple = std::make_unique( + st::defaultRippleAnimation, + Ui::RippleAnimation::RoundRectMask( + outer.size(), + _st.radius), + [=] { owner->requestViewRepaint(parent()); }); + } + _ripple->add(_lastPoint); + } else if (_ripple) { + _ripple->lastStop(); + } + } if (_attach) { _attach->clickHandlerPressedChanged(p, pressed); } @@ -835,6 +894,15 @@ QString WebPage::additionalInfoString() const { return _attach ? _attach->additionalInfoString() : QString(); } +bool WebPage::toggleSelectionByHandlerClick( + const ClickHandlerPtr &p) const { + return _attach && _attach->toggleSelectionByHandlerClick(p); +} + +bool WebPage::dragItemByHandler(const ClickHandlerPtr &p) const { + return _attach && _attach->dragItemByHandler(p); +} + TextForMimeData WebPage::selectedText(TextSelection selection) const { auto siteNameResult = _siteName.toTextForMimeData(selection); auto titleResult = _title.toTextForMimeData( @@ -859,11 +927,17 @@ TextForMimeData WebPage::selectedText(TextSelection selection) const { } QMargins WebPage::inBubblePadding() const { - auto lshift = st::msgPadding.left() + st::webPageLeft; - auto rshift = st::msgPadding.right(); - auto bshift = isBubbleBottom() ? st::msgPadding.left() : st::mediaInBubbleSkip; - auto tshift = isBubbleTop() ? st::msgPadding.left() : st::mediaInBubbleSkip; - return QMargins(lshift, tshift, rshift, bshift); + return { + st::msgPadding.left(), + isBubbleTop() ? st::msgPadding.left() : 0, + st::msgPadding.right(), + isBubbleBottom() ? (st::msgPadding.left() + bottomInfoPadding()) : 0 + }; +} + +QMargins WebPage::innerMargin() const { + const auto button = _openButtonWidth ? st::historyPageButtonHeight : 0; + return _st.padding + QMargins(0, 0, 0, button); } bool WebPage::isLogEntryOriginal() const { @@ -871,7 +945,9 @@ bool WebPage::isLogEntryOriginal() const { } int WebPage::bottomInfoPadding() const { - if (!isBubbleBottom()) return 0; + if (!isBubbleBottom()) { + return 0; + } auto result = st::msgDateFont->height; @@ -884,14 +960,6 @@ int WebPage::bottomInfoPadding() const { return result; } -QString WebPage::displayedSiteName() const { - return (_data->document && _data->document->isWallPaper()) - ? tr::lng_media_chat_background(tr::now) - : (_data->document && _data->document->isTheme()) - ? tr::lng_media_color_theme(tr::now) - : _data->siteName; -} - WebPage::~WebPage() { history()->owner().unregisterWebPageView(_data, _parent); if (_photoMedia) { diff --git a/Telegram/SourceFiles/history/view/media/history_view_web_page.h b/Telegram/SourceFiles/history/view/media/history_view_web_page.h index e9cb2d49c08b70..b4daf37033d307 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_web_page.h +++ b/Telegram/SourceFiles/history/view/media/history_view_web_page.h @@ -14,13 +14,20 @@ class Media; class PhotoMedia; } // namespace Data +namespace Ui { +class RippleAnimation; +} // namespace Ui + namespace HistoryView { class WebPage : public Media { public: WebPage( not_null parent, - not_null data); + not_null data, + MediaWebPageFlags flags); + + [[nodiscard]] static bool HasButton(not_null data); void refreshParentId(not_null realParent) override; @@ -34,25 +41,23 @@ class WebPage : public Media { [[nodiscard]] TextSelection adjustSelection( TextSelection selection, TextSelectType type) const override; - uint16 fullSelectionLength() const override { - return _title.length() + _description.length(); - } + uint16 fullSelectionLength() const override; bool hasTextForCopy() const override { - return false; // we do not add _title and _description in FullSelection text copy. + // We do not add _title and _description in FullSelection text copy. + return false; } QString additionalInfoString() const override; - bool toggleSelectionByHandlerClick(const ClickHandlerPtr &p) const override { - return _attach && _attach->toggleSelectionByHandlerClick(p); - } - bool dragItemByHandler(const ClickHandlerPtr &p) const override { - return _attach && _attach->dragItemByHandler(p); - } + bool toggleSelectionByHandlerClick( + const ClickHandlerPtr &p) const override; + bool dragItemByHandler(const ClickHandlerPtr &p) const override; TextForMimeData selectedText(TextSelection selection) const override; - void clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) override; - void clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) override; + void clickHandlerActiveChanged( + const ClickHandlerPtr &p, bool active) override; + void clickHandlerPressedChanged( + const ClickHandlerPtr &p, bool pressed) override; bool isDisplayed() const override; PhotoData *getPhoto() const override { @@ -99,39 +104,52 @@ class WebPage : public Media { void ensurePhotoMediaCreated() const; - TextSelection toTitleSelection(TextSelection selection) const; - TextSelection fromTitleSelection(TextSelection selection) const; - TextSelection toDescriptionSelection(TextSelection selection) const; - TextSelection fromDescriptionSelection(TextSelection selection) const; - QMargins inBubblePadding() const; - int bottomInfoPadding() const; - bool isLogEntryOriginal() const; - - QString displayedSiteName() const; - ClickHandlerPtr replaceAttachLink(const ClickHandlerPtr &link) const; - bool asArticle() const; - - not_null _data; + [[nodiscard]] TextSelection toTitleSelection( + TextSelection selection) const; + [[nodiscard]] TextSelection fromTitleSelection( + TextSelection selection) const; + [[nodiscard]] TextSelection toDescriptionSelection( + TextSelection selection) const; + [[nodiscard]] TextSelection fromDescriptionSelection( + TextSelection selection) const; + [[nodiscard]] QMargins inBubblePadding() const; + [[nodiscard]] QMargins innerMargin() const; + [[nodiscard]] int bottomInfoPadding() const; + [[nodiscard]] bool isLogEntryOriginal() const; + + [[nodiscard]] ClickHandlerPtr replaceAttachLink( + const ClickHandlerPtr &link) const; + [[nodiscard]] bool asArticle() const; + + const style::QuoteStyle &_st; + const not_null _data; std::vector> _collage; ClickHandlerPtr _openl; std::unique_ptr _attach; mutable std::shared_ptr _photoMedia; + mutable std::unique_ptr _ripple; - bool _asArticle = false; - bool _hasViewButton = false; int _dataVersion = -1; int _siteNameLines = 0; - int _titleLines = 0; int _descriptionLines = 0; + uint32 _titleLines : 31 = 0; + uint32 _asArticle : 1 = 0; - Ui::Text::String _siteName, _title, _description; + Ui::Text::String _siteName; + Ui::Text::String _title; + Ui::Text::String _description; + QString _openButton; QString _duration; + int _openButtonWidth = 0; int _durationWidth = 0; + mutable QPoint _lastPoint; int _pixw = 0; int _pixh = 0; + const MediaWebPageFlags _flags; + }; } // namespace HistoryView diff --git a/Telegram/SourceFiles/info/boosts/info_boosts_inner_widget.cpp b/Telegram/SourceFiles/info/boosts/info_boosts_inner_widget.cpp index 26b12a6907b8ff..7528c093be75f9 100644 --- a/Telegram/SourceFiles/info/boosts/info_boosts_inner_widget.cpp +++ b/Telegram/SourceFiles/info/boosts/info_boosts_inner_widget.cpp @@ -210,16 +210,14 @@ void InnerWidget::fill() { fakeShowed->events(), rpl::single(status.overview.isBoosted), dividerContent.data(), - Ui::BoostBoxData{ - .boost = Ui::BoostCounters{ - .level = status.overview.level, - .boosts = status.overview.boostCount, - .thisLevelBoosts - = status.overview.currentLevelBoostCount, - .nextLevelBoosts - = status.overview.nextLevelBoostCount, - .mine = status.overview.isBoosted, - } + Ui::BoostCounters{ + .level = status.overview.level, + .boosts = status.overview.boostCount, + .thisLevelBoosts + = status.overview.currentLevelBoostCount, + .nextLevelBoosts + = status.overview.nextLevelBoostCount, + .mine = status.overview.isBoosted, }, st::statisticsLimitsLinePadding); inner->add(object_ptr( diff --git a/Telegram/SourceFiles/info/info_layer_widget.cpp b/Telegram/SourceFiles/info/info_layer_widget.cpp index dad51a6c6ce26c..83728db54a814e 100644 --- a/Telegram/SourceFiles/info/info_layer_widget.cpp +++ b/Telegram/SourceFiles/info/info_layer_widget.cpp @@ -149,6 +149,7 @@ void LayerWidget::setContentHeight(int height) { void LayerWidget::showFinished() { floatPlayerShowVisible(); + _content->showFast(); } void LayerWidget::parentResized() { diff --git a/Telegram/SourceFiles/info/info_wrap_widget.cpp b/Telegram/SourceFiles/info/info_wrap_widget.cpp index 1086ede3d19b6e..009dc498bbbdba 100644 --- a/Telegram/SourceFiles/info/info_wrap_widget.cpp +++ b/Telegram/SourceFiles/info/info_wrap_widget.cpp @@ -60,8 +60,8 @@ const style::InfoTopBar &TopBarStyle(Wrap wrap) { [[nodiscard]] bool HasCustomTopBar(not_null controller) { const auto section = controller->section(); - return (section.type() == Section::Type::Settings - && (section.settingsType() == ::Settings::PremiumId())); + return (section.type() == Section::Type::Settings) + && section.settingsType()->hasCustomTopBar(); } } // namespace @@ -788,8 +788,8 @@ void WrapWidget::showNewContent( newController->takeStepData(_controller.get()); } auto newContent = object_ptr(nullptr); - const auto enableBackButton = hasBackButton(); - const auto createInAdvance = needAnimation || enableBackButton; + const auto withBackButton = willHaveBackButton(params); + const auto createInAdvance = needAnimation || withBackButton; if (createInAdvance) { newContent = createContent(memento, newController.get()); } @@ -823,7 +823,7 @@ void WrapWidget::showNewContent( _historyStack.clear(); } - if (enableBackButton) { + if (withBackButton) { newContent->enableBackButton(); } @@ -969,6 +969,17 @@ bool WrapWidget::hasBackButton() const { return (wrap() == Wrap::Narrow || hasStackHistory()); } +bool WrapWidget::willHaveBackButton( + const Window::SectionShow ¶ms) const { + using Way = Window::SectionShow::Way; + const auto willSaveToStack = (_content != nullptr) + && (params.way == Way::Forward); + const auto willClearStack = (params.way == Way::ClearStack); + const auto willHaveStack = !willClearStack + && (hasStackHistory() || willSaveToStack); + return (wrap() == Wrap::Narrow) || willHaveStack; +} + WrapWidget::~WrapWidget() = default; } // namespace Info diff --git a/Telegram/SourceFiles/info/info_wrap_widget.h b/Telegram/SourceFiles/info/info_wrap_widget.h index 8daf46001a5494..e2ba45152ac7dd 100644 --- a/Telegram/SourceFiles/info/info_wrap_widget.h +++ b/Telegram/SourceFiles/info/info_wrap_widget.h @@ -176,6 +176,8 @@ class WrapWidget final : public Window::SectionWidget { void setupShortcuts(); [[nodiscard]] bool hasBackButton() const; + [[nodiscard]] bool willHaveBackButton( + const Window::SectionShow ¶ms) const; not_null topWidget() const; diff --git a/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.cpp b/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.cpp index 1862bf56298854..49dab625eae536 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.cpp @@ -7,6 +7,8 @@ For license and copyright information please follow this link: */ #include "info/profile/info_profile_emoji_status_panel.h" +#include "api/api_peer_photo.h" +#include "apiwrap.h" #include "data/data_user.h" #include "data/data_session.h" #include "data/data_document.h" @@ -66,26 +68,17 @@ void EmojiStatusPanel::show( not_null controller, not_null button, Data::CustomEmojiSizeTag animationSizeTag) { - const auto self = controller->session().user(); - const auto &statuses = controller->session().data().emojiStatuses(); - const auto &recent = statuses.list(Data::EmojiStatuses::Type::Recent); - const auto &other = statuses.list(Data::EmojiStatuses::Type::Default); - auto list = statuses.list(Data::EmojiStatuses::Type::Colored); - list.insert(begin(list), 0); - if (list.size() > kLimitFirstRow) { - list.erase(begin(list) + kLimitFirstRow, end(list)); - } - list.reserve(list.size() + recent.size() + other.size() + 1); - for (const auto &id : ranges::views::concat(recent, other)) { - if (!ranges::contains(list, id)) { - list.push_back(id); - } - } - if (!ranges::contains(list, self->emojiStatusId())) { - list.push_back(self->emojiStatusId()); - } + show({ + .controller = controller, + .button = button, + .animationSizeTag = animationSizeTag, + }); +} + +void EmojiStatusPanel::show(Descriptor &&descriptor) { + const auto controller = descriptor.controller; if (!_panel) { - create(controller); + create(descriptor); _panel->shownValue( ) | rpl::filter([=] { @@ -98,23 +91,67 @@ void EmojiStatusPanel::show( } }, _panel->lifetime()); } + const auto button = descriptor.button; if (const auto previous = _panelButton.data()) { if (previous != button) { previous->removeEventFilter(_panel.get()); } } _panelButton = button; - _animationSizeTag = animationSizeTag; - _panel->selector()->provideRecentEmoji(list); + _animationSizeTag = descriptor.animationSizeTag; + auto list = std::vector(); + if (descriptor.backgroundEmojiMode) { + controller->session().api().peerPhoto().emojiListValue( + Api::PeerPhoto::EmojiListType::Background + ) | rpl::start_with_next([=](std::vector &&list) { + list.insert(begin(list), 0); + if (const auto now = descriptor.currentBackgroundEmojiId) { + if (!ranges::contains(list, now)) { + list.push_back(now); + } + } + _panel->selector()->provideRecentEmoji(list); + }, _panel->lifetime()); + } else { + const auto self = controller->session().user(); + const auto &statuses = controller->session().data().emojiStatuses(); + const auto &recent = statuses.list(Data::EmojiStatuses::Type::Recent); + const auto &other = statuses.list(Data::EmojiStatuses::Type::Default); + auto list = statuses.list(Data::EmojiStatuses::Type::Colored); + list.insert(begin(list), 0); + if (list.size() > kLimitFirstRow) { + list.erase(begin(list) + kLimitFirstRow, end(list)); + } + list.reserve(list.size() + recent.size() + other.size() + 1); + for (const auto &id : ranges::views::concat(recent, other)) { + if (!ranges::contains(list, id)) { + list.push_back(id); + } + } + if (!ranges::contains(list, self->emojiStatusId())) { + list.push_back(self->emojiStatusId()); + } + _panel->selector()->provideRecentEmoji(list); + } const auto parent = _panel->parentWidget(); const auto global = button->mapToGlobal(QPoint()); const auto local = parent->mapFromGlobal(global); - _panel->moveTopRight( - local.y() + button->height() - (st::normalFont->height / 2), - local.x() + button->width() * 3); + if (descriptor.backgroundEmojiMode) { + _panel->moveBottomRight( + local.y() + (st::normalFont->height / 2), + local.x() + button->width() * 3); + } else { + _panel->moveTopRight( + local.y() + button->height() - (st::normalFont->height / 2), + local.x() + button->width() * 3); + } _panel->toggleAnimated(); } +void EmojiStatusPanel::repaint() { + _panel->selector()->update(); +} + bool EmojiStatusPanel::paintBadgeFrame(not_null widget) { if (!_animation) { return false; @@ -125,19 +162,31 @@ bool EmojiStatusPanel::paintBadgeFrame(not_null widget) { return false; } -void EmojiStatusPanel::create( - not_null controller) { +void EmojiStatusPanel::create(const Descriptor &descriptor) { using Selector = ChatHelpers::TabbedSelector; + using Descriptor = ChatHelpers::TabbedSelectorDescriptor; + using Mode = ChatHelpers::TabbedSelector::Mode; + const auto controller = descriptor.controller; const auto body = controller->window().widget()->bodyWidget(); _panel = base::make_unique_q( body, controller, object_ptr( nullptr, - controller->uiShow(), - Window::GifPauseReason::Layer, - ChatHelpers::TabbedSelector::Mode::EmojiStatus)); - _panel->setDropDown(true); + Descriptor{ + .show = controller->uiShow(), + .st = (descriptor.backgroundEmojiMode + ? st::backgroundEmojiPan + : st::statusEmojiPan), + .level = Window::GifPauseReason::Layer, + .mode = (descriptor.backgroundEmojiMode + ? Mode::BackgroundEmoji + : Mode::EmojiStatus), + .customTextColor = descriptor.customTextColor, + })); + _customTextColor = descriptor.customTextColor; + _backgroundEmojiMode = descriptor.backgroundEmojiMode; + _panel->setDropDown(!_backgroundEmojiMode); _panel->setDesiredHeightValues( 1., st::emojiPanMinHeight / 2, @@ -169,34 +218,46 @@ void EmojiStatusPanel::create( return Chosen{ .animation = data.messageSendingFrom }; }); - const auto weak = Ui::MakeWeak(_panel.get()); - const auto accept = [=](Chosen chosen) { - Expects(chosen.until != Selector::kPickCustomTimeId); - - // From PickUntilBox is called after EmojiStatusPanel is destroyed! - const auto owner = &controller->session().data(); - if (weak) { + if (descriptor.backgroundEmojiMode) { + rpl::merge( + std::move(statusChosen), + std::move(emojiChosen) + ) | rpl::start_with_next([=](const Chosen &chosen) { + const auto owner = &controller->session().data(); startAnimation(owner, body, chosen.id, chosen.animation); - } - owner->emojiStatuses().set(chosen.id, chosen.until); - }; - - rpl::merge( - std::move(statusChosen), - std::move(emojiChosen) - ) | rpl::filter([=](const Chosen &chosen) { - return filter(controller, chosen.id); - }) | rpl::start_with_next([=](const Chosen &chosen) { - if (chosen.until == Selector::kPickCustomTimeId) { + _backgroundEmojiChosen.fire_copy(chosen.id); _panel->hideAnimated(); - controller->show(Box(PickUntilBox, [=](TimeId seconds) { - accept({ chosen.id, base::unixtime::now() + seconds }); - })); - } else { - accept(chosen); - _panel->hideAnimated(); - } - }, _panel->lifetime()); + }, _panel->lifetime()); + } else { + const auto weak = Ui::MakeWeak(_panel.get()); + const auto accept = [=](Chosen chosen) { + Expects(chosen.until != Selector::kPickCustomTimeId); + + // PickUntilBox calls this after EmojiStatusPanel is destroyed! + const auto owner = &controller->session().data(); + if (weak) { + startAnimation(owner, body, chosen.id, chosen.animation); + } + owner->emojiStatuses().set(chosen.id, chosen.until); + }; + + rpl::merge( + std::move(statusChosen), + std::move(emojiChosen) + ) | rpl::filter([=](const Chosen &chosen) { + return filter(controller, chosen.id); + }) | rpl::start_with_next([=](const Chosen &chosen) { + if (chosen.until == Selector::kPickCustomTimeId) { + _panel->hideAnimated(); + controller->show(Box(PickUntilBox, [=](TimeId seconds) { + accept({ chosen.id, base::unixtime::now() + seconds }); + })); + } else { + accept(chosen); + _panel->hideAnimated(); + } + }, _panel->lifetime()); + } } bool EmojiStatusPanel::filter( @@ -223,13 +284,17 @@ void EmojiStatusPanel::startAnimation( .id = { { statusId } }, .flyIcon = from.frame, .flyFrom = body->mapFromGlobal(from.globalStartGeometry), + .forceFirstFrame = _backgroundEmojiMode, }; + const auto color = _customTextColor + ? _customTextColor + : [] { return st::profileVerifiedCheckBg->c; }; _animation = std::make_unique( body, &owner->reactions(), std::move(args), [=] { _animation->repaint(); }, - [] { return st::profileVerifiedCheckBg->c; }, + _customTextColor, _animationSizeTag); } diff --git a/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.h b/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.h index d055983bd48e2c..ce3a47c70df9a7 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.h +++ b/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.h @@ -47,10 +47,25 @@ class EmojiStatusPanel final { not_null button, Data::CustomEmojiSizeTag animationSizeTag = {}); + struct Descriptor { + not_null controller; + not_null button; + Data::CustomEmojiSizeTag animationSizeTag = {}; + DocumentId currentBackgroundEmojiId = 0; + Fn customTextColor; + bool backgroundEmojiMode = false; + }; + void show(Descriptor &&descriptor); + void repaint(); + + [[nodiscard]] rpl::producer backgroundEmojiChosen() const { + return _backgroundEmojiChosen.events(); + } + bool paintBadgeFrame(not_null widget); private: - void create(not_null controller); + void create(const Descriptor &descriptor); [[nodiscard]] bool filter( not_null controller, DocumentId chosenId) const; @@ -62,10 +77,13 @@ class EmojiStatusPanel final { Ui::MessageSendingAnimationFrom from); base::unique_qptr _panel; + Fn _customTextColor; Fn _chooseFilter; QPointer _panelButton; std::unique_ptr _animation; + rpl::event_stream _backgroundEmojiChosen; Data::CustomEmojiSizeTag _animationSizeTag = {}; + bool _backgroundEmojiMode = false; }; diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp index 2d93e364451eb5..6fc941305be739 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp @@ -117,7 +117,6 @@ constexpr auto kRefreshBotsTimeout = 60 * 60 * crl::time(1000); .inMainMenu = data.is_show_in_side_menu(), .inAttachMenu = data.is_show_in_attach_menu(), .disclaimerRequired = data.is_side_menu_disclaimer_needed(), - .hasSettings = data.is_has_settings(), .requestWriteAccess = data.is_request_write_access(), } : std::optional(); }); @@ -556,14 +555,16 @@ Webview::ThemeParams AttachWebView::botThemeParams() { return Window::Theme::WebViewParams(); } -bool AttachWebView::botHandleLocalUri(QString uri) { +bool AttachWebView::botHandleLocalUri(QString uri, bool keepOpen) { const auto local = Core::TryConvertUrlToLocal(uri); if (uri == local || Core::InternalPassportLink(local)) { return local.startsWith(u"tg://"_q); } else if (!local.startsWith(u"tg://"_q, Qt::CaseInsensitive)) { return false; } - botClose(); + if (!keepOpen) { + botClose(); + } crl::on_main([=, shownUrl = _lastShownUrl] { const auto variant = QVariant::fromValue(ClickHandlerContext{ .attachBotWebviewUrl = shownUrl, @@ -1261,7 +1262,6 @@ void AttachWebView::requestApp( _bot->id, data.vapp()); _app = received ? received : already; - _app->hasSettings = data.is_has_settings(); if (!_app) { cancel(); showToast(tr::lng_username_app_not_found(tr::now)); @@ -1433,19 +1433,13 @@ void AttachWebView::show( _attachBots, not_null{ _bot }, &AttachWebViewBot::user); - const auto hasSettings = app - ? app->hasSettings - : ((attached != end(_attachBots)) - && !attached->inactive - && attached->hasSettings); const auto hasOpenBot = !_context || (_bot != _context->action.history->peer) || fromMainMenu; const auto hasRemoveFromMenu = !app && (attached != end(_attachBots)) && (!attached->inactive || attached->inMainMenu); - const auto buttons = (hasSettings ? Button::Settings : Button::None) - | (hasOpenBot ? Button::OpenBot : Button::None) + const auto buttons = (hasOpenBot ? Button::OpenBot : Button::None) | (!hasRemoveFromMenu ? Button::None : attached->inMainMenu diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h index aa7f6dfff2b77e..df5dc85f18fa1a 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h @@ -66,7 +66,6 @@ struct AttachWebViewBot { bool inMainMenu : 1 = false; bool inAttachMenu : 1 = false; bool disclaimerRequired : 1 = false; - bool hasSettings : 1 = false; bool requestWriteAccess : 1 = false; }; @@ -160,7 +159,7 @@ class AttachWebView final Webview::ThemeParams botThemeParams() override; - bool botHandleLocalUri(QString uri) override; + bool botHandleLocalUri(QString uri, bool keepOpen) override; void botHandleInvoice(QString slug) override; void botHandleMenuButton(Ui::BotWebView::MenuButton button) override; void botSendData(QByteArray data) override; diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.cpp b/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.cpp index fa5b95ec6e0f31..af19b536bc3af1 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.cpp +++ b/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.cpp @@ -161,7 +161,8 @@ QImage *ItemBase::getResultThumb(Data::FileOrigin origin) const { QPixmap ItemBase::getResultContactAvatar(int width, int height) const { if (_result->_type == Result::Type::Contact) { auto result = Ui::EmptyUserpic( - Ui::EmptyUserpic::UserpicColor(BareId(qHash(_result->_id))), + Ui::EmptyUserpic::UserpicColor(Ui::EmptyUserpic::ColorIndex( + BareId(qHash(_result->_id)))), _result->getLayoutTitle() ).generate(width); if (result.height() != height * cIntRetinaFactor()) { diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_result.cpp b/Telegram/SourceFiles/inline_bots/inline_bot_result.cpp index c10954827265cd..fadff142ea5725 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_result.cpp +++ b/Telegram/SourceFiles/inline_bots/inline_bot_result.cpp @@ -262,6 +262,12 @@ std::unique_ptr Result::Create( result->sendData = std::make_unique( session, media); + }, [&](const MTPDbotInlineMessageMediaWebPage &data) { + result->sendData = std::make_unique( + session, + qs(data.vmessage()), + Api::EntitiesFromMTP(session, data.ventities().value_or_empty()), + false); }); if (!result->sendData || !result->sendData->isValid()) { diff --git a/Telegram/SourceFiles/main/main_account.cpp b/Telegram/SourceFiles/main/main_account.cpp index 193b674c622c49..1720a3b8a36389 100644 --- a/Telegram/SourceFiles/main/main_account.cpp +++ b/Telegram/SourceFiles/main/main_account.cpp @@ -172,7 +172,9 @@ void Account::createSession( MTPstring(), // lang_code MTPEmojiStatus(), MTPVector(), - MTPint()), // stories_max_id + MTPint(), // stories_max_id + MTP_int(0), // color + MTPlong()), // background_emoji_id serialized, streamVersion, std::move(settings)); diff --git a/Telegram/SourceFiles/main/main_app_config.cpp b/Telegram/SourceFiles/main/main_app_config.cpp index 03e1d547f373be..fa4f5905132179 100644 --- a/Telegram/SourceFiles/main/main_app_config.cpp +++ b/Telegram/SourceFiles/main/main_app_config.cpp @@ -7,9 +7,10 @@ For license and copyright information please follow this link: */ #include "main/main_app_config.h" -#include "main/main_account.h" -#include "base/call_delayed.h" #include "apiwrap.h" +#include "base/call_delayed.h" +#include "main/main_account.h" +#include "ui/chat/chat_style.h" namespace Main { namespace { @@ -27,6 +28,8 @@ AppConfig::AppConfig(not_null account) : _account(account) { }, _lifetime); } +AppConfig::~AppConfig() = default; + void AppConfig::start() { _account->mtpMainSessionValue( ) | rpl::start_with_next([=](not_null instance) { @@ -58,6 +61,7 @@ void AppConfig::refresh() { _data.emplace_or_assign(qs(data.vkey()), data.vvalue()); }); } + parseColorIndices(); DEBUG_LOG(("getAppConfig result handled.")); _refreshed.fire({}); }, [](const MTPDhelp_appConfigNotModified &) {}); @@ -171,6 +175,27 @@ std::vector> AppConfig::getStringMapArray( }); } +std::vector AppConfig::getIntArray( + const QString &key, + std::vector &&fallback) const { + return getValue(key, [&](const MTPJSONValue &value) { + return value.match([&](const MTPDjsonArray &data) { + auto result = std::vector(); + result.reserve(data.vvalue().v.size()); + for (const auto &entry : data.vvalue().v) { + if (entry.type() != mtpc_jsonNumber) { + return std::move(fallback); + } + result.push_back( + int(base::SafeRound(entry.c_jsonNumber().vvalue().v))); + } + return result; + }, [&](const auto &data) { + return std::move(fallback); + }); + }); +} + bool AppConfig::suggestionCurrent(const QString &key) const { return !_dismissedSuggestions.contains(key) && ranges::contains( @@ -199,4 +224,121 @@ void AppConfig::dismissSuggestion(const QString &key) { )).send(); } +void AppConfig::parseColorIndices() { + constexpr auto parseColor = [](const MTPJSONValue &color) { + if (color.type() != mtpc_jsonString) { + LOG(("API Error: Bad type for color element.")); + return uint32(); + } + const auto value = color.c_jsonString().vvalue().v; + if (value.size() != 6) { + LOG(("API Error: Bad length for color element: %1" + ).arg(qs(value))); + return uint32(); + } + const auto hex = [](char ch) { + return (ch >= 'a' && ch <= 'f') + ? (ch - 'a' + 10) + : (ch >= 'A' && ch <= 'F') + ? (ch - 'A' + 10) + : (ch >= '0' && ch <= '9') + ? (ch - '0') + : 0; + }; + auto result = (uint32(1) << 24); + for (auto i = 0; i != 6; ++i) { + result |= (uint32(hex(value[i])) << ((5 - i) * 4)); + } + return result; + }; + + struct ParsedColor { + uint8 colorIndex = Ui::kColorIndexCount; + std::array colors; + + explicit operator bool() const { + return colorIndex < Ui::kColorIndexCount; + } + }; + const auto parseColors = [&](const MTPJSONObjectValue &element) { + const auto &data = element.data(); + if (data.vvalue().type() != mtpc_jsonArray) { + LOG(("API Error: Bad value for peer_colors element.")); + return ParsedColor(); + } + const auto &list = data.vvalue().c_jsonArray().vvalue().v; + if (list.empty() || list.size() > Ui::kColorPatternsCount) { + LOG(("API Error: Bad count for peer_colors element: %1" + ).arg(list.size())); + return ParsedColor(); + } + const auto index = data.vkey().v.toInt(); + if (index < Ui::kSimpleColorIndexCount + || index >= Ui::kColorIndexCount) { + LOG(("API Error: Bad index for peer_colors element: %1" + ).arg(qs(data.vkey().v))); + return ParsedColor(); + } + auto result = ParsedColor{ .colorIndex = uint8(index) }; + auto fill = result.colors.data(); + for (const auto &color : list) { + *fill++ = parseColor(color); + } + return result; + }; + const auto checkColorsObjectType = [&](const MTPJSONValue &value) { + if (value.type() != mtpc_jsonObject) { + if (value.type() != mtpc_jsonArray + || !value.c_jsonArray().vvalue().v.empty()) { + LOG(("API Error: Bad value for [dark_]peer_colors.")); + } + return false; + } + return true; + }; + + auto colors = std::make_shared< + std::array>(); + getValue(u"peer_colors"_q, [&](const MTPJSONValue &value) { + if (!checkColorsObjectType(value)) { + return; + } + for (const auto &element : value.c_jsonObject().vvalue().v) { + if (const auto parsed = parseColors(element)) { + auto &fields = (*colors)[parsed.colorIndex]; + fields.dark = fields.light = parsed.colors; + } + } + }); + getValue(u"dark_peer_colors"_q, [&](const MTPJSONValue &value) { + if (!checkColorsObjectType(value)) { + return; + } + for (const auto &element : value.c_jsonObject().vvalue().v) { + if (const auto parsed = parseColors(element)) { + (*colors)[parsed.colorIndex].dark = parsed.colors; + } + } + }); + + if (!_colorIndicesCurrent) { + _colorIndicesCurrent = std::make_unique( + Ui::ColorIndicesCompressed{ std::move(colors) }); + _colorIndicesChanged.fire({}); + } else if (*_colorIndicesCurrent->colors != *colors) { + _colorIndicesCurrent->colors = std::move(colors); + _colorIndicesChanged.fire({}); + } +} + +auto AppConfig::colorIndicesValue() const +-> rpl::producer { + return rpl::single(_colorIndicesCurrent + ? *_colorIndicesCurrent + : Ui::ColorIndicesCompressed() + ) | rpl::then(_colorIndicesChanged.events() | rpl::map([=] { + return *_colorIndicesCurrent; + })); +} + } // namespace Main diff --git a/Telegram/SourceFiles/main/main_app_config.h b/Telegram/SourceFiles/main/main_app_config.h index f20ace5a2f436a..5c60ebd67de3e7 100644 --- a/Telegram/SourceFiles/main/main_app_config.h +++ b/Telegram/SourceFiles/main/main_app_config.h @@ -10,6 +10,10 @@ For license and copyright information please follow this link: #include "mtproto/sender.h" #include "base/algorithm.h" +namespace Ui { +struct ColorIndicesCompressed; +} // namespace Ui + namespace Main { class Account; @@ -17,6 +21,7 @@ class Account; class AppConfig final { public: explicit AppConfig(not_null account); + ~AppConfig(); void start(); @@ -30,6 +35,8 @@ class AppConfig final { return getString(key, fallback); } else if constexpr (std::is_same_v>) { return getStringArray(key, std::move(fallback)); + } else if constexpr (std::is_same_v>) { + return getIntArray(key, std::move(fallback)); } else if constexpr (std::is_same_v< Type, std::vector>>) { @@ -47,10 +54,14 @@ class AppConfig final { const QString &key) const; void dismissSuggestion(const QString &key); + [[nodiscard]] auto colorIndicesValue() const + -> rpl::producer; + void refresh(); private: void refreshDelayed(); + void parseColorIndices(); template [[nodiscard]] auto getValue( @@ -72,6 +83,9 @@ class AppConfig final { [[nodiscard]] std::vector> getStringMapArray( const QString &key, std::vector> &&fallback) const; + [[nodiscard]] std::vector getIntArray( + const QString &key, + std::vector &&fallback) const; const not_null _account; std::optional _api; @@ -80,6 +94,10 @@ class AppConfig final { base::flat_map _data; rpl::event_stream<> _refreshed; base::flat_set _dismissedSuggestions; + + rpl::event_stream<> _colorIndicesChanged; + std::unique_ptr _colorIndicesCurrent; + rpl::lifetime _lifetime; }; diff --git a/Telegram/SourceFiles/main/main_session.cpp b/Telegram/SourceFiles/main/main_session.cpp index 58835d2fa1f467..f458c4724484c2 100644 --- a/Telegram/SourceFiles/main/main_session.cpp +++ b/Telegram/SourceFiles/main/main_session.cpp @@ -476,4 +476,9 @@ Window::SessionController *Session::tryResolveWindow() const { return _windows.front(); } +auto Session::colorIndicesValue() const +-> rpl::producer { + return _account->appConfig().colorIndicesValue(); +} + } // namespace Main diff --git a/Telegram/SourceFiles/main/main_session.h b/Telegram/SourceFiles/main/main_session.h index c681ba007bcf63..cb3f992d6c2409 100644 --- a/Telegram/SourceFiles/main/main_session.h +++ b/Telegram/SourceFiles/main/main_session.h @@ -57,6 +57,10 @@ namespace InlineBots { class AttachWebView; } // namespace InlineBots +namespace Ui { +struct ColorIndicesCompressed; +} // namespace Ui + namespace Main { class Account; @@ -187,6 +191,9 @@ class Session final : public base::has_weak_ptr { [[nodiscard]] Support::Helper &supportHelper() const; [[nodiscard]] Support::Templates &supportTemplates() const; + [[nodiscard]] auto colorIndicesValue() const + -> rpl::producer; + private: static constexpr auto kDefaultSaveDelay = crl::time(1000); @@ -227,6 +234,8 @@ class Session final : public base::has_weak_ptr { QByteArray _tmpPassword; TimeId _tmpPasswordValidUntil = 0; + rpl::event_stream _colorIndicesChanges; + rpl::lifetime _lifetime; }; diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index 9529b2dfcc3f90..0182e9e3fc56b4 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -602,10 +602,9 @@ bool MainWidget::shareUrl( const auto topicRootId = thread->topicRootId(); history->setLocalDraft(std::make_unique( textWithTags, - 0, // replyTo - topicRootId, + FullReplyTo{ .topicRootId = topicRootId }, cursor, - Data::PreviewState::Allowed)); + Data::WebPageDraft())); history->clearLocalEditDraft(topicRootId); history->session().changes().entryUpdated( thread, @@ -746,8 +745,8 @@ void MainWidget::sendBotCommand(Bot::SendCommandRequest request) { } } -void MainWidget::hideSingleUseKeyboard(PeerData *peer, MsgId replyTo) { - _history->hideSingleUseKeyboard(peer, replyTo); +void MainWidget::hideSingleUseKeyboard(FullMsgId replyToId) { + _history->hideSingleUseKeyboard(replyToId); } void MainWidget::searchMessages(const QString &query, Dialogs::Key inChat) { diff --git a/Telegram/SourceFiles/mainwidget.h b/Telegram/SourceFiles/mainwidget.h index 823241429d30f4..e3c75b5734475e 100644 --- a/Telegram/SourceFiles/mainwidget.h +++ b/Telegram/SourceFiles/mainwidget.h @@ -188,7 +188,7 @@ class MainWidget not_null data); void sendBotCommand(Bot::SendCommandRequest request); - void hideSingleUseKeyboard(PeerData *peer, MsgId replyTo); + void hideSingleUseKeyboard(FullMsgId replyToId); void searchMessages(const QString &query, Dialogs::Key inChat); diff --git a/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp b/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp index 2d9e017d577186..fd4a0ea8ac6154 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp @@ -157,7 +157,7 @@ ReactionView::ReactionView( const Data::SuggestedReaction &reaction) : RpWidget(parent) , _data(reaction) -, _chatStyle(std::make_unique()) +, _chatStyle(std::make_unique(session->colorIndicesValue())) , _pathGradient( std::make_unique( st::shadowFg, diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp index 4da1201f087dac..bb3a834a608b88 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp @@ -193,11 +193,11 @@ void ReplyArea::sendReaction(const Data::ReactionId &id) { } void ReplyArea::send(Api::SendOptions options) { - const auto webPageId = _controls->webPageId(); + const auto webPageDraft = _controls->webPageDraft(); auto message = Api::MessageToSend(prepareSendAction(options)); message.textWithTags = _controls->getTextWithAppliedMarkdown(); - message.webPageId = webPageId; + message.webPage = webPageDraft; send(std::move(message), options); } diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index 2d87e88f448c70..dfafca401e2a44 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -3037,7 +3037,7 @@ void OverlayWidget::refreshMediaViewer() { void OverlayWidget::refreshFromLabel() { if (_message) { - _from = _message->senderOriginal(); + _from = _message->originalSender(); if (const auto info = _message->hiddenSenderInfo()) { _fromName = info->name; } else { diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index 8a386d988536b5..41afd10252026a 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -43,6 +43,7 @@ inputMediaGeoLive#971fa843 flags:# stopped:flags.0?true geo_point:InputGeoPoint inputMediaPoll#f94e5f1 flags:# poll:Poll correct_answers:flags.0?Vector solution:flags.1?string solution_entities:flags.1?Vector = InputMedia; inputMediaDice#e66fbf7b emoticon:string = InputMedia; inputMediaStory#89fdd778 peer:InputPeer id:int = InputMedia; +inputMediaWebPage#c21b8849 flags:# force_large_media:flags.0?true force_small_media:flags.1?true optional:flags.2?true url:string = InputMedia; inputChatPhotoEmpty#1ca48f57 = InputChatPhoto; inputChatUploadedPhoto#bdcdaec0 flags:# file:flags.0?InputFile video:flags.1?InputFile video_start_ts:flags.2?double video_emoji_markup:flags.3?VideoSize = InputChatPhoto; @@ -81,7 +82,7 @@ storage.fileMp4#b3cea0e4 = storage.FileType; storage.fileWebp#1081464c = storage.FileType; userEmpty#d3bc4b7a id:long = User; -user#abb5f120 flags:# self:flags.10?true contact:flags.11?true mutual_contact:flags.12?true deleted:flags.13?true bot:flags.14?true bot_chat_history:flags.15?true bot_nochats:flags.16?true verified:flags.17?true restricted:flags.18?true min:flags.20?true bot_inline_geo:flags.21?true support:flags.23?true scam:flags.24?true apply_min_photo:flags.25?true fake:flags.26?true bot_attach_menu:flags.27?true premium:flags.28?true attach_menu_enabled:flags.29?true flags2:# bot_can_edit:flags2.1?true close_friend:flags2.2?true stories_hidden:flags2.3?true stories_unavailable:flags2.4?true id:long access_hash:flags.0?long first_name:flags.1?string last_name:flags.2?string username:flags.3?string phone:flags.4?string photo:flags.5?UserProfilePhoto status:flags.6?UserStatus bot_info_version:flags.14?int restriction_reason:flags.18?Vector bot_inline_placeholder:flags.19?string lang_code:flags.22?string emoji_status:flags.30?EmojiStatus usernames:flags2.0?Vector stories_max_id:flags2.5?int = User; +user#eb602f25 flags:# self:flags.10?true contact:flags.11?true mutual_contact:flags.12?true deleted:flags.13?true bot:flags.14?true bot_chat_history:flags.15?true bot_nochats:flags.16?true verified:flags.17?true restricted:flags.18?true min:flags.20?true bot_inline_geo:flags.21?true support:flags.23?true scam:flags.24?true apply_min_photo:flags.25?true fake:flags.26?true bot_attach_menu:flags.27?true premium:flags.28?true attach_menu_enabled:flags.29?true flags2:# bot_can_edit:flags2.1?true close_friend:flags2.2?true stories_hidden:flags2.3?true stories_unavailable:flags2.4?true id:long access_hash:flags.0?long first_name:flags.1?string last_name:flags.2?string username:flags.3?string phone:flags.4?string photo:flags.5?UserProfilePhoto status:flags.6?UserStatus bot_info_version:flags.14?int restriction_reason:flags.18?Vector bot_inline_placeholder:flags.19?string lang_code:flags.22?string emoji_status:flags.30?EmojiStatus usernames:flags2.0?Vector stories_max_id:flags2.5?int color:flags2.7?int background_emoji_id:flags2.6?long = User; userProfilePhotoEmpty#4f11bae1 = UserProfilePhoto; userProfilePhoto#82d1f706 flags:# has_video:flags.0?true personal:flags.2?true photo_id:long stripped_thumb:flags.1?bytes dc_id:int = UserProfilePhoto; @@ -96,7 +97,7 @@ userStatusLastMonth#77ebc742 = UserStatus; chatEmpty#29562865 id:long = Chat; chat#41cbf256 flags:# creator:flags.0?true left:flags.2?true deactivated:flags.5?true call_active:flags.23?true call_not_empty:flags.24?true noforwards:flags.25?true id:long title:string photo:ChatPhoto participants_count:int date:int version:int migrated_to:flags.6?InputChannel admin_rights:flags.14?ChatAdminRights default_banned_rights:flags.18?ChatBannedRights = Chat; chatForbidden#6592a1a7 id:long title:string = Chat; -channel#94f592db flags:# creator:flags.0?true left:flags.2?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true signatures:flags.11?true min:flags.12?true scam:flags.19?true has_link:flags.20?true has_geo:flags.21?true slowmode_enabled:flags.22?true call_active:flags.23?true call_not_empty:flags.24?true fake:flags.25?true gigagroup:flags.26?true noforwards:flags.27?true join_to_send:flags.28?true join_request:flags.29?true forum:flags.30?true flags2:# stories_hidden:flags2.1?true stories_hidden_min:flags2.2?true stories_unavailable:flags2.3?true id:long access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int restriction_reason:flags.9?Vector admin_rights:flags.14?ChatAdminRights banned_rights:flags.15?ChatBannedRights default_banned_rights:flags.18?ChatBannedRights participants_count:flags.17?int usernames:flags2.0?Vector stories_max_id:flags2.4?int = Chat; +channel#1981ea7e flags:# creator:flags.0?true left:flags.2?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true signatures:flags.11?true min:flags.12?true scam:flags.19?true has_link:flags.20?true has_geo:flags.21?true slowmode_enabled:flags.22?true call_active:flags.23?true call_not_empty:flags.24?true fake:flags.25?true gigagroup:flags.26?true noforwards:flags.27?true join_to_send:flags.28?true join_request:flags.29?true forum:flags.30?true flags2:# stories_hidden:flags2.1?true stories_hidden_min:flags2.2?true stories_unavailable:flags2.3?true id:long access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int restriction_reason:flags.9?Vector admin_rights:flags.14?ChatAdminRights banned_rights:flags.15?ChatBannedRights default_banned_rights:flags.18?ChatBannedRights participants_count:flags.17?int usernames:flags2.0?Vector stories_max_id:flags2.4?int color:flags2.6?int background_emoji_id:flags2.5?long = Chat; channelForbidden#17d493d5 flags:# broadcast:flags.5?true megagroup:flags.8?true id:long access_hash:long title:string until_date:flags.16?int = Chat; chatFull#c9d31138 flags:# can_set_username:flags.7?true has_scheduled:flags.8?true translations_disabled:flags.19?true id:long about:string participants:ChatParticipants chat_photo:flags.2?Photo notify_settings:PeerNotifySettings exported_invite:flags.13?ExportedChatInvite bot_info:flags.3?Vector pinned_msg_id:flags.6?int folder_id:flags.11?int call:flags.12?InputGroupCall ttl_period:flags.14?int groupcall_default_join_as:flags.15?Peer theme_emoticon:flags.16?string requests_pending:flags.17?int recent_requesters:flags.17?Vector available_reactions:flags.18?ChatReactions = ChatFull; @@ -113,7 +114,7 @@ chatPhotoEmpty#37c1011c = ChatPhoto; chatPhoto#1c6e1c11 flags:# has_video:flags.0?true photo_id:long stripped_thumb:flags.1?bytes dc_id:int = ChatPhoto; messageEmpty#90a6ca84 flags:# id:int peer_id:flags.0?Peer = Message; -message#38116ee0 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true noforwards:flags.26?true id:int from_id:flags.8?Peer peer_id:Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long reactions:flags.20?MessageReactions restriction_reason:flags.22?Vector ttl_period:flags.25?int = Message; +message#38116ee0 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true noforwards:flags.26?true invert_media:flags.27?true id:int from_id:flags.8?Peer peer_id:Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long reactions:flags.20?MessageReactions restriction_reason:flags.22?Vector ttl_period:flags.25?int = Message; messageService#2b085862 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true legacy:flags.19?true id:int from_id:flags.8?Peer peer_id:Peer reply_to:flags.3?MessageReplyHeader date:int action:MessageAction ttl_period:flags.25?int = Message; messageMediaEmpty#3ded6320 = MessageMedia; @@ -122,7 +123,7 @@ messageMediaGeo#56e0d474 geo:GeoPoint = MessageMedia; messageMediaContact#70322949 phone_number:string first_name:string last_name:string vcard:string user_id:long = MessageMedia; messageMediaUnsupported#9f84f49e = MessageMedia; messageMediaDocument#4cf4d72d flags:# nopremium:flags.3?true spoiler:flags.4?true document:flags.0?Document alt_document:flags.5?Document ttl_seconds:flags.2?int = MessageMedia; -messageMediaWebPage#a32dd600 webpage:WebPage = MessageMedia; +messageMediaWebPage#ddf10c3b flags:# force_large_media:flags.0?true force_small_media:flags.1?true manual:flags.3?true safe:flags.4?true webpage:WebPage = MessageMedia; messageMediaVenue#2ec0533f geo:GeoPoint title:string address:string provider:string venue_id:string venue_type:string = MessageMedia; messageMediaGame#fdb19008 game:Game = MessageMedia; messageMediaInvoice#f6a548d3 flags:# shipping_address_requested:flags.1?true test:flags.3?true title:string description:string photo:flags.0?WebDocument receipt_msg_id:flags.2?int currency:string total_amount:long start_param:string extended_media:flags.4?MessageExtendedMedia = MessageMedia; @@ -130,6 +131,7 @@ messageMediaGeoLive#b940c666 flags:# geo:GeoPoint heading:flags.0?int period:int messageMediaPoll#4bd6e798 poll:Poll results:PollResults = MessageMedia; messageMediaDice#3f7ee58b value:int emoticon:string = MessageMedia; messageMediaStory#68cb6283 flags:# via_mention:flags.1?true peer:Peer id:int story:flags.0?StoryItem = MessageMedia; +messageMediaGiveaway#58260664 flags:# only_new_subscribers:flags.0?true channels:Vector countries_iso2:flags.1?Vector quantity:int months:int until_date:int = MessageMedia; messageActionEmpty#b6aef7b0 = MessageAction; messageActionChatCreate#bd47cbad title:string users:Vector = MessageAction; @@ -170,6 +172,8 @@ messageActionSuggestProfilePhoto#57de635e photo:Photo = MessageAction; messageActionRequestedPeer#fe77345d button_id:int peer:Peer = MessageAction; messageActionSetChatWallPaper#bc44a927 wallpaper:WallPaper = MessageAction; messageActionSetSameChatWallPaper#c0787d6d wallpaper:WallPaper = MessageAction; +messageActionGiftCode#d2cfdb0e flags:# via_giveaway:flags.0?true unclaimed:flags.2?true boost_peer:flags.1?Peer months:int slug:string = MessageAction; +messageActionGiveawayLaunch#332ba9ed = MessageAction; dialog#d58a08c6 flags:# pinned:flags.2?true unread_mark:flags.3?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_mentions_count:int unread_reactions_count:int notify_settings:PeerNotifySettings pts:flags.0?int draft:flags.1?DraftMessage folder_id:flags.4?int ttl_period:flags.5?int = Dialog; dialogFolder#71bd134c flags:# pinned:flags.2?true folder:Folder peer:Peer top_message:int unread_muted_peers_count:int unread_unmuted_peers_count:int unread_muted_messages_count:int unread_unmuted_messages_count:int = Dialog; @@ -288,7 +292,7 @@ updateChatParticipantAdd#3dda5451 chat_id:long user_id:long inviter_id:long date updateChatParticipantDelete#e32f3d77 chat_id:long user_id:long version:int = Update; updateDcOptions#8e5e9873 dc_options:Vector = Update; updateNotifySettings#bec268ef peer:NotifyPeer notify_settings:PeerNotifySettings = Update; -updateServiceNotification#ebe46819 flags:# popup:flags.0?true inbox_date:flags.1?int type:string message:string media:MessageMedia entities:Vector = Update; +updateServiceNotification#ebe46819 flags:# popup:flags.0?true invert_media:flags.2?true inbox_date:flags.1?int type:string message:string media:MessageMedia entities:Vector = Update; updatePrivacy#ee3b272a key:PrivacyKey rules:Vector = Update; updateUserPhone#5492a13 user_id:long phone:string = Update; updateReadHistoryInbox#9c974fdf flags:# folder_id:flags.0?int peer:Peer max_id:int still_unread_count:int pts:int pts_count:int = Update; @@ -548,9 +552,9 @@ messages.allStickers#cdbbcebb hash:long sets:Vector = messages.AllSt messages.affectedMessages#84d19185 pts:int pts_count:int = messages.AffectedMessages; -webPageEmpty#eb1477e8 id:long = WebPage; -webPagePending#c586da1c id:long date:int = WebPage; -webPage#e89c45b2 flags:# id:long url:string display_url:string hash:int type:flags.0?string site_name:flags.1?string title:flags.2?string description:flags.3?string photo:flags.4?Photo embed_url:flags.5?string embed_type:flags.5?string embed_width:flags.6?int embed_height:flags.6?int duration:flags.7?int author:flags.8?string document:flags.9?Document cached_page:flags.10?Page attributes:flags.12?Vector = WebPage; +webPageEmpty#211a1788 flags:# id:long url:flags.0?string = WebPage; +webPagePending#b0d13e47 flags:# id:long url:flags.0?string date:int = WebPage; +webPage#e89c45b2 flags:# has_large_media:flags.13?true id:long url:string display_url:string hash:int type:flags.0?string site_name:flags.1?string title:flags.2?string description:flags.3?string photo:flags.4?Photo embed_url:flags.5?string embed_type:flags.5?string embed_width:flags.6?int embed_height:flags.6?int duration:flags.7?int author:flags.8?string document:flags.9?Document cached_page:flags.10?Page attributes:flags.12?Vector = WebPage; webPageNotModified#7311ca11 flags:# cached_page_views:flags.0?int = WebPage; authorization#ad01d61d flags:# current:flags.0?true official_app:flags.1?true password_pending:flags.2?true encrypted_requests_disabled:flags.3?true call_requests_disabled:flags.4?true unconfirmed:flags.5?true hash:long device_model:string platform:string system_version:string api_id:int app_name:string app_version:string date_created:int date_active:int ip:string country:string region:string = Authorization; @@ -571,7 +575,7 @@ chatInviteExported#ab4a819 flags:# revoked:flags.0?true permanent:flags.5?true r chatInvitePublicJoinRequests#ed107ab7 = ExportedChatInvite; chatInviteAlready#5a686d7c chat:Chat = ChatInvite; -chatInvite#300c44c1 flags:# channel:flags.0?true broadcast:flags.1?true public:flags.2?true megagroup:flags.3?true request_needed:flags.6?true verified:flags.7?true scam:flags.8?true fake:flags.9?true title:string about:flags.5?string photo:Photo participants_count:int participants:flags.4?Vector = ChatInvite; +chatInvite#cde0ec40 flags:# channel:flags.0?true broadcast:flags.1?true public:flags.2?true megagroup:flags.3?true request_needed:flags.6?true verified:flags.7?true scam:flags.8?true fake:flags.9?true title:string about:flags.5?string photo:Photo participants_count:int participants:flags.4?Vector color:int = ChatInvite; chatInvitePeek#61695cb0 chat:Chat expires:int = ChatInvite; inputStickerSetEmpty#ffb62b95 = InputStickerSet; @@ -585,7 +589,7 @@ inputStickerSetEmojiGenericAnimations#4c4d4ce = InputStickerSet; inputStickerSetEmojiDefaultStatuses#29d0f5ee = InputStickerSet; inputStickerSetEmojiDefaultTopicIcons#44c1f8e9 = InputStickerSet; -stickerSet#2dd14edc flags:# archived:flags.1?true official:flags.2?true masks:flags.3?true animated:flags.5?true videos:flags.6?true emojis:flags.7?true installed_date:flags.0?int id:long access_hash:long title:string short_name:string thumbs:flags.4?Vector thumb_dc_id:flags.4?int thumb_version:flags.4?int thumb_document_id:flags.8?long count:int hash:int = StickerSet; +stickerSet#2dd14edc flags:# archived:flags.1?true official:flags.2?true masks:flags.3?true animated:flags.5?true videos:flags.6?true emojis:flags.7?true text_color:flags.9?true installed_date:flags.0?int id:long access_hash:long title:string short_name:string thumbs:flags.4?Vector thumb_dc_id:flags.4?int thumb_version:flags.4?int thumb_document_id:flags.8?long count:int hash:int = StickerSet; messages.stickerSet#6e153f16 set:StickerSet packs:Vector keywords:Vector documents:Vector = messages.StickerSet; messages.stickerSetNotModified#d3f924eb = messages.StickerSet; @@ -635,10 +639,10 @@ messageEntityPhone#9b69e34b offset:int length:int = MessageEntity; messageEntityCashtag#4c4e743f offset:int length:int = MessageEntity; messageEntityUnderline#9c4e7e8b offset:int length:int = MessageEntity; messageEntityStrike#bf0693d4 offset:int length:int = MessageEntity; -messageEntityBlockquote#20df5d0 offset:int length:int = MessageEntity; messageEntityBankCard#761e6af4 offset:int length:int = MessageEntity; messageEntitySpoiler#32ca960f offset:int length:int = MessageEntity; messageEntityCustomEmoji#c8cf05f8 offset:int length:int document_id:long = MessageEntity; +messageEntityBlockquote#20df5d0 offset:int length:int = MessageEntity; inputChannelEmpty#ee8c1e86 = InputChannel; inputChannel#f35aec28 channel_id:long access_hash:long = InputChannel; @@ -681,25 +685,27 @@ help.termsOfService#780a0310 flags:# popup:flags.0?true id:DataJSON text:string messages.savedGifsNotModified#e8025ca2 = messages.SavedGifs; messages.savedGifs#84a02a0d hash:long gifs:Vector = messages.SavedGifs; -inputBotInlineMessageMediaAuto#3380c786 flags:# message:string entities:flags.1?Vector reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; -inputBotInlineMessageText#3dcd7a87 flags:# no_webpage:flags.0?true message:string entities:flags.1?Vector reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; +inputBotInlineMessageMediaAuto#3380c786 flags:# invert_media:flags.3?true message:string entities:flags.1?Vector reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; +inputBotInlineMessageText#3dcd7a87 flags:# no_webpage:flags.0?true invert_media:flags.3?true message:string entities:flags.1?Vector reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; inputBotInlineMessageMediaGeo#96929a85 flags:# geo_point:InputGeoPoint heading:flags.0?int period:flags.1?int proximity_notification_radius:flags.3?int reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; inputBotInlineMessageMediaVenue#417bbf11 flags:# geo_point:InputGeoPoint title:string address:string provider:string venue_id:string venue_type:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; inputBotInlineMessageMediaContact#a6edbffd flags:# phone_number:string first_name:string last_name:string vcard:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; inputBotInlineMessageGame#4b425864 flags:# reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; inputBotInlineMessageMediaInvoice#d7e78225 flags:# title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:string provider_data:DataJSON reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; +inputBotInlineMessageMediaWebPage#bddcc510 flags:# invert_media:flags.3?true force_large_media:flags.4?true force_small_media:flags.5?true optional:flags.6?true message:string entities:flags.1?Vector url:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; inputBotInlineResult#88bf9319 flags:# id:string type:string title:flags.1?string description:flags.2?string url:flags.3?string thumb:flags.4?InputWebDocument content:flags.5?InputWebDocument send_message:InputBotInlineMessage = InputBotInlineResult; inputBotInlineResultPhoto#a8d864a7 id:string type:string photo:InputPhoto send_message:InputBotInlineMessage = InputBotInlineResult; inputBotInlineResultDocument#fff8fdc4 flags:# id:string type:string title:flags.1?string description:flags.2?string document:InputDocument send_message:InputBotInlineMessage = InputBotInlineResult; inputBotInlineResultGame#4fa417f2 id:string short_name:string send_message:InputBotInlineMessage = InputBotInlineResult; -botInlineMessageMediaAuto#764cf810 flags:# message:string entities:flags.1?Vector reply_markup:flags.2?ReplyMarkup = BotInlineMessage; -botInlineMessageText#8c7f65e2 flags:# no_webpage:flags.0?true message:string entities:flags.1?Vector reply_markup:flags.2?ReplyMarkup = BotInlineMessage; +botInlineMessageMediaAuto#764cf810 flags:# invert_media:flags.3?true message:string entities:flags.1?Vector reply_markup:flags.2?ReplyMarkup = BotInlineMessage; +botInlineMessageText#8c7f65e2 flags:# no_webpage:flags.0?true invert_media:flags.3?true message:string entities:flags.1?Vector reply_markup:flags.2?ReplyMarkup = BotInlineMessage; botInlineMessageMediaGeo#51846fd flags:# geo:GeoPoint heading:flags.0?int period:flags.1?int proximity_notification_radius:flags.3?int reply_markup:flags.2?ReplyMarkup = BotInlineMessage; botInlineMessageMediaVenue#8a86659c flags:# geo:GeoPoint title:string address:string provider:string venue_id:string venue_type:string reply_markup:flags.2?ReplyMarkup = BotInlineMessage; botInlineMessageMediaContact#18d1cdc2 flags:# phone_number:string first_name:string last_name:string vcard:string reply_markup:flags.2?ReplyMarkup = BotInlineMessage; botInlineMessageMediaInvoice#354a9b09 flags:# shipping_address_requested:flags.1?true test:flags.3?true title:string description:string photo:flags.0?WebDocument currency:string total_amount:long reply_markup:flags.2?ReplyMarkup = BotInlineMessage; +botInlineMessageMediaWebPage#809ad9a6 flags:# invert_media:flags.3?true force_large_media:flags.4?true force_small_media:flags.5?true manual:flags.7?true safe:flags.8?true message:string entities:flags.1?Vector url:string reply_markup:flags.2?ReplyMarkup = BotInlineMessage; botInlineResult#11965f3a flags:# id:string type:string title:flags.1?string description:flags.2?string url:flags.3?string thumb:flags.4?WebDocument content:flags.5?WebDocument send_message:BotInlineMessage = BotInlineResult; botInlineMediaResult#17db940b flags:# id:string type:string photo:flags.0?Photo document:flags.1?Document title:flags.2?string description:flags.3?string send_message:BotInlineMessage = BotInlineResult; @@ -755,7 +761,7 @@ contacts.topPeers#70b772a8 categories:Vector chats:Vector< contacts.topPeersDisabled#b52c939d = contacts.TopPeers; draftMessageEmpty#1b0c841a flags:# date:flags.0?int = DraftMessage; -draftMessage#fd8e711f flags:# no_webpage:flags.1?true reply_to_msg_id:flags.0?int message:string entities:flags.3?Vector date:int = DraftMessage; +draftMessage#3fccf7ef flags:# no_webpage:flags.1?true invert_media:flags.6?true reply_to:flags.4?InputReplyTo message:string entities:flags.3?Vector media:flags.5?InputMedia date:int = DraftMessage; messages.featuredStickersNotModified#c6dc0c66 count:int = messages.FeaturedStickers; messages.featuredStickers#be382906 flags:# premium:flags.0?true hash:long count:int sets:Vector unread:Vector = messages.FeaturedStickers; @@ -960,6 +966,8 @@ channelAdminLogEventActionEditTopic#f06fe208 prev_topic:ForumTopic new_topic:For channelAdminLogEventActionDeleteTopic#ae168909 topic:ForumTopic = ChannelAdminLogEventAction; channelAdminLogEventActionPinTopic#5d8d353b flags:# prev_topic:flags.0?ForumTopic new_topic:flags.1?ForumTopic = ChannelAdminLogEventAction; channelAdminLogEventActionToggleAntiSpam#64f36dfc new_value:Bool = ChannelAdminLogEventAction; +channelAdminLogEventActionChangeColor#3c2b247b prev_value:int new_value:int = ChannelAdminLogEventAction; +channelAdminLogEventActionChangeBackgroundEmoji#445fc434 prev_value:long new_value:long = ChannelAdminLogEventAction; channelAdminLogEvent#1fad68cd id:long date:int user_id:long action:ChannelAdminLogEventAction = ChannelAdminLogEvent; @@ -1256,7 +1264,7 @@ messages.messageViews#b6c4f543 views:Vector chats:Vector use messages.discussionMessage#a6341782 flags:# messages:Vector max_id:flags.0?int read_inbox_max_id:flags.1?int read_outbox_max_id:flags.2?int unread_count:int chats:Vector users:Vector = messages.DiscussionMessage; -messageReplyHeader#a6d57763 flags:# reply_to_scheduled:flags.2?true forum_topic:flags.3?true reply_to_msg_id:int reply_to_peer_id:flags.0?Peer reply_to_top_id:flags.1?int = MessageReplyHeader; +messageReplyHeader#6eebcabd flags:# reply_to_scheduled:flags.2?true forum_topic:flags.3?true quote:flags.9?true reply_to_msg_id:flags.4?int reply_to_peer_id:flags.0?Peer reply_from:flags.5?MessageFwdHeader reply_media:flags.8?MessageMedia reply_to_top_id:flags.1?int quote_text:flags.6?string quote_entities:flags.7?Vector = MessageReplyHeader; messageReplyStoryHeader#9c98bfc1 user_id:long story_id:int = MessageReplyHeader; messageReplies#83d60fc2 flags:# comments:flags.0?true replies:int replies_pts:int recent_repliers:flags.1?Vector channel_id:flags.0?long max_id:flags.2?int read_max_id:flags.3?int = MessageReplies; @@ -1406,6 +1414,7 @@ attachMenuPeerTypeBroadcast#7bfbdefc = AttachMenuPeerType; inputInvoiceMessage#c5b56859 peer:InputPeer msg_id:int = InputInvoice; inputInvoiceSlug#c326caef slug:string = InputInvoice; +inputInvoicePremiumGiftCode#98986c0d purpose:InputStorePaymentPurpose option:PremiumGiftCodeOption = InputInvoice; payments.exportedInvoice#aed0cbd9 url:string = payments.ExportedInvoice; @@ -1415,6 +1424,8 @@ help.premiumPromo#5334759c status_text:string status_entities:Vector boost_peer:flags.0?InputPeer currency:string amount:long = InputStorePaymentPurpose; +inputStorePaymentPremiumGiveaway#7c9375e6 flags:# only_new_subscribers:flags.0?true boost_peer:InputPeer additional_peers:flags.1?Vector countries_iso2:flags.2?Vector random_id:long until_date:int currency:string amount:long = InputStorePaymentPurpose; premiumGiftOption#74c34319 flags:# months:int currency:string amount:long bot_url:string store_product:flags.0?string = PremiumGiftOption; @@ -1546,7 +1557,7 @@ stories.storyViewsList#46e9b9ec flags:# count:int reactions_count:int views:Vect stories.storyViews#de9eed1d views:Vector users:Vector = stories.StoryViews; -inputReplyToMessage#9c5386e4 flags:# reply_to_msg_id:int top_msg_id:flags.0?int = InputReplyTo; +inputReplyToMessage#73ec805 flags:# reply_to_msg_id:int top_msg_id:flags.0?int reply_to_peer_id:flags.1?InputPeer quote_text:flags.2?string quote_entities:flags.3?Vector = InputReplyTo; inputReplyToStory#15b0f283 user_id:InputUser story_id:int = InputReplyTo; exportedStoryLink#3fc9053b link:string = ExportedStoryLink; @@ -1564,16 +1575,26 @@ peerStories#9a35e999 flags:# peer:Peer max_read_id:flags.0?int stories:Vector users:Vector = stories.PeerStories; -stories.boostsStatus#e5c1aa5c flags:# my_boost:flags.2?true level:int current_level_boosts:int boosts:int next_level_boosts:flags.0?int premium_audience:flags.1?StatsPercentValue boost_url:string = stories.BoostsStatus; +messages.webPage#fd5e12bd webpage:WebPage chats:Vector users:Vector = messages.WebPage; -stories.canApplyBoostOk#c3173587 = stories.CanApplyBoostResult; -stories.canApplyBoostReplace#712c4655 current_boost:Peer chats:Vector = stories.CanApplyBoostResult; +premiumGiftCodeOption#257e962b flags:# users:int months:int store_product:flags.0?string store_quantity:flags.1?int currency:string amount:long = PremiumGiftCodeOption; -booster#e9e6380 user_id:long expires:int = Booster; +payments.checkedGiftCode#b722f158 flags:# via_giveaway:flags.2?true from_id:Peer giveaway_msg_id:flags.3?int to_id:flags.0?long date:int months:int used_date:flags.1?int chats:Vector users:Vector = payments.CheckedGiftCode; -stories.boostersList#f3dd3d1d flags:# count:int boosters:Vector next_offset:flags.0?string users:Vector = stories.BoostersList; +payments.giveawayInfo#4367daa0 flags:# participating:flags.0?true preparing_results:flags.3?true start_date:int joined_too_early_date:flags.1?int admin_disallowed_chat_id:flags.2?long disallowed_country:flags.4?string = payments.GiveawayInfo; +payments.giveawayInfoResults#cd5570 flags:# winner:flags.0?true refunded:flags.1?true start_date:int gift_code_slug:flags.0?string finish_date:int winners_count:int activated_count:int = payments.GiveawayInfo; -messages.webPage#fd5e12bd webpage:WebPage chats:Vector users:Vector = messages.WebPage; +prepaidGiveaway#b2539d54 id:long months:int quantity:int date:int = PrepaidGiveaway; + +boost#2a1c8c71 flags:# gift:flags.1?true giveaway:flags.2?true unclaimed:flags.3?true id:string user_id:flags.0?long giveaway_msg_id:flags.2?int date:int expires:int used_gift_slug:flags.4?string multiplier:flags.5?int = Boost; + +premium.boostsList#86f8613c flags:# count:int boosts:Vector next_offset:flags.0?string users:Vector = premium.BoostsList; + +myBoost#c448415c flags:# slot:int peer:flags.0?Peer date:int expires:int cooldown_until_date:flags.1?int = MyBoost; + +premium.myBoosts#9ae228e2 my_boosts:Vector chats:Vector users:Vector = premium.MyBoosts; + +premium.boostsStatus#4959427a flags:# my_boost:flags.2?true level:int current_level_boosts:int boosts:int gift_boosts:flags.4?int next_level_boosts:flags.0?int premium_audience:flags.1?StatsPercentValue boost_url:string prepaid_giveaways:flags.3?Vector my_boost_slots:flags.2?Vector = premium.BoostsStatus; ---functions--- @@ -1696,6 +1717,8 @@ account.getAutoSaveSettings#adcbbcda = account.AutoSaveSettings; account.saveAutoSaveSettings#d69b8361 flags:# users:flags.0?true chats:flags.1?true broadcasts:flags.2?true peer:flags.3?InputPeer settings:AutoSaveSettings = Bool; account.deleteAutoSaveExceptions#53bc0020 = Bool; account.invalidateSignInCodes#ca8ae8ba codes:Vector = Bool; +account.updateColor#a001cc43 flags:# color:int background_emoji_id:flags.0?long = Bool; +account.getDefaultBackgroundEmojis#a60ab9ce hash:long = EmojiList; users.getUsers#d91a548 id:Vector = Vector; users.getFullUser#b60f5918 id:InputUser = users.UserFull; @@ -1736,8 +1759,8 @@ messages.deleteHistory#b08f922a flags:# just_clear:flags.0?true revoke:flags.1?t messages.deleteMessages#e58e95d2 flags:# revoke:flags.0?true id:Vector = messages.AffectedMessages; messages.receivedMessages#5a954c0 max_id:int = Vector; messages.setTyping#58943ee2 flags:# peer:InputPeer top_msg_id:flags.0?int action:SendMessageAction = Bool; -messages.sendMessage#280d096f flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true peer:InputPeer reply_to:flags.0?InputReplyTo message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates; -messages.sendMedia#72ccc23d flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true peer:InputPeer reply_to:flags.0?InputReplyTo media:InputMedia message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates; +messages.sendMessage#280d096f flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true peer:InputPeer reply_to:flags.0?InputReplyTo message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates; +messages.sendMedia#72ccc23d flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true peer:InputPeer reply_to:flags.0?InputReplyTo media:InputMedia message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates; messages.forwardMessages#c661bbc4 flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true drop_author:flags.11?true drop_media_captions:flags.12?true noforwards:flags.14?true from_peer:InputPeer id:Vector random_id:Vector to_peer:InputPeer top_msg_id:flags.9?int schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates; messages.reportSpam#cf1592db peer:InputPeer = Bool; messages.getPeerSettings#efd9a6a2 peer:InputPeer = messages.PeerSettings; @@ -1783,12 +1806,12 @@ messages.getInlineBotResults#514e999d flags:# bot:InputUser peer:InputPeer geo_p messages.setInlineBotResults#bb12a419 flags:# gallery:flags.0?true private:flags.1?true query_id:long results:Vector cache_time:int next_offset:flags.2?string switch_pm:flags.3?InlineBotSwitchPM switch_webview:flags.4?InlineBotWebView = Bool; messages.sendInlineBotResult#f7bc68ba flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true hide_via:flags.11?true peer:InputPeer reply_to:flags.0?InputReplyTo random_id:long query_id:long id:string schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates; messages.getMessageEditData#fda68d36 peer:InputPeer id:int = messages.MessageEditData; -messages.editMessage#48f71778 flags:# no_webpage:flags.1?true peer:InputPeer id:int message:flags.11?string media:flags.14?InputMedia reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.15?int = Updates; -messages.editInlineBotMessage#83557dba flags:# no_webpage:flags.1?true id:InputBotInlineMessageID message:flags.11?string media:flags.14?InputMedia reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector = Bool; +messages.editMessage#48f71778 flags:# no_webpage:flags.1?true invert_media:flags.16?true peer:InputPeer id:int message:flags.11?string media:flags.14?InputMedia reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.15?int = Updates; +messages.editInlineBotMessage#83557dba flags:# no_webpage:flags.1?true invert_media:flags.16?true id:InputBotInlineMessageID message:flags.11?string media:flags.14?InputMedia reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector = Bool; messages.getBotCallbackAnswer#9342ca07 flags:# game:flags.1?true peer:InputPeer msg_id:int data:flags.0?bytes password:flags.2?InputCheckPasswordSRP = messages.BotCallbackAnswer; messages.setBotCallbackAnswer#d58f130a flags:# alert:flags.1?true query_id:long message:flags.0?string url:flags.2?string cache_time:int = Bool; messages.getPeerDialogs#e470bcfd peers:Vector = messages.PeerDialogs; -messages.saveDraft#b4331e3f flags:# no_webpage:flags.1?true reply_to_msg_id:flags.0?int top_msg_id:flags.2?int peer:InputPeer message:string entities:flags.3?Vector = Bool; +messages.saveDraft#7ff3b806 flags:# no_webpage:flags.1?true invert_media:flags.6?true reply_to:flags.4?InputReplyTo peer:InputPeer message:string entities:flags.3?Vector media:flags.5?InputMedia = Bool; messages.getAllDrafts#6a3f8d65 = Updates; messages.getFeaturedStickers#64780b14 hash:long = messages.FeaturedStickers; messages.readFeaturedStickers#5b118126 id:Vector = Bool; @@ -1816,7 +1839,7 @@ messages.faveSticker#b9ffc55b id:InputDocument unfave:Bool = Bool; messages.getUnreadMentions#f107e790 flags:# peer:InputPeer top_msg_id:flags.0?int offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages; messages.readMentions#36e5bf4d flags:# peer:InputPeer top_msg_id:flags.0?int = messages.AffectedHistory; messages.getRecentLocations#702a40e0 peer:InputPeer limit:int hash:long = messages.Messages; -messages.sendMultiMedia#456e8987 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true peer:InputPeer reply_to:flags.0?InputReplyTo multi_media:Vector schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates; +messages.sendMultiMedia#456e8987 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true peer:InputPeer reply_to:flags.0?InputReplyTo multi_media:Vector schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates; messages.uploadEncryptedFile#5057c497 peer:InputEncryptedChat file:InputEncryptedFile = EncryptedFile; messages.searchStickerSets#35705b8a flags:# exclude_featured:flags.0?true q:string hash:long = messages.FoundStickerSets; messages.getSplitRanges#1cff7e08 = Vector; @@ -2014,6 +2037,7 @@ channels.toggleAntiSpam#68f3e4eb channel:InputChannel enabled:Bool = Updates; channels.reportAntiSpamFalsePositive#a850a693 channel:InputChannel msg_id:int = Bool; channels.toggleParticipantsHidden#6a6e7854 channel:InputChannel enabled:Bool = Updates; channels.clickSponsoredMessage#18afbc93 channel:InputChannel random_id:bytes = Bool; +channels.updateColor#621a201f flags:# channel:InputChannel color:int background_emoji_id:flags.0?long = Updates; bots.sendCustomRequest#aa2769ed custom_method:string params:DataJSON = DataJSON; bots.answerWebhookJSONQuery#e6213f4d query_id:long data:DataJSON = Bool; @@ -2043,6 +2067,11 @@ payments.exportInvoice#f91b065 invoice_media:InputMedia = payments.ExportedInvoi payments.assignAppStoreTransaction#80ed747d receipt:bytes purpose:InputStorePaymentPurpose = Updates; payments.assignPlayMarketTransaction#dffd50d3 receipt:DataJSON purpose:InputStorePaymentPurpose = Updates; payments.canPurchasePremium#9fc19eb6 purpose:InputStorePaymentPurpose = Bool; +payments.getPremiumGiftCodeOptions#2757ba54 flags:# boost_peer:flags.0?InputPeer = Vector; +payments.checkGiftCode#8e51b4c1 slug:string = payments.CheckedGiftCode; +payments.applyGiftCode#f6e26854 slug:string = Updates; +payments.getGiveawayInfo#f4239425 peer:InputPeer msg_id:int = payments.GiveawayInfo; +payments.launchPrepaidGiveaway#5ff58f20 peer:InputPeer giveaway_id:long purpose:InputStorePaymentPurpose = Updates; stickers.createStickerSet#9021ab67 flags:# masks:flags.0?true animated:flags.1?true videos:flags.4?true emojis:flags.5?true text_color:flags.6?true user_id:InputUser title:string short_name:string thumb:flags.2?InputDocument stickers:Vector software:flags.3?string = messages.StickerSet; stickers.removeStickerFromSet#f7760f51 sticker:InputDocument = messages.StickerSet; @@ -2136,7 +2165,8 @@ stories.getAllReadPeerStories#9b5ae7f9 = Updates; stories.getPeerMaxIDs#535983c3 id:Vector = Vector; stories.getChatsToSend#a56a8b60 = messages.Chats; stories.togglePeerStoriesHidden#bd0415c4 peer:InputPeer hidden:Bool = Bool; -stories.getBoostsStatus#4c449472 peer:InputPeer = stories.BoostsStatus; -stories.getBoostersList#337ef980 peer:InputPeer offset:string limit:int = stories.BoostersList; -stories.canApplyBoost#db05c1bd peer:InputPeer = stories.CanApplyBoostResult; -stories.applyBoost#f29d7c2b peer:InputPeer = Bool; + +premium.getBoostsList#60f67660 flags:# gifts:flags.0?true peer:InputPeer offset:string limit:int = premium.BoostsList; +premium.getMyBoosts#be77b4a = premium.MyBoosts; +premium.applyBoost#6b7da746 flags:# slots:flags.0?Vector peer:InputPeer = premium.MyBoosts; +premium.getBoostsStatus#42f1f61 peer:InputPeer = premium.BoostsStatus; diff --git a/Telegram/SourceFiles/mtproto/scheme/layer.tl b/Telegram/SourceFiles/mtproto/scheme/layer.tl index 7a644419f6ad37..ede5063647065c 100644 --- a/Telegram/SourceFiles/mtproto/scheme/layer.tl +++ b/Telegram/SourceFiles/mtproto/scheme/layer.tl @@ -1 +1 @@ -// LAYER 165 +// LAYER 166 diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.cpp b/Telegram/SourceFiles/payments/payments_checkout_process.cpp index d8809fd35d6f89..2e847d0240706d 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.cpp +++ b/Telegram/SourceFiles/payments/payments_checkout_process.cpp @@ -38,6 +38,7 @@ namespace { struct SessionProcesses { base::flat_map> byItem; base::flat_map> bySlug; + base::flat_map> byRandomId; base::flat_map paymentStartedByItem; base::flat_map paymentStartedBySlug; rpl::lifetime lifetime; @@ -118,6 +119,28 @@ void CheckoutProcess::Start( j->second->requestActivate(); } +void CheckoutProcess::Start( + InvoicePremiumGiftCode giftCodeInvoice, + Fn reactivate) { + const auto randomId = giftCodeInvoice.randomId; + auto id = InvoiceId{ std::move(giftCodeInvoice) }; + auto &processes = LookupSessionProcesses(SessionFromId(id)); + const auto i = processes.byRandomId.find(randomId); + if (i != end(processes.byRandomId)) { + i->second->setReactivateCallback(std::move(reactivate)); + i->second->requestActivate(); + return; + } + const auto j = processes.byRandomId.emplace( + randomId, + std::make_unique( + std::move(id), + Mode::Payment, + std::move(reactivate), + PrivateTag{})).first; + j->second->requestActivate(); +} + std::optional CheckoutProcess::InvoicePaid( not_null item) { const auto session = &item->history()->session(); @@ -139,7 +162,8 @@ std::optional CheckoutProcess::InvoicePaid( } else if (i->second.paymentStartedByItem.empty() && i->second.byItem.empty() && i->second.paymentStartedBySlug.empty() - && i->second.bySlug.empty()) { + && i->second.bySlug.empty() + && i->second.byRandomId.empty()) { Processes.erase(i); } return result; @@ -165,7 +189,8 @@ std::optional CheckoutProcess::InvoicePaid( } else if (i->second.paymentStartedByItem.empty() && i->second.byItem.empty() && i->second.paymentStartedBySlug.empty() - && i->second.bySlug.empty()) { + && i->second.bySlug.empty() + && i->second.byRandomId.empty()) { Processes.erase(i); } return result; @@ -192,6 +217,11 @@ void CheckoutProcess::RegisterPaymentStart( return; } } + for (const auto &[randomId, itemProcess] : i->second.byRandomId) { + if (itemProcess.get() == process) { + return; + } + } } void CheckoutProcess::UnregisterPaymentStart( @@ -212,10 +242,16 @@ void CheckoutProcess::UnregisterPaymentStart( break; } } + for (const auto &[randomId, itemProcess] : i->second.byRandomId) { + if (itemProcess.get() == process) { + break; + } + } if (i->second.paymentStartedByItem.empty() && i->second.byItem.empty() && i->second.paymentStartedBySlug.empty() - && i->second.bySlug.empty()) { + && i->second.bySlug.empty() + && i->second.byRandomId.empty()) { Processes.erase(i); } } @@ -497,8 +533,16 @@ void CheckoutProcess::close() { if (k != end(entry.bySlug)) { entry.bySlug.erase(k); } + const auto l = ranges::find( + entry.byRandomId, + this, + [](const auto &pair) { return pair.second.get(); }); + if (l != end(entry.byRandomId)) { + entry.byRandomId.erase(l); + } if (entry.byItem.empty() && entry.bySlug.empty() + && i->second.byRandomId.empty() && entry.paymentStartedByItem.empty() && entry.paymentStartedBySlug.empty()) { Processes.erase(i); diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.h b/Telegram/SourceFiles/payments/payments_checkout_process.h index 91b51d9befcac5..fb7df4abecbea8 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.h +++ b/Telegram/SourceFiles/payments/payments_checkout_process.h @@ -37,6 +37,7 @@ class Form; struct FormUpdate; struct Error; struct InvoiceId; +struct InvoicePremiumGiftCode; enum class Mode { Payment, @@ -68,6 +69,9 @@ class CheckoutProcess final not_null session, const QString &slug, Fn reactivate); + static void Start( + InvoicePremiumGiftCode giftCodeInvoice, + Fn reactivate); [[nodiscard]] static std::optional InvoicePaid( not_null item); [[nodiscard]] static std::optional InvoicePaid( diff --git a/Telegram/SourceFiles/payments/payments_form.cpp b/Telegram/SourceFiles/payments/payments_form.cpp index d5032bb5d29a6a..a7b1ef2d07c7c9 100644 --- a/Telegram/SourceFiles/payments/payments_form.cpp +++ b/Telegram/SourceFiles/payments/payments_form.cpp @@ -8,6 +8,7 @@ For license and copyright information please follow this link: #include "payments/payments_form.h" #include "main/main_session.h" +#include "data/data_channel.h" #include "data/data_session.h" #include "data/data_media_types.h" #include "data/data_user.h" @@ -112,10 +113,21 @@ constexpr auto kPasswordPeriod = 15 * TimeId(60); } // namespace not_null SessionFromId(const InvoiceId &id) { - if (const auto slug = std::get_if(&id.value)) { + if (const auto message = std::get_if(&id.value)) { + return &message->peer->session(); + } else if (const auto slug = std::get_if(&id.value)) { return slug->session; } - return &v::get(id.value).peer->session(); + const auto &giftCode = v::get(id.value); + const auto users = std::get_if( + &giftCode.purpose); + if (users) { + Assert(!users->users.empty()); + return &users->users.front()->session(); + } + const auto &giveaway = v::get( + giftCode.purpose); + return &giveaway.boostPeer->session(); } Form::Form(InvoiceId id, bool receipt) @@ -207,11 +219,10 @@ void Form::loadThumbnail(not_null photo) { } Data::FileOrigin Form::thumbnailFileOrigin() const { - if (const auto slug = std::get_if(&_id.value)) { - return Data::FileOrigin(); + if (const auto message = std::get_if(&_id.value)) { + return FullMsgId(message->peer->id, message->itemId); } - const auto message = v::get(_id.value); - return FullMsgId(message.peer->id, message.itemId); + return Data::FileOrigin(); } QImage Form::prepareGoodThumbnail( @@ -257,13 +268,75 @@ QImage Form::prepareEmptyThumbnail() const { } MTPInputInvoice Form::inputInvoice() const { - if (const auto slug = std::get_if(&_id.value)) { + if (const auto message = std::get_if(&_id.value)) { + return MTP_inputInvoiceMessage( + message->peer->input, + MTP_int(message->itemId.bare)); + } else if (const auto slug = std::get_if(&_id.value)) { return MTP_inputInvoiceSlug(MTP_string(slug->slug)); } - const auto message = v::get(_id.value); - return MTP_inputInvoiceMessage( - message.peer->input, - MTP_int(message.itemId.bare)); + const auto &giftCode = v::get(_id.value); + using Flag = MTPDpremiumGiftCodeOption::Flag; + const auto option = MTP_premiumGiftCodeOption( + MTP_flags((giftCode.storeQuantity ? Flag::f_store_quantity : Flag()) + | (giftCode.storeProduct.isEmpty() + ? Flag() + : Flag::f_store_product)), + MTP_int(giftCode.users), + MTP_int(giftCode.months), + MTP_string(giftCode.storeProduct), + MTP_int(giftCode.storeQuantity), + MTP_string(giftCode.currency), + MTP_long(giftCode.amount)); + const auto users = std::get_if( + &giftCode.purpose); + if (users) { + using Flag = MTPDinputStorePaymentPremiumGiftCode::Flag; + return MTP_inputInvoicePremiumGiftCode( + MTP_inputStorePaymentPremiumGiftCode( + MTP_flags(users->boostPeer ? Flag::f_boost_peer : Flag()), + MTP_vector(ranges::views::all( + users->users + ) | ranges::views::transform([](not_null user) { + return MTPInputUser(user->inputUser); + }) | ranges::to), + users->boostPeer ? users->boostPeer->input : MTPInputPeer(), + MTP_string(giftCode.currency), + MTP_long(giftCode.amount)), + option); + } else { + const auto &giveaway = v::get( + giftCode.purpose); + using Flag = MTPDinputStorePaymentPremiumGiveaway::Flag; + return MTP_inputInvoicePremiumGiftCode( + MTP_inputStorePaymentPremiumGiveaway( + MTP_flags(Flag() + | (giveaway.onlyNewSubscribers + ? Flag::f_only_new_subscribers + : Flag()) + | (giveaway.additionalChannels.empty() + ? Flag() + : Flag::f_additional_peers) + | (giveaway.countries.empty() + ? Flag() + : Flag::f_countries_iso2)), + giveaway.boostPeer->input, + MTP_vector(ranges::views::all( + giveaway.additionalChannels + ) | ranges::views::transform([](not_null c) { + return MTPInputPeer(c->input); + }) | ranges::to()), + MTP_vector(ranges::views::all( + giveaway.countries + ) | ranges::views::transform([](QString value) { + return MTP_string(value); + }) | ranges::to()), + MTP_long(giftCode.randomId), + MTP_int(giveaway.untilDate), + MTP_string(giftCode.currency), + MTP_long(giftCode.amount)), + option); + } } void Form::requestForm() { diff --git a/Telegram/SourceFiles/payments/payments_form.h b/Telegram/SourceFiles/payments/payments_form.h index cdb36c6929972f..7aa33f691c8069 100644 --- a/Telegram/SourceFiles/payments/payments_form.h +++ b/Telegram/SourceFiles/payments/payments_form.h @@ -186,8 +186,35 @@ struct InvoiceSlug { QString slug; }; +struct InvoicePremiumGiftCodeGiveaway { + not_null boostPeer; + std::vector> additionalChannels; + std::vector countries; + TimeId untilDate = 0; + bool onlyNewSubscribers = false; +}; + +struct InvoicePremiumGiftCodeUsers { + std::vector> users; + ChannelData *boostPeer = nullptr; +}; + +struct InvoicePremiumGiftCode { + std::variant< + InvoicePremiumGiftCodeUsers, + InvoicePremiumGiftCodeGiveaway> purpose; + + uint64 randomId = 0; + QString currency; + uint64 amount = 0; + QString storeProduct; + int storeQuantity = 0; + int users = 0; + int months = 0; +}; + struct InvoiceId { - std::variant value; + std::variant value; }; [[nodiscard]] not_null SessionFromId(const InvoiceId &id); diff --git a/Telegram/SourceFiles/payments/ui/payments.style b/Telegram/SourceFiles/payments/ui/payments.style index 8969a58d9b0b19..9668db4039bb00 100644 --- a/Telegram/SourceFiles/payments/ui/payments.style +++ b/Telegram/SourceFiles/payments/ui/payments.style @@ -136,6 +136,7 @@ paymentsLoading: InfiniteRadialAnimation(defaultInfiniteRadialAnimation) { thickness: 4px; } +botWebViewPanelSize: size(384px, 694px); botWebViewBottomButton: RoundButton(paymentsPanelSubmit) { height: 56px; font: boxButtonFont; diff --git a/Telegram/SourceFiles/platform/linux/main_window_linux.cpp b/Telegram/SourceFiles/platform/linux/main_window_linux.cpp index 4a8335baf356c5..5542e90de23e8b 100644 --- a/Telegram/SourceFiles/platform/linux/main_window_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/main_window_linux.cpp @@ -373,6 +373,15 @@ void MainWindow::createGlobalMenu() { }, Ui::kStrikeOutSequence); + psBlockquote = edit->addAction( + tr::lng_menu_formatting_blockquote(tr::now), + [] { + SendKeySequence( + Qt::Key_Period, + Qt::ControlModifier | Qt::ShiftModifier); + }, + Ui::kBlockquoteSequence); + psMonospace = edit->addAction( tr::lng_menu_formatting_monospace(tr::now), [] { @@ -534,6 +543,7 @@ void MainWindow::updateGlobalMenuHook() { ForceDisabled(psItalic, !markdownEnabled); ForceDisabled(psUnderline, !markdownEnabled); ForceDisabled(psStrikeOut, !markdownEnabled); + ForceDisabled(psBlockquote, !markdownEnabled); ForceDisabled(psMonospace, !markdownEnabled); ForceDisabled(psClearFormat, !markdownEnabled); } diff --git a/Telegram/SourceFiles/platform/linux/main_window_linux.h b/Telegram/SourceFiles/platform/linux/main_window_linux.h index a8949a2c536c4d..6bf270e2b3ff64 100644 --- a/Telegram/SourceFiles/platform/linux/main_window_linux.h +++ b/Telegram/SourceFiles/platform/linux/main_window_linux.h @@ -57,6 +57,7 @@ class MainWindow : public Window::MainWindow { QAction *psItalic = nullptr; QAction *psUnderline = nullptr; QAction *psStrikeOut = nullptr; + QAction *psBlockquote = nullptr; QAction *psMonospace = nullptr; QAction *psClearFormat = nullptr; diff --git a/Telegram/SourceFiles/platform/mac/main_window_mac.h b/Telegram/SourceFiles/platform/mac/main_window_mac.h index 3526285d29f876..8ba73e407afcb9 100644 --- a/Telegram/SourceFiles/platform/mac/main_window_mac.h +++ b/Telegram/SourceFiles/platform/mac/main_window_mac.h @@ -78,6 +78,7 @@ class MainWindow : public Window::MainWindow { QAction *psItalic = nullptr; QAction *psUnderline = nullptr; QAction *psStrikeOut = nullptr; + QAction *psBlockquote = nullptr; QAction *psMonospace = nullptr; QAction *psClearFormat = nullptr; diff --git a/Telegram/SourceFiles/platform/mac/main_window_mac.mm b/Telegram/SourceFiles/platform/mac/main_window_mac.mm index 9311071fb7bfa3..23cb437215d96e 100644 --- a/Telegram/SourceFiles/platform/mac/main_window_mac.mm +++ b/Telegram/SourceFiles/platform/mac/main_window_mac.mm @@ -406,6 +406,15 @@ QString strNotificationAboutScreenUnlocked() { Qt::ControlModifier | Qt::ShiftModifier); }, Ui::kStrikeOutSequence); + psBlockquote = edit->addAction( + tr::lng_menu_formatting_blockquote(tr::now), + this, + [] { + SendKeySequence( + Qt::Key_Period, + Qt::ControlModifier | Qt::ShiftModifier); + }, + Ui::kBlockquoteSequence); psMonospace = edit->addAction( tr::lng_menu_formatting_monospace(tr::now), this, @@ -550,6 +559,7 @@ QString strNotificationAboutScreenUnlocked() { ForceDisabled(psItalic, !canApplyMarkdown); ForceDisabled(psUnderline, !canApplyMarkdown); ForceDisabled(psStrikeOut, !canApplyMarkdown); + ForceDisabled(psBlockquote, !canApplyMarkdown); ForceDisabled(psMonospace, !canApplyMarkdown); ForceDisabled(psClearFormat, !canApplyMarkdown); } diff --git a/Telegram/SourceFiles/platform/mac/touchbar/items/mac_formatter_item.mm b/Telegram/SourceFiles/platform/mac/touchbar/items/mac_formatter_item.mm index d7be9aa01c0727..48e3592a421410 100644 --- a/Telegram/SourceFiles/platform/mac/touchbar/items/mac_formatter_item.mm +++ b/Telegram/SourceFiles/platform/mac/touchbar/items/mac_formatter_item.mm @@ -24,9 +24,10 @@ constexpr auto kCommandItalic = 0x011; constexpr auto kCommandUnderline = 0x012; constexpr auto kCommandStrikeOut = 0x013; -constexpr auto kCommandMonospace = 0x014; -constexpr auto kCommandClear = 0x015; -constexpr auto kCommandLink = 0x016; +constexpr auto kCommandBlockquote = 0x014; +constexpr auto kCommandMonospace = 0x015; +constexpr auto kCommandClear = 0x016; +constexpr auto kCommandLink = 0x017; const auto kPopoverFormatter = @"popoverInputFormatter"; @@ -44,6 +45,10 @@ void SendKeyEvent(int command) { case kCommandItalic: key = Qt::Key_I; break; + case kCommandBlockquote: + key = Qt::Key_Period; + modifier |= Qt::ShiftModifier; + break; case kCommandMonospace: key = Qt::Key_M; modifier |= Qt::ShiftModifier; @@ -103,6 +108,7 @@ - (id)init:(NSTouchBarItemIdentifier)identifier { tr::lng_menu_formatting_italic, tr::lng_menu_formatting_underline, tr::lng_menu_formatting_strike_out, + tr::lng_menu_formatting_blockquote, tr::lng_menu_formatting_monospace, tr::lng_menu_formatting_clear, tr::lng_info_link_label, diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style index 177e704eedb297..259b2a07589a42 100644 --- a/Telegram/SourceFiles/settings/settings.style +++ b/Telegram/SourceFiles/settings/settings.style @@ -433,8 +433,6 @@ notifyPreviewBottomSkip: 9px; settingsPremiumButtonPadding: margins(11px, 11px, 11px, 3px); settingsPremiumTopBarBackIcon: icon {{ "info/info_back", premiumButtonFg }}; settingsPremiumTopBarBackIconOver: icon {{ "info/info_back", premiumButtonFg }}; -settingsPremiumStarSize: size(84px, 81px); -settingsPremiumStarTopSkip: 37px; settingsPremiumTopBarBack: IconButton(infoTopBarBack) { icon: settingsPremiumTopBarBackIcon; iconOver: settingsPremiumTopBarBackIconOver; @@ -476,21 +474,6 @@ settingsPremiumPreviewIconTitlePadding: margins(62px, 13px, 24px, 1px); settingsPremiumPreviewIconAboutPadding: margins(62px, 0px, 24px, 0px); settingsPremiumPreviewIconPosition: point(20px, 7px); -settingsPremiumTitlePadding: margins(0px, 18px, 0px, 11px); -settingsPremiumAboutTextStyle: TextStyle(defaultTextStyle) { - font: font(12px); - linkUnderline: kLinkUnderlineAlways; - lineHeight: 18px; -} -settingsPremiumAbout: FlatLabel(defaultFlatLabel) { - style: settingsPremiumAboutTextStyle; - palette: TextPalette(defaultTextPalette) { - linkFg: premiumButtonFg; - } - align: align(top); - textFg: premiumButtonFg; - minWidth: 190px; -} settingsPremiumArrowShift: point(-5px, -1px); settingsPremiumArrow: icon{{ "settings/premium/arrow", menuIconFg }}; settingsPremiumArrowOver: icon{{ "settings/premium/arrow", menuIconFgOver }}; @@ -508,12 +491,6 @@ settingsPremiumUserTitle: FlatLabel(boxTitle) { maxHeight: 0px; align: align(top); } -settingsPremiumUserAbout: FlatLabel(boxDividerLabel) { - style: settingsPremiumAboutTextStyle; - minWidth: 315px; - maxHeight: 0px; - align: align(top); -} settingsPremiumLock: icon{{ "emoji/premium_lock", windowActiveTextFg, point(0px, 1px) }}; settingsPremiumLockSkip: 3px; @@ -597,3 +574,15 @@ filterLinkSubsectionTitlePadding: margins(0px, 5px, 0px, -4px); filterLinkChatsList: PeerList(peerListBox) { padding: margins(0px, 0px, 0px, membersMarginBottom); } + +settingsColorSampleSize: 20px; +settingsColorSampleCenter: 6px; +settingsColorSampleCenterRadius: 2px; +settingsColorSamplePadding: margins(8px, 2px, 8px, 2px); +settingsColorSampleSkip: 6px; +settingsColorButton: SettingsButton(settingsButton) { + padding: margins(60px, 10px, 48px, 10px); +} +settingsColorRadioMargin: 17px; +settingsColorRadioSkip: 13px; +settingsColorRadioStroke: 2px; diff --git a/Telegram/SourceFiles/settings/settings_chat.cpp b/Telegram/SourceFiles/settings/settings_chat.cpp index 5dac9f5357ae01..8436ea5f0a3054 100644 --- a/Telegram/SourceFiles/settings/settings_chat.cpp +++ b/Telegram/SourceFiles/settings/settings_chat.cpp @@ -10,6 +10,7 @@ For license and copyright information please follow this link: #include "settings/settings_common.h" #include "settings/settings_advanced.h" #include "settings/settings_experimental.h" +#include "boxes/peers/edit_peer_color_box.h" #include "boxes/connection_box.h" #include "boxes/auto_download_box.h" #include "boxes/reactions_settings_box.h" @@ -27,6 +28,7 @@ For license and copyright information please follow this link: #include "ui/widgets/buttons.h" #include "ui/widgets/labels.h" #include "ui/chat/attach/attach_extensions.h" +#include "ui/chat/chat_style.h" #include "ui/chat/chat_theme.h" #include "ui/layers/generic_box.h" #include "ui/effects/radial_animation.h" @@ -55,6 +57,7 @@ For license and copyright information please follow this link: #include "data/data_file_origin.h" #include "data/data_message_reactions.h" #include "data/data_peer_values.h" +#include "data/data_user.h" #include "chat_helpers/emoji_sets_manager.h" #include "base/platform/base_platform_info.h" #include "platform/platform_specific.h" @@ -1561,51 +1564,44 @@ void SetupCloudThemes( wrap->setDuration(0)->toggleOn(list->empty() | rpl::map(!_1)); } -void SetupAutoNightMode( +void SetupThemeSettings( not_null controller, not_null container) { - if (!Core::App().settings().systemDarkMode().has_value()) { - return; - } - AddDivider(container); AddSkip(container, st::settingsPrivacySkip); - AddSubsectionTitle(container, tr::lng_settings_auto_night_mode()); - - auto wrap = object_ptr(container); - const auto autoNight = wrap->add( - object_ptr( - wrap, - tr::lng_settings_auto_night_enabled(tr::now), - Core::App().settings().systemDarkModeEnabled(), - st::settingsCheckbox), - st::settingsCheckboxPadding); - - autoNight->checkedChanges( - ) | rpl::filter([=](bool checked) { - return (checked != Core::App().settings().systemDarkModeEnabled()); - }) | rpl::start_with_next([=](bool checked) { - if (checked && Window::Theme::Background()->editingTheme()) { - autoNight->setChecked(false); - controller->show(Ui::MakeInformBox( - tr::lng_theme_editor_cant_change_theme())); - } else { - Core::App().settings().setSystemDarkModeEnabled(checked); - Core::App().saveSettingsDelayed(); - } - }, autoNight->lifetime()); - - Core::App().settings().systemDarkModeEnabledChanges( - ) | rpl::filter([=](bool value) { - return (value != autoNight->checked()); - }) | rpl::start_with_next([=](bool value) { - autoNight->setChecked(value); - }, autoNight->lifetime()); + AddSubsectionTitle(container, tr::lng_settings_theme_settings()); - container->add(object_ptr( + AddPeerColorButton( container, - std::move(wrap))); + controller->uiShow(), + controller->session().user()); + + const auto settings = &Core::App().settings(); + if (settings->systemDarkMode().has_value()) { + auto label = settings->systemDarkModeEnabledValue( + ) | rpl::map([=](bool enabled) { + return enabled + ? tr::lng_settings_auto_night_mode_on() + : tr::lng_settings_auto_night_mode_off(); + }) | rpl::flatten_latest(); + AddButtonWithLabel( + container, + tr::lng_settings_auto_night_mode(), + std::move(label), + st::settingsButton, + { &st::menuIconNightMode } + )->setClickedCallback([=] { + const auto now = !settings->systemDarkModeEnabled(); + if (now && Window::Theme::Background()->editingTheme()) { + controller->show(Ui::MakeInformBox( + tr::lng_theme_editor_cant_change_theme())); + } else { + settings->setSystemDarkModeEnabled(now); + Core::App().saveSettingsDelayed(); + } + }); + } AddSkip(container, st::settingsCheckboxesSkip); } @@ -1749,7 +1745,7 @@ void Chat::setupContent(not_null controller) { const auto content = Ui::CreateChild(this); SetupThemeOptions(controller, content); - SetupAutoNightMode(controller, content); + SetupThemeSettings(controller, content); SetupCloudThemes(controller, content); SetupChatBackground(controller, content); SetupStickersEmoji(controller, content); diff --git a/Telegram/SourceFiles/settings/settings_common.h b/Telegram/SourceFiles/settings/settings_common.h index f52242b1248ae3..fc10618112257c 100644 --- a/Telegram/SourceFiles/settings/settings_common.h +++ b/Telegram/SourceFiles/settings/settings_common.h @@ -56,6 +56,9 @@ struct AbstractSectionFactory { [[nodiscard]] virtual object_ptr create( not_null parent, not_null controller) const = 0; + [[nodiscard]] virtual bool hasCustomTopBar() const { + return false; + } virtual ~AbstractSectionFactory() = default; }; diff --git a/Telegram/SourceFiles/settings/settings_global_ttl.cpp b/Telegram/SourceFiles/settings/settings_global_ttl.cpp index 112c72395deb0b..79e1df74ae6690 100644 --- a/Telegram/SourceFiles/settings/settings_global_ttl.cpp +++ b/Telegram/SourceFiles/settings/settings_global_ttl.cpp @@ -377,7 +377,7 @@ void GlobalTTL::setupContent() { auto controller = std::make_unique(session); auto initBox = [=, controller = controller.get()]( not_null box) { - box->addButton(tr::lng_background_apply(), crl::guard(this, [=] { + box->addButton(tr::lng_settings_apply(), crl::guard(this, [=] { const auto &peers = box->collectSelectedRows(); if (peers.empty()) { return; diff --git a/Telegram/SourceFiles/settings/settings_premium.cpp b/Telegram/SourceFiles/settings/settings_premium.cpp index 461f7c4b85f986..9eb32cbd98d119 100644 --- a/Telegram/SourceFiles/settings/settings_premium.cpp +++ b/Telegram/SourceFiles/settings/settings_premium.cpp @@ -41,6 +41,7 @@ For license and copyright information please follow this link: #include "ui/text/format_values.h" #include "ui/text/text_utilities.h" #include "ui/text/text_utilities.h" +#include "ui/toast/toast.h" #include "ui/widgets/checkbox.h" // Ui::RadiobuttonGroup. #include "ui/widgets/gradient_round_button.h" #include "ui/widgets/labels.h" @@ -518,10 +519,10 @@ TopBarUser::TopBarUser( not_null controller, not_null peer, rpl::producer<> showFinished) -: TopBarAbstract(parent) +: TopBarAbstract(parent, st::userPremiumCover) , _content(this) , _title(_content, st::settingsPremiumUserTitle) -, _about(_content, st::settingsPremiumUserAbout) +, _about(_content, st::userPremiumCover.about) , _ministars(_content) , _smallTop({ .widget = object_ptr(this), @@ -1173,6 +1174,7 @@ QPointer Premium::createPinnedToTop( }; return Ui::CreateChild( parent.get(), + st::defaultPremiumCover, clickContextOther, std::move(title), std::move(about)); @@ -1371,6 +1373,24 @@ QPointer Premium::createPinnedToBottom( } // namespace +template <> +struct SectionFactory : AbstractSectionFactory { + object_ptr create( + not_null parent, + not_null controller + ) const final override { + return object_ptr(parent, controller); + } + bool hasCustomTopBar() const final override { + return true; + } + + [[nodiscard]] static const std::shared_ptr &Instance() { + static const auto result = std::make_shared(); + return result; + } +}; + Type PremiumId() { return Premium::Id(); } @@ -1447,6 +1467,36 @@ QString LookupPremiumRef(PremiumPreview section) { return QString(); } +void ShowPremiumPromoToast( + std::shared_ptr show, + TextWithEntities textWithLink, + const QString &ref) { + using WeakToast = base::weak_ptr; + const auto toast = std::make_shared(); + (*toast) = show->showToast({ + .text = std::move(textWithLink), + .st = &st::defaultMultilineToast, + .duration = Ui::Toast::kDefaultDuration * 2, + .multiline = true, + .filter = crl::guard(&show->session(), [=]( + const ClickHandlerPtr &, + Qt::MouseButton button) { + if (button == Qt::LeftButton) { + if (const auto strong = toast->get()) { + strong->hideAnimated(); + (*toast) = nullptr; + if (const auto controller = show->resolveWindow( + ChatHelpers::WindowUsage::PremiumPromo)) { + Settings::ShowPremium(controller, ref); + } + return true; + } + } + return false; + }), + }); +} + not_null CreateSubscribeButton( SubscribeButtonArgs &&args) { Expects(args.show || args.controller); diff --git a/Telegram/SourceFiles/settings/settings_premium.h b/Telegram/SourceFiles/settings/settings_premium.h index e237fbd7bfb073..804219ee5bc66f 100644 --- a/Telegram/SourceFiles/settings/settings_premium.h +++ b/Telegram/SourceFiles/settings/settings_premium.h @@ -51,6 +51,11 @@ void StartPremiumPayment( [[nodiscard]] QString LookupPremiumRef(PremiumPreview section); +void ShowPremiumPromoToast( + std::shared_ptr show, + TextWithEntities textWithLink, + const QString &ref); + struct SubscribeButtonArgs final { Window::SessionController *controller = nullptr; not_null parent; diff --git a/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp b/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp index e6cd5bea8c6baf..d48ee32d09371f 100644 --- a/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp +++ b/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp @@ -839,7 +839,9 @@ ForwardsPrivacyController::ForwardsPrivacyController( not_null controller) : SimpleElementDelegate(controller, [] {}) , _controller(controller) -, _chatStyle(std::make_unique()) { +, _chatStyle( + std::make_unique( + controller->session().colorIndicesValue())) { _chatStyle->apply(controller->defaultChatTheme().get()); } diff --git a/Telegram/SourceFiles/settings/settings_scale_preview.cpp b/Telegram/SourceFiles/settings/settings_scale_preview.cpp index 3c45341cb38e9c..eaf2231f3ffd03 100644 --- a/Telegram/SourceFiles/settings/settings_scale_preview.cpp +++ b/Telegram/SourceFiles/settings/settings_scale_preview.cpp @@ -13,6 +13,7 @@ For license and copyright information please follow this link: #include "data/data_peer_values.h" #include "history/history_item_components.h" #include "main/main_session.h" +#include "ui/chat/chat_style.h" #include "ui/chat/chat_theme.h" #include "ui/image/image_prepare.h" #include "ui/platform/ui_platform_utility.h" @@ -103,7 +104,7 @@ class Preview final { int _bubbleShadow = 0; int _localShiftLeft = 0; QImage _bubbleTail; - QRect _replyBar; + QRect _replyRect; QRect _name; QRect _reply; QRect _message; @@ -329,14 +330,7 @@ style::font Preview::scaled(const style::font &font, int size) const { style::QuoteStyle Preview::scaled(const style::QuoteStyle &value) const { return { - .padding = scaled(value.padding), - .verticalSkip = scaled(value.verticalSkip), - .header = scaled(value.header), - .headerPosition = scaled(value.headerPosition), .icon = value.icon, - .iconPosition = scaled(value.iconPosition), - .outline = scaled(value.outline), - .radius = scaled(value.radius), .scrollable = value.scrollable, }; } @@ -347,7 +341,6 @@ style::TextStyle Preview::scaled( return { .font = scaled(value.font, fontSize), .linkUnderline = value.linkUnderline, - .lineHeight = scaled(value.lineHeight), .blockquote = scaled(value.blockquote), .pre = scaled(value.pre), }; @@ -366,8 +359,22 @@ void Preview::updateToScale(int scale) { return; } _scale = scale; - _nameStyle = scaled(_nameStyle, 13); - _textStyle = scaled(_textStyle, 13); + _nameStyle = scaled(st::fwdTextStyle, 13); + _textStyle = scaled(st::messageTextStyle, 13); + _textStyle.blockquote.verticalSkip = scaled(4); + _textStyle.blockquote.outline = scaled(3); + _textStyle.blockquote.outlineShift = scaled(2); + _textStyle.blockquote.radius = scaled(5); + _textStyle.blockquote.padding = scaled(QMargins{ 10, 2, 20, 2 }); + _textStyle.blockquote.iconPosition = scaled(QPoint{ 4, 4 }); + _textStyle.pre.verticalSkip = scaled(4); + _textStyle.pre.outline = scaled(3); + _textStyle.pre.outlineShift = scaled(2); + _textStyle.pre.radius = scaled(5); + _textStyle.pre.header = scaled(20); + _textStyle.pre.headerPosition = scaled(QPoint{ 10, 2 }); + _textStyle.pre.padding = scaled(QMargins{ 10, 2, 4, 2 }); + _textStyle.pre.iconPosition = scaled(QPoint{ 4, 2 }); _nameText.setText( _nameStyle, u"Bob Harris"_q, @@ -381,21 +388,18 @@ void Preview::updateToScale(int scale) { u"Do you know what time it is?"_q, Ui::ItemTextDefaultOptions()); - _replyBar = QRect( - scaled(1), // st::msgReplyBarPos.x(), - scaled(6) + 0,// st::msgReplyPadding.top() + st::msgReplyBarPos.y(), - scaled(2), //st::msgReplyBarSize.width(), - scaled(36)); // st::msgReplyBarSize.height(), const auto namePosition = QPoint( - scaled(10), // st::msgReplyBarSkip - scaled(6)); // st::msgReplyPadding.top() + scaled(11), // st::historyReplyPadding.left() + scaled(2)); // st::historyReplyPadding.top() const auto replyPosition = QPoint( - scaled(10), // st::msgReplyBarSkip - scaled(6) + _nameStyle.font->height); // st::msgReplyPadding.top() + scaled(11), // st::historyReplyPadding.left() + (scaled(2) // st::historyReplyPadding.top() + + _nameStyle.font->height)); // + st::msgServiceNameFont->height + const auto paddingRight = scaled(6); // st::historyReplyPadding.right() const auto wantedWidth = std::max({ - namePosition.x() + _nameText.maxWidth(), - replyPosition.x() + _replyText.maxWidth(), + namePosition.x() + _nameText.maxWidth() + paddingRight, + replyPosition.x() + _replyText.maxWidth() + paddingRight, _messageText.maxWidth(), }); @@ -409,16 +413,25 @@ void Preview::updateToScale(int scale) { _messageText.countHeight(maxTextWidth), kMaxTextLines * _textStyle.font->height); + _replyRect = QRect( + 0, // st::msgReplyBarPos.x(), + scaled(2),// st::historyReplyTop + messageWidth, + (scaled(2) // st::historyReplyPadding.top() + + _nameStyle.font->height // + st::msgServiceNameFont->height + + _textStyle.font->height // + st::normalFont->height + + scaled(2))); // + st::historyReplyPadding.bottom() + _name = QRect( - namePosition, + _replyRect.topLeft() + namePosition, QSize(messageWidth - namePosition.x(), _nameStyle.font->height)); _reply = QRect( - replyPosition, + _replyRect.topLeft() + replyPosition, QSize(messageWidth - replyPosition.x(), _textStyle.font->height)); _message = QRect(0, 0, messageWidth, messageHeight); - // replyBar.bottom + st::msgReplyPadding.bottom(); - const auto replySkip = _replyBar.y() + _replyBar.height() + scaled(6); + // replyRect.bottom + st::historyReplyBottom; + const auto replySkip = _replyRect.y() + _replyRect.height() + scaled(2); _message.moveTop(replySkip); _content = QRect(0, 0, messageWidth, replySkip + messageHeight); @@ -676,9 +689,30 @@ void Preview::paintContent(Painter &p, QRect clip) { } void Preview::paintReply(Painter &p, QRect clip) { - p.setOpacity(HistoryMessageReply::kBarAlpha); - p.fillRect(_replyBar, st::msgInReplyBarColor); + { + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + p.setBrush(st::msgInReplyBarColor); + + const auto outline = _textStyle.blockquote.outline; + const auto radius = _textStyle.blockquote.radius; + p.setOpacity(Ui::kDefaultOutline1Opacity); + p.setClipRect( + _replyRect.x(), + _replyRect.y(), + outline, + _replyRect.height()); + p.drawRoundedRect(_replyRect, radius, radius); + p.setOpacity(Ui::kDefaultBgOpacity); + p.setClipRect( + _replyRect.x() + outline, + _replyRect.y(), + _replyRect.width() - outline, + _replyRect.height()); + p.drawRoundedRect(_replyRect, radius, radius); + } p.setOpacity(1.); + p.setClipping(false); p.setPen(st::msgInServiceFg); _nameText.drawLeftElided( diff --git a/Telegram/SourceFiles/storage/storage_account.cpp b/Telegram/SourceFiles/storage/storage_account.cpp index a62de4367c1697..57c30b1b6d0c83 100644 --- a/Telegram/SourceFiles/storage/storage_account.cpp +++ b/Telegram/SourceFiles/storage/storage_account.cpp @@ -59,6 +59,7 @@ constexpr auto kMultiDraftTagOld = quint64(0xFFFF'FFFF'FFFF'FF01ULL); constexpr auto kMultiDraftCursorsTagOld = quint64(0xFFFF'FFFF'FFFF'FF02ULL); constexpr auto kMultiDraftTag = quint64(0xFFFF'FFFF'FFFF'FF03ULL); constexpr auto kMultiDraftCursorsTag = quint64(0xFFFF'FFFF'FFFF'FF04ULL); +constexpr auto kRichDraftsTag = quint64(0xFFFF'FFFF'FFFF'FF05ULL); enum { // Local Storage Keys lskUserMap = 0x00, @@ -1041,22 +1042,22 @@ void EnumerateDrafts( } callback( key, - draft->msgId, + draft->reply, draft->textWithTags, - draft->previewState, + draft->webpage, draft->cursor); } for (const auto &[key, source] : sources) { const auto draft = source.draft(); const auto cursor = source.cursor(); - if (draft.msgId + if (draft.reply.messageId || !draft.textWithTags.text.isEmpty() || cursor != MessageCursor()) { callback( key, - draft.msgId, + draft.reply, draft.textWithTags, - draft.previewState, + draft.webpage, cursor); } } @@ -1119,14 +1120,20 @@ void Account::writeDrafts(not_null history) { auto size = int(sizeof(quint64) * 2 + sizeof(quint32)); const auto sizeCallback = [&]( auto&&, // key - MsgId, // msgId + const FullReplyTo &reply, const TextWithTags &text, - Data::PreviewState, + const Data::WebPageDraft &webpage, auto&&) { // cursor size += sizeof(qint64) // key + Serialize::stringSize(text.text) - + sizeof(qint64) + TextUtilities::SerializeTagsSize(text.tags) - + sizeof(qint64) + sizeof(qint32); // msgId, previewState + + TextUtilities::SerializeTagsSize(text.tags) + + sizeof(qint64) + sizeof(qint64) // messageId + + Serialize::stringSize(webpage.url) + + sizeof(qint32) // webpage.forceLargeMedia + + sizeof(qint32) // webpage.forceSmallMedia + + sizeof(qint32) // webpage.invert + + sizeof(qint32) // webpage.manual + + sizeof(qint32); // webpage.removed }; EnumerateDrafts( map, @@ -1136,22 +1143,28 @@ void Account::writeDrafts(not_null history) { EncryptedDescriptor data(size); data.stream - << quint64(kMultiDraftTag) + << quint64(kRichDraftsTag) << SerializePeerId(peerId) << quint32(count); const auto writeCallback = [&]( const Data::DraftKey &key, - MsgId msgId, + const FullReplyTo &reply, const TextWithTags &text, - Data::PreviewState previewState, + const Data::WebPageDraft &webpage, auto&&) { // cursor data.stream << key.serialize() << text.text << TextUtilities::SerializeTags(text.tags) - << qint64(msgId.bare) - << qint32(previewState); + << qint64(reply.messageId.peer.value) + << qint64(reply.messageId.msg.bare) + << webpage.url + << qint32(webpage.forceLargeMedia ? 1 : 0) + << qint32(webpage.forceSmallMedia ? 1 : 0) + << qint32(webpage.invert ? 1 : 0) + << qint32(webpage.manual ? 1 : 0) + << qint32(webpage.removed ? 1 : 0); }; EnumerateDrafts( map, @@ -1201,9 +1214,9 @@ void Account::writeDraftCursors(not_null history) { const auto writeCallback = [&]( const Data::DraftKey &key, - MsgId, // msgId + auto&&, // reply auto&&, // text - Data::PreviewState, + auto&&, // webpage const MessageCursor &cursor) { // cursor data.stream << key.serialize() @@ -1343,7 +1356,9 @@ void Account::readDraftsWithCursors(not_null history) { quint64 tag = 0; draft.stream >> tag; - if (tag != kMultiDraftTag && tag != kMultiDraftTagOld) { + if (tag != kRichDraftsTag + && tag != kMultiDraftTag + && tag != kMultiDraftTagOld) { readDraftsWithCursorsLegacy(history, draft, tag); return; } @@ -1359,40 +1374,77 @@ void Account::readDraftsWithCursors(not_null history) { } auto map = Data::HistoryDrafts(); const auto keysOld = (tag == kMultiDraftTagOld); + const auto rich = (tag == kRichDraftsTag); for (auto i = 0; i != count; ++i) { - TextWithTags data; - QByteArray tagsSerialized; - qint64 keyValue = 0, messageId = 0; - qint32 keyValueOld = 0, uncheckedPreviewState = 0; + TextWithTags text; + QByteArray textTagsSerialized; + qint64 keyValue = 0; + qint64 messageIdPeer = 0, messageIdMsg = 0; + qint32 keyValueOld = 0; + QString webpageUrl; + qint32 webpageForceLargeMedia = 0; + qint32 webpageForceSmallMedia = 0; + qint32 webpageInvert = 0; + qint32 webpageManual = 0; + qint32 webpageRemoved = 0; if (keysOld) { draft.stream >> keyValueOld; } else { draft.stream >> keyValue; } - draft.stream - >> data.text - >> tagsSerialized - >> messageId - >> uncheckedPreviewState; - data.tags = TextUtilities::DeserializeTags( - tagsSerialized, - data.text.size()); - auto previewState = Data::PreviewState::Allowed; - switch (static_cast(uncheckedPreviewState)) { - case Data::PreviewState::Cancelled: - case Data::PreviewState::EmptyOnEdit: - previewState = Data::PreviewState(uncheckedPreviewState); - } + if (!rich) { + qint32 uncheckedPreviewState = 0; + draft.stream + >> text.text + >> textTagsSerialized + >> messageIdMsg + >> uncheckedPreviewState; + enum class PreviewState : char { + Allowed, + Cancelled, + EmptyOnEdit, + }; + if (uncheckedPreviewState == int(PreviewState::Cancelled)) { + webpageRemoved = 1; + } + messageIdPeer = peerId.value; + } else { + draft.stream + >> text.text + >> textTagsSerialized + >> messageIdPeer + >> messageIdMsg + >> webpageUrl + >> webpageForceLargeMedia + >> webpageForceSmallMedia + >> webpageInvert + >> webpageManual + >> webpageRemoved; + } + text.tags = TextUtilities::DeserializeTags( + textTagsSerialized, + text.text.size()); const auto key = keysOld ? Data::DraftKey::FromSerializedOld(keyValueOld) : Data::DraftKey::FromSerialized(keyValue); if (key && !key.isCloud()) { map.emplace(key, std::make_unique( - data, - messageId, - key.topicRootId(), + text, + FullReplyTo{ + .messageId = FullMsgId( + PeerId(messageIdPeer), + MsgId(messageIdMsg)), + .topicRootId = key.topicRootId(), + }, MessageCursor(), - previewState)); + Data::WebPageDraft{ + .url = webpageUrl, + .forceLargeMedia = (webpageForceLargeMedia == 1), + .forceSmallMedia = (webpageForceSmallMedia == 1), + .invert = (webpageInvert == 1), + .manual = (webpageManual == 1), + .removed = (webpageRemoved == 1), + })); } } if (draft.stream.status() != QDataStream::Ok) { @@ -1455,24 +1507,22 @@ void Account::readDraftsWithCursorsLegacy( Data::DraftKey::Local(topicRootId), std::make_unique( msgData, - msgReplyTo, - topicRootId, + FullReplyTo{ FullMsgId(peerId, MsgId(msgReplyTo)) }, MessageCursor(), - (msgPreviewCancelled - ? Data::PreviewState::Cancelled - : Data::PreviewState::Allowed))); + Data::WebPageDraft{ + .removed = (msgPreviewCancelled == 1), + })); } if (editMsgId) { map.emplace( Data::DraftKey::LocalEdit(topicRootId), std::make_unique( editData, - editMsgId, - topicRootId, + FullReplyTo{ FullMsgId(peerId, editMsgId) }, MessageCursor(), - (editPreviewCancelled - ? Data::PreviewState::Cancelled - : Data::PreviewState::Allowed))); + Data::WebPageDraft{ + .removed = (editPreviewCancelled == 1), + })); } readDraftCursors(peerId, map); history->setDraftsMap(std::move(map)); diff --git a/Telegram/SourceFiles/storage/storage_account.h b/Telegram/SourceFiles/storage/storage_account.h index ecabbe2b3b63ce..4de23386a56652 100644 --- a/Telegram/SourceFiles/storage/storage_account.h +++ b/Telegram/SourceFiles/storage/storage_account.h @@ -51,9 +51,9 @@ using FileKey = quint64; enum class StartResult : uchar; struct MessageDraft { - MsgId msgId = 0; + FullReplyTo reply; TextWithTags textWithTags; - Data::PreviewState previewState = Data::PreviewState::Allowed; + Data::WebPageDraft webpage; }; struct MessageDraftSource { diff --git a/Telegram/SourceFiles/support/support_autocomplete.cpp b/Telegram/SourceFiles/support/support_autocomplete.cpp index e526024bdf9c7f..ab10bb67e1882e 100644 --- a/Telegram/SourceFiles/support/support_autocomplete.cpp +++ b/Telegram/SourceFiles/support/support_autocomplete.cpp @@ -506,7 +506,8 @@ ConfirmContactBox::ConfirmContactBox( const Contact &data, Fn submit) : SimpleElementDelegate(controller, [=] { update(); }) -, _chatStyle(std::make_unique()) +, _chatStyle(std::make_unique( + history->session().colorIndicesValue())) , _comment(GenerateCommentItem(this, history, data)) , _contact(GenerateContactItem(this, history, data)) , _submit(submit) { diff --git a/Telegram/SourceFiles/support/support_helper.cpp b/Telegram/SourceFiles/support/support_helper.cpp index 19b9383064161a..1e92ecfd2e491b 100644 --- a/Telegram/SourceFiles/support/support_helper.cpp +++ b/Telegram/SourceFiles/support/support_helper.cpp @@ -159,10 +159,9 @@ Data::Draft OccupiedDraft(const QString &normalizedName) { + QString::number(OccupationTag()) + ";n:" + normalizedName }, - MsgId(0), // replyTo - kTopicRootId, + FullReplyTo(), MessageCursor(), - Data::PreviewState::Allowed + Data::WebPageDraft() }; } diff --git a/Telegram/SourceFiles/ui/boxes/boost_box.cpp b/Telegram/SourceFiles/ui/boxes/boost_box.cpp index abae42f218f2e4..408b54c4f9cdd7 100644 --- a/Telegram/SourceFiles/ui/boxes/boost_box.cpp +++ b/Telegram/SourceFiles/ui/boxes/boost_box.cpp @@ -16,8 +16,9 @@ For license and copyright information please follow this link: #include "styles/style_layers.h" #include "styles/style_premium.h" +#include + namespace Ui { -namespace { void StartFireworks(not_null parent) { const auto result = Ui::CreateChild(parent.get()); @@ -37,8 +38,6 @@ void StartFireworks(not_null parent) { }, lifetime); } -} // namespace - void BoostBox( not_null box, BoostBoxData data, @@ -60,7 +59,7 @@ void BoostBox( BoxShowFinishes(box), state->you.value(), box->verticalLayout(), - data, + data.boost, st::boxRowPadding); box->addTopButton(st::boxTitleClose, [=] { box->closeBox(); }); @@ -173,31 +172,186 @@ void BoostBox( }, button->lifetime()); } +object_ptr MakeLinkLabel( + not_null parent, + rpl::producer text, + rpl::producer link, + std::shared_ptr show, + object_ptr right) { + auto result = object_ptr(parent); + const auto raw = result.data(); + + const auto rawRight = right.release(); + if (rawRight) { + rawRight->setParent(raw); + rawRight->show(); + } + + struct State { + State( + not_null parent, + rpl::producer value, + rpl::producer link) + : text(std::move(value)) + , link(std::move(link)) + , label(parent, text.value(), st::giveawayGiftCodeLink) + , bg(st::roundRadiusLarge, st::windowBgOver) { + } + + rpl::variable text; + rpl::variable link; + Ui::FlatLabel label; + Ui::RoundRect bg; + }; + + const auto state = raw->lifetime().make_state( + raw, + rpl::duplicate(text), + std::move(link)); + state->label.setSelectable(true); + + rpl::combine( + raw->widthValue(), + std::move(text) + ) | rpl::start_with_next([=](int outer, const auto&) { + const auto textWidth = state->label.textMaxWidth(); + const auto skipLeft = st::giveawayGiftCodeLink.margin.left(); + const auto skipRight = rawRight + ? rawRight->width() + : st::giveawayGiftCodeLink.margin.right(); + const auto available = outer - skipRight - skipLeft; + const auto use = std::min(textWidth, available); + state->label.resizeToWidth(use); + const auto forCenter = (outer - use) / 2; + const auto x = (forCenter < skipLeft) + ? skipLeft + : (forCenter > outer - skipRight - use) + ? (outer - skipRight - use) + : forCenter; + state->label.moveToLeft(x, st::giveawayGiftCodeLink.margin.top()); + }, raw->lifetime()); + + raw->paintRequest() | rpl::start_with_next([=] { + auto p = QPainter(raw); + state->bg.paint(p, raw->rect()); + }, raw->lifetime()); + + state->label.setAttribute(Qt::WA_TransparentForMouseEvents); + + raw->resize(raw->width(), st::giveawayGiftCodeLinkHeight); + if (rawRight) { + raw->widthValue() | rpl::start_with_next([=](int width) { + rawRight->move(width - rawRight->width(), 0); + }, raw->lifetime()); + } + raw->setClickedCallback([=] { + QGuiApplication::clipboard()->setText(state->link.current()); + show->showToast(tr::lng_username_copied(tr::now)); + }); + + return result; +} + +void AskBoostBox( + not_null box, + AskBoostBoxData data, + Fn openStatistics, + Fn startGiveaway) { + box->setWidth(st::boxWideWidth); + box->setStyle(st::boostBox); + + struct State { + rpl::variable you = false; + bool submitted = false; + }; + const auto state = box->lifetime().make_state(State{ + .you = data.boost.mine, + }); + + FillBoostLimit( + BoxShowFinishes(box), + state->you.value(), + box->verticalLayout(), + data.boost, + st::boxRowPadding); + + box->addTopButton(st::boxTitleClose, [=] { box->closeBox(); }); + + auto title = tr::lng_boost_channel_title_color(); + auto text = rpl::combine( + tr::lng_boost_channel_needs_level_color( + lt_count, + rpl::single(float64(data.requiredLevel)), + Ui::Text::RichLangValue), + tr::lng_boost_channel_ask(Ui::Text::RichLangValue) + ) | rpl::map([](TextWithEntities &&text, TextWithEntities &&ask) { + return text.append(u"\n\n"_q).append(std::move(ask)); + }); + box->addRow( + object_ptr( + box, + std::move(title), + st::boostTitle), + st::boxRowPadding + QMargins(0, st::boostTitleSkip, 0, 0)); + box->addRow( + object_ptr( + box, + std::move(text), + st::boostText), + (st::boxRowPadding + + QMargins(0, st::boostTextSkip, 0, st::boostBottomSkip))); + + auto stats = object_ptr(box, st::boostLinkStatsButton); + stats->setClickedCallback(openStatistics); + box->addRow(MakeLinkLabel( + box, + rpl::single(data.link), + rpl::single(data.link), + box->uiShow(), + std::move(stats))); + + auto submit = tr::lng_boost_channel_ask_button(); + const auto button = box->addButton(rpl::duplicate(submit), [=] { + QGuiApplication::clipboard()->setText(data.link); + box->uiShow()->showToast(tr::lng_username_copied(tr::now)); + }); + rpl::combine( + std::move(submit), + box->widthValue() + ) | rpl::start_with_next([=](const QString &, int width) { + const auto &padding = st::boostBox.buttonPadding; + button->resizeToWidth(width + - padding.left() + - padding.right()); + button->moveToLeft(padding.left(), button->y()); + }, button->lifetime()); +} + void FillBoostLimit( rpl::producer<> showFinished, rpl::producer you, not_null container, - BoostBoxData data, + BoostCounters data, style::margins limitLinePadding) { - const auto full = !data.boost.nextLevelBoosts; + const auto full = !data.nextLevelBoosts; - if (data.boost.mine && data.boost.boosts > 0) { - --data.boost.boosts; + if (data.mine && data.boosts > 0) { + --data.boosts; } if (full) { - data.boost.nextLevelBoosts = data.boost.boosts - + (data.boost.mine ? 1 : 0); - data.boost.thisLevelBoosts = 0; - if (data.boost.level > 0) { - --data.boost.level; + data.nextLevelBoosts = data.boosts + + (data.mine ? 1 : 0); + data.thisLevelBoosts = 0; + if (data.level > 0) { + --data.level; } - } else if (data.boost.mine - && data.boost.level > 0 - && data.boost.boosts < data.boost.thisLevelBoosts) { - --data.boost.level; - data.boost.nextLevelBoosts = data.boost.thisLevelBoosts; - data.boost.thisLevelBoosts = 0; + } else if (data.mine + && data.level > 0 + && data.boosts < data.thisLevelBoosts) { + --data.level; + data.nextLevelBoosts = data.thisLevelBoosts; + data.thisLevelBoosts = 0; } const auto addSkip = [&](int skip) { @@ -208,18 +362,18 @@ void FillBoostLimit( const auto levelWidth = [&](int add) { return st::normalFont->width( - tr::lng_boost_level(tr::now, lt_count, data.boost.level + add)); + tr::lng_boost_level(tr::now, lt_count, data.level + add)); }; const auto paddings = 2 * st::premiumLineTextSkip; const auto labelLeftWidth = paddings + levelWidth(0); const auto labelRightWidth = paddings + levelWidth(1); const auto ratio = [=](int boosts) { const auto min = std::min( - data.boost.boosts, - data.boost.thisLevelBoosts); + data.boosts, + data.thisLevelBoosts); const auto max = std::max({ - data.boost.boosts, - data.boost.nextLevelBoosts, + data.boosts, + data.nextLevelBoosts, 1, }); Assert(boosts >= min && boosts <= max); @@ -242,12 +396,12 @@ void FillBoostLimit( return (first + (index - 1) * other) / available; }; - const auto min = std::min(data.boost.boosts, data.boost.thisLevelBoosts); - const auto now = data.boost.boosts; - const auto max = (data.boost.nextLevelBoosts > min) - ? (data.boost.nextLevelBoosts) - : (data.boost.boosts > 0) - ? data.boost.boosts + const auto min = std::min(data.boosts, data.thisLevelBoosts); + const auto now = data.boosts; + const auto max = (data.nextLevelBoosts > min) + ? (data.nextLevelBoosts) + : (data.boosts > 0) + ? data.boosts : 1; auto bubbleRowState = ( std::move(you) @@ -283,8 +437,8 @@ void FillBoostLimit( container, st::boostLimits, Premium::LimitRowLabels{ - .leftLabel = level(data.boost.level), - .rightLabel = level(data.boost.level + 1), + .leftLabel = level(data.level), + .rightLabel = level(data.level + 1), .dynamic = true, }, std::move(ratioValue), diff --git a/Telegram/SourceFiles/ui/boxes/boost_box.h b/Telegram/SourceFiles/ui/boxes/boost_box.h index e35178fac7793b..da8d57572e61f1 100644 --- a/Telegram/SourceFiles/ui/boxes/boost_box.h +++ b/Telegram/SourceFiles/ui/boxes/boost_box.h @@ -7,8 +7,14 @@ For license and copyright information please follow this link: */ #pragma once +#include "base/object_ptr.h" + namespace Ui { +void StartFireworks(not_null parent); + +class Show; +class RpWidget; class GenericBox; class VerticalLayout; @@ -30,11 +36,30 @@ void BoostBox( BoostBoxData data, Fn)> boost); +struct AskBoostBoxData { + QString link; + BoostCounters boost; + int requiredLevel = 0; +}; + +void AskBoostBox( + not_null box, + AskBoostBoxData data, + Fn openStatistics, + Fn startGiveaway); + +[[nodiscard]] object_ptr MakeLinkLabel( + not_null parent, + rpl::producer text, + rpl::producer link, + std::shared_ptr show, + object_ptr right); + void FillBoostLimit( rpl::producer<> showFinished, rpl::producer you, not_null container, - BoostBoxData data, + BoostCounters data, style::margins limitLinePadding); } // namespace Ui diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp index fc56add1fde1cf..008b907a960a61 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp @@ -323,7 +323,7 @@ Panel::Panel( , _menuButtons(menuButtons) , _widget(std::make_unique()) , _allowClipboardRead(allowClipboardRead) { - _widget->setInnerSize(st::paymentsPanelSize); + _widget->setInnerSize(st::botWebViewPanelSize); _widget->setWindowFlag(Qt::WindowStaysOnTopHint, false); _widget->closeRequests( @@ -522,7 +522,7 @@ bool Panel::showWebview( _webviewBottom->resize(_webviewBottom->width(), height); } _widget->setMenuAllowed([=](const Ui::Menu::MenuCallback &callback) { - if (_menuButtons & MenuButton::Settings) { + if (_hasSettingsButton) { callback(tr::lng_bot_settings(tr::now), [=] { postEvent("settings_button_pressed"); }, &st::menuIconSettings); @@ -628,6 +628,8 @@ bool Panel::createWebview(const Webview::ThemeParams ¶ms) { processMainButtonMessage(arguments); } else if (command == "web_app_setup_back_button") { processBackButtonMessage(arguments); + } else if (command == "web_app_setup_settings_button") { + processSettingsButtonMessage(arguments); } else if (command == "web_app_request_theme") { _themeUpdateForced.fire({}); } else if (command == "web_app_request_viewport") { @@ -656,7 +658,7 @@ bool Panel::createWebview(const Webview::ThemeParams ¶ms) { }); raw->setNavigationStartHandler([=](const QString &uri, bool newWindow) { - if (_delegate->botHandleLocalUri(uri)) { + if (_delegate->botHandleLocalUri(uri, false)) { return false; } else if (newWindow) { return true; @@ -741,16 +743,17 @@ void Panel::switchInlineQueryMessage(const QJsonObject &args) { void Panel::openTgLink(const QJsonObject &args) { if (args.isEmpty()) { + LOG(("BotWebView Error: Bad arguments in 'web_app_open_tg_link'.")); _delegate->botClose(); return; } const auto path = args["path_full"].toString(); if (path.isEmpty()) { - LOG(("BotWebView Error: Bad 'path_full' in openTgLink.")); + LOG(("BotWebView Error: Bad 'path_full' in 'web_app_open_tg_link'.")); _delegate->botClose(); return; } - _delegate->botHandleLocalUri("https://t.me" + path); + _delegate->botHandleLocalUri("https://t.me" + path, true); } void Panel::openExternalLink(const QJsonObject &args) { @@ -1104,6 +1107,10 @@ void Panel::processBackButtonMessage(const QJsonObject &args) { _widget->setBackAllowed(args["is_visible"].toBool()); } +void Panel::processSettingsButtonMessage(const QJsonObject &args) { + _hasSettingsButton = args["is_visible"].toBool(); +} + void Panel::processHeaderColor(const QJsonObject &args) { if (const auto color = ParseColor(args["color"].toString())) { _widget->overrideTitleColor(color); diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h index 9e5598a34ae4b1..ea9c554e432a06 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h +++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h @@ -37,10 +37,9 @@ struct MainButtonArgs { enum class MenuButton { None = 0x00, - Settings = 0x01, - OpenBot = 0x02, - RemoveFromMenu = 0x04, - RemoveFromMainMenu = 0x08, + OpenBot = 0x01, + RemoveFromMenu = 0x02, + RemoveFromMainMenu = 0x04, }; inline constexpr bool is_flag_type(MenuButton) { return true; } using MenuButtons = base::flags; @@ -55,7 +54,7 @@ struct CustomMethodRequest { class Delegate { public: virtual Webview::ThemeParams botThemeParams() = 0; - virtual bool botHandleLocalUri(QString uri) = 0; + virtual bool botHandleLocalUri(QString uri, bool keepOpen) = 0; virtual void botHandleInvoice(QString slug) = 0; virtual void botHandleMenuButton(MenuButton button) = 0; virtual void botSendData(QByteArray data) = 0; @@ -114,6 +113,7 @@ class Panel final : public base::has_weak_ptr { void switchInlineQueryMessage(const QJsonObject &args); void processMainButtonMessage(const QJsonObject &args); void processBackButtonMessage(const QJsonObject &args); + void processSettingsButtonMessage(const QJsonObject &args); void processHeaderColor(const QJsonObject &args); void openTgLink(const QJsonObject &args); void openExternalLink(const QJsonObject &args); @@ -145,6 +145,7 @@ class Panel final : public base::has_weak_ptr { QString _userDataPath; const not_null _delegate; bool _closeNeedConfirmation = false; + bool _hasSettingsButton = false; MenuButtons _menuButtons = {}; std::unique_ptr _widget; std::unique_ptr _webview; diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index a8696f664540ec..7abd07b013fdf4 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -22,11 +22,17 @@ msgDateFont: font(13px); msgMinWidth: 160px; msgPhotoSize: 33px; msgPhotoSkip: 40px; -msgPadding: margins(13px, 7px, 13px, 8px); +msgPadding: margins(10px, 8px, 10px, 8px); msgMargin: margins(16px, 6px, 56px, 2px); msgMarginTopAttached: 0px; msgShadow: 2px; +historyReplyTop: 2px; +historyReplyBottom: 2px; +historyReplyPreview: 32px; +historyReplyPreviewMargin: margins(7px, 4px, 4px, 4px); +historyReplyPadding: margins(11px, 2px, 6px, 2px); + msgReplyPadding: margins(6px, 6px, 11px, 6px); msgReplyBarPos: point(1px, 0px); msgReplyBarSize: size(2px, 36px); @@ -44,13 +50,14 @@ messageQuoteStyle: QuoteStyle(defaultQuoteStyle) { padding: margins(10px, 2px, 4px, 2px); verticalSkip: 4px; outline: 3px; + outlineShift: 2px; radius: 5px; } messageTextStyle: TextStyle(defaultTextStyle) { blockquote: QuoteStyle(messageQuoteStyle) { padding: margins(10px, 2px, 20px, 2px); icon: icon{{ "chat/mini_quote", windowFg }}; - iconPosition: point(4px, 2px); + iconPosition: point(4px, 4px); } pre: QuoteStyle(messageQuoteStyle) { header: 20px; @@ -60,6 +67,9 @@ messageTextStyle: TextStyle(defaultTextStyle) { iconPosition: point(4px, 2px); } } +historyPagePreview: QuoteStyle(messageQuoteStyle) { + padding: margins(10px, 5px, 7px, 7px); +} msgDateTextStyle: defaultTextStyle; serviceTextPalette: TextPalette(defaultTextPalette) { linkFg: msgServiceFg; @@ -557,8 +567,6 @@ historyPsaForwardPalette: TextPalette(defaultTextPalette) { linkFg: boxTextFgGood; } -webPageLeft: 10px; -webPageBar: 2px; webPageTitleFont: semiboldFont; webPageTitleStyle: semiboldTextStyle; webPageDescriptionFont: normalFont; @@ -617,10 +625,14 @@ historyPollOutChosenSelected: icon {{ "poll_select_check", historyFileOutIconFgS historyPollInChosen: icon {{ "poll_select_check", historyFileInIconFg }}; historyPollInChosenSelected: icon {{ "poll_select_check", historyFileInIconFgSelected }}; -historyViewButtonHeight: 42px; -historyViewButtonMargins: margins(13px, 5px, 13px, 5px); +historyViewButtonHeight: 48px; +historyViewButtonMargins: margins(10px, 5px, 10px, 10px); historyViewButtonTextStyle: semiboldTextStyle; +historyPageButtonLine: 1px; +historyPageButtonHeight: 36px; +historyPageButtonPadding: margins(13px, 8px, 13px, 8px); + historyCommentsButtonHeight: 40px; historyCommentsSkipLeft: 9px; historyCommentsSkipText: 10px; @@ -851,7 +863,7 @@ searchInChatPeerList: PeerList(defaultPeerList) { item: searchInChatPeerListItem; } -msgServiceGiftBoxSize: size(206px, 231px); // Plus msgServiceGiftBoxTopSkip. +msgServiceGiftBoxSize: size(236px, 231px); // Plus msgServiceGiftBoxTopSkip. msgServiceGiftBoxRadius: 20px; msgServiceGiftBoxTopSkip: 4px; msgServiceGiftBoxButtonHeight: 32px; @@ -931,3 +943,22 @@ storyMentionUnreadStrokeTwice: 6px; storyMentionReadSkipTwice: 7px; storyMentionReadStrokeTwice: 3px; storyMentionButtonSkip: 5px; + +chatGiveawayWidth: 292px; +chatGiveawayStickerTop: -16px; +chatGiveawayBadgeFont: font(12px bold); +chatGiveawayBadgeTop: 106px; +chatGiveawayBadgePadding: margins(7px, 1px, 5px, 3px); +chatGiveawayBadgeStroke: 2px; +chatGiveawayPrizesTop: 16px; +chatGiveawayPrizesSkip: 4px; +chatGiveawayParticipantsTop: 16px; +chatGiveawayParticipantsSkip: 4px; +chatGiveawayChannelTop: 6px; +chatGiveawayChannelSize: 32px; +chatGiveawayChannelPadding: margins(5px, 7px, 12px, 0px); +chatGiveawayChannelSkip: 8px; +chatGiveawayCountriesSkip: 16px; +chatGiveawayDateTop: 6px; +chatGiveawayDateSkip: 4px; +chatGiveawayBottomSkip: 16px; diff --git a/Telegram/SourceFiles/ui/chat/chat_style.cpp b/Telegram/SourceFiles/ui/chat/chat_style.cpp index a68f0fdec92098..bc7955a21bc8ea 100644 --- a/Telegram/SourceFiles/ui/chat/chat_style.cpp +++ b/Telegram/SourceFiles/ui/chat/chat_style.cpp @@ -9,6 +9,7 @@ For license and copyright information please follow this link: #include "ui/chat/chat_theme.h" #include "ui/image/image_prepare.h" // ImageRoundRadius +#include "ui/text/text_custom_emoji.h" #include "ui/color_contrast.h" #include "ui/painter.h" #include "ui/ui_utility.h" @@ -25,22 +26,21 @@ void EnsureCorners( const style::color &color, const style::color *shadow = nullptr) { if (corners.p[0].isNull()) { - corners = Ui::PrepareCornerPixmaps(radius, color, shadow); + corners = PrepareCornerPixmaps(radius, color, shadow); } } void EnsureBlockquoteCache( std::unique_ptr &cache, - const style::color &color) { + Fn values) { if (cache) { return; } cache = std::make_unique(); - cache->bg = color->c; - cache->bg.setAlphaF(0.12); - cache->outline = color->c; - cache->outline.setAlphaF(0.9); - cache->icon = cache->outline; + const auto &colors = values(); + cache->bg = colors.bg; + cache->outlines = colors.outlines; + cache->icon = colors.name; } void EnsurePreCache( @@ -54,14 +54,15 @@ void EnsurePreCache( const auto bg = bgOverride(); cache->bg = bg.value_or(color->c); if (!bg) { - cache->bg.setAlphaF(0.12); + cache->bg.setAlpha(kDefaultBgOpacity * 255); } - cache->outline = color->c; - cache->outline.setAlphaF(0.9); + cache->outlines[0] = color->c; + cache->outlines[0].setAlpha(kDefaultOutline1Opacity * 255); + cache->outlines[1] = cache->outlines[2] = QColor(0, 0, 0, 0); cache->header = color->c; - cache->header.setAlphaF(0.25); - cache->icon = cache->outline; - cache->icon.setAlphaF(0.6); + cache->header.setAlpha(kDefaultOutline2Opacity * 255); + cache->icon = cache->outlines[0]; + cache->icon.setAlpha(kDefaultOutline3Opacity * 255); } } // namespace @@ -74,6 +75,14 @@ not_null ChatPaintContext::imageStyle() const { return &st->imageStyle(selected()); } +not_null ChatPaintContext::quoteCache( + uint8 colorIndex) const { + return !outbg + ? st->coloredQuoteCache(selected(), colorIndex).get() + : messageStyle()->quoteCache[ + st->colorPatternIndex(colorIndex)].get(); +} + int HistoryServiceMsgRadius() { static const auto result = [] { const auto minMessageHeight = st::msgServicePadding.top() @@ -99,7 +108,50 @@ int HistoryServiceMsgInvertedShrink() { return result; } -ChatStyle::ChatStyle() { +ColorIndexValues SimpleColorIndexValues(QColor color, int patternIndex) { + auto bg = color; + bg.setAlpha(kDefaultBgOpacity * 255); + auto result = ColorIndexValues{ + .name = color, + .bg = bg, + }; + result.outlines[0] = color; + result.outlines[0].setAlpha(kDefaultOutline1Opacity * 255); + if (patternIndex > 1) { + result.outlines[1] = result.outlines[0]; + result.outlines[1].setAlpha(kDefaultOutline2Opacity * 255); + result.outlines[2] = result.outlines[0]; + result.outlines[2].setAlpha(kDefaultOutline3Opacity * 255); + } else if (patternIndex > 0) { + result.outlines[1] = result.outlines[0]; + result.outlines[1].setAlpha(kDefaultOutlineOpacitySecond * 255); + result.outlines[2] = QColor(0, 0, 0, 0); + } else { + result.outlines[1] = result.outlines[2] = QColor(0, 0, 0, 0); + } + return result; +} + +int BackgroundEmojiData::CacheIndex( + bool selected, + bool outbg, + bool inbubble, + uint8 colorIndexPlusOne) { + const auto base = colorIndexPlusOne + ? (colorIndexPlusOne - 1) + : (kColorIndexCount + (!inbubble ? 0 : outbg ? 1 : 2)); + return (base * 2) + (selected ? 1 : 0); +}; + +ChatStyle::ChatStyle(rpl::producer colorIndices) { + if (colorIndices) { + _colorIndicesLifetime = std::move( + colorIndices + ) | rpl::start_with_next([=](ColorIndicesCompressed &&indices) { + _colorIndices = std::move(indices); + }); + } + finalize(); make(_historyPsaForwardPalette, st::historyPsaForwardPalette); make(_imgReplyTextPalette, st::imgReplyTextPalette); @@ -453,17 +505,28 @@ ChatStyle::ChatStyle() { &MessageImageStyle::historyVideoMessageMute, st::historyVideoMessageMute, st::historyVideoMessageMuteSelected); + + updateDarkValue(); } ChatStyle::ChatStyle(not_null isolated) -: ChatStyle() { +: ChatStyle(rpl::producer()) { assignPalette(isolated); } +ChatStyle::~ChatStyle() = default; + void ChatStyle::apply(not_null theme) { applyCustomPalette(theme->palette()); } +void ChatStyle::updateDarkValue() { + const auto withBg = [&](const QColor &color) { + return CountContrast(windowBg()->c, color); + }; + _dark = (withBg({ 0, 0, 0 }) < withBg({ 255, 255, 255 })); +} + void ChatStyle::applyCustomPalette(const style::palette *palette) { assignPalette(palette ? palette : style::main_palette::get().get()); if (palette) { @@ -482,7 +545,7 @@ void ChatStyle::applyAdjustedServiceBg(QColor serviceBg) { msgServiceBg().set(uchar(r), uchar(g), uchar(b), uchar(a)); } -std::span ChatStyle::highlightColors() const { +std::span ChatStyle::highlightColors() const { if (_highlightColors.empty()) { const auto push = [&](const style::color &color) { _highlightColors.push_back({ &color->p, &color->p }); @@ -517,14 +580,41 @@ std::span ChatStyle::highlightColors() const { return _highlightColors; } +void ChatStyle::clearColorIndexCaches() { + for (auto &style : _messageStyles) { + for (auto &cache : style.quoteCache) { + cache = nullptr; + } + for (auto &cache : style.replyCache) { + cache = nullptr; + } + } + for (auto &values : _coloredValues) { + values.reset(); + } + for (auto &palette : _coloredTextPalettes) { + palette.linkFg.reset(); + } + for (auto &cache : _coloredReplyCaches) { + cache = nullptr; + } + for (auto &cache : _coloredQuoteCaches) { + cache = nullptr; + } +} + void ChatStyle::assignPalette(not_null palette) { *static_cast(this) = *palette; style::internal::resetIcons(); + + clearColorIndexCaches(); for (auto &style : _messageStyles) { style.msgBgCornersSmall = {}; style.msgBgCornersLarge = {}; - style.blockquoteCache = nullptr; style.preCache = nullptr; + style.textPalette.linkAlwaysActive + = style.semiboldPalette.linkAlwaysActive + = (style.textPalette.linkFg->c == style.historyTextFg->c); } for (auto &style : _imageStyles) { style.msgDateImgBgCorners = {}; @@ -540,12 +630,7 @@ void ChatStyle::assignPalette(not_null palette) { for (auto &corners : _msgSelectOverlayCorners) { corners = {}; } - - for (auto &stm : _messageStyles) { - stm.textPalette.linkAlwaysActive - = stm.semiboldPalette.linkAlwaysActive - = (stm.textPalette.linkFg->c == stm.historyTextFg->c); - } + updateDarkValue(); _paletteChanged.fire({}); } @@ -560,7 +645,7 @@ const CornersPixmaps &ChatStyle::serviceBgCornersNormal() const { const CornersPixmaps &ChatStyle::serviceBgCornersInverted() const { if (_serviceBgCornersInverted.p[0].isNull()) { - _serviceBgCornersInverted = Ui::PrepareInvertedCornerPixmaps( + _serviceBgCornersInverted = PrepareInvertedCornerPixmaps( HistoryServiceMsgInvertedRadius(), msgServiceBg()); } @@ -579,16 +664,19 @@ const MessageStyle &ChatStyle::messageStyle(bool outbg, bool selected) const { BubbleRadiusLarge(), result.msgBg, &result.msgShadow); - EnsureBlockquoteCache( - result.blockquoteCache, - result.msgReplyBarColor); + const auto &replyBar = result.msgReplyBarColor->c; + for (auto i = 0; i != kColorPatternsCount; ++i) { + EnsureBlockquoteCache( + result.replyCache[i], + [&] { return SimpleColorIndexValues(replyBar, i); }); + if (!result.quoteCache[i]) { + result.quoteCache[i] = std::make_unique( + *result.replyCache[i]); + } + } const auto preBgOverride = [&] { - const auto withBg = [&](const QColor &color) { - return Ui::CountContrast(windowBg()->c, color); - }; - const auto dark = (withBg({ 0, 0, 0 }) < withBg({ 255, 255, 255 })); - return dark ? QColor(0, 0, 0, 192) : std::optional(); + return _dark ? QColor(0, 0, 0, 192) : std::optional(); }; EnsurePreCache( result.preCache, @@ -621,9 +709,166 @@ const MessageImageStyle &ChatStyle::imageStyle(bool selected) const { result.msgShadowCornersLarge, BubbleRadiusLarge(), result.msgShadow); + + return result; +} + +int ChatStyle::colorPatternIndex(uint8 colorIndex) const { + Expects(colorIndex >= 0 && colorIndex < kColorIndexCount); + + if (!_colorIndices.colors + || colorIndex < kSimpleColorIndexCount) { + return 0; + } + auto &data = (*_colorIndices.colors)[colorIndex]; + auto &colors = _dark ? data.dark : data.light; + return colors[2] ? 2 : colors[1] ? 1 : 0; +} + +ColorIndexValues ChatStyle::computeColorIndexValues( + bool selected, + uint8 colorIndex) const { + if (!_colorIndices.colors) { + colorIndex %= kSimpleColorIndexCount; + } + if (colorIndex < kSimpleColorIndexCount) { + const auto list = std::array{ + &historyPeer1NameFg(), + &historyPeer2NameFg(), + &historyPeer3NameFg(), + &historyPeer4NameFg(), + &historyPeer5NameFg(), + &historyPeer6NameFg(), + &historyPeer7NameFg(), + &historyPeer8NameFg(), + }; + const auto listSelected = std::array{ + &historyPeer1NameFgSelected(), + &historyPeer2NameFgSelected(), + &historyPeer3NameFgSelected(), + &historyPeer4NameFgSelected(), + &historyPeer5NameFgSelected(), + &historyPeer6NameFgSelected(), + &historyPeer7NameFgSelected(), + &historyPeer8NameFgSelected(), + }; + const auto paletteIndex = ColorIndexToPaletteIndex(colorIndex); + auto result = ColorIndexValues{ + .name = (*(selected ? listSelected : list)[paletteIndex])->c, + }; + result.bg = result.name; + result.bg.setAlpha(kDefaultBgOpacity * 255); + result.outlines[0] = result.name; + result.outlines[0].setAlpha(kDefaultOutline1Opacity * 255); + result.outlines[1] = result.outlines[2] = QColor(0, 0, 0, 0); + return result; + } + auto &data = (*_colorIndices.colors)[colorIndex]; + auto &colors = _dark ? data.dark : data.light; + if (!colors[0]) { + return computeColorIndexValues( + selected, + colorIndex % kSimpleColorIndexCount); + } + const auto color = [&](int index) { + const auto v = colors[index]; + return v + ? QColor((v >> 16) & 0xFF, (v >> 8) & 0xFF, v & 0xFF) + : QColor(0, 0, 0, 0); + }; + auto result = ColorIndexValues{ + .outlines = { color(0), color(1), color(2) } + }; + result.bg = result.outlines[0]; + result.bg.setAlpha(kDefaultBgOpacity * 255); + result.name = result.outlines[0]; return result; } +not_null ChatStyle::serviceQuoteCache( + bool twoColored) const { + const auto index = (twoColored ? 1 : 0); + const auto &service = msgServiceFg()->c; + EnsureBlockquoteCache( + _serviceQuoteCache[index], + [&] { return SimpleColorIndexValues(service, twoColored); }); + return _serviceQuoteCache[index].get(); +} + +not_null ChatStyle::serviceReplyCache( + bool twoColored) const { + const auto index = (twoColored ? 1 : 0); + const auto &service = msgServiceFg()->c; + EnsureBlockquoteCache( + _serviceReplyCache[index], + [&] { return SimpleColorIndexValues(service, twoColored); }); + return _serviceReplyCache[index].get(); +} + +const ColorIndexValues &ChatStyle::coloredValues( + bool selected, + uint8 colorIndex) const { + Expects(colorIndex >= 0 && colorIndex < kColorIndexCount); + + const auto shift = (selected ? kColorIndexCount : 0); + auto &result = _coloredValues[shift + colorIndex]; + if (!result) { + result.emplace(computeColorIndexValues(selected, colorIndex)); + } + return *result; +} + +const style::TextPalette &ChatStyle::coloredTextPalette( + bool selected, + uint8 colorIndex) const { + Expects(colorIndex >= 0 && colorIndex < kColorIndexCount); + + const auto shift = (selected ? kColorIndexCount : 0); + auto &result = _coloredTextPalettes[shift + colorIndex]; + if (!result.linkFg) { + result.linkFg.emplace(coloredValues(selected, colorIndex).name); + make( + result.data, + (selected + ? st::inReplyTextPaletteSelected + : st::inReplyTextPalette)); + result.data.linkFg = result.linkFg->color(); + result.data.selectLinkFg = result.data.linkFg; + } + return result.data; +} + +not_null ChatStyle::backgroundEmojiData( + uint64 id) const { + return &_backgroundEmojis[id]; +} + +not_null ChatStyle::coloredQuoteCache( + bool selected, + uint8 colorIndex) const { + return coloredCache(_coloredQuoteCaches, selected, colorIndex); +} + +not_null ChatStyle::coloredReplyCache( + bool selected, + uint8 colorIndex) const { + return coloredCache(_coloredReplyCaches, selected, colorIndex); +} + +not_null ChatStyle::coloredCache( + ColoredQuotePaintCaches &caches, + bool selected, + uint8 colorIndex) const { + Expects(colorIndex >= 0 && colorIndex < kColorIndexCount); + + const auto shift = (selected ? kColorIndexCount : 0); + auto &cache = caches[shift + colorIndex]; + EnsureBlockquoteCache(cache, [&] { + return coloredValues(selected, colorIndex); + }); + return cache.get(); +} + const CornersPixmaps &ChatStyle::msgBotKbOverBgAddCornersSmall() const { EnsureCorners( _msgBotKbOverBgAddCornersSmall, @@ -751,6 +996,24 @@ void ChatStyle::make( make(imageSelected().*my, originalSelected); } +uint8 DecideColorIndex(uint64 id) { + return id % kSimpleColorIndexCount; +} + +uint8 ColorIndexToPaletteIndex(uint8 colorIndex) { + Expects(colorIndex >= 0 && colorIndex < kColorIndexCount); + + const int8 map[] = { 0, 7, 4, 1, 6, 3, 5 }; + return map[colorIndex % kSimpleColorIndexCount]; +} + +QColor FromNameFg( + not_null st, + bool selected, + uint8 colorIndex) { + return st->coloredValues(selected, colorIndex).name; +} + void FillComplexOverlayRect( QPainter &p, QRect rect, diff --git a/Telegram/SourceFiles/ui/chat/chat_style.h b/Telegram/SourceFiles/ui/chat/chat_style.h index 8fae58a36d5912..ade21f257a1668 100644 --- a/Telegram/SourceFiles/ui/chat/chat_style.h +++ b/Telegram/SourceFiles/ui/chat/chat_style.h @@ -21,12 +21,26 @@ struct TwoIconButton; struct ScrollArea; } // namespace style +namespace Ui::Text { +class CustomEmoji; +} // namespace Ui::Text + namespace Ui { class ChatTheme; class ChatStyle; struct BubblePattern; +inline constexpr auto kColorPatternsCount = Text::kMaxQuoteOutlines; +inline constexpr auto kColorIndexCount = uint8(1 << 6); +inline constexpr auto kSimpleColorIndexCount = uint8(7); + +inline constexpr auto kDefaultBgOpacity = 0.12; +inline constexpr auto kDefaultOutline1Opacity = 0.9; +inline constexpr auto kDefaultOutline2Opacity = 0.3; +inline constexpr auto kDefaultOutline3Opacity = 0.6; +inline constexpr auto kDefaultOutlineOpacitySecond = 0.5; + struct MessageStyle { CornersPixmaps msgBgCornersSmall; CornersPixmaps msgBgCornersLarge; @@ -76,9 +90,13 @@ struct MessageStyle { style::icon historyPollChoiceRight = { Qt::Uninitialized }; style::icon historyTranscribeIcon = { Qt::Uninitialized }; style::icon historyTranscribeHide = { Qt::Uninitialized }; - std::unique_ptr blockquoteCache; + std::array< + std::unique_ptr, + kColorPatternsCount> quoteCache; + std::array< + std::unique_ptr, + kColorPatternsCount> replyCache; std::unique_ptr preCache; - }; struct MessageImageStyle { @@ -107,6 +125,23 @@ struct ReactionPaintInfo { Fn effectPaint; }; +struct BackgroundEmojiCache { + QColor color; + std::array frames; +}; + +struct BackgroundEmojiData { + std::unique_ptr emoji; + QImage firstFrameMask; + std::array caches; + + [[nodiscard]] static int CacheIndex( + bool selected, + bool outbg, + bool inbubble, + uint8 colorIndexPlusOne); +}; + struct ChatPaintContext { not_null st; const BubblePattern *bubblesPattern = nullptr; @@ -114,8 +149,6 @@ struct ChatPaintContext { QRect viewport; QRect clip; TextSelection selection; - bool outbg = false; - bool paused = false; crl::time now = 0; void translate(int x, int y) { @@ -131,6 +164,8 @@ struct ChatPaintContext { } [[nodiscard]] not_null messageStyle() const; [[nodiscard]] not_null imageStyle() const; + [[nodiscard]] not_null quoteCache( + uint8 colorIndex) const; [[nodiscard]] ChatPaintContext translated(int x, int y) const { auto result = *this; @@ -155,22 +190,51 @@ struct ChatPaintContext { }; SkipDrawingParts skipDrawingParts = SkipDrawingParts::None; + bool outbg = false; + bool paused = false; + }; [[nodiscard]] int HistoryServiceMsgRadius(); [[nodiscard]] int HistoryServiceMsgInvertedRadius(); [[nodiscard]] int HistoryServiceMsgInvertedShrink(); +struct ColorIndexData { + std::array light = {}; + std::array dark = {}; + + friend inline bool operator==( + const ColorIndexData&, + const ColorIndexData&) = default; +}; + +struct ColorIndicesCompressed { + std::shared_ptr> colors; +}; + +struct ColorIndexValues { + std::array outlines; + QColor name; + QColor bg; +}; + class ChatStyle final : public style::palette { public: - ChatStyle(); + explicit ChatStyle(rpl::producer colorIndices); explicit ChatStyle(not_null isolated); + ChatStyle(const ChatStyle &other) = delete; + ChatStyle &operator=(const ChatStyle &other) = delete; + ~ChatStyle(); void apply(not_null theme); void applyCustomPalette(const style::palette *palette); void applyAdjustedServiceBg(QColor serviceBg); - [[nodiscard]] std::span highlightColors() const; + [[nodiscard]] bool dark() const { + return _dark; + } + + [[nodiscard]] std::span highlightColors() const; [[nodiscard]] rpl::producer<> paletteChanged() const { return _paletteChanged.events(); @@ -200,6 +264,32 @@ class ChatStyle final : public style::palette { bool selected) const; [[nodiscard]] const MessageImageStyle &imageStyle(bool selected) const; + [[nodiscard]] int colorPatternIndex(uint8 colorIndex) const; + [[nodiscard]] ColorIndexValues computeColorIndexValues( + bool selected, + uint8 colorIndex) const; + + [[nodiscard]] auto serviceQuoteCache(bool twoColored) const + -> not_null; + [[nodiscard]] auto serviceReplyCache(bool twoColored) const + -> not_null; + [[nodiscard]] const ColorIndexValues &coloredValues( + bool selected, + uint8 colorIndex) const; + [[nodiscard]] not_null coloredQuoteCache( + bool selected, + uint8 colorIndex) const; + [[nodiscard]] not_null coloredReplyCache( + bool selected, + uint8 colorIndex) const; + + [[nodiscard]] const style::TextPalette &coloredTextPalette( + bool selected, + uint8 colorIndex) const; + + [[nodiscard]] not_null backgroundEmojiData( + uint64 id) const; + [[nodiscard]] const CornersPixmaps &msgBotKbOverBgAddCornersSmall() const; [[nodiscard]] const CornersPixmaps &msgBotKbOverBgAddCornersLarge() const; [[nodiscard]] const CornersPixmaps &msgSelectOverlayCorners( @@ -285,7 +375,23 @@ class ChatStyle final : public style::palette { } private: + using ColoredQuotePaintCaches = std::array< + std::unique_ptr, + kColorIndexCount * 2>; + + struct ColoredPalette { + std::optional linkFg; + style::TextPalette data; + }; + void assignPalette(not_null palette); + void clearColorIndexCaches(); + void updateDarkValue(); + + [[nodiscard]] not_null coloredCache( + ColoredQuotePaintCaches &caches, + bool selected, + uint8 colorIndex) const; void make(style::color &my, const style::color &original) const; void make(style::icon &my, const style::icon &original) const; @@ -336,7 +442,22 @@ class ChatStyle final : public style::palette { mutable CornersPixmaps _msgSelectOverlayCorners[ int(CachedCornerRadius::kCount)]; - mutable std::vector _highlightColors; + mutable std::vector _highlightColors; + mutable std::array< + std::unique_ptr, + 2> _serviceQuoteCache; + mutable std::array< + std::unique_ptr, + 2> _serviceReplyCache; + mutable std::array< + std::optional, + 2 * kColorIndexCount> _coloredValues; + mutable ColoredQuotePaintCaches _coloredQuoteCaches; + mutable ColoredQuotePaintCaches _coloredReplyCaches; + mutable std::array< + ColoredPalette, + 2 * kColorIndexCount> _coloredTextPalettes; + mutable base::flat_map _backgroundEmojis; style::TextPalette _historyPsaForwardPalette; style::TextPalette _imgReplyTextPalette; @@ -365,12 +486,31 @@ class ChatStyle final : public style::palette { style::icon _historyPollChoiceRight = { Qt::Uninitialized }; style::icon _historyPollChoiceWrong = { Qt::Uninitialized }; + ColorIndicesCompressed _colorIndices; + + bool _dark = false; + rpl::event_stream<> _paletteChanged; rpl::lifetime _defaultPaletteChangeLifetime; + rpl::lifetime _colorIndicesLifetime; }; +[[nodiscard]] uint8 DecideColorIndex(uint64 id); +[[nodiscard]] uint8 ColorIndexToPaletteIndex(uint8 colorIndex); + +[[nodiscard]] QColor FromNameFg( + not_null st, + bool selected, + uint8 colorIndex); + +[[nodiscard]] inline QColor FromNameFg( + const ChatPaintContext &context, + uint8 colorIndex) { + return FromNameFg(context.st, context.selected(), colorIndex); +} + void FillComplexOverlayRect( QPainter &p, QRect rect, diff --git a/Telegram/SourceFiles/ui/chat/chat_theme.cpp b/Telegram/SourceFiles/ui/chat/chat_theme.cpp index db2443034946f4..bd1440d929bf02 100644 --- a/Telegram/SourceFiles/ui/chat/chat_theme.cpp +++ b/Telegram/SourceFiles/ui/chat/chat_theme.cpp @@ -489,8 +489,8 @@ ChatPaintContext ChatTheme::preparePaintContext( .bubblesPattern = _bubblesBackgroundPattern.get(), .viewport = viewport, .clip = clip, - .paused = paused, .now = now, + .paused = paused, }; } diff --git a/Telegram/SourceFiles/ui/chat/choose_send_as.cpp b/Telegram/SourceFiles/ui/chat/choose_send_as.cpp index 7dd2252baf9fab..0f67264cf26189 100644 --- a/Telegram/SourceFiles/ui/chat/choose_send_as.cpp +++ b/Telegram/SourceFiles/ui/chat/choose_send_as.cpp @@ -14,7 +14,6 @@ For license and copyright information please follow this link: #include "history/history.h" #include "ui/controls/send_as_button.h" #include "ui/text/text_utilities.h" -#include "ui/toast/toast.h" #include "ui/painter.h" #include "window/window_session_controller.h" #include "main/main_session.h" @@ -168,39 +167,6 @@ rpl::producer> ListController::clicked() const { return _clicked.events(); } -void ShowPremiumPromoToast(not_null controller) { - using WeakToast = base::weak_ptr; - const auto toast = std::make_shared(); - - auto link = Ui::Text::Link( - tr::lng_send_as_premium_required_link(tr::now)); - link.entities.push_back( - EntityInText(EntityType::Semibold, 0, link.text.size())); - (*toast) = controller->showToast({ - .text = tr::lng_send_as_premium_required( - tr::now, - lt_link, - link, - Ui::Text::WithEntities), - .st = &st::defaultMultilineToast, - .duration = Ui::Toast::kDefaultDuration * 2, - .multiline = true, - .filter = crl::guard(&controller->session(), [=]( - const ClickHandlerPtr &, - Qt::MouseButton button) { - if (button == Qt::LeftButton) { - if (const auto strong = toast->get()) { - strong->hideAnimated(); - (*toast) = nullptr; - Settings::ShowPremium(controller, "send_as"); - return true; - } - } - return false; - }), - }); -} - } // namespace void ChooseSendAsBox( @@ -272,7 +238,17 @@ void SetupSendAsButton( if (i != end(list) && i->premiumRequired && !sendAs->session().premium()) { - ShowPremiumPromoToast(window); + Settings::ShowPremiumPromoToast( + window->uiShow(), + tr::lng_send_as_premium_required( + tr::now, + lt_link, + Ui::Text::Link( + Ui::Text::Bold( + tr::lng_send_as_premium_required_link( + tr::now))), + Ui::Text::WithEntities), + u"send_as"_q); return false; } session->sendAsPeers().saveChosen(peer, sendAs); diff --git a/Telegram/SourceFiles/ui/chat/message_bar.cpp b/Telegram/SourceFiles/ui/chat/message_bar.cpp index 3c79b423afe57d..eb7868fa414a6d 100644 --- a/Telegram/SourceFiles/ui/chat/message_bar.cpp +++ b/Telegram/SourceFiles/ui/chat/message_bar.cpp @@ -222,8 +222,8 @@ void MessageBar::updateFromContent(MessageBarContent &&content) { QRect MessageBar::imageRect() const { const auto left = st::msgReplyBarSkip + st::msgReplyBarSkip; - const auto top = st::msgReplyPadding.top(); - const auto size = st::msgReplyBarSize.height(); + const auto top = (st::historyReplyHeight - st::historyReplyPreview) / 2; + const auto size = st::historyReplyPreview; return QRect(left, top, size, size); } @@ -243,14 +243,11 @@ QRect MessageBar::titleRangeRect(int from, int till) const { QRect MessageBar::bodyRect(bool withImage) const { const auto innerLeft = st::msgReplyBarSkip + st::msgReplyBarSkip; - const auto imageSkip = st::msgReplyBarSize.height() - + st::msgReplyBarSkip - - st::msgReplyBarSize.width() - - st::msgReplyBarPos.x(); + const auto imageSkip = st::historyReplyPreview + st::msgReplyBarSkip; const auto left = innerLeft + (withImage ? imageSkip : 0); const auto top = st::msgReplyPadding.top(); const auto width = _widget.width() - left - st::msgReplyPadding.right(); - const auto height = st::msgReplyBarSize.height(); + const auto height = (st::historyReplyHeight - 2 * top); return QRect(left, top, width, height) - _content.margins; } diff --git a/Telegram/SourceFiles/ui/controls/userpic_button.cpp b/Telegram/SourceFiles/ui/controls/userpic_button.cpp index 4beca8ad151fd0..3b085f5d5b09d8 100644 --- a/Telegram/SourceFiles/ui/controls/userpic_button.cpp +++ b/Telegram/SourceFiles/ui/controls/userpic_button.cpp @@ -1050,8 +1050,7 @@ void UserpicButton::prepareUserpicPixmap() { } else { const auto user = _peer->asUser(); auto empty = Ui::EmptyUserpic( - Ui::EmptyUserpic::UserpicColor( - Data::PeerColorIndex(_peer->id)), + Ui::EmptyUserpic::UserpicColor(_peer->colorIndex()), ((user && user->isInaccessible()) ? Ui::EmptyUserpic::InaccessibleName() : _peer->name())); diff --git a/Telegram/SourceFiles/ui/effects/premium.style b/Telegram/SourceFiles/ui/effects/premium.style index 236d8e40a30f2a..84252ffbb66adf 100644 --- a/Telegram/SourceFiles/ui/effects/premium.style +++ b/Telegram/SourceFiles/ui/effects/premium.style @@ -26,6 +26,43 @@ PremiumBubble { tailSize: size; font: font; } +PremiumCover { + starSize: size; + starTopSkip: pixels; + titlePadding: margins; + titleFont: font; + about: FlatLabel; +} + +premiumAboutTextStyle: TextStyle(defaultTextStyle) { + font: font(12px); + linkUnderline: kLinkUnderlineAlways; + lineHeight: 18px; +} +defaultPremiumCover: PremiumCover { + starSize: size(84px, 81px); + starTopSkip: 37px; + titlePadding: margins(0px, 18px, 0px, 11px); + titleFont: boxTitleFont; + about: FlatLabel(defaultFlatLabel) { + style: premiumAboutTextStyle; + palette: TextPalette(defaultTextPalette) { + linkFg: premiumButtonFg; + } + align: align(top); + textFg: premiumButtonFg; + minWidth: 190px; + } +} +userPremiumCoverAbout: FlatLabel(boxDividerLabel) { + style: premiumAboutTextStyle; + minWidth: 315px; + maxHeight: 0px; + align: align(top); +} +userPremiumCover: PremiumCover(defaultPremiumCover) { + about: userPremiumCoverAbout; +} defaultPremiumBoxLabel: FlatLabel(defaultFlatLabel) { minWidth: 220px; @@ -239,3 +276,83 @@ boostReplaceIconSkip: 3px; boostReplaceIconOutline: 2px; boostReplaceIconAdd: point(4px, 2px); boostReplaceArrow: icon{{ "mediaview/next", windowSubTextFg }}; + +giveawayGiftCodeTopHeight: 195px; +giveawayGiftCodeLink: FlatLabel(defaultFlatLabel) { + margin: margins(10px, 12px, 10px, 8px); + textFg: menuIconColor; + maxHeight: 24px; +} +giveawayGiftCodeLinkCopy: icon{{ "menu/copy", menuIconColor }}; +giveawayGiftCodeLinkHeight: 42px; +giveawayGiftCodeLinkCopyWidth: 40px; +giveawayGiftCodeLinkMargin: margins(24px, 8px, 24px, 12px); + +boostLinkStatsButton: IconButton(defaultIconButton) { + width: giveawayGiftCodeLinkCopyWidth; + height: giveawayGiftCodeLinkHeight; + icon: icon{{ "menu/stats", menuIconColor }}; + iconOver: icon{{ "menu/stats", menuIconColor }}; + ripple: emptyRippleAnimation; +} + +giveawayGiftCodeTable: Table(defaultTable) { + labelMinWidth: 91px; +} +giveawayGiftCodeTableMargin: margins(24px, 4px, 24px, 4px); +giveawayGiftCodeLabel: FlatLabel(defaultFlatLabel) { + textFg: menuIconColor; + maxHeight: 24px; + style: TextStyle(semiboldTextStyle) { + font: font(12px semibold); + } +} +giveawayGiftCodeLabelMargin: margins(13px, 10px, 13px, 10px); +giveawayGiftCodeValue: FlatLabel(defaultFlatLabel) { + maxHeight: 24px; + style: TextStyle(defaultTextStyle) { + font: font(12px); + linkUnderline: kLinkUnderlineNever; + } +} +giveawayGiftCodeValueMargin: margins(13px, 9px, 13px, 9px); +giveawayGiftCodePeerMargin: margins(11px, 6px, 11px, 4px); +giveawayGiftCodeUserpic: UserpicButton(defaultUserpicButton) { + size: size(24px, 24px); + photoSize: 24px; + photoPosition: point(-1px, -1px); +} +giveawayGiftCodeNamePosition: point(32px, 4px); +giveawayGiftCodeCover: PremiumCover(userPremiumCover) { + starSize: size(92px, 90px); + starTopSkip: 20px; + titlePadding: margins(0px, 15px, 0px, 17px); + titleFont: font(15px semibold); + about: FlatLabel(userPremiumCoverAbout) { + textFg: windowBoldFg; + style: TextStyle(premiumAboutTextStyle) { + lineHeight: 17px; + } + } +} +giveawayGiftCodeFooter: FlatLabel(defaultFlatLabel) { + align: align(top); + textFg: windowBoldFg; +} +giveawayGiftCodeFooterMargin: margins(0px, 9px, 0px, 4px); +giveawayGiftCodeBox: Box(defaultBox) { + buttonPadding: margins(22px, 11px, 22px, 22px); + buttonHeight: 42px; + button: RoundButton(defaultActiveButton) { + height: 42px; + textTop: 12px; + font: font(13px semibold); + } + shadowIgnoreTopSkip: true; +} +giveawayRefundedLabel: FlatLabel(boxLabel) { + align: align(top); + style: semiboldTextStyle; + textFg: attentionButtonFg; +} +giveawayRefundedPadding: margins(8px, 10px, 8px, 10px); diff --git a/Telegram/SourceFiles/ui/effects/premium_top_bar.cpp b/Telegram/SourceFiles/ui/effects/premium_top_bar.cpp index 309db7a6a7df80..951eb8f219e40e 100644 --- a/Telegram/SourceFiles/ui/effects/premium_top_bar.cpp +++ b/Telegram/SourceFiles/ui/effects/premium_top_bar.cpp @@ -14,6 +14,7 @@ For license and copyright information please follow this link: #include "ui/wrap/fade_wrap.h" #include "styles/style_layers.h" #include "styles/style_settings.h" +#include "styles/style_premium.h" #include @@ -90,6 +91,13 @@ QImage GenerateStarForLightTopBar(QRectF rect) { return frame; } +TopBarAbstract::TopBarAbstract( + QWidget *parent, + const style::PremiumCover &st) +: RpWidget(parent) +, _st(st) { +} + void TopBarAbstract::setRoundEdges(bool value) { _roundEdges = value; update(); @@ -122,11 +130,11 @@ void TopBarAbstract::paintEdges(QPainter &p) const { QRectF TopBarAbstract::starRect( float64 topProgress, float64 sizeProgress) const { - const auto starSize = st::settingsPremiumStarSize * sizeProgress; + const auto starSize = _st.starSize * sizeProgress; return QRectF( QPointF( (width() - starSize.width()) / 2, - st::settingsPremiumStarTopSkip * topProgress), + _st.starTopSkip * topProgress), starSize); }; @@ -143,18 +151,16 @@ void TopBarAbstract::computeIsDark() { TopBar::TopBar( not_null parent, + const style::PremiumCover &st, Fn clickContextOther, rpl::producer title, rpl::producer about, bool light) -: TopBarAbstract(parent) +: TopBarAbstract(parent, st) , _light(light) -, _titleFont(st::boxTitle.style.font) -, _titlePadding(st::settingsPremiumTitlePadding) -, _about( - this, - std::move(about), - _light ? st::settingsPremiumUserAbout : st::settingsPremiumAbout) +, _titleFont(st.titleFont) +, _titlePadding(st.titlePadding) +, _about(this, std::move(about), st.about) , _ministars(this) { std::move( title @@ -219,7 +225,7 @@ void TopBar::setTextPosition(int x, int y) { rpl::producer TopBar::additionalHeight() const { return _about->heightValue( - ) | rpl::map([l = st::settingsPremiumAbout.style.lineHeight](int height) { + ) | rpl::map([l = st().about.style.lineHeight](int height) { return std::max(height - l * 2, 0); }); } diff --git a/Telegram/SourceFiles/ui/effects/premium_top_bar.h b/Telegram/SourceFiles/ui/effects/premium_top_bar.h index 0cef38dc58dc77..c0749df00addef 100644 --- a/Telegram/SourceFiles/ui/effects/premium_top_bar.h +++ b/Telegram/SourceFiles/ui/effects/premium_top_bar.h @@ -11,6 +11,14 @@ For license and copyright information please follow this link: #include "ui/rp_widget.h" #include "ui/effects/premium_stars_colored.h" +namespace style { +struct PremiumCover; +} // namespace style + +namespace st { +extern const style::PremiumCover &defaultPremiumCover; +} // namespace st + namespace Ui { class FlatLabel; } // namespace Ui @@ -23,7 +31,9 @@ namespace Ui::Premium { class TopBarAbstract : public RpWidget { public: - using RpWidget::RpWidget; + TopBarAbstract( + QWidget *parent = nullptr, + const style::PremiumCover &st = st::defaultPremiumCover); void setRoundEdges(bool value); @@ -32,6 +42,10 @@ class TopBarAbstract : public RpWidget { [[nodiscard]] virtual rpl::producer additionalHeight() const = 0; + [[nodiscard]] const style::PremiumCover &st() const { + return _st; + } + protected: void paintEdges(QPainter &p, const QBrush &brush) const; void paintEdges(QPainter &p) const; @@ -44,6 +58,7 @@ class TopBarAbstract : public RpWidget { void computeIsDark(); private: + const style::PremiumCover &_st; bool _roundEdges = true; bool _isDark = false; @@ -53,6 +68,7 @@ class TopBar final : public TopBarAbstract { public: TopBar( not_null parent, + const style::PremiumCover &st, Fn clickContextOther, rpl::producer title, rpl::producer about, diff --git a/Telegram/SourceFiles/ui/effects/reaction_fly_animation.cpp b/Telegram/SourceFiles/ui/effects/reaction_fly_animation.cpp index 4fb0c367a44ff6..5210d2e5ad346d 100644 --- a/Telegram/SourceFiles/ui/effects/reaction_fly_animation.cpp +++ b/Telegram/SourceFiles/ui/effects/reaction_fly_animation.cpp @@ -69,7 +69,8 @@ ReactionFlyAnimation::ReactionFlyAnimation( , _repaint(std::move(repaint)) , _flyFrom(args.flyFrom) , _scaleOutDuration(args.scaleOutDuration) -, _scaleOutTarget(args.scaleOutTarget) { +, _scaleOutTarget(args.scaleOutTarget) +, _forceFirstFrame(args.forceFirstFrame) { const auto &list = owner->list(::Data::Reactions::Type::All); auto centerIcon = (DocumentData*)nullptr; auto aroundAnimation = (DocumentData*)nullptr; @@ -251,6 +252,7 @@ void ReactionFlyAnimation::paintCenterFrame( target.x() + (target.width() - _customSize) / 2, target.y() + (target.height() - _customSize) / 2), .scaled = scaled, + .internal = { .forceFirstFrame = _forceFirstFrame }, }); } } @@ -278,6 +280,7 @@ void ReactionFlyAnimation::paintMiniCopies( .size = size, .now = now, .scaled = true, + .internal = { .forceFirstFrame = _forceFirstFrame }, }; for (const auto &mini : _miniCopies) { if (progress >= mini.duration) { diff --git a/Telegram/SourceFiles/ui/effects/reaction_fly_animation.h b/Telegram/SourceFiles/ui/effects/reaction_fly_animation.h index dd9575fc817c84..4a5d608b2e2ea5 100644 --- a/Telegram/SourceFiles/ui/effects/reaction_fly_animation.h +++ b/Telegram/SourceFiles/ui/effects/reaction_fly_animation.h @@ -31,6 +31,7 @@ struct ReactionFlyAnimationArgs { float64 scaleOutTarget = 0.; float64 miniCopyMultiplier = 1.; bool effectOnly = false; + bool forceFirstFrame = false; [[nodiscard]] ReactionFlyAnimationArgs translated(QPoint point) const; }; @@ -42,6 +43,7 @@ struct ReactionFlyCenter { float64 centerSizeMultiplier = 0.; int customSize = 0; int size = 0; + bool forceFirstFrame = false; }; class ReactionFlyAnimation final { @@ -121,6 +123,7 @@ class ReactionFlyAnimation final { crl::time _scaleOutDuration = 0; float64 _scaleOutTarget = 0.; bool _noEffectScaleStarted = false; + bool _forceFirstFrame = false; bool _effectOnly = false; bool _valid = false; diff --git a/Telegram/SourceFiles/ui/empty_userpic.cpp b/Telegram/SourceFiles/ui/empty_userpic.cpp index 5266771273e678..56eb0510619d11 100644 --- a/Telegram/SourceFiles/ui/empty_userpic.cpp +++ b/Telegram/SourceFiles/ui/empty_userpic.cpp @@ -9,7 +9,9 @@ For license and copyright information please follow this link: #include "rabbit/rabbit_settings.h" #include "ui/emoji_config.h" +#include "ui/chat/chat_style.h" #include "ui/effects/animation_value.h" +#include "ui/emoji_config.h" #include "ui/painter.h" #include "ui/ui_utility.h" #include "styles/style_chat.h" @@ -222,13 +224,11 @@ QString EmptyUserpic::InaccessibleName() { return QChar(0) + u"inaccessible"_q; } -int EmptyUserpic::ColorIndex(uint64 id) { - const auto index = id % 7; - const int map[] = { 0, 7, 4, 1, 6, 3, 5 }; - return map[index]; +uint8 EmptyUserpic::ColorIndex(uint64 id) { + return DecideColorIndex(id); } -EmptyUserpic::BgColors EmptyUserpic::UserpicColor(int id) { +EmptyUserpic::BgColors EmptyUserpic::UserpicColor(uint8 colorIndex) { const EmptyUserpic::BgColors colors[] = { { st::historyPeer1UserpicBg, st::historyPeer1UserpicBg2 }, { st::historyPeer2UserpicBg, st::historyPeer2UserpicBg2 }, @@ -239,7 +239,7 @@ EmptyUserpic::BgColors EmptyUserpic::UserpicColor(int id) { { st::historyPeer7UserpicBg, st::historyPeer7UserpicBg2 }, { st::historyPeer8UserpicBg, st::historyPeer8UserpicBg2 }, }; - return colors[id]; + return colors[ColorIndexToPaletteIndex(colorIndex)]; } void EmptyUserpic::paint( diff --git a/Telegram/SourceFiles/ui/empty_userpic.h b/Telegram/SourceFiles/ui/empty_userpic.h index 5ea91cdd8ba8a7..a6a4c6ec001c76 100644 --- a/Telegram/SourceFiles/ui/empty_userpic.h +++ b/Telegram/SourceFiles/ui/empty_userpic.h @@ -18,8 +18,9 @@ class EmptyUserpic final : public base::has_weak_ptr { const style::color color2; }; - [[nodiscard]] static int ColorIndex(uint64 id); - [[nodiscard]] static EmptyUserpic::BgColors UserpicColor(int id); + [[nodiscard]] static uint8 ColorIndex(uint64 id); + [[nodiscard]] static EmptyUserpic::BgColors UserpicColor( + uint8 colorIndex); [[nodiscard]] static QString ExternalName(); [[nodiscard]] static QString InaccessibleName(); diff --git a/Telegram/SourceFiles/ui/menu_icons.style b/Telegram/SourceFiles/ui/menu_icons.style index d43477c875a8f8..221e369d176c2b 100644 --- a/Telegram/SourceFiles/ui/menu_icons.style +++ b/Telegram/SourceFiles/ui/menu_icons.style @@ -141,6 +141,11 @@ menuIconPremium: icon {{ "menu/premium", menuIconColor }}; menuIconIpAddress: icon {{ "menu/ip_address", menuIconColor }}; menuIconAddress: icon {{ "menu/payment_address", menuIconColor }}; menuIconShowAll: icon {{ "menu/all_media", menuIconColor }}; +menuIconReplace: icon {{ "chat/input_replace", menuIconColor }}; +menuIconAbove: icon {{ "menu/link_above", menuIconColor }}; +menuIconBelow: icon {{ "menu/link_below", menuIconColor }}; +menuIconEnlarge: icon {{ "menu/link_enlarge", menuIconColor }}; +menuIconShrink: icon {{ "menu/link_shrink", menuIconColor }}; menuIconTTLAny: icon {{ "menu/auto_delete_plain", menuIconColor }}; menuIconTTLAnyTextPosition: point(11px, 22px); @@ -203,3 +208,7 @@ mediaSpeedVeryFast: icon {{ "player/speed/audiospeed_menu_1.7", mediaviewMenuFg mediaSpeedVeryFastActive: icon {{ "player/speed/audiospeed_menu_1.7", mediaviewTextLinkFg }}; mediaSpeedSuperFast: icon {{ "player/speed/audiospeed_menu_2.0", mediaviewMenuFg }}; mediaSpeedSuperFastActive: icon {{ "player/speed/audiospeed_menu_2.0", mediaviewTextLinkFg }}; + +menuIconMakeBig: icon {{ "player/player_fullscreen", menuIconColor }}; +menuIconMakeSmall: icon {{ "player/player_minimize", menuIconColor }}; +menuIconChangeOrder: icon {{ "player/player_order", menuIconColor }}; diff --git a/Telegram/SourceFiles/ui/text/text_options.cpp b/Telegram/SourceFiles/ui/text/text_options.cpp index c5e9b5dafdcc02..5eda68d110641e 100644 --- a/Telegram/SourceFiles/ui/text/text_options.cpp +++ b/Telegram/SourceFiles/ui/text/text_options.cpp @@ -113,7 +113,8 @@ void InitTextOptions() { = WebpageDescriptionOptions.maxw = st::msgMaxWidth - st::msgPadding.left() - - st::webPageLeft + - st::messageQuoteStyle.padding.left() + - st::messageQuoteStyle.padding.right() - st::msgPadding.right(); WebpageDescriptionOptions.maxh = st::webPageDescriptionFont->height * 3; } diff --git a/Telegram/SourceFiles/window/notifications_manager.cpp b/Telegram/SourceFiles/window/notifications_manager.cpp index c082f4674de292..509698af233d98 100644 --- a/Telegram/SourceFiles/window/notifications_manager.cpp +++ b/Telegram/SourceFiles/window/notifications_manager.cpp @@ -1060,18 +1060,20 @@ void Manager::notificationActivated( const auto replyToId = (id.msgId > 0 && !history->peer->isUser() && id.msgId != topicRootId) - ? id.msgId - : 0; + ? FullMsgId(history->peer->id, id.msgId) + : FullMsgId(); auto draft = std::make_unique( reply, - replyToId, - topicRootId, + FullReplyTo{ + .messageId = replyToId, + .topicRootId = topicRootId, + }, MessageCursor{ int(reply.text.size()), int(reply.text.size()), Ui::kQFixedMax, }, - Data::PreviewState::Allowed); + Data::WebPageDraft()); history->setLocalDraft(std::move(draft)); } window->widget()->showFromTray(); @@ -1150,7 +1152,7 @@ void Manager::notificationReplied( ? topicRootId : MsgId(0); message.action.replyTo = { - .msgId = replyToId, + .messageId = { replyToId ? history->peer->id : 0, replyToId }, .topicRootId = topic ? topic->rootId() : 0, }; message.action.clearDraft = false; diff --git a/Telegram/SourceFiles/window/themes/window_theme.cpp b/Telegram/SourceFiles/window/themes/window_theme.cpp index 001d300dd1091c..6d4c0d703a1fd8 100644 --- a/Telegram/SourceFiles/window/themes/window_theme.cpp +++ b/Telegram/SourceFiles/window/themes/window_theme.cpp @@ -1496,6 +1496,12 @@ bool ReadPaletteValues(const QByteArray &content, Fn DefaultChatThemeOn(rpl::lifetime &lifetime) { + auto result = std::make_unique(); + + const auto push = [=, raw = result.get()] { + const auto background = Background(); + const auto &paper = background->paper(); + raw->setBackground({ + .prepared = background->prepared(), + .preparedForTiled = background->preparedForTiled(), + .gradientForFill = background->gradientForFill(), + .colorForFill = background->colorForFill(), + .colors = paper.backgroundColors(), + .patternOpacity = paper.patternOpacity(), + .gradientRotation = paper.gradientRotation(), + .isPattern = paper.isPattern(), + .tile = background->tile(), + }); + }; + + push(); + Background()->updates( + ) | rpl::start_with_next([=](const BackgroundUpdate &update) { + if (update.type == BackgroundUpdate::Type::New + || update.type == BackgroundUpdate::Type::Changed) { + push(); + } + }, lifetime); + + return result; +} + } // namespace Theme } // namespace Window diff --git a/Telegram/SourceFiles/window/themes/window_theme.h b/Telegram/SourceFiles/window/themes/window_theme.h index f750f99b3d366d..9cb7a020ab43ad 100644 --- a/Telegram/SourceFiles/window/themes/window_theme.h +++ b/Telegram/SourceFiles/window/themes/window_theme.h @@ -28,6 +28,7 @@ class Controller; namespace Ui { struct ChatThemeBackground; +class ChatTheme; } // namespace Ui namespace Webview { @@ -309,5 +310,8 @@ bool ReadPaletteValues( [[nodiscard]] Webview::ThemeParams WebViewParams(); +[[nodiscard]] std::unique_ptr DefaultChatThemeOn( + rpl::lifetime &lifetime); + } // namespace Theme } // namespace Window diff --git a/Telegram/SourceFiles/window/themes/window_theme_preview.cpp b/Telegram/SourceFiles/window/themes/window_theme_preview.cpp index 4ba59a76125a6e..439c45055fb0ea 100644 --- a/Telegram/SourceFiles/window/themes/window_theme_preview.cpp +++ b/Telegram/SourceFiles/window/themes/window_theme_preview.cpp @@ -794,7 +794,12 @@ void Generator::paintRow(const Row &row) { void Generator::paintBubble(const Bubble &bubble) { auto height = bubble.height; if (!bubble.replyName.isEmpty()) { - height += st::msgReplyPadding.top() + st::msgReplyBarSize.height() + st::msgReplyPadding.bottom(); + height += st::historyReplyTop + + st::historyReplyPadding.top() + + st::msgServiceNameFont->height + + st::normalFont->height + + st::historyReplyPadding.bottom() + + st::historyReplyBottom; } auto isPhoto = !bubble.photo.isNull(); @@ -854,19 +859,45 @@ void Generator::paintBubble(const Bubble &bubble) { trect = trect.marginsRemoved(st::msgPadding); } if (!bubble.replyName.isEmpty()) { - auto h = st::msgReplyPadding.top() + st::msgReplyBarSize.height() + st::msgReplyPadding.bottom(); - + trect.setY(trect.y() + st::historyReplyTop); auto bar = (bubble.outbg ? st::msgOutReplyBarColor[_palette] : st::msgInReplyBarColor[_palette]); - auto rbar = style::rtlrect(trect.x() + st::msgReplyBarPos.x(), trect.y() + st::msgReplyPadding.top() + st::msgReplyBarPos.y(), st::msgReplyBarSize.width(), st::msgReplyBarSize.height(), _rect.width()); - _p->fillRect(rbar, bar); + auto rbar = style::rtlrect( + trect.x(), + trect.y(), + trect.width(), + (st::historyReplyPadding.top() + + st::msgServiceNameFont->height + + st::normalFont->height + + st::historyReplyPadding.bottom()), + _rect.width()); + { + auto hq = PainterHighQualityEnabler(*_p); + _p->setPen(Qt::NoPen); + _p->setBrush(bar); + + const auto outline = st::messageTextStyle.blockquote.outline; + const auto radius = st::messageTextStyle.blockquote.radius; + _p->setOpacity(Ui::kDefaultOutline1Opacity); + _p->setClipRect(rbar.x(), rbar.y(), outline, rbar.height()); + _p->drawRoundedRect(rbar, radius, radius); + _p->setOpacity(Ui::kDefaultBgOpacity); + _p->setClipRect( + rbar.x() + outline, + rbar.y(), + rbar.width() - outline, + rbar.height()); + _p->drawRoundedRect(rbar, radius, radius); + } + _p->setOpacity(1.); + _p->setClipping(false); _p->setPen(bubble.outbg ? st::msgOutServiceFg[_palette] : st::msgInServiceFg[_palette]); - bubble.replyName.drawLeftElided(*_p, trect.x() + st::msgReplyBarSkip, trect.y() + st::msgReplyPadding.top(), bubble.width - st::msgReplyBarSkip, _rect.width()); + bubble.replyName.drawLeftElided(*_p, trect.x() + st::historyReplyPadding.left(), trect.y() + st::historyReplyPadding.top(), bubble.width - st::historyReplyPadding.left() - st::historyReplyPadding.right(), _rect.width()); _p->setPen(bubble.outbg ? st::historyTextOutFg[_palette] : st::historyTextInFg[_palette]); - bubble.replyText.drawLeftElided(*_p, trect.x() + st::msgReplyBarSkip, trect.y() + st::msgReplyPadding.top() + st::msgServiceNameFont->height, bubble.width - st::msgReplyBarSkip, _rect.width()); + bubble.replyText.drawLeftElided(*_p, trect.x() + st::historyReplyPadding.left(), trect.y() + st::historyReplyPadding.top() + st::msgServiceNameFont->height, bubble.width - st::historyReplyPadding.left() - st::historyReplyPadding.right(), _rect.width()); - trect.setY(trect.y() + h); + trect.setY(trect.y() + rbar.height() + st::historyReplyBottom); } if (!bubble.text.isEmpty()) { @@ -986,7 +1017,7 @@ void Generator::paintService(QString text) { } void Generator::paintUserpic(int x, int y, Row::Type type, int index, QString letters) { - const auto colorIndex = Ui::EmptyUserpic::ColorIndex(index); + const auto colorIndex = Ui::DecideColorIndex(index); const auto colors = Ui::EmptyUserpic::UserpicColor(colorIndex); auto userpic = Ui::EmptyUserpic(colors, letters); diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index fb4a32b55cf97d..9f2201e407495d 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -112,7 +112,10 @@ void ShareBotGame( } histories.sendPreparedMessage( history, - FullReplyTo{ .msgId = replyTo, .topicRootId = topicRootId }, + FullReplyTo{ + .messageId = { replyTo ? history->peer->id : 0, replyTo }, + .topicRootId = topicRootId, + }, randomId, Data::Histories::PrepareMessage( MTP_flags(flags), @@ -1036,16 +1039,12 @@ void Filler::addCreatePoll() { ? SendMenu::Type::SilentOnly : SendMenu::Type::Scheduled; const auto flag = PollData::Flags(); - const auto topicRootId = _request.rootId; - const auto replyToId = _request.currentReplyToId - ? _request.currentReplyToId - : topicRootId; + const auto replyTo = _request.currentReplyTo; auto callback = [=] { PeerMenuCreatePoll( controller, peer, - replyToId, - topicRootId, + replyTo, flag, flag, source, @@ -1464,8 +1463,7 @@ void PeerMenuShareContactBox( void PeerMenuCreatePoll( not_null controller, not_null peer, - MsgId replyToId, - MsgId topicRootId, + FullReplyTo replyTo, PollData::Flags chosen, PollData::Flags disabled, Api::SendType sendType, @@ -1490,10 +1488,12 @@ void PeerMenuCreatePoll( auto action = Api::SendAction( peer->owner().history(peer), result.options); - action.clearDraft = false; - action.replyTo = { .msgId = replyToId, .topicRootId = topicRootId }; + action.replyTo = replyTo; + const auto topicRootId = replyTo.topicRootId; if (const auto local = action.history->localDraft(topicRootId)) { action.clearDraft = local->textWithTags.text.isEmpty(); + } else { + action.clearDraft = false; } const auto api = &peer->session().api(); api->polls().create(result.poll, action, crl::guard(weak, [=] { @@ -1637,7 +1637,7 @@ void BlockSenderFromRepliesBox( PeerMenuBlockUserBox( box, &controller->window(), - item->senderOriginal(), + item->originalSender(), true, Window::ClearReply{ id }); } diff --git a/Telegram/SourceFiles/window/window_peer_menu.h b/Telegram/SourceFiles/window/window_peer_menu.h index 5125b2c4b8eded..029c0c752ef4df 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.h +++ b/Telegram/SourceFiles/window/window_peer_menu.h @@ -86,8 +86,7 @@ void PeerMenuAddChannelMembers( void PeerMenuCreatePoll( not_null controller, not_null peer, - MsgId replyToId = 0, - MsgId topicRootId = 0, + FullReplyTo replyTo = FullReplyTo(), PollData::Flags chosen = PollData::Flags(), PollData::Flags disabled = PollData::Flags(), Api::SendType sendType = Api::SendType::Normal, diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index a0c04ef18b7ca3..ff166a7894a73d 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -625,9 +625,9 @@ void SessionNavigation::resolveBoostState(not_null channel) { return; } _boostStateResolving = channel; - _api.request(MTPstories_GetBoostsStatus( + _api.request(MTPpremium_GetBoostsStatus( channel->input - )).done([=](const MTPstories_BoostsStatus &result) { + )).done([=](const MTPpremium_BoostsStatus &result) { _boostStateResolving = nullptr; const auto &data = result.data(); const auto submit = [=](Fn done) { @@ -653,69 +653,94 @@ void SessionNavigation::resolveBoostState(not_null channel) { void SessionNavigation::applyBoost( not_null channel, Fn done) { - _api.request(MTPstories_CanApplyBoost( - channel->input - )).done([=](const MTPstories_CanApplyBoostResult &result) { - result.match([&](const MTPDstories_canApplyBoostOk &) { - applyBoostChecked(channel, done); - }, [&](const MTPDstories_canApplyBoostReplace &data) { - _session->data().processChats(data.vchats()); - const auto peer = _session->data().peer( - peerFromMTP(data.vcurrent_boost())); - replaceBoostConfirm(peer, channel, done); - }); - }).fail([=](const MTP::Error &error) { - const auto type = error.type(); - if (type == u"PREMIUM_ACCOUNT_REQUIRED"_q) { - const auto jumpToPremium = [=] { - const auto id = peerToChannel(channel->id).bare; - Settings::ShowPremium( - parentController(), - "channel_boost__" + QString::number(id)); - }; - uiShow()->show(Ui::MakeConfirmBox({ - .text = tr::lng_boost_error_premium_text( - Ui::Text::RichLangValue), - .confirmed = jumpToPremium, - .confirmText = tr::lng_boost_error_premium_yes(), - .title = tr::lng_boost_error_premium_title(), - })); - } else if (type == u"PREMIUM_GIFTED_NOT_ALLOWED"_q) { - uiShow()->show(Ui::MakeConfirmBox({ - .text = tr::lng_boost_error_gifted_text( - Ui::Text::RichLangValue), - .title = tr::lng_boost_error_gifted_title(), - .inform = true, - })); - } else if (type == u"BOOST_NOT_MODIFIED"_q) { + _api.request(MTPpremium_GetMyBoosts( + )).done([=](const MTPpremium_MyBoosts &result) { + const auto &data = result.data(); + _session->data().processUsers(data.vusers()); + _session->data().processChats(data.vchats()); + const auto &list = data.vmy_boosts().v; + if (list.isEmpty()) { + if (!_session->premium()) { + const auto jumpToPremium = [=] { + const auto id = peerToChannel(channel->id).bare; + Settings::ShowPremium( + parentController(), + "channel_boost__" + QString::number(id)); + }; + uiShow()->show(Ui::MakeConfirmBox({ + .text = tr::lng_boost_error_premium_text( + Ui::Text::RichLangValue), + .confirmed = jumpToPremium, + .confirmText = tr::lng_boost_error_premium_yes(), + .title = tr::lng_boost_error_premium_title(), + })); + } else { + uiShow()->show(Ui::MakeConfirmBox({ + .text = tr::lng_boost_error_gifted_text( + Ui::Text::RichLangValue), + .title = tr::lng_boost_error_gifted_title(), + .inform = true, + })); + } + done(false); + return; + } + auto different = PeerId(); + auto earliest = TimeId(-1); + const auto now = base::unixtime::now(); + for (const auto &my : list) { + const auto &data = my.data(); + const auto cooldown = data.vcooldown_until_date().value_or(0); + const auto peerId = data.vpeer() + ? peerFromMTP(*data.vpeer()) + : PeerId(); + if (!peerId && cooldown <= now) { + applyBoostChecked(channel, done); + return; + } else if (peerId != channel->id) { + different = peerId; + if (earliest < 0 || cooldown < earliest) { + earliest = cooldown; + } + } + } + if (different) { + if (earliest > now) { + const auto seconds = earliest - now; + const auto days = seconds / 86400; + const auto hours = seconds / 3600; + const auto minutes = seconds / 60; + uiShow()->show(Ui::MakeConfirmBox({ + .text = tr::lng_boost_error_flood_text( + lt_left, + rpl::single(Ui::Text::Bold((days > 1) + ? tr::lng_days(tr::now, lt_count, days) + : (hours > 1) + ? tr::lng_hours(tr::now, lt_count, hours) + : (minutes > 1) + ? tr::lng_minutes(tr::now, lt_count, minutes) + : tr::lng_seconds(tr::now, lt_count, seconds))), + Ui::Text::RichLangValue), + .title = tr::lng_boost_error_flood_title(), + .inform = true, + })); + done(false); + } else { + const auto peer = _session->data().peer(different); + replaceBoostConfirm(peer, channel, done); + } + } else { uiShow()->show(Ui::MakeConfirmBox({ .text = tr::lng_boost_error_already_text( Ui::Text::RichLangValue), .title = tr::lng_boost_error_already_title(), .inform = true, })); - } else if (type.startsWith(u"FLOOD_WAIT_"_q)) { - const auto seconds = type.mid(u"FLOOD_WAIT_"_q.size()).toInt(); - const auto days = seconds / 86400; - const auto hours = seconds / 3600; - const auto minutes = seconds / 60; - uiShow()->show(Ui::MakeConfirmBox({ - .text = tr::lng_boost_error_flood_text( - lt_left, - rpl::single(Ui::Text::Bold((days > 1) - ? tr::lng_days(tr::now, lt_count, days) - : (hours > 1) - ? tr::lng_hours(tr::now, lt_count, hours) - : (minutes > 1) - ? tr::lng_minutes(tr::now, lt_count, minutes) - : tr::lng_seconds(tr::now, lt_count, seconds))), - Ui::Text::RichLangValue), - .title = tr::lng_boost_error_flood_title(), - .inform = true, - })); - } else { - showToast(u"Error: "_q + type); + done(false); } + }).fail([=](const MTP::Error &error) { + const auto type = error.type(); + showToast(u"Error: "_q + type); done(false); }).handleFloodErrors().send(); } @@ -757,9 +782,11 @@ void SessionNavigation::replaceBoostConfirm( void SessionNavigation::applyBoostChecked( not_null channel, Fn done) { - _api.request(MTPstories_ApplyBoost( + _api.request(MTPpremium_ApplyBoost( + MTP_flags(0), + MTPVector(), // slots channel->input - )).done([=](const MTPBool &result) { + )).done([=](const MTPpremium_MyBoosts &result) { done(true); }).fail([=](const MTP::Error &error) { showToast(u"Error: "_q + error.type()); @@ -1060,7 +1087,7 @@ SessionController::SessionController( , _invitePeekTimer([=] { checkInvitePeek(); }) , _activeChatsFilter(session->data().chatsFilters().defaultId()) , _defaultChatTheme(std::make_shared()) -, _chatStyle(std::make_unique()) +, _chatStyle(std::make_unique(session->colorIndicesValue())) , _cachedReactionIconFactory(std::make_unique()) , _giftPremiumValidator(this) { init(); @@ -1526,10 +1553,9 @@ bool SessionController::switchInlineQuery( }; auto draft = std::make_unique( textWithTags, - to.currentReplyToId, - to.rootId, + to.currentReplyTo, cursor, - Data::PreviewState::Allowed); + Data::WebPageDraft()); auto params = Window::SectionShow(); params.reapplyLocalDraft = true; @@ -1539,11 +1565,12 @@ bool SessionController::switchInlineQuery( std::make_shared(history), params); } else { + const auto topicRootId = to.currentReplyTo.topicRootId; history->setLocalDraft(std::move(draft)); - history->clearLocalEditDraft(to.rootId); + history->clearLocalEditDraft(topicRootId); if (to.section == Section::Replies) { const auto commentId = MsgId(); - showRepliesForMessage(history, to.rootId, commentId, params); + showRepliesForMessage(history, topicRootId, commentId, params); } else { showPeerHistory(history->peer, params); } @@ -1560,7 +1587,7 @@ bool SessionController::switchInlineQuery( .section = (thread->asTopic() ? Dialogs::EntryState::Section::Replies : Dialogs::EntryState::Section::History), - .rootId = thread->topicRootId(), + .currentReplyTo = { .topicRootId = thread->topicRootId() }, }; return switchInlineQuery(entryState, bot, query); } diff --git a/Telegram/ThirdParty/fcitx-qt5 b/Telegram/ThirdParty/fcitx-qt5 index c2feea444ab79e..c328b03f32a427 160000 --- a/Telegram/ThirdParty/fcitx-qt5 +++ b/Telegram/ThirdParty/fcitx-qt5 @@ -1 +1 @@ -Subproject commit c2feea444ab79e6a8d6d205d4c7b13ab1db353c9 +Subproject commit c328b03f32a4276d9aeb3213d1f3f959ef6848f2 diff --git a/Telegram/ThirdParty/libprisma b/Telegram/ThirdParty/libprisma index 7f62494bb8cb64..629dc69fbffa5d 160000 --- a/Telegram/ThirdParty/libprisma +++ b/Telegram/ThirdParty/libprisma @@ -1 +1 @@ -Subproject commit 7f62494bb8cb6478a7cb6053ad7b5668fa013366 +Subproject commit 629dc69fbffa5d63954c8e8909a26efdd98573b0 diff --git a/Telegram/build/prepare/prepare.py b/Telegram/build/prepare/prepare.py index ee3e77d9b01e02..d1c11bd1dd8908 100644 --- a/Telegram/build/prepare/prepare.py +++ b/Telegram/build/prepare/prepare.py @@ -1421,7 +1421,7 @@ def runStages(): stage('tg_owt', """ git clone https://github.com/desktop-app/tg_owt.git cd tg_owt - git checkout 592b14d13b + git checkout be153adaa3 git submodule init git submodule update win: diff --git a/Telegram/build/version b/Telegram/build/version index 5f32b2f0c16721..0e22fe75e11247 100644 --- a/Telegram/build/version +++ b/Telegram/build/version @@ -1,7 +1,7 @@ -AppVersion 4010005 -AppVersionStrMajor 4.10 -AppVersionStrSmall 4.10.5 -AppVersionStr 4.10.5 -BetaChannel 1 +AppVersion 4011000 +AppVersionStrMajor 4.11 +AppVersionStrSmall 4.11 +AppVersionStr 4.11.0 +BetaChannel 0 AlphaVersion 0 -AppVersionOriginal 4.10.5.beta +AppVersionOriginal 4.11 diff --git a/Telegram/cmake/lib_prisma.cmake b/Telegram/cmake/lib_prisma.cmake index 89e203ace2157f..b8dc8cc17fb0ff 100644 --- a/Telegram/cmake/lib_prisma.cmake +++ b/Telegram/cmake/lib_prisma.cmake @@ -23,12 +23,6 @@ PRIVATE TokenList.h ) -target_compile_definitions(lib_prisma -PRIVATE - BOOST_NO_INTRINSIC_WCHAR_T - BOOST_REGEX_NO_W32 -) - target_include_directories(lib_prisma PUBLIC ${prisma_loc} diff --git a/Telegram/lib_base b/Telegram/lib_base index 013e7e9c3ca475..bb5e7fafd7aca5 160000 --- a/Telegram/lib_base +++ b/Telegram/lib_base @@ -1 +1 @@ -Subproject commit 013e7e9c3ca475eaed9b3ec31df05e6e32434b18 +Subproject commit bb5e7fafd7aca5808217baeeb9ee3af8c3be6b1b diff --git a/Telegram/lib_ui b/Telegram/lib_ui index 683cd6d6978b9b..4d1a5686a7620b 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit 683cd6d6978b9bd95d5eaa02794852bfe685d9da +Subproject commit 4d1a5686a7620b163639492e1b81101b677d2472 diff --git a/changelog.txt b/changelog.txt index 6e5d10d7405548..dea2160da802cc 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,18 @@ +4.11 (28.10.23) + +- View full statistics in your channels and group chats. +- Choose which link preview in added to the message. +- Choose if link preview is above or below the text. +- Choose if link preview has large or small image. +- Quote parts of text in replies. +- Add quote formatting. +- Reply in another chat. +- Add nice looking code blocks with syntax highlighting. +- Copy full code block by click on its header. +- Send a highlighted code block using ```language syntax. +- Change your name color in Chat Settings. +- Customize quotes, link previews and replies to your messages. + 4.10.5 beta (23.10.23) - Fix crash in replies to messages with spoilers. diff --git a/cmake b/cmake index 21a8d7996d50d9..021a0317bd6ca1 160000 --- a/cmake +++ b/cmake @@ -1 +1 @@ -Subproject commit 21a8d7996d50d973976607360f2f563f38af1c29 +Subproject commit 021a0317bd6ca12611ce84e57af847d0ab20f22e