Merge tag 'v5.4.1' into dev

# Conflicts:
#	.github/workflows/mac_packaged.yml
#	Telegram/Resources/winrc/Telegram.rc
#	Telegram/Resources/winrc/Updater.rc
#	Telegram/SourceFiles/core/local_url_handlers.cpp
#	Telegram/SourceFiles/core/version.h
#	Telegram/SourceFiles/data/data_message_reactions.cpp
#	Telegram/SourceFiles/history/view/history_view_message.h
#	Telegram/lib_ui
#	docs/building-win.md
#	snap/snapcraft.yaml
This commit is contained in:
AlexeyZavar 2024-08-18 01:22:08 +03:00
commit ec59d8679f
225 changed files with 7495 additions and 3920 deletions

View file

@ -549,6 +549,8 @@ PRIVATE
data/business/data_business_info.h data/business/data_business_info.h
data/business/data_shortcut_messages.cpp data/business/data_shortcut_messages.cpp
data/business/data_shortcut_messages.h data/business/data_shortcut_messages.h
data/components/credits.cpp
data/components/credits.h
data/components/factchecks.cpp data/components/factchecks.cpp
data/components/factchecks.h data/components/factchecks.h
data/components/location_pickers.cpp data/components/location_pickers.cpp
@ -898,6 +900,8 @@ PRIVATE
history/view/history_view_message.cpp history/view/history_view_message.cpp
history/view/history_view_message.h history/view/history_view_message.h
history/view/history_view_object.h history/view/history_view_object.h
history/view/history_view_paid_reaction_toast.cpp
history/view/history_view_paid_reaction_toast.h
history/view/history_view_pinned_bar.cpp history/view/history_view_pinned_bar.cpp
history/view/history_view_pinned_bar.h history/view/history_view_pinned_bar.h
history/view/history_view_pinned_section.cpp history/view/history_view_pinned_section.cpp
@ -1310,6 +1314,8 @@ PRIVATE
payments/payments_form.h payments/payments_form.h
payments/payments_non_panel_process.cpp payments/payments_non_panel_process.cpp
payments/payments_non_panel_process.h payments/payments_non_panel_process.h
payments/payments_reaction_process.cpp
payments/payments_reaction_process.h
platform/linux/file_utilities_linux.cpp platform/linux/file_utilities_linux.cpp
platform/linux/file_utilities_linux.h platform/linux/file_utilities_linux.h
platform/linux/launcher_linux.cpp platform/linux/launcher_linux.cpp
@ -1542,6 +1548,8 @@ PRIVATE
support/support_preload.h support/support_preload.h
support/support_templates.cpp support/support_templates.cpp
support/support_templates.h support/support_templates.h
ui/boxes/edit_invite_link_session.cpp
ui/boxes/edit_invite_link_session.h
ui/chat/attach/attach_item_single_file_preview.cpp ui/chat/attach/attach_item_single_file_preview.cpp
ui/chat/attach/attach_item_single_file_preview.h ui/chat/attach/attach_item_single_file_preview.h
ui/chat/attach/attach_item_single_media_preview.cpp ui/chat/attach/attach_item_single_media_preview.cpp
@ -1896,7 +1904,7 @@ endif()
set_target_properties(Telegram PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${output_folder}) set_target_properties(Telegram PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${output_folder})
if (WIN32) if (WIN32 AND CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC")
target_link_libraries(Telegram target_link_libraries(Telegram
PRIVATE PRIVATE
delayimp delayimp
@ -1993,7 +2001,7 @@ if (NOT DESKTOP_APP_DISABLE_AUTOUPDATE AND NOT build_macstore AND NOT build_wins
base/platform/win/base_windows_safe_library.h base/platform/win/base_windows_safe_library.h
) )
target_include_directories(Updater PRIVATE ${lib_base_loc}) target_include_directories(Updater PRIVATE ${lib_base_loc})
if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") if (CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC")
target_link_libraries(Updater target_link_libraries(Updater
PRIVATE PRIVATE
delayimp delayimp

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 565 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 740 B

View file

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="72px" height="72px" viewBox="0 0 72 72" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>General / menu_incomes</title>
<defs>
<rect id="path-1" x="0" y="0" width="72" height="37"></rect>
<rect id="path-3" x="0" y="0" width="72" height="42"></rect>
</defs>
<g id="General-/-menu_incomes" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group-2">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<g id="Rectangle"></g>
<g id="Group" mask="url(#mask-2)" stroke="#FFFFFF" stroke-linecap="round" stroke-width="5.4">
<g transform="translate(35.056529, 35.872413) rotate(66.000000) translate(-35.056529, -35.872413) translate(5.056529, 5.872413)" id="Path">
<path d="M7.74068421e-14,27 C7.74068421e-14,27.6514434 0.0207638485,31.2980371 0.0616658298,31.9391554 C1.06104802,47.603977 14.0829009,60 30,60 C46.5685425,60 60,46.5685425 60,30 C60,13.4314575 46.5685425,-1.34265911e-13 30,-1.34265911e-13 C20.9122281,-1.34265911e-13 12.7682399,4.04081874 7.26674545,10.4237463" transform="translate(30.000000, 30.000000) rotate(90.000000) translate(-30.000000, -30.000000) "></path>
</g>
</g>
</g>
<g id="Group-2-Copy" transform="translate(0.000000, 28.000000)">
<mask id="mask-4" fill="white">
<use xlink:href="#path-3"></use>
</mask>
<g id="Rectangle"></g>
<g id="Group" mask="url(#mask-4)" stroke="#FFFFFF" stroke-dasharray="5,14" stroke-linecap="round" stroke-width="5.4">
<g transform="translate(35.056529, 7.872413) rotate(66.000000) translate(-35.056529, -7.872413) translate(-2.745677, -29.929792)" id="Path">
<path d="M7.80220532,34.8022053 C7.80220532,35.4536487 7.82296917,39.1002424 7.86387115,39.7413607 C8.86325334,55.4061823 21.8851062,67.8022053 37.8022053,67.8022053 C54.3707478,67.8022053 67.8022053,54.3707478 67.8022053,37.8022053 C67.8022053,21.2336628 54.3707478,7.80220532 37.8022053,7.80220532 C28.7144334,7.80220532 20.5704452,11.8430241 15.0689508,18.2259516" transform="translate(37.802205, 37.802205) rotate(72.000000) translate(-37.802205, -37.802205) "></path>
</g>
</g>
</g>
<g id="$" transform="translate(23.000000, 16.000000)" fill="#FFFFFF" fill-rule="nonzero">
<path d="M12.034384,39.8 C13.0659026,39.8 14.182808,39.2321492 14.182808,37.869307 L14.182808,35.752184 C20.3489971,35.093477 24,31.5046593 24,26.280431 C24,21.7830518 21.2722063,19.0573675 15.6103152,17.8308096 L10.9570201,16.7859639 C8.04584527,16.1499709 6.62464183,14.8325568 6.62464183,12.9018637 C6.62464183,10.585032 8.64183381,8.90419336 11.8051576,8.90419336 C14.3724928,8.90419336 16.1604585,9.76732673 18.1547278,11.9705882 C19.1633238,13.0154339 19.9426934,13.4015725 20.9971347,13.4015725 C22.2808023,13.4015725 23.2664756,12.5157251 23.2664756,11.198311 C23.2664756,9.92632499 22.5100287,8.54076878 21.226361,7.2914968 C19.530086,5.70151427 17.1653295,4.65666861 14.3,4.29324403 L14.3,1.907979 C14.3,0.56785087 13.2034384,0 12.1489971,0 C11.1174785,0 10,0.545136833 10,1.907979 L10,4.22510192 C4.06303725,4.77023879 0.481375358,8.29091439 0.481375358,13.3334304 C0.481375358,17.7399534 3.20916905,20.6700641 8.43553009,21.8284799 L13.0888252,22.8960396 C16.4813754,23.6910309 17.8796562,24.8721607 17.8796562,26.8709959 C17.8796562,29.460396 15.8395415,31.0958066 12.1948424,31.0958066 C9.46704871,31.0958066 7.19770774,30.073675 5.13467049,27.8704135 C3.96561605,26.7119977 3.32378223,26.4621433 2.45272206,26.4621433 C1.05444126,26.4621433 0,27.3479907 0,28.8698311 C0,30.2099592 0.779369628,31.5955154 2.17765043,32.7993593 C4.01146132,34.457484 6.67679058,35.4796156 9.863037,35.7748981 L9.863037,37.869307 C9.863037,39.2321492 10.9799427,39.8 12.034384,39.8 Z" id="Path"></path>
</g>
<path d="M66.2217021,28.4347599 L72.4528242,38.8199633 C72.7823745,39.3692138 72.6042723,40.0816225 72.0550218,40.4111728 C71.8747737,40.5193217 71.668522,40.5764501 71.4583183,40.5764501 L57.5551143,40.5764501 C56.9145837,40.5764501 56.3953311,40.0571975 56.3953311,39.4166669 C56.3953311,39.2064632 56.4524595,39.0002115 56.5606084,38.8199633 L62.7917304,28.4347599 C63.3600268,27.4875994 64.5885473,27.1804692 65.5357078,27.7487655 C65.817207,27.9176651 66.0528026,28.1532607 66.2217021,28.4347599 Z" id="Triangle" fill="#FFFFFF" transform="translate(64.506716, 33.076450) rotate(168.000000) translate(-64.506716, -33.076450) "></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

View file

@ -11,6 +11,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"cloud_lng_forwarded_psa_covid" = "COVID-19 Notification from {channel}"; "cloud_lng_forwarded_psa_covid" = "COVID-19 Notification from {channel}";
"cloud_lng_tooltip_psa_covid" = "This message provides you with a public service announcement in relation to the ongoing COVID-19 pandemic. Learn more about this initiative at https://telegram.org/blog/coronavirus"; "cloud_lng_tooltip_psa_covid" = "This message provides you with a public service announcement in relation to the ongoing COVID-19 pandemic. Learn more about this initiative at https://telegram.org/blog/coronavirus";
"cloud_lng_topup_purpose_subs" = "Buy **Stars** to keep your channel subscriptions.";
"cloud_lng_passport_in_ar" = "Arabic"; "cloud_lng_passport_in_ar" = "Arabic";
"cloud_lng_passport_in_az" = "Azerbaijani"; "cloud_lng_passport_in_az" = "Azerbaijani";
"cloud_lng_passport_in_bg" = "Bulgarian"; "cloud_lng_passport_in_bg" = "Bulgarian";

View file

@ -1315,6 +1315,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_profile_bot_settings" = "Bot Settings"; "lng_profile_bot_settings" = "Bot Settings";
"lng_profile_bot_help" = "Bot Help"; "lng_profile_bot_help" = "Bot Help";
"lng_profile_bot_privacy" = "Bot Privacy Policy"; "lng_profile_bot_privacy" = "Bot Privacy Policy";
"lng_profile_bot_privacy_url" = "https://telegram.org/privacy-tpa";
"lng_profile_common_groups#one" = "{count} group in common"; "lng_profile_common_groups#one" = "{count} group in common";
"lng_profile_common_groups#other" = "{count} groups in common"; "lng_profile_common_groups#other" = "{count} groups in common";
"lng_profile_similar_channels#one" = "{count} similar channel"; "lng_profile_similar_channels#one" = "{count} similar channel";
@ -1550,6 +1551,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_manage_peer_reactions_max_slider#other" = "{count} reactions per post"; "lng_manage_peer_reactions_max_slider#other" = "{count} reactions per post";
"lng_manage_peer_reactions_max_about" = "Limit the number of different reactions that can be added to a post, including already published ones."; "lng_manage_peer_reactions_max_about" = "Limit the number of different reactions that can be added to a post, including already published ones.";
"lng_manage_peer_reactions_paid" = "Enable Paid Reactions";
"lng_manage_peer_reactions_paid_about" = "Switch this on to let your subscribers set paid reactions with Telegram Stars, which you will be able to withdraw later as TON. {link}";
"lng_manage_peer_reactions_paid_link" = "Learn more >";
"lng_manage_peer_antispam" = "Aggressive Anti-Spam"; "lng_manage_peer_antispam" = "Aggressive Anti-Spam";
"lng_manage_peer_antispam_about" = "Telegram will filter more spam but may occasionally affect ordinary messages. You can report False Positives in Recent Actions."; "lng_manage_peer_antispam_about" = "Telegram will filter more spam but may occasionally affect ordinary messages. You can report False Positives in Recent Actions.";
"lng_manage_peer_antispam_not_enough#one" = "Aggressive filtering can be enabled only in groups with more than **{count} member**."; "lng_manage_peer_antispam_not_enough#one" = "Aggressive filtering can be enabled only in groups with more than **{count} member**.";
@ -1943,6 +1948,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_group_invite_members#other" = "{count} members, among them:"; "lng_group_invite_members#other" = "{count} members, among them:";
"lng_channel_invite_private" = "This channel is private.\nPlease join it to continue viewing its content."; "lng_channel_invite_private" = "This channel is private.\nPlease join it to continue viewing its content.";
"lng_channel_invite_subscription_button" = "Subscribe";
"lng_channel_invite_subscription_title" = "Subscribe to the Channel";
"lng_channel_invite_subscription_about" = "Do you want to subscribe for {channel} for {price} per month?";
"lng_channel_invite_subscription_terms" = "By subscribing you agree to the {link}.";
"lng_group_invite_create" = "Create an invite link"; "lng_group_invite_create" = "Create an invite link";
"lng_group_invite_about_new" = "Your previous link will be deactivated and we'll generate a new invite link for you."; "lng_group_invite_about_new" = "Your previous link will be deactivated and we'll generate a new invite link for you.";
"lng_group_invite_copied" = "Invite link copied to clipboard."; "lng_group_invite_copied" = "Invite link copied to clipboard.";
@ -2017,6 +2027,21 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_group_invite_about_no_approve" = "New users will be able to join the group without being approved by the admins."; "lng_group_invite_about_no_approve" = "New users will be able to join the group without being approved by the admins.";
"lng_group_invite_about_approve_channel" = "New users will be able to join the channel only after having been approved by the admins."; "lng_group_invite_about_approve_channel" = "New users will be able to join the channel only after having been approved by the admins.";
"lng_group_invite_about_no_approve_channel" = "New users will be able to join the channel without being approved by the admins."; "lng_group_invite_about_no_approve_channel" = "New users will be able to join the channel without being approved by the admins.";
"lng_group_invite_subscription" = "Require Monthly Fee";
"lng_group_invite_subscription_ph" = "Stars Amount per month";
"lng_group_invite_subscription_price" = "~{cost} / month";
"lng_group_invite_subscription_toast" = "Sorry, you cannot change the number of Stars for an already created invite link.";
"lng_group_invite_subscription_about" = "Charge a subscription fee from people joining your channel via this link. {link}";
"lng_group_invite_subscription_about_link" = "Learn more {emoji}";
"lng_group_invite_subscription_about_url" = "https://telegram.org/tos/stars";
"lng_group_invite_subscription_info_subtitle" = "Subscription fee";
"lng_group_invite_subscription_info_title" = "{emoji} {price} / month {multiplier} {total}";
"lng_group_invite_subscription_info_title_none" = "{emoji} {price} / month";
"lng_group_invite_subscription_info_about" = "you get approximately {total} montly";
"lng_group_invite_joined_right" = "per month";
"lng_group_invite_joined_status" = "joined {date}";
"lng_group_invite_joined_row_subscriber" = "Subscriber";
"lng_group_invite_joined_row_date" = "Subscribed";
"lng_group_request_to_join" = "Request to Join"; "lng_group_request_to_join" = "Request to Join";
"lng_group_request_about" = "This group accepts new members only after they are approved by its admins."; "lng_group_request_about" = "This group accepts new members only after they are approved by its admins.";
@ -2352,6 +2377,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_credits_box_out_sure#other" = "Do you want to buy **\"{text}\"** in **{bot}** for **{count} Stars**?"; "lng_credits_box_out_sure#other" = "Do you want to buy **\"{text}\"** in **{bot}** for **{count} Stars**?";
"lng_credits_box_out_media#one" = "Do you want to unlock {media} in {chat} for **{count} Star**?"; "lng_credits_box_out_media#one" = "Do you want to unlock {media} in {chat} for **{count} Star**?";
"lng_credits_box_out_media#other" = "Do you want to unlock {media} in {chat} for **{count} Stars**?"; "lng_credits_box_out_media#other" = "Do you want to unlock {media} in {chat} for **{count} Stars**?";
"lng_credits_box_out_media_user#one" = "Do you want to unlock {media} from {user} for **{count} Star**?";
"lng_credits_box_out_media_user#other" = "Do you want to unlock {media} from {user} for **{count} Stars**?";
"lng_credits_box_out_photo" = "a photo"; "lng_credits_box_out_photo" = "a photo";
"lng_credits_box_out_photos#one" = "{count} photo"; "lng_credits_box_out_photos#one" = "{count} photo";
"lng_credits_box_out_photos#other" = "{count} photos"; "lng_credits_box_out_photos#other" = "{count} photos";
@ -2366,6 +2393,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_credits_media_done_title" = "Media Unlocked"; "lng_credits_media_done_title" = "Media Unlocked";
"lng_credits_media_done_text#one" = "**{count} Star** transferred to {chat}."; "lng_credits_media_done_text#one" = "**{count} Star** transferred to {chat}.";
"lng_credits_media_done_text#other" = "**{count} Stars** transferred to {chat}."; "lng_credits_media_done_text#other" = "**{count} Stars** transferred to {chat}.";
"lng_credits_media_done_text_user#one" = "**{count} Star** transferred to {user}.";
"lng_credits_media_done_text_user#other" = "**{count} Stars** transferred to {user}.";
"lng_credits_summary_in_toast_title" = "Stars Acquired"; "lng_credits_summary_in_toast_title" = "Stars Acquired";
"lng_credits_summary_in_toast_about#one" = "**{count}** Star added to your balance."; "lng_credits_summary_in_toast_about#one" = "**{count}** Star added to your balance.";
"lng_credits_summary_in_toast_about#other" = "**{count}** Stars added to your balance."; "lng_credits_summary_in_toast_about#other" = "**{count}** Stars added to your balance.";
@ -2392,10 +2421,42 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_credits_box_history_entry_media" = "Media"; "lng_credits_box_history_entry_media" = "Media";
"lng_credits_box_history_entry_about" = "You can dispute this transaction {link}."; "lng_credits_box_history_entry_about" = "You can dispute this transaction {link}.";
"lng_credits_box_history_entry_about_link" = "here"; "lng_credits_box_history_entry_about_link" = "here";
"lng_credits_box_history_entry_reaction_name" = "Star Reaction";
"lng_credits_box_history_entry_subscription" = "Monthly subscription fee";
"lng_credits_subscription_section" = "My subscriptions";
"lng_credits_box_subscription_title" = "Subscription";
"lng_credits_subscription_subtitle" = "{emoji} {cost} / month";
"lng_credits_subscriber_subtitle" = "appx. {total} per month";
"lng_credits_subscription_row_to" = "Subscription";
"lng_credits_subscription_row_from" = "Subscribed";
"lng_credits_subscription_row_next_on" = "Renews";
"lng_credits_subscription_row_next_off" = "Expires";
"lng_credits_subscription_row_next_none" = "Expired";
"lng_credits_subscription_on_button" = "Cancel Subscription";
"lng_credits_subscription_on_about" = "If you cancel now, you will still be able to access your subscription until {date}.";
"lng_credits_subscription_off_button" = "Renew Subscription";
"lng_credits_subscription_off_about" = "You have cancelled your subscription.";
"lng_credits_subscription_status_on" = "renews on {date}";
"lng_credits_subscription_status_off" = "expires on {date}";
"lng_credits_subscription_status_none" = "expired on {date}";
"lng_credits_subscription_status_off_right" = "cancelled";
"lng_credits_subscription_status_none_right" = "expired";
"lng_credits_small_balance_title#one" = "{count} Star Needed"; "lng_credits_small_balance_title#one" = "{count} Star Needed";
"lng_credits_small_balance_title#other" = "{count} Stars Needed"; "lng_credits_small_balance_title#other" = "{count} Stars Needed";
"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_subscribe" = "Buy **Stars** and subscribe to **{channel}** and other channels.";
"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_link" = "Buy anyway";
"lng_credits_gift_title" = "Gift Telegram Stars"; "lng_credits_gift_title" = "Gift Telegram Stars";
@ -3188,6 +3249,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_bot_add_to_side_menu" = "{bot} asks your permission to be added as an option to your main menu so you can access it any time."; "lng_bot_add_to_side_menu" = "{bot} asks your permission to be added as an option to your main menu so you can access it any time.";
"lng_bot_add_to_side_menu_done" = "Bot added to the main menu."; "lng_bot_add_to_side_menu_done" = "Bot added to the main menu.";
"lng_bot_no_scan_qr" = "QR Codes for bots are not supported on Desktop. Please use one of Telegram's mobile apps."; "lng_bot_no_scan_qr" = "QR Codes for bots are not supported on Desktop. Please use one of Telegram's mobile apps.";
"lng_bot_no_share_story" = "Sharing to Stories is not supported on Desktop. Please use one of Telegram's mobile apps.";
"lng_bot_click_to_start" = "Click here to use this bot."; "lng_bot_click_to_start" = "Click here to use this bot.";
"lng_bot_status_users#one" = "{count} user"; "lng_bot_status_users#one" = "{count} user";
"lng_bot_status_users#other" = "{count} users"; "lng_bot_status_users#other" = "{count} users";
@ -3412,6 +3474,29 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_paid_about_link_url" = "https://telegram.org/blog/telegram-stars"; "lng_paid_about_link_url" = "https://telegram.org/blog/telegram-stars";
"lng_paid_price" = "Unlock for {price}"; "lng_paid_price" = "Unlock for {price}";
"lng_paid_react_title" = "Star Reaction";
"lng_paid_react_about" = "Choose how many **Stars** you want to send to {channel} to support this post.";
"lng_paid_react_already#one" = "You sent **{count} Star** to support this post.";
"lng_paid_react_already#other" = "You sent **{count} Stars** to support this post.";
"lng_paid_react_top_title" = "Top Senders";
"lng_paid_react_send" = "Send {price}";
"lng_paid_react_agree" = "By sending stars, you agree to the {link}.";
"lng_paid_react_agree_link" = "Terms of Service";
"lng_paid_react_toast#one" = "Star Sent!";
"lng_paid_react_toast#other" = "Stars Sent!";
"lng_paid_react_toast_text#one" = "You reacted with **{count} Star**.";
"lng_paid_react_toast_text#other" = "You reacted with **{count} Stars**.";
"lng_paid_react_undo" = "Undo";
"lng_paid_react_show_in_top" = "Show me in Top Senders";
"lng_paid_react_anonymous" = "Anonymous";
"lng_sensitive_tag" = "18+";
"lng_sensitive_title" = "18+";
"lng_sensitive_text" = "This media may contain sensitive content suitable only for adults. Do you still want to view it?";
"lng_sensitive_always" = "Always show 18+ media";
"lng_sensitive_view" = "View Anyway";
"lng_sensitive_toast" = "You can update the visibility of sensitive media in **Settings > Chat Settings > Sensitive content**";
"lng_translate_show_original" = "Show Original"; "lng_translate_show_original" = "Show Original";
"lng_translate_bar_to" = "Translate to {name}"; "lng_translate_bar_to" = "Translate to {name}";
"lng_translate_bar_to_other" = "Translate to {name}"; "lng_translate_bar_to_other" = "Translate to {name}";
@ -3516,6 +3601,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_edit_channel_title" = "Edit channel"; "lng_edit_channel_title" = "Edit channel";
"lng_edit_bot_title" = "Edit bot"; "lng_edit_bot_title" = "Edit bot";
"lng_edit_sign_messages" = "Sign messages"; "lng_edit_sign_messages" = "Sign messages";
"lng_edit_sign_messages_about" = "Add names of admins to the messages they post.";
"lng_edit_sign_profiles" = "Show authors' profiles";
"lng_edit_sign_profiles_about" = "Add names and photos of admins to the messages they post, linking to their profiles.";
"lng_edit_group" = "Edit group"; "lng_edit_group" = "Edit group";
"lng_edit_channel_color" = "Change name color"; "lng_edit_channel_color" = "Change name color";
"lng_edit_channel_level_min" = "Level 1+"; "lng_edit_channel_level_min" = "Level 1+";
@ -4305,6 +4393,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_admin_log_invites_disabled" = "{from} disabled group invites"; "lng_admin_log_invites_disabled" = "{from} disabled group invites";
"lng_admin_log_signatures_enabled" = "{from} enabled signatures"; "lng_admin_log_signatures_enabled" = "{from} enabled signatures";
"lng_admin_log_signatures_disabled" = "{from} disabled signatures"; "lng_admin_log_signatures_disabled" = "{from} disabled signatures";
"lng_admin_log_signature_profiles_enabled" = "{from} enabled showing authors' profiles";
"lng_admin_log_signature_profiles_disabled" = "{from} disabled showing authors' profiles";
"lng_admin_log_forwards_enabled" = "{from} allowed content copying"; "lng_admin_log_forwards_enabled" = "{from} allowed content copying";
"lng_admin_log_forwards_disabled" = "{from} restricted content copying"; "lng_admin_log_forwards_disabled" = "{from} restricted content copying";
"lng_admin_log_history_made_hidden" = "{from} made group history hidden for new members"; "lng_admin_log_history_made_hidden" = "{from} made group history hidden for new members";
@ -5179,6 +5269,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_stats_loading" = "Loading stats..."; "lng_stats_loading" = "Loading stats...";
"lng_stats_loading_subtext" = "Please wait a few moments while we generate your stats."; "lng_stats_loading_subtext" = "Please wait a few moments while we generate your stats.";
"lng_stats_boosts_loading" = "Loading boosts list...";
"lng_stats_boosts_loading_subtext" = "Please wait a few moments while we generate your stats.";
"lng_stats_earn_loading" = "Loading rewards info...";
"lng_stats_earn_loading_subtext" = "Please wait a few moments while we generate your stats.";
"lng_chart_title_member_count" = "Growth"; "lng_chart_title_member_count" = "Growth";
"lng_chart_title_join" = "Followers"; "lng_chart_title_join" = "Followers";

View file

@ -11,20 +11,41 @@
<file alias="ttl.tgs">../../animations/ttl.tgs</file> <file alias="ttl.tgs">../../animations/ttl.tgs</file>
<file alias="discussion.tgs">../../animations/discussion.tgs</file> <file alias="discussion.tgs">../../animations/discussion.tgs</file>
<file alias="stats.tgs">../../animations/stats.tgs</file> <file alias="stats.tgs">../../animations/stats.tgs</file>
<file alias="stats_boosts.tgs">../../animations/stats_boosts.tgs</file>
<file alias="stats_earn.tgs">../../animations/stats_earn.tgs</file>
<file alias="voice_ttl_idle.tgs">../../animations/voice_ttl_idle.tgs</file> <file alias="voice_ttl_idle.tgs">../../animations/voice_ttl_idle.tgs</file>
<file alias="voice_ttl_start.tgs">../../animations/voice_ttl_start.tgs</file> <file alias="voice_ttl_start.tgs">../../animations/voice_ttl_start.tgs</file>
<file alias="palette.tgs">../../animations/palette.tgs</file> <file alias="palette.tgs">../../animations/palette.tgs</file>
<file alias="sleep.tgs">../../animations/sleep.tgs</file> <file alias="sleep.tgs">../../animations/sleep.tgs</file>
<file alias="greeting.tgs">../../animations/greeting.tgs</file> <file alias="greeting.tgs">../../animations/greeting.tgs</file>
<file alias="location.tgs">../../animations/location.tgs</file> <file alias="location.tgs">../../animations/location.tgs</file>
<file alias="robot.tgs">../../animations/robot.tgs</file> <file alias="robot.tgs">../../animations/robot.tgs</file>
<file alias="writing.tgs">../../animations/writing.tgs</file> <file alias="writing.tgs">../../animations/writing.tgs</file>
<file alias="hours.tgs">../../animations/hours.tgs</file> <file alias="hours.tgs">../../animations/hours.tgs</file>
<file alias="phone.tgs">../../animations/phone.tgs</file> <file alias="phone.tgs">../../animations/phone.tgs</file>
<file alias="chat_link.tgs">../../animations/chat_link.tgs</file> <file alias="chat_link.tgs">../../animations/chat_link.tgs</file>
<file alias="collectible_username.tgs">../../animations/collectible_username.tgs</file> <file alias="collectible_username.tgs">../../animations/collectible_username.tgs</file>
<file alias="collectible_phone.tgs">../../animations/collectible_phone.tgs</file> <file alias="collectible_phone.tgs">../../animations/collectible_phone.tgs</file>
<file alias="search.tgs">../../animations/search.tgs</file> <file alias="search.tgs">../../animations/search.tgs</file>
<file alias="noresults.tgs">../../animations/noresults.tgs</file> <file alias="noresults.tgs">../../animations/noresults.tgs</file>
<file alias="dice_idle.tgs">../../animations/dice/dice_idle.tgs</file>
<file alias="dart_idle.tgs">../../animations/dice/dart_idle.tgs</file>
<file alias="bball_idle.tgs">../../animations/dice/bball_idle.tgs</file>
<file alias="fball_idle.tgs">../../animations/dice/fball_idle.tgs</file>
<file alias="slot_0_idle.tgs">../../animations/dice/slot_0_idle.tgs</file>
<file alias="slot_1_idle.tgs">../../animations/dice/slot_1_idle.tgs</file>
<file alias="slot_2_idle.tgs">../../animations/dice/slot_2_idle.tgs</file>
<file alias="slot_back.tgs">../../animations/dice/slot_back.tgs</file>
<file alias="slot_pull.tgs">../../animations/dice/slot_pull.tgs</file>
<file alias="winners.tgs">../../animations/dice/winners.tgs</file>
<file alias="star_reaction_appear.tgs">../../animations/star_reaction/appear.tgs</file>
<file alias="star_reaction_center.tgs">../../animations/star_reaction/center.tgs</file>
<file alias="star_reaction_select.tgs">../../animations/star_reaction/select.tgs</file>
<file alias="star_reaction_toast.tgs">../../animations/star_reaction/toast.tgs</file>
<file alias="star_reaction_effect1.tgs">../../animations/star_reaction/effect1.tgs</file>
<file alias="star_reaction_effect2.tgs">../../animations/star_reaction/effect2.tgs</file>
<file alias="star_reaction_effect3.tgs">../../animations/star_reaction/effect3.tgs</file>
</qresource> </qresource>
</RCC> </RCC>

View file

@ -7,16 +7,6 @@
<file alias="art/logo_256.png">../../art/logo_256.png</file> <file alias="art/logo_256.png">../../art/logo_256.png</file>
<file alias="art/logo_256_no_margin.png">../../art/logo_256_no_margin.png</file> <file alias="art/logo_256_no_margin.png">../../art/logo_256_no_margin.png</file>
<file alias="art/themeimage.jpg">../../art/themeimage.jpg</file> <file alias="art/themeimage.jpg">../../art/themeimage.jpg</file>
<file alias="art/dice_idle.tgs">../../art/dice_idle.tgs</file>
<file alias="art/dart_idle.tgs">../../art/dart_idle.tgs</file>
<file alias="art/bball_idle.tgs">../../art/bball_idle.tgs</file>
<file alias="art/fball_idle.tgs">../../art/fball_idle.tgs</file>
<file alias="art/slot_0_idle.tgs">../../art/slot_0_idle.tgs</file>
<file alias="art/slot_1_idle.tgs">../../art/slot_1_idle.tgs</file>
<file alias="art/slot_2_idle.tgs">../../art/slot_2_idle.tgs</file>
<file alias="art/slot_back.tgs">../../art/slot_back.tgs</file>
<file alias="art/slot_pull.tgs">../../art/slot_pull.tgs</file>
<file alias="art/winners.tgs">../../art/winners.tgs</file>
<file alias="day-blue.tdesktop-theme">../../day-blue.tdesktop-theme</file> <file alias="day-blue.tdesktop-theme">../../day-blue.tdesktop-theme</file>
<file alias="night.tdesktop-theme">../../night.tdesktop-theme</file> <file alias="night.tdesktop-theme">../../night.tdesktop-theme</file>
<file alias="night-green.tdesktop-theme">../../night-green.tdesktop-theme</file> <file alias="night-green.tdesktop-theme">../../night-green.tdesktop-theme</file>
@ -40,6 +30,7 @@
<file alias="topic_icons/red.svg">../../art/topic_icons/red.svg</file> <file alias="topic_icons/red.svg">../../art/topic_icons/red.svg</file>
<file alias="topic_icons/gray.svg">../../art/topic_icons/gray.svg</file> <file alias="topic_icons/gray.svg">../../art/topic_icons/gray.svg</file>
<file alias="topic_icons/general.svg">../../art/topic_icons/general.svg</file> <file alias="topic_icons/general.svg">../../art/topic_icons/general.svg</file>
<file alias="links_subscription.svg">../../icons/info/edit/links_subscription.svg</file>
</qresource> </qresource>
<qresource prefix="/icons"> <qresource prefix="/icons">
<file alias="calls/hands.lottie">../../icons/calls/hands.lottie</file> <file alias="calls/hands.lottie">../../icons/calls/hands.lottie</file>

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.3.2.0" /> Version="5.4.1.0" />
<Properties> <Properties>
<DisplayName>Telegram Desktop</DisplayName> <DisplayName>Telegram Desktop</DisplayName>
<PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName> <PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName>
@ -37,8 +37,8 @@
<Extensions> <Extensions>
<uap3:Extension Category="windows.protocol"> <uap3:Extension Category="windows.protocol">
<uap3:Protocol Name="tg" Parameters="-- &quot;%1&quot;" /> <uap3:Protocol Name="tg" Parameters="-- &quot;%1&quot;" />
<uap3:Extension Category="windows.protocol">
</uap3:Extension> </uap3:Extension>
<uap3:Extension Category="windows.protocol">
<uap3:Protocol Name="tonsite" Parameters="-- &quot;%1&quot;" /> <uap3:Protocol Name="tonsite" Parameters="-- &quot;%1&quot;" />
</uap3:Extension> </uap3:Extension>
<desktop:Extension <desktop:Extension

View file

@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico"
// //
VS_VERSION_INFO VERSIONINFO VS_VERSION_INFO VERSIONINFO
FILEVERSION 5,3,2,0 FILEVERSION 5,4,1,0
PRODUCTVERSION 5,3,2,0 PRODUCTVERSION 5,4,1,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.3.2.0" VALUE "FileVersion", "5.4.1.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2024" VALUE "LegalCopyright", "Copyright (C) 2014-2024"
VALUE "ProductName", "AyuGram Desktop" VALUE "ProductName", "AyuGram Desktop"
VALUE "ProductVersion", "5.3.2.0" VALUE "ProductVersion", "5.4.1.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,3,2,0 FILEVERSION 5,4,1,0
PRODUCTVERSION 5,3,2,0 PRODUCTVERSION 5,4,1,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.3.2.0" VALUE "FileVersion", "5.4.1.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2024" VALUE "LegalCopyright", "Copyright (C) 2014-2024"
VALUE "ProductName", "AyuGram Desktop" VALUE "ProductName", "AyuGram Desktop"
VALUE "ProductVersion", "5.3.2.0" VALUE "ProductVersion", "5.4.1.0"
END END
END END
BLOCK "VarFileInfo" BLOCK "VarFileInfo"

View file

@ -8,26 +8,39 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_chat_invite.h" #include "api/api_chat_invite.h"
#include "apiwrap.h" #include "apiwrap.h"
#include "window/window_session_controller.h" #include "boxes/premium_limits_box.h"
#include "core/application.h"
#include "data/components/credits.h"
#include "data/data_channel.h"
#include "data/data_file_origin.h"
#include "data/data_forum.h"
#include "data/data_photo.h"
#include "data/data_photo_media.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "info/channel_statistics/boosts/giveaway/boost_badge.h"
#include "info/profile/info_profile_badge.h" #include "info/profile/info_profile_badge.h"
#include "lang/lang_keys.h" #include "lang/lang_keys.h"
#include "main/main_session.h" #include "main/main_session.h"
#include "ui/empty_userpic.h" #include "settings/settings_credits_graphics.h"
#include "ui/painter.h"
#include "core/application.h"
#include "data/data_session.h"
#include "data/data_photo.h"
#include "data/data_photo_media.h"
#include "data/data_channel.h"
#include "data/data_forum.h"
#include "data/data_user.h"
#include "data/data_file_origin.h"
#include "ui/boxes/confirm_box.h" #include "ui/boxes/confirm_box.h"
#include "ui/controls/userpic_button.h"
#include "ui/effects/premium_graphics.h"
#include "ui/effects/premium_stars_colored.h"
#include "ui/empty_userpic.h"
#include "ui/layers/generic_box.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h" #include "ui/toast/toast.h"
#include "boxes/premium_limits_box.h" #include "ui/vertical_list.h"
#include "window/window_session_controller.h"
#include "styles/style_boxes.h" #include "styles/style_boxes.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_credits.h"
#include "styles/style_info.h" #include "styles/style_info.h"
#include "styles/style_layers.h" #include "styles/style_layers.h"
#include "styles/style_premium.h"
namespace Api { namespace Api {
@ -101,12 +114,237 @@ void SubmitChatInvite(
}).send(); }).send();
} }
void ConfirmSubscriptionBox(
not_null<Ui::GenericBox*> box,
not_null<Main::Session*> session,
const QString &hash,
const MTPDchatInvite *data) {
box->setWidth(st::boxWideWidth);
const auto amount = data->vsubscription_pricing()->data().vamount().v;
const auto formId = data->vsubscription_form_id()->v;
const auto name = qs(data->vtitle());
const auto maybePhoto = session->data().processPhoto(data->vphoto());
const auto photo = maybePhoto->isNull() ? nullptr : maybePhoto.get();
struct State final {
std::shared_ptr<Data::PhotoMedia> photoMedia;
std::unique_ptr<Ui::EmptyUserpic> photoEmpty;
std::optional<MTP::Sender> api;
Ui::RpWidget* saveButton = nullptr;
rpl::variable<bool> loading;
};
const auto state = box->lifetime().make_state<State>();
const auto content = box->verticalLayout();
Ui::AddSkip(content, st::confirmInvitePhotoTop);
const auto userpicWrap = content->add(
object_ptr<Ui::CenterWrap<>>(
content,
object_ptr<Ui::RpWidget>(content)));
const auto userpic = userpicWrap->entity();
const auto photoSize = st::confirmInvitePhotoSize;
userpic->resize(Size(photoSize));
const auto options = Images::Option::RoundCircle;
userpic->paintRequest(
) | rpl::start_with_next([=, small = Data::PhotoSize::Small] {
auto p = QPainter(userpic);
if (state->photoMedia) {
if (const auto image = state->photoMedia->image(small)) {
p.drawPixmap(
0,
0,
image->pix(Size(photoSize), { .options = options }));
}
} else if (state->photoEmpty) {
state->photoEmpty->paintCircle(
p,
0,
0,
userpic->width(),
photoSize);
}
}, userpicWrap->lifetime());
userpicWrap->setAttribute(Qt::WA_TransparentForMouseEvents);
if (photo) {
state->photoMedia = photo->createMediaView();
state->photoMedia->wanted(Data::PhotoSize::Small, Data::FileOrigin());
if (!state->photoMedia->image(Data::PhotoSize::Small)) {
session->downloaderTaskFinished(
) | rpl::start_with_next([=] {
userpic->update();
}, userpicWrap->entity()->lifetime());
}
} else {
state->photoEmpty = std::make_unique<Ui::EmptyUserpic>(
Ui::EmptyUserpic::UserpicColor(0),
name);
}
Ui::AddSkip(content);
Ui::AddSkip(content);
{
const auto widget = Ui::CreateChild<Ui::RpWidget>(content);
using ColoredMiniStars = Ui::Premium::ColoredMiniStars;
const auto stars = widget->lifetime().make_state<ColoredMiniStars>(
widget,
false,
Ui::Premium::MiniStars::Type::BiStars);
stars->setColorOverride(Ui::Premium::CreditsIconGradientStops());
widget->resize(
st::boxWideWidth - photoSize,
photoSize * 2);
content->sizeValue(
) | rpl::start_with_next([=](const QSize &size) {
widget->moveToLeft(photoSize / 2, 0);
const auto starsRect = Rect(widget->size());
stars->setPosition(starsRect.topLeft());
stars->setSize(starsRect.size());
widget->lower();
}, widget->lifetime());
widget->paintRequest(
) | rpl::start_with_next([=](const QRect &r) {
auto p = QPainter(widget);
p.fillRect(r, Qt::transparent);
stars->paint(p);
}, widget->lifetime());
}
box->addRow(
object_ptr<Ui::CenterWrap<Ui::FlatLabel>>(
box,
object_ptr<Ui::FlatLabel>(
box,
tr::lng_channel_invite_subscription_title(),
st::inviteLinkSubscribeBoxTitle)));
box->addRow(
object_ptr<Ui::CenterWrap<Ui::FlatLabel>>(
box,
object_ptr<Ui::FlatLabel>(
box,
tr::lng_channel_invite_subscription_about(
lt_channel,
rpl::single(Ui::Text::Bold(name)),
lt_price,
tr::lng_credits_summary_options_credits(
lt_count,
rpl::single(amount) | tr::to_count(),
Ui::Text::Bold),
Ui::Text::WithEntities),
st::inviteLinkSubscribeBoxAbout)));
Ui::AddSkip(content);
box->addRow(
object_ptr<Ui::CenterWrap<Ui::FlatLabel>>(
box,
object_ptr<Ui::FlatLabel>(
box,
tr::lng_channel_invite_subscription_terms(
lt_link,
rpl::combine(
tr::lng_paid_react_agree_link(),
tr::lng_group_invite_subscription_about_url()
) | rpl::map([](const QString &text, const QString &url) {
return Ui::Text::Link(text, url);
}),
Ui::Text::RichLangValue),
st::inviteLinkSubscribeBoxTerms)));
{
const auto balance = Settings::AddBalanceWidget(
content,
session->credits().balanceValue(),
true);
session->credits().load(true);
rpl::combine(
balance->sizeValue(),
content->sizeValue()
) | rpl::start_with_next([=](const QSize &, const QSize &) {
balance->moveToRight(
st::creditsHistoryRightSkip * 2,
st::creditsHistoryRightSkip);
balance->update();
}, balance->lifetime());
}
const auto sendCredits = [=, weak = Ui::MakeWeak(box)] {
const auto show = box->uiShow();
const auto buttonWidth = state->saveButton
? state->saveButton->width()
: 0;
state->api->request(
MTPpayments_SendStarsForm(
MTP_flags(0),
MTP_long(formId),
MTP_inputInvoiceChatInviteSubscription(MTP_string(hash)))
).done([=](const MTPpayments_PaymentResult &result) {
state->api = std::nullopt;
state->loading.force_assign(false);
result.match([&](const MTPDpayments_paymentResult &data) {
session->api().applyUpdates(data.vupdates());
}, [](const MTPDpayments_paymentVerificationNeeded &data) {
});
if (weak) {
box->closeBox();
}
}).fail([=](const MTP::Error &error) {
const auto id = error.type();
if (weak) {
state->api = std::nullopt;
}
show->showToast(id);
state->loading.force_assign(false);
}).send();
if (state->saveButton) {
state->saveButton->resizeToWidth(buttonWidth);
}
};
auto confirmText = tr::lng_channel_invite_subscription_button();
state->saveButton = box->addButton(std::move(confirmText), [=] {
if (state->api) {
return;
}
state->api.emplace(&session->mtp());
state->loading.force_assign(true);
const auto done = [=](Settings::SmallBalanceResult result) {
if (result == Settings::SmallBalanceResult::Success
|| result == Settings::SmallBalanceResult::Already) {
sendCredits();
} else {
state->api = std::nullopt;
state->loading.force_assign(false);
}
};
Settings::MaybeRequestBalanceIncrease(
Main::MakeSessionShow(box->uiShow(), session),
amount,
Settings::SmallBalanceSubscription{ .name = name },
done);
});
if (const auto saveButton = state->saveButton) {
using namespace Info::Statistics;
const auto loadingAnimation = InfiniteRadialAnimationWidget(
saveButton,
saveButton->height() / 2,
&st::editStickerSetNameLoading);
AddChildToWidgetCenter(saveButton, loadingAnimation);
loadingAnimation->showOn(
state->loading.value() | rpl::map(rpl::mappers::_1));
}
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}
} // namespace } // namespace
void CheckChatInvite( void CheckChatInvite(
not_null<Window::SessionController*> controller, not_null<Window::SessionController*> controller,
const QString &hash, const QString &hash,
ChannelData *invitePeekChannel) { ChannelData *invitePeekChannel,
Fn<void()> loaded) {
const auto session = &controller->session(); const auto session = &controller->session();
const auto weak = base::make_weak(controller); const auto weak = base::make_weak(controller);
session->api().checkChatInvite(hash, [=](const MTPChatInvite &result) { session->api().checkChatInvite(hash, [=](const MTPChatInvite &result) {
@ -114,6 +352,9 @@ void CheckChatInvite(
if (!strong) { if (!strong) {
return; return;
} }
if (loaded) {
loaded();
}
Core::App().hideMediaView(); Core::App().hideMediaView();
const auto show = [&](not_null<PeerData*> chat) { const auto show = [&](not_null<PeerData*> chat) {
const auto way = Window::SectionShow::Way::Forward; const auto way = Window::SectionShow::Way::Forward;
@ -125,11 +366,23 @@ void CheckChatInvite(
}; };
result.match([=](const MTPDchatInvite &data) { result.match([=](const MTPDchatInvite &data) {
const auto isGroup = !data.is_broadcast(); const auto isGroup = !data.is_broadcast();
const auto box = strong->show(Box<ConfirmInviteBox>( const auto hasPricing = !!data.vsubscription_pricing();
session, if (hasPricing && !data.vsubscription_form_id()) {
data, strong->uiShow()->showToast(
invitePeekChannel, tr::lng_confirm_phone_link_invalid(tr::now));
[=] { SubmitChatInvite(weak, session, hash, isGroup); })); return;
}
const auto box = hasPricing
? strong->show(Box(
ConfirmSubscriptionBox,
session,
hash,
&data))
: strong->show(Box<ConfirmInviteBox>(
session,
data,
invitePeekChannel,
[=] { SubmitChatInvite(weak, session, hash, isGroup); }));
if (invitePeekChannel) { if (invitePeekChannel) {
box->boxClosing( box->boxClosing(
) | rpl::filter([=] { ) | rpl::filter([=] {

View file

@ -38,7 +38,8 @@ namespace Api {
void CheckChatInvite( void CheckChatInvite(
not_null<Window::SessionController*> controller, not_null<Window::SessionController*> controller,
const QString &hash, const QString &hash,
ChannelData *invitePeekChannel = nullptr); ChannelData *invitePeekChannel = nullptr,
Fn<void()> loaded = nullptr);
} // namespace Api } // namespace Api

View file

@ -94,32 +94,70 @@ constexpr auto kTransactionsLimit = 100;
}, [](const MTPDstarsTransactionPeerAds &) { }, [](const MTPDstarsTransactionPeerAds &) {
return Data::CreditsHistoryEntry::PeerType::Ads; return Data::CreditsHistoryEntry::PeerType::Ads;
}), }),
.refunded = tl.data().is_refund(), .subscriptionUntil = tl.data().vsubscription_period()
.pending = tl.data().is_pending(), ? base::unixtime::parse(base::unixtime::now()
.failed = tl.data().is_failed(), + tl.data().vsubscription_period()->v)
: QDateTime(),
.successDate = tl.data().vtransaction_date() .successDate = tl.data().vtransaction_date()
? 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()),
.reaction = tl.data().is_reaction(),
.refunded = tl.data().is_refund(),
.pending = tl.data().is_pending(),
.failed = tl.data().is_failed(),
.in = (int64(tl.data().vstars().v) >= 0), .in = (int64(tl.data().vstars().v) >= 0),
.gift = tl.data().is_gift(), .gift = tl.data().is_gift(),
}; };
} }
[[nodiscard]] Data::SubscriptionEntry SubscriptionFromTL(
const MTPStarsSubscription &tl) {
return Data::SubscriptionEntry{
.id = qs(tl.data().vid()),
.inviteHash = qs(tl.data().vchat_invite_hash().value_or_empty()),
.until = base::unixtime::parse(tl.data().vuntil_date().v),
.subscription = Data::PeerSubscription{
.credits = tl.data().vpricing().data().vamount().v,
.period = tl.data().vpricing().data().vperiod().v,
},
.barePeerId = peerFromMTP(tl.data().vpeer()).value,
.cancelled = tl.data().is_canceled(),
.expired = (base::unixtime::now() > tl.data().vuntil_date().v),
.canRefulfill = tl.data().is_can_refulfill(),
};
}
[[nodiscard]] Data::CreditsStatusSlice StatusFromTL( [[nodiscard]] Data::CreditsStatusSlice StatusFromTL(
const MTPpayments_StarsStatus &status, const MTPpayments_StarsStatus &status,
not_null<PeerData*> peer) { not_null<PeerData*> peer) {
peer->owner().processUsers(status.data().vusers()); const auto &data = status.data();
peer->owner().processChats(status.data().vchats()); peer->owner().processUsers(data.vusers());
peer->owner().processChats(data.vchats());
auto entries = std::vector<Data::CreditsHistoryEntry>();
if (const auto history = data.vhistory()) {
entries.reserve(history->v.size());
for (const auto &tl : history->v) {
entries.push_back(HistoryFromTL(tl, peer));
}
}
auto subscriptions = std::vector<Data::SubscriptionEntry>();
if (const auto history = data.vsubscriptions()) {
subscriptions.reserve(history->v.size());
for (const auto &tl : history->v) {
subscriptions.push_back(SubscriptionFromTL(tl));
}
}
return Data::CreditsStatusSlice{ return Data::CreditsStatusSlice{
.list = ranges::views::all( .list = std::move(entries),
status.data().vhistory().v .subscriptions = std::move(subscriptions),
) | ranges::views::transform([&](const MTPStarsTransaction &tl) {
return HistoryFromTL(tl, peer);
}) | ranges::to_vector,
.balance = status.data().vbalance().v, .balance = status.data().vbalance().v,
.subscriptionsMissingBalance
= status.data().vsubscriptions_missing_balance().value_or_empty(),
.allLoaded = !status.data().vnext_offset().has_value(), .allLoaded = !status.data().vnext_offset().has_value(),
.token = qs(status.data().vnext_offset().value_or_empty()), .token = qs(status.data().vnext_offset().value_or_empty()),
.tokenSubscriptions = qs(
status.data().vsubscriptions_next_offset().value_or_empty()),
}; };
} }
@ -195,10 +233,14 @@ 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;
done(StatusFromTL(result, _peer)); if (const auto onstack = done) {
onstack(StatusFromTL(result, _peer));
}
}).fail([=] { }).fail([=] {
_requestId = 0; _requestId = 0;
done({}); if (const auto onstack = done) {
onstack({});
}
}).send(); }).send();
} }
@ -220,6 +262,7 @@ void CreditsHistory::request(
} }
_requestId = _api.request(MTPpayments_GetStarsTransactions( _requestId = _api.request(MTPpayments_GetStarsTransactions(
MTP_flags(_flags), MTP_flags(_flags),
MTPstring(), // subscription_id
_peer->isSelf() ? MTP_inputPeerSelf() : _peer->input, _peer->isSelf() ? MTP_inputPeerSelf() : _peer->input,
MTP_string(token), MTP_string(token),
MTP_int(kTransactionsLimit) MTP_int(kTransactionsLimit)
@ -232,6 +275,25 @@ void CreditsHistory::request(
}).send(); }).send();
} }
void CreditsHistory::requestSubscriptions(
const Data::CreditsStatusSlice::OffsetToken &token,
Fn<void(Data::CreditsStatusSlice)> done) {
if (_requestId) {
return;
}
_requestId = _api.request(MTPpayments_GetStarsSubscriptions(
MTP_flags(0),
_peer->isSelf() ? MTP_inputPeerSelf() : _peer->input,
MTP_string(token)
)).done([=](const MTPpayments_StarsStatus &result) {
_requestId = 0;
done(StatusFromTL(result, _peer));
}).fail([=] {
_requestId = 0;
done({});
}).send();
}
Data::CreditTopupOptions CreditsTopupOptions::options() const { Data::CreditTopupOptions CreditsTopupOptions::options() const {
return _options; return _options;
} }

View file

@ -60,6 +60,9 @@ public:
void request( void request(
const Data::CreditsStatusSlice::OffsetToken &token, const Data::CreditsStatusSlice::OffsetToken &token,
Fn<void(Data::CreditsStatusSlice)> done); Fn<void(Data::CreditsStatusSlice)> done);
void requestSubscriptions(
const Data::CreditsStatusSlice::OffsetToken &token,
Fn<void(Data::CreditsStatusSlice)> done);
private: private:
using HistoryTL = MTPpayments_GetStarsTransactions; using HistoryTL = MTPpayments_GetStarsTransactions;

View file

@ -8,12 +8,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_invite_links.h" #include "api/api_invite_links.h"
#include "api/api_chat_participants.h" #include "api/api_chat_participants.h"
#include "data/data_peer.h"
#include "data/data_user.h"
#include "data/data_chat.h"
#include "data/data_channel.h"
#include "data/data_session.h"
#include "data/data_changes.h" #include "data/data_changes.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "main/main_session.h" #include "main/main_session.h"
#include "base/unixtime.h" #include "base/unixtime.h"
#include "apiwrap.h" #include "apiwrap.h"
@ -69,59 +69,46 @@ JoinedByLinkSlice ParseJoinedByLinkSlice(
InviteLinks::InviteLinks(not_null<ApiWrap*> api) : _api(api) { InviteLinks::InviteLinks(not_null<ApiWrap*> api) : _api(api) {
} }
void InviteLinks::create( void InviteLinks::create(const CreateInviteLinkArgs &args) {
not_null<PeerData*> peer, performCreate(args, false);
Fn<void(Link)> done,
const QString &label,
TimeId expireDate,
int usageLimit,
bool requestApproval) {
performCreate(
peer,
std::move(done),
false,
label,
expireDate,
usageLimit,
requestApproval);
} }
void InviteLinks::performCreate( void InviteLinks::performCreate(
not_null<PeerData*> peer, const CreateInviteLinkArgs &args,
Fn<void(Link)> done, bool revokeLegacyPermanent) {
bool revokeLegacyPermanent, if (const auto i = _createCallbacks.find(args.peer)
const QString &label,
TimeId expireDate,
int usageLimit,
bool requestApproval) {
if (const auto i = _createCallbacks.find(peer)
; i != end(_createCallbacks)) { ; i != end(_createCallbacks)) {
if (done) { if (args.done) {
i->second.push_back(std::move(done)); i->second.push_back(std::move(args.done));
} }
return; return;
} }
auto &callbacks = _createCallbacks[peer]; auto &callbacks = _createCallbacks[args.peer];
if (done) { if (args.done) {
callbacks.push_back(std::move(done)); callbacks.push_back(std::move(args.done));
} }
const auto requestApproval = !args.subscription && args.requestApproval;
using Flag = MTPmessages_ExportChatInvite::Flag; using Flag = MTPmessages_ExportChatInvite::Flag;
_api->request(MTPmessages_ExportChatInvite( _api->request(MTPmessages_ExportChatInvite(
MTP_flags((revokeLegacyPermanent MTP_flags((revokeLegacyPermanent
? Flag::f_legacy_revoke_permanent ? Flag::f_legacy_revoke_permanent
: Flag(0)) : Flag(0))
| (!label.isEmpty() ? Flag::f_title : Flag(0)) | (!args.label.isEmpty() ? Flag::f_title : Flag(0))
| (expireDate ? Flag::f_expire_date : Flag(0)) | (args.expireDate ? Flag::f_expire_date : Flag(0))
| ((!requestApproval && usageLimit) | ((!requestApproval && args.usageLimit)
? Flag::f_usage_limit ? Flag::f_usage_limit
: Flag(0)) : Flag(0))
| (requestApproval ? Flag::f_request_needed : Flag(0))), | (requestApproval ? Flag::f_request_needed : Flag(0))
peer->input, | (args.subscription ? Flag::f_subscription_pricing : Flag(0))),
MTP_int(expireDate), args.peer->input,
MTP_int(usageLimit), MTP_int(args.expireDate),
MTP_string(label) MTP_int(args.usageLimit),
)).done([=](const MTPExportedChatInvite &result) { MTP_string(args.label),
MTP_starsSubscriptionPricing(
MTP_int(args.subscription.period),
MTP_long(args.subscription.credits))
)).done([=, peer = args.peer](const MTPExportedChatInvite &result) {
const auto callbacks = _createCallbacks.take(peer); const auto callbacks = _createCallbacks.take(peer);
const auto link = prepend(peer, peer->session().user(), result); const auto link = prepend(peer, peer->session().user(), result);
if (link && callbacks) { if (link && callbacks) {
@ -129,7 +116,7 @@ void InviteLinks::performCreate(
callback(*link); callback(*link);
} }
} }
}).fail([=] { }).fail([=, peer = args.peer] {
_createCallbacks.erase(peer); _createCallbacks.erase(peer);
}).send(); }).send();
} }
@ -238,6 +225,15 @@ void InviteLinks::edit(
requestApproval); requestApproval);
} }
void InviteLinks::editTitle(
not_null<PeerData*> peer,
not_null<UserData*> admin,
const QString &link,
const QString &label,
Fn<void(Link)> done) {
performEdit(peer, admin, link, done, false, label, 0, 0, false, true);
}
void InviteLinks::performEdit( void InviteLinks::performEdit(
not_null<PeerData*> peer, not_null<PeerData*> peer,
not_null<UserData*> admin, not_null<UserData*> admin,
@ -247,7 +243,8 @@ void InviteLinks::performEdit(
const QString &label, const QString &label,
TimeId expireDate, TimeId expireDate,
int usageLimit, int usageLimit,
bool requestApproval) { bool requestApproval,
bool editOnlyTitle) {
const auto key = LinkKey{ peer, link }; const auto key = LinkKey{ peer, link };
if (_deleteCallbacks.contains(key)) { if (_deleteCallbacks.contains(key)) {
return; return;
@ -272,7 +269,7 @@ void InviteLinks::performEdit(
? Flag::f_request_needed ? Flag::f_request_needed
: Flag(0)); : Flag(0));
_api->request(MTPmessages_EditExportedChatInvite( _api->request(MTPmessages_EditExportedChatInvite(
MTP_flags(flags), MTP_flags(editOnlyTitle ? Flag::f_title : flags),
peer->input, peer->input,
MTP_string(link), MTP_string(link),
MTP_int(expireDate), MTP_int(expireDate),
@ -344,7 +341,7 @@ void InviteLinks::revokePermanent(
} else if (!admin->isSelf()) { } else if (!admin->isSelf()) {
crl::on_main(&peer->session(), done); crl::on_main(&peer->session(), done);
} else { } else {
performCreate(peer, callback, true); performCreate({ peer, callback }, true);
} }
} }
@ -750,6 +747,12 @@ auto InviteLinks::parse(
return std::optional<Link>(Link{ return std::optional<Link>(Link{
.link = qs(data.vlink()), .link = qs(data.vlink()),
.label = qs(data.vtitle().value_or_empty()), .label = qs(data.vtitle().value_or_empty()),
.subscription = data.vsubscription_pricing()
? Data::PeerSubscription{
data.vsubscription_pricing()->data().vamount().v,
data.vsubscription_pricing()->data().vperiod().v,
}
: Data::PeerSubscription(),
.admin = peer->session().data().user(data.vadmin_id()), .admin = peer->session().data().user(data.vadmin_id()),
.date = data.vdate().v, .date = data.vdate().v,
.startDate = data.vstart_date().value_or_empty(), .startDate = data.vstart_date().value_or_empty(),

View file

@ -9,11 +9,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
class ApiWrap; class ApiWrap;
#include "data/data_subscriptions.h"
namespace Api { namespace Api {
struct InviteLink { struct InviteLink {
QString link; QString link;
QString label; QString label;
Data::PeerSubscription subscription;
not_null<UserData*> admin; not_null<UserData*> admin;
TimeId date = 0; TimeId date = 0;
TimeId startDate = 0; TimeId startDate = 0;
@ -53,6 +56,16 @@ struct InviteLinkUpdate {
not_null<PeerData*> peer, not_null<PeerData*> peer,
const MTPmessages_ChatInviteImporters &slice); const MTPmessages_ChatInviteImporters &slice);
struct CreateInviteLinkArgs {
not_null<PeerData*> peer;
Fn<void(InviteLink)> done = nullptr;
QString label;
TimeId expireDate = 0;
int usageLimit = 0;
bool requestApproval = false;
Data::PeerSubscription subscription;
};
class InviteLinks final { class InviteLinks final {
public: public:
explicit InviteLinks(not_null<ApiWrap*> api); explicit InviteLinks(not_null<ApiWrap*> api);
@ -61,13 +74,7 @@ public:
using Links = PeerInviteLinks; using Links = PeerInviteLinks;
using Update = InviteLinkUpdate; using Update = InviteLinkUpdate;
void create( void create(const CreateInviteLinkArgs &args);
not_null<PeerData*> peer,
Fn<void(Link)> done = nullptr,
const QString &label = QString(),
TimeId expireDate = 0,
int usageLimit = 0,
bool requestApproval = false);
void edit( void edit(
not_null<PeerData*> peer, not_null<PeerData*> peer,
not_null<UserData*> admin, not_null<UserData*> admin,
@ -77,6 +84,12 @@ public:
int usageLimit, int usageLimit,
bool requestApproval, bool requestApproval,
Fn<void(Link)> done = nullptr); Fn<void(Link)> done = nullptr);
void editTitle(
not_null<PeerData*> peer,
not_null<UserData*> admin,
const QString &link,
const QString &label,
Fn<void(Link)> done = nullptr);
void revoke( void revoke(
not_null<PeerData*> peer, not_null<PeerData*> peer,
not_null<UserData*> admin, not_null<UserData*> admin,
@ -187,15 +200,11 @@ private:
const QString &label = QString(), const QString &label = QString(),
TimeId expireDate = 0, TimeId expireDate = 0,
int usageLimit = 0, int usageLimit = 0,
bool requestApproval = false); bool requestApproval = false,
bool editOnlyTitle = false);
void performCreate( void performCreate(
not_null<PeerData*> peer, const CreateInviteLinkArgs &args,
Fn<void(Link)> done, bool revokeLegacyPermanent);
bool revokeLegacyPermanent,
const QString &label = QString(),
TimeId expireDate = 0,
int usageLimit = 0,
bool requestApproval = false);
void requestJoinedFirstSlice(LinkKey key); void requestJoinedFirstSlice(LinkKey key);
[[nodiscard]] std::optional<JoinedByLinkSlice> lookupJoinedFirstSlice( [[nodiscard]] std::optional<JoinedByLinkSlice> lookupJoinedFirstSlice(

View file

@ -39,9 +39,9 @@ namespace {
}; };
} }
[[nodiscard]] Data::SubscriptionOptions GiftCodesFromTL( [[nodiscard]] Data::PremiumSubscriptionOptions GiftCodesFromTL(
const QVector<MTPPremiumGiftCodeOption> &tlOptions) { const QVector<MTPPremiumGiftCodeOption> &tlOptions) {
auto options = SubscriptionOptionsFromTL(tlOptions); auto options = PremiumSubscriptionOptionsFromTL(tlOptions);
for (auto i = 0; i < options.size(); i++) { for (auto i = 0; i < options.size(); i++) {
const auto &tlOption = tlOptions[i].data(); const auto &tlOption = tlOptions[i].data();
const auto perUserText = Ui::FillAmountAndCurrency( const auto perUserText = Ui::FillAmountAndCurrency(
@ -143,7 +143,7 @@ void Premium::reloadPromo() {
const auto &data = result.data(); const auto &data = result.data();
_session->data().processUsers(data.vusers()); _session->data().processUsers(data.vusers());
_subscriptionOptions = SubscriptionOptionsFromTL( _subscriptionOptions = PremiumSubscriptionOptionsFromTL(
data.vperiod_options().v); data.vperiod_options().v);
for (const auto &option : data.vperiod_options().v) { for (const auto &option : data.vperiod_options().v) {
if (option.data().vmonths().v == 1) { if (option.data().vmonths().v == 1) {
@ -372,7 +372,7 @@ void Premium::resolveGiveawayInfo(
}).send(); }).send();
} }
const Data::SubscriptionOptions &Premium::subscriptionOptions() const { const Data::PremiumSubscriptionOptions &Premium::subscriptionOptions() const {
return _subscriptionOptions; return _subscriptionOptions;
} }
@ -547,7 +547,7 @@ Payments::InvoicePremiumGiftCode PremiumGiftCodeOptions::invoice(
}; };
} }
Data::SubscriptionOptions 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)) {
return it->second; return it->second;

View file

@ -7,7 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#pragma once #pragma once
#include "data/data_subscription_option.h" #include "data/data_premium_subscription_option.h"
#include "mtproto/sender.h" #include "mtproto/sender.h"
class History; class History;
@ -106,7 +106,7 @@ public:
Fn<void(GiveawayInfo)> done); Fn<void(GiveawayInfo)> done);
[[nodiscard]] auto subscriptionOptions() const [[nodiscard]] auto subscriptionOptions() const
-> const Data::SubscriptionOptions &; -> const Data::PremiumSubscriptionOptions &;
[[nodiscard]] rpl::producer<> somePremiumRequiredResolved() const; [[nodiscard]] rpl::producer<> somePremiumRequiredResolved() const;
void resolvePremiumRequired(not_null<UserData*> user); void resolvePremiumRequired(not_null<UserData*> user);
@ -156,7 +156,7 @@ private:
MsgId _giveawayInfoMessageId = 0; MsgId _giveawayInfoMessageId = 0;
Fn<void(GiveawayInfo)> _giveawayInfoDone; Fn<void(GiveawayInfo)> _giveawayInfoDone;
Data::SubscriptionOptions _subscriptionOptions; Data::PremiumSubscriptionOptions _subscriptionOptions;
rpl::event_stream<> _somePremiumRequiredResolved; rpl::event_stream<> _somePremiumRequiredResolved;
base::flat_set<not_null<UserData*>> _resolvePremiumRequiredUsers; base::flat_set<not_null<UserData*>> _resolvePremiumRequiredUsers;
@ -170,7 +170,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]] Data::SubscriptionOptions 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);
[[nodiscard]] Payments::InvoicePremiumGiftCode invoice( [[nodiscard]] Payments::InvoicePremiumGiftCode invoice(
@ -200,8 +200,9 @@ private:
int quantity = 0; int quantity = 0;
}; };
using Amount = int; using Amount = int;
using PremiumSubscriptionOptions = Data::PremiumSubscriptionOptions;
const not_null<PeerData*> _peer; const not_null<PeerData*> _peer;
base::flat_map<Amount, Data::SubscriptionOptions> _subscriptionOptions; base::flat_map<Amount, PremiumSubscriptionOptions> _subscriptionOptions;
struct { struct {
std::vector<int> months; std::vector<int> months;
std::vector<float64> totalCosts; std::vector<float64> totalCosts;

View file

@ -13,7 +13,7 @@ namespace Api {
constexpr auto kDiscountDivider = 1.; constexpr auto kDiscountDivider = 1.;
Data::SubscriptionOption CreateSubscriptionOption( Data::PremiumSubscriptionOption CreateSubscriptionOption(
int months, int months,
int monthlyAmount, int monthlyAmount,
int64 amount, int64 amount,

View file

@ -7,11 +7,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#pragma once #pragma once
#include "data/data_subscription_option.h" #include "data/data_premium_subscription_option.h"
namespace Api { namespace Api {
[[nodiscard]] Data::SubscriptionOption CreateSubscriptionOption( [[nodiscard]] Data::PremiumSubscriptionOption CreateSubscriptionOption(
int months, int months,
int monthlyAmount, int monthlyAmount,
int64 amount, int64 amount,
@ -19,22 +19,22 @@ namespace Api {
const QString &botUrl); const QString &botUrl);
template<typename Option> template<typename Option>
[[nodiscard]] Data::SubscriptionOptions SubscriptionOptionsFromTL( [[nodiscard]] auto PremiumSubscriptionOptionsFromTL(
const QVector<Option> &tlOptions) { const QVector<Option> &tlOpts) -> Data::PremiumSubscriptionOptions {
if (tlOptions.isEmpty()) { if (tlOpts.isEmpty()) {
return {}; return {};
} }
auto result = Data::SubscriptionOptions(); auto result = Data::PremiumSubscriptionOptions();
const auto monthlyAmount = [&] { const auto monthlyAmount = [&] {
const auto &min = ranges::min_element( const auto &min = ranges::min_element(
tlOptions, tlOpts,
ranges::less(), ranges::less(),
[](const Option &o) { return o.data().vamount().v; } [](const Option &o) { return o.data().vamount().v; }
)->data(); )->data();
return min.vamount().v / float64(min.vmonths().v); return min.vamount().v / float64(min.vmonths().v);
}(); }();
result.reserve(tlOptions.size()); result.reserve(tlOpts.size());
for (const auto &tlOption : tlOptions) { for (const auto &tlOption : tlOpts) {
const auto &option = tlOption.data(); const auto &option = tlOption.data();
auto botUrl = QString(); auto botUrl = QString();
if constexpr (!std::is_same_v<Option, MTPPremiumGiftCodeOption>) { if constexpr (!std::is_same_v<Option, MTPPremiumGiftCodeOption>) {

View file

@ -41,14 +41,14 @@ void InnerFillMessagePostFlags(
const SendOptions &options, const SendOptions &options,
not_null<PeerData*> peer, not_null<PeerData*> peer,
MessageFlags &flags) { MessageFlags &flags) {
const auto anonymousPost = peer->amAnonymous();
if (ShouldSendSilent(peer, options)) { if (ShouldSendSilent(peer, options)) {
flags |= MessageFlag::Silent; flags |= MessageFlag::Silent;
} }
if (!anonymousPost || options.sendAs) { if (!peer->amAnonymous()) {
flags |= MessageFlag::HasFromId; flags |= MessageFlag::HasFromId;
return; }
} else if (peer->asMegagroup()) { const auto channel = peer->asBroadcast();
if (!channel) {
return; return;
} }
flags |= MessageFlag::Post; flags |= MessageFlag::Post;
@ -57,7 +57,7 @@ void InnerFillMessagePostFlags(
return; return;
} }
flags |= MessageFlag::HasViews; flags |= MessageFlag::HasViews;
if (peer->asChannel()->addsSignature()) { if (channel->addsSignature()) {
flags |= MessageFlag::HasPostAuthor; flags |= MessageFlag::HasPostAuthor;
} }
} }
@ -165,25 +165,15 @@ void SendExistingMedia(
flags |= MessageFlag::HasReplyInfo; flags |= MessageFlag::HasReplyInfo;
sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to; sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to;
} }
const auto anonymousPost = peer->amAnonymous();
const auto silentPost = ShouldSendSilent(peer, action.options); const auto silentPost = ShouldSendSilent(peer, action.options);
InnerFillMessagePostFlags(action.options, peer, flags); InnerFillMessagePostFlags(action.options, peer, flags);
if (silentPost) { if (silentPost) {
sendFlags |= MTPmessages_SendMedia::Flag::f_silent; sendFlags |= MTPmessages_SendMedia::Flag::f_silent;
} }
const auto sendAs = action.options.sendAs; const auto sendAs = action.options.sendAs;
const auto messageFromId = sendAs
? sendAs->id
: anonymousPost
? 0
: session->userPeerId();
if (sendAs) { if (sendAs) {
sendFlags |= MTPmessages_SendMedia::Flag::f_send_as; sendFlags |= MTPmessages_SendMedia::Flag::f_send_as;
} }
const auto messagePostAuthor = peer->isBroadcast()
? session->user()->name()
: QString();
auto caption = TextWithEntities{ auto caption = TextWithEntities{
message.textWithTags.text, message.textWithTags.text,
TextUtilities::ConvertTextTagsToEntities(message.textWithTags.tags) TextUtilities::ConvertTextTagsToEntities(message.textWithTags.tags)
@ -219,11 +209,11 @@ void SendExistingMedia(
history->addNewLocalMessage({ history->addNewLocalMessage({
.id = newId.msg, .id = newId.msg,
.flags = flags, .flags = flags,
.from = messageFromId, .from = NewMessageFromId(action),
.replyTo = action.replyTo, .replyTo = action.replyTo,
.date = HistoryItem::NewMessageDate(action.options), .date = NewMessageDate(action.options),
.shortcutId = action.options.shortcutId, .shortcutId = action.options.shortcutId,
.postAuthor = messagePostAuthor, .postAuthor = NewMessagePostAuthor(action),
.effectId = action.options.effectId, .effectId = action.options.effectId,
}, media, caption); }, media, caption);
@ -361,25 +351,15 @@ bool SendDice(MessageToSend &message) {
flags |= MessageFlag::HasReplyInfo; flags |= MessageFlag::HasReplyInfo;
sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to; sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to;
} }
const auto anonymousPost = peer->amAnonymous();
const auto silentPost = ShouldSendSilent(peer, action.options); const auto silentPost = ShouldSendSilent(peer, action.options);
InnerFillMessagePostFlags(action.options, peer, flags); InnerFillMessagePostFlags(action.options, peer, flags);
if (silentPost) { if (silentPost) {
sendFlags |= MTPmessages_SendMedia::Flag::f_silent; sendFlags |= MTPmessages_SendMedia::Flag::f_silent;
} }
const auto sendAs = action.options.sendAs; const auto sendAs = action.options.sendAs;
const auto messageFromId = sendAs
? sendAs->id
: anonymousPost
? 0
: session->userPeerId();
if (sendAs) { if (sendAs) {
sendFlags |= MTPmessages_SendMedia::Flag::f_send_as; sendFlags |= MTPmessages_SendMedia::Flag::f_send_as;
} }
const auto messagePostAuthor = peer->isBroadcast()
? session->user()->name()
: QString();
if (action.options.scheduled) { if (action.options.scheduled) {
flags |= MessageFlag::IsOrWasScheduled; flags |= MessageFlag::IsOrWasScheduled;
sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date; sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date;
@ -401,11 +381,11 @@ bool SendDice(MessageToSend &message) {
history->addNewLocalMessage({ history->addNewLocalMessage({
.id = newId.msg, .id = newId.msg,
.flags = flags, .flags = flags,
.from = messageFromId, .from = NewMessageFromId(action),
.replyTo = action.replyTo, .replyTo = action.replyTo,
.date = HistoryItem::NewMessageDate(action.options), .date = NewMessageDate(action.options),
.shortcutId = action.options.shortcutId, .shortcutId = action.options.shortcutId,
.postAuthor = messagePostAuthor, .postAuthor = NewMessagePostAuthor(action),
.effectId = action.options.effectId, .effectId = action.options.effectId,
}, TextWithEntities(), MTP_messageMediaDice( }, TextWithEntities(), MTP_messageMediaDice(
MTP_int(0), MTP_int(0),
@ -529,7 +509,6 @@ void SendConfirmedFile(
if (file->to.replyTo) { if (file->to.replyTo) {
flags |= MessageFlag::HasReplyInfo; flags |= MessageFlag::HasReplyInfo;
} }
const auto anonymousPost = peer->amAnonymous();
FillMessagePostFlags(action, peer, flags); FillMessagePostFlags(action, peer, flags);
if (file->to.options.scheduled) { if (file->to.options.scheduled) {
flags |= MessageFlag::IsOrWasScheduled; flags |= MessageFlag::IsOrWasScheduled;
@ -551,16 +530,6 @@ void SendConfirmedFile(
if (file->to.options.invertCaption) { if (file->to.options.invertCaption) {
flags |= MessageFlag::InvertMedia; flags |= MessageFlag::InvertMedia;
} }
const auto messageFromId = file->to.options.sendAs
? file->to.options.sendAs->id
: anonymousPost
? PeerId()
: session->userPeerId();
const auto messagePostAuthor = peer->isBroadcast()
? session->user()->name()
: QString();
const auto media = MTPMessageMedia([&] { const auto media = MTPMessageMedia([&] {
if (file->type == SendMediaType::Photo) { if (file->type == SendMediaType::Photo) {
using Flag = MTPDmessageMediaPhoto::Flag; using Flag = MTPDmessageMediaPhoto::Flag;
@ -626,11 +595,11 @@ void SendConfirmedFile(
history->addNewLocalMessage({ history->addNewLocalMessage({
.id = newId.msg, .id = newId.msg,
.flags = flags, .flags = flags,
.from = messageFromId, .from = NewMessageFromId(action),
.replyTo = file->to.replyTo, .replyTo = file->to.replyTo,
.date = HistoryItem::NewMessageDate(file->to.options), .date = NewMessageDate(file->to.options),
.shortcutId = file->to.options.shortcutId, .shortcutId = file->to.options.shortcutId,
.postAuthor = messagePostAuthor, .postAuthor = NewMessagePostAuthor(action),
.groupedId = groupId, .groupedId = groupId,
.effectId = file->to.options.effectId, .effectId = file->to.options.effectId,
}, caption, media); }, caption, media);

View file

@ -14,7 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Api { namespace Api {
namespace { namespace {
constexpr auto kRefreshAppConfigTimeout = 3 * crl::time(1000); constexpr auto kRefreshAppConfigTimeout = crl::time(1);
} // namespace } // namespace
@ -24,19 +24,40 @@ SensitiveContent::SensitiveContent(not_null<ApiWrap*> api)
, _appConfigReloadTimer([=] { _session->appConfig().refresh(); }) { , _appConfigReloadTimer([=] { _session->appConfig().refresh(); }) {
} }
void SensitiveContent::reload() { void SensitiveContent::preload() {
if (_requestId) { if (!_loaded) {
reload();
}
}
void SensitiveContent::reload(bool force) {
if (_loadRequestId) {
if (force) {
_loadPending = true;
}
return; return;
} }
_requestId = _api.request(MTPaccount_GetContentSettings( _loaded = true;
_loadRequestId = _api.request(MTPaccount_GetContentSettings(
)).done([=](const MTPaccount_ContentSettings &result) { )).done([=](const MTPaccount_ContentSettings &result) {
_requestId = 0; _loadRequestId = 0;
result.match([&](const MTPDaccount_contentSettings &data) { const auto &data = result.data();
_enabled = data.is_sensitive_enabled(); const auto enabled = data.is_sensitive_enabled();
_canChange = data.is_sensitive_can_change(); const auto canChange = data.is_sensitive_can_change();
}); const auto changed = (_enabled.current() != enabled)
|| (_canChange.current() != canChange);
if (changed) {
_enabled = enabled;
_canChange = canChange;
}
if (base::take(_appConfigReloadForce) || changed) {
_appConfigReloadTimer.callOnce(kRefreshAppConfigTimeout);
}
if (base::take(_loadPending)) {
reload();
}
}).fail([=] { }).fail([=] {
_requestId = 0; _loadRequestId = 0;
}).send(); }).send();
} }
@ -57,17 +78,24 @@ void SensitiveContent::update(bool enabled) {
return; return;
} }
using Flag = MTPaccount_SetContentSettings::Flag; using Flag = MTPaccount_SetContentSettings::Flag;
_api.request(_requestId).cancel(); _api.request(_saveRequestId).cancel();
_requestId = _api.request(MTPaccount_SetContentSettings( if (const auto load = base::take(_loadRequestId)) {
_api.request(load).cancel();
_loadPending = true;
}
const auto finish = [=] {
_saveRequestId = 0;
if (base::take(_loadPending)) {
_appConfigReloadForce = true;
reload(true);
} else {
_appConfigReloadTimer.callOnce(kRefreshAppConfigTimeout);
}
};
_saveRequestId = _api.request(MTPaccount_SetContentSettings(
MTP_flags(enabled ? Flag::f_sensitive_enabled : Flag(0)) MTP_flags(enabled ? Flag::f_sensitive_enabled : Flag(0))
)).done([=] { )).done(finish).fail(finish).send();
_requestId = 0;
}).fail([=] {
_requestId = 0;
}).send();
_enabled = enabled; _enabled = enabled;
_appConfigReloadTimer.callOnce(kRefreshAppConfigTimeout);
} }
} // namespace Api } // namespace Api

View file

@ -22,7 +22,8 @@ class SensitiveContent final {
public: public:
explicit SensitiveContent(not_null<ApiWrap*> api); explicit SensitiveContent(not_null<ApiWrap*> api);
void reload(); void preload();
void reload(bool force = false);
void update(bool enabled); void update(bool enabled);
[[nodiscard]] bool enabledCurrent() const; [[nodiscard]] bool enabledCurrent() const;
@ -32,10 +33,14 @@ public:
private: private:
const not_null<Main::Session*> _session; const not_null<Main::Session*> _session;
MTP::Sender _api; MTP::Sender _api;
mtpRequestId _requestId = 0; mtpRequestId _loadRequestId = 0;
mtpRequestId _saveRequestId = 0;
rpl::variable<bool> _enabled = false; rpl::variable<bool> _enabled = false;
rpl::variable<bool> _canChange = false; rpl::variable<bool> _canChange = false;
base::Timer _appConfigReloadTimer; base::Timer _appConfigReloadTimer;
bool _appConfigReloadForce = false;
bool _loadPending = false;
bool _loaded = false;
}; };

View file

@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "mtproto/mtproto_config.h" #include "mtproto/mtproto_config.h"
#include "mtproto/mtproto_dc_options.h" #include "mtproto/mtproto_dc_options.h"
#include "data/business/data_shortcut_messages.h" #include "data/business/data_shortcut_messages.h"
#include "data/components/credits.h"
#include "data/components/scheduled_messages.h" #include "data/components/scheduled_messages.h"
#include "data/components/top_peers.h" #include "data/components/top_peers.h"
#include "data/notify/data_notify_settings.h" #include "data/notify/data_notify_settings.h"
@ -2626,7 +2627,7 @@ void Updates::feedUpdate(const MTPUpdate &update) {
case mtpc_updateStarsBalance: { case mtpc_updateStarsBalance: {
const auto &data = update.c_updateStarsBalance(); const auto &data = update.c_updateStarsBalance();
_session->setCredits(data.vbalance().v); _session->credits().apply(data);
} break; } break;
} }

View file

@ -3326,9 +3326,8 @@ void ApiWrap::forwardMessages(
if (!action.options.scheduled && !action.options.shortcutId) { if (!action.options.scheduled && !action.options.shortcutId) {
histories.readInbox(history); histories.readInbox(history);
} }
const auto anonymousPost = peer->amAnonymous();
const auto silentPost = ShouldSendSilent(peer, action.options);
const auto sendAs = action.options.sendAs; const auto sendAs = action.options.sendAs;
const auto silentPost = ShouldSendSilent(peer, action.options);
using SendFlag = MTPmessages_ForwardMessages::Flag; using SendFlag = MTPmessages_ForwardMessages::Flag;
auto flags = MessageFlags(); auto flags = MessageFlags();
@ -3426,23 +3425,14 @@ void ApiWrap::forwardMessages(
const auto newId = FullMsgId( const auto newId = FullMsgId(
peer->id, peer->id,
_session->data().nextLocalMessageId()); _session->data().nextLocalMessageId());
const auto self = _session->user();
const auto messageFromId = sendAs
? sendAs->id
: anonymousPost
? PeerId(0)
: self->id;
const auto messagePostAuthor = peer->isBroadcast()
? self->name()
: QString();
history->addNewLocalMessage({ history->addNewLocalMessage({
.id = newId.msg, .id = newId.msg,
.flags = flags, .flags = flags,
.from = messageFromId, .from = NewMessageFromId(action),
.replyTo = { .topicRootId = topMsgId }, .replyTo = { .topicRootId = topMsgId },
.date = HistoryItem::NewMessageDate(action.options), .date = NewMessageDate(action.options),
.shortcutId = action.options.shortcutId, .shortcutId = action.options.shortcutId,
.postAuthor = messagePostAuthor, .postAuthor = NewMessagePostAuthor(action),
// forwarded messages don't have effects // forwarded messages don't have effects
//.effectId = action.options.effectId, //.effectId = action.options.effectId,
@ -3517,8 +3507,6 @@ void ApiWrap::sendSharedContact(
const auto newId = FullMsgId( const auto newId = FullMsgId(
peer->id, peer->id,
_session->data().nextLocalMessageId()); _session->data().nextLocalMessageId());
const auto anonymousPost = peer->amAnonymous();
auto flags = NewMessageFlags(peer); auto flags = NewMessageFlags(peer);
if (action.replyTo) { if (action.replyTo) {
flags |= MessageFlag::HasReplyInfo; flags |= MessageFlag::HasReplyInfo;
@ -3530,22 +3518,14 @@ void ApiWrap::sendSharedContact(
if (action.options.shortcutId) { if (action.options.shortcutId) {
flags |= MessageFlag::ShortcutMessage; flags |= MessageFlag::ShortcutMessage;
} }
const auto messageFromId = action.options.sendAs
? action.options.sendAs->id
: anonymousPost
? PeerId()
: _session->userPeerId();
const auto messagePostAuthor = peer->isBroadcast()
? _session->user()->name()
: QString();
const auto item = history->addNewLocalMessage({ const auto item = history->addNewLocalMessage({
.id = newId.msg, .id = newId.msg,
.flags = flags, .flags = flags,
.from = messageFromId, .from = NewMessageFromId(action),
.replyTo = action.replyTo, .replyTo = action.replyTo,
.date = HistoryItem::NewMessageDate(action.options), .date = NewMessageDate(action.options),
.shortcutId = action.options.shortcutId, .shortcutId = action.options.shortcutId,
.postAuthor = messagePostAuthor, .postAuthor = NewMessagePostAuthor(action),
.effectId = action.options.effectId, .effectId = action.options.effectId,
}, TextWithEntities(), MTP_messageMediaContact( }, TextWithEntities(), MTP_messageMediaContact(
MTP_string(phone), MTP_string(phone),
@ -3846,7 +3826,6 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
MTP_string(fields.url), MTP_string(fields.url),
MTP_int(page->pendingTill))); MTP_int(page->pendingTill)));
} }
const auto anonymousPost = peer->amAnonymous();
const auto silentPost = ShouldSendSilent(peer, action.options); const auto silentPost = ShouldSendSilent(peer, action.options);
FillMessagePostFlags(action, peer, flags); FillMessagePostFlags(action, peer, flags);
if ((exactWebPage && !ignoreWebPage && message.webPage.invert) if ((exactWebPage && !ignoreWebPage && message.webPage.invert)
@ -3874,18 +3853,10 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
history->startSavingCloudDraft(draftTopicRootId); history->startSavingCloudDraft(draftTopicRootId);
} }
const auto sendAs = action.options.sendAs; const auto sendAs = action.options.sendAs;
const auto messageFromId = sendAs
? sendAs->id
: anonymousPost
? PeerId()
: _session->userPeerId();
if (sendAs) { if (sendAs) {
sendFlags |= MTPmessages_SendMessage::Flag::f_send_as; sendFlags |= MTPmessages_SendMessage::Flag::f_send_as;
mediaFlags |= MTPmessages_SendMedia::Flag::f_send_as; mediaFlags |= MTPmessages_SendMedia::Flag::f_send_as;
} }
const auto messagePostAuthor = peer->isBroadcast()
? _session->user()->name()
: QString();
if (action.options.scheduled) { if (action.options.scheduled) {
flags |= MessageFlag::IsOrWasScheduled; flags |= MessageFlag::IsOrWasScheduled;
sendFlags |= MTPmessages_SendMessage::Flag::f_schedule_date; sendFlags |= MTPmessages_SendMessage::Flag::f_schedule_date;
@ -3903,11 +3874,11 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
lastMessage = history->addNewLocalMessage({ lastMessage = history->addNewLocalMessage({
.id = newId.msg, .id = newId.msg,
.flags = flags, .flags = flags,
.from = messageFromId, .from = NewMessageFromId(action),
.replyTo = action.replyTo, .replyTo = action.replyTo,
.date = HistoryItem::NewMessageDate(action.options), .date = NewMessageDate(action.options),
.shortcutId = action.options.shortcutId, .shortcutId = action.options.shortcutId,
.postAuthor = messagePostAuthor, .postAuthor = NewMessagePostAuthor(action),
.effectId = action.options.effectId, .effectId = action.options.effectId,
}, sending, media); }, sending, media);
const auto done = [=]( const auto done = [=](
@ -4057,7 +4028,6 @@ void ApiWrap::sendInlineResult(
flags |= MessageFlag::HasReplyInfo; flags |= MessageFlag::HasReplyInfo;
sendFlags |= SendFlag::f_reply_to; sendFlags |= SendFlag::f_reply_to;
} }
const auto anonymousPost = peer->amAnonymous();
const auto silentPost = ShouldSendSilent(peer, action.options); const auto silentPost = ShouldSendSilent(peer, action.options);
FillMessagePostFlags(action, peer, flags); FillMessagePostFlags(action, peer, flags);
if (silentPost) { if (silentPost) {
@ -4076,30 +4046,22 @@ void ApiWrap::sendInlineResult(
} }
const auto sendAs = action.options.sendAs; const auto sendAs = action.options.sendAs;
const auto messageFromId = sendAs
? sendAs->id
: anonymousPost ? PeerId()
: _session->userPeerId();
if (sendAs) { if (sendAs) {
sendFlags |= MTPmessages_SendInlineBotResult::Flag::f_send_as; sendFlags |= MTPmessages_SendInlineBotResult::Flag::f_send_as;
} }
const auto messagePostAuthor = peer->isBroadcast()
? _session->user()->name()
: QString();
_session->data().registerMessageRandomId(randomId, newId); _session->data().registerMessageRandomId(randomId, newId);
data->addToHistory(history, { data->addToHistory(history, {
.id = newId.msg, .id = newId.msg,
.flags = flags, .flags = flags,
.from = messageFromId, .from = NewMessageFromId(action),
.replyTo = action.replyTo, .replyTo = action.replyTo,
.date = HistoryItem::NewMessageDate(action.options), .date = NewMessageDate(action.options),
.shortcutId = action.options.shortcutId, .shortcutId = action.options.shortcutId,
.viaBotId = ((bot && !action.options.hideViaBot) .viaBotId = ((bot && !action.options.hideViaBot)
? peerToUser(bot->id) ? peerToUser(bot->id)
: UserId()), : UserId()),
.postAuthor = messagePostAuthor, .postAuthor = NewMessagePostAuthor(action),
}); });
history->clearCloudDraft(topicRootId); history->clearCloudDraft(topicRootId);

View file

@ -898,9 +898,10 @@ void GroupInfoBox::checkInviteLink() {
channelReady(); channelReady();
} else if (_createdChannel->isFullLoaded() && !_creatingInviteLink) { } else if (_createdChannel->isFullLoaded() && !_creatingInviteLink) {
_creatingInviteLink = true; _creatingInviteLink = true;
_createdChannel->session().api().inviteLinks().create( _createdChannel->session().api().inviteLinks().create({
_createdChannel, _createdChannel,
crl::guard(this, [=](auto&&) { channelReady(); })); crl::guard(this, [=](auto&&) { channelReady(); }),
});
} else { } else {
_createdChannel->session().changes().peerUpdates( _createdChannel->session().changes().peerUpdates(
_createdChannel, _createdChannel,
@ -1243,7 +1244,7 @@ void SetupChannelBox::mousePressEvent(QMouseEvent *e) {
showToast(tr::lng_create_channel_link_copied(tr::now)); showToast(tr::lng_create_channel_link_copied(tr::now));
} else if (_channel->isFullLoaded() && !_creatingInviteLink) { } else if (_channel->isFullLoaded() && !_creatingInviteLink) {
_creatingInviteLink = true; _creatingInviteLink = true;
_channel->session().api().inviteLinks().create(_channel); _channel->session().api().inviteLinks().create({ _channel });
} }
} }

View file

@ -25,7 +25,7 @@ DownloadPathBox::DownloadPathBox(
, _path(Core::App().settings().downloadPath()) , _path(Core::App().settings().downloadPath())
, _pathBookmark(Core::App().settings().downloadPathBookmark()) , _pathBookmark(Core::App().settings().downloadPathBookmark())
, _group(std::make_shared<Ui::RadioenumGroup<Directory>>(typeFromPath(_path))) , _group(std::make_shared<Ui::RadioenumGroup<Directory>>(typeFromPath(_path)))
, _default(Core::App().canReadDefaultDownloadPath(true) , _default(Core::App().canReadDefaultDownloadPath()
? object_ptr<Ui::Radioenum<Directory>>( ? object_ptr<Ui::Radioenum<Directory>>(
this, this,
_group, _group,
@ -149,7 +149,7 @@ void DownloadPathBox::setPathText(const QString &text) {
DownloadPathBox::Directory DownloadPathBox::typeFromPath( DownloadPathBox::Directory DownloadPathBox::typeFromPath(
const QString &path) { const QString &path) {
if (path.isEmpty()) { if (path.isEmpty()) {
return Core::App().canReadDefaultDownloadPath(true) return Core::App().canReadDefaultDownloadPath()
? Directory::Downloads ? Directory::Downloads
: Directory::Temp; : Directory::Temp;
} else if (path == FileDialog::Tmp()) { } else if (path == FileDialog::Tmp()) {

View file

@ -763,9 +763,6 @@ void EditMessagesPrivacyBox(
lt_link, lt_link,
link, link,
Ui::Text::WithEntities), Ui::Text::WithEntities),
.st = &st::defaultMultilineToast,
.duration = Ui::Toast::kDefaultDuration * 2,
.multiline = true,
.filter = crl::guard(&controller->session(), [=]( .filter = crl::guard(&controller->session(), [=](
const ClickHandlerPtr &, const ClickHandlerPtr &,
Qt::MouseButton button) { Qt::MouseButton button) {

View file

@ -39,6 +39,7 @@ void GiftCreditsBox(
not_null<Ui::GenericBox*> box, not_null<Ui::GenericBox*> box,
not_null<PeerData*> peer, not_null<PeerData*> peer,
Fn<void()> gifted) { Fn<void()> gifted) {
box->setWidth(st::boxWideWidth);
box->setStyle(st::creditsGiftBox); box->setStyle(st::creditsGiftBox);
box->setNoContentMargin(true); box->setNoContentMargin(true);
box->addButton(tr::lng_create_group_back(), [=] { box->closeBox(); }); box->addButton(tr::lng_create_group_back(), [=] { box->closeBox(); });

View file

@ -23,7 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_media_types.h" // Data::GiveawayStart. #include "data/data_media_types.h" // Data::GiveawayStart.
#include "data/data_peer_values.h" // Data::PeerPremiumValue. #include "data/data_peer_values.h" // Data::PeerPremiumValue.
#include "data/data_session.h" #include "data/data_session.h"
#include "data/data_subscription_option.h" #include "data/data_premium_subscription_option.h"
#include "data/data_user.h" #include "data/data_user.h"
#include "data/stickers/data_custom_emoji.h" #include "data/stickers/data_custom_emoji.h"
#include "info/channel_statistics/boosts/giveaway/boost_badge.h" // InfiniteRadialAnimationWidget. #include "info/channel_statistics/boosts/giveaway/boost_badge.h" // InfiniteRadialAnimationWidget.
@ -66,8 +66,8 @@ namespace {
constexpr auto kUserpicsMax = size_t(3); constexpr auto kUserpicsMax = size_t(3);
using GiftOption = Data::SubscriptionOption; using GiftOption = Data::PremiumSubscriptionOption;
using GiftOptions = Data::SubscriptionOptions; using GiftOptions = Data::PremiumSubscriptionOptions;
GiftOptions GiftOptionFromTL(const MTPDuserFull &data) { GiftOptions GiftOptionFromTL(const MTPDuserFull &data) {
auto result = GiftOptions(); auto result = GiftOptions();
@ -75,7 +75,7 @@ GiftOptions GiftOptionFromTL(const MTPDuserFull &data) {
if (!gifts) { if (!gifts) {
return result; return result;
} }
result = Api::SubscriptionOptionsFromTL(gifts->v); result = Api::PremiumSubscriptionOptionsFromTL(gifts->v);
for (auto &option : result) { for (auto &option : result) {
option.costPerMonth = tr::lng_premium_gift_per( option.costPerMonth = tr::lng_premium_gift_per(
tr::now, tr::now,
@ -932,8 +932,8 @@ void ShowAlreadyPremiumToast(
Ui::Text::Link( Ui::Text::Link(
Ui::Text::Bold(tr::lng_gift_link_already_link(tr::now))), Ui::Text::Bold(tr::lng_gift_link_already_link(tr::now))),
Ui::Text::WithEntities), Ui::Text::WithEntities),
.duration = 6 * crl::time(1000),
.filter = crl::guard(navigation, shareLink), .filter = crl::guard(navigation, shareLink),
.duration = 6 * crl::time(1000),
}); });
} }
@ -1642,6 +1642,9 @@ void AddCreditsHistoryEntryTable(
not_null<Window::SessionNavigation*> controller, not_null<Window::SessionNavigation*> controller,
not_null<Ui::VerticalLayout*> container, not_null<Ui::VerticalLayout*> container,
const Data::CreditsHistoryEntry &entry) { const Data::CreditsHistoryEntry &entry) {
if (!entry) {
return;
}
auto table = container->add( auto table = container->add(
object_ptr<Ui::TableLayout>( object_ptr<Ui::TableLayout>(
container, container,
@ -1757,3 +1760,56 @@ void AddCreditsHistoryEntryTable(
Ui::Text::Link(entry.successLink, entry.successLink))); Ui::Text::Link(entry.successLink, entry.successLink)));
} }
} }
void AddSubscriptionEntryTable(
not_null<Window::SessionNavigation*> controller,
not_null<Ui::VerticalLayout*> container,
const Data::SubscriptionEntry &s) {
if (!s) {
return;
}
auto table = container->add(
object_ptr<Ui::TableLayout>(
container,
st::giveawayGiftCodeTable),
st::giveawayGiftCodeTableMargin);
const auto peerId = PeerId(s.barePeerId);
AddTableRow(
table,
tr::lng_credits_subscription_row_to(),
controller,
peerId);
if (!s.until.isNull()) {
AddTableRow(
table,
s.expired
? tr::lng_credits_subscription_row_next_none()
: s.cancelled
? tr::lng_credits_subscription_row_next_off()
: tr::lng_credits_subscription_row_next_on(),
rpl::single(Ui::Text::WithEntities(langDateTime(s.until))));
}
}
void AddSubscriberEntryTable(
not_null<Window::SessionNavigation*> controller,
not_null<Ui::VerticalLayout*> container,
not_null<PeerData*> peer,
TimeId date) {
auto table = container->add(
object_ptr<Ui::TableLayout>(
container,
st::giveawayGiftCodeTable),
st::giveawayGiftCodeTableMargin);
AddTableRow(
table,
tr::lng_group_invite_joined_row_subscriber(),
controller,
peer->id);
if (const auto d = base::unixtime::parse(date); !d.isNull()) {
AddTableRow(
table,
tr::lng_group_invite_joined_row_date(),
rpl::single(Ui::Text::WithEntities(langDateTime(d))));
}
}

View file

@ -19,6 +19,7 @@ namespace Data {
struct CreditsHistoryEntry; struct CreditsHistoryEntry;
struct GiveawayStart; struct GiveawayStart;
struct GiveawayResults; struct GiveawayResults;
struct SubscriptionEntry;
} // namespace Data } // namespace Data
namespace Ui { namespace Ui {
@ -78,3 +79,13 @@ void AddCreditsHistoryEntryTable(
not_null<Window::SessionNavigation*> controller, not_null<Window::SessionNavigation*> controller,
not_null<Ui::VerticalLayout*> container, not_null<Ui::VerticalLayout*> container,
const Data::CreditsHistoryEntry &entry); const Data::CreditsHistoryEntry &entry);
void AddSubscriptionEntryTable(
not_null<Window::SessionNavigation*> controller,
not_null<Ui::VerticalLayout*> container,
const Data::SubscriptionEntry &s);
void AddSubscriberEntryTable(
not_null<Window::SessionNavigation*> controller,
not_null<Ui::VerticalLayout*> container,
not_null<PeerData*> peer,
TimeId date);

View file

@ -94,7 +94,7 @@ void MaxInviteBox::mousePressEvent(QMouseEvent *e) {
showToast(tr::lng_create_channel_link_copied(tr::now)); showToast(tr::lng_create_channel_link_copied(tr::now));
} else if (_channel->isFullLoaded() && !_creatingInviteLink) { } else if (_channel->isFullLoaded() && !_creatingInviteLink) {
_creatingInviteLink = true; _creatingInviteLink = true;
_channel->session().api().inviteLinks().create(_channel); _channel->session().api().inviteLinks().create({ _channel });
} }
} }
} }

View file

@ -517,10 +517,13 @@ void InviteForbiddenController::send(
}; };
const auto sendForFull = [=] { const auto sendForFull = [=] {
if (!sendLink()) { if (!sendLink()) {
_peer->session().api().inviteLinks().create(_peer, [=](auto) { _peer->session().api().inviteLinks().create({
if (!sendLink()) { _peer,
close(); [=](auto) {
} if (!sendLink()) {
close();
}
},
}); });
} }
}; };

View file

@ -318,6 +318,7 @@ private:
std::optional<bool> hiddenPreHistory; std::optional<bool> hiddenPreHistory;
std::optional<bool> forum; std::optional<bool> forum;
std::optional<bool> signatures; std::optional<bool> signatures;
std::optional<bool> signatureProfiles;
std::optional<bool> noForwards; std::optional<bool> noForwards;
std::optional<bool> joinToWrite; std::optional<bool> joinToWrite;
std::optional<bool> requestToJoin; std::optional<bool> requestToJoin;
@ -408,6 +409,7 @@ private:
std::optional<EditPeerTypeData> _typeDataSavedValue; std::optional<EditPeerTypeData> _typeDataSavedValue;
std::optional<bool> _forumSavedValue; std::optional<bool> _forumSavedValue;
std::optional<bool> _signaturesSavedValue; std::optional<bool> _signaturesSavedValue;
std::optional<bool> _signatureProfilesSavedValue;
const not_null<Window::SessionNavigation*> _navigation; const not_null<Window::SessionNavigation*> _navigation;
const not_null<Ui::BoxContent*> _box; const not_null<Ui::BoxContent*> _box;
@ -620,13 +622,17 @@ object_ptr<Ui::RpWidget> Controller::createTitleEdit() {
local.x() + emojiToggle->width() * 3); local.x() + emojiToggle->width() * 3);
}; };
base::install_event_filter(container, [=](not_null<QEvent*> event) {
const auto type = event->type(); field->lifetime().make_state<base::unique_qptr<QObject>>([&] {
if (type == QEvent::Move || type == QEvent::Resize) { return base::install_event_filter(container, [=](
crl::on_main(field, [=] { updateEmojiPanelGeometry(); }); not_null<QEvent*> event) {
} const auto type = event->type();
return base::EventFilterResult::Continue; if (type == QEvent::Move || type == QEvent::Resize) {
}); crl::on_main(field, [=] { updateEmojiPanelGeometry(); });
}
return base::EventFilterResult::Continue;
});
}());
field->widthValue() | rpl::start_with_next([=](int width) { field->widthValue() | rpl::start_with_next([=](int width) {
const auto &p = st::editPeerTitleEmojiPosition; const auto &p = st::editPeerTitleEmojiPosition;
@ -1051,17 +1057,50 @@ void Controller::fillSignaturesButton() {
return; return;
} }
AddButtonWithText( const auto signs = AddButtonWithText(
_controls.buttonsLayout, _controls.buttonsLayout,
tr::lng_edit_sign_messages(), tr::lng_edit_sign_messages(),
rpl::single(QString()), rpl::single(QString()),
[] {}, [] {},
{ &st::menuIconSigned } { &st::menuIconSigned }
)->toggleOn(rpl::single(channel->addsSignature()) )->toggleOn(rpl::single(channel->addsSignature()));
)->toggledValue(
const auto profiles = _controls.buttonsLayout->add(
object_ptr<Ui::SlideWrap<Ui::SettingsButton>>(
_controls.buttonsLayout,
EditPeerInfoBox::CreateButton(
_controls.buttonsLayout,
tr::lng_edit_sign_profiles(),
rpl::single(QString()),
[] {},
st::manageGroupTopButtonWithText,
{ &st::menuIconProfile })));
profiles->toggleOn(signs->toggledValue());
profiles->finishAnimating();
profiles->entity()->toggleOn(rpl::single(
channel->addsSignature() && channel->signatureProfiles()
))->toggledValue(
) | rpl::start_with_next([=](bool toggled) {
_signatureProfilesSavedValue = toggled;
}, profiles->entity()->lifetime());
signs->toggledValue(
) | rpl::start_with_next([=](bool toggled) { ) | rpl::start_with_next([=](bool toggled) {
_signaturesSavedValue = toggled; _signaturesSavedValue = toggled;
if (!toggled) {
_signatureProfilesSavedValue = false;
}
}, _controls.buttonsLayout->lifetime()); }, _controls.buttonsLayout->lifetime());
Ui::AddSkip(_controls.buttonsLayout);
Ui::AddDividerText(
_controls.buttonsLayout,
rpl::conditional(
signs->toggledValue(),
tr::lng_edit_sign_profiles_about(Ui::Text::WithEntities),
tr::lng_edit_sign_messages_about(Ui::Text::WithEntities)));
Ui::AddSkip(_controls.buttonsLayout);
} }
void Controller::fillHistoryVisibilityButton() { void Controller::fillHistoryVisibilityButton() {
@ -1219,11 +1258,9 @@ void Controller::fillManageSection() {
} }
if (canEditSignatures) { if (canEditSignatures) {
fillSignaturesButton(); fillSignaturesButton();
} } else if (canEditPreHistoryHidden
if (canEditPreHistoryHidden
|| canEditForum || canEditForum
|| canEditColorIndex || canEditColorIndex
|| canEditSignatures
//|| canEditInviteLinks //|| canEditInviteLinks
|| canViewOrEditLinkedChat || canViewOrEditLinkedChat
|| canEditType) { || canEditType) {
@ -1779,10 +1816,14 @@ bool Controller::validateForum(Saving &to) const {
} }
bool Controller::validateSignatures(Saving &to) const { bool Controller::validateSignatures(Saving &to) const {
Expects(_signaturesSavedValue.has_value()
== _signatureProfilesSavedValue.has_value());
if (!_signaturesSavedValue.has_value()) { if (!_signaturesSavedValue.has_value()) {
return true; return true;
} }
to.signatures = _signaturesSavedValue; to.signatures = _signaturesSavedValue;
to.signatureProfiles = _signatureProfilesSavedValue;
return true; return true;
} }
@ -2215,15 +2256,27 @@ void Controller::saveForum() {
} }
void Controller::saveSignatures() { void Controller::saveSignatures() {
Expects(_savingData.signatures.has_value()
== _savingData.signatureProfiles.has_value());
const auto channel = _peer->asChannel(); const auto channel = _peer->asChannel();
if (!_savingData.signatures if (!_savingData.signatures
|| !channel || !channel
|| *_savingData.signatures == channel->addsSignature()) { || ((*_savingData.signatures == channel->addsSignature())
&& (*_savingData.signatureProfiles
== channel->signatureProfiles()))) {
return continueSave(); return continueSave();
} }
using Flag = MTPchannels_ToggleSignatures::Flag;
_api.request(MTPchannels_ToggleSignatures( _api.request(MTPchannels_ToggleSignatures(
channel->inputChannel, MTP_flags(Flag()
MTP_bool(*_savingData.signatures) | (*_savingData.signatures
? Flag::f_signatures_enabled
: Flag())
| (*_savingData.signatureProfiles
? Flag::f_profiles_enabled
: Flag())),
channel->inputChannel
)).done([=](const MTPUpdates &result) { )).done([=](const MTPUpdates &result) {
channel->session().api().applyUpdates(result); channel->session().api().applyUpdates(result);
continueSave(); continueSave();

View file

@ -7,49 +7,58 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#include "boxes/peers/edit_peer_invite_link.h" #include "boxes/peers/edit_peer_invite_link.h"
#include "core/application.h"
#include "data/data_peer.h"
#include "data/data_user.h"
#include "data/data_channel.h"
#include "data/data_changes.h"
#include "data/data_session.h"
#include "data/data_histories.h"
#include "main/main_session.h"
#include "api/api_invite_links.h" #include "api/api_invite_links.h"
#include "base/unixtime.h"
#include "apiwrap.h" #include "apiwrap.h"
#include "base/unixtime.h"
#include "boxes/gift_premium_box.h"
#include "boxes/peer_list_box.h"
#include "boxes/share_box.h"
#include "core/application.h"
#include "core/ui_integration.h" // Core::MarkedTextContext.
#include "data/components/credits.h"
#include "data/data_changes.h"
#include "data/data_channel.h"
#include "data/data_histories.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "data/stickers/data_custom_emoji.h"
#include "history/history.h"
#include "history/history_item_helpers.h" // GetErrorTextForSending.
#include "history/view/history_view_group_call_bar.h" // GenerateUserpics...
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "qr/qr_generate.h"
#include "settings/settings_credits_graphics.h"
#include "ui/boxes/confirm_box.h"
#include "ui/boxes/edit_invite_link.h"
#include "ui/boxes/edit_invite_link_session.h"
#include "ui/controls/invite_link_buttons.h" #include "ui/controls/invite_link_buttons.h"
#include "ui/controls/invite_link_label.h" #include "ui/controls/invite_link_label.h"
#include "ui/wrap/vertical_layout.h" #include "ui/controls/userpic_button.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/wrap/padding_wrap.h"
#include "ui/widgets/popup_menu.h"
#include "ui/abstract_button.h"
#include "ui/toast/toast.h"
#include "ui/text/text_utilities.h"
#include "ui/boxes/edit_invite_link.h"
#include "ui/painter.h" #include "ui/painter.h"
#include "ui/rect.h"
#include "ui/text/format_values.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
#include "ui/vertical_list.h" #include "ui/vertical_list.h"
#include "boxes/share_box.h" #include "ui/widgets/popup_menu.h"
#include "history/view/history_view_group_call_bar.h" // GenerateUserpics... #include "ui/wrap/padding_wrap.h"
#include "history/history_item_helpers.h" // GetErrorTextForSending. #include "ui/wrap/slide_wrap.h"
#include "history/history.h" #include "ui/wrap/vertical_layout.h"
#include "ui/boxes/confirm_box.h"
#include "boxes/peer_list_box.h"
#include "mainwindow.h"
#include "lang/lang_keys.h"
#include "window/window_session_controller.h"
#include "window/window_controller.h" #include "window/window_controller.h"
#include "mtproto/sender.h" #include "window/window_session_controller.h"
#include "qr/qr_generate.h"
#include "intro/intro_qr.h" // TelegramLogoImage
#include "styles/style_boxes.h" #include "styles/style_boxes.h"
#include "styles/style_layers.h" // st::boxDividerLabel. #include "styles/style_credits.h"
#include "styles/style_giveaway.h"
#include "styles/style_info.h" #include "styles/style_info.h"
#include "styles/style_layers.h" // st::boxDividerLabel.
#include "styles/style_menu_icons.h" #include "styles/style_menu_icons.h"
#include "styles/style_premium.h"
#include <QtGui/QGuiApplication>
#include <QtCore/QMimeData> #include <QtCore/QMimeData>
#include <QtGui/QGuiApplication>
#include <QtSvg/QSvgRenderer>
namespace { namespace {
@ -72,6 +81,66 @@ void ShowPeerInfoSync(not_null<PeerData*> peer) {
} }
} }
class SubscriptionRow final : public PeerListRow {
public:
SubscriptionRow(
not_null<PeerData*> peer,
TimeId date,
Data::PeerSubscription subscription);
QSize rightActionSize() const override;
QMargins rightActionMargins() const override;
void rightActionPaint(
Painter &p,
int x,
int y,
int outerWidth,
bool selected,
bool actionSelected) override;
private:
std::optional<Settings::SubscriptionRightLabel> _rightLabel;
};
SubscriptionRow::SubscriptionRow(
not_null<PeerData*> peer,
TimeId date,
Data::PeerSubscription subscription)
: PeerListRow(peer) {
if (subscription) {
_rightLabel = Settings::PaintSubscriptionRightLabelCallback(
&peer->session(),
st::peerListBoxItem,
subscription.credits);
}
setCustomStatus(
tr::lng_group_invite_joined_status(
tr::now,
lt_date,
langDayOfMonthFull(base::unixtime::parse(date).date())));
}
QSize SubscriptionRow::rightActionSize() const {
return _rightLabel ? _rightLabel->size : QSize();
}
QMargins SubscriptionRow::rightActionMargins() const {
return QMargins(0, 0, st::boxRowPadding.right(), 0);
}
void SubscriptionRow::rightActionPaint(
Painter &p,
int x,
int y,
int outerWidth,
bool selected,
bool actionSelected) {
if (_rightLabel) {
return _rightLabel->draw(p, x, y, st::peerListBoxItem.height);
}
}
class RequestedRow final : public PeerListRow { class RequestedRow final : public PeerListRow {
public: public:
RequestedRow(not_null<PeerData*> peer, TimeId date); RequestedRow(not_null<PeerData*> peer, TimeId date);
@ -604,6 +673,101 @@ void Controller::setupAboveJoinedWidget() {
if (revoked || !current.permanent) { if (revoked || !current.permanent) {
addHeaderBlock(container); addHeaderBlock(container);
} }
if (current.subscription) {
const auto &st = st::peerListSingleRow.item;
Ui::AddSubsectionTitle(
container,
tr::lng_group_invite_subscription_info_subtitle());
const auto widget = container->add(
CreateSkipWidget(container, st.height));
const auto name = widget->lifetime().make_state<Ui::Text::String>();
auto userpic = QImage(
Size(st.photoSize) * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
{
constexpr auto kGreenIndex = 3;
const auto colors = Ui::EmptyUserpic::UserpicColor(kGreenIndex);
auto emptyUserpic = Ui::EmptyUserpic(colors, {});
userpic.setDevicePixelRatio(style::DevicePixelRatio());
userpic.fill(Qt::transparent);
auto p = QPainter(&userpic);
emptyUserpic.paintCircle(p, 0, 0, st.photoSize, st.photoSize);
auto svg = QSvgRenderer(u":/gui/links_subscription.svg"_q);
const auto size = st.photoSize / 4. * 3.;
const auto r = QRectF(
(st.photoSize - size) / 2.,
(st.photoSize - size) / 2.,
size,
size);
p.setPen(st::historyPeerUserpicFg);
p.setBrush(Qt::NoBrush);
svg.render(&p, r);
}
name->setMarkedText(
st.nameStyle,
current.usage
? tr::lng_group_invite_subscription_info_title(
tr::now,
lt_emoji,
session().data().customEmojiManager().creditsEmoji(),
lt_price,
{ QString::number(current.subscription.credits) },
lt_multiplier,
TextWithEntities{ .text = QString(QChar(0x00D7)) },
lt_total,
{ QString::number(current.usage) },
Ui::Text::WithEntities)
: tr::lng_group_invite_subscription_info_title_none(
tr::now,
lt_emoji,
session().data().customEmojiManager().creditsEmoji(),
lt_price,
{ QString::number(current.subscription.credits) },
Ui::Text::WithEntities),
kMarkupTextOptions,
Core::MarkedTextContext{
.session = &session(),
.customEmojiRepaint = [=] { widget->update(); },
});
auto &lifetime = widget->lifetime();
const auto rateValue = lifetime.make_state<rpl::variable<float64>>(
session().credits().rateValue(_peer));
const auto currency = u"USD"_q;
const auto allCredits = current.subscription.credits * current.usage;
widget->paintRequest(
) | rpl::start_with_next([=] {
auto p = Painter(widget);
p.setBrush(Qt::NoBrush);
p.setPen(st.nameFg);
name->draw(p, {
.position = st.namePosition,
.outerWidth = widget->width() - name->maxWidth(),
.availableWidth = widget->width() - name->maxWidth(),
});
p.drawImage(st.photoPosition, userpic);
const auto rate = rateValue->current();
const auto status = (allCredits <= 0)
? tr::lng_group_invite_no_joined(tr::now)
: (rate > 0)
? tr::lng_group_invite_subscription_info_about(
tr::now,
lt_total,
Ui::FillAmountAndCurrency(allCredits * rate, currency))
: QString();
p.setPen(st.statusFg);
p.setFont(st::contactsStatusFont);
p.drawTextLeft(
st.statusPosition.x(),
st.statusPosition.y(),
widget->width() - st.statusPosition.x(),
status);
}, widget->lifetime());
}
Ui::AddSubsectionTitle( Ui::AddSubsectionTitle(
container, container,
tr::lng_group_invite_created_by()); tr::lng_group_invite_created_by());
@ -743,6 +907,11 @@ void Controller::appendSlice(const Api::JoinedByLinkSlice &slice) {
_lastUser = user; _lastUser = user;
auto row = (_role == Role::Requested) auto row = (_role == Role::Requested)
? std::make_unique<RequestedRow>(user.user, user.date) ? std::make_unique<RequestedRow>(user.user, user.date)
: (_data.current().subscription)
? std::make_unique<SubscriptionRow>(
user.user,
user.date,
_data.current().subscription)
: std::make_unique<PeerListRow>(user.user); : std::make_unique<PeerListRow>(user.user);
if (_role != Role::Requested && user.viaFilterLink) { if (_role != Role::Requested && user.viaFilterLink) {
row->setCustomStatus( row->setCustomStatus(
@ -760,11 +929,117 @@ void Controller::appendSlice(const Api::JoinedByLinkSlice &slice) {
} }
void Controller::rowClicked(not_null<PeerListRow*> row) { void Controller::rowClicked(not_null<PeerListRow*> row) {
ShowPeerInfoSync(row->peer()); if (!_data.current().subscription) {
return ShowPeerInfoSync(row->peer());
}
const auto channel = _peer;
const auto data = _data.current();
const auto show = delegate()->peerListUiShow();
show->showBox(Box([=](not_null<Ui::GenericBox*> box) {
const auto w = Core::App().findWindow(box);
const auto controller = w ? w->sessionController() : nullptr;
if (!controller) {
return;
}
box->setStyle(st::giveawayGiftCodeBox);
box->setNoContentMargin(true);
const auto content = box->verticalLayout();
Ui::AddSkip(content);
Ui::AddSkip(content);
Ui::AddSkip(content);
const auto &stUser = st::boostReplaceUserpic;
const auto session = &row->peer()->session();
content->add(object_ptr<Ui::CenterWrap<>>(
content,
object_ptr<Ui::UserpicButton>(content, channel, stUser))
)->setAttribute(Qt::WA_TransparentForMouseEvents);
Ui::AddSkip(content);
Ui::AddSkip(content);
box->addRow(object_ptr<Ui::CenterWrap<>>(
box,
object_ptr<Ui::FlatLabel>(
box,
tr::lng_credits_box_subscription_title(),
st::creditsBoxAboutTitle)));
Ui::AddSkip(content);
const auto subtitle1 = box->addRow(
object_ptr<Ui::CenterWrap<Ui::FlatLabel>>(
box,
object_ptr<Ui::FlatLabel>(
box,
st::creditsTopupPrice)))->entity();
subtitle1->setMarkedText(
tr::lng_credits_subscription_subtitle(
tr::now,
lt_emoji,
session->data().customEmojiManager().creditsEmoji(),
lt_cost,
{ QString::number(data.subscription.credits) },
Ui::Text::WithEntities),
Core::MarkedTextContext{
.session = session,
.customEmojiRepaint = [=] { subtitle1->update(); },
});
const auto subtitle2 = box->addRow(
object_ptr<Ui::CenterWrap<Ui::FlatLabel>>(
box,
object_ptr<Ui::FlatLabel>(
box,
st::creditsTopupPrice)))->entity();
session->credits().rateValue(
channel
) | rpl::start_with_next([=, currency = u"USD"_q](float64 rate) {
subtitle2->setText(
tr::lng_credits_subscriber_subtitle(
tr::now,
lt_total,
Ui::FillAmountAndCurrency(
data.subscription.credits * rate,
currency)));
}, subtitle2->lifetime());
Ui::AddSkip(content);
Ui::AddSkip(content);
AddSubscriberEntryTable(controller, content, row->peer(), data.date);
Ui::AddSkip(content);
Ui::AddSkip(content);
box->addRow(object_ptr<Ui::CenterWrap<>>(
box,
object_ptr<Ui::FlatLabel>(
box,
tr::lng_credits_box_out_about(
lt_link,
tr::lng_payments_terms_link(
) | Ui::Text::ToLink(
tr::lng_credits_box_out_about_link(tr::now)),
Ui::Text::WithEntities),
st::creditsBoxAboutDivider)));
const auto button = box->addButton(tr::lng_box_ok(), [=] {
box->closeBox();
});
const auto buttonWidth = st::boxWidth
- rect::m::sum::h(st::giveawayGiftCodeBox.buttonPadding);
button->widthValue() | rpl::filter([=] {
return (button->widthNoMargins() != buttonWidth);
}) | rpl::start_with_next([=] {
button->resizeToWidth(buttonWidth);
}, button->lifetime());
}));
} }
void Controller::rowRightActionClicked(not_null<PeerListRow*> row) { void Controller::rowRightActionClicked(not_null<PeerListRow*> row) {
if (_role != Role::Requested) { if (_role != Role::Requested || _data.current().subscription) {
return; return;
} }
delegate()->peerListShowRowMenu(row, true); delegate()->peerListShowRowMenu(row, true);
@ -1251,6 +1526,8 @@ object_ptr<Ui::BoxContent> InviteLinkQrBox(
object_ptr<Ui::BoxContent> EditLinkBox( object_ptr<Ui::BoxContent> EditLinkBox(
not_null<PeerData*> peer, not_null<PeerData*> peer,
const Api::InviteLink &data) { const Api::InviteLink &data) {
constexpr auto kPeriod = 3600 * 24 * 30;
constexpr auto kTestModePeriod = 300;
const auto creating = data.link.isEmpty(); const auto creating = data.link.isEmpty();
const auto box = std::make_shared<QPointer<Ui::GenericBox>>(); const auto box = std::make_shared<QPointer<Ui::GenericBox>>();
using Fields = Ui::InviteLinkFields; using Fields = Ui::InviteLinkFields;
@ -1266,13 +1543,25 @@ object_ptr<Ui::BoxContent> EditLinkBox(
}; };
if (creating) { if (creating) {
Assert(data.admin->isSelf()); Assert(data.admin->isSelf());
peer->session().api().inviteLinks().create( const auto period = peer->session().isTestMode()
? kTestModePeriod
: kPeriod;
peer->session().api().inviteLinks().create({
peer, peer,
finish, finish,
result.label, result.label,
result.expireDate, result.expireDate,
result.usageLimit, result.usageLimit,
result.requestApproval); result.requestApproval,
{ uint64(result.subscriptionCredits), period },
});
} else if (result.subscriptionCredits) {
peer->session().api().inviteLinks().editTitle(
peer,
data.admin,
result.link,
result.label,
finish);
} else { } else {
peer->session().api().inviteLinks().edit( peer->session().api().inviteLinks().edit(
peer, peer,
@ -1287,26 +1576,33 @@ object_ptr<Ui::BoxContent> EditLinkBox(
}; };
const auto isGroup = !peer->isBroadcast(); const auto isGroup = !peer->isBroadcast();
const auto isPublic = peer->isChannel() && peer->asChannel()->isPublic(); const auto isPublic = peer->isChannel() && peer->asChannel()->isPublic();
if (creating) { auto object = Box([=](not_null<Ui::GenericBox*> box) {
auto object = Box(Ui::CreateInviteLinkBox, isGroup, isPublic, done); const auto fill = isGroup
*box = Ui::MakeWeak(object.data()); ? Fn<Ui::InviteLinkSubscriptionToggle()>(nullptr)
return object; : [=] {
} else { return Ui::FillCreateInviteLinkSubscriptionToggle(box, peer);
auto object = Box( };
Ui::EditInviteLinkBox, if (creating) {
Fields{ Ui::CreateInviteLinkBox(box, fill, isGroup, isPublic, done);
.link = data.link, } else {
.label = data.label, Ui::EditInviteLinkBox(
.expireDate = data.expireDate, box,
.usageLimit = data.usageLimit, fill,
.requestApproval = data.requestApproval, Fields{
.isGroup = isGroup, .link = data.link,
.isPublic = isPublic, .label = data.label,
}, .expireDate = data.expireDate,
done); .usageLimit = data.usageLimit,
*box = Ui::MakeWeak(object.data()); .subscriptionCredits = int(data.subscription.credits),
return object; .requestApproval = data.requestApproval,
} .isGroup = isGroup,
.isPublic = isPublic,
},
done);
}
});
*box = Ui::MakeWeak(object.data());
return object;
} }
object_ptr<Ui::BoxContent> RevokeLinkBox( object_ptr<Ui::BoxContent> RevokeLinkBox(

View file

@ -7,7 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#pragma once #pragma once
#include "ui/layers/generic_box.h" template <typename Object>
class object_ptr;
class PeerData; class PeerData;
@ -22,6 +23,7 @@ class Session;
namespace Ui { namespace Ui {
class VerticalLayout; class VerticalLayout;
class Show; class Show;
class BoxContent;
} // namespace Ui } // namespace Ui
[[nodiscard]] bool IsExpiredLink(const Api::InviteLink &data, TimeId now); [[nodiscard]] bool IsExpiredLink(const Api::InviteLink &data, TimeId now);

View file

@ -15,10 +15,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "main/session/session_show.h" #include "main/session/session_show.h"
#include "main/main_session.h" #include "main/main_session.h"
#include "api/api_invite_links.h" #include "api/api_invite_links.h"
#include "settings/settings_credits_graphics.h"
#include "ui/wrap/slide_wrap.h" #include "ui/wrap/slide_wrap.h"
#include "ui/widgets/buttons.h" #include "ui/widgets/buttons.h"
#include "ui/widgets/popup_menu.h" #include "ui/widgets/popup_menu.h"
#include "ui/painter.h" #include "ui/painter.h"
#include "ui/rect.h"
#include "ui/vertical_list.h" #include "ui/vertical_list.h"
#include "lang/lang_keys.h" #include "lang/lang_keys.h"
#include "ui/boxes/confirm_box.h" #include "ui/boxes/confirm_box.h"
@ -31,6 +33,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/style_layers.h" // st::boxDividerLabel #include "styles/style_layers.h" // st::boxDividerLabel
#include "styles/style_menu_icons.h" #include "styles/style_menu_icons.h"
#include <QtSvg/QSvgRenderer>
namespace { namespace {
enum class Color { enum class Color {
@ -39,6 +43,7 @@ enum class Color {
ExpireSoon, ExpireSoon,
Expired, Expired,
Revoked, Revoked,
Subscription,
Count, Count,
}; };
@ -60,8 +65,12 @@ struct InviteLinkAction {
class Row; class Row;
using SubscriptionRightLabel = Settings::SubscriptionRightLabel;
class RowDelegate { class RowDelegate {
public: public:
virtual std::optional<SubscriptionRightLabel> rightLabel(
int credits) const = 0;
virtual void rowUpdateRow(not_null<Row*> row) = 0; virtual void rowUpdateRow(not_null<Row*> row) = 0;
virtual void rowPaintIcon( virtual void rowPaintIcon(
QPainter &p, QPainter &p,
@ -91,6 +100,7 @@ public:
bool forceRound) override; bool forceRound) override;
QSize rightActionSize() const override; QSize rightActionSize() const override;
bool rightActionDisabled() const override;
QMargins rightActionMargins() const override; QMargins rightActionMargins() const override;
void rightActionPaint( void rightActionPaint(
Painter &p, Painter &p,
@ -102,6 +112,7 @@ public:
private: private:
const not_null<RowDelegate*> _delegate; const not_null<RowDelegate*> _delegate;
std::optional<SubscriptionRightLabel> _rightLabel;
InviteLinkData _data; InviteLinkData _data;
QString _status; QString _status;
float64 _progressTillExpire = 0.; float64 _progressTillExpire = 0.;
@ -137,7 +148,9 @@ private:
[[nodiscard]] Color ComputeColor( [[nodiscard]] Color ComputeColor(
const InviteLinkData &link, const InviteLinkData &link,
float64 progress) { float64 progress) {
return link.revoked return link.subscription
? Color::Subscription
: link.revoked
? Color::Revoked ? Color::Revoked
: (progress >= 1.) : (progress >= 1.)
? Color::Expired ? Color::Expired
@ -230,11 +243,13 @@ Row::Row(
, _data(data) , _data(data)
, _progressTillExpire(ComputeProgress(data, now)) , _progressTillExpire(ComputeProgress(data, now))
, _color(ComputeColor(data, _progressTillExpire)) { , _color(ComputeColor(data, _progressTillExpire)) {
_rightLabel = _delegate->rightLabel(_data.subscription.credits);
setCustomStatus(ComputeStatus(data, now)); setCustomStatus(ComputeStatus(data, now));
} }
void Row::update(const InviteLinkData &data, TimeId now) { void Row::update(const InviteLinkData &data, TimeId now) {
_data = data; _data = data;
_rightLabel = _delegate->rightLabel(_data.subscription.credits);
_progressTillExpire = ComputeProgress(data, now); _progressTillExpire = ComputeProgress(data, now);
_color = ComputeColor(data, _progressTillExpire); _color = ComputeColor(data, _progressTillExpire);
setCustomStatus(ComputeStatus(data, now)); setCustomStatus(ComputeStatus(data, now));
@ -305,12 +320,22 @@ PaintRoundImageCallback Row::generatePaintUserpicCallback(bool forceRound) {
} }
QSize Row::rightActionSize() const { QSize Row::rightActionSize() const {
if (_rightLabel) {
return _rightLabel->size;
}
return QSize( return QSize(
st::inviteLinkThreeDotsIcon.width(), st::inviteLinkThreeDotsIcon.width(),
st::inviteLinkThreeDotsIcon.height()); st::inviteLinkThreeDotsIcon.height());
} }
bool Row::rightActionDisabled() const {
return _rightLabel.has_value();
}
QMargins Row::rightActionMargins() const { QMargins Row::rightActionMargins() const {
if (_rightLabel) {
return QMargins(0, 0, st::boxRowPadding.right(), 0);
}
return QMargins( return QMargins(
0, 0,
(st::inviteLinkList.item.height - rightActionSize().height()) / 2, (st::inviteLinkList.item.height - rightActionSize().height()) / 2,
@ -325,6 +350,9 @@ void Row::rightActionPaint(
int outerWidth, int outerWidth,
bool selected, bool selected,
bool actionSelected) { bool actionSelected) {
if (_rightLabel) {
return _rightLabel->draw(p, x, y, st::inviteLinkList.item.height);
}
(actionSelected (actionSelected
? st::inviteLinkThreeDotsIconOver ? st::inviteLinkThreeDotsIconOver
: st::inviteLinkThreeDotsIcon).paint(p, x, y, outerWidth); : st::inviteLinkThreeDotsIcon).paint(p, x, y, outerWidth);
@ -354,6 +382,7 @@ public:
not_null<PeerListRow*> row) override; not_null<PeerListRow*> row) override;
Main::Session &session() const override; Main::Session &session() const override;
std::optional<SubscriptionRightLabel> rightLabel(int) const override;
void rowUpdateRow(not_null<Row*> row) override; void rowUpdateRow(not_null<Row*> row) override;
void rowPaintIcon( void rowPaintIcon(
QPainter &p, QPainter &p,
@ -639,6 +668,17 @@ void LinksController::expiringProgressTimer() {
} }
} }
std::optional<SubscriptionRightLabel> LinksController::rightLabel(
int credits) const {
if (credits > 0) {
return Settings::PaintSubscriptionRightLabelCallback(
&session(),
st::inviteLinkList.item,
credits);
}
return std::nullopt;
}
void LinksController::rowUpdateRow(not_null<Row*> row) { void LinksController::rowUpdateRow(not_null<Row*> row) {
delegate()->peerListUpdateRow(row); delegate()->peerListUpdateRow(row);
} }
@ -659,6 +699,7 @@ void LinksController::rowPaintIcon(
case Color::ExpireSoon: return &st::msgFile4Bg; case Color::ExpireSoon: return &st::msgFile4Bg;
case Color::Expired: return &st::msgFile3Bg; case Color::Expired: return &st::msgFile3Bg;
case Color::Revoked: return &st::windowSubTextFg; case Color::Revoked: return &st::windowSubTextFg;
case Color::Subscription: return &st::msgFile2Bg;
} }
Unexpected("Color in LinksController::rowPaintIcon."); Unexpected("Color in LinksController::rowPaintIcon.");
}(); }();
@ -676,15 +717,25 @@ void LinksController::rowPaintIcon(
p.setBrush(*bg); p.setBrush(*bg);
{ {
auto hq = PainterHighQualityEnabler(p); auto hq = PainterHighQualityEnabler(p);
auto rect = QRect(0, 0, inner, inner); const auto rect = QRect(0, 0, inner, inner)
if (color == Color::Expiring || color == Color::ExpireSoon) { - ((color == Color::Expiring || color == Color::ExpireSoon)
rect = rect.marginsRemoved({ stroke, stroke, stroke, stroke }); ? Margins(stroke)
} : Margins(0));
p.drawEllipse(rect); p.drawEllipse(rect);
} }
(color == Color::Revoked if (color == Color::Subscription) {
? st::inviteLinkRevokedIcon auto svg = QSvgRenderer(u":/gui/links_subscription.svg"_q);
: st::inviteLinkIcon).paintInCenter(p, { 0, 0, inner, inner }); const auto r = QRect(
(inner - st::inviteLinkSubscriptionSize) / 2,
(inner - st::inviteLinkSubscriptionSize) / 2,
st::inviteLinkSubscriptionSize,
st::inviteLinkSubscriptionSize);
svg.render(&p, r);
} else {
(color == Color::Revoked
? st::inviteLinkRevokedIcon
: st::inviteLinkIcon).paintInCenter(p, { 0, 0, inner, inner });
}
} }
p.drawImage(x + skip, y + skip, icon); p.drawImage(x + skip, y + skip, icon);
if (progress >= 0. && progress < 1.) { if (progress >= 0. && progress < 1.) {

View file

@ -282,6 +282,9 @@ void SetupOnlyCustomEmojiField(
const auto offset = size(); const auto offset = size();
if (unifiedId) { if (unifiedId) {
result.text.append('@'); result.text.append('@');
} else if (id.paid()) {
result.text.append(QChar(0x2B50));
unifiedId = reactions->lookupPaid()->selectAnimation->id;
} else { } else {
result.text.append(id.emoji()); result.text.append(id.emoji());
const auto i = ranges::find(all, id, &Data::Reaction::id); const auto i = ranges::find(all, id, &Data::Reaction::id);
@ -312,6 +315,7 @@ struct ReactionsSelectorArgs {
rpl::producer<QString> title; rpl::producer<QString> title;
std::vector<Data::Reaction> list; std::vector<Data::Reaction> list;
std::vector<Data::ReactionId> selected; std::vector<Data::ReactionId> selected;
rpl::producer<bool> paid;
Fn<void(std::vector<Data::ReactionId>, bool)> callback; Fn<void(std::vector<Data::ReactionId>, bool)> callback;
rpl::producer<ReactionsSelectorState> stateValue; rpl::producer<ReactionsSelectorState> stateValue;
int customAllowed = 0; int customAllowed = 0;
@ -341,13 +345,18 @@ object_ptr<Ui::RpWidget> AddReactionsSelector(
std::unique_ptr<UnifiedFactoryOwner> unifiedFactoryOwner; std::unique_ptr<UnifiedFactoryOwner> unifiedFactoryOwner;
UnifiedFactoryOwner::RecentFactory factory; UnifiedFactoryOwner::RecentFactory factory;
base::flat_set<DocumentId> allowed; base::flat_set<DocumentId> allowed;
std::vector<Data::ReactionId> reactions;
rpl::lifetime focusLifetime; rpl::lifetime focusLifetime;
}; };
const auto paid = reactions->lookupPaid();
auto normal = reactions->list(Data::Reactions::Type::Active);
normal.push_back(*paid);
const auto state = raw->lifetime().make_state<State>(); const auto state = raw->lifetime().make_state<State>();
state->unifiedFactoryOwner = std::make_unique<UnifiedFactoryOwner>( state->unifiedFactoryOwner = std::make_unique<UnifiedFactoryOwner>(
session, session,
reactions->list(Data::Reactions::Type::Active)); normal);
state->factory = state->unifiedFactoryOwner->factory(); state->factory = state->unifiedFactoryOwner->factory();
state->reactions = std::move(args.selected);
const auto customEmojiPaused = [controller = args.controller] { const auto customEmojiPaused = [controller = args.controller] {
return controller->isGifPausedAtLeastFor(PauseReason::Layer); return controller->isGifPausedAtLeastFor(PauseReason::Layer);
@ -396,9 +405,32 @@ object_ptr<Ui::RpWidget> AddReactionsSelector(
state->allowed = std::move(allowed); state->allowed = std::move(allowed);
raw->rawTextEdit()->update(); raw->rawTextEdit()->update();
} }
state->reactions = reactions;
callback(std::move(reactions), hardLimitHit); callback(std::move(reactions), hardLimitHit);
}, isCustom, args.customHardLimit); }, isCustom, args.customHardLimit);
raw->setTextWithTags(ComposeEmojiList(reactions, args.selected)); const auto applyFromState = [=] {
raw->setTextWithTags(ComposeEmojiList(reactions, state->reactions));
};
applyFromState();
std::move(
args.paid
) | rpl::start_with_next([=](bool paid) {
const auto id = Data::ReactionId::Paid();
if (paid && !ranges::contains(state->reactions, id)) {
state->reactions.insert(begin(state->reactions), id);
applyFromState();
} else if (!paid && ranges::contains(state->reactions, id)) {
state->reactions.erase(
ranges::remove(state->reactions, id),
end(state->reactions));
applyFromState();
}
}, raw->lifetime());
const auto toggle = Ui::CreateChild<Ui::IconButton>(
parent.get(),
st::manageGroupReactions);
using SelectorState = ReactionsSelectorState; using SelectorState = ReactionsSelectorState;
std::move( std::move(
@ -444,10 +476,6 @@ object_ptr<Ui::RpWidget> AddReactionsSelector(
} }
}, raw->lifetime()); }, raw->lifetime());
const auto toggle = Ui::CreateChild<Ui::IconButton>(
parent.get(),
st::manageGroupReactions);
const auto panel = Ui::CreateChild<TabbedPanel>( const auto panel = Ui::CreateChild<TabbedPanel>(
args.outer.get(), args.outer.get(),
args.controller, args.controller,
@ -458,8 +486,11 @@ object_ptr<Ui::RpWidget> AddReactionsSelector(
(args.all (args.all
? TabbedSelector::Mode::FullReactions ? TabbedSelector::Mode::FullReactions
: TabbedSelector::Mode::RecentReactions))); : TabbedSelector::Mode::RecentReactions)));
panel->selector()->provideRecentEmoji( auto panelList = state->unifiedFactoryOwner->unifiedIdsList();
state->unifiedFactoryOwner->unifiedIdsList()); panelList.erase(
ranges::remove(panelList, paid->selectAnimation->id),
end(panelList));
panel->selector()->provideRecentEmoji(panelList);
panel->setDesiredHeightValues( panel->setDesiredHeightValues(
1., 1.,
st::emojiPanMinHeight / 2, st::emojiPanMinHeight / 2,
@ -608,15 +639,17 @@ void EditAllowedReactionsBox(
rpl::variable<SelectorState> selectorState; rpl::variable<SelectorState> selectorState;
std::vector<Data::ReactionId> selected; std::vector<Data::ReactionId> selected;
rpl::variable<int> customCount; rpl::variable<int> customCount;
rpl::variable<bool> paidEnabled;
}; };
const auto allowed = args.allowed; const auto allowed = args.allowed;
const auto optionInitial = (allowed.type != AllowedReactionsType::Some) const auto optionInitial = (allowed.type != AllowedReactionsType::Some)
? Option::All ? Option::All
: allowed.some.empty() : (allowed.some.empty() && !allowed.paidEnabled)
? Option::None ? Option::None
: Option::Some; : Option::Some;
const auto state = box->lifetime().make_state<State>(State{ const auto state = box->lifetime().make_state<State>(State{
.option = optionInitial, .option = optionInitial,
.paidEnabled = allowed.paidEnabled,
}); });
const auto container = box->verticalLayout(); const auto container = box->verticalLayout();
@ -702,13 +735,19 @@ void EditAllowedReactionsBox(
| ranges::views::transform(&Data::Reaction::id) | ranges::views::transform(&Data::Reaction::id)
| ranges::to_vector) | ranges::to_vector)
: allowed.some; : allowed.some;
if (allowed.paidEnabled) {
selected.insert(begin(selected), Data::ReactionId::Paid());
}
const auto changed = [=]( const auto changed = [=](
std::vector<Data::ReactionId> chosen, std::vector<Data::ReactionId> chosen,
bool hardLimitHit) { bool hardLimitHit) {
state->selected = std::move(chosen); state->selected = std::move(chosen);
state->customCount = ranges::count_if( state->customCount = ranges::count_if(
state->selected, state->selected,
&Data::ReactionId::custom); &Data::ReactionId::custom);
state->paidEnabled = ranges::contains(
state->selected,
Data::ReactionId::Paid());
if (hardLimitHit) { if (hardLimitHit) {
box->uiShow()->showToast( box->uiShow()->showToast(
tr::lng_manage_peer_reactions_limit(tr::now)); tr::lng_manage_peer_reactions_limit(tr::now));
@ -727,6 +766,7 @@ void EditAllowedReactionsBox(
.title = tr::lng_manage_peer_reactions_available_ph(), .title = tr::lng_manage_peer_reactions_available_ph(),
.list = all, .list = all,
.selected = state->selected, .selected = state->selected,
.paid = state->paidEnabled.value(),
.callback = changed, .callback = changed,
.stateValue = state->selectorState.value(), .stateValue = state->selectorState.value(),
.customAllowed = args.allowedCustomReactions, .customAllowed = args.allowedCustomReactions,
@ -735,7 +775,7 @@ void EditAllowedReactionsBox(
}), st::boxRowPadding); }), st::boxRowPadding);
box->setFocusCallback([=] { box->setFocusCallback([=] {
if (!wrap || state->option.current() == Option::Some) { if (state->option.current() == Option::Some) {
state->selectorState.force_assign(SelectorState::Active); state->selectorState.force_assign(SelectorState::Active);
} }
}); });
@ -847,6 +887,29 @@ void EditAllowedReactionsBox(
) | rpl::map(rpl::mappers::_1 == SelectorState::Active))); ) | rpl::map(rpl::mappers::_1 == SelectorState::Active)));
Ui::AddDividerText(inner, tr::lng_manage_peer_reactions_max_about()); Ui::AddDividerText(inner, tr::lng_manage_peer_reactions_max_about());
Ui::AddSkip(inner);
const auto paid = inner->add(object_ptr<Ui::SettingsButton>(
inner,
tr::lng_manage_peer_reactions_paid(),
st::manageGroupNoIconButton.button));
paid->toggleOn(state->paidEnabled.value());
paid->toggledValue(
) | rpl::start_with_next([=](bool value) {
state->paidEnabled = value;
}, paid->lifetime());
Ui::AddSkip(inner);
Ui::AddDividerText(
inner,
tr::lng_manage_peer_reactions_paid_about(
lt_link,
tr::lng_manage_peer_reactions_paid_link([=](QString text) {
return Ui::Text::Link(
text,
u"https://telegram.org/tos/stars"_q);
}),
Ui::Text::WithEntities));
} }
const auto collect = [=] { const auto collect = [=] {
auto result = AllowedReactions(); auto result = AllowedReactions();
@ -856,6 +919,9 @@ void EditAllowedReactionsBox(
: (enabled->toggled())) { : (enabled->toggled())) {
result.some = state->selected; result.some = state->selected;
} }
if (!isGroup && enabled->toggled()) {
result.paidEnabled = state->paidEnabled.current();
}
auto some = result.some; auto some = result.some;
auto simple = all | ranges::views::transform( auto simple = all | ranges::views::transform(
&Data::Reaction::id &Data::Reaction::id
@ -907,15 +973,20 @@ void SaveAllowedReactions(
: allowed.some.empty() : allowed.some.empty()
? MTP_chatReactionsNone() ? MTP_chatReactionsNone()
: MTP_chatReactionsSome(MTP_vector<MTPReaction>(ids)); : MTP_chatReactionsSome(MTP_vector<MTPReaction>(ids));
const auto editPaidEnabled = peer->isBroadcast();
const auto paidEnabled = editPaidEnabled && allowed.paidEnabled;
const auto maxCount = allowed.maxCount;
peer->session().api().request(MTPmessages_SetChatAvailableReactions( peer->session().api().request(MTPmessages_SetChatAvailableReactions(
allowed.maxCount ? MTP_flags(Flag::f_reactions_limit) : MTP_flags(0), MTP_flags(Flag()
| (maxCount ? Flag::f_reactions_limit : Flag())
| (editPaidEnabled ? Flag::f_paid_enabled : Flag())),
peer->input, peer->input,
updated, updated,
MTP_int(allowed.maxCount) MTP_int(maxCount),
MTP_bool(paidEnabled)
)).done([=](const MTPUpdates &result) { )).done([=](const MTPUpdates &result) {
peer->session().api().applyUpdates(result); peer->session().api().applyUpdates(result);
auto parsed = Data::Parse(updated); auto parsed = Data::Parse(updated, maxCount, paidEnabled);
parsed.maxCount = allowed.maxCount;
if (const auto chat = peer->asChat()) { if (const auto chat = peer->asChat()) {
chat->setAllowedReactions(parsed); chat->setAllowedReactions(parsed);
} else if (const auto channel = peer->asChannel()) { } else if (const auto channel = peer->asChannel()) {

View file

@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/boxes/confirm_box.h" #include "ui/boxes/confirm_box.h"
#include "ui/controls/peer_list_dummy.h" #include "ui/controls/peer_list_dummy.h"
#include "ui/effects/premium_bubble.h"
#include "ui/effects/premium_graphics.h" #include "ui/effects/premium_graphics.h"
#include "ui/widgets/checkbox.h" #include "ui/widgets/checkbox.h"
#include "ui/wrap/padding_wrap.h" #include "ui/wrap/padding_wrap.h"
@ -136,6 +137,12 @@ private:
}; };
[[nodiscard]] Ui::Premium::BubbleType ChooseBubbleType(bool premium) {
return premium
? Ui::Premium::BubbleType::Premium
: Ui::Premium::BubbleType::NoPremium;
}
void InactiveDelegate::peerListSetTitle(rpl::producer<QString> title) { void InactiveDelegate::peerListSetTitle(rpl::producer<QString> title) {
} }
@ -421,7 +428,7 @@ void SimpleLimitBox(
(descriptor.complexRatio (descriptor.complexRatio
? descriptor.premiumLimit ? descriptor.premiumLimit
: 2 * descriptor.current), : 2 * descriptor.current),
premiumPossible, ChooseBubbleType(premiumPossible),
descriptor.phrase, descriptor.phrase,
descriptor.icon); descriptor.icon);
Ui::AddSkip(top, st::premiumLineTextSkip); Ui::AddSkip(top, st::premiumLineTextSkip);
@ -1109,7 +1116,7 @@ void AccountsLimitBox(
: (current > defaultLimit) : (current > defaultLimit)
? (current + 1) ? (current + 1)
: (defaultLimit * 2)), : (defaultLimit * 2)),
premiumPossible, ChooseBubbleType(premiumPossible),
std::nullopt, std::nullopt,
&st::premiumIconAccounts); &st::premiumIconAccounts);
Ui::AddSkip(top, st::premiumLineTextSkip); Ui::AddSkip(top, st::premiumLineTextSkip);

View file

@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_credits.h" #include "api/api_credits.h"
#include "apiwrap.h" #include "apiwrap.h"
#include "core/ui_integration.h" // Core::MarkedTextContext. #include "core/ui_integration.h" // Core::MarkedTextContext.
#include "data/components/credits.h"
#include "data/data_credits.h" #include "data/data_credits.h"
#include "data/data_photo.h" #include "data/data_photo.h"
#include "data/data_session.h" #include "data/data_session.h"
@ -80,14 +81,12 @@ struct PaidMediaData {
} }
} }
const auto bot = item->viaBot();
const auto sender = item->originalSender(); const auto sender = item->originalSender();
const auto broadcast = (sender && sender->isBroadcast())
? sender
: message->peer.get();
return { return {
.invoice = invoice, .invoice = invoice,
.item = item, .item = item,
.peer = broadcast, .peer = (bot ? bot : sender ? sender : message->peer.get()),
.photos = photos, .photos = photos,
.videos = videos, .videos = videos,
}; };
@ -130,6 +129,16 @@ struct PaidMediaData {
lt_video, lt_video,
std::move(videosBold), std::move(videosBold),
Ui::Text::WithEntities); Ui::Text::WithEntities);
if (const auto user = data.peer->asUser()) {
return tr::lng_credits_box_out_media_user(
lt_count,
rpl::single(form->invoice.amount) | tr::to_count(),
lt_media,
std::move(media),
lt_user,
rpl::single(Ui::Text::Bold(user->shortName())),
Ui::Text::RichLangValue);
}
return tr::lng_credits_box_out_media( return tr::lng_credits_box_out_media(
lt_count, lt_count,
rpl::single(form->invoice.amount) | tr::to_count(), rpl::single(form->invoice.amount) | tr::to_count(),
@ -301,7 +310,7 @@ void SendCreditsBox(
st::giveawayGiftCodeStartButton.height / 2); st::giveawayGiftCodeStartButton.height / 2);
AddChildToWidgetCenter(button.data(), loadingAnimation); AddChildToWidgetCenter(button.data(), loadingAnimation);
loadingAnimation->showOn(state->confirmButtonBusy.value()); loadingAnimation->showOn(state->confirmButtonBusy.value());
} }
{ {
auto buttonText = tr::lng_credits_box_out_confirm( auto buttonText = tr::lng_credits_box_out_confirm(
lt_count, lt_count,
@ -361,15 +370,11 @@ void SendCreditsBox(
} }
{ {
session->credits().load(true);
const auto balance = Settings::AddBalanceWidget( const auto balance = Settings::AddBalanceWidget(
content, content,
session->creditsValue(), session->credits().balanceValue(),
false); false);
const auto api = balance->lifetime().make_state<Api::CreditsStatus>(
session->user());
api->request({}, [=](Data::CreditsStatusSlice slice) {
session->setCredits(slice.balance);
});
rpl::combine( rpl::combine(
balance->sizeValue(), balance->sizeValue(),
content->sizeValue() content->sizeValue()

View file

@ -554,7 +554,6 @@ void Panel::reinitWithCall(Call *call) {
Ui::Toast::Show(widget(), Ui::Toast::Config{ Ui::Toast::Show(widget(), Ui::Toast::Config{
.text = { text }, .text = { text },
.st = &st::callErrorToast, .st = &st::callErrorToast,
.multiline = true,
}); });
}, _callLifetime); }, _callLifetime);

View file

@ -645,9 +645,10 @@ void SettingsBox(
shareLink = [=] { shareLink = [=] {
if (!copyLink() && !state->generatingLink) { if (!copyLink() && !state->generatingLink) {
state->generatingLink = true; state->generatingLink = true;
peer->session().api().inviteLinks().create( peer->session().api().inviteLinks().create({
peer, peer,
crl::guard(layout, [=](auto&&) { copyLink(); })); crl::guard(layout, [=](auto&&) { copyLink(); })
});
} }
}; };
} }

View file

@ -1126,10 +1126,8 @@ base::unique_qptr<Ui::RpWidget> CreateDisabledFieldView(
: list.back(); : list.back();
*toast = Ui::Toast::Show(parent, { *toast = Ui::Toast::Show(parent, {
.text = { tr::lng_send_text_no_about(tr::now, lt_types, types) }, .text = { tr::lng_send_text_no_about(tr::now, lt_types, types) },
.st = &st::defaultMultilineToast, .attach = RectPart::Bottom,
.duration = kTypesDuration, .duration = kTypesDuration,
.multiline = true,
.slideSide = RectPart::Bottom,
}); });
}); });
return result; return result;

View file

@ -8,11 +8,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "chat_helpers/stickers_dice_pack.h" #include "chat_helpers/stickers_dice_pack.h"
#include "main/main_session.h" #include "main/main_session.h"
#include "chat_helpers/stickers_lottie.h"
#include "data/data_session.h" #include "data/data_session.h"
#include "data/data_document.h" #include "data/data_document.h"
#include "ui/chat/attach/attach_prepare.h"
#include "ui/image/image_location_factory.h"
#include "storage/localimageloader.h"
#include "base/unixtime.h" #include "base/unixtime.h"
#include "apiwrap.h" #include "apiwrap.h"
@ -104,6 +102,11 @@ void DicePack::tryGenerateLocalZero() {
return; return;
} }
const auto generateLocal = [&](int index, const QString &name) {
_map.emplace(
index,
ChatHelpers::GenerateLocalTgsSticker(_session, name));
};
if (_emoji == DicePacks::kDiceString) { if (_emoji == DicePacks::kDiceString) {
generateLocal(0, u"dice_idle"_q); generateLocal(0, u"dice_idle"_q);
} else if (_emoji == DicePacks::kDartString) { } else if (_emoji == DicePacks::kDartString) {
@ -123,32 +126,8 @@ void DicePack::tryGenerateLocalZero() {
} }
} }
void DicePack::generateLocal(int index, const QString &name) { DicePacks::DicePacks(not_null<Main::Session*> session)
const auto path = u":/gui/art/"_q + name + u".tgs"_q; : _session(session) {
auto task = FileLoadTask(
_session,
path,
QByteArray(),
nullptr,
SendMediaType::File,
FileLoadTo(0, {}, {}, 0),
{},
false);
task.process({ .generateGoodThumbnail = false });
const auto result = task.peekResult();
Assert(result != nullptr);
const auto document = _session->data().processDocument(
result->document,
Images::FromImageInMemory(result->thumb, "WEBP", result->thumbbytes));
document->setLocation(Core::FileLocation(path));
_map.emplace(index, document);
Ensures(document->sticker());
Ensures(document->sticker()->isLottie());
}
DicePacks::DicePacks(not_null<Main::Session*> session) : _session(session) {
} }
DocumentData *DicePacks::lookup(const QString &emoji, int value) { DocumentData *DicePacks::lookup(const QString &emoji, int value) {

View file

@ -26,7 +26,6 @@ private:
void load(); void load();
void applySet(const MTPDmessages_stickerSet &data); void applySet(const MTPDmessages_stickerSet &data);
void tryGenerateLocalZero(); void tryGenerateLocalZero();
void generateLocal(int index, const QString &name);
const not_null<Main::Session*> _session; const not_null<Main::Session*> _session;
QString _emoji; QString _emoji;

View file

@ -15,12 +15,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_session.h" #include "data/data_session.h"
#include "data/data_file_origin.h" #include "data/data_file_origin.h"
#include "storage/cache/storage_cache_database.h" #include "storage/cache/storage_cache_database.h"
#include "storage/localimageloader.h"
#include "history/view/media/history_view_media_common.h" #include "history/view/media/history_view_media_common.h"
#include "media/clip/media_clip_reader.h" #include "media/clip/media_clip_reader.h"
#include "ui/chat/attach/attach_prepare.h"
#include "ui/effects/path_shift_gradient.h" #include "ui/effects/path_shift_gradient.h"
#include "ui/image/image_location_factory.h"
#include "ui/painter.h" #include "ui/painter.h"
#include "main/main_session.h" #include "main/main_session.h"
#include <xxhash.h>
namespace ChatHelpers { namespace ChatHelpers {
namespace { namespace {
@ -312,4 +317,41 @@ QSize ComputeStickerSize(not_null<DocumentData*> document, QSize box) {
return HistoryView::NonEmptySize(request.size(dimensions, 8) / ratio); return HistoryView::NonEmptySize(request.size(dimensions, 8) / ratio);
} }
[[nodiscard]] uint64 LocalTgsStickerId(QStringView name) {
auto full = u"local_tgs_sticker:"_q;
full.append(name);
return XXH64(full.data(), full.size() * sizeof(QChar), 0);
}
not_null<DocumentData*> GenerateLocalTgsSticker(
not_null<Main::Session*> session,
const QString &name) {
const auto path = u":/animations/"_q + name + u".tgs"_q;
auto task = FileLoadTask(
session,
path,
QByteArray(),
nullptr,
SendMediaType::File,
FileLoadTo(0, {}, {}, 0),
{},
false,
nullptr,
LocalTgsStickerId(name));
task.process({ .generateGoodThumbnail = false });
const auto result = task.peekResult();
Assert(result != nullptr);
const auto document = session->data().processDocument(
result->document,
Images::FromImageInMemory(
result->thumb,
"WEBP",
result->thumbbytes));
document->setLocation(Core::FileLocation(path));
Ensures(document->sticker());
Ensures(document->sticker()->isLottie());
return document;
}
} // namespace ChatHelpers } // namespace ChatHelpers

View file

@ -130,4 +130,8 @@ bool PaintStickerThumbnailPath(
not_null<DocumentData*> document, not_null<DocumentData*> document,
QSize box); QSize box);
[[nodiscard]] not_null<DocumentData*> GenerateLocalTgsSticker(
not_null<Main::Session*> session,
const QString &name);
} // namespace ChatHelpers } // namespace ChatHelpers

View file

@ -170,7 +170,7 @@ PreviewWrap::PreviewWrap(
} }
}, lifetime()); }, lifetime());
session->data().itemViewRefreshRequest( session->data().itemViewRefreshRequest(
) | rpl::start_with_next([=](not_null<HistoryItem*> item) { ) | rpl::start_with_next([=](not_null<const HistoryItem*> item) {
if (item == _item) { if (item == _item) {
if (goodItem()) { if (goodItem()) {
createView(); createView();

View file

@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_forum.h" #include "data/data_forum.h"
#include "data/data_photo.h" #include "data/data_photo.h"
#include "data/data_document.h" #include "data/data_document.h"
#include "data/data_message_reactions.h"
#include "data/data_session.h" #include "data/data_session.h"
#include "data/data_stories.h" #include "data/data_stories.h"
#include "data/data_user.h" #include "data/data_user.h"
@ -48,6 +49,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "countries/countries_manager.h" #include "countries/countries_manager.h"
#include "iv/iv_delegate_impl.h" #include "iv/iv_delegate_impl.h"
#include "iv/iv_instance.h" #include "iv/iv_instance.h"
#include "iv/iv_data.h"
#include "lang/lang_file_parser.h" #include "lang/lang_file_parser.h"
#include "lang/lang_translator.h" #include "lang/lang_translator.h"
#include "lang/lang_cloud_manager.h" #include "lang/lang_cloud_manager.h"
@ -340,6 +342,9 @@ void Application::run() {
// Create mime database, so it won't be slow later. // Create mime database, so it won't be slow later.
QMimeDatabase().mimeTypeForName(u"text/plain"_q); QMimeDatabase().mimeTypeForName(u"text/plain"_q);
// Check now to avoid re-entrance later.
[[maybe_unused]] const auto ivSupported = Iv::ShowButton();
_windows.emplace(nullptr, std::make_unique<Window::Controller>()); _windows.emplace(nullptr, std::make_unique<Window::Controller>());
setLastActiveWindow(_windows.front().second.get()); setLastActiveWindow(_windows.front().second.get());
_windowInSettings = _lastActivePrimaryWindow = _lastActiveWindow; _windowInSettings = _lastActivePrimaryWindow = _lastActiveWindow;
@ -745,14 +750,12 @@ void Application::saveSettings() {
Local::writeSettings(); Local::writeSettings();
} }
bool Application::canReadDefaultDownloadPath(bool always) const { bool Application::canReadDefaultDownloadPath() const {
if (KSandbox::isInside() return KSandbox::isInside()
&& (always || settings().downloadPath().isEmpty())) { ? base::CanReadDirectory(
const auto path = QStandardPaths::writableLocation( QStandardPaths::writableLocation(
QStandardPaths::DownloadLocation); QStandardPaths::DownloadLocation))
return base::CanReadDirectory(path); : true;
}
return true;
} }
bool Application::canSaveFileWithoutAskingForPath() const { bool Application::canSaveFileWithoutAskingForPath() const {
@ -1746,6 +1749,9 @@ bool Application::readyToQuit() {
if (session->data().stories().isQuitPrevent()) { if (session->data().stories().isQuitPrevent()) {
prevented = true; prevented = true;
} }
if (session->data().reactions().isQuitPrevent()) {
prevented = true;
}
} }
} }
} }

View file

@ -209,7 +209,7 @@ public:
void saveSettingsDelayed(crl::time delay = kDefaultSaveDelay); void saveSettingsDelayed(crl::time delay = kDefaultSaveDelay);
void saveSettings(); void saveSettings();
[[nodiscard]] bool canReadDefaultDownloadPath(bool always = false) const; [[nodiscard]] bool canReadDefaultDownloadPath() const;
[[nodiscard]] bool canSaveFileWithoutAskingForPath() const; [[nodiscard]] bool canSaveFileWithoutAskingForPath() const;
// Fallback config and proxy. // Fallback config and proxy.

View file

@ -33,6 +33,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "passport/passport_form_controller.h" #include "passport/passport_form_controller.h"
#include "ui/text/text_utilities.h" #include "ui/text/text_utilities.h"
#include "ui/toast/toast.h" #include "ui/toast/toast.h"
#include "data/components/credits.h"
#include "data/data_birthday.h" #include "data/data_birthday.h"
#include "data/data_channel.h" #include "data/data_channel.h"
#include "data/data_document.h" #include "data/data_document.h"
@ -46,6 +47,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "window/window_peer_menu.h" #include "window/window_peer_menu.h"
#include "window/themes/window_theme_editor_box.h" // GenerateSlug. #include "window/themes/window_theme_editor_box.h" // GenerateSlug.
#include "payments/payments_checkout_process.h" #include "payments/payments_checkout_process.h"
#include "settings/settings_credits.h"
#include "settings/settings_credits_graphics.h"
#include "settings/settings_information.h" #include "settings/settings_information.h"
#include "settings/settings_global_ttl.h" #include "settings/settings_global_ttl.h"
#include "settings/settings_folders.h" #include "settings/settings_folders.h"
@ -1174,6 +1177,52 @@ bool ResolveBoost(
return true; return true;
} }
bool ResolveTopUp(
Window::SessionController *controller,
const Match &match,
const QVariant &context) {
if (!controller) {
return false;
}
const auto params = url_parse_params(
match->captured(1),
qthelp::UrlParamNameTransform::ToLower);
const auto amount = std::clamp(
params.value(u"balance"_q).toULongLong(),
qulonglong(1),
qulonglong(1'000'000));
const auto purpose = params.value(u"purpose"_q);
const auto weak = base::make_weak(controller);
const auto done = [=](::Settings::SmallBalanceResult result) {
if (result == ::Settings::SmallBalanceResult::Already) {
if (const auto strong = weak.get()) {
const auto filter = [=](const auto &...) {
strong->showSettings(::Settings::CreditsId());
return false;
};
strong->showToast(Ui::Toast::Config{
.text = tr::lng_credits_enough(
tr::now,
lt_link,
Ui::Text::Link(
Ui::Text::Bold(
tr::lng_credits_enough_link(tr::now))),
Ui::Text::RichLangValue),
.filter = filter,
.duration = 4 * crl::time(1000),
});
}
}
};
::Settings::MaybeRequestBalanceIncrease(
controller->uiShow(),
amount,
::Settings::SmallBalanceDeepLink{ .purpose = purpose },
done);
return true;
}
bool ResolveChatLink( bool ResolveChatLink(
Window::SessionController *controller, Window::SessionController *controller,
const Match &match, const Match &match,
@ -1280,6 +1329,10 @@ const std::vector<LocalUrlHandler> &LocalUrlHandlers() {
u"^message/?\\?slug=([a-zA-Z0-9\\.\\_\\-]+)(&|$)"_q, u"^message/?\\?slug=([a-zA-Z0-9\\.\\_\\-]+)(&|$)"_q,
ResolveChatLink ResolveChatLink
}, },
{
u"^stars_topup/?\\?(.+)(#|$)"_q,
ResolveTopUp
},
{ {
u"^user\\?(.+)(#|$)"_q, u"^user\\?(.+)(#|$)"_q,
AyuUrlHandlers::ResolveUser AyuUrlHandlers::ResolveUser

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 = 5003002; constexpr auto AppVersion = 5004001;
constexpr auto AppVersionStr = "5.3.2"; constexpr auto AppVersionStr = "5.4.1";
constexpr auto AppBetaVersion = false; constexpr auto AppBetaVersion = false;
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION; constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;

View file

@ -449,9 +449,6 @@ void ShortcutMessages::preloadShortcuts() {
result.match([&](const MTPDmessages_quickReplies &data) { result.match([&](const MTPDmessages_quickReplies &data) {
owner->processUsers(data.vusers()); owner->processUsers(data.vusers());
owner->processChats(data.vchats()); owner->processChats(data.vchats());
owner->processMessages(
data.vmessages(),
NewMessageType::Existing);
updateShortcuts(data.vquick_replies().v); updateShortcuts(data.vquick_replies().v);
}, [&](const MTPDmessages_quickRepliesNotModified &) { }, [&](const MTPDmessages_quickRepliesNotModified &) {
if (!_shortcutsLoaded) { if (!_shortcutsLoaded) {

View file

@ -0,0 +1,122 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "data/components/credits.h"
#include "api/api_credits.h"
#include "data/data_user.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
namespace Data {
namespace {
constexpr auto kReloadThreshold = 60 * crl::time(1000);
} // namespace
Credits::Credits(not_null<Main::Session*> session)
: _session(session)
, _reload([=] { load(true); }) {
}
Credits::~Credits() = default;
void Credits::apply(const MTPDupdateStarsBalance &data) {
apply(data.vbalance().v);
}
rpl::producer<float64> Credits::rateValue(
not_null<PeerData*> ownedBotOrChannel) {
return rpl::single(
_session->appConfig().get<float64>(
u"stars_usd_withdraw_rate_x1000"_q,
1200) / 1000.);
}
void Credits::load(bool force) {
if (_loader
|| (!force
&& _lastLoaded
&& _lastLoaded + kReloadThreshold > crl::now())) {
return;
}
_loader = std::make_unique<Api::CreditsStatus>(_session->user());
_loader->request({}, [=](Data::CreditsStatusSlice slice) {
_loader = nullptr;
apply(slice.balance);
});
}
bool Credits::loaded() const {
return _lastLoaded != 0;
}
rpl::producer<bool> Credits::loadedValue() const {
if (loaded()) {
return rpl::single(true);
}
return rpl::single(
false
) | rpl::then(_loadedChanges.events() | rpl::map_to(true));
}
uint64 Credits::balance() const {
return _nonLockedBalance.current();
}
rpl::producer<uint64> Credits::balanceValue() const {
return _nonLockedBalance.value();
}
void Credits::updateNonLockedValue() {
_nonLockedBalance = (_balance >= _locked) ? (_balance - _locked) : 0;
}
void Credits::lock(int count) {
Expects(loaded());
Expects(count >= 0);
Expects(_locked + count <= _balance);
_locked += count;
updateNonLockedValue();
}
void Credits::unlock(int count) {
Expects(count >= 0);
Expects(_locked >= count);
_locked -= count;
updateNonLockedValue();
}
void Credits::withdrawLocked(int count) {
Expects(count >= 0);
Expects(_locked >= count);
_locked -= count;
apply(_balance >= count ? (_balance - count) : 0);
invalidate();
}
void Credits::invalidate() {
_reload.call();
}
void Credits::apply(uint64 balance) {
_balance = balance;
updateNonLockedValue();
const auto was = std::exchange(_lastLoaded, crl::now());
if (!was) {
_loadedChanges.fire({});
}
}
} // namespace Data

View file

@ -0,0 +1,61 @@
/*
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 Api {
class CreditsStatus;
} // namespace Api
namespace Main {
class Session;
} // namespace Main
namespace Data {
class Credits final {
public:
explicit Credits(not_null<Main::Session*> session);
~Credits();
void load(bool force = false);
void apply(uint64 balance);
[[nodiscard]] bool loaded() const;
[[nodiscard]] rpl::producer<bool> loadedValue() const;
[[nodiscard]] uint64 balance() const;
[[nodiscard]] rpl::producer<uint64> balanceValue() const;
[[nodiscard]] rpl::producer<float64> rateValue(
not_null<PeerData*> ownedBotOrChannel);
void lock(int count);
void unlock(int count);
void withdrawLocked(int count);
void invalidate();
void apply(const MTPDupdateStarsBalance &data);
private:
void updateNonLockedValue();
const not_null<Main::Session*> _session;
std::unique_ptr<Api::CreditsStatus> _loader;
uint64 _balance = 0;
uint64 _locked = 0;
rpl::variable<uint64> _nonLockedBalance;
rpl::event_stream<> _loadedChanges;
crl::time _lastLoaded = 0;
float64 _rate = 0.;
SingleQueuedInvokation _reload;
};
} // namespace Data

View file

@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "apiwrap.h" #include "apiwrap.h"
#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_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"
@ -275,6 +276,35 @@ 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 mediaDocumentId = DocumentId(0);
{
if (data.vmedia()) {
data.vmedia()->match([&](const MTPDmessageMediaPhoto &media) {
if (const auto tlPhoto = media.vphoto()) {
tlPhoto->match([&](const MTPDphoto &data) {
const auto p = history->owner().processPhoto(data);
mediaPhotoId = p->id;
}, [](const MTPDphotoEmpty &) {
});
}
}, [&](const MTPDmessageMediaDocument &media) {
if (const auto tlDocument = media.vdocument()) {
tlDocument->match([&](const MTPDdocument &data) {
const auto d = history->owner().processDocument(data);
if (d->isVideoFile()
|| d->isSilentVideo()
|| d->isAnimation()
|| d->isGifv()) {
mediaDocumentId = d->id;
}
}, [](const MTPDdocumentEmpty &) {
});
}
}, [](const auto &) {
});
}
};
const auto from = SponsoredFrom{ const auto from = SponsoredFrom{
.title = qs(data.vtitle()), .title = qs(data.vtitle()),
.link = qs(data.vurl()), .link = qs(data.vurl()),
@ -282,6 +312,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,
.mediaDocumentId = mediaDocumentId,
.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),
@ -398,6 +430,8 @@ SponsoredMessages::Details SponsoredMessages::lookupDetails(
.link = data.link, .link = data.link,
.buttonText = data.from.buttonText, .buttonText = data.from.buttonText,
.photoId = data.from.photoId, .photoId = data.from.photoId,
.mediaPhotoId = data.from.mediaPhotoId,
.mediaDocumentId = data.from.mediaDocumentId,
.backgroundEmojiId = data.from.backgroundEmojiId, .backgroundEmojiId = data.from.backgroundEmojiId,
.colorIndex = data.from.colorIndex, .colorIndex = data.from.colorIndex,
.isLinkInternal = data.from.isLinkInternal, .isLinkInternal = data.from.isLinkInternal,

View file

@ -44,6 +44,8 @@ struct SponsoredFrom {
QString link; QString link;
QString buttonText; QString buttonText;
PhotoId photoId = PhotoId(0); PhotoId photoId = PhotoId(0);
PhotoId mediaPhotoId = PhotoId(0);
DocumentId mediaDocumentId = DocumentId(0);
uint64 backgroundEmojiId = 0; uint64 backgroundEmojiId = 0;
uint8 colorIndex : 6 = 0; uint8 colorIndex : 6 = 0;
bool isLinkInternal = false; bool isLinkInternal = false;
@ -73,6 +75,8 @@ public:
QString link; QString link;
QString buttonText; QString buttonText;
PhotoId photoId = PhotoId(0); PhotoId photoId = PhotoId(0);
PhotoId mediaPhotoId = PhotoId(0);
DocumentId mediaDocumentId = DocumentId(0);
uint64 backgroundEmojiId = 0; uint64 backgroundEmojiId = 0;
uint8 colorIndex : 6 = 0; uint8 colorIndex : 6 = 0;
bool isLinkInternal = false; bool isLinkInternal = false;

View file

@ -184,7 +184,11 @@ void ChannelData::setFlags(ChannelDataFlags which) {
}); });
} }
} }
if (diff & (Flag::Forum | Flag::CallNotEmpty | Flag::SimilarExpanded)) { if (diff & (Flag::Forum
| Flag::CallNotEmpty
| Flag::SimilarExpanded
| Flag::Signatures
| Flag::SignatureProfiles)) {
if (const auto history = this->owner().historyLoaded(this)) { if (const auto history = this->owner().historyLoaded(this)) {
if (diff & Flag::CallNotEmpty) { if (diff & Flag::CallNotEmpty) {
history->updateChatListEntry(); history->updateChatListEntry();
@ -203,6 +207,12 @@ void ChannelData::setFlags(ChannelDataFlags which) {
history->owner().requestItemResize(item); history->owner().requestItemResize(item);
} }
} }
if (diff & Flag::SignatureProfiles) {
history->forceFullResize();
}
if (diff & (Flag::Signatures | Flag::SignatureProfiles)) {
session().changes().peerUpdated(this, UpdateFlag::Rights);
}
} }
} }
if (const auto raw = taken.get()) { if (const auto raw = taken.get()) {
@ -534,12 +544,9 @@ auto ChannelData::unavailableReasons() const
return _unavailableReasons; return _unavailableReasons;
} }
void ChannelData::setUnavailableReasons( void ChannelData::setUnavailableReasonsList(
std::vector<Data::UnavailableReason> &&reasons) { std::vector<Data::UnavailableReason> &&reasons) {
if (_unavailableReasons != reasons) { _unavailableReasons = std::move(reasons);
_unavailableReasons = std::move(reasons);
session().changes().peerUpdated(this, UpdateFlag::UnavailableReason);
}
} }
void ChannelData::setAvailableMinId(MsgId availableMinId) { void ChannelData::setAvailableMinId(MsgId availableMinId) {
@ -966,7 +973,8 @@ void ChannelData::setAllowedReactions(Data::AllowedReactions value) {
if (_allowedReactions != value) { if (_allowedReactions != value) {
const auto enabled = [](const Data::AllowedReactions &allowed) { const auto enabled = [](const Data::AllowedReactions &allowed) {
return (allowed.type != Data::AllowedReactionsType::Some) return (allowed.type != Data::AllowedReactionsType::Some)
|| !allowed.some.empty(); || !allowed.some.empty()
|| allowed.paidEnabled;
}; };
const auto was = enabled(_allowedReactions); const auto was = enabled(_allowedReactions);
_allowedReactions = std::move(value); _allowedReactions = std::move(value);
@ -1028,6 +1036,14 @@ void ChannelData::updateLevelHint(int levelHint) {
_levelHint = levelHint; _levelHint = levelHint;
} }
TimeId ChannelData::subscriptionUntilDate() const {
return _subscriptionUntilDate;
}
void ChannelData::updateSubscriptionUntilDate(TimeId subscriptionUntilDate) {
_subscriptionUntilDate = subscriptionUntilDate;
}
namespace Data { namespace Data {
void ApplyMigration( void ApplyMigration(
@ -1220,11 +1236,16 @@ void ApplyChannelUpdate(
const auto reactionsLimit = update.vreactions_limit().value_or_empty(); const auto reactionsLimit = update.vreactions_limit().value_or_empty();
if (const auto allowed = update.vavailable_reactions()) { if (const auto allowed = update.vavailable_reactions()) {
auto parsed = Data::Parse(*allowed); auto parsed = Data::Parse(
parsed.maxCount = reactionsLimit; *allowed,
reactionsLimit,
update.is_paid_reactions_available());
channel->setAllowedReactions(std::move(parsed)); channel->setAllowedReactions(std::move(parsed));
} else { } else {
channel->setAllowedReactions({ .maxCount = reactionsLimit }); channel->setAllowedReactions({
.maxCount = reactionsLimit,
.paidEnabled = update.is_paid_reactions_available(),
});
} }
channel->owner().stories().apply(channel, update.vstories()); channel->owner().stories().apply(channel, update.vstories());
channel->fullUpdated(); channel->fullUpdated();

View file

@ -68,6 +68,7 @@ enum class ChannelDataFlag : uint64 {
CanViewRevenue = (1ULL << 32), CanViewRevenue = (1ULL << 32),
PaidMediaAllowed = (1ULL << 33), PaidMediaAllowed = (1ULL << 33),
CanViewCreditsRevenue = (1ULL << 34), CanViewCreditsRevenue = (1ULL << 34),
SignatureProfiles = (1ULL << 35),
}; };
inline constexpr bool is_flag_type(ChannelDataFlag) { return true; }; inline constexpr bool is_flag_type(ChannelDataFlag) { return true; };
using ChannelDataFlags = base::flags<ChannelDataFlag>; using ChannelDataFlags = base::flags<ChannelDataFlag>;
@ -231,6 +232,9 @@ public:
[[nodiscard]] bool addsSignature() const { [[nodiscard]] bool addsSignature() const {
return flags() & Flag::Signatures; return flags() & Flag::Signatures;
} }
[[nodiscard]] bool signatureProfiles() const {
return flags() & Flag::SignatureProfiles;
}
[[nodiscard]] bool isForbidden() const { [[nodiscard]] bool isForbidden() const {
return flags() & Flag::Forbidden; return flags() & Flag::Forbidden;
} }
@ -429,9 +433,6 @@ public:
return _ptsWaiter.waitingForShortPoll(); return _ptsWaiter.waitingForShortPoll();
} }
void setUnavailableReasons(
std::vector<Data::UnavailableReason> &&reason);
[[nodiscard]] MsgId availableMinId() const { [[nodiscard]] MsgId availableMinId() const {
return _availableMinId; return _availableMinId;
} }
@ -485,6 +486,9 @@ public:
[[nodiscard]] int levelHint() const; [[nodiscard]] int levelHint() const;
void updateLevelHint(int levelHint); void updateLevelHint(int levelHint);
[[nodiscard]] TimeId subscriptionUntilDate() const;
void updateSubscriptionUntilDate(TimeId subscriptionUntilDate);
// Still public data members. // Still public data members.
uint64 access = 0; uint64 access = 0;
@ -508,6 +512,9 @@ private:
-> const std::vector<Data::UnavailableReason> & override; -> const std::vector<Data::UnavailableReason> & override;
bool canEditLastAdmin(not_null<UserData*> user) const; bool canEditLastAdmin(not_null<UserData*> user) const;
void setUnavailableReasonsList(
std::vector<Data::UnavailableReason> &&reasons) override;
Flags _flags = ChannelDataFlags(Flag::Forbidden); Flags _flags = ChannelDataFlags(Flag::Forbidden);
PtsWaiter _ptsWaiter; PtsWaiter _ptsWaiter;
@ -527,6 +534,7 @@ private:
AdminRightFlags _adminRights; AdminRightFlags _adminRights;
RestrictionFlags _restrictions; RestrictionFlags _restrictions;
TimeId _restrictedUntil; TimeId _restrictedUntil;
TimeId _subscriptionUntilDate;
std::vector<Data::UnavailableReason> _unavailableReasons; std::vector<Data::UnavailableReason> _unavailableReasons;
std::unique_ptr<InvitePeek> _invitePeek; std::unique_ptr<InvitePeek> _invitePeek;

View file

@ -293,7 +293,8 @@ void ChatData::setAllowedReactions(Data::AllowedReactions value) {
if (_allowedReactions != value) { if (_allowedReactions != value) {
const auto enabled = [](const Data::AllowedReactions &allowed) { const auto enabled = [](const Data::AllowedReactions &allowed) {
return (allowed.type != Data::AllowedReactionsType::Some) return (allowed.type != Data::AllowedReactionsType::Some)
|| !allowed.some.empty(); || !allowed.some.empty()
|| allowed.paidEnabled;
}; };
const auto was = enabled(_allowedReactions); const auto was = enabled(_allowedReactions);
_allowedReactions = std::move(value); _allowedReactions = std::move(value);
@ -486,8 +487,8 @@ void ApplyChatUpdate(not_null<ChatData*> chat, const MTPDchatFull &update) {
chat->setTranslationDisabled(update.is_translations_disabled()); chat->setTranslationDisabled(update.is_translations_disabled());
const auto reactionsLimit = update.vreactions_limit().value_or_empty(); const auto reactionsLimit = update.vreactions_limit().value_or_empty();
if (const auto allowed = update.vavailable_reactions()) { if (const auto allowed = update.vavailable_reactions()) {
auto parsed = Data::Parse(*allowed); const auto paidEnabled = false;
parsed.maxCount = reactionsLimit; auto parsed = Data::Parse(*allowed, reactionsLimit, paidEnabled);
chat->setAllowedReactions(std::move(parsed)); chat->setAllowedReactions(std::move(parsed));
} else { } else {
chat->setAllowedReactions({ .maxCount = reactionsLimit }); chat->setAllowedReactions({ .maxCount = reactionsLimit });

View file

@ -7,6 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#pragma once #pragma once
#include "data/data_subscriptions.h"
namespace Data { namespace Data {
struct CreditTopupOption final { struct CreditTopupOption final {
@ -31,6 +33,10 @@ struct CreditsHistoryMedia {
}; };
struct CreditsHistoryEntry final { struct CreditsHistoryEntry final {
explicit operator bool() const {
return !id.isEmpty();
}
using PhotoId = uint64; using PhotoId = uint64;
enum class PeerType { enum class PeerType {
Peer, Peer,
@ -52,11 +58,13 @@ struct CreditsHistoryEntry final {
uint64 bareMsgId = 0; uint64 bareMsgId = 0;
uint64 barePeerId = 0; uint64 barePeerId = 0;
PeerType peerType; PeerType peerType;
QDateTime subscriptionUntil;
QDateTime successDate;
QString successLink;
bool reaction = false;
bool refunded = false; bool refunded = false;
bool pending = false; bool pending = false;
bool failed = false; bool failed = false;
QDateTime successDate;
QString successLink;
bool in = false; bool in = false;
bool gift = false; bool gift = false;
}; };
@ -64,9 +72,12 @@ struct CreditsHistoryEntry final {
struct CreditsStatusSlice final { struct CreditsStatusSlice final {
using OffsetToken = QString; using OffsetToken = QString;
std::vector<CreditsHistoryEntry> list; std::vector<CreditsHistoryEntry> list;
std::vector<SubscriptionEntry> subscriptions;
uint64 balance = 0; uint64 balance = 0;
uint64 subscriptionsMissingBalance = 0;
bool allLoaded = false; bool allLoaded = false;
OffsetToken token; OffsetToken token;
OffsetToken tokenSubscriptions;
}; };
} // namespace Data } // namespace Data

View file

@ -143,7 +143,7 @@ void DownloadManager::trackSession(not_null<Main::Session*> session) {
}, data.lifetime); }, data.lifetime);
session->data().itemViewRefreshRequest( session->data().itemViewRefreshRequest(
) | rpl::start_with_next([=](not_null<HistoryItem*> item) { ) | rpl::start_with_next([=](not_null<const HistoryItem*> item) {
changed(item); changed(item);
}, data.lifetime); }, data.lifetime);

View file

@ -242,7 +242,8 @@ template <typename MediaType>
ImageRoundRadius radius, ImageRoundRadius radius,
bool spoiler) { bool spoiler) {
auto result = PreparePhotoPreviewImage(item, media, radius, spoiler); auto result = PreparePhotoPreviewImage(item, media, radius, spoiler);
if (media->owner()->extendedMediaVideoDuration().has_value()) { if (!result.data.isNull()
&& media->owner()->extendedMediaVideoDuration().has_value()) {
result.data = PutPlayIcon(std::move(result.data)); result.data = PutPlayIcon(std::move(result.data));
} }
return result; return result;
@ -2266,7 +2267,6 @@ ClickHandlerPtr MediaDice::MakeHandler(
.text = { tr::lng_about_random(tr::now, lt_emoji, emoji) }, .text = { tr::lng_about_random(tr::now, lt_emoji, emoji) },
.st = &st::historyDiceToast, .st = &st::historyDiceToast,
.duration = Ui::Toast::kDefaultDuration * 2, .duration = Ui::Toast::kDefaultDuration * 2,
.multiline = true,
}; };
if (CanSend(history->peer, ChatRestriction::SendOther)) { if (CanSend(history->peer, ChatRestriction::SendOther)) {
auto link = Ui::Text::Link(tr::lng_about_random_send(tr::now)); auto link = Ui::Text::Link(tr::lng_about_random_send(tr::now));
@ -2295,7 +2295,7 @@ ClickHandlerPtr MediaDice::MakeHandler(
if (const auto strong = weak.get()) { if (const auto strong = weak.get()) {
ShownToast = strong->showToast(std::move(config)); ShownToast = strong->showToast(std::move(config));
} else { } else {
ShownToast = Ui::Toast::Show(config); ShownToast = Ui::Toast::Show(std::move(config));
} }
}); });
} }

View file

@ -56,17 +56,21 @@ ReactionId ReactionFromMTP(const MTPReaction &reaction) {
return ReactionId{ qs(data.vemoticon()) }; return ReactionId{ qs(data.vemoticon()) };
}, [](const MTPDreactionCustomEmoji &data) { }, [](const MTPDreactionCustomEmoji &data) {
return ReactionId{ DocumentId(data.vdocument_id().v) }; return ReactionId{ DocumentId(data.vdocument_id().v) };
}, [](const MTPDreactionPaid &) {
return ReactionId::Paid();
}); });
} }
MTPReaction ReactionToMTP(ReactionId id) { MTPReaction ReactionToMTP(ReactionId id) {
if (const auto custom = id.custom()) { if (!id) {
return MTP_reactionEmpty();
} else if (id.paid()) {
return MTP_reactionPaid();
} else if (const auto custom = id.custom()) {
return MTP_reactionCustomEmoji(MTP_long(custom)); return MTP_reactionCustomEmoji(MTP_long(custom));
} else {
return MTP_reactionEmoji(MTP_string(id.emoji()));
} }
const auto emoji = id.emoji();
return emoji.isEmpty()
? MTP_reactionEmpty()
: MTP_reactionEmoji(MTP_string(emoji));
} }
} // namespace Data } // namespace Data

View file

@ -14,13 +14,28 @@ namespace Data {
struct ReactionId { struct ReactionId {
std::variant<QString, DocumentId> data; std::variant<QString, DocumentId> data;
[[nodiscard]] static QChar PaidTag() {
return '*';
}
[[nodiscard]] static ReactionId Paid() {
return { QString(PaidTag()) };
}
[[nodiscard]] bool empty() const { [[nodiscard]] bool empty() const {
const auto emoji = std::get_if<QString>(&data); const auto emoji = std::get_if<QString>(&data);
return emoji && emoji->isEmpty(); return emoji && emoji->isEmpty();
} }
[[nodiscard]] bool paid() const {
const auto emoji = std::get_if<QString>(&data);
return emoji
&& emoji->size() == 1
&& emoji->at(0) == PaidTag();
}
[[nodiscard]] QString emoji() const { [[nodiscard]] QString emoji() const {
const auto emoji = std::get_if<QString>(&data); const auto emoji = std::get_if<QString>(&data);
return emoji ? *emoji : QString(); return (emoji && (emoji->size() != 1 || emoji->at(0) != PaidTag()))
? *emoji
: QString();
} }
[[nodiscard]] DocumentId custom() const { [[nodiscard]] DocumentId custom() const {
const auto custom = std::get_if<DocumentId>(&data); const auto custom = std::get_if<DocumentId>(&data);

View file

@ -7,12 +7,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#include "data/data_message_reactions.h" #include "data/data_message_reactions.h"
#include "chat_helpers/stickers_lottie.h"
#include "core/application.h"
#include "history/history.h" #include "history/history.h"
#include "history/history_item.h" #include "history/history_item.h"
#include "history/history_item_components.h" #include "history/history_item_components.h"
#include "main/main_session.h" #include "main/main_session.h"
#include "main/main_app_config.h" #include "main/main_app_config.h"
#include "main/session/send_as_peers.h" #include "main/session/send_as_peers.h"
#include "data/components/credits.h"
#include "data/data_user.h" #include "data/data_user.h"
#include "data/data_session.h" #include "data/data_session.h"
#include "data/data_histories.h" #include "data/data_histories.h"
@ -29,9 +32,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "mtproto/mtproto_config.h" #include "mtproto/mtproto_config.h"
#include "base/timer_rpl.h" #include "base/timer_rpl.h"
#include "base/call_delayed.h" #include "base/call_delayed.h"
#include "base/unixtime.h"
#include "apiwrap.h" #include "apiwrap.h"
#include "styles/style_chat.h" #include "styles/style_chat.h"
#include "base/random.h"
// AyuGram includes // AyuGram includes
#include "ayu/ayu_settings.h" #include "ayu/ayu_settings.h"
#include "ayu/utils/telegram_helpers.h" #include "ayu/utils/telegram_helpers.h"
@ -48,6 +54,7 @@ constexpr auto kRecentReactionsLimit = 40;
constexpr auto kMyTagsRequestTimeout = crl::time(1000); constexpr auto kMyTagsRequestTimeout = crl::time(1000);
constexpr auto kTopRequestDelay = 60 * crl::time(1000); constexpr auto kTopRequestDelay = 60 * crl::time(1000);
constexpr auto kTopReactionsLimit = 14; constexpr auto kTopReactionsLimit = 14;
constexpr auto kPaidAccumulatePeriod = 5 * crl::time(1000) + 500;
[[nodiscard]] QString ReactionIdToLog(const ReactionId &id) { [[nodiscard]] QString ReactionIdToLog(const ReactionId &id) {
if (const auto custom = id.custom()) { if (const auto custom = id.custom()) {
@ -112,17 +119,17 @@ constexpr auto kTopReactionsLimit = 14;
: config->get<int>("reactions_user_max_default", 1); : config->get<int>("reactions_user_max_default", 1);
} }
bool IsMyRecent( [[nodiscard]] bool IsMyRecent(
const MTPDmessagePeerReaction &data, const MTPDmessagePeerReaction &data,
const ReactionId &id, const ReactionId &id,
not_null<PeerData*> peer, not_null<PeerData*> peer,
const base::flat_map< const base::flat_map<
ReactionId, ReactionId,
std::vector<RecentReaction>> &recent, std::vector<RecentReaction>> &recent,
bool ignoreChosen) { bool min) {
if (peer->id == peer->session().userPeerId()) { if (peer->isSelf()) {
return true; return true;
} else if (!ignoreChosen) { } else if (!min) {
return data.is_my(); return data.is_my();
} }
const auto j = recent.find(id); const auto j = recent.find(id);
@ -136,10 +143,25 @@ bool IsMyRecent(
return (k != end(j->second)) && k->my; return (k != end(j->second)) && k->my;
} }
[[nodiscard]] bool IsMyTop(
const MTPDmessageReactor &data,
PeerData *peer,
const std::vector<MessageReactionsTopPaid> &top,
bool min) {
if (peer && peer->isSelf()) {
return true;
} else if (!min) {
return data.is_my();
}
const auto i = ranges::find(top, peer, &MessageReactionsTopPaid::peer);
return (i != end(top)) && i->my;
}
} // namespace } // namespace
PossibleItemReactionsRef LookupPossibleReactions( PossibleItemReactionsRef LookupPossibleReactions(
not_null<HistoryItem*> item) { not_null<HistoryItem*> item,
bool paidInFront) {
if (!item->canReact()) { if (!item->canReact()) {
return {}; return {};
} }
@ -160,6 +182,7 @@ PossibleItemReactionsRef LookupPossibleReactions(
const auto &myTags = reactions->list(Reactions::Type::MyTags); const auto &myTags = reactions->list(Reactions::Type::MyTags);
const auto &tags = reactions->list(Reactions::Type::Tags); const auto &tags = reactions->list(Reactions::Type::Tags);
const auto &all = item->reactions(); const auto &all = item->reactions();
const auto &allowed = PeerAllowedReactions(peer);
const auto limit = UniqueReactionsLimit(peer); const auto limit = UniqueReactionsLimit(peer);
const auto premiumPossible = session->premiumPossible(); const auto premiumPossible = session->premiumPossible();
const auto limited = (all.size() >= limit) && [&] { const auto limited = (all.size() >= limit) && [&] {
@ -195,23 +218,30 @@ PossibleItemReactionsRef LookupPossibleReactions(
result.customAllowed = premiumPossible; result.customAllowed = premiumPossible;
result.tags = true; result.tags = true;
} else if (limited) { } else if (limited) {
result.recent.reserve(all.size()); result.recent.reserve((allowed.paidEnabled ? 1 : 0) + all.size());
add([&](const Reaction &reaction) { add([&](const Reaction &reaction) {
return ranges::contains(all, reaction.id, &MessageReaction::id); return ranges::contains(all, reaction.id, &MessageReaction::id);
}); });
for (const auto &reaction : all) { for (const auto &reaction : all) {
const auto id = reaction.id; const auto id = reaction.id;
if (!added.contains(id)) { if (added.emplace(id).second) {
if (const auto temp = reactions->lookupTemporary(id)) { if (const auto temp = reactions->lookupTemporary(id)) {
result.recent.push_back(temp); result.recent.push_back(temp);
} }
} }
} }
if (allowed.paidEnabled
&& !added.contains(Data::ReactionId::Paid())) {
result.recent.push_back(reactions->lookupPaid());
}
} else { } else {
const auto &allowed = PeerAllowedReactions(peer); result.recent.reserve((allowed.paidEnabled ? 1 : 0)
result.recent.reserve((allowed.type == AllowedReactionsType::Some) + ((allowed.type == AllowedReactionsType::Some)
? allowed.some.size() ? allowed.some.size()
: full.size()); : full.size()));
if (allowed.paidEnabled) {
result.recent.push_back(reactions->lookupPaid());
}
add([&](const Reaction &reaction) { add([&](const Reaction &reaction) {
const auto id = reaction.id; const auto id = reaction.id;
if (id.custom() && !premiumPossible) { if (id.custom() && !premiumPossible) {
@ -238,12 +268,15 @@ PossibleItemReactionsRef LookupPossibleReactions(
&& premiumPossible; && premiumPossible;
} }
if (!item->reactionsAreTags()) { if (!item->reactionsAreTags()) {
const auto i = ranges::find( const auto toFront = [&](Data::ReactionId id) {
result.recent, const auto i = ranges::find(result.recent, id, &Reaction::id);
reactions->favoriteId(), if (i != end(result.recent) && i != begin(result.recent)) {
&Reaction::id); std::rotate(begin(result.recent), i, i + 1);
if (i != end(result.recent) && i != begin(result.recent)) { }
std::rotate(begin(result.recent), i, i + 1); };
toFront(reactions->favoriteId());
if (paidInFront) {
toFront(Data::ReactionId::Paid());
} }
} }
return result; return result;
@ -264,7 +297,8 @@ PossibleItemReactions::PossibleItemReactions(
Reactions::Reactions(not_null<Session*> owner) Reactions::Reactions(not_null<Session*> owner)
: _owner(owner) : _owner(owner)
, _topRefreshTimer([=] { refreshTop(); }) , _topRefreshTimer([=] { refreshTop(); })
, _repaintTimer([=] { repaintCollected(); }) { , _repaintTimer([=] { repaintCollected(); })
, _sendPaidTimer([=] { sendPaid(); }) {
refreshDefault(); refreshDefault();
_myTags.emplace(nullptr); _myTags.emplace(nullptr);
@ -283,6 +317,15 @@ Reactions::Reactions(not_null<Session*> owner)
_pollingItems.remove(item); _pollingItems.remove(item);
_pollItems.remove(item); _pollItems.remove(item);
_repaintItems.remove(item); _repaintItems.remove(item);
_sendPaidItems.remove(item);
if (const auto i = _sendingPaid.find(item)
; i != end(_sendingPaid)) {
_sendingPaid.erase(i);
_owner->session().credits().invalidate();
crl::on_main(&_owner->session(), [=] {
sendPaid();
});
}
}, _lifetime); }, _lifetime);
crl::on_main(&owner->session(), [=] { crl::on_main(&owner->session(), [=] {
@ -509,21 +552,49 @@ DocumentData *Reactions::chooseGenericAnimation(
return i->aroundAnimation; return i->aroundAnimation;
} }
} }
if (_genericAnimations.empty()) { return randomLoadedFrom(_genericAnimations);
}
void Reactions::fillPaidReactionAnimations() const {
const auto generate = [&](int index) {
const auto session = &_owner->session();
const auto name = u"star_reaction_effect%1"_q.arg(index + 1);
return ChatHelpers::GenerateLocalTgsSticker(session, name);
};
const auto kCount = 3;
for (auto i = 0; i != kCount; ++i) {
const auto document = generate(i);
_paidReactionAnimations.push_back(document);
_paidReactionCache.emplace(
document,
document->createMediaView());
}
_paidReactionCache.front().second->checkStickerLarge();
}
DocumentData *Reactions::choosePaidReactionAnimation() const {
if (_paidReactionAnimations.empty()) {
fillPaidReactionAnimations();
}
return randomLoadedFrom(_paidReactionAnimations);
}
DocumentData *Reactions::randomLoadedFrom(
std::vector<not_null<DocumentData*>> list) const {
if (list.empty()) {
return nullptr; return nullptr;
} }
auto copy = _genericAnimations; ranges::shuffle(list);
ranges::shuffle(copy); const auto first = list.front();
const auto first = copy.front();
const auto view = first->createMediaView(); const auto view = first->createMediaView();
view->checkStickerLarge(); view->checkStickerLarge();
if (view->loaded()) { if (view->loaded()) {
return first; return first;
} }
const auto k = ranges::find_if(copy, [&](not_null<DocumentData*> value) { const auto k = ranges::find_if(list, [&](not_null<DocumentData*> value) {
return value->createMediaView()->loaded(); return value->createMediaView()->loaded();
}); });
return (k != end(copy)) ? (*k) : first; return (k != end(list)) ? (*k) : first;
} }
void Reactions::applyFavorite(const ReactionId &id) { void Reactions::applyFavorite(const ReactionId &id) {
@ -574,7 +645,7 @@ rpl::producer<> Reactions::effectsUpdates() const {
} }
void Reactions::preloadReactionImageFor(const ReactionId &emoji) { void Reactions::preloadReactionImageFor(const ReactionId &emoji) {
if (!emoji.emoji().isEmpty()) { if (emoji.paid() || !emoji.emoji().isEmpty()) {
preloadImageFor(emoji); preloadImageFor(emoji);
} }
} }
@ -591,6 +662,10 @@ void Reactions::preloadImageFor(const ReactionId &id) {
} }
auto &set = _images.emplace(id).first->second; auto &set = _images.emplace(id).first->second;
set.effect = (id.custom() != 0); set.effect = (id.custom() != 0);
if (id.paid()) {
loadImage(set, lookupPaid()->centerIcon, true);
return;
}
auto &list = set.effect ? _effects : _available; auto &list = set.effect ? _effects : _available;
const auto i = ranges::find(list, id, &Reaction::id); const auto i = ranges::find(list, id, &Reaction::id);
const auto document = (i == end(list)) const auto document = (i == end(list))
@ -626,6 +701,20 @@ void Reactions::preloadEffect(const Reaction &effect) {
} }
void Reactions::preloadAnimationsFor(const ReactionId &id) { void Reactions::preloadAnimationsFor(const ReactionId &id) {
const auto preload = [&](DocumentData *document) {
const auto view = document
? document->activeMediaView()
: nullptr;
if (view) {
view->checkStickerLarge();
}
};
if (id.paid()) {
const auto fake = lookupPaid();
preload(fake->centerIcon);
preload(fake->aroundAnimation);
return;
}
const auto custom = id.custom(); const auto custom = id.custom();
const auto document = custom ? _owner->document(custom).get() : nullptr; const auto document = custom ? _owner->document(custom).get() : nullptr;
const auto customSticker = document ? document->sticker() : nullptr; const auto customSticker = document ? document->sticker() : nullptr;
@ -636,15 +725,6 @@ void Reactions::preloadAnimationsFor(const ReactionId &id) {
if (i == end(_available)) { if (i == end(_available)) {
return; return;
} }
const auto preload = [&](DocumentData *document) {
const auto view = document
? document->activeMediaView()
: nullptr;
if (view) {
view->checkStickerLarge();
}
};
if (!custom) { if (!custom) {
preload(i->centerIcon); preload(i->centerIcon);
} }
@ -1361,7 +1441,10 @@ void Reactions::send(not_null<HistoryItem*> item, bool addToRecent) {
MTP_flags(flags), MTP_flags(flags),
item->history()->peer->input, item->history()->peer->input,
MTP_int(id.msg), MTP_int(id.msg),
MTP_vector<MTPReaction>(chosen | ranges::views::transform( MTP_vector<MTPReaction>(chosen | ranges::views::filter([](
const ReactionId &id) {
return !id.paid();
}) | ranges::views::transform(
ReactionToMTP ReactionToMTP
) | ranges::to<QVector<MTPReaction>>()) ) | ranges::to<QVector<MTPReaction>>())
)).done([=](const MTPUpdates &result) { )).done([=](const MTPUpdates &result) {
@ -1413,7 +1496,9 @@ void Reactions::clearTemporary() {
} }
Reaction *Reactions::lookupTemporary(const ReactionId &id) { Reaction *Reactions::lookupTemporary(const ReactionId &id) {
if (const auto emoji = id.emoji(); !emoji.isEmpty()) { if (id.paid()) {
return lookupPaid();
} else if (const auto emoji = id.emoji(); !emoji.isEmpty()) {
const auto i = ranges::find(_available, id, &Reaction::id); const auto i = ranges::find(_available, id, &Reaction::id);
return (i != end(_available)) ? &*i : nullptr; return (i != end(_available)) ? &*i : nullptr;
} else if (const auto customId = id.custom()) { } else if (const auto customId = id.custom()) {
@ -1434,6 +1519,41 @@ Reaction *Reactions::lookupTemporary(const ReactionId &id) {
return nullptr; return nullptr;
} }
not_null<Reaction*> Reactions::lookupPaid() {
if (!_paid) {
const auto generate = [&](const QString &name) {
const auto session = &_owner->session();
return ChatHelpers::GenerateLocalTgsSticker(session, name);
};
const auto appear = generate(u"star_reaction_appear"_q);
const auto center = generate(u"star_reaction_center"_q);
const auto select = generate(u"star_reaction_select"_q);
_paid.emplace(Reaction{
.id = ReactionId::Paid(),
.title = u"Telegram Star"_q,
.appearAnimation = appear,
.selectAnimation = select,
.centerIcon = center,
.active = true,
});
_iconsCache.emplace(appear, appear->createMediaView());
_iconsCache.emplace(center, center->createMediaView());
_iconsCache.emplace(select, select->createMediaView());
fillPaidReactionAnimations();
}
return &*_paid;
}
not_null<DocumentData*> Reactions::paidToastAnimation() {
if (!_paidToastAnimation) {
_paidToastAnimation = ChatHelpers::GenerateLocalTgsSticker(
&_owner->session(),
u"star_reaction_toast"_q);
}
return _paidToastAnimation;
}
rpl::producer<std::vector<Reaction>> Reactions::myTagsValue( rpl::producer<std::vector<Reaction>> Reactions::myTagsValue(
SavedSublist *sublist) { SavedSublist *sublist) {
refreshMyTags(sublist); refreshMyTags(sublist);
@ -1448,6 +1568,45 @@ rpl::producer<std::vector<Reaction>> Reactions::myTagsValue(
) | rpl::map(list)); ) | rpl::map(list));
} }
bool Reactions::isQuitPrevent() {
for (auto i = begin(_sendPaidItems); i != end(_sendPaidItems);) {
const auto item = i->first;
if (_sendingPaid.contains(item)) {
++i;
} else {
i = _sendPaidItems.erase(i);
sendPaid(item);
}
}
if (_sendingPaid.empty()) {
return false;
}
LOG(("Reactions prevents quit, sending paid..."));
return true;
}
void Reactions::schedulePaid(not_null<HistoryItem*> item) {
_sendPaidItems[item] = crl::now() + kPaidAccumulatePeriod;
if (!_sendPaidTimer.isActive()) {
_sendPaidTimer.callOnce(kPaidAccumulatePeriod);
}
}
void Reactions::undoScheduledPaid(not_null<HistoryItem*> item) {
_sendPaidItems.remove(item);
item->cancelScheduledPaidReaction();
}
crl::time Reactions::sendingScheduledPaidAt(
not_null<HistoryItem*> item) const {
const auto i = _sendPaidItems.find(item);
return (i != end(_sendPaidItems)) ? i->second : crl::time();
}
crl::time Reactions::ScheduledPaidDelay() {
return kPaidAccumulatePeriod;
}
void Reactions::repaintCollected() { void Reactions::repaintCollected() {
const auto now = crl::now(); const auto now = crl::now();
auto closest = crl::time(); auto closest = crl::time();
@ -1503,7 +1662,8 @@ void Reactions::pollCollected() {
} }
bool Reactions::sending(not_null<HistoryItem*> item) const { bool Reactions::sending(not_null<HistoryItem*> item) const {
return _sentRequests.contains(item->fullId()); return _sentRequests.contains(item->fullId())
|| _sendingPaid.contains(item);
} }
bool Reactions::HasUnread(const MTPMessageReactions &data) { bool Reactions::HasUnread(const MTPMessageReactions &data) {
@ -1535,12 +1695,150 @@ void Reactions::CheckUnknownForUnread(
}); });
} }
void Reactions::sendPaid() {
if (!_sendingPaid.empty()) {
return;
}
auto next = crl::time();
const auto now = crl::now();
for (auto i = begin(_sendPaidItems); i != end(_sendPaidItems);) {
const auto item = i->first;
const auto when = i->second;
if (when > now) {
if (!next || next > when) {
next = when;
}
++i;
} else {
i = _sendPaidItems.erase(i);
if (sendPaid(item)) {
return;
}
}
}
if (next) {
_sendPaidTimer.callOnce(next - now);
}
}
bool Reactions::sendPaid(not_null<HistoryItem*> item) {
const auto send = item->startPaidReactionSending();
if (!send.valid) {
return false;
}
sendPaidRequest(item, send);
return true;
}
void Reactions::sendPaidPrivacyRequest(
not_null<HistoryItem*> item,
PaidReactionSend send) {
Expects(!_sendingPaid.contains(item));
Expects(!send.count);
const auto id = item->fullId();
auto &api = _owner->session().api();
const auto requestId = api.request(
MTPmessages_TogglePaidReactionPrivacy(
item->history()->peer->input,
MTP_int(id.msg),
MTP_bool(send.anonymous))
).done([=] {
if (const auto item = _owner->message(id)) {
if (_sendingPaid.remove(item)) {
sendPaidFinish(item, send, true);
}
}
checkQuitPreventFinished();
}).fail([=](const MTP::Error &error) {
if (const auto item = _owner->message(id)) {
if (_sendingPaid.remove(item)) {
sendPaidFinish(item, send, false);
}
}
checkQuitPreventFinished();
}).send();
_sendingPaid[item] = requestId;
}
void Reactions::sendPaidRequest(
not_null<HistoryItem*> item,
PaidReactionSend send) {
Expects(!_sendingPaid.contains(item));
if (!send.count) {
sendPaidPrivacyRequest(item, send);
return;
}
const auto id = item->fullId();
const auto randomId = base::unixtime::mtproto_msg_id();
auto &api = _owner->session().api();
using Flag = MTPmessages_SendPaidReaction::Flag;
const auto requestId = api.request(MTPmessages_SendPaidReaction(
MTP_flags(send.anonymous ? Flag::f_private : Flag()),
item->history()->peer->input,
MTP_int(id.msg),
MTP_int(send.count),
MTP_long(randomId)
)).done([=](const MTPUpdates &result) {
if (const auto item = _owner->message(id)) {
if (_sendingPaid.remove(item)) {
sendPaidFinish(item, send, true);
}
}
_owner->session().api().applyUpdates(result);
checkQuitPreventFinished();
}).fail([=](const MTP::Error &error) {
if (const auto item = _owner->message(id)) {
_sendingPaid.remove(item);
if (error.type() == u"RANDOM_ID_EXPIRED"_q) {
sendPaidRequest(item, send);
} else {
sendPaidFinish(item, send, false);
}
}
checkQuitPreventFinished();
}).send();
_sendingPaid[item] = requestId;
}
void Reactions::checkQuitPreventFinished() {
if (_sendingPaid.empty()) {
if (Core::Quitting()) {
LOG(("Reactions doesn't prevent quit any more."));
}
Core::App().quitPreventFinished();
}
}
void Reactions::sendPaidFinish(
not_null<HistoryItem*> item,
PaidReactionSend send,
bool success) {
item->finishPaidReactionSending(send, success);
sendPaid();
}
MessageReactions::MessageReactions(not_null<HistoryItem*> item) MessageReactions::MessageReactions(not_null<HistoryItem*> item)
: _item(item) { : _item(item) {
} }
MessageReactions::~MessageReactions() {
cancelScheduledPaid();
if (const auto paid = _paid.get()) {
if (paid->sending > 0) {
finishPaidSending(
{ int(paid->sending), (paid->sendingAnonymous == 1) },
false);
}
}
}
void MessageReactions::add(const ReactionId &id, bool addToRecent) { void MessageReactions::add(const ReactionId &id, bool addToRecent) {
Expects(!id.empty()); Expects(!id.empty());
Expects(!id.paid());
const auto history = _item->history(); const auto history = _item->history();
const auto myLimit = SentReactionsLimit(_item); const auto myLimit = SentReactionsLimit(_item);
@ -1554,6 +1852,9 @@ void MessageReactions::add(const ReactionId &id, bool addToRecent) {
history->owner().reactions().incrementMyTag(id, sublist); history->owner().reactions().incrementMyTag(id, sublist);
} }
_list.erase(ranges::remove_if(_list, [&](MessageReaction &one) { _list.erase(ranges::remove_if(_list, [&](MessageReaction &one) {
if (one.id.paid()) {
return false;
}
const auto removing = one.my && (my == myLimit || ++my == myLimit); const auto removing = one.my && (my == myLimit || ++my == myLimit);
if (!removing) { if (!removing) {
return false; return false;
@ -1603,6 +1904,8 @@ void MessageReactions::add(const ReactionId &id, bool addToRecent) {
} }
void MessageReactions::remove(const ReactionId &id) { void MessageReactions::remove(const ReactionId &id) {
Expects(!id.paid());
const auto history = _item->history(); const auto history = _item->history();
const auto self = history->session().user(); const auto self = history->session().user();
const auto i = ranges::find(_list, id, &MessageReaction::id); const auto i = ranges::find(_list, id, &MessageReaction::id);
@ -1706,6 +2009,7 @@ bool MessageReactions::checkIfChanged(
bool MessageReactions::change( bool MessageReactions::change(
const QVector<MTPReactionCount> &list, const QVector<MTPReactionCount> &list,
const QVector<MTPMessagePeerReaction> &recent, const QVector<MTPMessagePeerReaction> &recent,
const QVector<MTPMessageReactor> &top,
bool min) { bool min) {
auto &owner = _item->history()->owner(); auto &owner = _item->history()->owner();
if (owner.reactions().sending(_item)) { if (owner.reactions().sending(_item)) {
@ -1785,8 +2089,7 @@ bool MessageReactions::change(
if (list.size() >= i->count) { if (list.size() >= i->count) {
return; return;
} }
const auto peerId = peerFromMTP(data.vpeer_id()); const auto peer = owner.peer(peerFromMTP(data.vpeer_id()));
const auto peer = owner.peer(peerId);
const auto my = IsMyRecent(data, id, peer, _recent, min); const auto my = IsMyRecent(data, id, peer, _recent, min);
list.push_back({ list.push_back({
.peer = peer, .peer = peer,
@ -1800,6 +2103,57 @@ bool MessageReactions::change(
_recent = std::move(parsed); _recent = std::move(parsed);
changed = true; changed = true;
} }
auto paidTop = std::vector<TopPaid>();
const auto &paindTopNow = _paid ? _paid->top : std::vector<TopPaid>();
for (const auto &reactor : top) {
const auto &data = reactor.data();
const auto peerId = (data.is_anonymous() || !data.vpeer_id())
? PeerId()
: peerFromMTP(*data.vpeer_id());
const auto peer = peerId ? owner.peer(peerId).get() : nullptr;
paidTop.push_back({
.peer = peer,
.count = uint32(data.vcount().v),
.top = data.is_top(),
.my = IsMyTop(data, peer, paindTopNow, min),
});
}
if (paidTop.empty()) {
if (_paid && !_paid->top.empty()) {
changed = true;
if (localPaidData()) {
_paid->top.clear();
} else {
_paid = nullptr;
}
}
} else {
if (min && _paid) {
const auto mine = [](const TopPaid &entry) {
return entry.my != 0;
};
if (!ranges::contains(paidTop, true, mine)) {
const auto nonTopMine = [](const TopPaid &entry) {
return entry.my && !entry.top;
};
const auto i = ranges::find(_paid->top, true, nonTopMine);
if (i != end(_paid->top)) {
paidTop.push_back(*i);
}
}
}
ranges::sort(paidTop, std::greater(), [](const TopPaid &entry) {
return entry.count;
});
if (!_paid) {
_paid = std::make_unique<Paid>();
}
if (_paid->top != paidTop) {
_paid->top = std::move(paidTop);
changed = true;
}
}
return changed; return changed;
} }
@ -1812,6 +2166,11 @@ auto MessageReactions::recent() const
return _recent; return _recent;
} }
auto MessageReactions::topPaid() const -> const std::vector<TopPaid> & {
static const auto kEmpty = std::vector<TopPaid>();
return _paid ? _paid->top : kEmpty;
}
bool MessageReactions::empty() const { bool MessageReactions::empty() const {
return _list.empty(); return _list.empty();
} }
@ -1833,6 +2192,128 @@ void MessageReactions::markRead() {
} }
} }
void MessageReactions::scheduleSendPaid(int count, bool anonymous) {
Expects(count >= 0);
if (!_paid) {
_paid = std::make_unique<Paid>();
}
_paid->scheduled += count;
_paid->scheduledFlag = 1;
_paid->scheduledAnonymous = anonymous ? 1 : 0;
if (count > 0) {
_item->history()->session().credits().lock(count);
}
_item->history()->owner().reactions().schedulePaid(_item);
}
int MessageReactions::scheduledPaid() const {
return _paid ? _paid->scheduled : 0;
}
void MessageReactions::cancelScheduledPaid() {
if (_paid) {
if (_paid->scheduledFlag) {
if (const auto amount = int(_paid->scheduled)) {
_item->history()->session().credits().unlock(amount);
}
_paid->scheduled = 0;
_paid->scheduledFlag = 0;
_paid->scheduledAnonymous = 0;
}
if (!_paid->sendingFlag && _paid->top.empty()) {
_paid = nullptr;
}
}
}
PaidReactionSend MessageReactions::startPaidSending() {
if (!_paid || !_paid->scheduledFlag || _paid->sendingFlag) {
return {};
}
_paid->sending = _paid->scheduled;
_paid->sendingFlag = _paid->scheduledFlag;
_paid->sendingAnonymous = _paid->scheduledAnonymous;
_paid->scheduled = 0;
_paid->scheduledFlag = 0;
_paid->scheduledAnonymous = 0;
return {
.count = int(_paid->sending),
.valid = true,
.anonymous = (_paid->sendingAnonymous == 1),
};
}
void MessageReactions::finishPaidSending(
PaidReactionSend send,
bool success) {
Expects(_paid != nullptr);
Expects(send.count == _paid->sending);
Expects(send.valid == (_paid->sendingFlag == 1));
Expects(send.anonymous == (_paid->sendingAnonymous == 1));
_paid->sending = 0;
_paid->sendingFlag = 0;
_paid->sendingAnonymous = 0;
if (!_paid->scheduledFlag && _paid->top.empty()) {
_paid = nullptr;
} else if (!send.count) {
const auto i = ranges::find_if(_paid->top, [](const TopPaid &top) {
return top.my;
});
if (i != end(_paid->top)) {
i->peer = send.anonymous
? nullptr
: _item->history()->session().user().get();
}
}
if (const auto amount = send.count) {
const auto credits = &_item->history()->session().credits();
if (success) {
credits->withdrawLocked(amount);
} else {
credits->unlock(amount);
}
}
}
bool MessageReactions::localPaidData() const {
return _paid && (_paid->scheduledFlag || _paid->sendingFlag);
}
int MessageReactions::localPaidCount() const {
return _paid ? (_paid->scheduled + _paid->sending) : 0;
}
bool MessageReactions::localPaidAnonymous() const {
const auto minePaidAnonymous = [&] {
for (const auto &entry : _paid->top) {
if (entry.my) {
return !entry.peer;
}
}
return false;
};
return _paid
&& (_paid->scheduledFlag
? (_paid->scheduledAnonymous == 1)
: _paid->sendingFlag
? (_paid->sendingAnonymous == 1)
: minePaidAnonymous());
}
bool MessageReactions::clearCloudData() {
const auto result = !_list.empty();
_recent.clear();
_list.clear();
if (localPaidData()) {
_paid->top.clear();
} else {
_paid = nullptr;
}
return result;
}
std::vector<ReactionId> MessageReactions::chosen() const { std::vector<ReactionId> MessageReactions::chosen() const {
return _list return _list
| ranges::views::filter(&MessageReaction::my) | ranges::views::filter(&MessageReaction::my)

View file

@ -59,7 +59,8 @@ struct PossibleItemReactions {
}; };
[[nodiscard]] PossibleItemReactionsRef LookupPossibleReactions( [[nodiscard]] PossibleItemReactionsRef LookupPossibleReactions(
not_null<HistoryItem*> item); not_null<HistoryItem*> item,
bool paidInFront = false);
struct MyTagInfo { struct MyTagInfo {
ReactionId id; ReactionId id;
@ -67,6 +68,12 @@ struct MyTagInfo {
int count = 0; int count = 0;
}; };
struct PaidReactionSend {
int count = 0;
bool valid = false;
bool anonymous = false;
};
class Reactions final : private CustomEmojiManager::Listener { class Reactions final : private CustomEmojiManager::Listener {
public: public:
explicit Reactions(not_null<Session*> owner); explicit Reactions(not_null<Session*> owner);
@ -106,6 +113,7 @@ public:
void renameTag(const ReactionId &id, const QString &name); void renameTag(const ReactionId &id, const QString &name);
[[nodiscard]] DocumentData *chooseGenericAnimation( [[nodiscard]] DocumentData *chooseGenericAnimation(
not_null<DocumentData*> custom) const; not_null<DocumentData*> custom) const;
[[nodiscard]] DocumentData *choosePaidReactionAnimation() const;
[[nodiscard]] rpl::producer<> topUpdates() const; [[nodiscard]] rpl::producer<> topUpdates() const;
[[nodiscard]] rpl::producer<> recentUpdates() const; [[nodiscard]] rpl::producer<> recentUpdates() const;
@ -137,10 +145,19 @@ public:
void clearTemporary(); void clearTemporary();
[[nodiscard]] Reaction *lookupTemporary(const ReactionId &id); [[nodiscard]] Reaction *lookupTemporary(const ReactionId &id);
[[nodiscard]] not_null<Reaction*> lookupPaid();
[[nodiscard]] not_null<DocumentData*> paidToastAnimation();
[[nodiscard]] rpl::producer<std::vector<Reaction>> myTagsValue( [[nodiscard]] rpl::producer<std::vector<Reaction>> myTagsValue(
SavedSublist *sublist = nullptr); SavedSublist *sublist = nullptr);
[[nodiscard]] bool isQuitPrevent();
void schedulePaid(not_null<HistoryItem*> item);
void undoScheduledPaid(not_null<HistoryItem*> item);
[[nodiscard]] crl::time sendingScheduledPaidAt(
not_null<HistoryItem*> item) const;
[[nodiscard]] static crl::time ScheduledPaidDelay();
[[nodiscard]] static bool HasUnread(const MTPMessageReactions &data); [[nodiscard]] static bool HasUnread(const MTPMessageReactions &data);
static void CheckUnknownForUnread( static void CheckUnknownForUnread(
not_null<Session*> owner, not_null<Session*> owner,
@ -231,9 +248,27 @@ private:
void resolveEffectImages(); void resolveEffectImages();
void downloadTaskFinished(); void downloadTaskFinished();
void fillPaidReactionAnimations() const;
[[nodiscard]] DocumentData *randomLoadedFrom(
std::vector<not_null<DocumentData*>> list) const;
void repaintCollected(); void repaintCollected();
void pollCollected(); void pollCollected();
void sendPaid();
bool sendPaid(not_null<HistoryItem*> item);
void sendPaidRequest(
not_null<HistoryItem*> item,
PaidReactionSend send);
void sendPaidPrivacyRequest(
not_null<HistoryItem*> item,
PaidReactionSend send);
void sendPaidFinish(
not_null<HistoryItem*> item,
PaidReactionSend send,
bool success);
void checkQuitPreventFinished();
const not_null<Session*> _owner; const not_null<Session*> _owner;
std::vector<Reaction> _active; std::vector<Reaction> _active;
@ -252,6 +287,7 @@ private:
std::vector<ReactionId> _topIds; std::vector<ReactionId> _topIds;
base::flat_set<ReactionId> _unresolvedTop; base::flat_set<ReactionId> _unresolvedTop;
std::vector<not_null<DocumentData*>> _genericAnimations; std::vector<not_null<DocumentData*>> _genericAnimations;
mutable std::vector<not_null<DocumentData*>> _paidReactionAnimations;
std::vector<Reaction> _effects; std::vector<Reaction> _effects;
ReactionId _favoriteId; ReactionId _favoriteId;
ReactionId _unresolvedFavoriteId; ReactionId _unresolvedFavoriteId;
@ -262,6 +298,9 @@ private:
base::flat_map< base::flat_map<
not_null<DocumentData*>, not_null<DocumentData*>,
std::shared_ptr<DocumentMedia>> _genericCache; std::shared_ptr<DocumentMedia>> _genericCache;
mutable base::flat_map<
not_null<DocumentData*>,
std::shared_ptr<DocumentMedia>> _paidReactionCache;
rpl::event_stream<> _topUpdated; rpl::event_stream<> _topUpdated;
rpl::event_stream<> _recentUpdated; rpl::event_stream<> _recentUpdated;
rpl::event_stream<> _defaultUpdated; rpl::event_stream<> _defaultUpdated;
@ -275,6 +314,8 @@ private:
// So we use std::map instead of base::flat_map here. // So we use std::map instead of base::flat_map here.
// Otherwise we could use flat_map<DocumentId, unique_ptr<Reaction>>. // Otherwise we could use flat_map<DocumentId, unique_ptr<Reaction>>.
std::map<DocumentId, Reaction> _temporary; std::map<DocumentId, Reaction> _temporary;
std::optional<Reaction> _paid;
DocumentData *_paidToastAnimation = nullptr;
base::Timer _topRefreshTimer; base::Timer _topRefreshTimer;
mtpRequestId _topRequestId = 0; mtpRequestId _topRequestId = 0;
@ -308,6 +349,10 @@ private:
base::flat_set<not_null<HistoryItem*>> _pollingItems; base::flat_set<not_null<HistoryItem*>> _pollingItems;
mtpRequestId _pollRequestId = 0; mtpRequestId _pollRequestId = 0;
base::flat_map<not_null<HistoryItem*>, crl::time> _sendPaidItems;
base::flat_map<not_null<HistoryItem*>, mtpRequestId> _sendingPaid;
base::Timer _sendPaidTimer;
mtpRequestId _saveFaveRequestId = 0; mtpRequestId _saveFaveRequestId = 0;
rpl::lifetime _lifetime; rpl::lifetime _lifetime;
@ -320,42 +365,77 @@ struct RecentReaction {
bool big = false; bool big = false;
bool my = false; bool my = false;
friend inline auto operator<=>(
const RecentReaction &a,
const RecentReaction &b) = default;
friend inline bool operator==( friend inline bool operator==(
const RecentReaction &a, const RecentReaction &a,
const RecentReaction &b) = default; const RecentReaction &b) = default;
}; };
struct MessageReactionsTopPaid {
PeerData *peer = nullptr;
uint32 count : 30 = 0;
uint32 top : 1 = 0;
uint32 my : 1 = 0;
friend inline bool operator==(
const MessageReactionsTopPaid &a,
const MessageReactionsTopPaid &b) = default;
};
class MessageReactions final { class MessageReactions final {
public: public:
explicit MessageReactions(not_null<HistoryItem*> item); explicit MessageReactions(not_null<HistoryItem*> item);
~MessageReactions();
using TopPaid = MessageReactionsTopPaid;
void add(const ReactionId &id, bool addToRecent); void add(const ReactionId &id, bool addToRecent);
void remove(const ReactionId &id); void remove(const ReactionId &id);
bool change( bool change(
const QVector<MTPReactionCount> &list, const QVector<MTPReactionCount> &list,
const QVector<MTPMessagePeerReaction> &recent, const QVector<MTPMessagePeerReaction> &recent,
bool ignoreChosen); const QVector<MTPMessageReactor> &top,
bool min);
[[nodiscard]] bool checkIfChanged( [[nodiscard]] bool checkIfChanged(
const QVector<MTPReactionCount> &list, const QVector<MTPReactionCount> &list,
const QVector<MTPMessagePeerReaction> &recent, const QVector<MTPMessagePeerReaction> &recent,
bool ignoreChosen) const; bool min) const;
[[nodiscard]] const std::vector<MessageReaction> &list() const; [[nodiscard]] const std::vector<MessageReaction> &list() const;
[[nodiscard]] auto recent() const [[nodiscard]] auto recent() const
-> const base::flat_map<ReactionId, std::vector<RecentReaction>> &; -> const base::flat_map<ReactionId, std::vector<RecentReaction>> &;
[[nodiscard]] const std::vector<TopPaid> &topPaid() const;
[[nodiscard]] std::vector<ReactionId> chosen() const; [[nodiscard]] std::vector<ReactionId> chosen() const;
[[nodiscard]] bool empty() const; [[nodiscard]] bool empty() const;
[[nodiscard]] bool hasUnread() const; [[nodiscard]] bool hasUnread() const;
void markRead(); void markRead();
void scheduleSendPaid(int count, bool anonymous);
[[nodiscard]] int scheduledPaid() const;
void cancelScheduledPaid();
[[nodiscard]] PaidReactionSend startPaidSending();
void finishPaidSending(PaidReactionSend send, bool success);
[[nodiscard]] bool localPaidData() const;
[[nodiscard]] int localPaidCount() const;
[[nodiscard]] bool localPaidAnonymous() const;
bool clearCloudData();
private: private:
struct Paid {
std::vector<TopPaid> top;
uint32 scheduled: 30 = 0;
uint32 scheduledFlag : 1 = 0;
uint32 scheduledAnonymous : 1 = 0;
uint32 sending : 30 = 0;
uint32 sendingFlag : 1 = 0;
uint32 sendingAnonymous : 1 = 0;
};
const not_null<HistoryItem*> _item; const not_null<HistoryItem*> _item;
std::vector<MessageReaction> _list; std::vector<MessageReaction> _list;
base::flat_map<ReactionId, std::vector<RecentReaction>> _recent; base::flat_map<ReactionId, std::vector<RecentReaction>> _recent;
std::unique_ptr<Paid> _paid;
}; };

View file

@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#include "data/data_peer.h" #include "data/data_peer.h"
#include "api/api_sensitive_content.h"
#include "data/data_user.h" #include "data/data_user.h"
#include "data/data_chat.h" #include "data/data_chat.h"
#include "data/data_chat_participant_status.h" #include "data/data_chat_participant_status.h"
@ -58,6 +59,11 @@ constexpr auto kUserpicSize = 160;
using UpdateFlag = Data::PeerUpdate::Flag; using UpdateFlag = Data::PeerUpdate::Flag;
[[nodiscard]] const std::vector<QString> &IgnoredReasons(
not_null<Main::Session*> session) {
return session->appConfig().ignoredRestrictionReasons();
}
} // namespace } // namespace
namespace Data { namespace Data {
@ -74,6 +80,62 @@ PeerId FakePeerIdForJustName(const QString &name) {
return peerFromUser(kShift + std::abs(base)); return peerFromUser(kShift + std::abs(base));
} }
bool UnavailableReason::sensitive() const {
return reason == u"sensitive"_q;
}
UnavailableReason UnavailableReason::Sensitive() {
return { u"sensitive"_q };
}
QString UnavailableReason::Compute(
not_null<Main::Session*> session,
const std::vector<UnavailableReason> &list) {
const auto &skip = IgnoredReasons(session);
auto &&filtered = ranges::views::all(
list
) | ranges::views::filter([&](const Data::UnavailableReason &reason) {
return !reason.sensitive()
&& !ranges::contains(skip, reason.reason);
});
const auto first = filtered.begin();
return (first != filtered.end()) ? first->text : QString();
}
bool UnavailableReason::IgnoreSensitiveMark(
not_null<Main::Session*> session) {
return ranges::contains(
IgnoredReasons(session),
UnavailableReason::Sensitive().reason);
}
// We should get a full restriction in "{full}: {reason}" format and we
// need to find an "-all" tag in {full}, otherwise ignore this restriction.
std::vector<UnavailableReason> UnavailableReason::Extract(
const MTPvector<MTPRestrictionReason> *list) {
if (!list) {
return {};
}
return ranges::views::all(
list->v
) | ranges::views::filter([](const MTPRestrictionReason &restriction) {
return restriction.match([&](const MTPDrestrictionReason &data) {
const auto platform = data.vplatform().v;
return false
#ifdef OS_MAC_STORE
|| (platform == "ios"_q)
#elif defined OS_WIN_STORE // OS_MAC_STORE
|| (platform == "ms"_q)
#endif // OS_MAC_STORE || OS_WIN_STORE
|| (platform == "all"_q);
});
}) | ranges::views::transform([](const MTPRestrictionReason &restriction) {
return restriction.match([&](const MTPDrestrictionReason &data) {
return UnavailableReason{ qs(data.vreason()), qs(data.vtext()) };
});
}) | ranges::to_vector;
}
bool ApplyBotMenuButton( bool ApplyBotMenuButton(
not_null<BotInfo*> info, not_null<BotInfo*> info,
const MTPBotMenuButton *button) { const MTPBotMenuButton *button) {
@ -95,28 +157,22 @@ bool ApplyBotMenuButton(
return changed; return changed;
} }
bool operator<( AllowedReactions Parse(
const AllowedReactions &a, const MTPChatReactions &value,
const AllowedReactions &b) { int maxCount,
return (a.type < b.type) || ((a.type == b.type) && (a.some < b.some)); bool paidEnabled) {
}
bool operator==(
const AllowedReactions &a,
const AllowedReactions &b) {
return (a.type == b.type)
&& (a.some == b.some)
&& (a.maxCount == b.maxCount);
}
AllowedReactions Parse(const MTPChatReactions &value) {
return value.match([&](const MTPDchatReactionsNone &) { return value.match([&](const MTPDchatReactionsNone &) {
return AllowedReactions(); return AllowedReactions{
.maxCount = maxCount,
.paidEnabled = paidEnabled,
};
}, [&](const MTPDchatReactionsAll &data) { }, [&](const MTPDchatReactionsAll &data) {
return AllowedReactions{ return AllowedReactions{
.maxCount = maxCount,
.type = (data.is_allow_custom() .type = (data.is_allow_custom()
? AllowedReactionsType::All ? AllowedReactionsType::All
: AllowedReactionsType::Default), : AllowedReactionsType::Default),
.paidEnabled = paidEnabled,
}; };
}, [&](const MTPDchatReactionsSome &data) { }, [&](const MTPDchatReactionsSome &data) {
return AllowedReactions{ return AllowedReactions{
@ -125,7 +181,9 @@ AllowedReactions Parse(const MTPChatReactions &value) {
) | ranges::views::transform( ) | ranges::views::transform(
ReactionFromMTP ReactionFromMTP
) | ranges::to_vector, ) | ranges::to_vector,
.maxCount = maxCount,
.type = AllowedReactionsType::Some, .type = AllowedReactionsType::Some,
.paidEnabled = paidEnabled,
}; };
}); });
} }
@ -501,18 +559,50 @@ auto PeerData::unavailableReasons() const
} }
QString PeerData::computeUnavailableReason() const { QString PeerData::computeUnavailableReason() const {
const auto &list = unavailableReasons(); return Data::UnavailableReason::Compute(
const auto &config = session().appConfig(); &session(),
const auto skip = config.get<std::vector<QString>>( unavailableReasons());
"ignore_restriction_reasons", }
std::vector<QString>());
auto &&filtered = ranges::views::all( bool PeerData::hasSensitiveContent() const {
list return _sensitiveContent == 1;
) | ranges::views::filter([&](const Data::UnavailableReason &reason) { }
return !ranges::contains(skip, reason.reason);
}); void PeerData::setUnavailableReasonsList(
const auto first = filtered.begin(); std::vector<Data::UnavailableReason> &&reasons) {
return (first != filtered.end()) ? first->text : QString(); Unexpected("PeerData::setUnavailableReasonsList.");
}
void PeerData::setUnavailableReasons(
std::vector<Data::UnavailableReason> &&reasons) {
const auto i = ranges::find(
reasons,
true,
&Data::UnavailableReason::sensitive);
const auto sensitive = (i != end(reasons));
if (sensitive) {
reasons.erase(i);
}
auto changed = (sensitive != hasSensitiveContent());
if (changed) {
setHasSensitiveContent(sensitive);
}
if (reasons != unavailableReasons()) {
setUnavailableReasonsList(std::move(reasons));
changed = true;
}
if (changed) {
session().changes().peerUpdated(
this,
UpdateFlag::UnavailableReason);
}
}
void PeerData::setHasSensitiveContent(bool has) {
_sensitiveContent = has ? 1 : 0;
if (has) {
session().api().sensitiveContent().preload();
}
} }
// This is duplicated in CanPinMessagesValue(). // This is duplicated in CanPinMessagesValue().
@ -1222,9 +1312,12 @@ Data::RestrictionCheckResult PeerData::amRestricted(
} }
bool PeerData::amAnonymous() const { bool PeerData::amAnonymous() const {
return isBroadcast() if (const auto channel = asChannel()) {
|| (isChannel() return channel->isBroadcast()
&& (asChannel()->adminRights() & ChatAdminRight::Anonymous)); ? !channel->signatureProfiles()
: (channel->adminRights() & ChatAdminRight::Anonymous);
}
return false;
} }
bool PeerData::canRevokeFullHistory() const { bool PeerData::canRevokeFullHistory() const {

View file

@ -89,19 +89,28 @@ struct UnavailableReason {
QString reason; QString reason;
QString text; QString text;
bool operator==(const UnavailableReason &other) const { friend inline bool operator==(
return (reason == other.reason) && (text == other.text); const UnavailableReason &,
} const UnavailableReason &) = default;
bool operator!=(const UnavailableReason &other) const {
return !(*this == other); [[nodiscard]] bool sensitive() const;
} [[nodiscard]] static UnavailableReason Sensitive();
[[nodiscard]] static QString Compute(
not_null<Main::Session*> session,
const std::vector<UnavailableReason> &list);
[[nodiscard]] static bool IgnoreSensitiveMark(
not_null<Main::Session*> session);
[[nodiscard]] static std::vector<UnavailableReason> Extract(
const MTPvector<MTPRestrictionReason> *list);
}; };
bool ApplyBotMenuButton( bool ApplyBotMenuButton(
not_null<BotInfo*> info, not_null<BotInfo*> info,
const MTPBotMenuButton *button); const MTPBotMenuButton *button);
enum class AllowedReactionsType { enum class AllowedReactionsType : uchar {
All, All,
Default, Default,
Some, Some,
@ -109,14 +118,19 @@ enum class AllowedReactionsType {
struct AllowedReactions { struct AllowedReactions {
std::vector<ReactionId> some; std::vector<ReactionId> some;
AllowedReactionsType type = AllowedReactionsType::Some;
int maxCount = 0; int maxCount = 0;
AllowedReactionsType type = AllowedReactionsType::Some;
bool paidEnabled = false;
friend inline bool operator==(
const AllowedReactions &,
const AllowedReactions &) = default;
}; };
bool operator<(const AllowedReactions &a, const AllowedReactions &b); [[nodiscard]] AllowedReactions Parse(
bool operator==(const AllowedReactions &a, const AllowedReactions &b); const MTPChatReactions &value,
int maxCount,
[[nodiscard]] AllowedReactions Parse(const MTPChatReactions &value); bool paidEnabled);
[[nodiscard]] PeerData *PeerFromInputMTP( [[nodiscard]] PeerData *PeerFromInputMTP(
not_null<Session*> owner, not_null<Session*> owner,
const MTPInputPeer &input); const MTPInputPeer &input);
@ -335,6 +349,9 @@ public:
// If this string is not empty we must not allow to open the // If this string is not empty we must not allow to open the
// conversation and we must show this string instead. // conversation and we must show this string instead.
[[nodiscard]] QString computeUnavailableReason() const; [[nodiscard]] QString computeUnavailableReason() const;
[[nodiscard]] bool hasSensitiveContent() const;
void setUnavailableReasons(
std::vector<Data::UnavailableReason> &&reason);
[[nodiscard]] ClickHandlerPtr createOpenLink(); [[nodiscard]] ClickHandlerPtr createOpenLink();
[[nodiscard]] const ClickHandlerPtr &openLink() { [[nodiscard]] const ClickHandlerPtr &openLink() {
@ -476,6 +493,10 @@ private:
const ImageLocation &location, const ImageLocation &location,
bool hasVideo); bool hasVideo);
virtual void setUnavailableReasonsList(
std::vector<Data::UnavailableReason> &&reasons);
void setHasSensitiveContent(bool has);
const not_null<Data::Session*> _owner; const not_null<Data::Session*> _owner;
mutable Data::CloudImage _userpic; mutable Data::CloudImage _userpic;
@ -494,7 +515,8 @@ private:
crl::time _lastFullUpdate = 0; crl::time _lastFullUpdate = 0;
QString _name; QString _name;
uint32 _nameVersion : 31 = 1; uint32 _nameVersion : 30 = 1;
uint32 _sensitiveContent : 1 = 0;
uint32 _wallPaperOverriden : 1 = 0; uint32 _wallPaperOverriden : 1 = 0;
TimeId _ttlPeriod = 0; TimeId _ttlPeriod = 0;

View file

@ -13,13 +13,9 @@ struct BotCommand final {
QString command; QString command;
QString description; QString description;
inline bool operator==(const BotCommand &other) const { friend inline bool operator==(
return (command == other.command) const BotCommand &,
&& (description == other.description); const BotCommand &) = default;
}
inline bool operator!=(const BotCommand &other) const {
return !(*this == other);
}
}; };
[[nodiscard]] BotCommand BotCommandFromTL(const MTPBotCommand &result); [[nodiscard]] BotCommand BotCommandFromTL(const MTPBotCommand &result);

View file

@ -517,6 +517,10 @@ bool ChannelHasActiveCall(not_null<ChannelData*> channel) {
return (channel->flags() & ChannelDataFlag::CallNotEmpty); return (channel->flags() & ChannelDataFlag::CallNotEmpty);
} }
bool ChannelHasSubscriptionUntilDate(ChannelData *channel) {
return channel && channel->subscriptionUntilDate() > 0;
}
rpl::producer<QImage> PeerUserpicImageValue( rpl::producer<QImage> PeerUserpicImageValue(
not_null<PeerData*> peer, not_null<PeerData*> peer,
int size, int size,

View file

@ -164,6 +164,7 @@ inline auto PeerFullFlagValue(
[[nodiscard]] bool OnlineTextActive(not_null<UserData*> user, TimeId now); [[nodiscard]] bool OnlineTextActive(not_null<UserData*> user, TimeId now);
[[nodiscard]] bool IsUserOnline(not_null<UserData*> user, TimeId now = 0); [[nodiscard]] bool IsUserOnline(not_null<UserData*> user, TimeId now = 0);
[[nodiscard]] bool ChannelHasActiveCall(not_null<ChannelData*> channel); [[nodiscard]] bool ChannelHasActiveCall(not_null<ChannelData*> channel);
[[nodiscard]] bool ChannelHasSubscriptionUntilDate(ChannelData *channel);
[[nodiscard]] rpl::producer<QImage> PeerUserpicImageValue( [[nodiscard]] rpl::producer<QImage> PeerUserpicImageValue(
not_null<PeerData*> peer, not_null<PeerData*> peer,

View file

@ -252,7 +252,7 @@ Image *PhotoData::getReplyPreview(
Image *PhotoData::getReplyPreview(not_null<HistoryItem*> item) { Image *PhotoData::getReplyPreview(not_null<HistoryItem*> item) {
const auto media = item->media(); const auto media = item->media();
const auto spoiler = media && media->hasSpoiler(); const auto spoiler = (media && media->hasSpoiler());
return getReplyPreview(item->fullId(), item->history()->peer, spoiler); return getReplyPreview(item->fullId(), item->history()->peer, spoiler);
} }

View file

@ -9,7 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Data { namespace Data {
struct SubscriptionOption { struct PremiumSubscriptionOption {
QString duration; QString duration;
QString discount; QString discount;
QString costPerMonth; QString costPerMonth;
@ -17,6 +17,6 @@ struct SubscriptionOption {
QString total; QString total;
QString botUrl; QString botUrl;
}; };
using SubscriptionOptions = std::vector<SubscriptionOption>; using PremiumSubscriptionOptions = std::vector<PremiumSubscriptionOption>;
} // namespace Data } // namespace Data

View file

@ -132,30 +132,6 @@ void CheckForSwitchInlineButton(not_null<HistoryItem*> item) {
} }
} }
// We should get a full restriction in "{full}: {reason}" format and we
// need to find an "-all" tag in {full}, otherwise ignore this restriction.
std::vector<UnavailableReason> ExtractUnavailableReasons(
const QVector<MTPRestrictionReason> &restrictions) {
return ranges::views::all(
restrictions
) | ranges::views::filter([](const MTPRestrictionReason &restriction) {
return restriction.match([&](const MTPDrestrictionReason &data) {
const auto platform = qs(data.vplatform());
return false
#ifdef OS_MAC_STORE
|| (platform == u"ios"_q)
#elif defined OS_WIN_STORE // OS_MAC_STORE
|| (platform == u"ms"_q)
#endif // OS_MAC_STORE || OS_WIN_STORE
|| (platform == u"all"_q);
});
}) | ranges::views::transform([](const MTPRestrictionReason &restriction) {
return restriction.match([&](const MTPDrestrictionReason &data) {
return UnavailableReason{ qs(data.vreason()), qs(data.vtext()) };
});
}) | ranges::to_vector;
}
[[nodiscard]] InlineImageLocation FindInlineThumbnail( [[nodiscard]] InlineImageLocation FindInlineThumbnail(
const QVector<MTPPhotoSize> &sizes) { const QVector<MTPPhotoSize> &sizes) {
const auto i = ranges::find( const auto i = ranges::find(
@ -345,6 +321,22 @@ Session::Session(not_null<Main::Session*> session)
_stories->loadMore(Data::StorySourcesList::NotHidden); _stories->loadMore(Data::StorySourcesList::NotHidden);
} }
}); });
session->appConfig().ignoredRestrictionReasonsChanges(
) | rpl::start_with_next([=](std::vector<QString> &&changed) {
auto refresh = std::vector<not_null<const HistoryItem*>>();
for (const auto &[item, reasons] : _possiblyRestricted) {
for (const auto &reason : changed) {
if (reasons.contains(reason)) {
refresh.push_back(item);
break;
}
}
}
for (const auto &item : refresh) {
requestItemViewRefresh(item);
}
}, _lifetime);
} }
void Session::subscribeForTopicRepliesLists() { void Session::subscribeForTopicRepliesLists() {
@ -616,12 +608,8 @@ not_null<UserData*> Session::processUser(const MTPUser &data) {
result->input = MTP_inputPeerUser(data.vid(), MTP_long(result->accessHash())); result->input = MTP_inputPeerUser(data.vid(), MTP_long(result->accessHash()));
result->inputUser = MTP_inputUser(data.vid(), MTP_long(result->accessHash())); result->inputUser = MTP_inputUser(data.vid(), MTP_long(result->accessHash()));
} }
if (const auto restriction = data.vrestriction_reason()) { result->setUnavailableReasons(Data::UnavailableReason::Extract(
result->setUnavailableReasons( data.vrestriction_reason()));
ExtractUnavailableReasons(restriction->v));
} else {
result->setUnavailableReasons({});
}
} }
if (data.is_deleted()) { if (data.is_deleted()) {
if (!result->phone().isEmpty()) { if (!result->phone().isEmpty()) {
@ -890,6 +878,8 @@ not_null<PeerData*> Session::processChat(const MTPChat &data) {
const auto wasCallNotEmpty = Data::ChannelHasActiveCall(channel); const auto wasCallNotEmpty = Data::ChannelHasActiveCall(channel);
channel->updateLevelHint(data.vlevel().value_or_empty()); channel->updateLevelHint(data.vlevel().value_or_empty());
channel->updateSubscriptionUntilDate(
data.vsubscription_until_date().value_or_empty());
if (const auto count = data.vparticipants_count()) { if (const auto count = data.vparticipants_count()) {
channel->setMembersCount(count->v); channel->setMembersCount(count->v);
} }
@ -923,12 +913,8 @@ not_null<PeerData*> Session::processChat(const MTPChat &data) {
channel->setAccessHash( channel->setAccessHash(
data.vaccess_hash().value_or(channel->access)); data.vaccess_hash().value_or(channel->access));
channel->date = data.vdate().v; channel->date = data.vdate().v;
if (const auto restriction = data.vrestriction_reason()) { channel->setUnavailableReasons(Data::UnavailableReason::Extract(
channel->setUnavailableReasons( data.vrestriction_reason()));
ExtractUnavailableReasons(restriction->v));
} else {
channel->setUnavailableReasons({});
}
} }
{ {
@ -954,6 +940,7 @@ not_null<PeerData*> Session::processChat(const MTPChat &data) {
| Flag::Gigagroup | Flag::Gigagroup
| Flag::Username | Flag::Username
| Flag::Signatures | Flag::Signatures
| Flag::SignatureProfiles
| Flag::HasLink | Flag::HasLink
| Flag::SlowmodeEnabled | Flag::SlowmodeEnabled
| Flag::CallActive | Flag::CallActive
@ -982,6 +969,7 @@ not_null<PeerData*> Session::processChat(const MTPChat &data) {
| (data.is_gigagroup() ? Flag::Gigagroup : Flag()) | (data.is_gigagroup() ? Flag::Gigagroup : Flag())
| (hasUsername ? Flag::Username : Flag()) | (hasUsername ? Flag::Username : Flag())
| (data.is_signatures() ? Flag::Signatures : Flag()) | (data.is_signatures() ? Flag::Signatures : Flag())
| (data.is_signature_profiles() ? Flag::SignatureProfiles : Flag())
| (data.is_has_link() ? Flag::HasLink : Flag()) | (data.is_has_link() ? Flag::HasLink : Flag())
| (data.is_slowmode_enabled() ? Flag::SlowmodeEnabled : Flag()) | (data.is_slowmode_enabled() ? Flag::SlowmodeEnabled : Flag())
| (data.is_call_active() ? Flag::CallActive : Flag()) | (data.is_call_active() ? Flag::CallActive : Flag())
@ -1293,24 +1281,31 @@ History *Session::historyLoaded(const PeerData *peer) {
} }
void Session::deleteConversationLocally(not_null<PeerData*> peer) { void Session::deleteConversationLocally(not_null<PeerData*> peer) {
const auto history = historyLoaded(peer); const auto markLeft = [&] {
if (history) { if (const auto channel = peer->asMegagroup()) {
channel->addFlags(ChannelDataFlag::Left);
if (const auto from = channel->getMigrateFromChat()) {
if (const auto migrated = historyLoaded(from)) {
migrated->updateChatListExistence();
}
}
}
};
if (const auto history = historyLoaded(peer)) {
if (history->folderKnown()) { if (history->folderKnown()) {
setChatPinned(history, FilterId(), false); setChatPinned(history, FilterId(), false);
} }
removeChatListEntry(history); removeChatListEntry(history);
history->clearFolder(); history->clearFolder();
// We want to mark the channel as left before unloading the history,
// otherwise some parts of updating may return us to the chats list.
markLeft();
history->clear(peer->isChannel() history->clear(peer->isChannel()
? History::ClearType::Unload ? History::ClearType::Unload
: History::ClearType::DeleteChat); : History::ClearType::DeleteChat);
} } else {
if (const auto channel = peer->asMegagroup()) { markLeft();
channel->addFlags(ChannelDataFlag::Left);
if (const auto from = channel->getMigrateFromChat()) {
if (const auto migrated = historyLoaded(from)) {
migrated->updateChatListExistence();
}
}
} }
} }
@ -1811,7 +1806,7 @@ rpl::producer<not_null<ViewElement*>> Session::viewResizeRequest() const {
return _viewResizeRequest.events(); return _viewResizeRequest.events();
} }
void Session::requestItemViewRefresh(not_null<HistoryItem*> item) { void Session::requestItemViewRefresh(not_null<const HistoryItem*> item) {
if (const auto view = item->mainView()) { if (const auto view = item->mainView()) {
notifyHistoryChangeDelayed(item->history()); notifyHistoryChangeDelayed(item->history());
view->refreshInBlock(); view->refreshInBlock();
@ -1819,7 +1814,7 @@ void Session::requestItemViewRefresh(not_null<HistoryItem*> item) {
_itemViewRefreshRequest.fire_copy(item); _itemViewRefreshRequest.fire_copy(item);
} }
rpl::producer<not_null<HistoryItem*>> Session::itemViewRefreshRequest() const { rpl::producer<not_null<const HistoryItem*>> Session::itemViewRefreshRequest() const {
return _itemViewRefreshRequest.events(); return _itemViewRefreshRequest.events();
} }
@ -1845,6 +1840,31 @@ void Session::requestItemTextRefresh(not_null<HistoryItem*> item) {
} }
} }
void Session::registerRestricted(
not_null<const HistoryItem*> item,
const QString &reason) {
Expects(item->hasPossibleRestrictions());
_possiblyRestricted[item].emplace(reason);
}
void Session::registerRestricted(
not_null<const HistoryItem*> item,
const std::vector<UnavailableReason> &reasons) {
Expects(item->hasPossibleRestrictions());
auto &list = _possiblyRestricted[item];
if (list.empty()) {
auto &&simple = reasons
| ranges::views::transform(&UnavailableReason::reason);
list = { begin(simple), end(simple) };
} else {
for (const auto &reason : reasons) {
list.emplace(reason.reason);
}
}
}
void Session::registerHighlightProcess( void Session::registerHighlightProcess(
uint64 processId, uint64 processId,
not_null<HistoryItem*> item) { not_null<HistoryItem*> item) {
@ -1892,6 +1912,14 @@ rpl::producer<not_null<const ViewElement*>> Session::viewRemoved() const {
return _viewRemoved.events(); return _viewRemoved.events();
} }
void Session::notifyViewPaidReactionSent(not_null<const ViewElement*> view) {
_viewPaidReactionSent.fire_copy(view);
}
rpl::producer<not_null<const ViewElement*>> Session::viewPaidReactionSent() const {
return _viewPaidReactionSent.events();
}
void Session::notifyHistoryUnloaded(not_null<const History*> history) { void Session::notifyHistoryUnloaded(not_null<const History*> history) {
_historyUnloaded.fire_copy(history); _historyUnloaded.fire_copy(history);
} }
@ -2592,6 +2620,9 @@ void Session::unregisterMessage(not_null<HistoryItem*> item) {
const auto peerId = item->history()->peer->id; const auto peerId = item->history()->peer->id;
const auto itemId = item->id; const auto itemId = item->id;
_itemRemoved.fire_copy(item); _itemRemoved.fire_copy(item);
if (item->hasPossibleRestrictions()) {
_possiblyRestricted.remove(item);
}
session().changes().messageUpdated( session().changes().messageUpdated(
item, item,
Data::MessageUpdate::Flag::Destroyed); Data::MessageUpdate::Flag::Destroyed);

View file

@ -69,6 +69,7 @@ class SavedMessages;
class Chatbots; class Chatbots;
class BusinessInfo; class BusinessInfo;
struct ReactionId; struct ReactionId;
struct UnavailableReason;
struct RepliesReadTillUpdate { struct RepliesReadTillUpdate {
FullMsgId id; FullMsgId id;
@ -288,8 +289,8 @@ public:
[[nodiscard]] rpl::producer<not_null<const HistoryItem*>> itemResizeRequest() const; [[nodiscard]] rpl::producer<not_null<const HistoryItem*>> itemResizeRequest() const;
void requestViewResize(not_null<ViewElement*> view); void requestViewResize(not_null<ViewElement*> view);
[[nodiscard]] rpl::producer<not_null<ViewElement*>> viewResizeRequest() const; [[nodiscard]] rpl::producer<not_null<ViewElement*>> viewResizeRequest() const;
void requestItemViewRefresh(not_null<HistoryItem*> item); void requestItemViewRefresh(not_null<const HistoryItem*> item);
[[nodiscard]] rpl::producer<not_null<HistoryItem*>> itemViewRefreshRequest() const; [[nodiscard]] rpl::producer<not_null<const HistoryItem*>> itemViewRefreshRequest() const;
void requestItemTextRefresh(not_null<HistoryItem*> item); void requestItemTextRefresh(not_null<HistoryItem*> item);
void requestUnreadReactionsAnimation(not_null<HistoryItem*> item); void requestUnreadReactionsAnimation(not_null<HistoryItem*> item);
void notifyHistoryUnloaded(not_null<const History*> history); void notifyHistoryUnloaded(not_null<const History*> history);
@ -306,11 +307,20 @@ public:
[[nodiscard]] rpl::producer<not_null<const History*>> historyCleared() const; [[nodiscard]] rpl::producer<not_null<const History*>> historyCleared() const;
void notifyHistoryChangeDelayed(not_null<History*> history); void notifyHistoryChangeDelayed(not_null<History*> history);
[[nodiscard]] rpl::producer<not_null<History*>> historyChanged() const; [[nodiscard]] rpl::producer<not_null<History*>> historyChanged() const;
void notifyViewPaidReactionSent(not_null<const ViewElement*> view);
[[nodiscard]] rpl::producer<not_null<const ViewElement*>> viewPaidReactionSent() const;
void sendHistoryChangeNotifications(); void sendHistoryChangeNotifications();
void notifyPinnedDialogsOrderUpdated(); void notifyPinnedDialogsOrderUpdated();
[[nodiscard]] rpl::producer<> pinnedDialogsOrderUpdated() const; [[nodiscard]] rpl::producer<> pinnedDialogsOrderUpdated() const;
void registerRestricted(
not_null<const HistoryItem*> item,
const QString &reason);
void registerRestricted(
not_null<const HistoryItem*> item,
const std::vector<UnavailableReason> &reasons);
void registerHighlightProcess( void registerHighlightProcess(
uint64 processId, uint64 processId,
not_null<HistoryItem*> item); not_null<HistoryItem*> item);
@ -918,11 +928,12 @@ private:
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;
rpl::event_stream<not_null<ViewElement*>> _viewResizeRequest; rpl::event_stream<not_null<ViewElement*>> _viewResizeRequest;
rpl::event_stream<not_null<HistoryItem*>> _itemViewRefreshRequest; rpl::event_stream<not_null<const HistoryItem*>> _itemViewRefreshRequest;
rpl::event_stream<not_null<HistoryItem*>> _itemTextRefreshRequest; rpl::event_stream<not_null<HistoryItem*>> _itemTextRefreshRequest;
rpl::event_stream<not_null<HistoryItem*>> _itemDataChanges; rpl::event_stream<not_null<HistoryItem*>> _itemDataChanges;
rpl::event_stream<not_null<const HistoryItem*>> _itemRemoved; rpl::event_stream<not_null<const HistoryItem*>> _itemRemoved;
rpl::event_stream<not_null<const ViewElement*>> _viewRemoved; rpl::event_stream<not_null<const ViewElement*>> _viewRemoved;
rpl::event_stream<not_null<const ViewElement*>> _viewPaidReactionSent;
rpl::event_stream<not_null<const History*>> _historyUnloaded; rpl::event_stream<not_null<const History*>> _historyUnloaded;
rpl::event_stream<not_null<const History*>> _historyCleared; rpl::event_stream<not_null<const History*>> _historyCleared;
base::flat_set<not_null<History*>> _historiesChanged; base::flat_set<not_null<History*>> _historiesChanged;
@ -1018,6 +1029,10 @@ private:
base::flat_multi_map<TimeId, not_null<PollData*>> _pollsClosings; base::flat_multi_map<TimeId, not_null<PollData*>> _pollsClosings;
base::Timer _pollsClosingTimer; base::Timer _pollsClosingTimer;
base::flat_map<
not_null<const HistoryItem*>,
base::flat_set<QString>> _possiblyRestricted;
base::flat_map<FolderId, std::unique_ptr<Folder>> _folders; base::flat_map<FolderId, std::unique_ptr<Folder>> _folders;
std::unordered_map< std::unordered_map<

View file

@ -0,0 +1,36 @@
/*
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 PeerSubscription final {
uint64 credits = 0;
int period = 0;
explicit operator bool() const {
return credits > 0 && period > 0;
}
};
struct SubscriptionEntry final {
explicit operator bool() const {
return !id.isEmpty();
}
QString id;
QString inviteHash;
QDateTime until;
PeerSubscription subscription;
uint64 barePeerId = 0;
bool cancelled = false;
bool expired = false;
bool canRefulfill = false;
};
} // namespace Data

View file

@ -313,16 +313,20 @@ enum class MessageFlag : uint64 {
// If not set then we need to refresh _displayFrom value. // If not set then we need to refresh _displayFrom value.
DisplayFromChecked = (1ULL << 40), DisplayFromChecked = (1ULL << 40),
DisplayFromProfiles = (1ULL << 41),
ShowSimilarChannels = (1ULL << 41), ShowSimilarChannels = (1ULL << 42),
Sponsored = (1ULL << 42), Sponsored = (1ULL << 43),
ReactionsAreTags = (1ULL << 43), ReactionsAreTags = (1ULL << 44),
ShortcutMessage = (1ULL << 44), ShortcutMessage = (1ULL << 45),
EffectWatched = (1ULL << 45), EffectWatched = (1ULL << 46),
SensitiveContent = (1ULL << 47),
HasRestrictions = (1ULL << 48),
}; };
inline constexpr bool is_flag_type(MessageFlag) { return true; } inline constexpr bool is_flag_type(MessageFlag) { return true; }
using MessageFlags = base::flags<MessageFlag>; using MessageFlags = base::flags<MessageFlag>;

View file

@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#include "data/data_user.h" #include "data/data_user.h"
#include "api/api_sensitive_content.h"
#include "storage/localstorage.h" #include "storage/localstorage.h"
#include "storage/storage_user_photos.h" #include "storage/storage_user_photos.h"
#include "main/main_session.h" #include "main/main_session.h"
@ -122,14 +123,9 @@ auto UserData::unavailableReasons() const
return _unavailableReasons; return _unavailableReasons;
} }
void UserData::setUnavailableReasons( void UserData::setUnavailableReasonsList(
std::vector<Data::UnavailableReason> &&reasons) { std::vector<Data::UnavailableReason> &&reasons) {
if (_unavailableReasons != reasons) { _unavailableReasons = std::move(reasons);
_unavailableReasons = std::move(reasons);
session().changes().peerUpdated(
this,
UpdateFlag::UnavailableReason);
}
} }
void UserData::setCommonChatsCount(int count) { void UserData::setCommonChatsCount(int count) {
@ -336,7 +332,11 @@ void UserData::setBotInfo(const MTPBotInfo &info) {
d.vmenu_button()); d.vmenu_button());
botInfo->inited = true; botInfo->inited = true;
if (changedCommands || changedButton) { const auto privacy = qs(d.vprivacy_policy_url().value_or_empty());
const auto privacyChanged = (botInfo->privacyPolicyUrl != privacy);
botInfo->privacyPolicyUrl = privacy;
if (changedCommands || changedButton || privacyChanged) {
owner().botCommandsChanged(this); owner().botCommandsChanged(this);
} }
} break; } break;
@ -526,6 +526,10 @@ void UserData::setBirthday(Data::Birthday value) {
if (_birthday != value) { if (_birthday != value) {
_birthday = value; _birthday = value;
session().changes().peerUpdated(this, UpdateFlag::Birthday); session().changes().peerUpdated(this, UpdateFlag::Birthday);
if (isSelf()) {
session().api().sensitiveContent().reload(true);
}
} }
} }

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