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_shortcut_messages.cpp
data/business/data_shortcut_messages.h
data/components/credits.cpp
data/components/credits.h
data/components/factchecks.cpp
data/components/factchecks.h
data/components/location_pickers.cpp
@ -898,6 +900,8 @@ PRIVATE
history/view/history_view_message.cpp
history/view/history_view_message.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.h
history/view/history_view_pinned_section.cpp
@ -1310,6 +1314,8 @@ PRIVATE
payments/payments_form.h
payments/payments_non_panel_process.cpp
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.h
platform/linux/launcher_linux.cpp
@ -1542,6 +1548,8 @@ PRIVATE
support/support_preload.h
support/support_templates.cpp
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.h
ui/chat/attach/attach_item_single_media_preview.cpp
@ -1896,7 +1904,7 @@ endif()
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
PRIVATE
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
)
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
PRIVATE
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_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_az" = "Azerbaijani";
"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_help" = "Bot Help";
"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#other" = "{count} groups in common";
"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_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_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**.";
@ -1943,6 +1948,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"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_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_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.";
@ -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_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_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_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_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_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_photos#one" = "{count} photo";
"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_text#one" = "**{count} Star** 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_about#one" = "**{count}** Star 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_about" = "You can dispute this transaction {link}.";
"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#other" = "{count} Stars Needed";
"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_enough" = "You have enough stars at the moment. {link}";
"lng_credits_enough_link" = "Buy anyway";
"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_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_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_status_users#one" = "{count} user";
"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_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_bar_to" = "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_bot_title" = "Edit bot";
"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_channel_color" = "Change name color";
"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_signatures_enabled" = "{from} enabled 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_disabled" = "{from} restricted content copying";
"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_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_join" = "Followers";

View file

@ -11,20 +11,41 @@
<file alias="ttl.tgs">../../animations/ttl.tgs</file>
<file alias="discussion.tgs">../../animations/discussion.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_start.tgs">../../animations/voice_ttl_start.tgs</file>
<file alias="palette.tgs">../../animations/palette.tgs</file>
<file alias="sleep.tgs">../../animations/sleep.tgs</file>
<file alias="greeting.tgs">../../animations/greeting.tgs</file>
<file alias="location.tgs">../../animations/location.tgs</file>
<file alias="robot.tgs">../../animations/robot.tgs</file>
<file alias="writing.tgs">../../animations/writing.tgs</file>
<file alias="hours.tgs">../../animations/hours.tgs</file>
<file alias="phone.tgs">../../animations/phone.tgs</file>
<file alias="greeting.tgs">../../animations/greeting.tgs</file>
<file alias="location.tgs">../../animations/location.tgs</file>
<file alias="robot.tgs">../../animations/robot.tgs</file>
<file alias="writing.tgs">../../animations/writing.tgs</file>
<file alias="hours.tgs">../../animations/hours.tgs</file>
<file alias="phone.tgs">../../animations/phone.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_phone.tgs">../../animations/collectible_phone.tgs</file>
<file alias="search.tgs">../../animations/search.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>
</RCC>

View file

@ -7,16 +7,6 @@
<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/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="night.tdesktop-theme">../../night.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/gray.svg">../../art/topic_icons/gray.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 prefix="/icons">
<file alias="calls/hands.lottie">../../icons/calls/hands.lottie</file>

View file

@ -10,7 +10,7 @@
<Identity Name="TelegramMessengerLLP.TelegramDesktop"
ProcessorArchitecture="ARCHITECTURE"
Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
Version="5.3.2.0" />
Version="5.4.1.0" />
<Properties>
<DisplayName>Telegram Desktop</DisplayName>
<PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName>
@ -37,8 +37,8 @@
<Extensions>
<uap3:Extension Category="windows.protocol">
<uap3:Protocol Name="tg" Parameters="-- &quot;%1&quot;" />
<uap3:Extension Category="windows.protocol">
</uap3:Extension>
<uap3:Extension Category="windows.protocol">
<uap3:Protocol Name="tonsite" Parameters="-- &quot;%1&quot;" />
</uap3:Extension>
<desktop:Extension

View file

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

View file

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

View file

@ -8,26 +8,39 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_chat_invite.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 "lang/lang_keys.h"
#include "main/main_session.h"
#include "ui/empty_userpic.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 "settings/settings_credits_graphics.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 "boxes/premium_limits_box.h"
#include "ui/vertical_list.h"
#include "window/window_session_controller.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_layers.h"
#include "styles/style_premium.h"
namespace Api {
@ -101,12 +114,237 @@ void SubmitChatInvite(
}).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
void CheckChatInvite(
not_null<Window::SessionController*> controller,
const QString &hash,
ChannelData *invitePeekChannel) {
ChannelData *invitePeekChannel,
Fn<void()> loaded) {
const auto session = &controller->session();
const auto weak = base::make_weak(controller);
session->api().checkChatInvite(hash, [=](const MTPChatInvite &result) {
@ -114,6 +352,9 @@ void CheckChatInvite(
if (!strong) {
return;
}
if (loaded) {
loaded();
}
Core::App().hideMediaView();
const auto show = [&](not_null<PeerData*> chat) {
const auto way = Window::SectionShow::Way::Forward;
@ -125,11 +366,23 @@ void CheckChatInvite(
};
result.match([=](const MTPDchatInvite &data) {
const auto isGroup = !data.is_broadcast();
const auto box = strong->show(Box<ConfirmInviteBox>(
session,
data,
invitePeekChannel,
[=] { SubmitChatInvite(weak, session, hash, isGroup); }));
const auto hasPricing = !!data.vsubscription_pricing();
if (hasPricing && !data.vsubscription_form_id()) {
strong->uiShow()->showToast(
tr::lng_confirm_phone_link_invalid(tr::now));
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) {
box->boxClosing(
) | rpl::filter([=] {

View file

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

View file

@ -94,32 +94,70 @@ constexpr auto kTransactionsLimit = 100;
}, [](const MTPDstarsTransactionPeerAds &) {
return Data::CreditsHistoryEntry::PeerType::Ads;
}),
.refunded = tl.data().is_refund(),
.pending = tl.data().is_pending(),
.failed = tl.data().is_failed(),
.subscriptionUntil = tl.data().vsubscription_period()
? base::unixtime::parse(base::unixtime::now()
+ tl.data().vsubscription_period()->v)
: QDateTime(),
.successDate = tl.data().vtransaction_date()
? base::unixtime::parse(tl.data().vtransaction_date()->v)
: QDateTime(),
.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),
.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(
const MTPpayments_StarsStatus &status,
not_null<PeerData*> peer) {
peer->owner().processUsers(status.data().vusers());
peer->owner().processChats(status.data().vchats());
const auto &data = status.data();
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{
.list = ranges::views::all(
status.data().vhistory().v
) | ranges::views::transform([&](const MTPStarsTransaction &tl) {
return HistoryFromTL(tl, peer);
}) | ranges::to_vector,
.list = std::move(entries),
.subscriptions = std::move(subscriptions),
.balance = status.data().vbalance().v,
.subscriptionsMissingBalance
= status.data().vsubscriptions_missing_balance().value_or_empty(),
.allLoaded = !status.data().vnext_offset().has_value(),
.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
)).done([=](const TLResult &result) {
_requestId = 0;
done(StatusFromTL(result, _peer));
if (const auto onstack = done) {
onstack(StatusFromTL(result, _peer));
}
}).fail([=] {
_requestId = 0;
done({});
if (const auto onstack = done) {
onstack({});
}
}).send();
}
@ -220,6 +262,7 @@ void CreditsHistory::request(
}
_requestId = _api.request(MTPpayments_GetStarsTransactions(
MTP_flags(_flags),
MTPstring(), // subscription_id
_peer->isSelf() ? MTP_inputPeerSelf() : _peer->input,
MTP_string(token),
MTP_int(kTransactionsLimit)
@ -232,6 +275,25 @@ void CreditsHistory::request(
}).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 {
return _options;
}

View file

@ -60,6 +60,9 @@ public:
void request(
const Data::CreditsStatusSlice::OffsetToken &token,
Fn<void(Data::CreditsStatusSlice)> done);
void requestSubscriptions(
const Data::CreditsStatusSlice::OffsetToken &token,
Fn<void(Data::CreditsStatusSlice)> done);
private:
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_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_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 "base/unixtime.h"
#include "apiwrap.h"
@ -69,59 +69,46 @@ JoinedByLinkSlice ParseJoinedByLinkSlice(
InviteLinks::InviteLinks(not_null<ApiWrap*> api) : _api(api) {
}
void InviteLinks::create(
not_null<PeerData*> peer,
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::create(const CreateInviteLinkArgs &args) {
performCreate(args, false);
}
void InviteLinks::performCreate(
not_null<PeerData*> peer,
Fn<void(Link)> done,
bool revokeLegacyPermanent,
const QString &label,
TimeId expireDate,
int usageLimit,
bool requestApproval) {
if (const auto i = _createCallbacks.find(peer)
const CreateInviteLinkArgs &args,
bool revokeLegacyPermanent) {
if (const auto i = _createCallbacks.find(args.peer)
; i != end(_createCallbacks)) {
if (done) {
i->second.push_back(std::move(done));
if (args.done) {
i->second.push_back(std::move(args.done));
}
return;
}
auto &callbacks = _createCallbacks[peer];
if (done) {
callbacks.push_back(std::move(done));
auto &callbacks = _createCallbacks[args.peer];
if (args.done) {
callbacks.push_back(std::move(args.done));
}
const auto requestApproval = !args.subscription && args.requestApproval;
using Flag = MTPmessages_ExportChatInvite::Flag;
_api->request(MTPmessages_ExportChatInvite(
MTP_flags((revokeLegacyPermanent
? Flag::f_legacy_revoke_permanent
: Flag(0))
| (!label.isEmpty() ? Flag::f_title : Flag(0))
| (expireDate ? Flag::f_expire_date : Flag(0))
| ((!requestApproval && usageLimit)
| (!args.label.isEmpty() ? Flag::f_title : Flag(0))
| (args.expireDate ? Flag::f_expire_date : Flag(0))
| ((!requestApproval && args.usageLimit)
? Flag::f_usage_limit
: Flag(0))
| (requestApproval ? Flag::f_request_needed : Flag(0))),
peer->input,
MTP_int(expireDate),
MTP_int(usageLimit),
MTP_string(label)
)).done([=](const MTPExportedChatInvite &result) {
| (requestApproval ? Flag::f_request_needed : Flag(0))
| (args.subscription ? Flag::f_subscription_pricing : Flag(0))),
args.peer->input,
MTP_int(args.expireDate),
MTP_int(args.usageLimit),
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 link = prepend(peer, peer->session().user(), result);
if (link && callbacks) {
@ -129,7 +116,7 @@ void InviteLinks::performCreate(
callback(*link);
}
}
}).fail([=] {
}).fail([=, peer = args.peer] {
_createCallbacks.erase(peer);
}).send();
}
@ -238,6 +225,15 @@ void InviteLinks::edit(
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(
not_null<PeerData*> peer,
not_null<UserData*> admin,
@ -247,7 +243,8 @@ void InviteLinks::performEdit(
const QString &label,
TimeId expireDate,
int usageLimit,
bool requestApproval) {
bool requestApproval,
bool editOnlyTitle) {
const auto key = LinkKey{ peer, link };
if (_deleteCallbacks.contains(key)) {
return;
@ -272,7 +269,7 @@ void InviteLinks::performEdit(
? Flag::f_request_needed
: Flag(0));
_api->request(MTPmessages_EditExportedChatInvite(
MTP_flags(flags),
MTP_flags(editOnlyTitle ? Flag::f_title : flags),
peer->input,
MTP_string(link),
MTP_int(expireDate),
@ -344,7 +341,7 @@ void InviteLinks::revokePermanent(
} else if (!admin->isSelf()) {
crl::on_main(&peer->session(), done);
} else {
performCreate(peer, callback, true);
performCreate({ peer, callback }, true);
}
}
@ -750,6 +747,12 @@ auto InviteLinks::parse(
return std::optional<Link>(Link{
.link = qs(data.vlink()),
.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()),
.date = data.vdate().v,
.startDate = data.vstart_date().value_or_empty(),

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -22,7 +22,8 @@ class SensitiveContent final {
public:
explicit SensitiveContent(not_null<ApiWrap*> api);
void reload();
void preload();
void reload(bool force = false);
void update(bool enabled);
[[nodiscard]] bool enabledCurrent() const;
@ -32,10 +33,14 @@ public:
private:
const not_null<Main::Session*> _session;
MTP::Sender _api;
mtpRequestId _requestId = 0;
mtpRequestId _loadRequestId = 0;
mtpRequestId _saveRequestId = 0;
rpl::variable<bool> _enabled = false;
rpl::variable<bool> _canChange = false;
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_dc_options.h"
#include "data/business/data_shortcut_messages.h"
#include "data/components/credits.h"
#include "data/components/scheduled_messages.h"
#include "data/components/top_peers.h"
#include "data/notify/data_notify_settings.h"
@ -2626,7 +2627,7 @@ void Updates::feedUpdate(const MTPUpdate &update) {
case mtpc_updateStarsBalance: {
const auto &data = update.c_updateStarsBalance();
_session->setCredits(data.vbalance().v);
_session->credits().apply(data);
} break;
}

View file

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

View file

@ -898,9 +898,10 @@ void GroupInfoBox::checkInviteLink() {
channelReady();
} else if (_createdChannel->isFullLoaded() && !_creatingInviteLink) {
_creatingInviteLink = true;
_createdChannel->session().api().inviteLinks().create(
_createdChannel->session().api().inviteLinks().create({
_createdChannel,
crl::guard(this, [=](auto&&) { channelReady(); }));
crl::guard(this, [=](auto&&) { channelReady(); }),
});
} else {
_createdChannel->session().changes().peerUpdates(
_createdChannel,
@ -1243,7 +1244,7 @@ void SetupChannelBox::mousePressEvent(QMouseEvent *e) {
showToast(tr::lng_create_channel_link_copied(tr::now));
} else if (_channel->isFullLoaded() && !_creatingInviteLink) {
_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())
, _pathBookmark(Core::App().settings().downloadPathBookmark())
, _group(std::make_shared<Ui::RadioenumGroup<Directory>>(typeFromPath(_path)))
, _default(Core::App().canReadDefaultDownloadPath(true)
, _default(Core::App().canReadDefaultDownloadPath()
? object_ptr<Ui::Radioenum<Directory>>(
this,
_group,
@ -149,7 +149,7 @@ void DownloadPathBox::setPathText(const QString &text) {
DownloadPathBox::Directory DownloadPathBox::typeFromPath(
const QString &path) {
if (path.isEmpty()) {
return Core::App().canReadDefaultDownloadPath(true)
return Core::App().canReadDefaultDownloadPath()
? Directory::Downloads
: Directory::Temp;
} else if (path == FileDialog::Tmp()) {

View file

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

View file

@ -39,6 +39,7 @@ void GiftCreditsBox(
not_null<Ui::GenericBox*> box,
not_null<PeerData*> peer,
Fn<void()> gifted) {
box->setWidth(st::boxWideWidth);
box->setStyle(st::creditsGiftBox);
box->setNoContentMargin(true);
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_peer_values.h" // Data::PeerPremiumValue.
#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/stickers/data_custom_emoji.h"
#include "info/channel_statistics/boosts/giveaway/boost_badge.h" // InfiniteRadialAnimationWidget.
@ -66,8 +66,8 @@ namespace {
constexpr auto kUserpicsMax = size_t(3);
using GiftOption = Data::SubscriptionOption;
using GiftOptions = Data::SubscriptionOptions;
using GiftOption = Data::PremiumSubscriptionOption;
using GiftOptions = Data::PremiumSubscriptionOptions;
GiftOptions GiftOptionFromTL(const MTPDuserFull &data) {
auto result = GiftOptions();
@ -75,7 +75,7 @@ GiftOptions GiftOptionFromTL(const MTPDuserFull &data) {
if (!gifts) {
return result;
}
result = Api::SubscriptionOptionsFromTL(gifts->v);
result = Api::PremiumSubscriptionOptionsFromTL(gifts->v);
for (auto &option : result) {
option.costPerMonth = tr::lng_premium_gift_per(
tr::now,
@ -932,8 +932,8 @@ void ShowAlreadyPremiumToast(
Ui::Text::Link(
Ui::Text::Bold(tr::lng_gift_link_already_link(tr::now))),
Ui::Text::WithEntities),
.duration = 6 * crl::time(1000),
.filter = crl::guard(navigation, shareLink),
.duration = 6 * crl::time(1000),
});
}
@ -1642,6 +1642,9 @@ void AddCreditsHistoryEntryTable(
not_null<Window::SessionNavigation*> controller,
not_null<Ui::VerticalLayout*> container,
const Data::CreditsHistoryEntry &entry) {
if (!entry) {
return;
}
auto table = container->add(
object_ptr<Ui::TableLayout>(
container,
@ -1757,3 +1760,56 @@ void AddCreditsHistoryEntryTable(
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 GiveawayStart;
struct GiveawayResults;
struct SubscriptionEntry;
} // namespace Data
namespace Ui {
@ -78,3 +79,13 @@ void AddCreditsHistoryEntryTable(
not_null<Window::SessionNavigation*> controller,
not_null<Ui::VerticalLayout*> container,
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));
} else if (_channel->isFullLoaded() && !_creatingInviteLink) {
_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 = [=] {
if (!sendLink()) {
_peer->session().api().inviteLinks().create(_peer, [=](auto) {
if (!sendLink()) {
close();
}
_peer->session().api().inviteLinks().create({
_peer,
[=](auto) {
if (!sendLink()) {
close();
}
},
});
}
};

View file

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

View file

@ -7,49 +7,58 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#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 "base/unixtime.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_label.h"
#include "ui/wrap/vertical_layout.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/controls/userpic_button.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 "boxes/share_box.h"
#include "history/view/history_view_group_call_bar.h" // GenerateUserpics...
#include "history/history_item_helpers.h" // GetErrorTextForSending.
#include "history/history.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 "ui/widgets/popup_menu.h"
#include "ui/wrap/padding_wrap.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/wrap/vertical_layout.h"
#include "window/window_controller.h"
#include "mtproto/sender.h"
#include "qr/qr_generate.h"
#include "intro/intro_qr.h" // TelegramLogoImage
#include "window/window_session_controller.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_layers.h" // st::boxDividerLabel.
#include "styles/style_menu_icons.h"
#include "styles/style_premium.h"
#include <QtGui/QGuiApplication>
#include <QtCore/QMimeData>
#include <QtGui/QGuiApplication>
#include <QtSvg/QSvgRenderer>
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 {
public:
RequestedRow(not_null<PeerData*> peer, TimeId date);
@ -604,6 +673,101 @@ void Controller::setupAboveJoinedWidget() {
if (revoked || !current.permanent) {
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(
container,
tr::lng_group_invite_created_by());
@ -743,6 +907,11 @@ void Controller::appendSlice(const Api::JoinedByLinkSlice &slice) {
_lastUser = user;
auto row = (_role == Role::Requested)
? 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);
if (_role != Role::Requested && user.viaFilterLink) {
row->setCustomStatus(
@ -760,11 +929,117 @@ void Controller::appendSlice(const Api::JoinedByLinkSlice &slice) {
}
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) {
if (_role != Role::Requested) {
if (_role != Role::Requested || _data.current().subscription) {
return;
}
delegate()->peerListShowRowMenu(row, true);
@ -1251,6 +1526,8 @@ object_ptr<Ui::BoxContent> InviteLinkQrBox(
object_ptr<Ui::BoxContent> EditLinkBox(
not_null<PeerData*> peer,
const Api::InviteLink &data) {
constexpr auto kPeriod = 3600 * 24 * 30;
constexpr auto kTestModePeriod = 300;
const auto creating = data.link.isEmpty();
const auto box = std::make_shared<QPointer<Ui::GenericBox>>();
using Fields = Ui::InviteLinkFields;
@ -1266,13 +1543,25 @@ object_ptr<Ui::BoxContent> EditLinkBox(
};
if (creating) {
Assert(data.admin->isSelf());
peer->session().api().inviteLinks().create(
const auto period = peer->session().isTestMode()
? kTestModePeriod
: kPeriod;
peer->session().api().inviteLinks().create({
peer,
finish,
result.label,
result.expireDate,
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 {
peer->session().api().inviteLinks().edit(
peer,
@ -1287,26 +1576,33 @@ object_ptr<Ui::BoxContent> EditLinkBox(
};
const auto isGroup = !peer->isBroadcast();
const auto isPublic = peer->isChannel() && peer->asChannel()->isPublic();
if (creating) {
auto object = Box(Ui::CreateInviteLinkBox, isGroup, isPublic, done);
*box = Ui::MakeWeak(object.data());
return object;
} else {
auto object = Box(
Ui::EditInviteLinkBox,
Fields{
.link = data.link,
.label = data.label,
.expireDate = data.expireDate,
.usageLimit = data.usageLimit,
.requestApproval = data.requestApproval,
.isGroup = isGroup,
.isPublic = isPublic,
},
done);
*box = Ui::MakeWeak(object.data());
return object;
}
auto object = Box([=](not_null<Ui::GenericBox*> box) {
const auto fill = isGroup
? Fn<Ui::InviteLinkSubscriptionToggle()>(nullptr)
: [=] {
return Ui::FillCreateInviteLinkSubscriptionToggle(box, peer);
};
if (creating) {
Ui::CreateInviteLinkBox(box, fill, isGroup, isPublic, done);
} else {
Ui::EditInviteLinkBox(
box,
fill,
Fields{
.link = data.link,
.label = data.label,
.expireDate = data.expireDate,
.usageLimit = data.usageLimit,
.subscriptionCredits = int(data.subscription.credits),
.requestApproval = data.requestApproval,
.isGroup = isGroup,
.isPublic = isPublic,
},
done);
}
});
*box = Ui::MakeWeak(object.data());
return object;
}
object_ptr<Ui::BoxContent> RevokeLinkBox(

View file

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

View file

@ -282,6 +282,9 @@ void SetupOnlyCustomEmojiField(
const auto offset = size();
if (unifiedId) {
result.text.append('@');
} else if (id.paid()) {
result.text.append(QChar(0x2B50));
unifiedId = reactions->lookupPaid()->selectAnimation->id;
} else {
result.text.append(id.emoji());
const auto i = ranges::find(all, id, &Data::Reaction::id);
@ -312,6 +315,7 @@ struct ReactionsSelectorArgs {
rpl::producer<QString> title;
std::vector<Data::Reaction> list;
std::vector<Data::ReactionId> selected;
rpl::producer<bool> paid;
Fn<void(std::vector<Data::ReactionId>, bool)> callback;
rpl::producer<ReactionsSelectorState> stateValue;
int customAllowed = 0;
@ -341,13 +345,18 @@ object_ptr<Ui::RpWidget> AddReactionsSelector(
std::unique_ptr<UnifiedFactoryOwner> unifiedFactoryOwner;
UnifiedFactoryOwner::RecentFactory factory;
base::flat_set<DocumentId> allowed;
std::vector<Data::ReactionId> reactions;
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>();
state->unifiedFactoryOwner = std::make_unique<UnifiedFactoryOwner>(
session,
reactions->list(Data::Reactions::Type::Active));
normal);
state->factory = state->unifiedFactoryOwner->factory();
state->reactions = std::move(args.selected);
const auto customEmojiPaused = [controller = args.controller] {
return controller->isGifPausedAtLeastFor(PauseReason::Layer);
@ -396,9 +405,32 @@ object_ptr<Ui::RpWidget> AddReactionsSelector(
state->allowed = std::move(allowed);
raw->rawTextEdit()->update();
}
state->reactions = reactions;
callback(std::move(reactions), hardLimitHit);
}, 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;
std::move(
@ -444,10 +476,6 @@ object_ptr<Ui::RpWidget> AddReactionsSelector(
}
}, raw->lifetime());
const auto toggle = Ui::CreateChild<Ui::IconButton>(
parent.get(),
st::manageGroupReactions);
const auto panel = Ui::CreateChild<TabbedPanel>(
args.outer.get(),
args.controller,
@ -458,8 +486,11 @@ object_ptr<Ui::RpWidget> AddReactionsSelector(
(args.all
? TabbedSelector::Mode::FullReactions
: TabbedSelector::Mode::RecentReactions)));
panel->selector()->provideRecentEmoji(
state->unifiedFactoryOwner->unifiedIdsList());
auto panelList = state->unifiedFactoryOwner->unifiedIdsList();
panelList.erase(
ranges::remove(panelList, paid->selectAnimation->id),
end(panelList));
panel->selector()->provideRecentEmoji(panelList);
panel->setDesiredHeightValues(
1.,
st::emojiPanMinHeight / 2,
@ -608,15 +639,17 @@ void EditAllowedReactionsBox(
rpl::variable<SelectorState> selectorState;
std::vector<Data::ReactionId> selected;
rpl::variable<int> customCount;
rpl::variable<bool> paidEnabled;
};
const auto allowed = args.allowed;
const auto optionInitial = (allowed.type != AllowedReactionsType::Some)
? Option::All
: allowed.some.empty()
: (allowed.some.empty() && !allowed.paidEnabled)
? Option::None
: Option::Some;
const auto state = box->lifetime().make_state<State>(State{
.option = optionInitial,
.paidEnabled = allowed.paidEnabled,
});
const auto container = box->verticalLayout();
@ -702,13 +735,19 @@ void EditAllowedReactionsBox(
| ranges::views::transform(&Data::Reaction::id)
| ranges::to_vector)
: allowed.some;
if (allowed.paidEnabled) {
selected.insert(begin(selected), Data::ReactionId::Paid());
}
const auto changed = [=](
std::vector<Data::ReactionId> chosen,
bool hardLimitHit) {
std::vector<Data::ReactionId> chosen,
bool hardLimitHit) {
state->selected = std::move(chosen);
state->customCount = ranges::count_if(
state->selected,
&Data::ReactionId::custom);
state->paidEnabled = ranges::contains(
state->selected,
Data::ReactionId::Paid());
if (hardLimitHit) {
box->uiShow()->showToast(
tr::lng_manage_peer_reactions_limit(tr::now));
@ -727,6 +766,7 @@ void EditAllowedReactionsBox(
.title = tr::lng_manage_peer_reactions_available_ph(),
.list = all,
.selected = state->selected,
.paid = state->paidEnabled.value(),
.callback = changed,
.stateValue = state->selectorState.value(),
.customAllowed = args.allowedCustomReactions,
@ -735,7 +775,7 @@ void EditAllowedReactionsBox(
}), st::boxRowPadding);
box->setFocusCallback([=] {
if (!wrap || state->option.current() == Option::Some) {
if (state->option.current() == Option::Some) {
state->selectorState.force_assign(SelectorState::Active);
}
});
@ -847,6 +887,29 @@ void EditAllowedReactionsBox(
) | rpl::map(rpl::mappers::_1 == SelectorState::Active)));
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 = [=] {
auto result = AllowedReactions();
@ -856,6 +919,9 @@ void EditAllowedReactionsBox(
: (enabled->toggled())) {
result.some = state->selected;
}
if (!isGroup && enabled->toggled()) {
result.paidEnabled = state->paidEnabled.current();
}
auto some = result.some;
auto simple = all | ranges::views::transform(
&Data::Reaction::id
@ -907,15 +973,20 @@ void SaveAllowedReactions(
: allowed.some.empty()
? MTP_chatReactionsNone()
: 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(
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,
updated,
MTP_int(allowed.maxCount)
MTP_int(maxCount),
MTP_bool(paidEnabled)
)).done([=](const MTPUpdates &result) {
peer->session().api().applyUpdates(result);
auto parsed = Data::Parse(updated);
parsed.maxCount = allowed.maxCount;
auto parsed = Data::Parse(updated, maxCount, paidEnabled);
if (const auto chat = peer->asChat()) {
chat->setAllowedReactions(parsed);
} 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/controls/peer_list_dummy.h"
#include "ui/effects/premium_bubble.h"
#include "ui/effects/premium_graphics.h"
#include "ui/widgets/checkbox.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) {
}
@ -421,7 +428,7 @@ void SimpleLimitBox(
(descriptor.complexRatio
? descriptor.premiumLimit
: 2 * descriptor.current),
premiumPossible,
ChooseBubbleType(premiumPossible),
descriptor.phrase,
descriptor.icon);
Ui::AddSkip(top, st::premiumLineTextSkip);
@ -1109,7 +1116,7 @@ void AccountsLimitBox(
: (current > defaultLimit)
? (current + 1)
: (defaultLimit * 2)),
premiumPossible,
ChooseBubbleType(premiumPossible),
std::nullopt,
&st::premiumIconAccounts);
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 "apiwrap.h"
#include "core/ui_integration.h" // Core::MarkedTextContext.
#include "data/components/credits.h"
#include "data/data_credits.h"
#include "data/data_photo.h"
#include "data/data_session.h"
@ -80,14 +81,12 @@ struct PaidMediaData {
}
}
const auto bot = item->viaBot();
const auto sender = item->originalSender();
const auto broadcast = (sender && sender->isBroadcast())
? sender
: message->peer.get();
return {
.invoice = invoice,
.item = item,
.peer = broadcast,
.peer = (bot ? bot : sender ? sender : message->peer.get()),
.photos = photos,
.videos = videos,
};
@ -130,6 +129,16 @@ struct PaidMediaData {
lt_video,
std::move(videosBold),
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(
lt_count,
rpl::single(form->invoice.amount) | tr::to_count(),
@ -301,7 +310,7 @@ void SendCreditsBox(
st::giveawayGiftCodeStartButton.height / 2);
AddChildToWidgetCenter(button.data(), loadingAnimation);
loadingAnimation->showOn(state->confirmButtonBusy.value());
}
}
{
auto buttonText = tr::lng_credits_box_out_confirm(
lt_count,
@ -361,15 +370,11 @@ void SendCreditsBox(
}
{
session->credits().load(true);
const auto balance = Settings::AddBalanceWidget(
content,
session->creditsValue(),
session->credits().balanceValue(),
false);
const auto api = balance->lifetime().make_state<Api::CreditsStatus>(
session->user());
api->request({}, [=](Data::CreditsStatusSlice slice) {
session->setCredits(slice.balance);
});
rpl::combine(
balance->sizeValue(),
content->sizeValue()

View file

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

View file

@ -645,9 +645,10 @@ void SettingsBox(
shareLink = [=] {
if (!copyLink() && !state->generatingLink) {
state->generatingLink = true;
peer->session().api().inviteLinks().create(
peer->session().api().inviteLinks().create({
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();
*toast = Ui::Toast::Show(parent, {
.text = { tr::lng_send_text_no_about(tr::now, lt_types, types) },
.st = &st::defaultMultilineToast,
.attach = RectPart::Bottom,
.duration = kTypesDuration,
.multiline = true,
.slideSide = RectPart::Bottom,
});
});
return result;

View file

@ -8,11 +8,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "chat_helpers/stickers_dice_pack.h"
#include "main/main_session.h"
#include "chat_helpers/stickers_lottie.h"
#include "data/data_session.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 "apiwrap.h"
@ -104,6 +102,11 @@ void DicePack::tryGenerateLocalZero() {
return;
}
const auto generateLocal = [&](int index, const QString &name) {
_map.emplace(
index,
ChatHelpers::GenerateLocalTgsSticker(_session, name));
};
if (_emoji == DicePacks::kDiceString) {
generateLocal(0, u"dice_idle"_q);
} else if (_emoji == DicePacks::kDartString) {
@ -123,32 +126,8 @@ void DicePack::tryGenerateLocalZero() {
}
}
void DicePack::generateLocal(int index, const QString &name) {
const auto path = u":/gui/art/"_q + name + u".tgs"_q;
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) {
DicePacks::DicePacks(not_null<Main::Session*> session)
: _session(session) {
}
DocumentData *DicePacks::lookup(const QString &emoji, int value) {

View file

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

View file

@ -15,12 +15,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_session.h"
#include "data/data_file_origin.h"
#include "storage/cache/storage_cache_database.h"
#include "storage/localimageloader.h"
#include "history/view/media/history_view_media_common.h"
#include "media/clip/media_clip_reader.h"
#include "ui/chat/attach/attach_prepare.h"
#include "ui/effects/path_shift_gradient.h"
#include "ui/image/image_location_factory.h"
#include "ui/painter.h"
#include "main/main_session.h"
#include <xxhash.h>
namespace ChatHelpers {
namespace {
@ -312,4 +317,41 @@ QSize ComputeStickerSize(not_null<DocumentData*> document, QSize box) {
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

View file

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

View file

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

View file

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

View file

@ -209,7 +209,7 @@ public:
void saveSettingsDelayed(crl::time delay = kDefaultSaveDelay);
void saveSettings();
[[nodiscard]] bool canReadDefaultDownloadPath(bool always = false) const;
[[nodiscard]] bool canReadDefaultDownloadPath() const;
[[nodiscard]] bool canSaveFileWithoutAskingForPath() const;
// 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 "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
#include "data/components/credits.h"
#include "data/data_birthday.h"
#include "data/data_channel.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/themes/window_theme_editor_box.h" // GenerateSlug.
#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_global_ttl.h"
#include "settings/settings_folders.h"
@ -1174,6 +1177,52 @@ bool ResolveBoost(
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(
Window::SessionController *controller,
const Match &match,
@ -1280,6 +1329,10 @@ const std::vector<LocalUrlHandler> &LocalUrlHandlers() {
u"^message/?\\?slug=([a-zA-Z0-9\\.\\_\\-]+)(&|$)"_q,
ResolveChatLink
},
{
u"^stars_topup/?\\?(.+)(#|$)"_q,
ResolveTopUp
},
{
u"^user\\?(.+)(#|$)"_q,
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 AppName = "AyuGram Desktop"_cs;
constexpr auto AppFile = "AyuGram"_cs;
constexpr auto AppVersion = 5003002;
constexpr auto AppVersionStr = "5.3.2";
constexpr auto AppVersion = 5004001;
constexpr auto AppVersionStr = "5.4.1";
constexpr auto AppBetaVersion = false;
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;

View file

@ -449,9 +449,6 @@ void ShortcutMessages::preloadShortcuts() {
result.match([&](const MTPDmessages_quickReplies &data) {
owner->processUsers(data.vusers());
owner->processChats(data.vchats());
owner->processMessages(
data.vmessages(),
NewMessageType::Existing);
updateShortcuts(data.vquick_replies().v);
}, [&](const MTPDmessages_quickRepliesNotModified &) {
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 "core/click_handler_types.h"
#include "data/data_channel.h"
#include "data/data_document.h"
#include "data/data_photo.h"
#include "data/data_session.h"
#include "data/data_user.h"
@ -275,6 +276,35 @@ void SponsoredMessages::append(
const MTPSponsoredMessage &message) {
const auto &data = message.data();
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{
.title = qs(data.vtitle()),
.link = qs(data.vurl()),
@ -282,6 +312,8 @@ void SponsoredMessages::append(
.photoId = data.vphoto()
? history->session().data().processPhoto(*data.vphoto())->id
: PhotoId(0),
.mediaPhotoId = mediaPhotoId,
.mediaDocumentId = mediaDocumentId,
.backgroundEmojiId = data.vcolor().has_value()
? data.vcolor()->data().vbackground_emoji_id().value_or_empty()
: uint64(0),
@ -398,6 +430,8 @@ SponsoredMessages::Details SponsoredMessages::lookupDetails(
.link = data.link,
.buttonText = data.from.buttonText,
.photoId = data.from.photoId,
.mediaPhotoId = data.from.mediaPhotoId,
.mediaDocumentId = data.from.mediaDocumentId,
.backgroundEmojiId = data.from.backgroundEmojiId,
.colorIndex = data.from.colorIndex,
.isLinkInternal = data.from.isLinkInternal,

View file

@ -44,6 +44,8 @@ struct SponsoredFrom {
QString link;
QString buttonText;
PhotoId photoId = PhotoId(0);
PhotoId mediaPhotoId = PhotoId(0);
DocumentId mediaDocumentId = DocumentId(0);
uint64 backgroundEmojiId = 0;
uint8 colorIndex : 6 = 0;
bool isLinkInternal = false;
@ -73,6 +75,8 @@ public:
QString link;
QString buttonText;
PhotoId photoId = PhotoId(0);
PhotoId mediaPhotoId = PhotoId(0);
DocumentId mediaDocumentId = DocumentId(0);
uint64 backgroundEmojiId = 0;
uint8 colorIndex : 6 = 0;
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 (diff & Flag::CallNotEmpty) {
history->updateChatListEntry();
@ -203,6 +207,12 @@ void ChannelData::setFlags(ChannelDataFlags which) {
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()) {
@ -534,12 +544,9 @@ auto ChannelData::unavailableReasons() const
return _unavailableReasons;
}
void ChannelData::setUnavailableReasons(
void ChannelData::setUnavailableReasonsList(
std::vector<Data::UnavailableReason> &&reasons) {
if (_unavailableReasons != reasons) {
_unavailableReasons = std::move(reasons);
session().changes().peerUpdated(this, UpdateFlag::UnavailableReason);
}
_unavailableReasons = std::move(reasons);
}
void ChannelData::setAvailableMinId(MsgId availableMinId) {
@ -966,7 +973,8 @@ void ChannelData::setAllowedReactions(Data::AllowedReactions value) {
if (_allowedReactions != value) {
const auto enabled = [](const Data::AllowedReactions &allowed) {
return (allowed.type != Data::AllowedReactionsType::Some)
|| !allowed.some.empty();
|| !allowed.some.empty()
|| allowed.paidEnabled;
};
const auto was = enabled(_allowedReactions);
_allowedReactions = std::move(value);
@ -1028,6 +1036,14 @@ void ChannelData::updateLevelHint(int levelHint) {
_levelHint = levelHint;
}
TimeId ChannelData::subscriptionUntilDate() const {
return _subscriptionUntilDate;
}
void ChannelData::updateSubscriptionUntilDate(TimeId subscriptionUntilDate) {
_subscriptionUntilDate = subscriptionUntilDate;
}
namespace Data {
void ApplyMigration(
@ -1220,11 +1236,16 @@ void ApplyChannelUpdate(
const auto reactionsLimit = update.vreactions_limit().value_or_empty();
if (const auto allowed = update.vavailable_reactions()) {
auto parsed = Data::Parse(*allowed);
parsed.maxCount = reactionsLimit;
auto parsed = Data::Parse(
*allowed,
reactionsLimit,
update.is_paid_reactions_available());
channel->setAllowedReactions(std::move(parsed));
} else {
channel->setAllowedReactions({ .maxCount = reactionsLimit });
channel->setAllowedReactions({
.maxCount = reactionsLimit,
.paidEnabled = update.is_paid_reactions_available(),
});
}
channel->owner().stories().apply(channel, update.vstories());
channel->fullUpdated();

View file

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

View file

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

View file

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

View file

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

View file

@ -242,7 +242,8 @@ template <typename MediaType>
ImageRoundRadius radius,
bool 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));
}
return result;
@ -2266,7 +2267,6 @@ ClickHandlerPtr MediaDice::MakeHandler(
.text = { tr::lng_about_random(tr::now, lt_emoji, emoji) },
.st = &st::historyDiceToast,
.duration = Ui::Toast::kDefaultDuration * 2,
.multiline = true,
};
if (CanSend(history->peer, ChatRestriction::SendOther)) {
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()) {
ShownToast = strong->showToast(std::move(config));
} 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()) };
}, [](const MTPDreactionCustomEmoji &data) {
return ReactionId{ DocumentId(data.vdocument_id().v) };
}, [](const MTPDreactionPaid &) {
return ReactionId::Paid();
});
}
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));
} 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

View file

@ -14,13 +14,28 @@ namespace Data {
struct ReactionId {
std::variant<QString, DocumentId> data;
[[nodiscard]] static QChar PaidTag() {
return '*';
}
[[nodiscard]] static ReactionId Paid() {
return { QString(PaidTag()) };
}
[[nodiscard]] bool empty() const {
const auto emoji = std::get_if<QString>(&data);
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 {
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 {
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 "chat_helpers/stickers_lottie.h"
#include "core/application.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/history_item_components.h"
#include "main/main_session.h"
#include "main/main_app_config.h"
#include "main/session/send_as_peers.h"
#include "data/components/credits.h"
#include "data/data_user.h"
#include "data/data_session.h"
#include "data/data_histories.h"
@ -29,9 +32,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "mtproto/mtproto_config.h"
#include "base/timer_rpl.h"
#include "base/call_delayed.h"
#include "base/unixtime.h"
#include "apiwrap.h"
#include "styles/style_chat.h"
#include "base/random.h"
// AyuGram includes
#include "ayu/ayu_settings.h"
#include "ayu/utils/telegram_helpers.h"
@ -48,6 +54,7 @@ constexpr auto kRecentReactionsLimit = 40;
constexpr auto kMyTagsRequestTimeout = crl::time(1000);
constexpr auto kTopRequestDelay = 60 * crl::time(1000);
constexpr auto kTopReactionsLimit = 14;
constexpr auto kPaidAccumulatePeriod = 5 * crl::time(1000) + 500;
[[nodiscard]] QString ReactionIdToLog(const ReactionId &id) {
if (const auto custom = id.custom()) {
@ -112,17 +119,17 @@ constexpr auto kTopReactionsLimit = 14;
: config->get<int>("reactions_user_max_default", 1);
}
bool IsMyRecent(
[[nodiscard]] bool IsMyRecent(
const MTPDmessagePeerReaction &data,
const ReactionId &id,
not_null<PeerData*> peer,
const base::flat_map<
ReactionId,
std::vector<RecentReaction>> &recent,
bool ignoreChosen) {
if (peer->id == peer->session().userPeerId()) {
bool min) {
if (peer->isSelf()) {
return true;
} else if (!ignoreChosen) {
} else if (!min) {
return data.is_my();
}
const auto j = recent.find(id);
@ -136,10 +143,25 @@ bool IsMyRecent(
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
PossibleItemReactionsRef LookupPossibleReactions(
not_null<HistoryItem*> item) {
not_null<HistoryItem*> item,
bool paidInFront) {
if (!item->canReact()) {
return {};
}
@ -160,6 +182,7 @@ PossibleItemReactionsRef LookupPossibleReactions(
const auto &myTags = reactions->list(Reactions::Type::MyTags);
const auto &tags = reactions->list(Reactions::Type::Tags);
const auto &all = item->reactions();
const auto &allowed = PeerAllowedReactions(peer);
const auto limit = UniqueReactionsLimit(peer);
const auto premiumPossible = session->premiumPossible();
const auto limited = (all.size() >= limit) && [&] {
@ -195,23 +218,30 @@ PossibleItemReactionsRef LookupPossibleReactions(
result.customAllowed = premiumPossible;
result.tags = true;
} else if (limited) {
result.recent.reserve(all.size());
result.recent.reserve((allowed.paidEnabled ? 1 : 0) + all.size());
add([&](const Reaction &reaction) {
return ranges::contains(all, reaction.id, &MessageReaction::id);
});
for (const auto &reaction : all) {
const auto id = reaction.id;
if (!added.contains(id)) {
if (added.emplace(id).second) {
if (const auto temp = reactions->lookupTemporary(id)) {
result.recent.push_back(temp);
}
}
}
if (allowed.paidEnabled
&& !added.contains(Data::ReactionId::Paid())) {
result.recent.push_back(reactions->lookupPaid());
}
} else {
const auto &allowed = PeerAllowedReactions(peer);
result.recent.reserve((allowed.type == AllowedReactionsType::Some)
? allowed.some.size()
: full.size());
result.recent.reserve((allowed.paidEnabled ? 1 : 0)
+ ((allowed.type == AllowedReactionsType::Some)
? allowed.some.size()
: full.size()));
if (allowed.paidEnabled) {
result.recent.push_back(reactions->lookupPaid());
}
add([&](const Reaction &reaction) {
const auto id = reaction.id;
if (id.custom() && !premiumPossible) {
@ -238,12 +268,15 @@ PossibleItemReactionsRef LookupPossibleReactions(
&& premiumPossible;
}
if (!item->reactionsAreTags()) {
const auto i = ranges::find(
result.recent,
reactions->favoriteId(),
&Reaction::id);
if (i != end(result.recent) && i != begin(result.recent)) {
std::rotate(begin(result.recent), i, i + 1);
const auto toFront = [&](Data::ReactionId id) {
const auto i = ranges::find(result.recent, id, &Reaction::id);
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;
@ -264,7 +297,8 @@ PossibleItemReactions::PossibleItemReactions(
Reactions::Reactions(not_null<Session*> owner)
: _owner(owner)
, _topRefreshTimer([=] { refreshTop(); })
, _repaintTimer([=] { repaintCollected(); }) {
, _repaintTimer([=] { repaintCollected(); })
, _sendPaidTimer([=] { sendPaid(); }) {
refreshDefault();
_myTags.emplace(nullptr);
@ -283,6 +317,15 @@ Reactions::Reactions(not_null<Session*> owner)
_pollingItems.remove(item);
_pollItems.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);
crl::on_main(&owner->session(), [=] {
@ -509,21 +552,49 @@ DocumentData *Reactions::chooseGenericAnimation(
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;
}
auto copy = _genericAnimations;
ranges::shuffle(copy);
const auto first = copy.front();
ranges::shuffle(list);
const auto first = list.front();
const auto view = first->createMediaView();
view->checkStickerLarge();
if (view->loaded()) {
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 (k != end(copy)) ? (*k) : first;
return (k != end(list)) ? (*k) : first;
}
void Reactions::applyFavorite(const ReactionId &id) {
@ -574,7 +645,7 @@ rpl::producer<> Reactions::effectsUpdates() const {
}
void Reactions::preloadReactionImageFor(const ReactionId &emoji) {
if (!emoji.emoji().isEmpty()) {
if (emoji.paid() || !emoji.emoji().isEmpty()) {
preloadImageFor(emoji);
}
}
@ -591,6 +662,10 @@ void Reactions::preloadImageFor(const ReactionId &id) {
}
auto &set = _images.emplace(id).first->second;
set.effect = (id.custom() != 0);
if (id.paid()) {
loadImage(set, lookupPaid()->centerIcon, true);
return;
}
auto &list = set.effect ? _effects : _available;
const auto i = ranges::find(list, id, &Reaction::id);
const auto document = (i == end(list))
@ -626,6 +701,20 @@ void Reactions::preloadEffect(const Reaction &effect) {
}
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 document = custom ? _owner->document(custom).get() : nullptr;
const auto customSticker = document ? document->sticker() : nullptr;
@ -636,15 +725,6 @@ void Reactions::preloadAnimationsFor(const ReactionId &id) {
if (i == end(_available)) {
return;
}
const auto preload = [&](DocumentData *document) {
const auto view = document
? document->activeMediaView()
: nullptr;
if (view) {
view->checkStickerLarge();
}
};
if (!custom) {
preload(i->centerIcon);
}
@ -1361,7 +1441,10 @@ void Reactions::send(not_null<HistoryItem*> item, bool addToRecent) {
MTP_flags(flags),
item->history()->peer->input,
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
) | ranges::to<QVector<MTPReaction>>())
)).done([=](const MTPUpdates &result) {
@ -1413,7 +1496,9 @@ void Reactions::clearTemporary() {
}
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);
return (i != end(_available)) ? &*i : nullptr;
} else if (const auto customId = id.custom()) {
@ -1434,6 +1519,41 @@ Reaction *Reactions::lookupTemporary(const ReactionId &id) {
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(
SavedSublist *sublist) {
refreshMyTags(sublist);
@ -1448,6 +1568,45 @@ rpl::producer<std::vector<Reaction>> Reactions::myTagsValue(
) | 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() {
const auto now = crl::now();
auto closest = crl::time();
@ -1503,7 +1662,8 @@ void Reactions::pollCollected() {
}
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) {
@ -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)
: _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) {
Expects(!id.empty());
Expects(!id.paid());
const auto history = _item->history();
const auto myLimit = SentReactionsLimit(_item);
@ -1554,6 +1852,9 @@ void MessageReactions::add(const ReactionId &id, bool addToRecent) {
history->owner().reactions().incrementMyTag(id, sublist);
}
_list.erase(ranges::remove_if(_list, [&](MessageReaction &one) {
if (one.id.paid()) {
return false;
}
const auto removing = one.my && (my == myLimit || ++my == myLimit);
if (!removing) {
return false;
@ -1603,6 +1904,8 @@ void MessageReactions::add(const ReactionId &id, bool addToRecent) {
}
void MessageReactions::remove(const ReactionId &id) {
Expects(!id.paid());
const auto history = _item->history();
const auto self = history->session().user();
const auto i = ranges::find(_list, id, &MessageReaction::id);
@ -1706,6 +2009,7 @@ bool MessageReactions::checkIfChanged(
bool MessageReactions::change(
const QVector<MTPReactionCount> &list,
const QVector<MTPMessagePeerReaction> &recent,
const QVector<MTPMessageReactor> &top,
bool min) {
auto &owner = _item->history()->owner();
if (owner.reactions().sending(_item)) {
@ -1785,8 +2089,7 @@ bool MessageReactions::change(
if (list.size() >= i->count) {
return;
}
const auto peerId = peerFromMTP(data.vpeer_id());
const auto peer = owner.peer(peerId);
const auto peer = owner.peer(peerFromMTP(data.vpeer_id()));
const auto my = IsMyRecent(data, id, peer, _recent, min);
list.push_back({
.peer = peer,
@ -1800,6 +2103,57 @@ bool MessageReactions::change(
_recent = std::move(parsed);
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;
}
@ -1812,6 +2166,11 @@ auto MessageReactions::recent() const
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 {
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 {
return _list
| ranges::views::filter(&MessageReaction::my)

View file

@ -59,7 +59,8 @@ struct PossibleItemReactions {
};
[[nodiscard]] PossibleItemReactionsRef LookupPossibleReactions(
not_null<HistoryItem*> item);
not_null<HistoryItem*> item,
bool paidInFront = false);
struct MyTagInfo {
ReactionId id;
@ -67,6 +68,12 @@ struct MyTagInfo {
int count = 0;
};
struct PaidReactionSend {
int count = 0;
bool valid = false;
bool anonymous = false;
};
class Reactions final : private CustomEmojiManager::Listener {
public:
explicit Reactions(not_null<Session*> owner);
@ -106,6 +113,7 @@ public:
void renameTag(const ReactionId &id, const QString &name);
[[nodiscard]] DocumentData *chooseGenericAnimation(
not_null<DocumentData*> custom) const;
[[nodiscard]] DocumentData *choosePaidReactionAnimation() const;
[[nodiscard]] rpl::producer<> topUpdates() const;
[[nodiscard]] rpl::producer<> recentUpdates() const;
@ -137,10 +145,19 @@ public:
void clearTemporary();
[[nodiscard]] Reaction *lookupTemporary(const ReactionId &id);
[[nodiscard]] not_null<Reaction*> lookupPaid();
[[nodiscard]] not_null<DocumentData*> paidToastAnimation();
[[nodiscard]] rpl::producer<std::vector<Reaction>> myTagsValue(
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);
static void CheckUnknownForUnread(
not_null<Session*> owner,
@ -231,9 +248,27 @@ private:
void resolveEffectImages();
void downloadTaskFinished();
void fillPaidReactionAnimations() const;
[[nodiscard]] DocumentData *randomLoadedFrom(
std::vector<not_null<DocumentData*>> list) const;
void repaintCollected();
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;
std::vector<Reaction> _active;
@ -252,6 +287,7 @@ private:
std::vector<ReactionId> _topIds;
base::flat_set<ReactionId> _unresolvedTop;
std::vector<not_null<DocumentData*>> _genericAnimations;
mutable std::vector<not_null<DocumentData*>> _paidReactionAnimations;
std::vector<Reaction> _effects;
ReactionId _favoriteId;
ReactionId _unresolvedFavoriteId;
@ -262,6 +298,9 @@ private:
base::flat_map<
not_null<DocumentData*>,
std::shared_ptr<DocumentMedia>> _genericCache;
mutable base::flat_map<
not_null<DocumentData*>,
std::shared_ptr<DocumentMedia>> _paidReactionCache;
rpl::event_stream<> _topUpdated;
rpl::event_stream<> _recentUpdated;
rpl::event_stream<> _defaultUpdated;
@ -275,6 +314,8 @@ private:
// So we use std::map instead of base::flat_map here.
// Otherwise we could use flat_map<DocumentId, unique_ptr<Reaction>>.
std::map<DocumentId, Reaction> _temporary;
std::optional<Reaction> _paid;
DocumentData *_paidToastAnimation = nullptr;
base::Timer _topRefreshTimer;
mtpRequestId _topRequestId = 0;
@ -308,6 +349,10 @@ private:
base::flat_set<not_null<HistoryItem*>> _pollingItems;
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;
rpl::lifetime _lifetime;
@ -320,42 +365,77 @@ struct RecentReaction {
bool big = false;
bool my = false;
friend inline auto operator<=>(
const RecentReaction &a,
const RecentReaction &b) = default;
friend inline bool operator==(
const RecentReaction &a,
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 {
public:
explicit MessageReactions(not_null<HistoryItem*> item);
~MessageReactions();
using TopPaid = MessageReactionsTopPaid;
void add(const ReactionId &id, bool addToRecent);
void remove(const ReactionId &id);
bool change(
const QVector<MTPReactionCount> &list,
const QVector<MTPMessagePeerReaction> &recent,
bool ignoreChosen);
const QVector<MTPMessageReactor> &top,
bool min);
[[nodiscard]] bool checkIfChanged(
const QVector<MTPReactionCount> &list,
const QVector<MTPMessagePeerReaction> &recent,
bool ignoreChosen) const;
bool min) const;
[[nodiscard]] const std::vector<MessageReaction> &list() const;
[[nodiscard]] auto recent() const
-> const base::flat_map<ReactionId, std::vector<RecentReaction>> &;
[[nodiscard]] const std::vector<TopPaid> &topPaid() const;
[[nodiscard]] std::vector<ReactionId> chosen() const;
[[nodiscard]] bool empty() const;
[[nodiscard]] bool hasUnread() const;
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:
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;
std::vector<MessageReaction> _list;
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 "api/api_sensitive_content.h"
#include "data/data_user.h"
#include "data/data_chat.h"
#include "data/data_chat_participant_status.h"
@ -58,6 +59,11 @@ constexpr auto kUserpicSize = 160;
using UpdateFlag = Data::PeerUpdate::Flag;
[[nodiscard]] const std::vector<QString> &IgnoredReasons(
not_null<Main::Session*> session) {
return session->appConfig().ignoredRestrictionReasons();
}
} // namespace
namespace Data {
@ -74,6 +80,62 @@ PeerId FakePeerIdForJustName(const QString &name) {
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(
not_null<BotInfo*> info,
const MTPBotMenuButton *button) {
@ -95,28 +157,22 @@ bool ApplyBotMenuButton(
return changed;
}
bool operator<(
const AllowedReactions &a,
const AllowedReactions &b) {
return (a.type < b.type) || ((a.type == b.type) && (a.some < b.some));
}
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) {
AllowedReactions Parse(
const MTPChatReactions &value,
int maxCount,
bool paidEnabled) {
return value.match([&](const MTPDchatReactionsNone &) {
return AllowedReactions();
return AllowedReactions{
.maxCount = maxCount,
.paidEnabled = paidEnabled,
};
}, [&](const MTPDchatReactionsAll &data) {
return AllowedReactions{
.maxCount = maxCount,
.type = (data.is_allow_custom()
? AllowedReactionsType::All
: AllowedReactionsType::Default),
.paidEnabled = paidEnabled,
};
}, [&](const MTPDchatReactionsSome &data) {
return AllowedReactions{
@ -125,7 +181,9 @@ AllowedReactions Parse(const MTPChatReactions &value) {
) | ranges::views::transform(
ReactionFromMTP
) | ranges::to_vector,
.maxCount = maxCount,
.type = AllowedReactionsType::Some,
.paidEnabled = paidEnabled,
};
});
}
@ -501,18 +559,50 @@ auto PeerData::unavailableReasons() const
}
QString PeerData::computeUnavailableReason() const {
const auto &list = unavailableReasons();
const auto &config = session().appConfig();
const auto skip = config.get<std::vector<QString>>(
"ignore_restriction_reasons",
std::vector<QString>());
auto &&filtered = ranges::views::all(
list
) | ranges::views::filter([&](const Data::UnavailableReason &reason) {
return !ranges::contains(skip, reason.reason);
});
const auto first = filtered.begin();
return (first != filtered.end()) ? first->text : QString();
return Data::UnavailableReason::Compute(
&session(),
unavailableReasons());
}
bool PeerData::hasSensitiveContent() const {
return _sensitiveContent == 1;
}
void PeerData::setUnavailableReasonsList(
std::vector<Data::UnavailableReason> &&reasons) {
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().
@ -1222,9 +1312,12 @@ Data::RestrictionCheckResult PeerData::amRestricted(
}
bool PeerData::amAnonymous() const {
return isBroadcast()
|| (isChannel()
&& (asChannel()->adminRights() & ChatAdminRight::Anonymous));
if (const auto channel = asChannel()) {
return channel->isBroadcast()
? !channel->signatureProfiles()
: (channel->adminRights() & ChatAdminRight::Anonymous);
}
return false;
}
bool PeerData::canRevokeFullHistory() const {

View file

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

View file

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

View file

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

View file

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

View file

@ -252,7 +252,7 @@ Image *PhotoData::getReplyPreview(
Image *PhotoData::getReplyPreview(not_null<HistoryItem*> item) {
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);
}

View file

@ -9,7 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Data {
struct SubscriptionOption {
struct PremiumSubscriptionOption {
QString duration;
QString discount;
QString costPerMonth;
@ -17,6 +17,6 @@ struct SubscriptionOption {
QString total;
QString botUrl;
};
using SubscriptionOptions = std::vector<SubscriptionOption>;
using PremiumSubscriptionOptions = std::vector<PremiumSubscriptionOption>;
} // 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(
const QVector<MTPPhotoSize> &sizes) {
const auto i = ranges::find(
@ -345,6 +321,22 @@ Session::Session(not_null<Main::Session*> session)
_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() {
@ -616,12 +608,8 @@ not_null<UserData*> Session::processUser(const MTPUser &data) {
result->input = MTP_inputPeerUser(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(
ExtractUnavailableReasons(restriction->v));
} else {
result->setUnavailableReasons({});
}
result->setUnavailableReasons(Data::UnavailableReason::Extract(
data.vrestriction_reason()));
}
if (data.is_deleted()) {
if (!result->phone().isEmpty()) {
@ -890,6 +878,8 @@ not_null<PeerData*> Session::processChat(const MTPChat &data) {
const auto wasCallNotEmpty = Data::ChannelHasActiveCall(channel);
channel->updateLevelHint(data.vlevel().value_or_empty());
channel->updateSubscriptionUntilDate(
data.vsubscription_until_date().value_or_empty());
if (const auto count = data.vparticipants_count()) {
channel->setMembersCount(count->v);
}
@ -923,12 +913,8 @@ not_null<PeerData*> Session::processChat(const MTPChat &data) {
channel->setAccessHash(
data.vaccess_hash().value_or(channel->access));
channel->date = data.vdate().v;
if (const auto restriction = data.vrestriction_reason()) {
channel->setUnavailableReasons(
ExtractUnavailableReasons(restriction->v));
} else {
channel->setUnavailableReasons({});
}
channel->setUnavailableReasons(Data::UnavailableReason::Extract(
data.vrestriction_reason()));
}
{
@ -954,6 +940,7 @@ not_null<PeerData*> Session::processChat(const MTPChat &data) {
| Flag::Gigagroup
| Flag::Username
| Flag::Signatures
| Flag::SignatureProfiles
| Flag::HasLink
| Flag::SlowmodeEnabled
| Flag::CallActive
@ -982,6 +969,7 @@ not_null<PeerData*> Session::processChat(const MTPChat &data) {
| (data.is_gigagroup() ? Flag::Gigagroup : Flag())
| (hasUsername ? Flag::Username : Flag())
| (data.is_signatures() ? Flag::Signatures : Flag())
| (data.is_signature_profiles() ? Flag::SignatureProfiles : Flag())
| (data.is_has_link() ? Flag::HasLink : Flag())
| (data.is_slowmode_enabled() ? Flag::SlowmodeEnabled : 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) {
const auto history = historyLoaded(peer);
if (history) {
const auto markLeft = [&] {
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()) {
setChatPinned(history, FilterId(), false);
}
removeChatListEntry(history);
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::ClearType::Unload
: History::ClearType::DeleteChat);
}
if (const auto channel = peer->asMegagroup()) {
channel->addFlags(ChannelDataFlag::Left);
if (const auto from = channel->getMigrateFromChat()) {
if (const auto migrated = historyLoaded(from)) {
migrated->updateChatListExistence();
}
}
} else {
markLeft();
}
}
@ -1811,7 +1806,7 @@ rpl::producer<not_null<ViewElement*>> Session::viewResizeRequest() const {
return _viewResizeRequest.events();
}
void Session::requestItemViewRefresh(not_null<HistoryItem*> item) {
void Session::requestItemViewRefresh(not_null<const HistoryItem*> item) {
if (const auto view = item->mainView()) {
notifyHistoryChangeDelayed(item->history());
view->refreshInBlock();
@ -1819,7 +1814,7 @@ void Session::requestItemViewRefresh(not_null<HistoryItem*> 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();
}
@ -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(
uint64 processId,
not_null<HistoryItem*> item) {
@ -1892,6 +1912,14 @@ rpl::producer<not_null<const ViewElement*>> Session::viewRemoved() const {
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) {
_historyUnloaded.fire_copy(history);
}
@ -2592,6 +2620,9 @@ void Session::unregisterMessage(not_null<HistoryItem*> item) {
const auto peerId = item->history()->peer->id;
const auto itemId = item->id;
_itemRemoved.fire_copy(item);
if (item->hasPossibleRestrictions()) {
_possiblyRestricted.remove(item);
}
session().changes().messageUpdated(
item,
Data::MessageUpdate::Flag::Destroyed);

View file

@ -69,6 +69,7 @@ class SavedMessages;
class Chatbots;
class BusinessInfo;
struct ReactionId;
struct UnavailableReason;
struct RepliesReadTillUpdate {
FullMsgId id;
@ -288,8 +289,8 @@ public:
[[nodiscard]] rpl::producer<not_null<const HistoryItem*>> itemResizeRequest() const;
void requestViewResize(not_null<ViewElement*> view);
[[nodiscard]] rpl::producer<not_null<ViewElement*>> viewResizeRequest() const;
void requestItemViewRefresh(not_null<HistoryItem*> item);
[[nodiscard]] rpl::producer<not_null<HistoryItem*>> itemViewRefreshRequest() const;
void requestItemViewRefresh(not_null<const HistoryItem*> item);
[[nodiscard]] rpl::producer<not_null<const HistoryItem*>> itemViewRefreshRequest() const;
void requestItemTextRefresh(not_null<HistoryItem*> item);
void requestUnreadReactionsAnimation(not_null<HistoryItem*> item);
void notifyHistoryUnloaded(not_null<const History*> history);
@ -306,11 +307,20 @@ public:
[[nodiscard]] rpl::producer<not_null<const History*>> historyCleared() const;
void notifyHistoryChangeDelayed(not_null<History*> history);
[[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 notifyPinnedDialogsOrderUpdated();
[[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(
uint64 processId,
not_null<HistoryItem*> item);
@ -918,11 +928,12 @@ private:
rpl::event_stream<not_null<const ViewElement*>> _viewRepaintRequest;
rpl::event_stream<not_null<const HistoryItem*>> _itemResizeRequest;
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*>> _itemDataChanges;
rpl::event_stream<not_null<const HistoryItem*>> _itemRemoved;
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*>> _historyCleared;
base::flat_set<not_null<History*>> _historiesChanged;
@ -1018,6 +1029,10 @@ private:
base::flat_multi_map<TimeId, not_null<PollData*>> _pollsClosings;
base::Timer _pollsClosingTimer;
base::flat_map<
not_null<const HistoryItem*>,
base::flat_set<QString>> _possiblyRestricted;
base::flat_map<FolderId, std::unique_ptr<Folder>> _folders;
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.
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; }
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 "api/api_sensitive_content.h"
#include "storage/localstorage.h"
#include "storage/storage_user_photos.h"
#include "main/main_session.h"
@ -122,14 +123,9 @@ auto UserData::unavailableReasons() const
return _unavailableReasons;
}
void UserData::setUnavailableReasons(
void UserData::setUnavailableReasonsList(
std::vector<Data::UnavailableReason> &&reasons) {
if (_unavailableReasons != reasons) {
_unavailableReasons = std::move(reasons);
session().changes().peerUpdated(
this,
UpdateFlag::UnavailableReason);
}
_unavailableReasons = std::move(reasons);
}
void UserData::setCommonChatsCount(int count) {
@ -336,7 +332,11 @@ void UserData::setBotInfo(const MTPBotInfo &info) {
d.vmenu_button());
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);
}
} break;
@ -526,6 +526,10 @@ void UserData::setBirthday(Data::Birthday value) {
if (_birthday != value) {
_birthday = value;
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