Merge tag 'v5.6.3' into dev

This commit is contained in:
AlexeyZavar 2024-10-16 13:41:32 +03:00
commit 7321b654bf
270 changed files with 9084 additions and 3098 deletions

View file

@ -402,6 +402,8 @@ PRIVATE
boxes/sessions_box.h boxes/sessions_box.h
boxes/share_box.cpp boxes/share_box.cpp
boxes/share_box.h boxes/share_box.h
boxes/star_gift_box.cpp
boxes/star_gift_box.h
boxes/sticker_set_box.cpp boxes/sticker_set_box.cpp
boxes/sticker_set_box.h boxes/sticker_set_box.h
boxes/stickers_box.cpp boxes/stickers_box.cpp
@ -649,6 +651,8 @@ PRIVATE
data/data_lastseen_status.h data/data_lastseen_status.h
data/data_location.cpp data/data_location.cpp
data/data_location.h data/data_location.h
data/data_media_preload.cpp
data/data_media_preload.h
data/data_media_rotation.cpp data/data_media_rotation.cpp
data/data_media_rotation.h data/data_media_rotation.h
data/data_media_types.cpp data/data_media_types.cpp
@ -684,6 +688,7 @@ PRIVATE
data/data_replies_list.h data/data_replies_list.h
data/data_reply_preview.cpp data/data_reply_preview.cpp
data/data_reply_preview.h data/data_reply_preview.h
data/data_report.h
data/data_saved_messages.cpp data/data_saved_messages.cpp
data/data_saved_messages.h data/data_saved_messages.h
data/data_saved_sublist.cpp data/data_saved_sublist.cpp
@ -1027,6 +1032,10 @@ PRIVATE
info/media/info_media_widget.h info/media/info_media_widget.h
info/members/info_members_widget.cpp info/members/info_members_widget.cpp
info/members/info_members_widget.h info/members/info_members_widget.h
info/peer_gifts/info_peer_gifts_common.cpp
info/peer_gifts/info_peer_gifts_common.h
info/peer_gifts/info_peer_gifts_widget.cpp
info/peer_gifts/info_peer_gifts_widget.h
info/polls/info_polls_results_inner_widget.cpp info/polls/info_polls_results_inner_widget.cpp
info/polls/info_polls_results_inner_widget.h info/polls/info_polls_results_inner_widget.h
info/polls/info_polls_results_widget.cpp info/polls/info_polls_results_widget.cpp

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 MiB

After

Width:  |  Height:  |  Size: 1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 832 KiB

After

Width:  |  Height:  |  Size: 935 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 234 KiB

View file

@ -582,3 +582,55 @@ div.toast_shown {
.bot_button_column_separator { .bot_button_column_separator {
width: 2px width: 2px
} }
.reactions {
margin: 5px 0;
}
.reactions .reaction {
display: inline-flex;
height: 20px;
border-radius: 15px;
background-color: #e8f5fc;
color: #168acd;
font-weight: bold;
margin-bottom: 5px;
}
.reactions .reaction.active {
background-color: #40a6e2;
color: #fff;
}
.reactions .reaction.paid {
background-color: #fdf6e1;
color: #c58523;
}
.reactions .reaction.active.paid {
background-color: #ecae0a;
color: #fdf6e1;
}
.reactions .reaction .emoji {
line-height: 20px;
margin: 0 5px;
font-size: 15px;
}
.reactions .reaction .userpic:not(:first-child) {
margin-left: -8px;
}
.reactions .reaction .userpic {
display: inline-block;
}
.reactions .reaction .userpic .initials {
font-size: 8px;
}
.reactions .reaction .count {
margin-right: 8px;
line-height: 20px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -12,6 +12,7 @@ body {
margin: 0; margin: 0;
background-color: var(--td-window-bg); background-color: var(--td-window-bg);
color: var(--td-window-fg); color: var(--td-window-fg);
zoom: var(--td-zoom-percentage);
} }
html.custom_scroll ::-webkit-scrollbar { html.custom_scroll ::-webkit-scrollbar {

View file

@ -453,6 +453,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_username_app_not_found" = "Bot application not found."; "lng_username_app_not_found" = "Bot application not found.";
"lng_username_link" = "This link opens a chat with you:"; "lng_username_link" = "This link opens a chat with you:";
"lng_username_copied" = "Link copied to clipboard."; "lng_username_copied" = "Link copied to clipboard.";
"lng_username_text_copied" = "Username copied to clipboard.";
"lng_usernames_edit" = "click to edit"; "lng_usernames_edit" = "click to edit";
"lng_usernames_active" = "active"; "lng_usernames_active" = "active";
@ -487,6 +488,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_collectible_phone_info" = "This phone number was bought on **Fragment** on {date} for {price}"; "lng_collectible_phone_info" = "This phone number was bought on **Fragment** on {date} for {price}";
"lng_collectible_phone_copy" = "Copy Phone Number"; "lng_collectible_phone_copy" = "Copy Phone Number";
"lng_collectible_learn_more" = "Learn More"; "lng_collectible_learn_more" = "Learn More";
"lng_collectible_phone_copied" = "Phone number copied to clipboard.";
"lng_settings_section_info" = "Info"; "lng_settings_section_info" = "Info";
@ -508,6 +510,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_settings_alert_linux" = "Draw attention to the window"; "lng_settings_alert_linux" = "Draw attention to the window";
"lng_settings_badge_title" = "Badge counter"; "lng_settings_badge_title" = "Badge counter";
"lng_settings_include_muted" = "Include muted chats in unread count"; "lng_settings_include_muted" = "Include muted chats in unread count";
"lng_settings_include_muted_folders" = "Include muted chats in folder counters";
"lng_settings_count_unread" = "Count unread messages"; "lng_settings_count_unread" = "Count unread messages";
"lng_settings_events_title" = "Events"; "lng_settings_events_title" = "Events";
"lng_settings_events_joined" = "Contact joined Telegram"; "lng_settings_events_joined" = "Contact joined Telegram";
@ -1322,6 +1325,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_profile_similar_channels#other" = "{count} similar channels"; "lng_profile_similar_channels#other" = "{count} similar channels";
"lng_profile_saved_messages#one" = "{count} saved message"; "lng_profile_saved_messages#one" = "{count} saved message";
"lng_profile_saved_messages#other" = "{count} saved messages"; "lng_profile_saved_messages#other" = "{count} saved messages";
"lng_profile_peer_gifts#one" = "{count} gift";
"lng_profile_peer_gifts#other" = "{count} gifts";
"lng_profile_participants_section" = "Members"; "lng_profile_participants_section" = "Members";
"lng_profile_subscribers_section" = "Subscribers"; "lng_profile_subscribers_section" = "Subscribers";
"lng_profile_add_contact" = "Add Contact"; "lng_profile_add_contact" = "Add Contact";
@ -1376,6 +1381,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_profile_copy_fullname" = "Copy Name"; "lng_profile_copy_fullname" = "Copy Name";
"lng_profile_photo_by_you" = "photo set by you"; "lng_profile_photo_by_you" = "photo set by you";
"lng_profile_public_photo" = "public photo"; "lng_profile_public_photo" = "public photo";
"lng_profile_administrators#one" = "{count} administrator";
"lng_profile_administrators#other" = "{count} administrators";
"lng_profile_manage" = "Channel settings";
"lng_invite_upgrade_title" = "Upgrade to Premium"; "lng_invite_upgrade_title" = "Upgrade to Premium";
"lng_invite_upgrade_group_invite#one" = "{users} only accepts invitations to groups from Contacts and **Premium** users."; "lng_invite_upgrade_group_invite#one" = "{users} only accepts invitations to groups from Contacts and **Premium** users.";
@ -1659,6 +1667,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_report_and_ban_button" = "Ban user"; "lng_report_and_ban_button" = "Ban user";
"lng_report_details_about" = "Please enter any additional details relevant to your report."; "lng_report_details_about" = "Please enter any additional details relevant to your report.";
"lng_report_details" = "Additional Details"; "lng_report_details" = "Additional Details";
"lng_report_details_optional" = "Add Comment (Optional)";
"lng_report_details_non_optional" = "Add Comment";
"lng_report_details_message_about" = "Please help us by telling what is wrong with the message you have selected";
"lng_report_reason_spam" = "Spam"; "lng_report_reason_spam" = "Spam";
"lng_report_reason_fake" = "Fake Account"; "lng_report_reason_fake" = "Fake Account";
"lng_report_reason_violence" = "Violence"; "lng_report_reason_violence" = "Violence";
@ -1852,8 +1863,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_action_proximity_distance_km#other" = "{count} km"; "lng_action_proximity_distance_km#other" = "{count} km";
"lng_action_webview_data_done" = "Data from the \"{text}\" button was transferred to the bot."; "lng_action_webview_data_done" = "Data from the \"{text}\" button was transferred to the bot.";
"lng_action_gift_received" = "{user} sent you a gift for {cost}"; "lng_action_gift_received" = "{user} sent you a gift for {cost}";
"lng_action_gift_received_me" = "You sent to {user} a gift for {cost}"; "lng_action_gift_sent" = "You sent a gift for {cost}";
"lng_action_gift_received_anonymous" = "Unknown user sent you a gift for {cost}"; "lng_action_gift_received_anonymous" = "Unknown user sent you a gift for {cost}";
"lng_action_gift_for_stars#one" = "{count} Star";
"lng_action_gift_for_stars#other" = "{count} Stars";
"lng_action_gift_got_subtitle" = "Gift from {user}";
"lng_action_gift_got_stars_text#one" = "Display this gift on your page or convert it to **{count}** Star.";
"lng_action_gift_got_stars_text#other" = "Display this gift on your page or convert it to **{count}** Stars.";
"lng_action_gift_sent_subtitle" = "Gift for {user}";
"lng_action_gift_sent_text#one" = "{user} can display this gift on their page or convert it to {count} Star.";
"lng_action_gift_sent_text#other" = "{user} can display this gift on their page or convert it to {count} Stars.";
"lng_action_gift_premium_months#one" = "{count} Month Premium";
"lng_action_gift_premium_months#other" = "{count} Months Premium";
"lng_action_gift_premium_about" = "Subscription for exclusive Telegram features.";
"lng_action_suggested_photo_me" = "You suggested this photo for {user}'s Telegram profile."; "lng_action_suggested_photo_me" = "You suggested this photo for {user}'s Telegram profile.";
"lng_action_suggested_photo" = "{user} suggests this photo for your Telegram profile."; "lng_action_suggested_photo" = "{user} suggests this photo for your Telegram profile.";
"lng_action_suggested_photo_button" = "View Photo"; "lng_action_suggested_photo_button" = "View Photo";
@ -1915,6 +1937,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_similar_channels_premium_all_link" = "Telegram Premium"; "lng_similar_channels_premium_all_link" = "Telegram Premium";
"lng_similar_channels_show_more" = "Show more channels"; "lng_similar_channels_show_more" = "Show more channels";
"lng_peer_gifts_title" = "Gifts";
"lng_peer_gifts_about" = "These gifts were sent to {user} by other users.";
"lng_peer_gifts_about_mine" = "These gifts were sent to you by other users. Click on a gift to convert it to Stars or change its privacy settings.";
"lng_premium_gift_duration_months#one" = "for {count} month"; "lng_premium_gift_duration_months#one" = "for {count} month";
"lng_premium_gift_duration_months#other" = "for {count} months"; "lng_premium_gift_duration_months#other" = "for {count} months";
"lng_premium_gift_duration_years#one" = "for {count} year"; "lng_premium_gift_duration_years#one" = "for {count} year";
@ -2415,6 +2441,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_credits_box_history_entry_gift_name" = "Received Gift"; "lng_credits_box_history_entry_gift_name" = "Received Gift";
"lng_credits_box_history_entry_giveaway_name" = "Received Prize"; "lng_credits_box_history_entry_giveaway_name" = "Received Prize";
"lng_credits_box_history_entry_gift_sent" = "Sent Gift"; "lng_credits_box_history_entry_gift_sent" = "Sent Gift";
"lng_credits_box_history_entry_gift_converted" = "Converted Gift";
"lng_credits_box_history_entry_gift_out_about" = "With Stars, **{user}** will be able to unlock content and services on Telegram.\n{link}"; "lng_credits_box_history_entry_gift_out_about" = "With Stars, **{user}** will be able to unlock content and services on Telegram.\n{link}";
"lng_credits_box_history_entry_gift_in_about" = "Use Stars to unlock content and services on Telegram. {link}"; "lng_credits_box_history_entry_gift_in_about" = "Use Stars to unlock content and services on Telegram. {link}";
"lng_credits_box_history_entry_gift_about_link" = "See Examples {emoji}"; "lng_credits_box_history_entry_gift_about_link" = "See Examples {emoji}";
@ -2461,6 +2488,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_credits_small_balance_about" = "Buy **Stars** and use them on **{bot}** and other miniapps."; "lng_credits_small_balance_about" = "Buy **Stars** and use them on **{bot}** and other miniapps.";
"lng_credits_small_balance_reaction" = "Buy **Stars** and send them to {channel} to support their posts."; "lng_credits_small_balance_reaction" = "Buy **Stars** and send them to {channel} to support their posts.";
"lng_credits_small_balance_subscribe" = "Buy **Stars** and subscribe to **{channel}** and other channels."; "lng_credits_small_balance_subscribe" = "Buy **Stars** and subscribe to **{channel}** and other channels.";
"lng_credits_small_balance_star_gift" = "Buy **Stars** to send gifts to {user} and other contacts.";
"lng_credits_small_balance_fallback" = "Buy **Stars** to unlock content and services on Telegram."; "lng_credits_small_balance_fallback" = "Buy **Stars** to unlock content and services on Telegram.";
"lng_credits_purchase_blocked" = "Sorry, you can't purchase this item with Telegram Stars."; "lng_credits_purchase_blocked" = "Sorry, you can't purchase this item with Telegram Stars.";
"lng_credits_enough" = "You have enough stars at the moment. {link}"; "lng_credits_enough" = "You have enough stars at the moment. {link}";
@ -2980,6 +3008,53 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_gift_stars_incoming" = "Use Stars to unlock content and services on Telegram."; "lng_gift_stars_incoming" = "Use Stars to unlock content and services on Telegram.";
"lng_gift_until" = "Until"; "lng_gift_until" = "Until";
"lng_gift_premium_or_stars" = "Gift Premium or Stars";
"lng_gift_premium_subtitle" = "Gift Premium";
"lng_gift_premium_about" = "Give {name} access to exclusive features with Telegram Premium. {features}";
"lng_gift_premium_features" = "See Features >";
"lng_gift_premium_label" = "Premium";
"lng_gift_stars_subtitle" = "Gift Stars";
"lng_gift_stars_about" = "Give {name} gifts that can be kept on your profile or converted to Stars. {link}";
"lng_gift_stars_link" = "What are Stars >";
"lng_gift_stars_limited" = "limited";
"lng_gift_stars_sold_out" = "sold out";
"lng_gift_stars_tabs_all" = "All Gifts";
"lng_gift_stars_tabs_limited" = "Limited";
"lng_gift_send_title" = "Send a Gift";
"lng_gift_send_message" = "Enter Message";
"lng_gift_send_anonymous" = "Hide My Name";
"lng_gift_send_anonymous_about" = "You can hide your name and message from visitors to {user}'s profile. {recipient} will still see your name and message.";
"lng_gift_send_premium_about" = "Only {user} will see your message.";
"lng_gift_send_button" = "Send a Gift for {cost}";
"lng_gift_sent_title" = "Gift Sent!";
"lng_gift_sent_about#one" = "You spent **{count}** Star from your balance.";
"lng_gift_sent_about#other" = "You spent **{count}** Stars from your balance.";
"lng_gift_limited_of_one" = "unique";
"lng_gift_limited_of_count" = "1 of {amount}";
"lng_gift_anonymous_hint" = "Only you can see the sender's name.";
"lng_gift_hidden_hint" = "This gift is hidden. Only you can see it.";
"lng_gift_visible_hint" = "This gift is visible to visitors of your page.";
"lng_gift_availability" = "Availability";
"lng_gift_from_hidden" = "Hidden User";
"lng_gift_availability_left#one" = "{count} of {amount} left";
"lng_gift_availability_left#other" = "{count} of {amount} left";
"lng_gift_availability_none" = "None of {amount} left";
"lng_gift_display_on_page" = "Display on my Page";
"lng_gift_display_on_page_hide" = "Hide from my Page";
"lng_gift_convert_to_stars#one" = "Convert to {count} Star";
"lng_gift_convert_to_stars#other" = "Convert to {count} Stars";
"lng_gift_convert_sure_title" = "Convert Gift to Stars";
"lng_gift_convert_sure_text#one" = "Do you want to convert this gift from {user} to **{count} Star**?\n\nThis action cannot be undone.";
"lng_gift_convert_sure_text#other" = "Do you want to convert this gift from {user} to **{count} Stars**?\n\nThis action cannot be undone.";
"lng_gift_convert_sure" = "Convert";
"lng_gift_display_done" = "The gift is now shown on your profile page.";
"lng_gift_display_done_hide" = "The gift is now hidden from your profile page.";
"lng_gift_got_stars#one" = "You got **{count} Star** for this gift.";
"lng_gift_got_stars#other" = "You got **{count} Stars** for this gift.";
"lng_gift_sold_out_title" = "Sold Out!";
"lng_gift_sold_out_text#one" = "All {count} gift was already sold.";
"lng_gift_sold_out_text#other" = "All {count} gifts were already sold.";
"lng_accounts_limit_title" = "Limit Reached"; "lng_accounts_limit_title" = "Limit Reached";
"lng_accounts_limit1#one" = "You have reached the limit of **{count}** connected account."; "lng_accounts_limit1#one" = "You have reached the limit of **{count}** connected account.";
"lng_accounts_limit1#other" = "You have reached the limit of **{count}** connected accounts."; "lng_accounts_limit1#other" = "You have reached the limit of **{count}** connected accounts.";
@ -3222,6 +3297,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_replies_discussion_started" = "Discussion started"; "lng_replies_discussion_started" = "Discussion started";
"lng_replies_no_comments" = "No comments here yet..."; "lng_replies_no_comments" = "No comments here yet...";
"lng_verification_codes" = "Verification Codes";
"lng_archived_name" = "Archived chats"; "lng_archived_name" = "Archived chats";
"lng_archived_add" = "Archive"; "lng_archived_add" = "Archive";
"lng_archived_remove" = "Unarchive"; "lng_archived_remove" = "Unarchive";
@ -3246,7 +3323,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_open_link" = "Open"; "lng_open_link" = "Open";
"lng_allow_bot_pass" = "Allow {bot_name} to pass your Telegram name and ID (not your phone number) to the web pages you open via this bot?"; "lng_allow_bot_pass" = "Allow {bot_name} to pass your Telegram name and ID (not your phone number) to the web pages you open via this bot?";
"lng_allow_bot" = "Allow"; "lng_allow_bot" = "Allow";
"lng_allow_bot_webview" = "{bot_name} would like to open its web app to proceed.\n\nIt will be able to access your **IP address** and basic device info."; "lng_allow_bot_webview_details" = "More about this bot {emoji}";
"lng_allow_bot_webview_details_about" = "To launch this web app, you will connect to its website.\n\nIt will be able to access your **IP address** and basic device info.";
"lng_url_auth_open_confirm" = "Do you want to open {link}?"; "lng_url_auth_open_confirm" = "Do you want to open {link}?";
"lng_url_auth_login_option" = "Log in to {domain} as {user}"; "lng_url_auth_login_option" = "Log in to {domain} as {user}";
"lng_url_auth_allow_messages" = "Allow {bot} to send me messages"; "lng_url_auth_allow_messages" = "Allow {bot} to send me messages";
@ -3498,6 +3576,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_context_animated_reactions_many#one" = "Reactions contain emoji from **{count} pack**."; "lng_context_animated_reactions_many#one" = "Reactions contain emoji from **{count} pack**.";
"lng_context_animated_reactions_many#other" = "Reactions contain emoji from **{count} packs**."; "lng_context_animated_reactions_many#other" = "Reactions contain emoji from **{count} packs**.";
"lng_context_noforwards_info_channel" = "Copying and forwarding is not allowed in this channel.";
"lng_context_noforwards_info_group" = "Copying and forwarding is not allowed in this group.";
"lng_context_noforwards_info_bot" = "Copying and forwarding is not allowed from this bot.";
"lng_context_spoiler_effect" = "Hide with Spoiler"; "lng_context_spoiler_effect" = "Hide with Spoiler";
"lng_context_disable_spoiler" = "Remove Spoiler"; "lng_context_disable_spoiler" = "Remove Spoiler";
"lng_context_make_paid" = "Make This Content Paid"; "lng_context_make_paid" = "Make This Content Paid";
@ -3608,6 +3690,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_reply_header_short" = "Reply"; "lng_reply_header_short" = "Reply";
"lng_reply_quote_selected" = "Quote Selected"; "lng_reply_quote_selected" = "Quote Selected";
"lng_reply_from_private_chat" = "This reply is from a private chat."; "lng_reply_from_private_chat" = "This reply is from a private chat.";
"lng_reply_quote_long_title" = "Quote too long!";
"lng_reply_quote_long_text" = "The selected text is too long to quote.";
"lng_link_options_header" = "Link Preview Settings"; "lng_link_options_header" = "Link Preview Settings";
"lng_link_header_short" = "Link"; "lng_link_header_short" = "Link";
"lng_link_move_up" = "Move Up"; "lng_link_move_up" = "Move Up";
@ -4233,6 +4317,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_rights_restriction_for_all" = "This option is disabled for all members in Group Permissions."; "lng_rights_restriction_for_all" = "This option is disabled for all members in Group Permissions.";
"lng_rights_permission_for_all" = "This option is enabled for all members in Group Permissions."; "lng_rights_permission_for_all" = "This option is enabled for all members in Group Permissions.";
"lng_rights_permission_unavailable" = "This permission is not available in public groups."; "lng_rights_permission_unavailable" = "This permission is not available in public groups.";
"lng_rights_permission_in_discuss" = "This permission is not available in discussion groups.";
"lng_rights_permission_cant_edit" = "You cannot change this permission."; "lng_rights_permission_cant_edit" = "You cannot change this permission.";
"lng_rights_user_restrictions" = "User permissions"; "lng_rights_user_restrictions" = "User permissions";
"lng_rights_user_restrictions_header" = "What can this member do?"; "lng_rights_user_restrictions_header" = "What can this member do?";
@ -5550,6 +5635,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_qr_box_quality1" = "Normal"; "lng_qr_box_quality1" = "Normal";
"lng_qr_box_quality2" = "High"; "lng_qr_box_quality2" = "High";
"lng_qr_box_quality3" = "Very High"; "lng_qr_box_quality3" = "Very High";
"lng_qr_box_transparent_background" = "Transparent Background";
"lng_qr_box_font_size" = "Font size";
// Wnd specific // Wnd specific

View file

@ -10,7 +10,7 @@
<Identity Name="TelegramMessengerLLP.TelegramDesktop" <Identity Name="TelegramMessengerLLP.TelegramDesktop"
ProcessorArchitecture="ARCHITECTURE" ProcessorArchitecture="ARCHITECTURE"
Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A" Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
Version="5.5.5.0" /> Version="5.6.3.0" />
<Properties> <Properties>
<DisplayName>Telegram Desktop</DisplayName> <DisplayName>Telegram Desktop</DisplayName>
<PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName> <PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName>

View file

@ -16,6 +16,8 @@
</compatibility> </compatibility>
<application xmlns="urn:schemas-microsoft-com:asm.v3"> <application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings> <windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitor</dpiAwareness>
<activeCodePage xmlns="http://schemas.microsoft.com/SMI/2019/WindowsSettings">UTF-8</activeCodePage> <activeCodePage xmlns="http://schemas.microsoft.com/SMI/2019/WindowsSettings">UTF-8</activeCodePage>
</windowsSettings> </windowsSettings>
</application> </application>

View file

@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico"
// //
VS_VERSION_INFO VERSIONINFO VS_VERSION_INFO VERSIONINFO
FILEVERSION 5,5,5,0 FILEVERSION 5,6,3,0
PRODUCTVERSION 5,5,5,0 PRODUCTVERSION 5,6,3,0
FILEFLAGSMASK 0x3fL FILEFLAGSMASK 0x3fL
#ifdef _DEBUG #ifdef _DEBUG
FILEFLAGS 0x1L FILEFLAGS 0x1L
@ -62,10 +62,10 @@ BEGIN
BEGIN BEGIN
VALUE "CompanyName", "Radolyn Labs" VALUE "CompanyName", "Radolyn Labs"
VALUE "FileDescription", "AyuGram Desktop" VALUE "FileDescription", "AyuGram Desktop"
VALUE "FileVersion", "5.5.5.0" VALUE "FileVersion", "5.6.3.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2024" VALUE "LegalCopyright", "Copyright (C) 2014-2024"
VALUE "ProductName", "AyuGram Desktop" VALUE "ProductName", "AyuGram Desktop"
VALUE "ProductVersion", "5.5.5.0" VALUE "ProductVersion", "5.6.3.0"
END END
END END
BLOCK "VarFileInfo" BLOCK "VarFileInfo"

View file

@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
// //
VS_VERSION_INFO VERSIONINFO VS_VERSION_INFO VERSIONINFO
FILEVERSION 5,5,5,0 FILEVERSION 5,6,3,0
PRODUCTVERSION 5,5,5,0 PRODUCTVERSION 5,6,3,0
FILEFLAGSMASK 0x3fL FILEFLAGSMASK 0x3fL
#ifdef _DEBUG #ifdef _DEBUG
FILEFLAGS 0x1L FILEFLAGS 0x1L
@ -53,10 +53,10 @@ BEGIN
BEGIN BEGIN
VALUE "CompanyName", "Radolyn Labs" VALUE "CompanyName", "Radolyn Labs"
VALUE "FileDescription", "AyuGram Desktop Updater" VALUE "FileDescription", "AyuGram Desktop Updater"
VALUE "FileVersion", "5.5.5.0" VALUE "FileVersion", "5.6.3.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2024" VALUE "LegalCopyright", "Copyright (C) 2014-2024"
VALUE "ProductName", "AyuGram Desktop" VALUE "ProductName", "AyuGram Desktop"
VALUE "ProductVersion", "5.5.5.0" VALUE "ProductVersion", "5.6.3.0"
END END
END END
BLOCK "VarFileInfo" BLOCK "VarFileInfo"

View file

@ -39,6 +39,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/layers/generic_box.h" #include "ui/layers/generic_box.h"
#include "ui/text/text_utilities.h" #include "ui/text/text_utilities.h"
#include <QtGui/QGuiApplication>
#include <QtGui/QClipboard>
namespace Api { namespace Api {
namespace { namespace {
@ -503,11 +506,19 @@ void ActivateBotCommand(ClickHandlerContext context, int row, int column) {
bot->session().attachWebView().open({ bot->session().attachWebView().open({
.bot = bot, .bot = bot,
.context = { .controller = controller }, .context = { .controller = controller },
.button = {.text = button->text, .url = button->data }, .button = { .text = button->text, .url = button->data },
.source = InlineBots::WebViewSourceButton{ .simple = true }, .source = InlineBots::WebViewSourceButton{ .simple = true },
}); });
} }
} break; } break;
case ButtonType::CopyText: {
const auto text = QString::fromUtf8(button->data);
if (!text.isEmpty()) {
QGuiApplication::clipboard()->setText(text);
controller->showToast(tr::lng_text_copied(tr::now));
}
} break;
} }
} }

View file

@ -275,7 +275,6 @@ void ConfirmSubscriptionBox(
: 0; : 0;
state->api->request( state->api->request(
MTPpayments_SendStarsForm( MTPpayments_SendStarsForm(
MTP_flags(0),
MTP_long(formId), MTP_long(formId),
MTP_inputInvoiceChatInviteSubscription(MTP_string(hash))) MTP_inputInvoiceChatInviteSubscription(MTP_string(hash)))
).done([=](const MTPpayments_PaymentResult &result) { ).done([=](const MTPpayments_PaymentResult &result) {

View file

@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_updates.h" #include "api/api_updates.h"
#include "apiwrap.h" #include "apiwrap.h"
#include "base/unixtime.h" #include "base/unixtime.h"
#include "data/components/credits.h"
#include "data/data_channel.h" #include "data/data_channel.h"
#include "data/data_document.h" #include "data/data_document.h"
#include "data/data_peer.h" #include "data/data_peer.h"
@ -69,10 +70,12 @@ constexpr auto kTransactionsLimit = 100;
}, [](const auto &) { }, [](const auto &) {
return PeerId(0); return PeerId(0);
}).value; }).value;
const auto stargift = tl.data().vstargift();
const auto incoming = (int64(tl.data().vstars().v) >= 0);
return Data::CreditsHistoryEntry{ return Data::CreditsHistoryEntry{
.id = qs(tl.data().vid()), .id = qs(tl.data().vid()),
.title = qs(tl.data().vtitle().value_or_empty()), .title = qs(tl.data().vtitle().value_or_empty()),
.description = qs(tl.data().vdescription().value_or_empty()), .description = { qs(tl.data().vdescription().value_or_empty()) },
.date = base::unixtime::parse(tl.data().vdate().v), .date = base::unixtime::parse(tl.data().vdate().v),
.photoId = photo ? photo->id : 0, .photoId = photo ? photo->id : 0,
.extended = std::move(extended), .extended = std::move(extended),
@ -81,6 +84,9 @@ constexpr auto kTransactionsLimit = 100;
.barePeerId = barePeerId, .barePeerId = barePeerId,
.bareGiveawayMsgId = uint64( .bareGiveawayMsgId = uint64(
tl.data().vgiveaway_post_id().value_or_empty()), tl.data().vgiveaway_post_id().value_or_empty()),
.bareGiftStickerId = (stargift
? owner->processDocument(stargift->data().vsticker())->id
: 0),
.peerType = tl.data().vpeer().match([](const HistoryPeerTL &) { .peerType = tl.data().vpeer().match([](const HistoryPeerTL &) {
return Data::CreditsHistoryEntry::PeerType::Peer; return Data::CreditsHistoryEntry::PeerType::Peer;
}, [](const MTPDstarsTransactionPeerPlayMarket &) { }, [](const MTPDstarsTransactionPeerPlayMarket &) {
@ -104,12 +110,16 @@ constexpr auto kTransactionsLimit = 100;
? base::unixtime::parse(tl.data().vtransaction_date()->v) ? base::unixtime::parse(tl.data().vtransaction_date()->v)
: QDateTime(), : QDateTime(),
.successLink = qs(tl.data().vtransaction_url().value_or_empty()), .successLink = qs(tl.data().vtransaction_url().value_or_empty()),
.convertStars = int(stargift
? stargift->data().vconvert_stars().v
: 0),
.converted = stargift && incoming,
.reaction = tl.data().is_reaction(), .reaction = tl.data().is_reaction(),
.refunded = tl.data().is_refund(), .refunded = tl.data().is_refund(),
.pending = tl.data().is_pending(), .pending = tl.data().is_pending(),
.failed = tl.data().is_failed(), .failed = tl.data().is_failed(),
.in = (int64(tl.data().vstars().v) >= 0), .in = incoming,
.gift = tl.data().is_gift(), .gift = tl.data().is_gift() || stargift.has_value(),
}; };
} }
@ -239,6 +249,8 @@ void CreditsStatus::request(
_peer->isSelf() ? MTP_inputPeerSelf() : _peer->input _peer->isSelf() ? MTP_inputPeerSelf() : _peer->input
)).done([=](const TLResult &result) { )).done([=](const TLResult &result) {
_requestId = 0; _requestId = 0;
const auto balance = result.data().vbalance().v;
_peer->session().credits().apply(_peer->id, balance);
if (const auto onstack = done) { if (const auto onstack = done) {
onstack(StatusFromTL(result, _peer)); onstack(StatusFromTL(result, _peer));
} }

View file

@ -37,7 +37,8 @@ MTPVector<MTPDocumentAttribute> ComposeSendingDocumentAttributes(
MTP_int(dimensions.width()), MTP_int(dimensions.width()),
MTP_int(dimensions.height()), MTP_int(dimensions.height()),
MTPint(), // preload_prefix_size MTPint(), // preload_prefix_size
MTPdouble())); // video_start_ts MTPdouble(), // video_start_ts
MTPstring())); // video_codec
} else { } else {
attributes.push_back(MTP_documentAttributeImageSize( attributes.push_back(MTP_documentAttributeImageSize(
MTP_int(dimensions.width()), MTP_int(dimensions.width()),

View file

@ -550,6 +550,24 @@ Payments::InvoicePremiumGiftCode PremiumGiftCodeOptions::invoice(
}; };
} }
std::vector<GiftOptionData> PremiumGiftCodeOptions::optionsForPeer() const {
auto result = std::vector<GiftOptionData>();
if (!_optionsForOnePerson.currency.isEmpty()) {
const auto count = int(_optionsForOnePerson.months.size());
result.reserve(count);
for (auto i = 0; i != count; ++i) {
Assert(i < _optionsForOnePerson.totalCosts.size());
result.push_back({
.cost = _optionsForOnePerson.totalCosts[i],
.currency = _optionsForOnePerson.currency,
.months = _optionsForOnePerson.months[i],
});
}
}
return result;
}
Data::PremiumSubscriptionOptions PremiumGiftCodeOptions::options(int amount) { Data::PremiumSubscriptionOptions PremiumGiftCodeOptions::options(int amount) {
const auto it = _subscriptionOptions.find(amount); const auto it = _subscriptionOptions.find(amount);
if (it != end(_subscriptionOptions)) { if (it != end(_subscriptionOptions)) {
@ -571,6 +589,41 @@ Data::PremiumSubscriptionOptions PremiumGiftCodeOptions::options(int amount) {
} }
} }
auto PremiumGiftCodeOptions::requestStarGifts()
-> rpl::producer<rpl::no_value, QString> {
return [=](auto consumer) {
auto lifetime = rpl::lifetime();
_api.request(MTPpayments_GetStarGifts(
MTP_int(0)
)).done([=](const MTPpayments_StarGifts &result) {
result.match([&](const MTPDpayments_starGifts &data) {
_giftsHash = data.vhash().v;
const auto &list = data.vgifts().v;
const auto session = &_peer->session();
auto gifts = std::vector<StarGift>();
gifts.reserve(list.size());
for (const auto &gift : list) {
if (auto parsed = FromTL(session, gift)) {
gifts.push_back(std::move(*parsed));
}
}
_gifts = std::move(gifts);
}, [&](const MTPDpayments_starGiftsNotModified &) {
});
consumer.put_done();
}).fail([=](const MTP::Error &error) {
consumer.put_error_copy(error.type());
}).send();
return lifetime;
};
}
const std::vector<StarGift> &PremiumGiftCodeOptions::starGifts() const {
return _gifts;
}
int PremiumGiftCodeOptions::giveawayBoostsPerPremium() const { int PremiumGiftCodeOptions::giveawayBoostsPerPremium() const {
constexpr auto kFallbackCount = 4; constexpr auto kFallbackCount = 4;
return _peer->session().appConfig().get<int>( return _peer->session().appConfig().get<int>(
@ -705,4 +758,56 @@ rpl::producer<DocumentData*> RandomHelloStickerValue(
}) | rpl::take(1) | rpl::map(random)); }) | rpl::take(1) | rpl::map(random));
} }
std::optional<StarGift> FromTL(
not_null<Main::Session*> session,
const MTPstarGift &gift) {
const auto &data = gift.data();
const auto document = session->data().processDocument(
data.vsticker());
const auto remaining = data.vavailability_remains();
const auto total = data.vavailability_total();
if (!document->sticker()) {
return {};
}
return StarGift{
.id = uint64(data.vid().v),
.stars = int64(data.vstars().v),
.convertStars = int64(data.vconvert_stars().v),
.document = document,
.limitedLeft = remaining.value_or_empty(),
.limitedCount = total.value_or_empty(),
};
}
std::optional<UserStarGift> FromTL(
not_null<UserData*> to,
const MTPuserStarGift &gift) {
const auto session = &to->session();
const auto &data = gift.data();
auto parsed = FromTL(session, data.vgift());
if (!parsed) {
return {};
}
return UserStarGift{
.gift = std::move(*parsed),
.message = (data.vmessage()
? TextWithEntities{
.text = qs(data.vmessage()->data().vtext()),
.entities = Api::EntitiesFromMTP(
session,
data.vmessage()->data().ventities().v),
}
: TextWithEntities()),
.convertStars = int64(data.vconvert_stars().value_or_empty()),
.fromId = (data.vfrom_id()
? peerFromUser(data.vfrom_id()->v)
: PeerId()),
.messageId = data.vmsg_id().value_or_empty(),
.date = data.vdate().v,
.anonymous = data.is_name_hidden(),
.hidden = data.is_unsaved(),
.mine = to->isSelf(),
};
}
} // namespace Api } // namespace Api

View file

@ -67,6 +67,33 @@ struct GiveawayInfo {
} }
}; };
struct GiftOptionData {
int64 cost = 0;
QString currency;
int months = 0;
};
struct StarGift {
uint64 id = 0;
int64 stars = 0;
int64 convertStars = 0;
not_null<DocumentData*> document;
int limitedLeft = 0;
int limitedCount = 0;
};
struct UserStarGift {
StarGift gift;
TextWithEntities message;
int64 convertStars = 0;
PeerId fromId = 0;
MsgId messageId = 0;
TimeId date = 0;
bool anonymous = false;
bool hidden = false;
bool mine = false;
};
class Premium final { class Premium final {
public: public:
explicit Premium(not_null<ApiWrap*> api); explicit Premium(not_null<ApiWrap*> api);
@ -171,6 +198,7 @@ public:
PremiumGiftCodeOptions(not_null<PeerData*> peer); PremiumGiftCodeOptions(not_null<PeerData*> peer);
[[nodiscard]] rpl::producer<rpl::no_value, QString> request(); [[nodiscard]] rpl::producer<rpl::no_value, QString> request();
[[nodiscard]] std::vector<GiftOptionData> optionsForPeer() const;
[[nodiscard]] Data::PremiumSubscriptionOptions options(int amount); [[nodiscard]] Data::PremiumSubscriptionOptions options(int amount);
[[nodiscard]] const std::vector<int> &availablePresets() const; [[nodiscard]] const std::vector<int> &availablePresets() const;
[[nodiscard]] int monthsFromPreset(int monthsIndex); [[nodiscard]] int monthsFromPreset(int monthsIndex);
@ -187,6 +215,9 @@ public:
[[nodiscard]] int giveawayPeriodMax() const; [[nodiscard]] int giveawayPeriodMax() const;
[[nodiscard]] bool giveawayGiftsPurchaseAvailable() const; [[nodiscard]] bool giveawayGiftsPurchaseAvailable() const;
[[nodiscard]] rpl::producer<rpl::no_value, QString> requestStarGifts();
[[nodiscard]] const std::vector<StarGift> &starGifts() const;
private: private:
struct Token final { struct Token final {
int users = 0; int users = 0;
@ -206,7 +237,7 @@ private:
base::flat_map<Amount, PremiumSubscriptionOptions> _subscriptionOptions; base::flat_map<Amount, PremiumSubscriptionOptions> _subscriptionOptions;
struct { struct {
std::vector<int> months; std::vector<int> months;
std::vector<float64> totalCosts; std::vector<int64> totalCosts;
QString currency; QString currency;
} _optionsForOnePerson; } _optionsForOnePerson;
@ -214,6 +245,9 @@ private:
base::flat_map<Token, Store> _stores; base::flat_map<Token, Store> _stores;
int32 _giftsHash = 0;
std::vector<StarGift> _gifts;
MTP::Sender _api; MTP::Sender _api;
}; };
@ -242,4 +276,11 @@ enum class RequirePremiumState {
[[nodiscard]] rpl::producer<DocumentData*> RandomHelloStickerValue( [[nodiscard]] rpl::producer<DocumentData*> RandomHelloStickerValue(
not_null<Main::Session*> session); not_null<Main::Session*> session);
[[nodiscard]] std::optional<StarGift> FromTL(
not_null<Main::Session*> session,
const MTPstarGift &gift);
[[nodiscard]] std::optional<UserStarGift> FromTL(
not_null<UserData*> to,
const MTPuserStarGift &gift);
} // namespace Api } // namespace Api

View file

@ -10,10 +10,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "apiwrap.h" #include "apiwrap.h"
#include "data/data_peer.h" #include "data/data_peer.h"
#include "data/data_photo.h" #include "data/data_photo.h"
#include "data/data_report.h"
#include "data/data_user.h" #include "data/data_user.h"
#include "lang/lang_keys.h" #include "lang/lang_keys.h"
#include "main/main_session.h" #include "main/main_session.h"
#include "ui/boxes/report_box.h" #include "ui/boxes/report_box_graphics.h"
#include "ui/layers/show.h" #include "ui/layers/show.h"
namespace Api { namespace Api {
@ -40,15 +41,11 @@ MTPreportReason ReasonToTL(const Ui::ReportReason &reason) {
} // namespace } // namespace
void SendReport( void SendReport(
std::shared_ptr<Ui::Show> show, std::shared_ptr<Ui::Show> show,
not_null<PeerData*> peer, not_null<PeerData*> peer,
Ui::ReportReason reason, Ui::ReportReason reason,
const QString &comment, const QString &comment,
std::variant< std::variant<v::null_t, not_null<PhotoData*>> data) {
v::null_t,
MessageIdsList,
not_null<PhotoData*>,
StoryId> data) {
auto done = [=] { auto done = [=] {
show->showToast(tr::lng_report_thanks(tr::now)); show->showToast(tr::lng_report_thanks(tr::now));
}; };
@ -58,18 +55,6 @@ void SendReport(
ReasonToTL(reason), ReasonToTL(reason),
MTP_string(comment) MTP_string(comment)
)).done(std::move(done)).send(); )).done(std::move(done)).send();
}, [&](const MessageIdsList &ids) {
auto apiIds = QVector<MTPint>();
apiIds.reserve(ids.size());
for (const auto &fullId : ids) {
apiIds.push_back(MTP_int(fullId.msg));
}
peer->session().api().request(MTPmessages_Report(
peer->input,
MTP_vector<MTPint>(apiIds),
ReasonToTL(reason),
MTP_string(comment)
)).done(std::move(done)).send();
}, [&](not_null<PhotoData*> photo) { }, [&](not_null<PhotoData*> photo) {
peer->session().api().request(MTPaccount_ReportProfilePhoto( peer->session().api().request(MTPaccount_ReportProfilePhoto(
peer->input, peer->input,
@ -77,14 +62,93 @@ void SendReport(
ReasonToTL(reason), ReasonToTL(reason),
MTP_string(comment) MTP_string(comment)
)).done(std::move(done)).send(); )).done(std::move(done)).send();
}, [&](StoryId id) {
peer->session().api().request(MTPstories_Report(
peer->input,
MTP_vector<MTPint>(1, MTP_int(id)),
ReasonToTL(reason),
MTP_string(comment)
)).done(std::move(done)).send();
}); });
} }
auto CreateReportMessagesOrStoriesCallback(
std::shared_ptr<Ui::Show> show,
not_null<PeerData*> peer)
-> Fn<void(Data::ReportInput, Fn<void(ReportResult)>)> {
using TLChoose = MTPDreportResultChooseOption;
using TLAddComment = MTPDreportResultAddComment;
using TLReported = MTPDreportResultReported;
using Result = ReportResult;
struct State final {
#ifdef _DEBUG
~State() {
qDebug() << "Messages or Stories Report ~State().";
}
#endif
mtpRequestId requestId = 0;
};
const auto state = std::make_shared<State>();
return [=](
Data::ReportInput reportInput,
Fn<void(Result)> done) {
auto apiIds = QVector<MTPint>();
apiIds.reserve(reportInput.ids.size() + reportInput.stories.size());
for (const auto &id : reportInput.ids) {
apiIds.push_back(MTP_int(id));
}
for (const auto &story : reportInput.stories) {
apiIds.push_back(MTP_int(story));
}
const auto received = [=](
const MTPReportResult &result,
mtpRequestId requestId) {
if (state->requestId != requestId) {
return;
}
state->requestId = 0;
done(result.match([&](const TLChoose &data) {
const auto t = qs(data.vtitle());
auto list = Result::Options();
list.reserve(data.voptions().v.size());
for (const auto &tl : data.voptions().v) {
list.emplace_back(Result::Option{
.id = tl.data().voption().v,
.text = qs(tl.data().vtext()),
});
}
return Result{ .options = std::move(list), .title = t };
}, [&](const TLAddComment &data) -> Result {
return {
.commentOption = ReportResult::CommentOption{
.optional = data.is_optional(),
.id = data.voption().v,
}
};
}, [&](const TLReported &data) -> Result {
return { .successful = true };
}));
};
const auto fail = [=](const MTP::Error &error) {
state->requestId = 0;
done({ .error = error.type() });
};
if (!reportInput.stories.empty()) {
state->requestId = peer->session().api().request(
MTPstories_Report(
peer->input,
MTP_vector<MTPint>(apiIds),
MTP_bytes(reportInput.optionId),
MTP_string(reportInput.comment))
).done(received).fail(fail).send();
} else {
state->requestId = peer->session().api().request(
MTPmessages_Report(
peer->input,
MTP_vector<MTPint>(apiIds),
MTP_bytes(reportInput.optionId),
MTP_string(reportInput.comment))
).done(received).fail(fail).send();
}
};
}
} // namespace Api } // namespace Api

View file

@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#pragma once #pragma once
class HistoryItem;
class PeerData; class PeerData;
class PhotoData; class PhotoData;
@ -15,17 +16,41 @@ class Show;
enum class ReportReason; enum class ReportReason;
} // namespace Ui } // namespace Ui
namespace Data {
struct ReportInput;
} // namespace Data
namespace Api { namespace Api {
struct ReportResult final {
using Id = QByteArray;
struct Option final {
Id id = 0;
QString text;
};
using Options = std::vector<Option>;
Options options;
QString title;
QString error;
QString comment;
struct CommentOption {
bool optional = false;
Id id = 0;
};
std::optional<CommentOption> commentOption;
bool successful = false;
};
void SendReport( void SendReport(
std::shared_ptr<Ui::Show> show, std::shared_ptr<Ui::Show> show,
not_null<PeerData*> peer, not_null<PeerData*> peer,
Ui::ReportReason reason, Ui::ReportReason reason,
const QString &comment, const QString &comment,
std::variant< std::variant<v::null_t, not_null<PhotoData*>> data);
v::null_t,
MessageIdsList, [[nodiscard]] auto CreateReportMessagesOrStoriesCallback(
not_null<PhotoData*>, std::shared_ptr<Ui::Show> show,
StoryId> data); not_null<PeerData*> peer)
-> Fn<void(Data::ReportInput, Fn<void(ReportResult)>)>;
} // namespace Api } // namespace Api

View file

@ -547,7 +547,7 @@ void SendConfirmedFile(
MTP_flags(Flag::f_document MTP_flags(Flag::f_document
| (file->spoiler ? Flag::f_spoiler : Flag())), | (file->spoiler ? Flag::f_spoiler : Flag())),
file->document, file->document,
MTPDocument(), // alt_document MTPVector<MTPDocument>(), // alt_documents
MTPint()); MTPint());
} else if (file->type == SendMediaType::Audio) { } else if (file->type == SendMediaType::Audio) {
const auto ttlSeconds = file->to.options.ttlSeconds; const auto ttlSeconds = file->to.options.ttlSeconds;
@ -572,7 +572,7 @@ void SendConfirmedFile(
| (isVoice ? Flag::f_voice : Flag()) | (isVoice ? Flag::f_voice : Flag())
| (ttlSeconds ? Flag::f_ttl_seconds : Flag())), | (ttlSeconds ? Flag::f_ttl_seconds : Flag())),
file->document, file->document,
MTPDocument(), // alt_document MTPVector<MTPDocument>(), // alt_documents
MTP_int(ttlSeconds)); MTP_int(ttlSeconds));
} else { } else {
Unexpected("Type in sendFilesConfirmed."); Unexpected("Type in sendFilesConfirmed.");

View file

@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/premium_limits_box.h" #include "boxes/premium_limits_box.h"
#include "boxes/premium_preview_box.h" #include "boxes/premium_preview_box.h"
#include "chat_helpers/emoji_suggestions_widget.h" #include "chat_helpers/emoji_suggestions_widget.h"
#include "chat_helpers/field_autocomplete.h"
#include "chat_helpers/message_field.h" #include "chat_helpers/message_field.h"
#include "chat_helpers/tabbed_panel.h" #include "chat_helpers/tabbed_panel.h"
#include "chat_helpers/tabbed_selector.h" #include "chat_helpers/tabbed_selector.h"
@ -371,6 +372,14 @@ void EditCaptionBox::StartPhotoEdit(
}); });
} }
void EditCaptionBox::showFinished() {
if (const auto raw = _autocomplete.get()) {
InvokeQueued(raw, [=] {
raw->raise();
});
}
}
void EditCaptionBox::prepare() { void EditCaptionBox::prepare() {
const auto button = addButton(tr::lng_settings_save(), [=] { save(); }); const auto button = addButton(tr::lng_settings_save(), [=] { save(); });
addButton(tr::lng_cancel(), [=] { closeBox(); }); addButton(tr::lng_cancel(), [=] { closeBox(); });
@ -525,6 +534,7 @@ void EditCaptionBox::setupField() {
_field.get(), _field.get(),
Window::GifPauseReason::Layer, Window::GifPauseReason::Layer,
allow); allow);
setupFieldAutocomplete();
Ui::Emoji::SuggestionsController::Init( Ui::Emoji::SuggestionsController::Init(
getDelegate()->outerContainer(), getDelegate()->outerContainer(),
_field, _field,
@ -562,6 +572,55 @@ void EditCaptionBox::setupField() {
}); });
} }
void EditCaptionBox::setupFieldAutocomplete() {
const auto parent = getDelegate()->outerContainer();
ChatHelpers::InitFieldAutocomplete(_autocomplete, {
.parent = parent,
.show = _controller->uiShow(),
.field = _field.get(),
.peer = _historyItem->history()->peer,
.features = [=] {
auto result = ChatHelpers::ComposeFeatures();
result.autocompleteCommands = false;
result.suggestStickersByEmoji = false;
return result;
},
});
const auto raw = _autocomplete.get();
const auto scheduled = std::make_shared<bool>();
const auto recountPostponed = [=] {
if (*scheduled) {
return;
}
*scheduled = true;
Ui::PostponeCall(raw, [=] {
*scheduled = false;
auto field = Ui::MapFrom(parent, this, _field->geometry());
_autocomplete->setBoundings(QRect(
field.x() - _field->x(),
st::defaultBox.margin.top(),
width(),
(field.y()
+ st::defaultComposeFiles.caption.textMargins.top()
+ st::defaultComposeFiles.caption.placeholderShift
+ st::defaultComposeFiles.caption.placeholderFont->height
- st::defaultBox.margin.top())));
});
};
for (auto w = (QWidget*)_field.get(); w; w = w->parentWidget()) {
base::install_event_filter(raw, w, [=](not_null<QEvent*> e) {
if (e->type() == QEvent::Move || e->type() == QEvent::Resize) {
recountPostponed();
}
return base::EventFilterResult::Continue;
});
if (w == parent) {
break;
}
}
}
void EditCaptionBox::setInitialText() { void EditCaptionBox::setInitialText() {
_field->setTextWithTags( _field->setTextWithTags(
_initialText, _initialText,

View file

@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace ChatHelpers { namespace ChatHelpers {
class TabbedPanel; class TabbedPanel;
class FieldAutocomplete;
} // namespace ChatHelpers } // namespace ChatHelpers
namespace Window { namespace Window {
@ -68,6 +69,8 @@ public:
bool invertCaption, bool invertCaption,
Fn<void()> saved); Fn<void()> saved);
void showFinished() override;
protected: protected:
void prepare() override; void prepare() override;
void setInnerFocus() override; void setInnerFocus() override;
@ -81,6 +84,7 @@ private:
void setupEditEventHandler(); void setupEditEventHandler();
void setupPhotoEditorEventHandler(); void setupPhotoEditorEventHandler();
void setupField(); void setupField();
void setupFieldAutocomplete();
void setupControls(); void setupControls();
void setInitialText(); void setInitialText();
@ -115,6 +119,8 @@ private:
const base::unique_qptr<Ui::InputField> _field; const base::unique_qptr<Ui::InputField> _field;
const base::unique_qptr<Ui::EmojiButton> _emojiToggle; const base::unique_qptr<Ui::EmojiButton> _emojiToggle;
std::unique_ptr<ChatHelpers::FieldAutocomplete> _autocomplete;
base::unique_qptr<Ui::AbstractSinglePreview> _content; base::unique_qptr<Ui::AbstractSinglePreview> _content;
base::unique_qptr<ChatHelpers::TabbedPanel> _emojiPanel; base::unique_qptr<ChatHelpers::TabbedPanel> _emojiPanel;
base::unique_qptr<QObject> _emojiFilter; base::unique_qptr<QObject> _emojiFilter;

View file

@ -356,11 +356,12 @@ void PrivacyExceptionsBoxController::rowClicked(not_null<PeerListRow*> row) {
auto PrivacyExceptionsBoxController::createRow(not_null<History*> history) auto PrivacyExceptionsBoxController::createRow(not_null<History*> history)
-> std::unique_ptr<Row> { -> std::unique_ptr<Row> {
if (history->peer->isSelf() || history->peer->isRepliesChat()) { const auto peer = history->peer;
if (peer->isSelf() || peer->isRepliesChat() || peer->isVerifyCodes()) {
return nullptr; return nullptr;
} else if (!history->peer->isUser() } else if (!peer->isUser()
&& !history->peer->isChat() && !peer->isChat()
&& !history->peer->isMegagroup()) { && !peer->isMegagroup()) {
return nullptr; return nullptr;
} }
auto result = std::make_unique<Row>(history); auto result = std::make_unique<Row>(history);

View file

@ -131,10 +131,13 @@ ExceptionRow::ExceptionRow(not_null<History*> history) : Row(history) {
} }
QString ExceptionRow::generateName() { QString ExceptionRow::generateName() {
return peer()->isSelf() const auto peer = this->peer();
return peer->isSelf()
? tr::lng_saved_messages(tr::now) ? tr::lng_saved_messages(tr::now)
: peer()->isRepliesChat() : peer->isRepliesChat()
? tr::lng_replies_messages(tr::now) ? tr::lng_replies_messages(tr::now)
: peer->isVerifyCodes()
? tr::lng_verification_codes(tr::now)
: Row::generateName(); : Row::generateName();
} }
@ -152,10 +155,11 @@ PaintRoundImageCallback ExceptionRow::generatePaintUserpicCallback(
return ForceRoundUserpicCallback(peer); return ForceRoundUserpicCallback(peer);
} }
return [=](Painter &p, int x, int y, int outerWidth, int size) mutable { return [=](Painter &p, int x, int y, int outerWidth, int size) mutable {
using namespace Ui;
if (saved) { if (saved) {
Ui::EmptyUserpic::PaintSavedMessages(p, x, y, outerWidth, size); EmptyUserpic::PaintSavedMessages(p, x, y, outerWidth, size);
} else if (replies) { } else if (replies) {
Ui::EmptyUserpic::PaintRepliesMessages(p, x, y, outerWidth, size); EmptyUserpic::PaintRepliesMessages(p, x, y, outerWidth, size);
} else { } else {
peer->paintUserpicLeft(p, userpic, x, y, outerWidth, size); peer->paintUserpicLeft(p, userpic, x, y, outerWidth, size);
} }

View file

@ -122,9 +122,11 @@ void FilterChatsPreview::paintEvent(QPaintEvent *e) {
top += st.height; top += st.height;
} }
for (auto &[history, userpic, name, button] : _removePeer) { for (auto &[history, userpic, name, button] : _removePeer) {
const auto savedMessages = history->peer->isSelf(); const auto peer = history->peer;
const auto repliesMessages = history->peer->isRepliesChat(); const auto savedMessages = peer->isSelf();
if (savedMessages || repliesMessages) { const auto repliesMessages = peer->isRepliesChat();
const auto verifyCodes = peer->isVerifyCodes();
if (savedMessages || repliesMessages || verifyCodes) {
if (savedMessages) { if (savedMessages) {
Ui::EmptyUserpic::PaintSavedMessages( Ui::EmptyUserpic::PaintSavedMessages(
p, p,
@ -132,13 +134,21 @@ void FilterChatsPreview::paintEvent(QPaintEvent *e) {
top + iconTop, top + iconTop,
width(), width(),
st.photoSize); st.photoSize);
} else { } else if (repliesMessages) {
Ui::EmptyUserpic::PaintRepliesMessages( Ui::EmptyUserpic::PaintRepliesMessages(
p, p,
iconLeft, iconLeft,
top + iconTop, top + iconTop,
width(), width(),
st.photoSize); st.photoSize);
} else {
history->peer->paintUserpicLeft(
p,
userpic,
iconLeft,
top + iconTop,
width(),
st.photoSize);
} }
p.setPen(st::contactsNameFg); p.setPen(st::contactsNameFg);
p.drawTextLeft( p.drawTextLeft(
@ -147,7 +157,9 @@ void FilterChatsPreview::paintEvent(QPaintEvent *e) {
width(), width(),
(savedMessages (savedMessages
? tr::lng_saved_messages(tr::now) ? tr::lng_saved_messages(tr::now)
: tr::lng_replies_messages(tr::now))); : repliesMessages
? tr::lng_replies_messages(tr::now)
: tr::lng_verification_codes(tr::now)));
} else { } else {
history->peer->paintUserpicLeft( history->peer->paintUserpicLeft(
p, p,

View file

@ -337,12 +337,13 @@ PaintRoundImageCallback ChatRow::generatePaintUserpicCallback(
int y, int y,
int outerWidth, int outerWidth,
int size) mutable { int size) mutable {
using namespace Ui;
if (forceRound && peer->isForum()) { if (forceRound && peer->isForum()) {
ForceRoundUserpicCallback(peer)(p, x, y, outerWidth, size); ForceRoundUserpicCallback(peer)(p, x, y, outerWidth, size);
} else if (saved) { } else if (saved) {
Ui::EmptyUserpic::PaintSavedMessages(p, x, y, outerWidth, size); EmptyUserpic::PaintSavedMessages(p, x, y, outerWidth, size);
} else if (replies) { } else if (replies) {
Ui::EmptyUserpic::PaintRepliesMessages(p, x, y, outerWidth, size); EmptyUserpic::PaintRepliesMessages(p, x, y, outerWidth, size);
} else { } else {
peer->paintUserpicLeft(p, userpic, x, y, outerWidth, size); peer->paintUserpicLeft(p, userpic, x, y, outerWidth, size);
} }

View file

@ -64,11 +64,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace { namespace {
constexpr auto kUserpicsMax = size_t(3);
using GiftOption = Data::PremiumSubscriptionOption;
using GiftOptions = Data::PremiumSubscriptionOptions;
[[nodiscard]] QString CreateMessageLink( [[nodiscard]] QString CreateMessageLink(
not_null<Main::Session*> session, not_null<Main::Session*> session,
PeerId peerId, PeerId peerId,
@ -87,638 +82,6 @@ using GiftOptions = Data::PremiumSubscriptionOptions;
return QString(); return QString();
}; };
GiftOptions GiftOptionFromTL(const MTPDuserFull &data) {
auto result = GiftOptions();
const auto gifts = data.vpremium_gifts();
if (!gifts) {
return result;
}
result = Api::PremiumSubscriptionOptionsFromTL(gifts->v);
for (auto &option : result) {
option.costPerMonth = tr::lng_premium_gift_per(
tr::now,
lt_cost,
option.costPerMonth);
}
return result;
}
[[nodiscard]] Fn<TextWithEntities(TextWithEntities)> BoostsForGiftText(
const std::vector<not_null<UserData*>> users) {
Expects(!users.empty());
const auto session = &users.front()->session();
const auto emoji = Ui::Text::SingleCustomEmoji(
session->data().customEmojiManager().registerInternalEmoji(
st::premiumGiftsBoostIcon,
QMargins(0, st::premiumGiftsUserpicBadgeInner, 0, 0),
false));
return [=, count = users.size()](TextWithEntities text) {
text.append('\n');
text.append('\n');
text.append(tr::lng_premium_gifts_about_reward(
tr::now,
lt_count,
count * BoostsForGift(session),
lt_emoji,
emoji,
Ui::Text::RichLangValue));
return text;
};
}
using TagUser1 = lngtag_user;
using TagUser2 = lngtag_second_user;
using TagUser3 = lngtag_name;
[[nodiscard]] rpl::producer<TextWithEntities> ComplexAboutLabel(
const std::vector<not_null<UserData*>> &users,
tr::phrase<TagUser1> phrase1,
tr::phrase<TagUser1, TagUser2> phrase2,
tr::phrase<TagUser1, TagUser2, TagUser3> phrase3,
tr::phrase<lngtag_count, TagUser1, TagUser2, TagUser3> phraseMore) {
Expects(!users.empty());
const auto count = users.size();
const auto nameValue = [&](not_null<UserData*> user) {
return user->session().changes().peerFlagsValue(
user,
Data::PeerUpdate::Flag::Name
) | rpl::map([=] { return TextWithEntities{ user->firstName }; });
};
if (count == 1) {
return phrase1(
lt_user,
nameValue(users.front()),
Ui::Text::RichLangValue);
} else if (count == 2) {
return phrase2(
lt_user,
nameValue(users.front()),
lt_second_user,
nameValue(users[1]),
Ui::Text::RichLangValue);
} else if (count == 3) {
return phrase3(
lt_user,
nameValue(users.front()),
lt_second_user,
nameValue(users[1]),
lt_name,
nameValue(users[2]),
Ui::Text::RichLangValue);
} else {
return phraseMore(
lt_count,
rpl::single(count - kUserpicsMax) | tr::to_count(),
lt_user,
nameValue(users.front()),
lt_second_user,
nameValue(users[1]),
lt_name,
nameValue(users[2]),
Ui::Text::RichLangValue);
}
}
[[nodiscard]] not_null<Ui::RpWidget*> CircleBadge(
not_null<Ui::RpWidget*> parent,
const QString &text) {
const auto widget = Ui::CreateChild<Ui::RpWidget>(parent.get());
const auto full = Rect(st::premiumGiftsUserpicBadgeSize);
const auto inner = full - Margins(st::premiumGiftsUserpicBadgeInner);
auto gradient = QLinearGradient(
QPointF(0, full.height()),
QPointF(full.width(), 0));
gradient.setStops(Ui::Premium::GiftGradientStops());
widget->paintRequest(
) | rpl::start_with_next([=] {
auto p = QPainter(widget);
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(st::boxBg);
p.drawEllipse(full);
p.setPen(Qt::NoPen);
p.setBrush(gradient);
p.drawEllipse(inner);
p.setFont(st::premiumGiftsUserpicBadgeFont);
p.setPen(st::premiumButtonFg);
p.drawText(full, text, style::al_center);
}, widget->lifetime());
widget->resize(full.size());
return widget;
}
[[nodiscard]] not_null<Ui::RpWidget*> UserpicsContainer(
not_null<Ui::RpWidget*> parent,
std::vector<not_null<UserData*>> users) {
Expects(!users.empty());
if (users.size() == 1) {
const auto userpic = Ui::CreateChild<Ui::UserpicButton>(
parent.get(),
users.front(),
st::defaultUserpicButton);
userpic->setAttribute(Qt::WA_TransparentForMouseEvents);
return userpic;
}
const auto &singleSize = st::defaultUserpicButton.size;
const auto container = Ui::CreateChild<Ui::RpWidget>(parent.get());
const auto single = singleSize.width();
const auto shift = single - st::boostReplaceUserpicsShift;
const auto maxWidth = users.size() * (single - shift) + shift;
container->resize(maxWidth, singleSize.height());
container->setAttribute(Qt::WA_TransparentForMouseEvents);
const auto diff = (single - st::premiumGiftsUserpicButton.size.width())
/ 2;
for (auto i = 0; i < users.size(); i++) {
const auto bg = Ui::CreateChild<Ui::RpWidget>(container);
bg->resize(singleSize);
bg->paintRequest(
) | rpl::start_with_next([=] {
auto p = QPainter(bg);
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(st::boxBg);
p.drawEllipse(bg->rect());
}, bg->lifetime());
bg->moveToLeft(std::max(0, i * (single - shift)), 0);
const auto userpic = Ui::CreateChild<Ui::UserpicButton>(
bg,
users[i],
st::premiumGiftsUserpicButton);
userpic->moveToLeft(diff, diff);
}
return container;
}
void GiftBox(
not_null<Ui::GenericBox*> box,
not_null<Window::SessionController*> controller,
not_null<UserData*> user,
GiftOptions options) {
const auto boxWidth = st::boxWideWidth;
box->setWidth(boxWidth);
box->setNoContentMargin(true);
const auto buttonsParent = box->verticalLayout().get();
struct State {
rpl::event_stream<QString> buttonText;
};
const auto state = box->lifetime().make_state<State>();
const auto userpicPadding = st::premiumGiftUserpicPadding;
const auto top = box->addRow(object_ptr<Ui::FixedHeightWidget>(
buttonsParent,
userpicPadding.top()
+ userpicPadding.bottom()
+ st::defaultUserpicButton.size.height()));
using ColoredMiniStars = Ui::Premium::ColoredMiniStars;
const auto stars = box->lifetime().make_state<ColoredMiniStars>(
top,
true);
const auto userpic = UserpicsContainer(top, { user });
userpic->setAttribute(Qt::WA_TransparentForMouseEvents);
top->widthValue(
) | rpl::start_with_next([=](int width) {
userpic->moveToLeft(
(width - userpic->width()) / 2,
userpicPadding.top());
const auto center = top->rect().center();
const auto size = QSize(
userpic->width() * Ui::Premium::MiniStars::kSizeFactor,
userpic->height());
const auto ministarsRect = QRect(
QPoint(center.x() - size.width(), center.y() - size.height()),
QPoint(center.x() + size.width(), center.y() + size.height()));
stars->setPosition(ministarsRect.topLeft());
stars->setSize(ministarsRect.size());
}, userpic->lifetime());
top->paintRequest(
) | rpl::start_with_next([=](const QRect &r) {
auto p = QPainter(top);
p.fillRect(r, Qt::transparent);
stars->paint(p);
}, top->lifetime());
const auto close = Ui::CreateChild<Ui::IconButton>(
buttonsParent,
st::infoTopBarClose);
close->setClickedCallback([=] { box->closeBox(); });
buttonsParent->widthValue(
) | rpl::start_with_next([=](int width) {
close->moveToRight(0, 0, width);
}, close->lifetime());
// Header.
const auto &padding = st::premiumGiftAboutPadding;
const auto available = boxWidth - padding.left() - padding.right();
const auto &stTitle = st::premiumPreviewAboutTitle;
auto titleLabel = object_ptr<Ui::FlatLabel>(
box,
tr::lng_premium_gift_title(),
stTitle);
titleLabel->resizeToWidth(available);
box->addRow(
object_ptr<Ui::CenterWrap<Ui::FlatLabel>>(
box,
std::move(titleLabel)),
st::premiumGiftTitlePadding);
auto textLabel = Ui::CreateLabelWithCustomEmoji(
box,
tr::lng_premium_gift_about(
lt_user,
user->session().changes().peerFlagsValue(
user,
Data::PeerUpdate::Flag::Name
) | rpl::map([=] { return TextWithEntities{ user->firstName }; }),
Ui::Text::RichLangValue
) | rpl::map(
BoostsForGiftText({ user })
),
{ .session = &user->session() },
st::premiumPreviewAbout);
textLabel->setTextColorOverride(stTitle.textFg->c);
textLabel->resizeToWidth(available);
box->addRow(
object_ptr<Ui::CenterWrap<Ui::FlatLabel>>(box, std::move(textLabel)),
padding);
// List.
const auto group = std::make_shared<Ui::RadiobuttonGroup>();
const auto groupValueChangedCallback = [=](int value) {
Expects(value < options.size() && value >= 0);
auto text = tr::lng_premium_gift_button(
tr::now,
lt_cost,
options[value].costTotal);
state->buttonText.fire(std::move(text));
};
group->setChangedCallback(groupValueChangedCallback);
Ui::Premium::AddGiftOptions(
buttonsParent,
group,
options,
st::premiumGiftOption);
// Footer.
auto terms = object_ptr<Ui::FlatLabel>(
box,
tr::lng_premium_gift_terms(
lt_link,
tr::lng_premium_gift_terms_link(
) | rpl::map([=](const QString &t) {
return Ui::Text::Link(t, 1);
}),
Ui::Text::WithEntities),
st::premiumGiftTerms);
terms->setLink(1, std::make_shared<LambdaClickHandler>([=] {
box->closeBox();
Settings::ShowPremium(&user->session(), QString());
}));
terms->resizeToWidth(available);
box->addRow(
object_ptr<Ui::CenterWrap<Ui::FlatLabel>>(box, std::move(terms)),
st::premiumGiftTermsPadding);
// Button.
const auto &stButton = st::premiumGiftBox;
box->setStyle(stButton);
auto raw = Settings::CreateSubscribeButton({
controller,
box,
[] { return u"gift"_q; },
state->buttonText.events(),
Ui::Premium::GiftGradientStops(),
[=] {
const auto value = group->current();
return (value < options.size() && value >= 0)
? options[value].botUrl
: QString();
},
});
auto button = object_ptr<Ui::GradientButton>::fromRaw(raw);
button->resizeToWidth(boxWidth - rect::m::sum::h(stButton.buttonPadding));
box->setShowFinishedCallback([raw = button.data()] {
raw->startGlareAnimation();
});
box->addButton(std::move(button));
groupValueChangedCallback(0);
Data::PeerPremiumValue(
user
) | rpl::skip(1) | rpl::start_with_next([=] {
box->closeBox();
}, box->lifetime());
}
void GiftsBox(
not_null<Ui::GenericBox*> box,
not_null<Window::SessionController*> controller,
std::vector<not_null<UserData*>> users,
not_null<Api::PremiumGiftCodeOptions*> api,
const QString &ref) {
Expects(!users.empty());
const auto boxWidth = st::boxWideWidth;
box->setWidth(boxWidth);
box->setNoContentMargin(true);
const auto buttonsParent = box->verticalLayout().get();
const auto session = &users.front()->session();
struct State {
rpl::event_stream<QString> buttonText;
rpl::variable<bool> confirmButtonBusy = false;
rpl::variable<bool> isPaymentComplete = false;
};
const auto state = box->lifetime().make_state<State>();
const auto userpicPadding = st::premiumGiftUserpicPadding;
const auto top = box->addRow(object_ptr<Ui::FixedHeightWidget>(
buttonsParent,
userpicPadding.top()
+ userpicPadding.bottom()
+ st::defaultUserpicButton.size.height()));
using ColoredMiniStars = Ui::Premium::ColoredMiniStars;
const auto stars = box->lifetime().make_state<ColoredMiniStars>(
top,
true);
const auto maxWithUserpic = std::min(users.size(), kUserpicsMax);
const auto userpics = UserpicsContainer(
top,
{ users.begin(), users.begin() + maxWithUserpic });
top->widthValue(
) | rpl::start_with_next([=](int width) {
userpics->moveToLeft(
(width - userpics->width()) / 2,
userpicPadding.top());
const auto center = top->rect().center();
const auto size = QSize(
userpics->width() * Ui::Premium::MiniStars::kSizeFactor,
userpics->height());
const auto ministarsRect = QRect(
QPoint(center.x() - size.width(), center.y() - size.height()),
QPoint(center.x() + size.width(), center.y() + size.height()));
stars->setPosition(ministarsRect.topLeft());
stars->setSize(ministarsRect.size());
}, userpics->lifetime());
if (const auto rest = users.size() - maxWithUserpic; rest > 0) {
const auto badge = CircleBadge(
userpics,
QChar('+') + QString::number(rest));
badge->moveToRight(0, userpics->height() - badge->height());
}
top->paintRequest(
) | rpl::start_with_next([=](const QRect &r) {
auto p = QPainter(top);
p.fillRect(r, Qt::transparent);
stars->paint(p);
}, top->lifetime());
const auto close = Ui::CreateChild<Ui::IconButton>(
buttonsParent,
st::infoTopBarClose);
close->setClickedCallback([=] { box->closeBox(); });
buttonsParent->widthValue(
) | rpl::start_with_next([=](int width) {
close->moveToRight(0, 0, width);
}, close->lifetime());
// Header.
const auto &padding = st::premiumGiftAboutPadding;
const auto available = boxWidth - padding.left() - padding.right();
const auto &stTitle = st::premiumPreviewAboutTitle;
auto titleLabel = object_ptr<Ui::FlatLabel>(
box,
rpl::conditional(
state->isPaymentComplete.value(),
tr::lng_premium_gifts_about_paid_title(),
tr::lng_premium_gift_title()),
stTitle);
titleLabel->resizeToWidth(available);
box->addRow(
object_ptr<Ui::CenterWrap<Ui::FlatLabel>>(
box,
std::move(titleLabel)),
st::premiumGiftTitlePadding);
// About.
{
auto text = rpl::conditional(
state->isPaymentComplete.value(),
ComplexAboutLabel(
users,
tr::lng_premium_gifts_about_paid1,
tr::lng_premium_gifts_about_paid2,
tr::lng_premium_gifts_about_paid3,
tr::lng_premium_gifts_about_paid_more
) | rpl::map([count = users.size()](TextWithEntities text) {
text.append('\n');
text.append('\n');
text.append(tr::lng_premium_gifts_about_paid_below(
tr::now,
lt_count,
float64(count),
Ui::Text::RichLangValue));
return text;
}),
ComplexAboutLabel(
users,
tr::lng_premium_gifts_about_user1,
tr::lng_premium_gifts_about_user2,
tr::lng_premium_gifts_about_user3,
tr::lng_premium_gifts_about_user_more
) | rpl::map(BoostsForGiftText(users))
);
const auto label = box->addRow(
object_ptr<Ui::CenterWrap<Ui::FlatLabel>>(
box,
Ui::CreateLabelWithCustomEmoji(
box,
std::move(text),
{ .session = session },
st::premiumPreviewAbout)),
padding)->entity();
label->setTextColorOverride(stTitle.textFg->c);
label->resizeToWidth(available);
}
// List.
const auto optionsContainer = buttonsParent->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
buttonsParent,
object_ptr<Ui::VerticalLayout>(buttonsParent)));
const auto options = api->options(users.size());
const auto group = std::make_shared<Ui::RadiobuttonGroup>();
const auto groupValueChangedCallback = [=](int value) {
Expects(value < options.size() && value >= 0);
auto text = tr::lng_premium_gift_button(
tr::now,
lt_cost,
options[value].costTotal);
state->buttonText.fire(std::move(text));
};
group->setChangedCallback(groupValueChangedCallback);
Ui::Premium::AddGiftOptions(
optionsContainer->entity(),
group,
options,
st::premiumGiftOption);
optionsContainer->toggleOn(
state->isPaymentComplete.value() | rpl::map(!rpl::mappers::_1),
anim::type::instant);
// Summary.
{
{
// Will be hidden after payment.
const auto content = optionsContainer->entity();
Ui::AddSkip(content);
Ui::AddDivider(content);
Ui::AddSkip(content);
Ui::AddSubsectionTitle(
content,
tr::lng_premium_gifts_summary_subtitle());
}
const auto content = box->addRow(
object_ptr<Ui::VerticalLayout>(box),
{});
auto buttonCallback = [=](PremiumFeature section) {
stars->setPaused(true);
const auto previewBoxShown = [=](
not_null<Ui::BoxContent*> previewBox) {
previewBox->boxClosing(
) | rpl::start_with_next(crl::guard(box, [=] {
stars->setPaused(false);
}), previewBox->lifetime());
};
ShowPremiumPreviewBox(
controller->uiShow(),
section,
previewBoxShown,
true);
};
Settings::AddSummaryPremium(
content,
controller,
ref,
std::move(buttonCallback));
}
// Footer.
{
box->addRow(
object_ptr<Ui::DividerLabel>(
box,
object_ptr<Ui::FlatLabel>(
box,
tr::lng_premium_gifts_terms(
lt_link,
tr::lng_payments_terms_link(
) | rpl::map([](const QString &t) {
using namespace Ui::Text;
return Link(t, u"https://telegram.org/tos"_q);
}),
lt_policy,
tr::lng_premium_gifts_terms_policy(
) | rpl::map([](const QString &t) {
using namespace Ui::Text;
return Link(t, u"https://telegram.org/privacy"_q);
}),
Ui::Text::RichLangValue),
st::premiumGiftTerms),
st::defaultBoxDividerLabelPadding),
{});
}
// Button.
const auto &stButton = st::premiumGiftBox;
box->setStyle(stButton);
auto raw = Settings::CreateSubscribeButton({
controller,
box,
[=] { return ref; },
rpl::combine(
state->buttonText.events(),
state->confirmButtonBusy.value(),
state->isPaymentComplete.value()
) | rpl::map([](const QString &text, bool busy, bool paid) {
return busy
? QString()
: paid
? tr::lng_close(tr::now)
: text;
}),
Ui::Premium::GiftGradientStops(),
});
raw->setClickedCallback([=] {
if (state->confirmButtonBusy.current()) {
return;
}
if (state->isPaymentComplete.current()) {
return box->closeBox();
}
auto invoice = api->invoice(
users.size(),
api->monthsFromPreset(group->current()));
invoice.purpose = Payments::InvoicePremiumGiftCodeUsers{ users };
state->confirmButtonBusy = true;
const auto show = box->uiShow();
const auto weak = Ui::MakeWeak(box.get());
const auto done = [=](Payments::CheckoutResult result) {
if (const auto strong = weak.data()) {
strong->window()->setFocus();
state->confirmButtonBusy = false;
if (result == Payments::CheckoutResult::Paid) {
state->isPaymentComplete = true;
Ui::StartFireworks(box->parentWidget());
}
}
};
Payments::CheckoutProcess::Start(std::move(invoice), done);
});
{
using namespace Info::Statistics;
const auto loadingAnimation = InfiniteRadialAnimationWidget(
raw,
raw->height() / 2);
AddChildToWidgetCenter(raw, loadingAnimation);
loadingAnimation->showOn(state->confirmButtonBusy.value());
}
auto button = object_ptr<Ui::GradientButton>::fromRaw(raw);
button->resizeToWidth(boxWidth - rect::m::sum::h(stButton.buttonPadding));
box->setShowFinishedCallback([raw = button.data()] {
raw->startGlareAnimation();
});
box->addButton(std::move(button));
groupValueChangedCallback(0);
}
[[nodiscard]] Data::GiftCodeLink MakeGiftCodeLink( [[nodiscard]] Data::GiftCodeLink MakeGiftCodeLink(
not_null<Main::Session*> session, not_null<Main::Session*> session,
const QString &slug) { const QString &slug) {
@ -793,16 +156,55 @@ void GiftsBox(
return result; return result;
} }
[[nodiscard]] object_ptr<Ui::RpWidget> MakeHiddenPeerTableValue(
not_null<QWidget*> parent,
not_null<Window::SessionNavigation*> controller) {
auto result = object_ptr<Ui::RpWidget>(parent);
const auto raw = result.data();
const auto &st = st::giveawayGiftCodeUserpic;
raw->resize(raw->width(), st.photoSize);
const auto userpic = Ui::CreateChild<Ui::RpWidget>(raw);
const auto usize = st.photoSize;
userpic->resize(usize, usize);
userpic->paintRequest() | rpl::start_with_next([=] {
auto p = QPainter(userpic);
Ui::EmptyUserpic::PaintHiddenAuthor(p, 0, 0, usize, usize);
}, userpic->lifetime());
const auto label = Ui::CreateChild<Ui::FlatLabel>(
raw,
tr::lng_gift_from_hidden(),
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::windowFg->c);
return result;
}
void AddTableRow( void AddTableRow(
not_null<Ui::TableLayout*> table, not_null<Ui::TableLayout*> table,
rpl::producer<QString> label, rpl::producer<QString> label,
object_ptr<Ui::RpWidget> value, object_ptr<Ui::RpWidget> value,
style::margins valueMargins) { style::margins valueMargins) {
table->addRow( table->addRow(
object_ptr<Ui::FlatLabel>( (label
table, ? object_ptr<Ui::FlatLabel>(
std::move(label), table,
st::giveawayGiftCodeLabel), std::move(label),
st::giveawayGiftCodeLabel)
: object_ptr<Ui::FlatLabel>(nullptr)),
std::move(value), std::move(value),
st::giveawayGiftCodeLabelMargin, st::giveawayGiftCodeLabelMargin,
valueMargins); valueMargins);
@ -957,180 +359,6 @@ void ShowAlreadyPremiumToast(
} // namespace } // namespace
GiftPremiumValidator::GiftPremiumValidator(
not_null<Window::SessionController*> controller)
: _controller(controller)
, _api(&_controller->session().mtp()) {
}
void GiftPremiumValidator::cancel() {
_requestId = 0;
}
void GiftPremiumValidator::showChoosePeerBox(const QString &ref) {
if (_manyGiftsLifetime) {
return;
}
using namespace Api;
const auto api = _manyGiftsLifetime.make_state<PremiumGiftCodeOptions>(
_controller->session().user());
const auto show = _controller->uiShow();
api->request(
) | rpl::start_with_error_done([=](const QString &error) {
show->showToast(error);
}, [=] {
const auto maxAmount = *ranges::max_element(api->availablePresets());
class Controller final : public ContactsBoxController {
public:
Controller(
not_null<Main::Session*> session,
Fn<bool(int)> checkErrorCallback)
: ContactsBoxController(session)
, _checkErrorCallback(std::move(checkErrorCallback)) {
}
protected:
std::unique_ptr<PeerListRow> createRow(
not_null<UserData*> user) override {
if (user->isSelf()
|| user->isBot()
|| user->isServiceUser()
|| user->isInaccessible()) {
return nullptr;
}
return ContactsBoxController::createRow(user);
}
void rowClicked(not_null<PeerListRow*> row) override {
const auto checked = !row->checked();
if (checked
&& _checkErrorCallback
&& _checkErrorCallback(
delegate()->peerListSelectedRowsCount())) {
return;
}
delegate()->peerListSetRowChecked(row, checked);
}
private:
const Fn<bool(int)> _checkErrorCallback;
};
auto initBox = [=](not_null<PeerListBox*> peersBox) {
const auto ignoreClose = peersBox->lifetime().make_state<bool>(0);
auto process = [=] {
const auto selected = peersBox->collectSelectedRows();
const auto users = ranges::views::all(
selected
) | ranges::views::transform([](not_null<PeerData*> p) {
return p->asUser();
}) | ranges::views::filter([](UserData *u) -> bool {
return u;
}) | ranges::to<std::vector<not_null<UserData*>>>();
if (users.empty()) {
show->showToast(
tr::lng_settings_gift_premium_choose(tr::now));
return;
}
const auto giftBox = show->show(
Box(GiftsBox, _controller, users, api, ref));
giftBox->boxClosing(
) | rpl::start_with_next([=] {
_manyGiftsLifetime.destroy();
}, giftBox->lifetime());
(*ignoreClose) = true;
peersBox->closeBox();
};
peersBox->setTitle(tr::lng_premium_gift_title());
peersBox->addButton(
tr::lng_settings_gift_premium_users_confirm(),
std::move(process));
peersBox->addButton(tr::lng_cancel(), [=] {
peersBox->closeBox();
});
peersBox->boxClosing(
) | rpl::start_with_next([=] {
if (!(*ignoreClose)) {
_manyGiftsLifetime.destroy();
}
}, peersBox->lifetime());
};
auto listController = std::make_unique<Controller>(
&_controller->session(),
[=](int count) {
if (count <= maxAmount) {
return false;
}
show->showToast(tr::lng_settings_gift_premium_users_error(
tr::now,
lt_count,
maxAmount));
return true;
});
show->showBox(
Box<PeerListBox>(
std::move(listController),
std::move(initBox)),
Ui::LayerOption::KeepOther);
}, _manyGiftsLifetime);
}
void GiftPremiumValidator::showChosenPeerBox(
not_null<UserData*> user,
const QString &ref) {
if (_manyGiftsLifetime) {
return;
}
using namespace Api;
const auto api = _manyGiftsLifetime.make_state<PremiumGiftCodeOptions>(
_controller->session().user());
const auto show = _controller->uiShow();
api->request(
) | rpl::start_with_error_done([=](const QString &error) {
show->showToast(error);
}, [=] {
const auto users = std::vector<not_null<UserData*>>{ user };
const auto giftBox = show->show(
Box(GiftsBox, _controller, users, api, ref));
giftBox->boxClosing(
) | rpl::start_with_next([=] {
_manyGiftsLifetime.destroy();
}, giftBox->lifetime());
}, _manyGiftsLifetime);
}
void GiftPremiumValidator::showBox(not_null<UserData*> user) {
if (_requestId) {
return;
}
_requestId = _api.request(MTPusers_GetFullUser(
user->inputUser
)).done([=](const MTPusers_UserFull &result) {
if (!_requestId) {
// Canceled.
return;
}
_requestId = 0;
// _controller->api().processFullPeer(peer, result);
_controller->session().data().processUsers(result.data().vusers());
_controller->session().data().processChats(result.data().vchats());
const auto &fullUser = result.data().vfull_user().data();
auto options = GiftOptionFromTL(fullUser);
if (!options.empty()) {
_controller->show(
Box(GiftBox, _controller, user, std::move(options)));
}
}).fail([=] {
_requestId = 0;
}).send();
}
rpl::producer<QString> GiftDurationValue(int months) { rpl::producer<QString> GiftDurationValue(int months) {
return GiftDurationPhrase(months)( return GiftDurationPhrase(months)(
lt_count, lt_count,
@ -1708,6 +936,77 @@ void ResolveGiveawayInfo(
crl::guard(controller, show)); crl::guard(controller, show));
} }
void AddStarGiftTable(
not_null<Window::SessionNavigation*> controller,
not_null<Ui::VerticalLayout*> container,
const Data::CreditsHistoryEntry &entry) {
auto table = container->add(
object_ptr<Ui::TableLayout>(
container,
st::giveawayGiftCodeTable),
st::giveawayGiftCodeTableMargin);
const auto peerId = PeerId(entry.barePeerId);
if (peerId) {
AddTableRow(
table,
tr::lng_credits_box_history_entry_peer_in(),
controller,
peerId);
} else {
AddTableRow(
table,
tr::lng_credits_box_history_entry_peer_in(),
MakeHiddenPeerTableValue(table, controller),
st::giveawayGiftCodePeerMargin);
}
if (!entry.date.isNull()) {
AddTableRow(
table,
tr::lng_gift_link_label_date(),
rpl::single(Ui::Text::WithEntities(langDateTime(entry.date))));
}
if (entry.limitedCount > 0) {
auto amount = rpl::single(TextWithEntities{
QString::number(entry.limitedCount)
});
AddTableRow(
table,
tr::lng_gift_availability(),
((entry.limitedLeft > 0)
? tr::lng_gift_availability_left(
lt_count,
rpl::single(entry.limitedLeft * 1.),
lt_amount,
std::move(amount),
Ui::Text::WithEntities)
: tr::lng_gift_availability_none(
lt_amount,
std::move(amount),
Ui::Text::WithEntities)));
}
if (!entry.description.empty()) {
const auto session = &controller->session();
const auto makeContext = [=](Fn<void()> update) {
return Core::MarkedTextContext{
.session = session,
.customEmojiRepaint = std::move(update),
};
};
auto label = object_ptr<Ui::FlatLabel>(
table,
rpl::single(entry.description),
st::giveawayGiftMessage,
st::defaultPopupMenu,
makeContext);
label->setSelectable(true);
table->addRow(
nullptr,
std::move(label),
st::giveawayGiftCodeLabelMargin,
st::giveawayGiftCodeValueMargin);
}
}
void AddCreditsHistoryEntryTable( void AddCreditsHistoryEntryTable(
not_null<Window::SessionNavigation*> controller, not_null<Window::SessionNavigation*> controller,
not_null<Ui::VerticalLayout*> container, not_null<Ui::VerticalLayout*> container,

View file

@ -9,8 +9,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "mtproto/sender.h" #include "mtproto/sender.h"
class UserData;
namespace Api { namespace Api {
struct GiftCode; struct GiftCode;
} // namespace Api } // namespace Api
@ -29,29 +27,9 @@ class VerticalLayout;
} // namespace Ui } // namespace Ui
namespace Window { namespace Window {
class SessionController;
class SessionNavigation; class SessionNavigation;
} // namespace Window } // namespace Window
class GiftPremiumValidator final {
public:
GiftPremiumValidator(not_null<Window::SessionController*> controller);
void showBox(not_null<UserData*> user);
void showChoosePeerBox(const QString &ref);
void showChosenPeerBox(not_null<UserData*> user, const QString &ref);
void cancel();
private:
const not_null<Window::SessionController*> _controller;
MTP::Sender _api;
mtpRequestId _requestId = 0;
rpl::lifetime _manyGiftsLifetime;
};
[[nodiscard]] rpl::producer<QString> GiftDurationValue(int months); [[nodiscard]] rpl::producer<QString> GiftDurationValue(int months);
[[nodiscard]] QString GiftDuration(int months); [[nodiscard]] QString GiftDuration(int months);
@ -76,6 +54,10 @@ void ResolveGiveawayInfo(
std::optional<Data::GiveawayStart> start, std::optional<Data::GiveawayStart> start,
std::optional<Data::GiveawayResults> results); std::optional<Data::GiveawayResults> results);
void AddStarGiftTable(
not_null<Window::SessionNavigation*> controller,
not_null<Ui::VerticalLayout*> container,
const Data::CreditsHistoryEntry &entry);
void AddCreditsHistoryEntryTable( void AddCreditsHistoryEntryTable(
not_null<Window::SessionNavigation*> controller, not_null<Window::SessionNavigation*> controller,
not_null<Ui::VerticalLayout*> container, not_null<Ui::VerticalLayout*> container,

View file

@ -447,6 +447,8 @@ void PeerListBox::addSelectItem(
? tr::lng_saved_short(tr::now) ? tr::lng_saved_short(tr::now)
: (respect && peer->isRepliesChat()) : (respect && peer->isRepliesChat())
? tr::lng_replies_messages(tr::now) ? tr::lng_replies_messages(tr::now)
: (respect && peer->isVerifyCodes())
? tr::lng_verification_codes(tr::now)
: peer->shortName(); : peer->shortName();
addSelectItem( addSelectItem(
peer->id.value, peer->id.value,
@ -625,6 +627,8 @@ void PeerListRow::refreshName(const style::PeerListItem &st) {
? tr::lng_saved_messages(tr::now) ? tr::lng_saved_messages(tr::now)
: _isRepliesMessagesChat : _isRepliesMessagesChat
? tr::lng_replies_messages(tr::now) ? tr::lng_replies_messages(tr::now)
: _isVerifyCodesChat
? tr::lng_verification_codes(tr::now)
: generateName(); : generateName();
_name.setText(st.nameStyle, text, Ui::NameTextOptions()); _name.setText(st.nameStyle, text, Ui::NameTextOptions());
} }
@ -695,6 +699,8 @@ QString PeerListRow::generateShortName() {
? tr::lng_saved_short(tr::now) ? tr::lng_saved_short(tr::now)
: _isRepliesMessagesChat : _isRepliesMessagesChat
? tr::lng_replies_messages(tr::now) ? tr::lng_replies_messages(tr::now)
: _isVerifyCodesChat
? tr::lng_verification_codes(tr::now)
: peer()->shortName(); : peer()->shortName();
} }
@ -715,10 +721,11 @@ PaintRoundImageCallback PeerListRow::generatePaintUserpicCallback(
return ForceRoundUserpicCallback(peer); return ForceRoundUserpicCallback(peer);
} }
return [=](Painter &p, int x, int y, int outerWidth, int size) mutable { return [=](Painter &p, int x, int y, int outerWidth, int size) mutable {
using namespace Ui;
if (saved) { if (saved) {
Ui::EmptyUserpic::PaintSavedMessages(p, x, y, outerWidth, size); EmptyUserpic::PaintSavedMessages(p, x, y, outerWidth, size);
} else if (replies) { } else if (replies) {
Ui::EmptyUserpic::PaintRepliesMessages(p, x, y, outerWidth, size); EmptyUserpic::PaintRepliesMessages(p, x, y, outerWidth, size);
} else { } else {
peer->paintUserpicLeft(p, userpic, x, y, outerWidth, size); peer->paintUserpicLeft(p, userpic, x, y, outerWidth, size);
} }
@ -757,12 +764,14 @@ int PeerListRow::paintNameIconGetWidth(
int availableWidth, int availableWidth,
int outerWidth, int outerWidth,
bool selected) { bool selected) {
if (special() if (_skipPeerBadge
|| special()
|| !_savedMessagesStatus.isEmpty() || !_savedMessagesStatus.isEmpty()
|| _isRepliesMessagesChat) { || _isRepliesMessagesChat
|| _isVerifyCodesChat) {
return 0; return 0;
} }
return _bagde.drawGetWidth( return _badge.drawGetWidth(
p, p,
QRect( QRect(
nameLeft, nameLeft,
@ -874,12 +883,13 @@ void PeerListRow::paintDisabledCheckUserpic(
auto iconBorderPen = st.checkbox.check.border->p; auto iconBorderPen = st.checkbox.check.border->p;
iconBorderPen.setWidth(st.checkbox.selectWidth); iconBorderPen.setWidth(st.checkbox.selectWidth);
const auto size = userpicRadius * 2;
if (!_savedMessagesStatus.isEmpty()) { if (!_savedMessagesStatus.isEmpty()) {
Ui::EmptyUserpic::PaintSavedMessages(p, userpicLeft, userpicTop, outerWidth, userpicRadius * 2); Ui::EmptyUserpic::PaintSavedMessages(p, userpicLeft, userpicTop, outerWidth, size);
} else if (_isRepliesMessagesChat) { } else if (_isRepliesMessagesChat) {
Ui::EmptyUserpic::PaintRepliesMessages(p, userpicLeft, userpicTop, outerWidth, userpicRadius * 2); Ui::EmptyUserpic::PaintRepliesMessages(p, userpicLeft, userpicTop, outerWidth, size);
} else { } else {
peer()->paintUserpicLeft(p, _userpic, userpicLeft, userpicTop, outerWidth, userpicRadius * 2); peer()->paintUserpicLeft(p, _userpic, userpicLeft, userpicTop, outerWidth, size);
} }
{ {
@ -1069,10 +1079,13 @@ void PeerListContent::setRowHidden(not_null<PeerListRow*> row, bool hidden) {
void PeerListContent::addRowEntry(not_null<PeerListRow*> row) { void PeerListContent::addRowEntry(not_null<PeerListRow*> row) {
const auto savedMessagesStatus = _controller->savedMessagesChatStatus(); const auto savedMessagesStatus = _controller->savedMessagesChatStatus();
if (!savedMessagesStatus.isEmpty() && !row->special()) { if (!savedMessagesStatus.isEmpty() && !row->special()) {
if (row->peer()->isSelf()) { const auto peer = row->peer();
if (peer->isSelf()) {
row->setSavedMessagesChatStatus(savedMessagesStatus); row->setSavedMessagesChatStatus(savedMessagesStatus);
} else if (row->peer()->isRepliesChat()) { } else if (peer->isRepliesChat()) {
row->setIsRepliesMessagesChat(true); row->setIsRepliesMessagesChat(true);
} else if (peer->isVerifyCodes()) {
row->setIsVerifyCodesChat(true);
} }
} }
_rowsById.emplace(row->id(), row); _rowsById.emplace(row->id(), row);

View file

@ -200,6 +200,9 @@ public:
void setIsRepliesMessagesChat(bool isRepliesMessagesChat) { void setIsRepliesMessagesChat(bool isRepliesMessagesChat) {
_isRepliesMessagesChat = isRepliesMessagesChat; _isRepliesMessagesChat = isRepliesMessagesChat;
} }
void setIsVerifyCodesChat(bool isVerifyCodesChat) {
_isVerifyCodesChat = isVerifyCodesChat;
}
template <typename UpdateCallback> template <typename UpdateCallback>
void setChecked( void setChecked(
@ -251,6 +254,10 @@ public:
return _nameFirstLetters; return _nameFirstLetters;
} }
void setSkipPeerBadge(bool skip) {
_skipPeerBadge = skip;
}
virtual void lazyInitialize(const style::PeerListItem &st); virtual void lazyInitialize(const style::PeerListItem &st);
virtual void paintStatusText( virtual void paintStatusText(
Painter &p, Painter &p,
@ -288,7 +295,7 @@ private:
std::unique_ptr<Ui::RoundImageCheckbox> _checkbox; std::unique_ptr<Ui::RoundImageCheckbox> _checkbox;
Ui::Text::String _name; Ui::Text::String _name;
Ui::Text::String _status; Ui::Text::String _status;
Ui::PeerBadge _bagde; Ui::PeerBadge _badge;
StatusType _statusType = StatusType::Online; StatusType _statusType = StatusType::Online;
crl::time _statusValidTill = 0; crl::time _statusValidTill = 0;
base::flat_set<QChar> _nameFirstLetters; base::flat_set<QChar> _nameFirstLetters;
@ -299,6 +306,8 @@ private:
bool _initialized : 1 = false; bool _initialized : 1 = false;
bool _isSearchResult : 1 = false; bool _isSearchResult : 1 = false;
bool _isRepliesMessagesChat : 1 = false; bool _isRepliesMessagesChat : 1 = false;
bool _isVerifyCodesChat : 1 = false;
bool _skipPeerBadge : 1 = false;
}; };

View file

@ -842,6 +842,7 @@ auto ChooseRecipientBoxController::createRow(
? !_filter(history) ? !_filter(history)
: ((peer->isBroadcast() && !Data::CanSendAnything(peer)) : ((peer->isBroadcast() && !Data::CanSendAnything(peer))
|| peer->isRepliesChat() || peer->isRepliesChat()
|| peer->isVerifyCodes()
|| (peer->isUser() && (_premiumRequiredError || (peer->isUser() && (_premiumRequiredError
? !peer->asUser()->canSendIgnoreRequirePremium() ? !peer->asUser()->canSendIgnoreRequirePremium()
: !Data::CanSendAnything(peer)))); : !Data::CanSendAnything(peer))));

View file

@ -269,7 +269,8 @@ PaintRoundImageCallback ForbiddenRow::generatePaintUserpicCallback(
const auto peer = this->peer(); const auto peer = this->peer();
const auto saved = peer->isSelf(); const auto saved = peer->isSelf();
const auto replies = peer->isRepliesChat(); const auto replies = peer->isRepliesChat();
auto userpic = (saved || replies) const auto verifyCodes = peer->isVerifyCodes();
auto userpic = (saved || replies || verifyCodes)
? Ui::PeerUserpicView() ? Ui::PeerUserpicView()
: ensureUserpicView(); : ensureUserpicView();
auto paint = [=]( auto paint = [=](
@ -302,6 +303,7 @@ PaintRoundImageCallback ForbiddenRow::generatePaintUserpicCallback(
repaint = (_paletteVersion != style::PaletteVersion()) repaint = (_paletteVersion != style::PaletteVersion())
|| (!saved || (!saved
&& !replies && !replies
&& !verifyCodes
&& (_userpicKey != peer->userpicUniqueKey(userpic))); && (_userpicKey != peer->userpicUniqueKey(userpic)));
} }
if (repaint) { if (repaint) {

View file

@ -33,6 +33,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "chat_helpers/tabbed_selector.h" #include "chat_helpers/tabbed_selector.h"
#include "core/application.h" #include "core/application.h"
#include "core/core_settings.h" #include "core/core_settings.h"
#include "data/components/credits.h"
#include "data/data_channel.h" #include "data/data_channel.h"
#include "data/data_chat.h" #include "data/data_chat.h"
#include "data/data_peer.h" #include "data/data_peer.h"
@ -1591,6 +1592,9 @@ void Controller::fillBotBalanceButton() {
auto &lifetime = _controls.buttonsLayout->lifetime(); auto &lifetime = _controls.buttonsLayout->lifetime();
const auto state = lifetime.make_state<State>(); const auto state = lifetime.make_state<State>();
if (const auto balance = _peer->session().credits().balance(_peer->id)) {
state->balance = QString::number(balance);
}
const auto wrap = _controls.buttonsLayout->add( const auto wrap = _controls.buttonsLayout->add(
object_ptr<Ui::SlideWrap<Ui::SettingsButton>>( object_ptr<Ui::SlideWrap<Ui::SettingsButton>>(
@ -1604,7 +1608,7 @@ void Controller::fillBotBalanceButton() {
}, },
st::manageGroupButton, st::manageGroupButton,
{}))); {})));
wrap->toggle(false, anim::type::instant); wrap->toggle(!state->balance.current().isEmpty(), anim::type::instant);
const auto button = wrap->entity(); const auto button = wrap->entity();
{ {

View file

@ -1131,11 +1131,14 @@ void ShowEditPeerPermissionsBox(
disabledByAdminRights, disabledByAdminRights,
tr::lng_rights_permission_cant_edit(tr::now)); tr::lng_rights_permission_cant_edit(tr::now));
if (const auto channel = peer->asChannel()) { if (const auto channel = peer->asChannel()) {
if (channel->isPublic() if (channel->isPublic()) {
|| (channel->isMegagroup() && channel->linkedChat())) {
result.emplace( result.emplace(
Flag::ChangeInfo | Flag::PinMessages, Flag::ChangeInfo | Flag::PinMessages,
tr::lng_rights_permission_unavailable(tr::now)); tr::lng_rights_permission_unavailable(tr::now));
} else if (channel->isMegagroup() && channel->linkedChat()) {
result.emplace(
Flag::ChangeInfo | Flag::PinMessages,
tr::lng_rights_permission_in_discuss(tr::now));
} }
} }
return result; return result;

View file

@ -8,23 +8,28 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/report_messages_box.h" #include "boxes/report_messages_box.h"
#include "api/api_report.h" #include "api/api_report.h"
#include "core/application.h"
#include "data/data_peer.h" #include "data/data_peer.h"
#include "data/data_photo.h" #include "data/data_photo.h"
#include "lang/lang_keys.h" #include "lang/lang_keys.h"
#include "ui/boxes/report_box.h" #include "ui/boxes/report_box_graphics.h"
#include "ui/layers/generic_box.h" #include "ui/layers/generic_box.h"
#include "ui/rect.h"
#include "ui/vertical_list.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/fields/input_field.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h" #include "window/window_session_controller.h"
#include "styles/style_boxes.h"
#include "styles/style_chat_helpers.h" #include "styles/style_chat_helpers.h"
#include "styles/style_layers.h"
#include "styles/style_settings.h"
namespace { namespace {
[[nodiscard]] object_ptr<Ui::BoxContent> Report( [[nodiscard]] object_ptr<Ui::BoxContent> Report(
not_null<PeerData*> peer, not_null<PeerData*> peer,
std::variant< std::variant<v::null_t, not_null<PhotoData*>> data,
v::null_t,
MessageIdsList,
not_null<PhotoData*>,
StoryId> data,
const style::ReportBox *stOverride) { const style::ReportBox *stOverride) {
const auto source = v::match(data, [](const MessageIdsList &ids) { const auto source = v::match(data, [](const MessageIdsList &ids) {
return Ui::ReportSource::Message; return Ui::ReportSource::Message;
@ -62,64 +67,151 @@ namespace {
} // namespace } // namespace
object_ptr<Ui::BoxContent> ReportItemsBox(
not_null<PeerData*> peer,
MessageIdsList ids) {
return Report(peer, ids, nullptr);
}
object_ptr<Ui::BoxContent> ReportProfilePhotoBox( object_ptr<Ui::BoxContent> ReportProfilePhotoBox(
not_null<PeerData*> peer, not_null<PeerData*> peer,
not_null<PhotoData*> photo) { not_null<PhotoData*> photo) {
return Report(peer, photo, nullptr); return Report(peer, photo, nullptr);
} }
void ShowReportPeerBox( void ShowReportMessageBox(
not_null<Window::SessionController*> window, std::shared_ptr<Ui::Show> show,
not_null<PeerData*> peer) { not_null<PeerData*> peer,
struct State { const std::vector<MsgId> &ids,
QPointer<Ui::BoxContent> reasonBox; const std::vector<StoryId> &stories,
QPointer<Ui::BoxContent> detailsBox; const style::ReportBox *stOverride) {
MessageIdsList ids; const auto report = Api::CreateReportMessagesOrStoriesCallback(
}; show,
const auto state = std::make_shared<State>(); peer);
const auto chosen = [=](Ui::ReportReason reason) {
const auto send = [=](const QString &text) { auto performRequest = [=](
window->clearChooseReportMessages(); const auto &repeatRequest,
Api::SendReport( Data::ReportInput reportInput) -> void {
window->uiShow(), constexpr auto kToastDuration = crl::time(4000);
peer, report(reportInput, [=](const Api::ReportResult &result) {
reason, if (!result.error.isEmpty()) {
text, if (result.error == u"MESSAGE_ID_REQUIRED"_q) {
std::move(state->ids)); const auto widget = show->toastParent();
if (const auto strong = state->reasonBox.data()) { const auto window = Core::App().findWindow(widget);
strong->closeBox(); const auto controller = window
? window->sessionController()
: nullptr;
if (controller) {
const auto callback = [=](std::vector<MsgId> ids) {
auto copy = reportInput;
copy.ids = std::move(ids);
repeatRequest(repeatRequest, std::move(copy));
};
controller->showChooseReportMessages(
peer,
reportInput,
std::move(callback));
}
} else {
show->showToast(result.error);
}
return;
} }
if (const auto strong = state->detailsBox.data()) { if (!result.options.empty() || result.commentOption) {
strong->closeBox(); show->show(Box([=](not_null<Ui::GenericBox*> box) {
box->setTitle(
rpl::single(
result.title.isEmpty()
? reportInput.optionText
: result.title));
for (const auto &option : result.options) {
const auto button = Ui::AddReportOptionButton(
box->verticalLayout(),
option.text,
stOverride);
button->setClickedCallback([=] {
auto copy = reportInput;
copy.optionId = option.id;
copy.optionText = option.text;
repeatRequest(repeatRequest, std::move(copy));
});
}
if (const auto commentOption = result.commentOption) {
constexpr auto kReportReasonLengthMax = 512;
const auto &st = stOverride
? stOverride
: &st::defaultReportBox;
Ui::AddReportDetailsIconButton(box);
Ui::AddSkip(box->verticalLayout());
Ui::AddSkip(box->verticalLayout());
const auto details = box->addRow(
object_ptr<Ui::InputField>(
box,
st->field,
Ui::InputField::Mode::MultiLine,
commentOption->optional
? tr::lng_report_details_optional()
: tr::lng_report_details_non_optional(),
QString()));
Ui::AddSkip(box->verticalLayout());
Ui::AddSkip(box->verticalLayout());
{
const auto container = box->verticalLayout();
auto label = object_ptr<Ui::FlatLabel>(
container,
tr::lng_report_details_message_about(),
st::boxDividerLabel);
label->setTextColorOverride(st->dividerFg->c);
using namespace Ui;
const auto widget = container->add(
object_ptr<PaddingWrap<>>(
container,
std::move(label),
st::defaultBoxDividerLabelPadding));
const auto background
= CreateChild<BoxContentDivider>(
widget,
st::boxDividerHeight,
st->dividerBg,
RectPart::Top | RectPart::Bottom);
background->lower();
widget->sizeValue(
) | rpl::start_with_next([=](const QSize &s) {
background->resize(s);
}, background->lifetime());
}
details->setMaxLength(kReportReasonLengthMax);
box->setFocusCallback([=] {
details->setFocusFast();
});
const auto submit = [=] {
if (!commentOption->optional
&& details->empty()) {
details->showError();
details->setFocus();
return;
}
auto copy = reportInput;
copy.optionId = commentOption->id;
copy.comment = details->getLastText();
repeatRequest(repeatRequest, std::move(copy));
};
details->submits(
) | rpl::start_with_next(submit, details->lifetime());
box->addButton(tr::lng_report_button(), submit);
} else {
box->addButton(
tr::lng_close(),
[=] { show->hideLayer(); });
}
if (!reportInput.optionId.isNull()) {
box->addLeftButton(
tr::lng_create_group_back(),
[=] { box->closeBox(); });
}
}));
} else if (result.successful) {
show->showToast(
tr::lng_report_thanks(tr::now),
kToastDuration);
show->hideLayer();
} }
};
if (reason == Ui::ReportReason::Fake
|| reason == Ui::ReportReason::Other) {
state->ids = {};
state->detailsBox = window->show(
Box(Ui::ReportDetailsBox, st::defaultReportBox, send));
return;
}
window->showChooseReportMessages(peer, reason, [=](
MessageIdsList ids) {
state->ids = std::move(ids);
state->detailsBox = window->show(
Box(Ui::ReportDetailsBox, st::defaultReportBox, send));
}); });
}; };
state->reasonBox = window->show(Box( performRequest(performRequest, { .ids = ids, .stories = stories });
Ui::ReportReasonBox,
st::defaultReportBox,
(peer->isBroadcast()
? Ui::ReportSource::Channel
: peer->isUser()
? Ui::ReportSource::Bot
: Ui::ReportSource::Group),
chosen));
} }

View file

@ -12,20 +12,22 @@ class object_ptr;
namespace Ui { namespace Ui {
class BoxContent; class BoxContent;
class Show;
} // namespace Ui } // namespace Ui
namespace Window { namespace style {
class SessionController; struct ReportBox;
} // namespace Main } // namespace style
class PeerData; class PeerData;
[[nodiscard]] object_ptr<Ui::BoxContent> ReportItemsBox(
not_null<PeerData*> peer,
MessageIdsList ids);
[[nodiscard]] object_ptr<Ui::BoxContent> ReportProfilePhotoBox( [[nodiscard]] object_ptr<Ui::BoxContent> ReportProfilePhotoBox(
not_null<PeerData*> peer, not_null<PeerData*> peer,
not_null<PhotoData*> photo); not_null<PhotoData*> photo);
void ShowReportPeerBox(
not_null<Window::SessionController*> window, void ShowReportMessageBox(
not_null<PeerData*> peer); std::shared_ptr<Ui::Show> show,
not_null<PeerData*> peer,
const std::vector<MsgId> &ids,
const std::vector<StoryId> &stories,
const style::ReportBox *stOverride = nullptr);

View file

@ -272,10 +272,13 @@ void SendCreditsBox(
state->confirmButtonBusy = true; state->confirmButtonBusy = true;
session->api().request( session->api().request(
MTPpayments_SendStarsForm( MTPpayments_SendStarsForm(
MTP_flags(0),
MTP_long(form->formId), MTP_long(form->formId),
form->inputInvoice) form->inputInvoice)
).done([=](auto result) { ).done([=](const MTPpayments_PaymentResult &result) {
result.match([&](const MTPDpayments_paymentResult &data) {
session->api().applyUpdates(data.vupdates());
}, [](const MTPDpayments_paymentVerificationNeeded &data) {
});
if (weak) { if (weak) {
state->confirmButtonBusy = false; state->confirmButtonBusy = false;
box->closeBox(); box->closeBox();
@ -311,41 +314,22 @@ void SendCreditsBox(
AddChildToWidgetCenter(button.data(), loadingAnimation); AddChildToWidgetCenter(button.data(), loadingAnimation);
loadingAnimation->showOn(state->confirmButtonBusy.value()); loadingAnimation->showOn(state->confirmButtonBusy.value());
} }
{ SetButtonMarkedLabel(
auto buttonText = tr::lng_credits_box_out_confirm( button,
lt_count, rpl::combine(
rpl::single(form->invoice.amount) | tr::to_count(), tr::lng_credits_box_out_confirm(
lt_emoji, lt_count,
rpl::single(CreditsEmojiSmall(session)), rpl::single(form->invoice.amount) | tr::to_count(),
Ui::Text::RichLangValue); lt_emoji,
const auto buttonLabel = Ui::CreateChild<Ui::FlatLabel>( rpl::single(CreditsEmojiSmall(session)),
button, Ui::Text::RichLangValue),
rpl::single(QString()), state->confirmButtonBusy.value()
st::creditsBoxButtonLabel); ) | rpl::map([](TextWithEntities &&text, bool busy) {
std::move( return busy ? TextWithEntities() : std::move(text);
buttonText }),
) | rpl::start_with_next([=](const TextWithEntities &text) { session,
buttonLabel->setMarkedText( st::creditsBoxButtonLabel,
text, box->getDelegate()->style().button.textFg->c);
Core::MarkedTextContext{
.session = session,
.customEmojiRepaint = [=] { buttonLabel->update(); },
});
}, buttonLabel->lifetime());
buttonLabel->setTextColorOverride(
box->getDelegate()->style().button.textFg->c);
button->sizeValue(
) | rpl::start_with_next([=](const QSize &size) {
buttonLabel->moveToLeft(
(size.width() - buttonLabel->width()) / 2,
(size.height() - buttonLabel->height()) / 2);
}, buttonLabel->lifetime());
buttonLabel->setAttribute(Qt::WA_TransparentForMouseEvents);
state->confirmButtonBusy.value(
) | rpl::start_with_next([=](bool busy) {
buttonLabel->setVisible(!busy);
}, buttonLabel->lifetime());
}
const auto buttonWidth = st::boxWidth const auto buttonWidth = st::boxWidth
- rect::m::sum::h(st::giveawayGiftCodeBox.buttonPadding); - rect::m::sum::h(st::giveawayGiftCodeBox.buttonPadding);
@ -405,4 +389,73 @@ TextWithEntities CreditsEmojiSmall(not_null<Main::Session*> session) {
QString(QChar(0x2B50))); QString(QChar(0x2B50)));
} }
not_null<FlatLabel*> SetButtonMarkedLabel(
not_null<RpWidget*> button,
rpl::producer<TextWithEntities> text,
Fn<std::any(Fn<void()> update)> context,
const style::FlatLabel &st,
std::optional<QColor> textFg) {
const auto buttonLabel = Ui::CreateChild<Ui::FlatLabel>(
button,
rpl::single(QString()),
st);
rpl::duplicate(
text
) | rpl::filter([=](const TextWithEntities &text) {
return !text.text.isEmpty();
}) | rpl::start_with_next([=](const TextWithEntities &text) {
buttonLabel->setMarkedText(
text,
context([=] { buttonLabel->update(); }));
}, buttonLabel->lifetime());
if (textFg) {
buttonLabel->setTextColorOverride(textFg);
}
button->sizeValue(
) | rpl::start_with_next([=](const QSize &size) {
buttonLabel->moveToLeft(
(size.width() - buttonLabel->width()) / 2,
(size.height() - buttonLabel->height()) / 2);
}, buttonLabel->lifetime());
buttonLabel->setAttribute(Qt::WA_TransparentForMouseEvents);
buttonLabel->showOn(std::move(
text
) | rpl::map([=](const TextWithEntities &text) {
return !text.text.isEmpty();
}));
return buttonLabel;
}
not_null<FlatLabel*> SetButtonMarkedLabel(
not_null<RpWidget*> button,
rpl::producer<TextWithEntities> text,
not_null<Main::Session*> session,
const style::FlatLabel &st,
std::optional<QColor> textFg) {
return SetButtonMarkedLabel(button, text, [=](Fn<void()> update) {
return Core::MarkedTextContext{
.session = session,
.customEmojiRepaint = update,
};
}, st, textFg);
}
void SendStarGift(
not_null<Main::Session*> session,
std::shared_ptr<Payments::CreditsFormData> data,
Fn<void(std::optional<QString>)> done) {
session->api().request(MTPpayments_SendStarsForm(
MTP_long(data->formId),
data->inputInvoice
)).done([=](const MTPpayments_PaymentResult &result) {
result.match([&](const MTPDpayments_paymentResult &data) {
session->api().applyUpdates(data.vupdates());
}, [](const MTPDpayments_paymentVerificationNeeded &data) {
});
done(std::nullopt);
}).fail([=](const MTP::Error &error) {
done(error.type());
}).send();
}
} // namespace Ui } // namespace Ui

View file

@ -9,6 +9,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
class HistoryItem; class HistoryItem;
namespace style {
struct FlatLabel;
} // namespace style
namespace Main { namespace Main {
class Session; class Session;
} // namespace Main } // namespace Main
@ -19,7 +23,9 @@ struct CreditsFormData;
namespace Ui { namespace Ui {
class RpWidget;
class GenericBox; class GenericBox;
class FlatLabel;
void SendCreditsBox( void SendCreditsBox(
not_null<Ui::GenericBox*> box, not_null<Ui::GenericBox*> box,
@ -32,4 +38,23 @@ void SendCreditsBox(
[[nodiscard]] TextWithEntities CreditsEmojiSmall( [[nodiscard]] TextWithEntities CreditsEmojiSmall(
not_null<Main::Session*> session); not_null<Main::Session*> session);
not_null<FlatLabel*> SetButtonMarkedLabel(
not_null<RpWidget*> button,
rpl::producer<TextWithEntities> text,
Fn<std::any(Fn<void()> update)> context,
const style::FlatLabel &st,
std::optional<QColor> textFg = {});
not_null<FlatLabel*> SetButtonMarkedLabel(
not_null<RpWidget*> button,
rpl::producer<TextWithEntities> text,
not_null<Main::Session*> session,
const style::FlatLabel &st,
std::optional<QColor> textFg = {});
void SendStarGift(
not_null<Main::Session*> session,
std::shared_ptr<Payments::CreditsFormData> data,
Fn<void(std::optional<QString>)> done);
} // namespace Ui } // namespace Ui

View file

@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "chat_helpers/message_field.h" #include "chat_helpers/message_field.h"
#include "menu/menu_send.h" #include "menu/menu_send.h"
#include "chat_helpers/emoji_suggestions_widget.h" #include "chat_helpers/emoji_suggestions_widget.h"
#include "chat_helpers/field_autocomplete.h"
#include "chat_helpers/tabbed_panel.h" #include "chat_helpers/tabbed_panel.h"
#include "chat_helpers/tabbed_selector.h" #include "chat_helpers/tabbed_selector.h"
#include "editor/photo_editor_layer_widget.h" #include "editor/photo_editor_layer_widget.h"
@ -1304,13 +1305,17 @@ void SendFilesBox::setupCaption() {
: (_limits & SendFilesAllow::EmojiWithoutPremium); : (_limits & SendFilesAllow::EmojiWithoutPremium);
}; };
const auto show = _show; const auto show = _show;
InitMessageFieldHandlers( InitMessageFieldHandlers({
&show->session(), .session = &show->session(),
show, .show = show,
_caption.data(), .field = _caption.data(),
[=] { return show->paused(Window::GifPauseReason::Layer); }, .customEmojiPaused = [=] {
allow, return show->paused(Window::GifPauseReason::Layer);
&_st.files.caption); },
.allowPremiumEmoji = allow,
.fieldStyle = &_st.files.caption,
});
setupCaptionAutocomplete();
Ui::Emoji::SuggestionsController::Init( Ui::Emoji::SuggestionsController::Init(
getDelegate()->outerContainer(), getDelegate()->outerContainer(),
_caption, _caption,
@ -1370,6 +1375,59 @@ void SendFilesBox::setupCaption() {
}, _caption->lifetime()); }, _caption->lifetime());
} }
void SendFilesBox::setupCaptionAutocomplete() {
if (!_captionToPeer || !_caption) {
return;
}
const auto parent = getDelegate()->outerContainer();
ChatHelpers::InitFieldAutocomplete(_autocomplete, {
.parent = parent,
.show = _show,
.field = _caption.data(),
.peer = _captionToPeer,
.features = [=] {
auto result = ChatHelpers::ComposeFeatures();
result.autocompleteCommands = false;
result.suggestStickersByEmoji = false;
return result;
},
.sendMenuDetails = _sendMenuDetails,
});
const auto raw = _autocomplete.get();
const auto scheduled = std::make_shared<bool>();
const auto recountPostponed = [=] {
if (*scheduled) {
return;
}
*scheduled = true;
Ui::PostponeCall(raw, [=] {
*scheduled = false;
auto field = Ui::MapFrom(parent, this, _caption->geometry());
_autocomplete->setBoundings(QRect(
field.x() - _caption->x(),
st::defaultBox.margin.top(),
width(),
(field.y()
+ _st.files.caption.textMargins.top()
+ _st.files.caption.placeholderShift
+ _st.files.caption.placeholderFont->height
- st::defaultBox.margin.top())));
});
};
for (auto w = (QWidget*)_caption.data(); w; w = w->parentWidget()) {
base::install_event_filter(raw, w, [=](not_null<QEvent*> e) {
if (e->type() == QEvent::Move || e->type() == QEvent::Resize) {
recountPostponed();
}
return base::EventFilterResult::Continue;
});
if (w == parent) {
break;
}
}
}
void SendFilesBox::checkCharsLimitation() { void SendFilesBox::checkCharsLimitation() {
const auto limits = Data::PremiumLimits(&_show->session()); const auto limits = Data::PremiumLimits(&_show->session());
const auto caption = (_caption && !_caption->isHidden()) const auto caption = (_caption && !_caption->isHidden())
@ -1685,6 +1743,14 @@ void SendFilesBox::updateControlsGeometry() {
_scroll->move(0, _titleHeight.current()); _scroll->move(0, _titleHeight.current());
} }
void SendFilesBox::showFinished() {
if (const auto raw = _autocomplete.get()) {
InvokeQueued(raw, [=] {
raw->raise();
});
}
}
void SendFilesBox::setInnerFocus() { void SendFilesBox::setInnerFocus() {
if (_caption && !_caption->isHidden()) { if (_caption && !_caption->isHidden()) {
_caption->setFocusFast(); _caption->setFocusFast();

View file

@ -28,6 +28,7 @@ enum class SendType;
namespace ChatHelpers { namespace ChatHelpers {
class TabbedPanel; class TabbedPanel;
class Show; class Show;
class FieldAutocomplete;
} // namespace ChatHelpers } // namespace ChatHelpers
namespace Ui { namespace Ui {
@ -126,6 +127,8 @@ public:
_cancelledCallback = std::move(callback); _cancelledCallback = std::move(callback);
} }
void showFinished() override;
~SendFilesBox(); ~SendFilesBox();
protected: protected:
@ -206,6 +209,7 @@ private:
void refreshControls(bool initial = false); void refreshControls(bool initial = false);
void setupSendWayControls(); void setupSendWayControls();
void setupCaption(); void setupCaption();
void setupCaptionAutocomplete();
void setupEmojiPanel(); void setupEmojiPanel();
void updateSendWayControls(); void updateSendWayControls();
@ -257,6 +261,7 @@ private:
SendFilesLimits _limits = {}; SendFilesLimits _limits = {};
Fn<MenuDetails()> _sendMenuDetails; Fn<MenuDetails()> _sendMenuDetails;
Fn<void(MenuAction, MenuDetails)> _sendMenuCallback; Fn<void(MenuAction, MenuDetails)> _sendMenuCallback;
PeerData *_captionToPeer = nullptr; PeerData *_captionToPeer = nullptr;
SendFilesCheck _check; SendFilesCheck _check;
SendFilesConfirmed _confirmedCallback; SendFilesConfirmed _confirmedCallback;
@ -268,6 +273,7 @@ private:
bool _invertCaption = false; bool _invertCaption = false;
object_ptr<Ui::InputField> _caption = { nullptr }; object_ptr<Ui::InputField> _caption = { nullptr };
std::unique_ptr<ChatHelpers::FieldAutocomplete> _autocomplete;
TextWithTags _prefilledCaptionText; TextWithTags _prefilledCaptionText;
object_ptr<Ui::EmojiButton> _emojiToggle = { nullptr }; object_ptr<Ui::EmojiButton> _emojiToggle = { nullptr };
base::unique_qptr<ChatHelpers::TabbedPanel> _emojiPanel; base::unique_qptr<ChatHelpers::TabbedPanel> _emojiPanel;

View file

@ -7,7 +7,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#include "boxes/send_gif_with_caption_box.h" #include "boxes/send_gif_with_caption_box.h"
#include "base/event_filter.h"
#include "boxes/premium_preview_box.h" #include "boxes/premium_preview_box.h"
#include "chat_helpers/field_autocomplete.h"
#include "chat_helpers/message_field.h" #include "chat_helpers/message_field.h"
#include "chat_helpers/tabbed_panel.h" #include "chat_helpers/tabbed_panel.h"
#include "chat_helpers/tabbed_selector.h" #include "chat_helpers/tabbed_selector.h"
@ -30,9 +32,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/controls/emoji_button.h" #include "ui/controls/emoji_button.h"
#include "ui/controls/emoji_button_factory.h" #include "ui/controls/emoji_button_factory.h"
#include "ui/layers/generic_box.h" #include "ui/layers/generic_box.h"
#include "ui/rect.h"
#include "ui/vertical_list.h"
#include "ui/widgets/fields/input_field.h" #include "ui/widgets/fields/input_field.h"
#include "ui/rect.h"
#include "ui/ui_utility.h"
#include "ui/vertical_list.h"
#include "window/window_controller.h" #include "window/window_controller.h"
#include "window/window_session_controller.h" #include "window/window_session_controller.h"
#include "styles/style_boxes.h" #include "styles/style_boxes.h"
@ -226,6 +229,7 @@ namespace {
void SendGifWithCaptionBox( void SendGifWithCaptionBox(
not_null<Ui::GenericBox*> box, not_null<Ui::GenericBox*> box,
not_null<DocumentData*> document, not_null<DocumentData*> document,
not_null<PeerData*> peer,
const SendMenu::Details &details, const SendMenu::Details &details,
Fn<void(Api::SendOptions, TextWithTags)> done) { Fn<void(Api::SendOptions, TextWithTags)> done) {
const auto window = Core::App().findWindow(box); const auto window = Core::App().findWindow(box);
@ -255,6 +259,61 @@ void SendGifWithCaptionBox(
return true; return true;
}); });
const auto sendMenuDetails = [=] { return details; };
struct Autocomplete {
std::unique_ptr<ChatHelpers::FieldAutocomplete> dropdown;
bool geometryUpdateScheduled = false;
};
const auto autocomplete = box->lifetime().make_state<Autocomplete>();
const auto outer = box->getDelegate()->outerContainer();
ChatHelpers::InitFieldAutocomplete(autocomplete->dropdown, {
.parent = outer,
.show = controller->uiShow(),
.field = input,
.peer = peer,
.features = [=] {
auto result = ChatHelpers::ComposeFeatures();
result.autocompleteCommands = false;
result.suggestStickersByEmoji = false;
return result;
},
.sendMenuDetails = sendMenuDetails,
});
const auto raw = autocomplete->dropdown.get();
const auto recountPostponed = [=] {
if (autocomplete->geometryUpdateScheduled) {
return;
}
autocomplete->geometryUpdateScheduled = true;
Ui::PostponeCall(raw, [=] {
autocomplete->geometryUpdateScheduled = false;
const auto from = input->parentWidget();
auto field = Ui::MapFrom(outer, from, input->geometry());
const auto &st = st::defaultComposeFiles;
autocomplete->dropdown->setBoundings(QRect(
field.x() - input->x(),
st::defaultBox.margin.top(),
input->width(),
(field.y()
+ st.caption.textMargins.top()
+ st.caption.placeholderShift
+ st.caption.placeholderFont->height
- st::defaultBox.margin.top())));
});
};
for (auto w = (QWidget*)input; w; w = w->parentWidget()) {
base::install_event_filter(raw, w, [=](not_null<QEvent*> e) {
if (e->type() == QEvent::Move || e->type() == QEvent::Resize) {
recountPostponed();
}
return base::EventFilterResult::Continue;
});
if (w == outer) {
break;
}
}
const auto send = [=](Api::SendOptions options) { const auto send = [=](Api::SendOptions options) {
done(std::move(options), input->getTextWithTags()); done(std::move(options), input->getTextWithTags());
}; };
@ -264,8 +323,15 @@ void SendGifWithCaptionBox(
SendMenu::SetupMenuAndShortcuts( SendMenu::SetupMenuAndShortcuts(
confirm, confirm,
controller->uiShow(), controller->uiShow(),
[=] { return details; }, sendMenuDetails,
SendMenu::DefaultCallback(controller->uiShow(), send)); SendMenu::DefaultCallback(controller->uiShow(), send));
box->setShowFinishedCallback([=] {
if (const auto raw = autocomplete->dropdown.get()) {
InvokeQueued(raw, [=] {
raw->raise();
});
}
});
box->addButton(tr::lng_cancel(), [=] { box->addButton(tr::lng_cancel(), [=] {
box->closeBox(); box->closeBox();
}); });

View file

@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#pragma once #pragma once
class PeerData;
class DocumentData; class DocumentData;
namespace Api { namespace Api {
@ -24,6 +25,7 @@ class GenericBox;
void SendGifWithCaptionBox( void SendGifWithCaptionBox(
not_null<Ui::GenericBox*> box, not_null<Ui::GenericBox*> box,
not_null<DocumentData*> document, not_null<DocumentData*> document,
not_null<PeerData*> peer,
const SendMenu::Details &details, const SendMenu::Details &details,
Fn<void(Api::SendOptions, TextWithTags)> done); Fn<void(Api::SendOptions, TextWithTags)> done);

View file

@ -245,13 +245,12 @@ void ShareBox::prepareCommentField() {
}, field->lifetime()); }, field->lifetime());
if (const auto show = uiShow(); show->valid()) { if (const auto show = uiShow(); show->valid()) {
InitMessageFieldHandlers( InitMessageFieldHandlers({
_descriptor.session, .session = _descriptor.session,
Main::MakeSessionShow(show, _descriptor.session), .show = Main::MakeSessionShow(show, _descriptor.session),
field, .field = field,
nullptr, .fieldStyle = _descriptor.stLabel,
nullptr, });
_descriptor.stLabel);
} }
field->setSubmitSettings(Core::App().settings().sendSubmitWay()); field->setSubmitSettings(Core::App().settings().sendSubmitWay());
@ -843,6 +842,8 @@ void ShareBox::Inner::updateChatName(not_null<Chat*> chat) {
? tr::lng_saved_messages(tr::now) ? tr::lng_saved_messages(tr::now)
: peer->isRepliesChat() : peer->isRepliesChat()
? tr::lng_replies_messages(tr::now) ? tr::lng_replies_messages(tr::now)
: peer->isVerifyCodes()
? tr::lng_verification_codes(tr::now)
: peer->name(); : peer->name();
chat->name.setText(_st.item.nameStyle, text, Ui::NameTextOptions()); chat->name.setText(_st.item.nameStyle, text, Ui::NameTextOptions());
} }

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,23 @@
/*
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 Window {
class SessionController;
} // namespace Window
namespace Ui {
void ChooseStarGiftRecipient(
not_null<Window::SessionController*> controller);
void ShowStarGiftBox(
not_null<Window::SessionController*> controller,
not_null<PeerData*> peer);
} // namespace Ui

View file

@ -89,7 +89,8 @@ using TLStickerSet = MTPmessages_StickerSet;
[[nodiscard]] std::optional<QColor> ComputeImageColor( [[nodiscard]] std::optional<QColor> ComputeImageColor(
const style::icon &lockIcon, const style::icon &lockIcon,
const QImage &frame) { const QImage &frame,
RectPart part) {
if (frame.isNull() if (frame.isNull()
|| frame.format() != QImage::Format_ARGB32_Premultiplied) { || frame.format() != QImage::Format_ARGB32_Premultiplied) {
return {}; return {};
@ -98,13 +99,29 @@ using TLStickerSet = MTPmessages_StickerSet;
auto sg = int64(); auto sg = int64();
auto sb = int64(); auto sb = int64();
auto sa = int64(); auto sa = int64();
const auto factor = frame.devicePixelRatio(); const auto factor = style::DevicePixelRatio();
const auto size = lockIcon.size() * factor; const auto size = lockIcon.size() * factor;
const auto width = std::min(frame.width(), size.width()); const auto width = std::min(frame.width(), size.width());
const auto height = std::min(frame.height(), size.height()); const auto height = std::min(frame.height(), size.height());
const auto skipx = (frame.width() - width) / 2;
const auto radius = st::roundRadiusSmall; const auto radius = st::roundRadiusSmall;
const auto skipy = std::max(frame.height() - height - radius, 0); const auto skipx = (part == RectPart::TopLeft
|| part == RectPart::Left
|| part == RectPart::BottomLeft)
? 0
: (part == RectPart::Top
|| part == RectPart::Center
|| part == RectPart::Bottom)
? (frame.width() - width) / 2
: std::max(frame.width() - width - radius, 0);
const auto skipy = (part == RectPart::TopLeft
|| part == RectPart::Top
|| part == RectPart::TopRight)
? 0
: (part == RectPart::Left
|| part == RectPart::Center
|| part == RectPart::Right)
? (frame.height() - height) / 2
: std::max(frame.height() - height - radius, 0);
const auto perline = frame.bytesPerLine(); const auto perline = frame.bytesPerLine();
const auto addperline = perline - (width * 4); const auto addperline = perline - (width * 4);
auto bits = static_cast<const uchar*>(frame.bits()) auto bits = static_cast<const uchar*>(frame.bits())
@ -128,17 +145,20 @@ using TLStickerSet = MTPmessages_StickerSet;
[[nodiscard]] QColor ComputeLockColor( [[nodiscard]] QColor ComputeLockColor(
const style::icon &lockIcon, const style::icon &lockIcon,
const QImage &frame) { const QImage &frame,
RectPart part) {
return ComputeImageColor( return ComputeImageColor(
lockIcon, lockIcon,
frame frame,
part
).value_or(st::windowSubTextFg->c); ).value_or(st::windowSubTextFg->c);
} }
void ValidatePremiumLockBg( void ValidatePremiumLockBg(
const style::icon &lockIcon, const style::icon &lockIcon,
QImage &image, QImage &image,
const QImage &frame) { const QImage &frame,
RectPart part) {
if (!image.isNull()) { if (!image.isNull()) {
return; return;
} }
@ -149,7 +169,7 @@ void ValidatePremiumLockBg(
QImage::Format_ARGB32_Premultiplied); QImage::Format_ARGB32_Premultiplied);
image.setDevicePixelRatio(factor); image.setDevicePixelRatio(factor);
auto p = QPainter(&image); auto p = QPainter(&image);
const auto color = ComputeLockColor(lockIcon, frame); const auto color = ComputeLockColor(lockIcon, frame, part);
p.fillRect( p.fillRect(
QRect(QPoint(), size), QRect(QPoint(), size),
anim::color(color, st::windowSubTextFg, kGrayLockOpacity)); anim::color(color, st::windowSubTextFg, kGrayLockOpacity));
@ -202,8 +222,10 @@ void ValidatePremiumStarFg(const style::icon &lockIcon, QImage &image) {
StickerPremiumMark::StickerPremiumMark( StickerPremiumMark::StickerPremiumMark(
not_null<Main::Session*> session, not_null<Main::Session*> session,
const style::icon &lockIcon) const style::icon &lockIcon,
: _lockIcon(lockIcon) { RectPart part)
: _lockIcon(lockIcon)
, _part(part) {
style::PaletteChanged( style::PaletteChanged(
) | rpl::start_with_next([=] { ) | rpl::start_with_next([=] {
_lockGray = QImage(); _lockGray = QImage();
@ -228,11 +250,15 @@ void StickerPremiumMark::paint(
const auto &bg = frame.isNull() ? _lockGray : backCache; const auto &bg = frame.isNull() ? _lockGray : backCache;
const auto factor = style::DevicePixelRatio(); const auto factor = style::DevicePixelRatio();
const auto radius = st::roundRadiusSmall; const auto radius = st::roundRadiusSmall;
const auto point = position + QPoint( const auto shiftx = (_part == RectPart::Center)
(singleSize.width() - (bg.width() / factor) - radius), ? (singleSize.width() - (bg.width() / factor)) / 2
singleSize.height() - (bg.height() / factor) - radius); : (singleSize.width() - (bg.width() / factor) - radius);
const auto shifty = (_part == RectPart::Center)
? (singleSize.height() - (bg.height() / factor)) / 2
: (singleSize.height() - (bg.height() / factor) - radius);
const auto point = position + QPoint(shiftx, shifty);
p.drawImage(point, bg); p.drawImage(point, bg);
if (_premium) { if (_premium && _part != RectPart::Center) {
validateStar(); validateStar();
p.drawImage(point, _star); p.drawImage(point, _star);
} else { } else {
@ -244,7 +270,7 @@ void StickerPremiumMark::validateLock(
const QImage &frame, const QImage &frame,
QImage &backCache) { QImage &backCache) {
auto &image = frame.isNull() ? _lockGray : backCache; auto &image = frame.isNull() ? _lockGray : backCache;
ValidatePremiumLockBg(_lockIcon, image, frame); ValidatePremiumLockBg(_lockIcon, image, frame, _part);
} }
void StickerPremiumMark::validateStar() { void StickerPremiumMark::validateStar() {
@ -1447,9 +1473,13 @@ void StickerSetBox::Inner::contextMenuEvent(QContextMenuEvent *e) {
const auto send = crl::guard(this, [=](Api::SendOptions options) { const auto send = crl::guard(this, [=](Api::SendOptions options) {
chosen(index, document, options); chosen(index, document, options);
}); });
// In case we're adding items after FillSendMenu we have
// to pass nullptr for showForEffect and attach selector later.
// Otherwise added items widths won't be respected in menu geometry.
SendMenu::FillSendMenu( SendMenu::FillSendMenu(
_menu.get(), _menu.get(),
_show, nullptr, // showForEffect
details, details,
SendMenu::DefaultCallback(_show, send)); SendMenu::DefaultCallback(_show, send));
@ -1483,6 +1513,12 @@ void StickerSetBox::Inner::contextMenuEvent(QContextMenuEvent *e) {
.isAttention = true, .isAttention = true,
}); });
} }
SendMenu::AttachSendMenuEffect(
_menu.get(),
_show,
details,
SendMenu::DefaultCallback(_show, send));
} }
if (_menu->empty()) { if (_menu->empty()) {
_menu = nullptr; _menu = nullptr;

View file

@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/layers/box_content.h" #include "ui/layers/box_content.h"
#include "base/timer.h" #include "base/timer.h"
#include "data/stickers/data_stickers.h" #include "data/stickers/data_stickers.h"
#include "ui/rect_part.h"
namespace Window { namespace Window {
class SessionController; class SessionController;
@ -32,7 +33,8 @@ class StickerPremiumMark final {
public: public:
StickerPremiumMark( StickerPremiumMark(
not_null<Main::Session*> session, not_null<Main::Session*> session,
const style::icon &lockIcon); const style::icon &lockIcon,
RectPart part = RectPart::Bottom);
void paint( void paint(
QPainter &p, QPainter &p,
@ -49,6 +51,7 @@ private:
const style::icon &_lockIcon; const style::icon &_lockIcon;
QImage _lockGray; QImage _lockGray;
QImage _star; QImage _star;
RectPart _part = RectPart::Bottom;
bool _premium = false; bool _premium = false;
rpl::lifetime _lifetime; rpl::lifetime _lifetime;

View file

@ -1139,6 +1139,7 @@ groupCallTitle: WindowTitle(defaultWindowTitle) {
bgActive: groupCallBg; bgActive: groupCallBg;
fg: transparent; fg: transparent;
fgActive: transparent; fgActive: transparent;
oneSideControls: true;
minimize: IconButton(groupCallTitleButton) { minimize: IconButton(groupCallTitleButton) {
icon: groupCallTitleMinimizeIcon; icon: groupCallTitleMinimizeIcon;
iconOver: groupCallTitleMinimizeIconOver; iconOver: groupCallTitleMinimizeIconOver;

View file

@ -198,7 +198,9 @@ void Panel::initWindow() {
return Flag::None | Flag(0); return Flag::None | Flag(0);
} }
#ifndef Q_OS_MAC #ifndef Q_OS_MAC
if (_controls->controls.geometry().contains(widgetPoint)) { using Result = Ui::Platform::HitTestResult;
const auto windowPoint = widget()->mapTo(window(), widgetPoint);
if (_controls->controls.hitTest(windowPoint) != Result::None) {
return Flag::None | Flag(0); return Flag::None | Flag(0);
} }
#endif // !Q_OS_MAC #endif // !Q_OS_MAC

View file

@ -23,7 +23,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include <tgcalls/desktop_capturer/DesktopCaptureSourceManager.h> #include <tgcalls/desktop_capturer/DesktopCaptureSourceManager.h>
#include <tgcalls/desktop_capturer/DesktopCaptureSourceHelper.h> #include <tgcalls/desktop_capturer/DesktopCaptureSourceHelper.h>
#include <QtGui/QGuiApplication>
#include <QtGui/QWindow> #include <QtGui/QWindow>
namespace Calls::Group::Ui::DesktopCapture { namespace Calls::Group::Ui::DesktopCapture {
@ -585,13 +584,7 @@ void ChooseSourceProcess::setupSourcesGeometry() {
void ChooseSourceProcess::setupGeometryWithParent( void ChooseSourceProcess::setupGeometryWithParent(
not_null<QWidget*> parent) { not_null<QWidget*> parent) {
const auto parentScreen = [&] { const auto parentScreen = parent->screen();
if (const auto screen = QGuiApplication::screenAt(
parent->geometry().center())) {
return screen;
}
return parent->screen();
}();
const auto myScreen = _window->screen(); const auto myScreen = _window->screen();
if (parentScreen && myScreen != parentScreen) { if (parentScreen && myScreen != parentScreen) {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)

View file

@ -218,8 +218,11 @@ ComposeControls {
ReportBox { ReportBox {
button: SettingsButton; button: SettingsButton;
noIconButton: SettingsButton;
label: FlatLabel; label: FlatLabel;
field: InputField; field: InputField;
dividerBg: color;
dividerFg: color;
spam: icon; spam: icon;
fake: icon; fake: icon;
violence: icon; violence: icon;
@ -1360,8 +1363,13 @@ reportReasonButton: SettingsButton(defaultSettingsButton) {
defaultReportBox: ReportBox { defaultReportBox: ReportBox {
button: reportReasonButton; button: reportReasonButton;
noIconButton: SettingsButton(reportReasonButton) {
padding: margins(22px, 7px, 8px, 7px);
}
label: boxLabel; label: boxLabel;
field: newGroupDescription; field: newGroupDescription;
dividerBg: boxDividerBg;
dividerFg: windowSubTextFg;
spam: menuIconDelete; spam: menuIconDelete;
fake: menuIconFake; fake: menuIconFake;
violence: menuIconViolence; violence: menuIconViolence;

View file

@ -10,20 +10,21 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace ChatHelpers { namespace ChatHelpers {
struct ComposeFeatures { struct ComposeFeatures {
bool likes = false; bool likes : 1 = false;
bool sendAs = true; bool sendAs : 1 = true;
bool ttlInfo = true; bool ttlInfo : 1 = true;
bool botCommandSend = true; bool botCommandSend : 1 = true;
bool silentBroadcastToggle = true; bool silentBroadcastToggle : 1 = true;
bool attachBotsMenu = true; bool attachBotsMenu : 1 = true;
bool inlineBots = true; bool inlineBots : 1 = true;
bool megagroupSet = true; bool megagroupSet : 1 = true;
bool stickersSettings = true; bool stickersSettings : 1 = true;
bool openStickerSets = true; bool openStickerSets : 1 = true;
bool autocompleteHashtags = true; bool autocompleteHashtags : 1 = true;
bool autocompleteMentions = true; bool autocompleteMentions : 1 = true;
bool autocompleteCommands = true; bool autocompleteCommands : 1 = true;
bool commonTabbedPanel = true; bool suggestStickersByEmoji : 1 = true;
bool commonTabbedPanel : 1 = true;
}; };
} // namespace ChatHelpers } // namespace ChatHelpers

View file

@ -41,9 +41,9 @@ inline auto PreviewPath(int i) {
const auto kSets = { const auto kSets = {
Set{ { 0, 0, 0, "Mac" }, PreviewPath(0) }, Set{ { 0, 0, 0, "Mac" }, PreviewPath(0) },
Set{ { 1, 1804, 8'115'639, "Android" }, PreviewPath(1) }, Set{ { 1, 2290, 8'306'943, "Android" }, PreviewPath(1) },
Set{ { 2, 1805, 5'481'197, "Twemoji" }, PreviewPath(2) }, Set{ { 2, 2291, 5'694'303, "Twemoji" }, PreviewPath(2) },
Set{ { 3, 1806, 7'047'594, "JoyPixels" }, PreviewPath(3) }, Set{ { 3, 2292, 7'261'223, "JoyPixels" }, PreviewPath(3) },
}; };
using Loading = MTP::DedicatedLoader::Progress; using Loading = MTP::DedicatedLoader::Progress;

View file

@ -713,11 +713,13 @@ void SuggestionsWidget::leaveEventHook(QEvent *e) {
} }
SuggestionsController::SuggestionsController( SuggestionsController::SuggestionsController(
not_null<QWidget*> parent,
not_null<QWidget*> outer, not_null<QWidget*> outer,
not_null<QTextEdit*> field, not_null<QTextEdit*> field,
not_null<Main::Session*> session, not_null<Main::Session*> session,
const Options &options) const Options &options)
: _st(options.st ? *options.st : st::defaultEmojiSuggestions) : QObject(parent)
, _st(options.st ? *options.st : st::defaultEmojiSuggestions)
, _field(field) , _field(field)
, _session(session) , _session(session)
, _showExactTimer([=] { showWithQuery(getEmojiQuery()); }) , _showExactTimer([=] { showWithQuery(getEmojiQuery()); })

View file

@ -37,7 +37,7 @@ class SuggestionsWidget;
using SuggestionsQuery = std::variant<QString, EmojiPtr>; using SuggestionsQuery = std::variant<QString, EmojiPtr>;
class SuggestionsController { class SuggestionsController final : public QObject {
public: public:
struct Options { struct Options {
bool suggestExactFirstWord = true; bool suggestExactFirstWord = true;
@ -47,6 +47,7 @@ public:
}; };
SuggestionsController( SuggestionsController(
not_null<QWidget*> parent,
not_null<QWidget*> outer, not_null<QWidget*> outer,
not_null<QTextEdit*> field, not_null<QTextEdit*> field,
not_null<Main::Session*> session, not_null<Main::Session*> session,

View file

@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/business/data_shortcut_messages.h" #include "data/business/data_shortcut_messages.h"
#include "data/data_document.h" #include "data/data_document.h"
#include "data/data_document_media.h" #include "data/data_document_media.h"
#include "data/data_changes.h"
#include "data/data_channel.h" #include "data/data_channel.h"
#include "data/data_chat.h" #include "data/data_chat.h"
#include "data/data_user.h" #include "data/data_user.h"
@ -53,6 +54,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include <QtWidgets/QApplication> #include <QtWidgets/QApplication>
namespace ChatHelpers {
namespace { namespace {
[[nodiscard]] QString PrimaryUsername(not_null<UserData*> user) { [[nodiscard]] QString PrimaryUsername(not_null<UserData*> user) {
@ -60,6 +62,18 @@ namespace {
return usernames.empty() ? user->username() : usernames.front(); return usernames.empty() ? user->username() : usernames.front();
} }
template <typename T, typename U>
inline int indexOfInFirstN(const T &v, const U &elem, int last) {
for (auto b = v.cbegin(), i = b, e = b + std::max(int(v.size()), last)
; i != e
; ++i) {
if (i->user == elem) {
return (i - b);
}
}
return -1;
}
} // namespace } // namespace
class FieldAutocomplete::Inner final : public Ui::RpWidget { class FieldAutocomplete::Inner final : public Ui::RpWidget {
@ -70,7 +84,7 @@ public:
}; };
Inner( Inner(
std::shared_ptr<ChatHelpers::Show> show, std::shared_ptr<Show> show,
const style::EmojiPan &st, const style::EmojiPan &st,
not_null<FieldAutocomplete*> parent, not_null<FieldAutocomplete*> parent,
not_null<MentionRows*> mrows, not_null<MentionRows*> mrows,
@ -127,7 +141,7 @@ private:
Media::Clip::Notification notification, Media::Clip::Notification notification,
not_null<DocumentData*> document); not_null<DocumentData*> document);
const std::shared_ptr<ChatHelpers::Show> _show; const std::shared_ptr<Show> _show;
const not_null<Main::Session*> _session; const not_null<Main::Session*> _session;
const style::EmojiPan &_st; const style::EmojiPan &_st;
const not_null<FieldAutocomplete*> _parent; const not_null<FieldAutocomplete*> _parent;
@ -191,13 +205,7 @@ struct FieldAutocomplete::BotCommandRow {
FieldAutocomplete::FieldAutocomplete( FieldAutocomplete::FieldAutocomplete(
QWidget *parent, QWidget *parent,
not_null<Window::SessionController*> controller) std::shared_ptr<Show> show,
: FieldAutocomplete(parent, controller->uiShow()) {
}
FieldAutocomplete::FieldAutocomplete(
QWidget *parent,
std::shared_ptr<ChatHelpers::Show> show,
const style::EmojiPan *stOverride) const style::EmojiPan *stOverride)
: RpWidget(parent) : RpWidget(parent)
, _show(std::move(show)) , _show(std::move(show))
@ -235,10 +243,26 @@ FieldAutocomplete::FieldAutocomplete(
}), lifetime()); }), lifetime());
} }
std::shared_ptr<ChatHelpers::Show> FieldAutocomplete::uiShow() const { std::shared_ptr<Show> FieldAutocomplete::uiShow() const {
return _show; return _show;
} }
void FieldAutocomplete::requestRefresh() {
_refreshRequests.fire({});
}
rpl::producer<> FieldAutocomplete::refreshRequests() const {
return _refreshRequests.events();
}
void FieldAutocomplete::requestStickersUpdate() {
_stickersUpdateRequests.fire({});
}
rpl::producer<> FieldAutocomplete::stickersUpdateRequests() const {
return _stickersUpdateRequests.events();
}
auto FieldAutocomplete::mentionChosen() const auto FieldAutocomplete::mentionChosen() const
-> rpl::producer<FieldAutocomplete::MentionChosen> { -> rpl::producer<FieldAutocomplete::MentionChosen> {
return _inner->mentionChosen(); return _inner->mentionChosen();
@ -365,6 +389,10 @@ void FieldAutocomplete::showStickers(EmojiPtr emoji) {
updateFiltered(resetScroll); updateFiltered(resetScroll);
} }
EmojiPtr FieldAutocomplete::stickersEmoji() const {
return _emoji;
}
bool FieldAutocomplete::clearFilteredBotCommands() { bool FieldAutocomplete::clearFilteredBotCommands() {
if (_brows.empty()) { if (_brows.empty()) {
return false; return false;
@ -373,18 +401,6 @@ bool FieldAutocomplete::clearFilteredBotCommands() {
return true; return true;
} }
namespace {
template <typename T, typename U>
inline int indexOfInFirstN(const T &v, const U &elem, int last) {
for (auto b = v.cbegin(), i = b, e = b + std::max(int(v.size()), last); i != e; ++i) {
if (i->user == elem) {
return (i - b);
}
}
return -1;
}
}
FieldAutocomplete::StickerRows FieldAutocomplete::getStickerSuggestions() { FieldAutocomplete::StickerRows FieldAutocomplete::getStickerSuggestions() {
const auto data = &_session->data().stickers(); const auto data = &_session->data().stickers();
const auto list = data->getListByEmoji({ _emoji }, _stickersSeed); const auto list = data->getListByEmoji({ _emoji }, _stickersSeed);
@ -871,7 +887,7 @@ bool FieldAutocomplete::eventFilter(QObject *obj, QEvent *e) {
} }
FieldAutocomplete::Inner::Inner( FieldAutocomplete::Inner::Inner(
std::shared_ptr<ChatHelpers::Show> show, std::shared_ptr<Show> show,
const style::EmojiPan &st, const style::EmojiPan &st,
not_null<FieldAutocomplete*> parent, not_null<FieldAutocomplete*> parent,
not_null<MentionRows*> mrows, not_null<MentionRows*> mrows,
@ -963,8 +979,8 @@ void FieldAutocomplete::Inner::paintEvent(QPaintEvent *e) {
media->checkStickerSmall(); media->checkStickerSmall();
const auto paused = _show->paused( const auto paused = _show->paused(
ChatHelpers::PauseReason::TabbedPanel); PauseReason::TabbedPanel);
const auto size = ChatHelpers::ComputeStickerSize( const auto size = ComputeStickerSize(
document, document,
stickerBoundingBox()); stickerBoundingBox());
const auto ppos = pos + QPoint( const auto ppos = pos + QPoint(
@ -989,7 +1005,7 @@ void FieldAutocomplete::Inner::paintEvent(QPaintEvent *e) {
} else if (const auto image = media->getStickerSmall()) { } else if (const auto image = media->getStickerSmall()) {
p.drawPixmapLeft(ppos, width(), image->pix(size)); p.drawPixmapLeft(ppos, width(), image->pix(size));
} else { } else {
ChatHelpers::PaintStickerThumbnailPath( PaintStickerThumbnailPath(
p, p,
media.get(), media.get(),
QRect(ppos, size), QRect(ppos, size),
@ -1250,7 +1266,7 @@ bool FieldAutocomplete::Inner::chooseAtIndex(
const auto bounding = selectedRect(index); const auto bounding = selectedRect(index);
auto contentRect = QRect( auto contentRect = QRect(
QPoint(), QPoint(),
ChatHelpers::ComputeStickerSize( ComputeStickerSize(
document, document,
stickerBoundingBox())); stickerBoundingBox()));
contentRect.moveCenter(bounding.center()); contentRect.moveCenter(bounding.center());
@ -1464,9 +1480,9 @@ auto FieldAutocomplete::Inner::getLottieRenderer()
void FieldAutocomplete::Inner::setupLottie(StickerSuggestion &suggestion) { void FieldAutocomplete::Inner::setupLottie(StickerSuggestion &suggestion) {
const auto document = suggestion.document; const auto document = suggestion.document;
suggestion.lottie = ChatHelpers::LottiePlayerFromDocument( suggestion.lottie = LottiePlayerFromDocument(
suggestion.documentMedia.get(), suggestion.documentMedia.get(),
ChatHelpers::StickerLottieSize::InlineResults, StickerLottieSize::InlineResults,
stickerBoundingBox() * style::DevicePixelRatio(), stickerBoundingBox() * style::DevicePixelRatio(),
Lottie::Quality::Default, Lottie::Quality::Default,
getLottieRenderer()); getLottieRenderer());
@ -1534,7 +1550,7 @@ void FieldAutocomplete::Inner::clipCallback(
} else if (i->webm->state() == State::Error) { } else if (i->webm->state() == State::Error) {
i->webm.setBad(); i->webm.setBad();
} else if (i->webm->ready() && !i->webm->started()) { } else if (i->webm->ready() && !i->webm->started()) {
const auto size = ChatHelpers::ComputeStickerSize( const auto size = ComputeStickerSize(
i->document, i->document,
stickerBoundingBox()); stickerBoundingBox());
i->webm->start({ .frame = size, .keepAlpha = true }); i->webm->start({ .frame = size, .keepAlpha = true });
@ -1632,3 +1648,171 @@ auto FieldAutocomplete::Inner::scrollToRequested() const
-> rpl::producer<ScrollTo> { -> rpl::producer<ScrollTo> {
return _scrollToRequested.events(); return _scrollToRequested.events();
} }
void InitFieldAutocomplete(
std::unique_ptr<FieldAutocomplete> &autocomplete,
FieldAutocompleteDescriptor &&descriptor) {
Expects(!autocomplete);
autocomplete = std::make_unique<FieldAutocomplete>(
descriptor.parent,
descriptor.show,
descriptor.stOverride);
const auto raw = autocomplete.get();
const auto field = descriptor.field;
field->rawTextEdit()->installEventFilter(raw);
field->customTab(true);
raw->mentionChosen(
) | rpl::start_with_next([=](FieldAutocomplete::MentionChosen data) {
const auto user = data.user;
if (data.mention.isEmpty()) {
field->insertTag(
user->firstName.isEmpty() ? user->name() : user->firstName,
PrepareMentionTag(user));
} else {
field->insertTag('@' + data.mention);
}
}, raw->lifetime());
const auto sendCommand = descriptor.sendBotCommand;
const auto setText = descriptor.setText;
raw->hashtagChosen(
) | rpl::start_with_next([=](FieldAutocomplete::HashtagChosen data) {
field->insertTag(data.hashtag);
}, raw->lifetime());
const auto peer = descriptor.peer;
const auto features = descriptor.features;
const auto processShortcut = descriptor.processShortcut;
const auto shortcutMessages = (processShortcut != nullptr)
? &peer->owner().shortcutMessages()
: nullptr;
raw->botCommandChosen(
) | rpl::start_with_next([=](FieldAutocomplete::BotCommandChosen data) {
if (!features().autocompleteCommands) {
return;
}
using Method = FieldAutocompleteChooseMethod;
const auto byTab = (data.method == Method::ByTab);
const auto shortcut = data.user->isSelf();
// Send bot command at once, if it was not inserted by pressing Tab.
if (byTab && data.command.size() > 1) {
field->insertTag(data.command);
} else if (!shortcut) {
sendCommand(data.command);
setText(
field->getTextWithTagsPart(field->textCursor().position()));
} else if (processShortcut) {
processShortcut(data.command.mid(1));
}
}, raw->lifetime());
raw->setModerateKeyActivateCallback(std::move(descriptor.moderateKeyActivateCallback));
if (const auto stickerChoosing = descriptor.stickerChoosing) {
raw->choosingProcesses(
) | rpl::start_with_next([=](FieldAutocomplete::Type type) {
if (type == FieldAutocomplete::Type::Stickers) {
stickerChoosing();
}
}, raw->lifetime());
}
if (const auto chosen = descriptor.stickerChosen) {
raw->stickerChosen(
) | rpl::start_with_next(chosen, raw->lifetime());
}
field->tabbed(
) | rpl::start_with_next([=] {
if (!raw->isHidden()) {
raw->chooseSelected(FieldAutocomplete::ChooseMethod::ByTab);
}
}, raw->lifetime());
const auto check = [=] {
auto parsed = ParseMentionHashtagBotCommandQuery(field, features());
if (parsed.query.isEmpty()) {
} else if (parsed.query[0] == '#'
&& cRecentWriteHashtags().isEmpty()
&& cRecentSearchHashtags().isEmpty()) {
peer->session().local().readRecentHashtagsAndBots();
} else if (parsed.query[0] == '@'
&& cRecentInlineBots().isEmpty()) {
peer->session().local().readRecentHashtagsAndBots();
} else if (parsed.query[0] == '/'
&& peer->isUser()
&& !peer->asUser()->isBot()
&& (!shortcutMessages
|| shortcutMessages->shortcuts().list.empty())) {
parsed = {};
}
raw->showFiltered(peer, parsed.query, parsed.fromStart);
};
const auto updateStickersByEmoji = [=] {
const auto errorForStickers = Data::RestrictionError(
peer,
ChatRestriction::SendStickers);
if (features().suggestStickersByEmoji && !errorForStickers) {
const auto &text = field->getTextWithTags().text;
auto length = 0;
if (const auto emoji = Ui::Emoji::Find(text, &length)) {
if (text.size() <= length) {
raw->showStickers(emoji);
return;
}
}
}
raw->showStickers(nullptr);
};
raw->refreshRequests(
) | rpl::start_with_next(check, raw->lifetime());
raw->stickersUpdateRequests(
) | rpl::start_with_next(updateStickersByEmoji, raw->lifetime());
peer->owner().botCommandsChanges(
) | rpl::filter([=](not_null<PeerData*> changed) {
return (peer == changed);
}) | rpl::start_with_next([=] {
if (raw->clearFilteredBotCommands()) {
check();
}
}, raw->lifetime());
peer->owner().stickers().updated(
Data::StickersType::Stickers
) | rpl::start_with_next(updateStickersByEmoji, raw->lifetime());
QObject::connect(
field->rawTextEdit(),
&QTextEdit::cursorPositionChanged,
raw,
check,
Qt::QueuedConnection);
field->changes() | rpl::start_with_next(
updateStickersByEmoji,
raw->lifetime());
peer->session().changes().peerUpdates(
Data::PeerUpdate::Flag::Rights
) | rpl::filter([=](const Data::PeerUpdate &update) {
return (update.peer == peer);
}) | rpl::start_with_next(updateStickersByEmoji, raw->lifetime());
if (shortcutMessages) {
shortcutMessages->shortcutsChanged(
) | rpl::start_with_next(check, raw->lifetime());
}
raw->setSendMenuDetails(std::move(descriptor.sendMenuDetails));
raw->hideFast();
}
} // namespace ChatHelpers

View file

@ -46,46 +46,49 @@ struct Details;
} // namespace SendMenu } // namespace SendMenu
namespace ChatHelpers { namespace ChatHelpers {
struct ComposeFeatures;
struct FileChosen; struct FileChosen;
class Show; class Show;
} // namespace ChatHelpers
enum class FieldAutocompleteChooseMethod {
ByEnter,
ByTab,
ByClick,
};
class FieldAutocomplete final : public Ui::RpWidget { class FieldAutocomplete final : public Ui::RpWidget {
public: public:
FieldAutocomplete( FieldAutocomplete(
QWidget *parent, QWidget *parent,
not_null<Window::SessionController*> controller); std::shared_ptr<Show> show,
FieldAutocomplete(
QWidget *parent,
std::shared_ptr<ChatHelpers::Show> show,
const style::EmojiPan *stOverride = nullptr); const style::EmojiPan *stOverride = nullptr);
~FieldAutocomplete(); ~FieldAutocomplete();
[[nodiscard]] std::shared_ptr<ChatHelpers::Show> uiShow() const; [[nodiscard]] std::shared_ptr<Show> uiShow() const;
bool clearFilteredBotCommands(); bool clearFilteredBotCommands();
void showFiltered( void showFiltered(
not_null<PeerData*> peer, not_null<PeerData*> peer,
QString query, QString query,
bool addInlineBots); bool addInlineBots);
void showStickers(EmojiPtr emoji); void showStickers(EmojiPtr emoji);
[[nodiscard]] EmojiPtr stickersEmoji() const;
void setBoundings(QRect boundings); void setBoundings(QRect boundings);
const QString &filter() const; [[nodiscard]] const QString &filter() const;
ChatData *chat() const; [[nodiscard]] ChatData *chat() const;
ChannelData *channel() const; [[nodiscard]] ChannelData *channel() const;
UserData *user() const; [[nodiscard]] UserData *user() const;
int32 innerTop(); [[nodiscard]] int32 innerTop();
int32 innerBottom(); [[nodiscard]] int32 innerBottom();
bool eventFilter(QObject *obj, QEvent *e) override; bool eventFilter(QObject *obj, QEvent *e) override;
enum class ChooseMethod { using ChooseMethod = FieldAutocompleteChooseMethod;
ByEnter,
ByTab,
ByClick,
};
struct MentionChosen { struct MentionChosen {
not_null<UserData*> user; not_null<UserData*> user;
QString mention; QString mention;
@ -100,7 +103,7 @@ public:
QString command; QString command;
ChooseMethod method = ChooseMethod::ByEnter; ChooseMethod method = ChooseMethod::ByEnter;
}; };
using StickerChosen = ChatHelpers::FileChosen; using StickerChosen = FileChosen;
enum class Type { enum class Type {
Mentions, Mentions,
Hashtags, Hashtags,
@ -110,13 +113,14 @@ public:
bool chooseSelected(ChooseMethod method) const; bool chooseSelected(ChooseMethod method) const;
bool stickersShown() const { [[nodiscard]] bool stickersShown() const {
return !_srows.empty(); return !_srows.empty();
} }
bool overlaps(const QRect &globalRect) { [[nodiscard]] bool overlaps(const QRect &globalRect) {
if (isHidden() || !testAttribute(Qt::WA_OpaquePaintEvent)) return false; if (isHidden() || !testAttribute(Qt::WA_OpaquePaintEvent)) {
return false;
}
return rect().contains(QRect(mapFromGlobal(globalRect.topLeft()), globalRect.size())); return rect().contains(QRect(mapFromGlobal(globalRect.topLeft()), globalRect.size()));
} }
@ -129,11 +133,16 @@ public:
void showAnimated(); void showAnimated();
void hideAnimated(); void hideAnimated();
rpl::producer<MentionChosen> mentionChosen() const; void requestRefresh();
rpl::producer<HashtagChosen> hashtagChosen() const; [[nodiscard]] rpl::producer<> refreshRequests() const;
rpl::producer<BotCommandChosen> botCommandChosen() const; void requestStickersUpdate();
rpl::producer<StickerChosen> stickerChosen() const; [[nodiscard]] rpl::producer<> stickersUpdateRequests() const;
rpl::producer<Type> choosingProcesses() const;
[[nodiscard]] rpl::producer<MentionChosen> mentionChosen() const;
[[nodiscard]] rpl::producer<HashtagChosen> hashtagChosen() const;
[[nodiscard]] rpl::producer<BotCommandChosen> botCommandChosen() const;
[[nodiscard]] rpl::producer<StickerChosen> stickerChosen() const;
[[nodiscard]] rpl::producer<Type> choosingProcesses() const;
protected: protected:
void paintEvent(QPaintEvent *e) override; void paintEvent(QPaintEvent *e) override;
@ -157,7 +166,7 @@ private:
void recount(bool resetScroll = false); void recount(bool resetScroll = false);
StickerRows getStickerSuggestions(); StickerRows getStickerSuggestions();
const std::shared_ptr<ChatHelpers::Show> _show; const std::shared_ptr<Show> _show;
const not_null<Main::Session*> _session; const not_null<Main::Session*> _session;
const style::EmojiPan &_st; const style::EmojiPan &_st;
QPixmap _cache; QPixmap _cache;
@ -189,7 +198,30 @@ private:
bool _hiding = false; bool _hiding = false;
Ui::Animations::Simple _a_opacity; Ui::Animations::Simple _a_opacity;
rpl::event_stream<> _refreshRequests;
rpl::event_stream<> _stickersUpdateRequests;
Fn<bool(int)> _moderateKeyActivateCallback; Fn<bool(int)> _moderateKeyActivateCallback;
}; };
struct FieldAutocompleteDescriptor {
not_null<QWidget*> parent;
std::shared_ptr<Show> show;
not_null<Ui::InputField*> field;
const style::EmojiPan *stOverride = nullptr;
not_null<PeerData*> peer;
Fn<ComposeFeatures()> features;
Fn<SendMenu::Details()> sendMenuDetails;
Fn<void()> stickerChoosing;
Fn<void(FileChosen&&)> stickerChosen;
Fn<void(TextWithTags)> setText;
Fn<void(QString)> sendBotCommand;
Fn<void(QString)> processShortcut;
Fn<bool(int)> moderateKeyActivateCallback;
};
void InitFieldAutocomplete(
std::unique_ptr<FieldAutocomplete> &autocomplete,
FieldAutocompleteDescriptor &&descriptor);
} // namespace ChatHelpers

View file

@ -409,24 +409,30 @@ base::unique_qptr<Ui::PopupMenu> GifsListWidget::fillContextMenu(
// inline results don't have effects // inline results don't have effects
copyDetails.effectAllowed = false; copyDetails.effectAllowed = false;
} }
// In case we're adding items after FillSendMenu we have
// to pass nullptr for showForEffect and attach selector later.
// Otherwise added items widths won't be respected in menu geometry.
SendMenu::FillSendMenu( SendMenu::FillSendMenu(
menu, menu,
_show, nullptr, // showForMenu
copyDetails, copyDetails,
SendMenu::DefaultCallback(_show, send), SendMenu::DefaultCallback(_show, send),
icons); icons);
if (!isInlineResult) { if (!isInlineResult && _inlineQueryPeer) {
auto done = crl::guard(this, [=]( auto done = crl::guard(this, [=](
Api::SendOptions options, Api::SendOptions options,
TextWithTags text) { TextWithTags text) {
selectInlineResult(selected, options, true, std::move(text)); selectInlineResult(selected, options, true, std::move(text));
}); });
const auto show = _show; const auto show = _show;
const auto peer = _inlineQueryPeer;
menu->addAction(tr::lng_send_gif_with_caption(tr::now), [=] { menu->addAction(tr::lng_send_gif_with_caption(tr::now), [=] {
show->show(Box( show->show(Box(
Ui::SendGifWithCaptionBox, Ui::SendGifWithCaptionBox,
item->getDocument(), item->getDocument(),
peer,
copyDetails, copyDetails,
std::move(done))); std::move(done)));
}, &st::menuIconEdit); }, &st::menuIconEdit);
@ -446,6 +452,13 @@ base::unique_qptr<Ui::PopupMenu> GifsListWidget::fillContextMenu(
AddGifAction(std::move(callback), _show, document, icons); AddGifAction(std::move(callback), _show, document, icons);
} }
} }
SendMenu::AttachSendMenuEffect(
menu,
_show,
copyDetails,
SendMenu::DefaultCallback(_show, send));
return menu; return menu;
} }

View file

@ -63,6 +63,12 @@ constexpr auto kParseLinksTimeout = crl::time(500);
constexpr auto kTypesDuration = 4 * crl::time(1000); constexpr auto kTypesDuration = 4 * crl::time(1000);
constexpr auto kCodeLanguageLimit = 32; constexpr auto kCodeLanguageLimit = 32;
constexpr auto kLinkProtocols = {
"http://",
"https://",
"tonsite://"
};
// For mention / custom emoji tags save and validate selfId, // For mention / custom emoji tags save and validate selfId,
// ignore tags for different users. // ignore tags for different users.
[[nodiscard]] Fn<QString(QStringView)> FieldTagMimeProcessor( [[nodiscard]] Fn<QString(QStringView)> FieldTagMimeProcessor(
@ -152,11 +158,10 @@ void EditLinkBox(
return startLink.trimmed(); return startLink.trimmed();
} }
const auto clipboard = QGuiApplication::clipboard()->text().trimmed(); const auto clipboard = QGuiApplication::clipboard()->text().trimmed();
if (clipboard.startsWith("http://") const auto starts = [&](const auto &protocol) {
|| clipboard.startsWith("https://")) { return clipboard.startsWith(protocol);
return clipboard; };
} return std::ranges::any_of(kLinkProtocols, starts) ? clipboard : QString();
return QString();
}(); }();
const auto url = Ui::AttachParentChild( const auto url = Ui::AttachParentChild(
content, content,
@ -220,6 +225,9 @@ void EditLinkBox(
if (startText.isEmpty()) { if (startText.isEmpty()) {
text->setFocusFast(); text->setFocusFast();
} else { } else {
if (!url->empty()) {
url->selectAll();
}
url->setFocusFast(); url->setFocusFast();
} }
}); });
@ -227,12 +235,31 @@ void EditLinkBox(
url->customTab(true); url->customTab(true);
text->customTab(true); text->customTab(true);
const auto clearFullSelection = [=](not_null<Ui::InputField*> input) {
if (input->empty()) {
return;
}
auto cursor = input->rawTextEdit()->textCursor();
const auto hasFull = (!cursor.selectionStart()
&& (cursor.selectionEnd()
== (input->rawTextEdit()->document()->characterCount() - 1)));
if (hasFull) {
cursor.clearSelection();
input->setTextCursor(cursor);
}
};
url->tabbed( url->tabbed(
) | rpl::start_with_next([=] { ) | rpl::start_with_next([=] {
clearFullSelection(url);
text->setFocus(); text->setFocus();
}, url->lifetime()); }, url->lifetime());
text->tabbed( text->tabbed(
) | rpl::start_with_next([=] { ) | rpl::start_with_next([=] {
if (!url->empty()) {
url->selectAll();
}
clearFullSelection(text);
url->setFocus(); url->setFocus();
}, text->lifetime()); }, text->lifetime());
} }
@ -396,18 +423,14 @@ Fn<void(QString now, Fn<void(QString)> save)> DefaultEditLanguageCallback(
}; };
} }
void InitMessageFieldHandlers( void InitMessageFieldHandlers(MessageFieldHandlersArgs &&args) {
not_null<Main::Session*> session, const auto paused = [passed = args.customEmojiPaused] {
std::shared_ptr<Main::SessionShow> show, return passed && passed();
not_null<Ui::InputField*> field,
Fn<bool()> customEmojiPaused,
Fn<bool(not_null<DocumentData*>)> allowPremiumEmoji,
const style::InputField *fieldStyle) {
const auto paused = [customEmojiPaused] {
return customEmojiPaused && customEmojiPaused();
}; };
const auto field = args.field;
const auto session = args.session;
field->setTagMimeProcessor( field->setTagMimeProcessor(
FieldTagMimeProcessor(session, allowPremiumEmoji)); FieldTagMimeProcessor(session, args.allowPremiumEmoji));
field->setCustomTextContext([=](Fn<void()> repaint) { field->setCustomTextContext([=](Fn<void()> repaint) {
return std::any(Core::MarkedTextContext{ return std::any(Core::MarkedTextContext{
.session = session, .session = session,
@ -421,12 +444,14 @@ void InitMessageFieldHandlers(
field->setInstantReplaces(Ui::InstantReplaces::Default()); field->setInstantReplaces(Ui::InstantReplaces::Default());
field->setInstantReplacesEnabled( field->setInstantReplacesEnabled(
Core::App().settings().replaceEmojiValue()); Core::App().settings().replaceEmojiValue());
field->setMarkdownReplacesEnabled(true); field->setMarkdownReplacesEnabled(rpl::single(Ui::MarkdownEnabledState{
if (show) { Ui::MarkdownEnabled{ std::move(args.allowMarkdownTags) }
}));
if (const auto &show = args.show) {
field->setEditLinkCallback( field->setEditLinkCallback(
DefaultEditLinkCallback(show, field, fieldStyle)); DefaultEditLinkCallback(show, field, args.fieldStyle));
field->setEditLanguageCallback(DefaultEditLanguageCallback(show)); field->setEditLanguageCallback(DefaultEditLanguageCallback(show));
InitSpellchecker(show, field, fieldStyle != nullptr); InitSpellchecker(show, field, args.fieldStyle != nullptr);
} }
const auto style = field->lifetime().make_state<Ui::ChatStyle>( const auto style = field->lifetime().make_state<Ui::ChatStyle>(
session->colorIndicesValue()); session->colorIndicesValue());
@ -526,12 +551,15 @@ void InitMessageFieldHandlers(
not_null<Ui::InputField*> field, not_null<Ui::InputField*> field,
ChatHelpers::PauseReason pauseReasonLevel, ChatHelpers::PauseReason pauseReasonLevel,
Fn<bool(not_null<DocumentData*>)> allowPremiumEmoji) { Fn<bool(not_null<DocumentData*>)> allowPremiumEmoji) {
InitMessageFieldHandlers( InitMessageFieldHandlers({
&controller->session(), .session = &controller->session(),
controller->uiShow(), .show = controller->uiShow(),
field, .field = field,
[=] { return controller->isGifPausedAtLeastFor(pauseReasonLevel); }, .customEmojiPaused = [=] {
allowPremiumEmoji); return controller->isGifPausedAtLeastFor(pauseReasonLevel);
},
.allowPremiumEmoji = std::move(allowPremiumEmoji),
});
} }
void InitMessageFieldGeometry(not_null<Ui::InputField*> field) { void InitMessageFieldGeometry(not_null<Ui::InputField*> field) {
@ -547,14 +575,16 @@ void InitMessageField(
std::shared_ptr<ChatHelpers::Show> show, std::shared_ptr<ChatHelpers::Show> show,
not_null<Ui::InputField*> field, not_null<Ui::InputField*> field,
Fn<bool(not_null<DocumentData*>)> allowPremiumEmoji) { Fn<bool(not_null<DocumentData*>)> allowPremiumEmoji) {
InitMessageFieldHandlers( InitMessageFieldHandlers({
&show->session(), .session = &show->session(),
show, .show = show,
field, .field = field,
[=] { return show->paused(ChatHelpers::PauseReason::Any); }, .customEmojiPaused = [=] {
std::move(allowPremiumEmoji)); return show->paused(ChatHelpers::PauseReason::Any);
},
.allowPremiumEmoji = std::move(allowPremiumEmoji),
});
InitMessageFieldGeometry(field); InitMessageFieldGeometry(field);
field->customTab(true);
} }
void InitMessageField( void InitMessageField(

View file

@ -54,13 +54,18 @@ Fn<bool(
const style::InputField *fieldStyle = nullptr); const style::InputField *fieldStyle = nullptr);
Fn<void(QString now, Fn<void(QString)> save)> DefaultEditLanguageCallback( Fn<void(QString now, Fn<void(QString)> save)> DefaultEditLanguageCallback(
std::shared_ptr<Ui::Show> show); std::shared_ptr<Ui::Show> show);
void InitMessageFieldHandlers(
not_null<Main::Session*> session, struct MessageFieldHandlersArgs {
std::shared_ptr<Main::SessionShow> show, // may be null not_null<Main::Session*> session;
not_null<Ui::InputField*> field, std::shared_ptr<Main::SessionShow> show; // may be null
Fn<bool()> customEmojiPaused, not_null<Ui::InputField*> field;
Fn<bool(not_null<DocumentData*>)> allowPremiumEmoji = nullptr, Fn<bool()> customEmojiPaused;
const style::InputField *fieldStyle = nullptr); Fn<bool(not_null<DocumentData*>)> allowPremiumEmoji;
const style::InputField *fieldStyle = nullptr;
base::flat_set<QString> allowMarkdownTags;
};
void InitMessageFieldHandlers(MessageFieldHandlersArgs &&args);
void InitMessageFieldHandlers( void InitMessageFieldHandlers(
not_null<Window::SessionController*> controller, not_null<Window::SessionController*> controller,
not_null<Ui::InputField*> field, not_null<Ui::InputField*> field,

View file

@ -22,6 +22,10 @@ GiftBoxPack::GiftBoxPack(not_null<Main::Session*> session)
GiftBoxPack::~GiftBoxPack() = default; GiftBoxPack::~GiftBoxPack() = default;
rpl::producer<> GiftBoxPack::updated() const {
return _updated.events();
}
int GiftBoxPack::monthsForStars(int stars) const { int GiftBoxPack::monthsForStars(int stars) const {
if (stars <= 1000) { if (stars <= 1000) {
return 3; return 3;
@ -112,6 +116,7 @@ void GiftBoxPack::applySet(const MTPDmessages_stickerSet &data) {
} }
}); });
} }
_updated.fire({});
} }
} // namespace Stickers } // namespace Stickers

View file

@ -28,6 +28,7 @@ public:
[[nodiscard]] int monthsForStars(int stars) const; [[nodiscard]] int monthsForStars(int stars) const;
[[nodiscard]] DocumentData *lookup(int months) const; [[nodiscard]] DocumentData *lookup(int months) const;
[[nodiscard]] Data::FileOrigin origin() const; [[nodiscard]] Data::FileOrigin origin() const;
[[nodiscard]] rpl::producer<> updated() const;
private: private:
using SetId = uint64; using SetId = uint64;
@ -36,6 +37,7 @@ private:
const not_null<Main::Session*> _session; const not_null<Main::Session*> _session;
const std::vector<int> _localMonths; const std::vector<int> _localMonths;
rpl::event_stream<> _updated;
std::vector<DocumentData*> _documents; std::vector<DocumentData*> _documents;
SetId _setId = 0; SetId _setId = 0;
uint64 _accessHash = 0; uint64 _accessHash = 0;

View file

@ -1800,9 +1800,13 @@ base::unique_qptr<Ui::PopupMenu> StickersListWidget::fillContextMenu(
}); });
}); });
const auto icons = &st().icons; const auto icons = &st().icons;
// In case we're adding items after FillSendMenu we have
// to pass nullptr for showForEffect and attach selector later.
// Otherwise added items widths won't be respected in menu geometry.
SendMenu::FillSendMenu( SendMenu::FillSendMenu(
menu, menu,
_show, nullptr, // showForEffect
details, details,
SendMenu::DefaultCallback(_show, send), SendMenu::DefaultCallback(_show, send),
icons); icons);
@ -1836,6 +1840,13 @@ base::unique_qptr<Ui::PopupMenu> StickersListWidget::fillContextMenu(
false); false);
}, &icons->menuRecentRemove); }, &icons->menuRecentRemove);
} }
SendMenu::AttachSendMenuEffect(
menu,
_show,
details,
SendMenu::DefaultCallback(_show, send));
return menu; return menu;
} }

View file

@ -1170,7 +1170,14 @@ bool Application::openCustomUrl(
|| passcodeLocked()) { || passcodeLocked()) {
return false; return false;
} }
const auto command = base::StringViewMid(urlTrimmed, protocol.size(), 8192); static const auto kTagExp = QRegularExpression(
u"^\\~[a-zA-Z0-9_\\-]+\\~:"_q);
auto skip = protocol.size();
const auto match = kTagExp.match(base::StringViewMid(urlTrimmed, skip));
if (match.hasMatch()) {
skip += match.capturedLength();
}
const auto command = base::StringViewMid(urlTrimmed, skip, 8192);
const auto my = context.value<ClickHandlerContext>(); const auto my = context.value<ClickHandlerContext>();
const auto controller = my.sessionWindow.get() const auto controller = my.sessionWindow.get()
? my.sessionWindow.get() ? my.sessionWindow.get()

View file

@ -367,17 +367,6 @@ void MonospaceClickHandler::onClick(ClickContext context) const {
} }
const auto my = context.other.value<ClickHandlerContext>(); const auto my = context.other.value<ClickHandlerContext>();
if (const auto controller = my.sessionWindow.get()) { if (const auto controller = my.sessionWindow.get()) {
auto &data = controller->session().data();
const auto item = data.message(my.itemId);
const auto hasCopyRestriction = item
&& (!item->history()->peer->allowsForwarding()
|| item->forbidsForward());
if (hasCopyRestriction) {
controller->showToast(item->history()->peer->isBroadcast()
? tr::lng_error_nocopy_channel(tr::now)
: tr::lng_error_nocopy_group(tr::now));
return;
}
controller->showToast(tr::lng_text_copied(tr::now)); controller->showToast(tr::lng_text_copied(tr::now));
} }
TextUtilities::SetClipboardText(TextForMimeData::Simple(_text.trimmed())); TextUtilities::SetClipboardText(TextForMimeData::Simple(_text.trimmed()));

View file

@ -225,7 +225,8 @@ QByteArray Settings::serialize() const {
+ Serialize::stringSize(noWarningExtensions) + Serialize::stringSize(noWarningExtensions)
+ Serialize::stringSize(_customFontFamily) + Serialize::stringSize(_customFontFamily)
+ sizeof(qint32) * 3 + sizeof(qint32) * 3
+ Serialize::bytearraySize(_tonsiteStorageToken); + Serialize::bytearraySize(_tonsiteStorageToken)
+ sizeof(qint32) * 2;
auto result = QByteArray(); auto result = QByteArray();
result.reserve(size); result.reserve(size);
@ -379,7 +380,9 @@ QByteArray Settings::serialize() const {
1000000)) 1000000))
<< qint32(_systemUnlockEnabled ? 1 : 0) << qint32(_systemUnlockEnabled ? 1 : 0)
<< qint32(!_weatherInCelsius ? 0 : *_weatherInCelsius ? 1 : 2) << qint32(!_weatherInCelsius ? 0 : *_weatherInCelsius ? 1 : 2)
<< _tonsiteStorageToken; << _tonsiteStorageToken
<< qint32(_includeMutedCounterFolders ? 1 : 0)
<< qint32(_ivZoom.current());
} }
Ensures(result.size() == size); Ensures(result.size() == size);
@ -429,6 +432,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
qint32 sendFilesWay = _sendFilesWay.serialize(); qint32 sendFilesWay = _sendFilesWay.serialize();
qint32 sendSubmitWay = static_cast<qint32>(_sendSubmitWay.current()); qint32 sendSubmitWay = static_cast<qint32>(_sendSubmitWay.current());
qint32 includeMutedCounter = _includeMutedCounter ? 1 : 0; qint32 includeMutedCounter = _includeMutedCounter ? 1 : 0;
qint32 includeMutedCounterFolders = _includeMutedCounterFolders ? 1 : 0;
qint32 countUnreadMessages = _countUnreadMessages ? 1 : 0; qint32 countUnreadMessages = _countUnreadMessages ? 1 : 0;
std::optional<QString> noWarningExtensions; std::optional<QString> noWarningExtensions;
qint32 legacyExeLaunchWarning = 1; qint32 legacyExeLaunchWarning = 1;
@ -504,6 +508,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
qint32 systemUnlockEnabled = _systemUnlockEnabled ? 1 : 0; qint32 systemUnlockEnabled = _systemUnlockEnabled ? 1 : 0;
qint32 weatherInCelsius = !_weatherInCelsius ? 0 : *_weatherInCelsius ? 1 : 2; qint32 weatherInCelsius = !_weatherInCelsius ? 0 : *_weatherInCelsius ? 1 : 2;
QByteArray tonsiteStorageToken = _tonsiteStorageToken; QByteArray tonsiteStorageToken = _tonsiteStorageToken;
qint32 ivZoom = _ivZoom.current();
stream >> themesAccentColors; stream >> themesAccentColors;
if (!stream.atEnd()) { if (!stream.atEnd()) {
@ -810,6 +815,12 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
if (!stream.atEnd()) { if (!stream.atEnd()) {
stream >> tonsiteStorageToken; stream >> tonsiteStorageToken;
} }
if (!stream.atEnd()) {
stream >> includeMutedCounterFolders;
}
if (!stream.atEnd()) {
stream >> ivZoom;
}
if (stream.status() != QDataStream::Ok) { if (stream.status() != QDataStream::Ok) {
LOG(("App Error: " LOG(("App Error: "
"Bad data for Core::Settings::constructFromSerialized()")); "Bad data for Core::Settings::constructFromSerialized()"));
@ -851,6 +862,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
case ScreenCorner::TopCenter: _notificationsCorner = uncheckedNotificationsCorner; break; case ScreenCorner::TopCenter: _notificationsCorner = uncheckedNotificationsCorner; break;
} }
_includeMutedCounter = (includeMutedCounter == 1); _includeMutedCounter = (includeMutedCounter == 1);
_includeMutedCounterFolders = (includeMutedCounterFolders == 1);
_countUnreadMessages = (countUnreadMessages == 1); _countUnreadMessages = (countUnreadMessages == 1);
_notifyAboutPinned = (notifyAboutPinned == 1); _notifyAboutPinned = (notifyAboutPinned == 1);
_autoLock = autoLock; _autoLock = autoLock;
@ -871,8 +883,6 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
case Ui::InputSubmitSettings::Enter: case Ui::InputSubmitSettings::Enter:
case Ui::InputSubmitSettings::CtrlEnter: _sendSubmitWay = uncheckedSendSubmitWay; break; case Ui::InputSubmitSettings::CtrlEnter: _sendSubmitWay = uncheckedSendSubmitWay; break;
} }
_includeMutedCounter = (includeMutedCounter == 1);
_countUnreadMessages = (countUnreadMessages == 1);
if (noWarningExtensions) { if (noWarningExtensions) {
const auto list = noWarningExtensions->mid(0, 10240) const auto list = noWarningExtensions->mid(0, 10240)
.split(' ', Qt::SkipEmptyParts) .split(' ', Qt::SkipEmptyParts)
@ -1023,6 +1033,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
? std::optional<bool>() ? std::optional<bool>()
: (weatherInCelsius == 1); : (weatherInCelsius == 1);
_tonsiteStorageToken = tonsiteStorageToken; _tonsiteStorageToken = tonsiteStorageToken;
_ivZoom = ivZoom;
} }
QString Settings::getSoundPath(const QString &key) const { QString Settings::getSoundPath(const QString &key) const {
@ -1352,6 +1363,7 @@ void Settings::resetOnLastLogout() {
//_notificationsCount = 3; //_notificationsCount = 3;
//_notificationsCorner = ScreenCorner::BottomRight; //_notificationsCorner = ScreenCorner::BottomRight;
_includeMutedCounter = true; _includeMutedCounter = true;
_includeMutedCounterFolders = true;
_countUnreadMessages = true; _countUnreadMessages = true;
_notifyAboutPinned = true; _notifyAboutPinned = true;
//_autoLock = 3600; //_autoLock = 3600;
@ -1409,6 +1421,7 @@ void Settings::resetOnLastLogout() {
_hiddenGroupCallTooltips = 0; _hiddenGroupCallTooltips = 0;
_storiesClickTooltipHidden = false; _storiesClickTooltipHidden = false;
_ttlVoiceClickTooltipHidden = false; _ttlVoiceClickTooltipHidden = false;
_ivZoom = 100;
_recentEmojiPreload.clear(); _recentEmojiPreload.clear();
_recentEmoji.clear(); _recentEmoji.clear();
@ -1548,4 +1561,16 @@ bool Settings::rememberedDeleteMessageOnlyForYou() const {
return _rememberedDeleteMessageOnlyForYou; return _rememberedDeleteMessageOnlyForYou;
} }
int Settings::ivZoom() const {
return _ivZoom.current();
}
rpl::producer<int> Settings::ivZoomValue() const {
return _ivZoom.value();
}
void Settings::setIvZoom(int value) {
constexpr auto kMin = 30;
constexpr auto kMax = 200;
_ivZoom = std::clamp(value, kMin, kMax);
}
} // namespace Core } // namespace Core

View file

@ -244,6 +244,12 @@ public:
void setIncludeMutedCounter(bool value) { void setIncludeMutedCounter(bool value) {
_includeMutedCounter = value; _includeMutedCounter = value;
} }
[[nodiscard]] bool includeMutedCounterFolders() const {
return _includeMutedCounterFolders;
}
void setIncludeMutedCounterFolders(bool value) {
_includeMutedCounterFolders = value;
}
[[nodiscard]] bool countUnreadMessages() const { [[nodiscard]] bool countUnreadMessages() const {
return _countUnreadMessages; return _countUnreadMessages;
} }
@ -915,6 +921,10 @@ public:
_tonsiteStorageToken = value; _tonsiteStorageToken = value;
} }
[[nodiscard]] int ivZoom() const;
[[nodiscard]] rpl::producer<int> ivZoomValue() const;
void setIvZoom(int value);
[[nodiscard]] static bool ThirdColumnByDefault(); [[nodiscard]] static bool ThirdColumnByDefault();
[[nodiscard]] static float64 DefaultDialogsWidthRatio(); [[nodiscard]] static float64 DefaultDialogsWidthRatio();
@ -957,6 +967,7 @@ private:
int _notificationsCount = 3; int _notificationsCount = 3;
ScreenCorner _notificationsCorner = ScreenCorner::BottomRight; ScreenCorner _notificationsCorner = ScreenCorner::BottomRight;
bool _includeMutedCounter = true; bool _includeMutedCounter = true;
bool _includeMutedCounterFolders = true;
bool _countUnreadMessages = true; bool _countUnreadMessages = true;
rpl::variable<bool> _notifyAboutPinned = true; rpl::variable<bool> _notifyAboutPinned = true;
int _autoLock = 3600; int _autoLock = 3600;
@ -1049,6 +1060,7 @@ private:
bool _systemUnlockEnabled = false; bool _systemUnlockEnabled = false;
std::optional<bool> _weatherInCelsius; std::optional<bool> _weatherInCelsius;
QByteArray _tonsiteStorageToken; QByteArray _tonsiteStorageToken;
rpl::variable<int> _ivZoom = 100;
bool _tabbedReplacedWithInfo = false; // per-window bool _tabbedReplacedWithInfo = false; // per-window
rpl::event_stream<bool> _tabbedReplacedWithInfoValue; // per-window rpl::event_stream<bool> _tabbedReplacedWithInfoValue; // per-window

View file

@ -27,10 +27,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "payments/payments_non_panel_process.h" #include "payments/payments_non_panel_process.h"
#include "boxes/share_box.h" #include "boxes/share_box.h"
#include "boxes/connection_box.h" #include "boxes/connection_box.h"
#include "boxes/gift_premium_box.h"
#include "boxes/edit_privacy_box.h" #include "boxes/edit_privacy_box.h"
#include "boxes/premium_preview_box.h" #include "boxes/premium_preview_box.h"
#include "boxes/sticker_set_box.h" #include "boxes/sticker_set_box.h"
#include "boxes/sessions_box.h" #include "boxes/sessions_box.h"
#include "boxes/star_gift_box.h"
#include "boxes/language_box.h" #include "boxes/language_box.h"
#include "passport/passport_form_controller.h" #include "passport/passport_form_controller.h"
#include "ui/text/text_utilities.h" #include "ui/text/text_utilities.h"
@ -787,9 +789,9 @@ bool CopyPeerId(
Window::SessionController *controller, Window::SessionController *controller,
const Match &match, const Match &match,
const QVariant &context) { const QVariant &context) {
TextUtilities::SetClipboardText(TextForMimeData{ match->captured(1) }); TextUtilities::SetClipboardText({ match->captured(1) });
if (controller) { if (controller) {
controller->showToast(tr::lng_text_copied(tr::now)); controller->showToast(u"ID copied to clipboard."_q);
} }
return true; return true;
} }
@ -928,6 +930,34 @@ bool ShowCollectibleUsername(
return true; return true;
} }
bool CopyUsernameLink(
Window::SessionController *controller,
const Match &match,
const QVariant &context) {
if (!controller) {
return false;
}
const auto username = match->captured(1);
TextUtilities::SetClipboardText({
controller->session().createInternalLinkFull(username)
});
controller->showToast(tr::lng_username_copied(tr::now));
return true;
}
bool CopyUsername(
Window::SessionController *controller,
const Match &match,
const QVariant &context) {
if (!controller) {
return false;
}
const auto username = match->captured(1);
TextUtilities::SetClipboardText({ '@' + username });
controller->showToast(tr::lng_username_text_copied(tr::now));
return true;
}
bool ShowStarsExamples( bool ShowStarsExamples(
Window::SessionController *controller, Window::SessionController *controller,
const Match &match, const Match &match,
@ -1135,10 +1165,7 @@ bool ResolvePremiumMultigift(
if (!controller) { if (!controller) {
return false; return false;
} }
const auto params = url_parse_params( Ui::ChooseStarGiftRecipient(controller);
match->captured(1).mid(1),
qthelp::UrlParamNameTransform::ToLower);
controller->showGiftPremiumsBox(params.value(u"ref"_q, u"gift_url"_q));
controller->window().activate(); controller->window().activate();
return true; return true;
} }
@ -1404,6 +1431,14 @@ const std::vector<LocalUrlHandler> &InternalUrlHandlers() {
u"^collectible_username/([a-zA-Z0-9\\-\\_\\.]+)@([0-9]+)$"_q, u"^collectible_username/([a-zA-Z0-9\\-\\_\\.]+)@([0-9]+)$"_q,
ShowCollectibleUsername, ShowCollectibleUsername,
}, },
{
u"^username_link/([a-zA-Z0-9\\-\\_\\.]+)@([0-9]+)$"_q,
CopyUsernameLink,
},
{
u"^username_regular/([a-zA-Z0-9\\-\\_\\.]+)@([0-9]+)$"_q,
CopyUsername,
},
{ {
u"^stars_examples$"_q, u"^stars_examples$"_q,
ShowStarsExamples, ShowStarsExamples,

View file

@ -13,7 +13,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "core/application.h" #include "core/application.h"
#include "core/sandbox.h" #include "core/sandbox.h"
#include "core/click_handler_types.h" #include "core/click_handler_types.h"
#include "data/components/sponsored_messages.h"
#include "data/stickers/data_custom_emoji.h" #include "data/stickers/data_custom_emoji.h"
#include "data/data_session.h" #include "data/data_session.h"
#include "iv/iv_instance.h" #include "iv/iv_instance.h"
@ -304,16 +303,6 @@ Fn<void()> UiIntegration::createSpoilerRepaint(const std::any &context) {
return my ? my->customEmojiRepaint : nullptr; return my ? my->customEmojiRepaint : nullptr;
} }
bool UiIntegration::allowClickHandlerActivation(
const std::shared_ptr<ClickHandler> &handler,
const ClickContext &context) {
const auto my = context.other.value<ClickHandlerContext>();
if (const auto window = my.sessionWindow.get()) {
window->session().sponsoredMessages().clicked(my.itemId);
}
return true;
}
rpl::producer<> UiIntegration::forcePopupMenuHideRequests() { rpl::producer<> UiIntegration::forcePopupMenuHideRequests() {
return Core::App().passcodeLockChanges() | rpl::to_empty; return Core::App().passcodeLockChanges() | rpl::to_empty;
} }

View file

@ -61,9 +61,6 @@ public:
QStringView data, QStringView data,
const std::any &context) override; const std::any &context) override;
Fn<void()> createSpoilerRepaint(const std::any &context) override; Fn<void()> createSpoilerRepaint(const std::any &context) override;
bool allowClickHandlerActivation(
const std::shared_ptr<ClickHandler> &handler,
const ClickContext &context) override;
QString phraseContextCopyText() override; QString phraseContextCopyText() override;
QString phraseContextCopyEmail() override; QString phraseContextCopyEmail() override;

View file

@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D666}"_cs;
constexpr auto AppNameOld = "AyuGram for Windows"_cs; constexpr auto AppNameOld = "AyuGram for Windows"_cs;
constexpr auto AppName = "AyuGram Desktop"_cs; constexpr auto AppName = "AyuGram Desktop"_cs;
constexpr auto AppFile = "AyuGram"_cs; constexpr auto AppFile = "AyuGram"_cs;
constexpr auto AppVersion = 5005005; constexpr auto AppVersion = 5006003;
constexpr auto AppVersionStr = "5.5.5"; constexpr auto AppVersionStr = "5.6.3";
constexpr auto AppBetaVersion = false; constexpr auto AppBetaVersion = false;
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION; constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;

View file

@ -69,6 +69,11 @@ uint64 Credits::balance() const {
return _nonLockedBalance.current(); return _nonLockedBalance.current();
} }
uint64 Credits::balance(PeerId peerId) const {
const auto it = _cachedPeerBalances.find(peerId);
return (it != _cachedPeerBalances.end()) ? it->second : 0;
}
rpl::producer<uint64> Credits::balanceValue() const { rpl::producer<uint64> Credits::balanceValue() const {
return _nonLockedBalance.value(); return _nonLockedBalance.value();
} }
@ -119,4 +124,8 @@ void Credits::apply(uint64 balance) {
} }
} }
void Credits::apply(PeerId peerId, uint64 balance) {
_cachedPeerBalances[peerId] = balance;
}
} // namespace Data } // namespace Data

View file

@ -24,11 +24,13 @@ public:
void load(bool force = false); void load(bool force = false);
void apply(uint64 balance); void apply(uint64 balance);
void apply(PeerId peerId, uint64 balance);
[[nodiscard]] bool loaded() const; [[nodiscard]] bool loaded() const;
[[nodiscard]] rpl::producer<bool> loadedValue() const; [[nodiscard]] rpl::producer<bool> loadedValue() const;
[[nodiscard]] uint64 balance() const; [[nodiscard]] uint64 balance() const;
[[nodiscard]] uint64 balance(PeerId peerId) const;
[[nodiscard]] rpl::producer<uint64> balanceValue() const; [[nodiscard]] rpl::producer<uint64> balanceValue() const;
[[nodiscard]] rpl::producer<float64> rateValue( [[nodiscard]] rpl::producer<float64> rateValue(
not_null<PeerData*> ownedBotOrChannel); not_null<PeerData*> ownedBotOrChannel);
@ -47,6 +49,8 @@ private:
std::unique_ptr<Api::CreditsStatus> _loader; std::unique_ptr<Api::CreditsStatus> _loader;
base::flat_map<PeerId, uint64> _cachedPeerBalances;
uint64 _balance = 0; uint64 _balance = 0;
uint64 _locked = 0; uint64 _locked = 0;
rpl::variable<uint64> _nonLockedBalance; rpl::variable<uint64> _nonLockedBalance;

View file

@ -95,6 +95,7 @@ QByteArray RecentPeers::serialize() const {
void RecentPeers::applyLocal(QByteArray serialized) { void RecentPeers::applyLocal(QByteArray serialized) {
_list.clear(); _list.clear();
if (serialized.isEmpty()) { if (serialized.isEmpty()) {
DEBUG_LOG(("Suggestions: Bad RecentPeers local, empty."));
return; return;
} }
auto stream = Serialize::ByteArrayReader(serialized); auto stream = Serialize::ByteArrayReader(serialized);
@ -102,8 +103,13 @@ void RecentPeers::applyLocal(QByteArray serialized) {
auto count = quint32(); auto count = quint32();
stream >> streamAppVersion >> count; stream >> streamAppVersion >> count;
if (!stream.ok()) { if (!stream.ok()) {
DEBUG_LOG(("Suggestions: Bad RecentPeers local, not ok."));
return; return;
} }
DEBUG_LOG(("Suggestions: "
"Start RecentPeers read, count: %1, version: %2."
).arg(count
).arg(streamAppVersion));
_list.reserve(count); _list.reserve(count);
for (auto i = 0; i != int(count); ++i) { for (auto i = 0; i != int(count); ++i) {
const auto peer = Serialize::readPeer( const auto peer = Serialize::readPeer(
@ -114,9 +120,15 @@ void RecentPeers::applyLocal(QByteArray serialized) {
_list.push_back(peer); _list.push_back(peer);
} else { } else {
_list.clear(); _list.clear();
DEBUG_LOG(("Suggestions: Failed RecentPeers reading %1 / %2."
).arg(i + 1
).arg(count));
_list.clear();
return; return;
} }
} }
DEBUG_LOG(
("Suggestions: RecentPeers read OK, count: %1").arg(_list.size()));
} }
} // namespace Data } // namespace Data

View file

@ -12,6 +12,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "core/click_handler_types.h" #include "core/click_handler_types.h"
#include "data/data_channel.h" #include "data/data_channel.h"
#include "data/data_document.h" #include "data/data_document.h"
#include "data/data_file_origin.h"
#include "data/data_media_preload.h"
#include "data/data_photo.h" #include "data/data_photo.h"
#include "data/data_session.h" #include "data/data_session.h"
#include "data/data_user.h" #include "data/data_user.h"
@ -73,16 +75,17 @@ void SponsoredMessages::clearOldRequests() {
} }
} }
bool SponsoredMessages::append(not_null<History*> history) { SponsoredMessages::AppendResult SponsoredMessages::append(
not_null<History*> history) {
const auto it = _data.find(history); const auto it = _data.find(history);
if (it == end(_data)) { if (it == end(_data)) {
return false; return SponsoredMessages::AppendResult::None;
} }
auto &list = it->second; auto &list = it->second;
if (list.showedAll if (list.showedAll
|| !TooEarlyForRequest(list.received) || !TooEarlyForRequest(list.received)
|| list.postsBetween) { || list.postsBetween) {
return false; return SponsoredMessages::AppendResult::None;
} }
const auto entryIt = ranges::find_if(list.entries, [](const Entry &e) { const auto entryIt = ranges::find_if(list.entries, [](const Entry &e) {
@ -90,19 +93,16 @@ bool SponsoredMessages::append(not_null<History*> history) {
}); });
if (entryIt == end(list.entries)) { if (entryIt == end(list.entries)) {
list.showedAll = true; list.showedAll = true;
return false; return SponsoredMessages::AppendResult::None;
} else if (entryIt->preload) {
return SponsoredMessages::AppendResult::MediaLoading;
} }
// SponsoredMessages::Details can be requested within
// the constructor of HistoryItem, so itemFullId is used as a key.
entryIt->itemFullId = FullMsgId(
history->peer->id,
_session->data().nextLocalMessageId());
entryIt->item.reset(history->addSponsoredMessage( entryIt->item.reset(history->addSponsoredMessage(
entryIt->itemFullId.msg, entryIt->itemFullId.msg,
entryIt->sponsored.from, entryIt->sponsored.from,
entryIt->sponsored.textWithEntities)); entryIt->sponsored.textWithEntities));
return true; return SponsoredMessages::AppendResult::Appended;
} }
void SponsoredMessages::inject( void SponsoredMessages::inject(
@ -227,8 +227,7 @@ void SponsoredMessages::request(not_null<History*> history, Fn<void()> done) {
const auto channel = history->peer->asChannel(); const auto channel = history->peer->asChannel();
Assert(channel != nullptr); Assert(channel != nullptr);
request.requestId = _session->api().request( request.requestId = _session->api().request(
MTPchannels_GetSponsoredMessages( MTPchannels_GetSponsoredMessages(channel->inputChannel)
channel->inputChannel)
).done([=](const MTPmessages_sponsoredMessages &result) { ).done([=](const MTPmessages_sponsoredMessages &result) {
parse(history, result); parse(history, result);
if (done) { if (done) {
@ -276,15 +275,14 @@ void SponsoredMessages::append(
const MTPSponsoredMessage &message) { const MTPSponsoredMessage &message) {
const auto &data = message.data(); const auto &data = message.data();
const auto randomId = data.vrandom_id().v; const auto randomId = data.vrandom_id().v;
auto mediaPhotoId = PhotoId(0); auto mediaPhoto = (PhotoData*)nullptr;
auto mediaDocumentId = DocumentId(0); auto mediaDocument = (DocumentData*)nullptr;
{ {
if (data.vmedia()) { if (data.vmedia()) {
data.vmedia()->match([&](const MTPDmessageMediaPhoto &media) { data.vmedia()->match([&](const MTPDmessageMediaPhoto &media) {
if (const auto tlPhoto = media.vphoto()) { if (const auto tlPhoto = media.vphoto()) {
tlPhoto->match([&](const MTPDphoto &data) { tlPhoto->match([&](const MTPDphoto &data) {
const auto p = history->owner().processPhoto(data); mediaPhoto = history->owner().processPhoto(data);
mediaPhotoId = p->id;
}, [](const MTPDphotoEmpty &) { }, [](const MTPDphotoEmpty &) {
}); });
} }
@ -296,7 +294,7 @@ void SponsoredMessages::append(
|| d->isSilentVideo() || d->isSilentVideo()
|| d->isAnimation() || d->isAnimation()
|| d->isGifv()) { || d->isGifv()) {
mediaDocumentId = d->id; mediaDocument = d;
} }
}, [](const MTPDdocumentEmpty &) { }, [](const MTPDdocumentEmpty &) {
}); });
@ -312,8 +310,8 @@ void SponsoredMessages::append(
.photoId = data.vphoto() .photoId = data.vphoto()
? history->session().data().processPhoto(*data.vphoto())->id ? history->session().data().processPhoto(*data.vphoto())->id
: PhotoId(0), : PhotoId(0),
.mediaPhotoId = mediaPhotoId, .mediaPhotoId = (mediaPhoto ? mediaPhoto->id : 0),
.mediaDocumentId = mediaDocumentId, .mediaDocumentId = (mediaDocument ? mediaDocument->id : 0),
.backgroundEmojiId = data.vcolor().has_value() .backgroundEmojiId = data.vcolor().has_value()
? data.vcolor()->data().vbackground_emoji_id().value_or_empty() ? data.vcolor()->data().vbackground_emoji_id().value_or_empty()
: uint64(0), : uint64(0),
@ -347,7 +345,56 @@ void SponsoredMessages::append(
.sponsorInfo = std::move(sponsorInfo), .sponsorInfo = std::move(sponsorInfo),
.additionalInfo = std::move(additionalInfo), .additionalInfo = std::move(additionalInfo),
}; };
list.entries.push_back({ nullptr, {}, std::move(sharedMessage) }); list.entries.push_back({
.sponsored = std::move(sharedMessage),
});
auto &entry = list.entries.back();
const auto itemId = entry.itemFullId = FullMsgId(
history->peer->id,
_session->data().nextLocalMessageId());
const auto fileOrigin = FileOrigin(); // No way to refresh in ads.
static const auto kFlaggedPreload = ((MediaPreload*)quintptr(0x01));
const auto preloaded = [=] {
const auto i = _data.find(history);
if (i == end(_data)) {
return;
}
auto &entries = i->second.entries;
const auto j = ranges::find(entries, itemId, &Entry::itemFullId);
if (j == end(entries)) {
return;
}
auto &entry = *j;
if (entry.preload.get() == kFlaggedPreload) {
entry.preload.release();
} else {
entry.preload = nullptr;
}
};
auto preload = std::unique_ptr<MediaPreload>();
entry.preload.reset(kFlaggedPreload);
if (mediaPhoto) {
preload = std::make_unique<PhotoPreload>(
mediaPhoto,
fileOrigin,
preloaded);
} else if (mediaDocument && VideoPreload::Can(mediaDocument)) {
preload = std::make_unique<VideoPreload>(
mediaDocument,
fileOrigin,
preloaded);
}
// Preload constructor may have called preloaded(), which zero-ed
// entry.preload, that way we're ready and don't need to save it.
// Otherwise we're preloading and need to save the task.
if (entry.preload.get() == kFlaggedPreload) {
entry.preload.release();
if (preload) {
entry.preload = std::move(preload);
}
}
} }
void SponsoredMessages::clearItems(not_null<History*> history) { void SponsoredMessages::clearItems(not_null<History*> history) {
@ -439,7 +486,10 @@ SponsoredMessages::Details SponsoredMessages::lookupDetails(
}; };
} }
void SponsoredMessages::clicked(const FullMsgId &fullId) { void SponsoredMessages::clicked(
const FullMsgId &fullId,
bool isMedia,
bool isFullscreen) {
const auto entryPtr = find(fullId); const auto entryPtr = find(fullId);
if (!entryPtr) { if (!entryPtr) {
return; return;
@ -447,7 +497,11 @@ void SponsoredMessages::clicked(const FullMsgId &fullId) {
const auto randomId = entryPtr->sponsored.randomId; const auto randomId = entryPtr->sponsored.randomId;
const auto channel = entryPtr->item->history()->peer->asChannel(); const auto channel = entryPtr->item->history()->peer->asChannel();
Assert(channel != nullptr); Assert(channel != nullptr);
using Flag = MTPchannels_ClickSponsoredMessage::Flag;
_session->api().request(MTPchannels_ClickSponsoredMessage( _session->api().request(MTPchannels_ClickSponsoredMessage(
MTP_flags(Flag(0)
| (isMedia ? Flag::f_media : Flag(0))
| (isFullscreen ? Flag::f_fullscreen : Flag(0))),
channel->inputChannel, channel->inputChannel,
MTP_bytes(randomId) MTP_bytes(randomId)
)).send(); )).send();

View file

@ -20,6 +20,8 @@ class Session;
namespace Data { namespace Data {
class MediaPreload;
struct SponsoredReportResult final { struct SponsoredReportResult final {
using Id = QByteArray; using Id = QByteArray;
struct Option final { struct Option final {
@ -65,6 +67,11 @@ struct SponsoredMessage {
class SponsoredMessages final { class SponsoredMessages final {
public: public:
enum class AppendResult {
None,
Appended,
MediaLoading,
};
enum class State { enum class State {
None, None,
AppendToEnd, AppendToEnd,
@ -90,9 +97,9 @@ public:
void request(not_null<History*> history, Fn<void()> done); void request(not_null<History*> history, Fn<void()> done);
void clearItems(not_null<History*> history); void clearItems(not_null<History*> history);
[[nodiscard]] Details lookupDetails(const FullMsgId &fullId) const; [[nodiscard]] Details lookupDetails(const FullMsgId &fullId) const;
void clicked(const FullMsgId &fullId); void clicked(const FullMsgId &fullId, bool isMedia, bool isFullscreen);
[[nodiscard]] bool append(not_null<History*> history); [[nodiscard]] AppendResult append(not_null<History*> history);
void inject( void inject(
not_null<History*> history, not_null<History*> history,
MsgId injectAfterMsgId, MsgId injectAfterMsgId,
@ -114,6 +121,7 @@ private:
OwnedItem item; OwnedItem item;
FullMsgId itemFullId; FullMsgId itemFullId;
SponsoredMessage sponsored; SponsoredMessage sponsored;
std::unique_ptr<MediaPreload> preload;
}; };
struct List { struct List {
std::vector<Entry> entries; std::vector<Entry> entries;

View file

@ -274,11 +274,13 @@ QByteArray TopPeers::serialize() const {
void TopPeers::applyLocal(QByteArray serialized) { void TopPeers::applyLocal(QByteArray serialized) {
if (_lastReceived) { if (_lastReceived) {
DEBUG_LOG(("Suggestions: Skipping TopPeers local, got already."));
return; return;
} }
_list.clear(); _list.clear();
_disabled = false; _disabled = false;
if (serialized.isEmpty()) { if (serialized.isEmpty()) {
DEBUG_LOG(("Suggestions: Bad TopPeers local, empty."));
return; return;
} }
auto stream = Serialize::ByteArrayReader(serialized); auto stream = Serialize::ByteArrayReader(serialized);
@ -287,8 +289,14 @@ void TopPeers::applyLocal(QByteArray serialized) {
auto count = quint32(); auto count = quint32();
stream >> streamAppVersion >> disabled >> count; stream >> streamAppVersion >> disabled >> count;
if (!stream.ok()) { if (!stream.ok()) {
DEBUG_LOG(("Suggestions: Bad TopPeers local, not ok."));
return; return;
} }
DEBUG_LOG(("Suggestions: "
"Start TopPeers read, count: %1, version: %2, disabled: %3."
).arg(count
).arg(streamAppVersion
).arg(disabled));
_list.reserve(count); _list.reserve(count);
for (auto i = 0; i != int(count); ++i) { for (auto i = 0; i != int(count); ++i) {
auto rating = quint64(); auto rating = quint64();
@ -303,11 +311,15 @@ void TopPeers::applyLocal(QByteArray serialized) {
.rating = DeserializeRating(rating), .rating = DeserializeRating(rating),
}); });
} else { } else {
DEBUG_LOG(("Suggestions: "
"Failed TopPeers reading %1 / %2.").arg(i + 1).arg(count));
_list.clear(); _list.clear();
return; return;
} }
} }
_disabled = (disabled == 1); _disabled = (disabled == 1);
DEBUG_LOG(
("Suggestions: TopPeers read OK, count: %1").arg(_list.size()));
} }
} // namespace Data } // namespace Data

View file

@ -84,34 +84,35 @@ struct PeerUpdate {
BotCanBeInvited = (1ULL << 22), BotCanBeInvited = (1ULL << 22),
BotStartToken = (1ULL << 23), BotStartToken = (1ULL << 23),
CommonChats = (1ULL << 24), CommonChats = (1ULL << 24),
HasCalls = (1ULL << 25), PeerGifts = (1ULL << 25),
SupportInfo = (1ULL << 26), HasCalls = (1ULL << 26),
IsBot = (1ULL << 27), SupportInfo = (1ULL << 27),
EmojiStatus = (1ULL << 28), IsBot = (1ULL << 28),
BusinessDetails = (1ULL << 29), EmojiStatus = (1ULL << 29),
Birthday = (1ULL << 30), BusinessDetails = (1ULL << 30),
PersonalChannel = (1ULL << 31), Birthday = (1ULL << 31),
PersonalChannel = (1ULL << 32),
// For chats and channels // For chats and channels
InviteLinks = (1ULL << 32), InviteLinks = (1ULL << 33),
Members = (1ULL << 33), Members = (1ULL << 34),
Admins = (1ULL << 34), Admins = (1ULL << 35),
BannedUsers = (1ULL << 35), BannedUsers = (1ULL << 36),
Rights = (1ULL << 36), Rights = (1ULL << 37),
PendingRequests = (1ULL << 37), PendingRequests = (1ULL << 38),
Reactions = (1ULL << 38), Reactions = (1ULL << 39),
// For channels // For channels
ChannelAmIn = (1ULL << 39), ChannelAmIn = (1ULL << 40),
StickersSet = (1ULL << 40), StickersSet = (1ULL << 41),
EmojiSet = (1ULL << 41), EmojiSet = (1ULL << 42),
ChannelLinkedChat = (1ULL << 42), ChannelLinkedChat = (1ULL << 43),
ChannelLocation = (1ULL << 43), ChannelLocation = (1ULL << 44),
Slowmode = (1ULL << 44), Slowmode = (1ULL << 45),
GroupCall = (1ULL << 45), GroupCall = (1ULL << 46),
// For iteration // For iteration
LastUsedBit = (1ULL << 45), LastUsedBit = (1ULL << 46),
}; };
using Flags = base::flags<Flag>; using Flags = base::flags<Flag>;
friend inline constexpr auto is_flag_type(Flag) { return true; } friend inline constexpr auto is_flag_type(Flag) { return true; }

View file

@ -112,7 +112,9 @@ bool CanSendAnyOf(
ChatRestrictions rights, ChatRestrictions rights,
bool forbidInForums) { bool forbidInForums) {
if (const auto user = peer->asUser()) { if (const auto user = peer->asUser()) {
if (user->isInaccessible() || user->isRepliesChat()) { if (user->isInaccessible()
|| user->isRepliesChat()
|| user->isVerifyCodes()) {
return false; return false;
} else if (user->meRequiresPremiumToWrite() } else if (user->meRequiresPremiumToWrite()
&& !user->session().premium()) { && !user->session().premium()) {

View file

@ -50,7 +50,7 @@ struct CreditsHistoryEntry final {
QString id; QString id;
QString title; QString title;
QString description; TextWithEntities description;
QDateTime date; QDateTime date;
PhotoId photoId = 0; PhotoId photoId = 0;
std::vector<CreditsHistoryMedia> extended; std::vector<CreditsHistoryMedia> extended;
@ -58,10 +58,18 @@ struct CreditsHistoryEntry final {
uint64 bareMsgId = 0; uint64 bareMsgId = 0;
uint64 barePeerId = 0; uint64 barePeerId = 0;
uint64 bareGiveawayMsgId = 0; uint64 bareGiveawayMsgId = 0;
uint64 bareGiftStickerId = 0;
PeerType peerType; PeerType peerType;
QDateTime subscriptionUntil; QDateTime subscriptionUntil;
QDateTime successDate; QDateTime successDate;
QString successLink; QString successLink;
int limitedCount = 0;
int limitedLeft = 0;
int convertStars = 0;
bool converted = false;
bool anonymous = false;
bool savedToProfile = false;
bool fromGiftsList = false;
bool reaction = false; bool reaction = false;
bool refunded = false; bool refunded = false;
bool pending = false; bool pending = false;

View file

@ -936,14 +936,14 @@ void DocumentData::setFileName(const QString &remoteFileName) {
// in filenames, because they introduce a security issue, when // in filenames, because they introduce a security issue, when
// an executable "Fil[x]gepj.exe" may look like "Filexe.jpeg". // an executable "Fil[x]gepj.exe" may look like "Filexe.jpeg".
QChar controls[] = { QChar controls[] = {
0x200E, // LTR Mark QChar(0x200E), // LTR Mark
0x200F, // RTL Mark QChar(0x200F), // RTL Mark
0x202A, // LTR Embedding QChar(0x202A), // LTR Embedding
0x202B, // RTL Embedding QChar(0x202B), // RTL Embedding
0x202D, // LTR Override QChar(0x202D), // LTR Override
0x202E, // RTL Override QChar(0x202E), // RTL Override
0x2066, // LTR Isolate QChar(0x2066), // LTR Isolate
0x2067, // RTL Isolate QChar(0x2067), // RTL Isolate
}; };
for (const auto &ch : controls) { for (const auto &ch : controls) {
_filename = std::move(_filename).replace(ch, "_"); _filename = std::move(_filename).replace(ch, "_");

View file

@ -42,6 +42,8 @@ namespace {
base::options::toggle OptionExternalVideoPlayer({ base::options::toggle OptionExternalVideoPlayer({
.id = kOptionExternalVideoPlayer, .id = kOptionExternalVideoPlayer,
.name = "External video player", .name = "External video player",
.description = "Use system video player instead of the internal one. "
"This disabes video playback in messages.",
}); });
void ConfirmDontWarnBox( void ConfirmDontWarnBox(

View file

@ -0,0 +1,209 @@
/*
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 "data/data_media_preload.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
#include "data/data_file_origin.h"
#include "data/data_photo.h"
#include "data/data_photo_media.h"
#include "data/data_session.h"
#include "main/main_session.h"
#include "main/main_session_settings.h"
#include "media/streaming/media_streaming_reader.h"
#include "storage/file_download.h" // kMaxFileInMemory.
namespace Data {
namespace {
constexpr auto kDefaultPreloadPrefix = 4 * 1024 * 1024;
[[nodiscard]] int64 ChoosePreloadPrefix(not_null<DocumentData*> video) {
const auto result = video->videoPreloadPrefix();
return result
? result
: std::min(int64(kDefaultPreloadPrefix), video->size);
}
} // namespace
MediaPreload::MediaPreload(Fn<void()> done)
: _done(std::move(done)) {
}
void MediaPreload::callDone() {
if (const auto onstack = _done) {
onstack();
}
}
PhotoPreload::PhotoPreload(
not_null<PhotoData*> photo,
FileOrigin origin,
Fn<void()> done)
: MediaPreload(std::move(done))
, _photo(photo->createMediaView()) {
start(origin);
}
PhotoPreload::~PhotoPreload() {
if (_photo) {
base::take(_photo)->owner()->cancel();
}
}
bool PhotoPreload::Should(
not_null<PhotoData*> photo,
not_null<PeerData*> context) {
return !photo->cancelled()
&& AutoDownload::Should(
photo->session().settings().autoDownload(),
context,
photo);
}
void PhotoPreload::start(FileOrigin origin) {
if (_photo->loaded()) {
callDone();
} else {
_photo->owner()->load(origin, LoadFromCloudOrLocal, true);
_photo->owner()->session().downloaderTaskFinished(
) | rpl::filter([=] {
return _photo->loaded();
}) | rpl::start_with_next([=] { callDone(); }, _lifetime);
}
}
VideoPreload::VideoPreload(
not_null<DocumentData*> video,
FileOrigin origin,
Fn<void()> done)
: MediaPreload(std::move(done))
, DownloadMtprotoTask(
&video->session().downloader(),
video->videoPreloadLocation(),
origin)
, _video(video)
, _full(video->size) {
if (Can(video)) {
check();
} else {
callDone();
}
}
void VideoPreload::check() {
const auto key = _video->bigFileBaseCacheKey();
const auto weak = base::make_weak(static_cast<has_weak_ptr*>(this));
_video->owner().cacheBigFile().get(key, [weak](
const QByteArray &result) {
if (!result.isEmpty()) {
crl::on_main([weak] {
if (const auto strong = weak.get()) {
static_cast<VideoPreload*>(strong)->callDone();
}
});
} else {
crl::on_main([weak] {
if (const auto strong = weak.get()) {
static_cast<VideoPreload*>(strong)->load();
}
});
}
});
}
void VideoPreload::load() {
if (!Can(_video)) {
callDone();
return;
}
const auto prefix = ChoosePreloadPrefix(_video);
Assert(prefix > 0 && prefix <= _video->size);
const auto part = Storage::kDownloadPartSize;
const auto parts = (prefix + part - 1) / part;
for (auto i = 0; i != parts; ++i) {
_parts.emplace(i * part, QByteArray());
}
addToQueue();
}
void VideoPreload::done(QByteArray result) {
const auto key = _video->bigFileBaseCacheKey();
if (!result.isEmpty() && key) {
Assert(result.size() < Storage::kMaxFileInMemory);
_video->owner().cacheBigFile().putIfEmpty(
key,
Storage::Cache::Database::TaggedValue(std::move(result), 0));
}
callDone();
}
VideoPreload::~VideoPreload() {
if (!_finished && !_failed) {
cancelAllRequests();
}
}
bool VideoPreload::Can(not_null<DocumentData*> video) {
return video->canBeStreamed(nullptr)
&& video->videoPreloadLocation().valid()
&& video->bigFileBaseCacheKey();
}
bool VideoPreload::readyToRequest() const {
const auto part = Storage::kDownloadPartSize;
return !_failed && (_nextRequestOffset < _parts.size() * part);
}
int64 VideoPreload::takeNextRequestOffset() {
Expects(readyToRequest());
_requestedOffsets.emplace(_nextRequestOffset);
_nextRequestOffset += Storage::kDownloadPartSize;
return _requestedOffsets.back();
}
bool VideoPreload::feedPart(
int64 offset,
const QByteArray &bytes) {
Expects(offset < _parts.size() * Storage::kDownloadPartSize);
Expects(_requestedOffsets.contains(int(offset)));
Expects(bytes.size() <= Storage::kDownloadPartSize);
const auto part = Storage::kDownloadPartSize;
_requestedOffsets.remove(int(offset));
_parts[offset] = bytes;
if ((_nextRequestOffset + part >= _parts.size() * part)
&& _requestedOffsets.empty()) {
_finished = true;
removeFromQueue();
auto result = ::Media::Streaming::SerializeComplexPartsMap(_parts);
if (result.size() == _full) {
// Make sure it is parsed as a complex map.
result.push_back(char(0));
}
done(std::move(result));
}
return true;
}
void VideoPreload::cancelOnFail() {
_failed = true;
cancelAllRequests();
done({});
}
bool VideoPreload::setWebFileSizeHook(int64 size) {
_failed = true;
cancelAllRequests();
done({});
return false;
}
} // namespace Data

View file

@ -0,0 +1,83 @@
/*
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 "storage/download_manager_mtproto.h"
namespace Data {
class PhotoMedia;
struct FileOrigin;
class MediaPreload {
public:
explicit MediaPreload(Fn<void()> done);
virtual ~MediaPreload() = default;
protected:
void callDone();
private:
Fn<void()> _done;
};
class PhotoPreload final : public MediaPreload {
public:
[[nodiscard]] static bool Should(
not_null<PhotoData*> photo,
not_null<PeerData*> context);
PhotoPreload(
not_null<PhotoData*> data,
FileOrigin origin,
Fn<void()> done);
~PhotoPreload();
private:
void start(FileOrigin origin);
std::shared_ptr<PhotoMedia> _photo;
rpl::lifetime _lifetime;
};
class VideoPreload final
: public MediaPreload
, private Storage::DownloadMtprotoTask {
public:
[[nodiscard]] static bool Can(not_null<DocumentData*> video);
VideoPreload(
not_null<DocumentData*> video,
FileOrigin origin,
Fn<void()> done);
~VideoPreload();
private:
void check();
void load();
void done(QByteArray result);
bool readyToRequest() const override;
int64 takeNextRequestOffset() override;
bool feedPart(int64 offset, const QByteArray &bytes) override;
void cancelOnFail() override;
bool setWebFileSizeHook(int64 size) override;
const not_null<DocumentData*> _video;
base::flat_map<uint32, QByteArray> _parts;
base::flat_set<int> _requestedOffsets;
int64 _full = 0;
int _nextRequestOffset = 0;
bool _finished = false;
bool _failed = false;
};
} // namespace Data

View file

@ -579,6 +579,10 @@ const Invoice *Media::invoice() const {
return nullptr; return nullptr;
} }
const GiftCode *Media::gift() const {
return nullptr;
}
CloudImage *Media::location() const { CloudImage *Media::location() const {
return nullptr; return nullptr;
} }
@ -2331,8 +2335,8 @@ not_null<PeerData*> MediaGiftBox::from() const {
return _from; return _from;
} }
const GiftCode &MediaGiftBox::data() const { const GiftCode *MediaGiftBox::gift() const {
return _data; return &_data;
} }
TextWithEntities MediaGiftBox::notificationText() const { TextWithEntities MediaGiftBox::notificationText() const {

View file

@ -130,16 +130,25 @@ struct GiveawayResults {
enum class GiftType : uchar { enum class GiftType : uchar {
Premium, // count - months Premium, // count - months
Credits, // count - credits Credits, // count - credits
StarGift, // count - stars
}; };
struct GiftCode { struct GiftCode {
QString slug; QString slug;
DocumentData *document = nullptr;
TextWithEntities message;
ChannelData *channel = nullptr; ChannelData *channel = nullptr;
MsgId giveawayMsgId = 0;
int convertStars = 0;
int limitedCount = 0;
int limitedLeft = 0;
int count = 0; int count = 0;
int giveawayMsgId = 0;
GiftType type = GiftType::Premium; GiftType type = GiftType::Premium;
bool viaGiveaway = false; bool viaGiveaway : 1 = false;
bool unclaimed = false; bool unclaimed : 1 = false;
bool anonymous : 1 = false;
bool converted : 1 = false;
bool saved : 1 = false;
}; };
class Media { class Media {
@ -163,6 +172,7 @@ public:
virtual const Call *call() const; virtual const Call *call() const;
virtual GameData *game() const; virtual GameData *game() const;
virtual const Invoice *invoice() const; virtual const Invoice *invoice() const;
virtual const GiftCode *gift() const;
virtual CloudImage *location() const; virtual CloudImage *location() const;
virtual PollData *poll() const; virtual PollData *poll() const;
virtual const WallPaper *paper() const; virtual const WallPaper *paper() const;
@ -612,7 +622,7 @@ public:
std::unique_ptr<Media> clone(not_null<HistoryItem*> parent) override; std::unique_ptr<Media> clone(not_null<HistoryItem*> parent) override;
[[nodiscard]] not_null<PeerData*> from() const; [[nodiscard]] not_null<PeerData*> from() const;
[[nodiscard]] const GiftCode &data() const; [[nodiscard]] const GiftCode *gift() const override;
TextWithEntities notificationText() const override; TextWithEntities notificationText() const override;
QString pinnedTextSubstring() const override; QString pinnedTextSubstring() const override;

View file

@ -394,7 +394,8 @@ void PeerData::paintUserpic(
Ui::PeerUserpicView &view, Ui::PeerUserpicView &view,
int x, int x,
int y, int y,
int size) const { int size,
bool forceCircle) const {
const auto cloud = userpicCloudImage(view); const auto cloud = userpicCloudImage(view);
const auto ratio = style::DevicePixelRatio(); const auto ratio = style::DevicePixelRatio();
Ui::ValidateUserpicCache( Ui::ValidateUserpicCache(
@ -402,7 +403,7 @@ void PeerData::paintUserpic(
cloud, cloud,
cloud ? nullptr : ensureEmptyUserpic().get(), cloud ? nullptr : ensureEmptyUserpic().get(),
size * ratio, size * ratio,
isForum()); !forceCircle && isForum());
p.drawImage(QRect(x, y, size, size), view.cached); p.drawImage(QRect(x, y, size, size), view.cached);
} }
@ -627,7 +628,8 @@ bool PeerData::canCreatePolls() const {
if (const auto user = asUser()) { if (const auto user = asUser()) {
return user->isBot() return user->isBot()
&& !user->isSupport() && !user->isSupport()
&& !user->isRepliesChat(); && !user->isRepliesChat()
&& !user->isVerifyCodes();
} }
return Data::CanSend(this, ChatRestriction::SendPolls); return Data::CanSend(this, ChatRestriction::SendPolls);
} }
@ -663,7 +665,7 @@ bool PeerData::canEditMessagesIndefinitely() const {
} }
bool PeerData::canExportChatHistory() const { bool PeerData::canExportChatHistory() const {
if (isRepliesChat() || !allowsForwarding()) { if (isRepliesChat() || isVerifyCodes() || !allowsForwarding()) {
return false; return false;
} else if (const auto channel = asChannel()) { } else if (const auto channel = asChannel()) {
if (!channel->amIn() && channel->invitePeekExpires()) { if (!channel->amIn() && channel->invitePeekExpires()) {
@ -864,6 +866,13 @@ void PeerData::fillNames() {
if (localized != english) { if (localized != english) {
appendToIndex(localized); appendToIndex(localized);
} }
} else if (isVerifyCodes()) {
const auto english = u"Verification Codes"_q;
const auto localized = tr::lng_verification_codes(tr::now);
appendToIndex(english);
if (localized != english) {
appendToIndex(localized);
}
} }
} else if (const auto channel = asChannel()) { } else if (const auto channel = asChannel()) {
appendToIndex(channel->username()); appendToIndex(channel->username());
@ -1201,6 +1210,11 @@ bool PeerData::isRepliesChat() const {
: kTestId) == id; : kTestId) == id;
} }
bool PeerData::isVerifyCodes() const {
constexpr auto kVerifyCodesId = peerFromUser(489000);
return (id == kVerifyCodesId);
}
bool PeerData::sharedMediaInfo() const { bool PeerData::sharedMediaInfo() const {
return isSelf() || isRepliesChat(); return isSelf() || isRepliesChat();
} }

View file

@ -227,6 +227,7 @@ public:
[[nodiscard]] bool isForum() const; [[nodiscard]] bool isForum() const;
[[nodiscard]] bool isGigagroup() const; [[nodiscard]] bool isGigagroup() const;
[[nodiscard]] bool isRepliesChat() const; [[nodiscard]] bool isRepliesChat() const;
[[nodiscard]] bool isVerifyCodes() const;
[[nodiscard]] bool sharedMediaInfo() const; [[nodiscard]] bool sharedMediaInfo() const;
[[nodiscard]] bool savedSublistsInfo() const; [[nodiscard]] bool savedSublistsInfo() const;
[[nodiscard]] bool hasStoriesHidden() const; [[nodiscard]] bool hasStoriesHidden() const;
@ -317,15 +318,23 @@ public:
Ui::PeerUserpicView &view, Ui::PeerUserpicView &view,
int x, int x,
int y, int y,
int size) const; int size,
bool forceCircle = false) const;
void paintUserpicLeft( void paintUserpicLeft(
Painter &p, Painter &p,
Ui::PeerUserpicView &view, Ui::PeerUserpicView &view,
int x, int x,
int y, int y,
int w, int w,
int size) const { int size,
paintUserpic(p, view, rtl() ? (w - x - size) : x, y, size); bool forceCircle = false) const {
paintUserpic(
p,
view,
rtl() ? (w - x - size) : x,
y,
size,
forceCircle);
} }
void loadUserpic(); void loadUserpic();
[[nodiscard]] bool hasUserpic() const; [[nodiscard]] bool hasUserpic() const;

View file

@ -220,7 +220,7 @@ inline auto DefaultRestrictionValue(
ChatRestrictions rights, ChatRestrictions rights,
bool forbidInForums) { bool forbidInForums) {
if (const auto user = peer->asUser()) { if (const auto user = peer->asUser()) {
if (user->isRepliesChat()) { if (user->isRepliesChat() || user->isVerifyCodes()) {
return rpl::single(false); return rpl::single(false);
} }
using namespace rpl::mappers; using namespace rpl::mappers;

View file

@ -0,0 +1,24 @@
/*
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 Data {
struct ReportInput final {
QByteArray optionId;
QString optionText;
QString comment;
std::vector<MsgId> ids;
std::vector<StoryId> stories;
inline bool operator==(const ReportInput &other) const {
return optionId == other.optionId && comment == other.comment;
}
};
} // namespace Data

View file

@ -1668,6 +1668,14 @@ rpl::producer<not_null<HistoryItem*>> Session::newItemAdded() const {
return _newItemAdded.events(); return _newItemAdded.events();
} }
void Session::notifyGiftUpdate(GiftUpdate &&update) {
_giftUpdates.fire(std::move(update));
}
rpl::producer<GiftUpdate> Session::giftUpdates() const {
return _giftUpdates.events();
}
HistoryItem *Session::changeMessageId(PeerId peerId, MsgId wasId, MsgId nowId) { HistoryItem *Session::changeMessageId(PeerId peerId, MsgId wasId, MsgId nowId) {
const auto list = messagesListForInsert(peerId); const auto list = messagesListForInsert(peerId);
const auto i = list->find(wasId); const auto i = list->find(wasId);

View file

@ -77,6 +77,18 @@ struct RepliesReadTillUpdate {
bool out = false; bool out = false;
}; };
struct GiftUpdate {
enum class Action : uchar {
Save,
Unsave,
Convert,
Delete,
};
FullMsgId itemId;
Action action = {};
};
class Session final { class Session final {
public: public:
using ViewElement = HistoryView::Element; using ViewElement = HistoryView::Element;
@ -281,6 +293,8 @@ public:
[[nodiscard]] rpl::producer<not_null<const ViewElement*>> viewLayoutChanged() const; [[nodiscard]] rpl::producer<not_null<const ViewElement*>> viewLayoutChanged() const;
void notifyNewItemAdded(not_null<HistoryItem*> item); void notifyNewItemAdded(not_null<HistoryItem*> item);
[[nodiscard]] rpl::producer<not_null<HistoryItem*>> newItemAdded() const; [[nodiscard]] rpl::producer<not_null<HistoryItem*>> newItemAdded() const;
void notifyGiftUpdate(GiftUpdate &&update);
[[nodiscard]] rpl::producer<GiftUpdate> giftUpdates() const;
void requestItemRepaint(not_null<const HistoryItem*> item); void requestItemRepaint(not_null<const HistoryItem*> item);
[[nodiscard]] rpl::producer<not_null<const HistoryItem*>> itemRepaintRequest() const; [[nodiscard]] rpl::producer<not_null<const HistoryItem*>> itemRepaintRequest() const;
void requestViewRepaint(not_null<const ViewElement*> view); void requestViewRepaint(not_null<const ViewElement*> view);
@ -924,6 +938,7 @@ private:
rpl::event_stream<not_null<const HistoryItem*>> _itemLayoutChanges; rpl::event_stream<not_null<const HistoryItem*>> _itemLayoutChanges;
rpl::event_stream<not_null<const ViewElement*>> _viewLayoutChanges; rpl::event_stream<not_null<const ViewElement*>> _viewLayoutChanges;
rpl::event_stream<not_null<HistoryItem*>> _newItemAdded; rpl::event_stream<not_null<HistoryItem*>> _newItemAdded;
rpl::event_stream<GiftUpdate> _giftUpdates;
rpl::event_stream<not_null<const HistoryItem*>> _itemRepaintRequest; rpl::event_stream<not_null<const HistoryItem*>> _itemRepaintRequest;
rpl::event_stream<not_null<const ViewElement*>> _viewRepaintRequest; rpl::event_stream<not_null<const ViewElement*>> _viewRepaintRequest;
rpl::event_stream<not_null<const HistoryItem*>> _itemResizeRequest; rpl::event_stream<not_null<const HistoryItem*>> _itemResizeRequest;

Some files were not shown because too many files have changed in this diff Show more