Merge tag 'v5.13.1' into dev

This commit is contained in:
AlexeyZavar 2025-04-25 02:03:14 +03:00
commit 1447b62999
200 changed files with 6117 additions and 1350 deletions

View file

@ -231,6 +231,8 @@ PRIVATE
api/api_peer_colors.h
api/api_peer_photo.cpp
api/api_peer_photo.h
api/api_peer_search.cpp
api/api_peer_search.h
api/api_polls.cpp
api/api_polls.h
api/api_premium.cpp
@ -764,6 +766,8 @@ PRIVATE
dialogs/dialogs_main_list.h
dialogs/dialogs_pinned_list.cpp
dialogs/dialogs_pinned_list.h
dialogs/dialogs_quick_action.cpp
dialogs/dialogs_quick_action.h
dialogs/dialogs_row.cpp
dialogs/dialogs_row.h
dialogs/dialogs_search_from_controllers.cpp
@ -1001,6 +1005,8 @@ PRIVATE
history/history_unread_things.h
history/history_view_highlight_manager.cpp
history/history_view_highlight_manager.h
history/history_view_swipe_back_session.cpp
history/history_view_swipe_back_session.h
history/history_widget.cpp
history/history_widget.h
info/bot/earn/info_bot_earn_list.cpp
@ -2143,14 +2149,16 @@ if (LINUX AND DESKTOP_APP_USE_PACKAGED)
configure_file("../lib/xdg/com.ayugram.desktop.metainfo.xml" "${CMAKE_CURRENT_BINARY_DIR}/com.ayugram.desktop.metainfo.xml" @ONLY)
generate_appdata_changelog(Telegram "${CMAKE_SOURCE_DIR}/changelog.txt" "${CMAKE_CURRENT_BINARY_DIR}/com.ayugram.desktop.metainfo.xml")
install(TARGETS Telegram RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" BUNDLE DESTINATION "${CMAKE_INSTALL_BINDIR}")
install(FILES "Resources/art/icon16.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/16x16/apps" RENAME "ayugram.png")
install(FILES "Resources/art/icon32.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/32x32/apps" RENAME "ayugram.png")
install(FILES "Resources/art/icon48.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/48x48/apps" RENAME "ayugram.png")
install(FILES "Resources/art/icon64.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/64x64/apps" RENAME "ayugram.png")
install(FILES "Resources/art/icon128.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/128x128/apps" RENAME "ayugram.png")
install(FILES "Resources/art/icon256.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/256x256/apps" RENAME "ayugram.png")
install(FILES "Resources/art/icon512.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/512x512/apps" RENAME "ayugram.png")
install(FILES "Resources/icons/tray_monochrome.svg" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/symbolic/apps" RENAME "ayugram-symbolic.svg")
install(FILES "Resources/art/icon16.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/16x16/apps" RENAME "com.ayugram.desktop.png")
install(FILES "Resources/art/icon32.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/32x32/apps" RENAME "com.ayugram.desktop.png")
install(FILES "Resources/art/icon48.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/48x48/apps" RENAME "com.ayugram.desktop.png")
install(FILES "Resources/art/icon64.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/64x64/apps" RENAME "com.ayugram.desktop.png")
install(FILES "Resources/art/icon128.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/128x128/apps" RENAME "com.ayugram.desktop.png")
install(FILES "Resources/art/icon256.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/256x256/apps" RENAME "com.ayugram.desktop.png")
install(FILES "Resources/art/icon512.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/512x512/apps" RENAME "com.ayugram.desktop.png")
install(FILES "Resources/icons/tray_monochrome.svg" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/symbolic/apps" RENAME "com.ayugram.desktop-symbolic.svg")
install(FILES "Resources/icons/tray_monochrome_attention.svg" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/symbolic/apps" RENAME "com.ayugram.desktop-attention-symbolic.svg")
install(FILES "Resources/icons/tray_monochrome_mute.svg" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/symbolic/apps" RENAME "com.ayugram.desktop-mute-symbolic.svg")
install(FILES "../lib/xdg/com.ayugram.desktop.desktop" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/applications")
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/com.ayugram.desktop.service" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/dbus-1/services")
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/com.ayugram.desktop.metainfo.xml" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/metainfo")

File diff suppressed because one or more lines are too long

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.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 670 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 630 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 748 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="plane" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M1.3311718,6.36592184 C5.3576954,4.67244493 8.04267511,3.5560013 9.38611094,3.01659096 C13.2218932,1.47646481 14.0189359,1.2089284 14.5384372,1.2 C14.6526967,1.19815119 14.9081723,1.22548649 15.0736587,1.35511219 C15.2133922,1.4645656 15.2518384,1.61242159 15.2702362,1.71619544 C15.288634,1.81996929 15.3115436,2.05636876 15.2933322,2.24108442 C15.0854698,4.34939964 14.1860526,9.46572464 13.7284802,11.8270738 C13.5348641,12.8262491 13.1536281,13.1612675 12.7845475,13.1940535 C11.9824498,13.265305 11.3733733,12.6823476 10.5965026,12.190753 C9.3808532,11.4215044 8.69408865,10.9426448 7.51409044,10.1920004 C6.15039834,9.32450079 7.03442319,8.84770795 7.81158733,8.06849502 C8.01497489,7.86457129 11.5490353,4.7615061 11.6174372,4.48000946 C11.625992,4.44480359 11.6339313,4.31357282 11.5531696,4.24427815 C11.472408,4.17498349 11.3532107,4.19867957 11.2671947,4.21752527 C11.1452695,4.24423848 9.20325394,5.48334063 5.44114787,7.93483171 C4.88991321,8.30022994 4.39062196,8.47826423 3.94327414,8.46893456 C3.45010907,8.45864936 2.50145729,8.19975808 1.79623221,7.97846422 C0.931244952,7.70703829 0.243770289,7.56353344 0.303633888,7.10256824 C0.334814555,6.86246904 0.677327192,6.61692024 1.3311718,6.36592184 Z" id="Path-3" fill="#FFFFFF"></path>
</g>
<circle class="error" fill="#f23c34" cx="3.9" cy="12.7" r="2.2"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="plane" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M1.3311718,6.36592184 C5.3576954,4.67244493 8.04267511,3.5560013 9.38611094,3.01659096 C13.2218932,1.47646481 14.0189359,1.2089284 14.5384372,1.2 C14.6526967,1.19815119 14.9081723,1.22548649 15.0736587,1.35511219 C15.2133922,1.4645656 15.2518384,1.61242159 15.2702362,1.71619544 C15.288634,1.81996929 15.3115436,2.05636876 15.2933322,2.24108442 C15.0854698,4.34939964 14.1860526,9.46572464 13.7284802,11.8270738 C13.5348641,12.8262491 13.1536281,13.1612675 12.7845475,13.1940535 C11.9824498,13.265305 11.3733733,12.6823476 10.5965026,12.190753 C9.3808532,11.4215044 8.69408865,10.9426448 7.51409044,10.1920004 C6.15039834,9.32450079 7.03442319,8.84770795 7.81158733,8.06849502 C8.01497489,7.86457129 11.5490353,4.7615061 11.6174372,4.48000946 C11.625992,4.44480359 11.6339313,4.31357282 11.5531696,4.24427815 C11.472408,4.17498349 11.3532107,4.19867957 11.2671947,4.21752527 C11.1452695,4.24423848 9.20325394,5.48334063 5.44114787,7.93483171 C4.88991321,8.30022994 4.39062196,8.47826423 3.94327414,8.46893456 C3.45010907,8.45864936 2.50145729,8.19975808 1.79623221,7.97846422 C0.931244952,7.70703829 0.243770289,7.56353344 0.303633888,7.10256824 C0.334814555,6.86246904 0.677327192,6.61692024 1.3311718,6.36592184 Z" id="Path-3" fill="#FFFFFF"></path>
</g>
<circle fill="#888888" cx="3.9" cy="12.7" r="2.2"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -681,6 +681,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_shortcuts_archive_chat" = "Archive chat";
"lng_shortcuts_media_fullscreen" = "Toggle video fullscreen";
"lng_shortcuts_show_chat_menu" = "Show chat menu";
"lng_shortcuts_show_chat_preview" = "Show chat preview";
"lng_settings_chat_reactions_title" = "Quick Reaction";
"lng_settings_chat_reactions_subtitle" = "Choose your favorite reaction";
@ -1179,6 +1180,24 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_settings_restart_now" = "Restart";
"lng_settings_restart_later" = "Later";
"lng_settings_quick_dialog_action_title" = "Chat list quick action";
"lng_settings_quick_dialog_action_about" = "Choose the action you want to perform when you middle-click or swipe left in the chat list.";
"lng_settings_quick_dialog_action_both" = "Swipe left and Middle-click";
"lng_settings_quick_dialog_action_swipe" = "Swipe left";
"lng_settings_quick_dialog_action_mute" = "Mute";
"lng_settings_quick_dialog_action_unmute" = "Unmute";
"lng_settings_quick_dialog_action_pin" = "Pin";
"lng_settings_quick_dialog_action_unpin" = "Unpin";
"lng_settings_quick_dialog_action_read" = "Read";
"lng_settings_quick_dialog_action_unread" = "Unread";
"lng_settings_quick_dialog_action_archive" = "Archive";
"lng_settings_quick_dialog_action_unarchive" = "Unarchive";
"lng_settings_quick_dialog_action_delete" = "Delete";
"lng_settings_quick_dialog_action_disabled" = "Change folder";
"lng_settings_generic_subscribe" = "Subscribe to {link} to use this setting.";
"lng_settings_generic_subscribe_link" = "Telegram Premium";
"lng_sessions_header" = "This device";
"lng_sessions_other_header" = "Active Devices";
"lng_sessions_other_desc" = "You can log in to Telegram from other mobile, tablet and desktop devices, using the same phone number. All your data will be instantly synchronized.";
@ -1294,6 +1313,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_edit_privacy_gifts_always_title" = "Always allow";
"lng_edit_privacy_gifts_never_title" = "Never allow";
"lng_edit_privacy_gifts_types" = "Accepted Gift Types";
"lng_edit_privacy_gifts_premium" = "Premium Subscriptions";
"lng_edit_privacy_gifts_unlimited" = "Unlimited";
"lng_edit_privacy_gifts_limited" = "Limited-Edition";
"lng_edit_privacy_gifts_unique" = "Unique";
"lng_edit_privacy_gifts_types_about" = "Choose the types of gifts that you accept.";
"lng_edit_privacy_gifts_show_icon" = "Show Gift Icon in Chats";
"lng_edit_privacy_gifts_show_icon_about" = "Display the {emoji}Gift icon in the message input field for both participants in all chats.";
"lng_edit_privacy_gifts_restricted" = "This user doesn't accept gifts.";
"lng_edit_privacy_calls_title" = "Calls";
"lng_edit_privacy_calls_header" = "Who can call me";
"lng_edit_privacy_calls_always_empty" = "Always allow";
@ -2173,6 +2202,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_action_paid_message_some#other" = "send {count} messages";
"lng_action_paid_message_got#one" = "You received {count} Star from {name}";
"lng_action_paid_message_got#other" = "You received {count} Stars from {name}";
"lng_action_paid_message_refund#one" = "{from} refunded {count} Star to you";
"lng_action_paid_message_refund#other" = "{from} refunded {count} Stars to you";
"lng_action_paid_message_refund_self#one" = "You refunded {count} Star to {name}";
"lng_action_paid_message_refund_self#other" = "You refunded {count} Stars to {name}";
"lng_action_message_price_free" = "Messages are now free in this group.";
"lng_action_message_price_paid#one" = "Messages now cost {count} Star each in this group.";
"lng_action_message_price_paid#other" = "Messages now cost {count} Stars each in this group.";
"lng_you_paid_stars#one" = "You paid {count} Star.";
"lng_you_paid_stars#other" = "You paid {count} Stars.";
@ -2906,8 +2942,29 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_chatbots_include_button" = "Select Chats";
"lng_chatbots_exclude_about" = "Select chats or entire chat categories which the bot will not have access to.";
"lng_chatbots_permissions_title" = "Bot permissions";
"lng_chatbots_manage_messages" = "Manage Messages";
"lng_chatbots_read" = "Read Messages";
"lng_chatbots_reply" = "Reply to Messages";
"lng_chatbots_reply_about" = "The bot can only reply on your behalf in chats that were active during the last 24h.";
"lng_chatbots_mark_as_read" = "Mark Messages as Read";
"lng_chatbots_delete_sent" = "Delete Sent Messages";
"lng_chatbots_delete_received" = "Delete Received Messages";
"lng_chatbots_manage_profile" = "Manage Profile";
"lng_chatbots_edit_name" = "Edit Name";
"lng_chatbots_edit_bio" = "Edit Bio";
"lng_chatbots_edit_userpic" = "Edit Profile Picture";
"lng_chatbots_edit_username" = "Edit Username";
"lng_chatbots_manage_gifts" = "Manage Gifts and Stars";
"lng_chatbots_view_gifts" = "View Gifts";
"lng_chatbots_sell_gifts" = "Sell Gifts";
"lng_chatbots_gift_settings" = "Change Gift Settings";
"lng_chatbots_transfer_gifts" = "Transfer and Upgrade Gifts";
"lng_chatbots_transfer_stars" = "Transfer Stars";
"lng_chatbots_manage_stories" = "Manage Stories";
"lng_chatbots_remove" = "Remove Bot";
"lng_chatbots_not_found" = "Chatbot not found.";
"lng_chatbots_not_supported" = "This bot doesn't support Telegram Business yet.";
@ -3342,6 +3399,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_gift_stars_limited" = "limited";
"lng_gift_stars_sold_out" = "sold out";
"lng_gift_stars_tabs_all" = "All Gifts";
"lng_gift_stars_tabs_my" = "My Gifts";
"lng_gift_stars_tabs_limited" = "Limited";
"lng_gift_stars_tabs_in_stock" = "In Stock";
"lng_gift_send_title" = "Send a Gift";
@ -3371,7 +3429,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_gift_limited_of_one" = "unique";
"lng_gift_limited_of_count" = "1 of {amount}";
"lng_gift_collectible_tag" = "gift";
"lng_gift_price_unique" = "Unique";
"lng_gift_view_unpack" = "Unpack";
"lng_gift_anonymous_hint" = "Only you can see the sender's name.";
"lng_gift_anonymous_hint_channel" = "Only admins of this channel can see the sender's name.";
@ -3428,7 +3485,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_gift_display_done_channel" = "The gift is now shown in channel's Gifts.";
"lng_gift_display_done_hide" = "The gift is now hidden from your profile page.";
"lng_gift_display_done_hide_channel" = "The gift is now hidden from channel's Gifts.";
"lng_gift_pinned_done_title" = "{gift} pinned";
"lng_gift_pinned_done" = "The gift will always be shown on top.";
"lng_gift_pinned_done_replaced" = "replacing {gift}";
"lng_gift_got_stars#one" = "You got **{count} Star** for this gift.";
"lng_gift_got_stars#other" = "You got **{count} Stars** for this gift.";
"lng_gift_channel_got#one" = "Channel got **{count} Star** for this gift.";
@ -3501,6 +3560,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_gift_wear_subscribe" = "Subscribe to {link} to wear collectibles.";
"lng_gift_wear_start_toast" = "You put on {name}";
"lng_gift_wear_end_toast" = "You took off {name}";
"lng_gift_many_pinned_title" = "Too Many Pinned Gifts";
"lng_gift_many_pinned_choose" = "Select a gift to unpin below";
"lng_accounts_limit_title" = "Limit Reached";
"lng_accounts_limit1#one" = "You have reached the limit of **{count}** connected account.";
@ -4331,6 +4392,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_search_filter_private" = "Private chats";
"lng_search_filter_group" = "Group chats";
"lng_search_filter_channel" = "Channels";
"lng_search_sponsored_button" = "Ad ⋮";
"lng_media_save_progress" = "{ready} of {total} {mb}";
"lng_mediaview_save_as" = "Save As...";
@ -5745,6 +5807,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_sponsored_revenued_info1_title" = "Respect Your Privacy";
"lng_sponsored_revenued_info1_description" = "Ads on Telegram do not use your personal information and are based on the channel in which you see them.";
"lng_sponsored_revenued_info1_bot_description" = "Ads on Telegram do not use your personal information and are based on the mini app in which you see them.";
"lng_sponsored_revenued_info1_search_description" = "Ads on Telegram do not use your personal information and are based on the search query you entered.";
"lng_sponsored_revenued_info2_title" = "Help the Channel Creator";
"lng_sponsored_revenued_info2_bot_title" = "Help the Bot Developer";
"lng_sponsored_revenued_info2_description" = "50% of the revenue from Telegram Ads goes to the owner of the channel where they are displayed.";
@ -5753,9 +5816,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_sponsored_revenued_info3_description#one" = "You can turn off ads by subscribing to {link}, and Level {count} channels can remove them for their subscribers.";
"lng_sponsored_revenued_info3_description#other" = "You can turn off ads by subscribing to {link}, and Level {count} channels can remove them for their subscribers.";
"lng_sponsored_revenued_info3_bot_description" = "You can turn off ads in mini apps by subscribing to {link}.";
"lng_sponsored_revenued_info3_search_description" = "You can turn off ads by subscribing to Telegram Premium. {link}";
"lng_sponsored_revenued_info3_search_link" = "Subscribe {arrow}";
"lng_sponsored_revenued_footer_title" = "Can I Launch an Ad?";
"lng_sponsored_revenued_footer_description" = "Anyone can create an ad to display in this channel — with minimal budgets. Check out the **Telegram Ad Platform** for details. {link}";
"lng_sponsored_revenued_footer_bot_description" = "Anyone can create an ad to display in this bot — with minimal budgets. Check out the **Telegram Ad Platform** for details. {link}";
"lng_sponsored_revenued_footer_search_description" = "Anyone can create an ad to display in search results for any query. Check out the **Telegram Ad Platform** for details. {link}";
"lng_sponsored_top_bar_hide" = "remove";
"lng_telegram_features_url" = "https://t.me/TelegramTips";
@ -6241,6 +6307,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_qr_box_transparent_background" = "Transparent Background";
"lng_qr_box_font_size" = "Font size";
"lng_frozen_bar_title" = "Your account is frozen!";
"lng_frozen_bar_text" = "Click to view details {arrow}";
"lng_frozen_restrict_title" = "Your account is frozen";
"lng_frozen_restrict_text" = "Click to view details";
"lng_frozen_title" = "Your Account is Frozen";
"lng_frozen_subtitle1" = "Violation of Terms";
"lng_frozen_text1" = "Your account was frozen for breaking Telegram's Terms and Conditions.";
"lng_frozen_subtitle2" = "Read-Only Mode";
"lng_frozen_text2" = "You can access your account but can't send messages or take actions.";
"lng_frozen_subtitle3" = "Appeal Before Deactivation";
"lng_frozen_text3" = "Appeal via {link} before {date}, or your account will be deleted.";
"lng_frozen_appeal_button" = "Submit an Appeal";
// Wnd specific
"lng_wnd_choose_program_menu" = "Choose Default Program...";

View file

@ -30,6 +30,7 @@
<file alias="noresults.tgs">../../animations/noresults.tgs</file>
<file alias="hello_status.tgs">../../animations/hello_status.tgs</file>
<file alias="starref_link.tgs">../../animations/starref_link.tgs</file>
<file alias="media_forbidden.tgs">../../animations/media_forbidden.tgs</file>
<file alias="dice_idle.tgs">../../animations/dice/dice_idle.tgs</file>
<file alias="dart_idle.tgs">../../animations/dice/dart_idle.tgs</file>
@ -49,5 +50,16 @@
<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>
<file alias="swipe_archive.tgs">../../animations/swipe_action/archive.tgs</file>
<file alias="swipe_unarchive.tgs">../../animations/swipe_action/unarchive.tgs</file>
<file alias="swipe_delete.tgs">../../animations/swipe_action/delete.tgs</file>
<file alias="swipe_disabled.tgs">../../animations/swipe_action/disabled.tgs</file>
<file alias="swipe_mute.tgs">../../animations/swipe_action/mute.tgs</file>
<file alias="swipe_unmute.tgs">../../animations/swipe_action/unmute.tgs</file>
<file alias="swipe_pin.tgs">../../animations/swipe_action/pin.tgs</file>
<file alias="swipe_unpin.tgs">../../animations/swipe_action/unpin.tgs</file>
<file alias="swipe_read.tgs">../../animations/swipe_action/read.tgs</file>
<file alias="swipe_unread.tgs">../../animations/swipe_action/unread.tgs</file>
</qresource>
</RCC>

View file

@ -24,6 +24,8 @@
<file alias="icons/settings/star.svg">../../icons/settings/star.svg</file>
<file alias="icons/settings/starmini.svg">../../icons/settings/starmini.svg</file>
<file alias="icons/tray/monochrome.svg">../../icons/tray_monochrome.svg</file>
<file alias="icons/tray/monochrome_attention.svg">../../icons/tray_monochrome_attention.svg</file>
<file alias="icons/tray/monochrome_mute.svg">../../icons/tray_monochrome_mute.svg</file>
<file alias="topic_icons/blue.svg">../../art/topic_icons/blue.svg</file>
<file alias="topic_icons/yellow.svg">../../art/topic_icons/yellow.svg</file>
<file alias="topic_icons/violet.svg">../../art/topic_icons/violet.svg</file>

View file

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

View file

@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico"
//
VS_VERSION_INFO VERSIONINFO
FILEVERSION 5,12,3,0
PRODUCTVERSION 5,12,3,0
FILEVERSION 5,13,1,0
PRODUCTVERSION 5,13,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.12.3.0"
VALUE "FileVersion", "5.13.1.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2025"
VALUE "ProductName", "AyuGram Desktop"
VALUE "ProductVersion", "5.12.3.0"
VALUE "ProductVersion", "5.13.1.0"
END
END
BLOCK "VarFileInfo"

View file

@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
//
VS_VERSION_INFO VERSIONINFO
FILEVERSION 5,12,3,0
PRODUCTVERSION 5,12,3,0
FILEVERSION 5,13,1,0
PRODUCTVERSION 5,13,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.12.3.0"
VALUE "FileVersion", "5.13.1.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2025"
VALUE "ProductName", "AyuGram Desktop"
VALUE "ProductVersion", "5.12.3.0"
VALUE "ProductVersion", "5.13.1.0"
END
END
BLOCK "VarFileInfo"

View file

@ -150,6 +150,9 @@ void ConfirmPhone::resolve(
}, [](const MTPDauth_sentCodeSuccess &) {
LOG(("API Error: Unexpected auth.sentCodeSuccess "
"(Api::ConfirmPhone)."));
}, [](const MTPDauth_sentCodePaymentRequired &) {
LOG(("API Error: Unexpected auth.sentCodePaymentRequired "
"(Api::ConfirmPhone)."));
});
}).fail([=](const MTP::Error &error) {
_sendRequestId = 0;

View file

@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_global_privacy.h"
#include "apiwrap.h"
#include "data/data_user.h"
#include "main/main_session.h"
#include "main/main_app_config.h"
@ -115,7 +116,8 @@ void GlobalPrivacy::updateHideReadTime(bool hide) {
unarchiveOnNewMessageCurrent(),
hide,
newRequirePremiumCurrent(),
newChargeStarsCurrent());
newChargeStarsCurrent(),
disallowedGiftTypesCurrent());
}
bool GlobalPrivacy::hideReadTimeCurrent() const {
@ -150,7 +152,27 @@ void GlobalPrivacy::updateMessagesPrivacy(
unarchiveOnNewMessageCurrent(),
hideReadTimeCurrent(),
requirePremium,
chargeStars);
chargeStars,
disallowedGiftTypesCurrent());
}
DisallowedGiftTypes GlobalPrivacy::disallowedGiftTypesCurrent() const {
return _disallowedGiftTypes.current();
}
auto GlobalPrivacy::disallowedGiftTypes() const
-> rpl::producer<DisallowedGiftTypes> {
return _disallowedGiftTypes.value();
}
void GlobalPrivacy::updateDisallowedGiftTypes(DisallowedGiftTypes types) {
update(
archiveAndMuteCurrent(),
unarchiveOnNewMessageCurrent(),
hideReadTimeCurrent(),
newRequirePremiumCurrent(),
newChargeStarsCurrent(),
types);
}
void GlobalPrivacy::loadPaidReactionShownPeer() {
@ -182,7 +204,8 @@ void GlobalPrivacy::updateArchiveAndMute(bool value) {
unarchiveOnNewMessageCurrent(),
hideReadTimeCurrent(),
newRequirePremiumCurrent(),
newChargeStarsCurrent());
newChargeStarsCurrent(),
disallowedGiftTypesCurrent());
}
void GlobalPrivacy::updateUnarchiveOnNewMessage(
@ -192,7 +215,8 @@ void GlobalPrivacy::updateUnarchiveOnNewMessage(
value,
hideReadTimeCurrent(),
newRequirePremiumCurrent(),
newChargeStarsCurrent());
newChargeStarsCurrent(),
disallowedGiftTypesCurrent());
}
void GlobalPrivacy::update(
@ -200,12 +224,16 @@ void GlobalPrivacy::update(
UnarchiveOnNewMessage unarchiveOnNewMessage,
bool hideReadTime,
bool newRequirePremium,
int newChargeStars) {
int newChargeStars,
DisallowedGiftTypes disallowedGiftTypes) {
using Flag = MTPDglobalPrivacySettings::Flag;
using DisallowedFlag = MTPDdisallowedGiftsSettings::Flag;
_api.request(_requestId).cancel();
const auto newRequirePremiumAllowed = _session->premium()
|| _session->appConfig().newRequirePremiumFree();
const auto showGiftIcon
= (disallowedGiftTypes & DisallowedGiftType::SendHide);
const auto flags = Flag()
| (archiveAndMute
? Flag::f_archive_and_mute_new_noncontact_peers
@ -220,14 +248,35 @@ void GlobalPrivacy::update(
| ((newRequirePremium && newRequirePremiumAllowed)
? Flag::f_new_noncontact_peers_require_premium
: Flag())
| Flag::f_noncontact_peers_paid_stars;
| Flag::f_noncontact_peers_paid_stars
| (showGiftIcon ? Flag::f_display_gifts_button : Flag())
| Flag::f_disallowed_gifts;
const auto disallowedFlags = DisallowedFlag()
| ((disallowedGiftTypes & DisallowedGiftType::Premium)
? DisallowedFlag::f_disallow_premium_gifts
: DisallowedFlag())
| ((disallowedGiftTypes & DisallowedGiftType::Unlimited)
? DisallowedFlag::f_disallow_unlimited_stargifts
: DisallowedFlag())
| ((disallowedGiftTypes & DisallowedGiftType::Limited)
? DisallowedFlag::f_disallow_limited_stargifts
: DisallowedFlag())
| ((disallowedGiftTypes & DisallowedGiftType::Unique)
? DisallowedFlag::f_disallow_unique_stargifts
: DisallowedFlag());
const auto typesWas = _disallowedGiftTypes.current();
const auto typesChanged = (typesWas != disallowedGiftTypes);
_requestId = _api.request(MTPaccount_SetGlobalPrivacySettings(
MTP_globalPrivacySettings(
MTP_flags(flags),
MTP_long(newChargeStars))
MTP_long(newChargeStars),
MTP_disallowedGiftsSettings(MTP_flags(disallowedFlags)))
)).done([=](const MTPGlobalPrivacySettings &result) {
_requestId = 0;
apply(result);
if (typesChanged) {
_session->user()->updateFullForced();
}
}).fail([=](const MTP::Error &error) {
_requestId = 0;
if (error.type() == u"PREMIUM_ACCOUNT_REQUIRED"_q) {
@ -236,7 +285,8 @@ void GlobalPrivacy::update(
unarchiveOnNewMessage,
hideReadTime,
false,
0);
0,
DisallowedGiftTypes());
}
}).send();
_archiveAndMute = archiveAndMute;
@ -244,6 +294,7 @@ void GlobalPrivacy::update(
_hideReadTime = hideReadTime;
_newRequirePremium = newRequirePremium;
_newChargeStars = newChargeStars;
_disallowedGiftTypes = disallowedGiftTypes;
}
void GlobalPrivacy::apply(const MTPGlobalPrivacySettings &settings) {
@ -257,6 +308,29 @@ void GlobalPrivacy::apply(const MTPGlobalPrivacySettings &settings) {
_hideReadTime = data.is_hide_read_marks();
_newRequirePremium = data.is_new_noncontact_peers_require_premium();
_newChargeStars = data.vnoncontact_peers_paid_stars().value_or_empty();
if (const auto gifts = data.vdisallowed_gifts()) {
const auto &disallow = gifts->data();
_disallowedGiftTypes = DisallowedGiftType()
| (disallow.is_disallow_unlimited_stargifts()
? DisallowedGiftType::Unlimited
: DisallowedGiftType())
| (disallow.is_disallow_limited_stargifts()
? DisallowedGiftType::Limited
: DisallowedGiftType())
| (disallow.is_disallow_unique_stargifts()
? DisallowedGiftType::Unique
: DisallowedGiftType())
| (disallow.is_disallow_premium_gifts()
? DisallowedGiftType::Premium
: DisallowedGiftType())
| (data.is_display_gifts_button()
? DisallowedGiftType::SendHide
: DisallowedGiftType());
} else {
_disallowedGiftTypes = data.is_display_gifts_button()
? DisallowedGiftType::SendHide
: DisallowedGiftType();
}
}
} // namespace Api

View file

@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/flags.h"
#include "mtproto/sender.h"
class ApiWrap;
@ -23,6 +24,17 @@ enum class UnarchiveOnNewMessage {
AnyUnmuted,
};
enum class DisallowedGiftType : uchar {
Limited = 0x01,
Unlimited = 0x02,
Unique = 0x04,
Premium = 0x08,
SendHide = 0x10,
};
inline constexpr bool is_flag_type(DisallowedGiftType) { return true; }
using DisallowedGiftTypes = base::flags<DisallowedGiftType>;
[[nodiscard]] PeerId ParsePaidReactionShownPeer(
not_null<Main::Session*> session,
const MTPPaidReactionPrivacy &value);
@ -57,6 +69,11 @@ public:
void updateMessagesPrivacy(bool requirePremium, int chargeStars);
[[nodiscard]] DisallowedGiftTypes disallowedGiftTypesCurrent() const;
[[nodiscard]] auto disallowedGiftTypes() const
-> rpl::producer<DisallowedGiftTypes>;
void updateDisallowedGiftTypes(DisallowedGiftTypes types);
void loadPaidReactionShownPeer();
void updatePaidReactionShownPeer(PeerId shownPeer);
[[nodiscard]] PeerId paidReactionShownPeerCurrent() const;
@ -70,7 +87,8 @@ private:
UnarchiveOnNewMessage unarchiveOnNewMessage,
bool hideReadTime,
bool newRequirePremium,
int newChargeStars);
int newChargeStars,
DisallowedGiftTypes disallowedGiftTypes);
const not_null<Main::Session*> _session;
MTP::Sender _api;
@ -82,6 +100,7 @@ private:
rpl::variable<bool> _hideReadTime = false;
rpl::variable<bool> _newRequirePremium = false;
rpl::variable<int> _newChargeStars = 0;
rpl::variable<DisallowedGiftTypes> _disallowedGiftTypes;
rpl::variable<PeerId> _paidReactionShownPeer = false;
std::vector<Fn<void()>> _callbacks;
bool _paidReactionShownPeerLoaded = false;

View file

@ -0,0 +1,170 @@
/*
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 "api/api_peer_search.h"
#include "api/api_single_message_search.h"
#include "apiwrap.h"
#include "data/data_session.h"
#include "dialogs/ui/chat_search_in.h" // IsHashOrCashtagSearchQuery
#include "main/main_session.h"
namespace Api {
namespace {
constexpr auto kMinSponsoredQueryLength = 4;
} // namespace
PeerSearch::PeerSearch(not_null<Main::Session*> session, Type type)
: _session(session)
, _type(type) {
}
PeerSearch::~PeerSearch() {
clear();
}
void PeerSearch::request(
const QString &query,
Fn<void(PeerSearchResult)> callback,
RequestType type) {
using namespace Dialogs;
_query = Api::ConvertPeerSearchQuery(query);
_callback = callback;
if (_query.isEmpty()
|| IsHashOrCashtagSearchQuery(_query) != HashOrCashtag::None) {
finish(PeerSearchResult{});
return;
}
auto &cache = _cache[_query];
if (cache.peersReady && cache.sponsoredReady) {
finish(cache.result);
return;
} else if (type == RequestType::CacheOnly) {
_callback = nullptr;
return;
} else if (cache.requested) {
return;
}
cache.requested = true;
cache.result.query = _query;
if (_query.size() < kMinSponsoredQueryLength) {
cache.sponsoredReady = true;
} else if (_type == Type::WithSponsored) {
requestSponsored();
}
requestPeers();
}
void PeerSearch::requestPeers() {
const auto requestId = _session->api().request(MTPcontacts_Search(
MTP_string(_query),
MTP_int(SearchPeopleLimit)
)).done([=](const MTPcontacts_Found &result, mtpRequestId requestId) {
const auto &data = result.data();
_session->data().processUsers(data.vusers());
_session->data().processChats(data.vchats());
auto parsed = PeerSearchResult();
parsed.my.reserve(data.vmy_results().v.size());
for (const auto &id : data.vmy_results().v) {
const auto peerId = peerFromMTP(id);
parsed.my.push_back(_session->data().peer(peerId));
}
parsed.peers.reserve(data.vresults().v.size());
for (const auto &id : data.vresults().v) {
const auto peerId = peerFromMTP(id);
parsed.peers.push_back(_session->data().peer(peerId));
}
finishPeers(requestId, std::move(parsed));
}).fail([=](const MTP::Error &error, mtpRequestId requestId) {
finishPeers(requestId, PeerSearchResult{});
}).send();
_peerRequests.emplace(requestId, _query);
}
void PeerSearch::requestSponsored() {
const auto requestId = _session->api().request(
MTPcontacts_GetSponsoredPeers(MTP_string(_query))
).done([=](
const MTPcontacts_SponsoredPeers &result,
mtpRequestId requestId) {
result.match([&](const MTPDcontacts_sponsoredPeersEmpty &) {
finishSponsored(requestId, PeerSearchResult{});
}, [&](const MTPDcontacts_sponsoredPeers &data) {
_session->data().processUsers(data.vusers());
_session->data().processChats(data.vchats());
auto parsed = PeerSearchResult();
parsed.sponsored.reserve(data.vpeers().v.size());
for (const auto &peer : data.vpeers().v) {
const auto &data = peer.data();
const auto peerId = peerFromMTP(data.vpeer());
parsed.sponsored.push_back({
.peer = _session->data().peer(peerId),
.randomId = data.vrandom_id().v,
.sponsorInfo = TextWithEntities::Simple(
qs(data.vsponsor_info().value_or_empty())),
.additionalInfo = TextWithEntities::Simple(
qs(data.vadditional_info().value_or_empty())),
});
}
finishSponsored(requestId, std::move(parsed));
});
}).fail([=](const MTP::Error &error, mtpRequestId requestId) {
finishSponsored(requestId, PeerSearchResult{});
}).send();
_sponsoredRequests.emplace(requestId, _query);
}
void PeerSearch::finishPeers(
mtpRequestId requestId,
PeerSearchResult result) {
const auto query = _peerRequests.take(requestId);
Assert(query.has_value());
auto &cache = _cache[*query];
cache.peersReady = true;
cache.result.my = std::move(result.my);
cache.result.peers = std::move(result.peers);
if (cache.sponsoredReady && _query == *query) {
finish(cache.result);
}
}
void PeerSearch::finishSponsored(
mtpRequestId requestId,
PeerSearchResult result) {
const auto query = _sponsoredRequests.take(requestId);
Assert(query.has_value());
auto &cache = _cache[*query];
cache.sponsoredReady = true;
cache.result.sponsored = std::move(result.sponsored);
if (cache.peersReady && _query == *query) {
finish(cache.result);
}
}
void PeerSearch::finish(PeerSearchResult result) {
if (const auto onstack = base::take(_callback)) {
onstack(std::move(result));
}
}
void PeerSearch::clear() {
_query = QString();
_callback = nullptr;
_cache.clear();
for (const auto &[requestId, query] : base::take(_peerRequests)) {
_session->api().request(requestId).cancel();
}
for (const auto &[requestId, query] : base::take(_sponsoredRequests)) {
_session->api().request(requestId).cancel();
}
}
} // namespace Api

View file

@ -0,0 +1,76 @@
/*
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 Main {
class Session;
} // namespace Main
namespace Api {
struct SponsoredSearchResult {
not_null<PeerData*> peer;
QByteArray randomId;
TextWithEntities sponsorInfo;
TextWithEntities additionalInfo;
};
struct PeerSearchResult {
QString query;
std::vector<not_null<PeerData*>> my;
std::vector<not_null<PeerData*>> peers;
std::vector<SponsoredSearchResult> sponsored;
};
class PeerSearch final {
public:
enum class Type {
WithSponsored,
JustPeers,
};
PeerSearch(not_null<Main::Session*> session, Type type);
~PeerSearch();
enum class RequestType {
CacheOnly,
CacheOrRemote,
};
void request(
const QString &query,
Fn<void(PeerSearchResult)> callback,
RequestType type = RequestType::CacheOrRemote);
void clear();
private:
struct CacheEntry {
PeerSearchResult result;
bool requested = false;
bool peersReady = false;
bool sponsoredReady = false;
};
void requestPeers();
void requestSponsored();
void finish(PeerSearchResult result);
void finishPeers(mtpRequestId requestId, PeerSearchResult result);
void finishSponsored(mtpRequestId requestId, PeerSearchResult result);
const not_null<Main::Session*> _session;
const Type _type;
QString _query;
Fn<void(PeerSearchResult)> _callback;
base::flat_map<QString, CacheEntry> _cache;
base::flat_map<mtpRequestId, QString> _peerRequests;
base::flat_map<mtpRequestId, QString> _sponsoredRequests;
};
} // namespace Api

View file

@ -816,6 +816,7 @@ std::optional<Data::StarGift> FromTL(
.lastSaleDate = data.vlast_sale_date().value_or_empty(),
.upgradable = data.vupgrade_stars().has_value(),
.birthday = data.is_birthday(),
.soldOut = data.is_sold_out(),
});
}, [&](const MTPDstarGiftUnique &data) {
const auto total = data.vavailability_total().v;
@ -882,6 +883,7 @@ std::optional<Data::SavedStarGift> FromTL(
unique->exportAt = data.vcan_export_at().value_or_empty();
}
using Id = Data::SavedStarGiftId;
const auto hasUnique = parsed->unique != nullptr;
return Data::SavedStarGift{
.info = std::move(*parsed),
.manageId = (to->isUser()
@ -904,7 +906,7 @@ std::optional<Data::SavedStarGift> FromTL(
.date = data.vdate().v,
.upgradable = data.is_can_upgrade(),
.anonymous = data.is_name_hidden(),
.pinned = data.is_pinned_to_top(),
.pinned = data.is_pinned_to_top() && hasUnique,
.hidden = data.is_unsaved(),
.mine = to->isSelf(),
};

View file

@ -295,6 +295,7 @@ void FillChooseFilterMenu(
contains ? &st::mediaPlayerMenuCheck : nullptr);
item->setMarkedText(title.text, QString(), Core::TextContext({
.session = &history->session(),
.repaint = [raw = item.get()] { raw->update(); },
.customEmojiLoopLimit = title.isStatic ? -1 : 0,
}));

View file

@ -271,7 +271,8 @@ void DeleteMessagesBox::prepare() {
appendDetails({
tr::lng_delete_for_me_chat_hint(tr::now, lt_count, count)
});
} else if (!peer->isSelf()) {
} else if (!peer->isSelf()
&& (!peer->isUser() || !peer->asUser()->isInaccessible())) {
if (const auto user = peer->asUser(); user && user->isBot()) {
_revokeForBot = true;
}

View file

@ -1054,7 +1054,7 @@ void EditMessagesPrivacyBox(
tr::now,
lt_count,
always)
: QString();
: tr::lng_edit_privacy_exceptions_add(tr::now);
});
const auto exceptions = Settings::AddButtonWithLabel(

View file

@ -111,6 +111,9 @@ void AddBotToGroupBoxController::Start(
Scope scope,
const QString &token,
ChatAdminRights requestedRights) {
if (controller->showFrozenError()) {
return;
}
auto initBox = [=](not_null<PeerListBox*> box) {
box->addButton(tr::lng_cancel(), [box] { box->closeBox(); });
};

View file

@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/admin_log/history_admin_log_filter.h"
#include "core/ui_integration.h"
#include "data/stickers/data_custom_emoji.h"
#include "data/business/data_business_chatbots.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_session.h"
@ -63,6 +64,11 @@ constexpr auto kForceDisableTooltipDuration = 3 * crl::time(1000);
return std::vector<std::pair<Flag, Flag>>{};
}
[[nodiscard]] auto Dependencies(Data::ChatbotsPermissions) {
using Flag = Data::ChatbotsPermission;
return std::vector<std::pair<Flag, Flag>>{};
}
[[nodiscard]] auto NestedRestrictionLabelsList(
Data::RestrictionsSetOptions options)
-> std::vector<NestedEditFlagsLabels<ChatRestrictions>> {
@ -680,7 +686,7 @@ template <typename Flags>
checkView->setChecked(false, anim::type::instant);
} else if (locked.has_value()) {
if (checked != toggled) {
if (!state->toast) {
if (!state->toast && !locked->isEmpty()) {
state->toast = Ui::Toast::Show(container, {
.text = { *locked },
.duration = kForceDisableTooltipDuration,
@ -1490,3 +1496,20 @@ EditFlagsControl<AdminLog::FilterValue::Flags> CreateEditAdminLogFilter(
return result;
}
EditFlagsControl<Data::ChatbotsPermissions> CreateEditChatbotPermissions(
QWidget *parent,
Data::ChatbotsPermissions flags) {
auto widget = object_ptr<Ui::VerticalLayout>(parent);
auto descriptor = Data::ChatbotsPermissionsLabels();
descriptor.disabledMessages.emplace(
Data::ChatbotsPermission::ViewMessages,
QString());
auto result = CreateEditFlags(
widget.data(),
flags | Data::ChatbotsPermission::ViewMessages,
std::move(descriptor));
result.widget = std::move(widget);
return result;
}

View file

@ -27,6 +27,11 @@ enum Flag : uint32;
using Flags = base::flags<Flag>;
} // namespace PowerSaving
namespace Data {
enum class ChatbotsPermission;
using ChatbotsPermissions = base::flags<ChatbotsPermission>;
} // namespace Data
template <typename Object>
class object_ptr;
@ -120,3 +125,8 @@ using AdminRightLabel = EditFlagsLabel<ChatAdminRights>;
AdminLog::FilterValue::Flags flags,
bool isChannel
) -> EditFlagsControl<AdminLog::FilterValue::Flags>;
[[nodiscard]] auto CreateEditChatbotPermissions(
QWidget *parent,
Data::ChatbotsPermissions flags
) -> EditFlagsControl<Data::ChatbotsPermissions>;

View file

@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "apiwrap.h"
#include "api/api_credits.h"
#include "api/api_global_privacy.h"
#include "api/api_premium.h"
#include "base/event_filter.h"
#include "base/random.h"
@ -20,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/peer_list_controllers.h"
#include "boxes/premium_preview_box.h"
#include "boxes/send_credits_box.h"
#include "boxes/transfer_gift_box.h"
#include "chat_helpers/emoji_suggestions_widget.h"
#include "chat_helpers/message_field.h"
#include "chat_helpers/stickers_gift_box_pack.h"
@ -101,8 +103,10 @@ namespace Ui {
namespace {
constexpr auto kPriceTabAll = 0;
constexpr auto kPriceTabLimited = -2;
constexpr auto kPriceTabInStock = -1;
constexpr auto kPriceTabLimited = -2;
constexpr auto kPriceTabMy = -3;
constexpr auto kMyGiftsPerPage = 50;
constexpr auto kGiftMessageLimit = 255;
constexpr auto kSentToastDuration = 3 * crl::time(1000);
constexpr auto kSwitchUpgradeCoverInterval = 3 * crl::time(1000);
@ -118,6 +122,11 @@ struct PremiumGiftsDescriptor {
std::shared_ptr<Api::PremiumGiftCodeOptions> api;
};
struct MyGiftsDescriptor {
std::vector<Data::SavedStarGift> list;
QString offset;
};
struct GiftsDescriptor {
std::vector<GiftDescriptor> list;
std::shared_ptr<Api::PremiumGiftCodeOptions> api;
@ -360,6 +369,30 @@ auto GenerateGiftMedia(
return Images::Round(std::move(result), mask, RectPart::FullTop);
}
struct VisibleRange {
int top = 0;
int bottom = 0;
friend inline bool operator==(VisibleRange, VisibleRange) = default;
};
class WidgetWithRange final : public RpWidget {
public:
using RpWidget::RpWidget;
[[nodiscard]] rpl::producer<VisibleRange> visibleRange() const {
return _visibleRange.value();
}
private:
void visibleTopBottomUpdated(
int visibleTop,
int visibleBottom) override {
_visibleRange = VisibleRange{ visibleTop, visibleBottom };
}
rpl::variable<VisibleRange> _visibleRange;
};
void PrepareImage(
QImage &image,
not_null<Text::CustomEmoji*> emoji,
@ -673,7 +706,7 @@ void PreviewWrap::paintEvent(QPaintEvent *e) {
}
ranges::sort(list, ranges::less(), &GiftTypePremium::months);
auto &map = Map[session];
if (map.last.list != list) {
if (map.last.list != list || list.empty()) {
map.last = PremiumGiftsDescriptor{
std::move(list),
api,
@ -686,6 +719,24 @@ void PreviewWrap::paintEvent(QPaintEvent *e) {
};
}
[[nodiscard]] bool AllowedToSend(
const GiftTypeStars &gift,
not_null<PeerData*> peer) {
using Type = Api::DisallowedGiftType;
const auto user = peer->asUser();
if (!user || user->isSelf()) {
return true;
}
const auto disallowedTypes = user ? user->disallowedGiftTypes() : Type();
const auto allowLimited = !(disallowedTypes & Type::Limited);
const auto allowUnlimited = !(disallowedTypes & Type::Unlimited);
const auto allowUnique = !(disallowedTypes & Type::Unique);
if (!gift.info.limitedCount) {
return allowUnlimited;
}
return allowLimited || (gift.info.starsToUpgrade && allowUnique);
}
[[nodiscard]] rpl::producer<std::vector<GiftTypeStars>> GiftsStars(
not_null<Main::Session*> session,
not_null<PeerData*> peer) {
@ -694,6 +745,12 @@ void PreviewWrap::paintEvent(QPaintEvent *e) {
};
static auto Map = base::flat_map<not_null<Main::Session*>, Session>();
const auto filtered = [=](std::vector<GiftTypeStars> list) {
list.erase(ranges::remove_if(list, [&](const GiftTypeStars &gift) {
return !AllowedToSend(gift, peer);
}), end(list));
return list;
};
return [=](auto consumer) {
auto lifetime = rpl::lifetime();
@ -703,7 +760,7 @@ void PreviewWrap::paintEvent(QPaintEvent *e) {
session->lifetime().add([=] { Map.remove(session); });
}
if (!i->second.last.empty()) {
consumer.put_next_copy(i->second.last);
consumer.put_next(filtered(i->second.last));
}
using namespace Api;
@ -718,25 +775,14 @@ void PreviewWrap::paintEvent(QPaintEvent *e) {
for (auto &gift : gifts) {
list.push_back({ .info = gift });
}
ranges::sort(list, [](
const GiftTypeStars &a,
const GiftTypeStars &b) {
if (!a.info.limitedCount && !b.info.limitedCount) {
return a.info.stars <= b.info.stars;
} else if (!a.info.limitedCount) {
return true;
} else if (!b.info.limitedCount) {
return false;
} else if (a.info.limitedLeft != b.info.limitedLeft) {
return a.info.limitedLeft > b.info.limitedLeft;
}
return a.info.stars <= b.info.stars;
ranges::stable_sort(list, [](const auto &a, const auto &b) {
return a.info.soldOut < b.info.soldOut;
});
auto &map = Map[session];
if (map.last != list) {
if (map.last != list || list.empty()) {
map.last = list;
consumer.put_next_copy(list);
consumer.put_next(filtered(std::move(list)));
}
}, lifetime);
@ -744,6 +790,48 @@ void PreviewWrap::paintEvent(QPaintEvent *e) {
};
}
[[nodiscard]] rpl::producer<MyGiftsDescriptor> UniqueGiftsSlice(
not_null<Main::Session*> session,
QString offset = QString()) {
return [=](auto consumer) {
using Flag = MTPpayments_GetSavedStarGifts::Flag;
const auto user = session->user();
const auto requestId = session->api().request(
MTPpayments_GetSavedStarGifts(
MTP_flags(Flag::f_exclude_limited | Flag::f_exclude_unlimited),
user->input,
MTP_string(offset),
MTP_int(kMyGiftsPerPage)
)).done([=](const MTPpayments_SavedStarGifts &result) {
auto gifts = MyGiftsDescriptor();
const auto &data = result.data();
if (const auto next = data.vnext_offset()) {
gifts.offset = qs(*next);
}
const auto owner = &session->data();
owner->processUsers(data.vusers());
owner->processChats(data.vchats());
gifts.list.reserve(data.vgifts().v.size());
for (const auto &gift : data.vgifts().v) {
if (auto parsed = Api::FromTL(user, gift)) {
gifts.list.push_back(std::move(*parsed));
}
}
consumer.put_next(std::move(gifts));
consumer.put_done();
}).fail([=] {
consumer.put_next({});
consumer.put_done();
}).send();
auto lifetime = rpl::lifetime();
lifetime.add([=] { session->api().request(requestId).cancel(); });
return lifetime;
};
}
[[nodiscard]] Text::String TabTextForPrice(
not_null<Main::Session*> session,
int price) {
@ -752,6 +840,8 @@ void PreviewWrap::paintEvent(QPaintEvent *e) {
};
if (price == kPriceTabAll) {
return simple(tr::lng_gift_stars_tabs_all(tr::now));
} else if (price == kPriceTabMy) {
return simple(tr::lng_gift_stars_tabs_my(tr::now));
} else if (price == kPriceTabLimited) {
return simple(tr::lng_gift_stars_tabs_limited(tr::now));
} else if (price == kPriceTabInStock) {
@ -774,7 +864,8 @@ struct GiftPriceTabs {
[[nodiscard]] GiftPriceTabs MakeGiftsPriceTabs(
not_null<Window::SessionController*> window,
not_null<PeerData*> peer,
rpl::producer<std::vector<GiftTypeStars>> gifts) {
rpl::producer<std::vector<GiftTypeStars>> gifts,
bool hasMyUnique) {
auto widget = object_ptr<RpWidget>((QWidget*)nullptr);
const auto raw = widget.data();
@ -798,6 +889,13 @@ struct GiftPriceTabs {
int pressed = -1;
int active = -1;
};
const auto user = peer->asUser();
const auto disallowed = user
? user->disallowedGiftTypes()
: Api::DisallowedGiftType();
if (disallowed & Api::DisallowedGiftType::Unique) {
hasMyUnique = false;
}
const auto state = raw->lifetime().make_state<State>();
const auto scroll = [=] {
return QPoint(int(base::SafeRound(state->scroll)), 0);
@ -805,25 +903,14 @@ struct GiftPriceTabs {
state->prices = std::move(
gifts
) | rpl::map([](const std::vector<GiftTypeStars> &gifts) {
) | rpl::map([=](const std::vector<GiftTypeStars> &gifts) {
auto result = std::vector<int>();
result.push_back(kPriceTabAll);
auto special = 1;
auto same = true;
auto sameKey = 0;
auto hasNonSoldOut = false;
auto hasSoldOut = false;
auto hasLimited = false;
auto hasNonLimited = false;
for (const auto &gift : gifts) {
if (same) {
const auto key = gift.info.stars
* (gift.info.limitedCount ? -1 : 1);
if (!sameKey) {
sameKey = key;
} else if (sameKey != key) {
same = false;
}
}
if (IsSoldOut(gift.info)) {
hasSoldOut = true;
} else {
@ -831,19 +918,21 @@ struct GiftPriceTabs {
}
if (gift.info.limitedCount) {
hasLimited = true;
} else {
hasNonLimited = true;
}
if (!ranges::contains(result, gift.info.stars)) {
result.push_back(gift.info.stars);
}
}
if (same) {
return std::vector<int>();
if (hasMyUnique && !gifts.empty()) {
result.push_back(kPriceTabMy);
}
if (hasSoldOut && hasNonSoldOut) {
result.insert(begin(result) + (special++), kPriceTabInStock);
result.push_back(kPriceTabInStock);
}
if (hasLimited) {
result.insert(begin(result) + (special++), kPriceTabLimited);
if (hasLimited && hasNonLimited) {
result.push_back(kPriceTabLimited);
}
ranges::sort(begin(result) + 1, end(result));
return result;
@ -1446,6 +1535,13 @@ void SendGiftBox(
const auto limited = stars
&& (stars->info.limitedCount > stars->info.limitedLeft)
&& (stars->info.limitedLeft > 0);
const auto costToUpgrade = stars ? stars->info.starsToUpgrade : 0;
const auto user = peer->asUser();
const auto disallowed = user
? user->disallowedGiftTypes()
: Api::DisallowedGiftTypes();
const auto disallowLimited = !peer->isSelf()
&& (disallowed & Api::DisallowedGiftType::Limited);
box->setStyle(limited ? st::giftLimitedBox : st::giftBox);
box->setWidth(st::boxWideWidth);
box->setTitle(tr::lng_gift_send_title());
@ -1465,6 +1561,7 @@ void SendGiftBox(
state->details = GiftDetails{
.descriptor = descriptor,
.randomId = base::RandomValue<uint64>(),
.upgraded = disallowLimited && (costToUpgrade > 0),
};
peer->updateFull();
state->messageAllowed = peer->session().changes().peerFlagsValue(
@ -1563,13 +1660,13 @@ void SendGiftBox(
session,
{ .suggestCustomEmoji = true, .allowCustomWithoutPremium = allow });
if (stars) {
const auto cost = stars->info.starsToUpgrade;
if (cost > 0 && !peer->isSelf()) {
if (costToUpgrade > 0 && !peer->isSelf() && !disallowLimited) {
const auto id = stars->info.id;
const auto showing = std::make_shared<bool>();
AddDivider(container);
AddSkip(container);
AddUpgradeButton(container, session, cost, peer, [=](bool on) {
AddUpgradeButton(container, session, costToUpgrade, peer, [=](
bool on) {
auto now = state->details.current();
now.upgraded = on;
state->details = std::move(now);
@ -1583,7 +1680,7 @@ void SendGiftBox(
.stargiftId = id,
.ready = [=](bool) { *showing = false; },
.peer = peer,
.cost = int(cost),
.cost = int(costToUpgrade),
});
});
} else {
@ -1726,17 +1823,24 @@ void SendGiftBox(
[[nodiscard]] object_ptr<RpWidget> MakeGiftsList(
not_null<Window::SessionController*> window,
not_null<PeerData*> peer,
rpl::producer<GiftsDescriptor> gifts) {
auto result = object_ptr<RpWidget>((QWidget*)nullptr);
rpl::producer<GiftsDescriptor> gifts,
Fn<void()> loadMore) {
auto result = object_ptr<WidgetWithRange>((QWidget*)nullptr);
const auto raw = result.data();
struct State {
Delegate delegate;
std::vector<int> order;
std::vector<bool> validated;
std::vector<GiftDescriptor> list;
std::vector<std::unique_ptr<GiftButton>> buttons;
std::shared_ptr<Api::PremiumGiftCodeOptions> api;
rpl::variable<VisibleRange> visibleRange;
bool sending = false;
int perRow = 1;
};
const auto state = raw->lifetime().make_state<State>(State{
.delegate = Delegate(window, GiftButtonMode::Full),
.delegate = Delegate(&window->session(), GiftButtonMode::Full),
});
const auto single = state->delegate.buttonSize();
const auto shadow = st::defaultDropdownMenu.wrap.shadow;
@ -1745,74 +1849,165 @@ void SendGiftBox(
auto &packs = window->session().giftBoxStickersPacks();
packs.updated() | rpl::start_with_next([=] {
for (const auto &button : state->buttons) {
button->update();
if (const auto raw = button.get()) {
raw->update();
}
}
}, raw->lifetime());
const auto rebuild = [=] {
const auto width = st::boxWideWidth;
const auto padding = st::giftBoxPadding;
const auto available = width - padding.left() - padding.right();
const auto range = state->visibleRange.current();
const auto count = int(state->list.size());
auto &buttons = state->buttons;
if (buttons.size() < count) {
buttons.resize(count);
}
auto &validated = state->validated;
validated.resize(count);
auto x = padding.left();
auto y = padding.top();
const auto perRow = state->perRow;
const auto singlew = single.width() + st::giftBoxGiftSkip.x();
const auto singleh = single.height() + st::giftBoxGiftSkip.y();
const auto rowFrom = std::max(range.top - y, 0) / singleh;
const auto rowTill = (std::max(range.bottom - y + st::giftBoxGiftSkip.y(), 0) + singleh - 1)
/ singleh;
Assert(rowTill >= rowFrom);
const auto first = rowFrom * perRow;
const auto last = std::min(rowTill * perRow, count);
auto checkedFrom = 0;
auto checkedTill = int(buttons.size());
const auto ensureButton = [&](int index) {
auto &button = buttons[index];
if (!button) {
validated[index] = false;
for (; checkedFrom != first; ++checkedFrom) {
if (buttons[checkedFrom]) {
button = std::move(buttons[checkedFrom]);
break;
}
}
}
if (!button) {
for (; checkedTill != last; ) {
--checkedTill;
if (buttons[checkedTill]) {
button = std::move(buttons[checkedTill]);
break;
}
}
}
if (!button) {
button = std::make_unique<GiftButton>(
raw,
&state->delegate);
}
if (validated[index]) {
return;
}
button->show();
validated[index] = true;
const auto &descriptor = state->list[state->order[index]];
button->setDescriptor(descriptor, GiftButton::Mode::Full);
button->setClickedCallback([=] {
const auto star = std::get_if<GiftTypeStars>(&descriptor);
if (star && star->info.unique) {
const auto done = [=] {
window->session().credits().load(true);
window->showPeerHistory(peer);
};
ShowTransferToBox(
window,
peer,
star->info.unique,
star->transferId,
done);
} else if (star && IsSoldOut(star->info)) {
window->show(Box(SoldOutBox, window, *star));
} else {
window->show(Box(
SendGiftBox,
window,
peer,
state->api,
descriptor));
}
});
button->setGeometry(QRect(QPoint(x, y), single), extend);
};
y += rowFrom * singleh;
for (auto row = rowFrom; row != rowTill; ++row) {
for (auto col = 0; col != perRow; ++col) {
const auto index = row * perRow + col;
if (index >= count) {
break;
}
const auto last = !((col + 1) % perRow);
if (last) {
x = padding.left() + available - single.width();
}
ensureButton(index);
if (last) {
x = padding.left();
y += singleh;
} else {
x += singlew;
}
}
}
const auto till = std::min(int(buttons.size()), rowTill * perRow);
for (auto i = count; i < till; ++i) {
if (const auto button = buttons[i].get()) {
button->hide();
}
}
const auto page = range.bottom - range.top;
if (loadMore && page > 0 && range.bottom + page > raw->height()) {
loadMore();
}
};
state->visibleRange = raw->visibleRange();
state->visibleRange.value(
) | rpl::start_with_next(rebuild, raw->lifetime());
std::move(
gifts
) | rpl::start_with_next([=](const GiftsDescriptor &gifts) {
const auto width = st::boxWideWidth;
const auto padding = st::giftBoxPadding;
const auto available = width - padding.left() - padding.right();
const auto perRow = available / single.width();
const auto count = int(gifts.list.size());
state->perRow = available / single.width();
state->list = std::move(gifts.list);
state->api = gifts.api;
auto order = ranges::views::ints
const auto count = int(state->list.size());
state->order = ranges::views::ints
| ranges::views::take(count)
| ranges::to_vector;
state->validated.clear();
if (SortForBirthday(peer)) {
ranges::stable_partition(order, [&](int i) {
const auto &gift = gifts.list[i];
ranges::stable_partition(state->order, [&](int i) {
const auto &gift = state->list[i];
const auto stars = std::get_if<GiftTypeStars>(&gift);
return stars && stars->info.birthday;
return stars && stars->info.birthday && !stars->info.unique;
});
}
auto x = padding.left();
auto y = padding.top();
state->buttons.resize(count);
for (auto &button : state->buttons) {
if (!button) {
button = std::make_unique<GiftButton>(raw, &state->delegate);
button->show();
}
}
const auto api = gifts.api;
for (auto i = 0; i != count; ++i) {
const auto button = state->buttons[i].get();
const auto &descriptor = gifts.list[order[i]];
button->setDescriptor(descriptor, GiftButton::Mode::Full);
const auto last = !((i + 1) % perRow);
if (last) {
x = padding.left() + available - single.width();
}
button->setGeometry(QRect(QPoint(x, y), single), extend);
if (last) {
x = padding.left();
y += single.height() + st::giftBoxGiftSkip.y();
} else {
x += single.width() + st::giftBoxGiftSkip.x();
}
button->setClickedCallback([=] {
const auto star = std::get_if<GiftTypeStars>(&descriptor);
if (star && IsSoldOut(star->info)) {
window->show(Box(SoldOutBox, window, *star));
} else {
window->show(
Box(SendGiftBox, window, peer, api, descriptor));
}
});
}
if (count % perRow) {
y += padding.bottom() + single.height();
} else {
y += padding.bottom() - st::giftBoxGiftSkip.y();
}
raw->resize(raw->width(), count ? y : 0);
const auto rows = (count + state->perRow - 1) / state->perRow;
const auto height = padding.top()
+ (rows * single.height())
+ ((rows - 1) * st::giftBoxGiftSkip.y())
+ padding.bottom();
raw->resize(raw->width(), height);
rebuild();
}, raw->lifetime());
return result;
@ -1876,42 +2071,83 @@ void AddBlock(
gifts.list | ranges::to<std::vector<GiftDescriptor>>,
gifts.api,
};
}));
}), nullptr);
result->lifetime().add([state = std::move(state)] {});
return result;
}
[[nodiscard]] object_ptr<RpWidget> MakeStarsGifts(
not_null<Window::SessionController*> window,
not_null<PeerData*> peer) {
not_null<PeerData*> peer,
MyGiftsDescriptor my) {
auto result = object_ptr<VerticalLayout>((QWidget*)nullptr);
struct State {
rpl::variable<std::vector<GiftTypeStars>> gifts;
rpl::variable<int> priceTab = kPriceTabAll;
rpl::event_stream<> myUpdated;
MyGiftsDescriptor my;
rpl::lifetime myLoading;
};
const auto state = result->lifetime().make_state<State>();
state->my = std::move(my);
state->gifts = GiftsStars(&window->session(), peer);
auto tabs = MakeGiftsPriceTabs(window, peer, state->gifts.value());
auto tabs = MakeGiftsPriceTabs(
window,
peer,
state->gifts.value(),
!state->my.list.empty());
state->priceTab = std::move(tabs.priceTab);
result->add(std::move(tabs.widget));
result->add(MakeGiftsList(window, peer, rpl::combine(
state->gifts.value(),
state->priceTab.value()
) | rpl::map([=](std::vector<GiftTypeStars> &&gifts, int price) {
gifts.erase(ranges::remove_if(gifts, [&](const GiftTypeStars &gift) {
state->priceTab.value(),
rpl::single(rpl::empty) | rpl::then(state->myUpdated.events())
) | rpl::map([=](std::vector<GiftTypeStars> &&gifts, int price, auto) {
if (price == kPriceTabMy) {
gifts.clear();
for (const auto &gift : state->my.list) {
gifts.push_back({
.transferId = gift.manageId,
.info = gift.info,
.mine = true,
});
}
} else {
const auto pred = [&](const GiftTypeStars &gift) {
return (price == kPriceTabLimited)
? (!gift.info.limitedCount)
: (price == kPriceTabInStock)
? IsSoldOut(gift.info)
: (price && gift.info.stars != price);
}), end(gifts));
};
gifts.erase(ranges::remove_if(gifts, pred), end(gifts));
}
return GiftsDescriptor{
gifts | ranges::to<std::vector<GiftDescriptor>>(),
};
})));
}), [=] {
if (state->priceTab.current() == kPriceTabMy
&& !state->my.offset.isEmpty()
&& !state->myLoading) {
state->myLoading = UniqueGiftsSlice(
&peer->session(),
state->my.offset
) | rpl::start_with_next([=](MyGiftsDescriptor &&descriptor) {
state->myLoading.destroy();
state->my.offset = descriptor.list.empty()
? QString()
: descriptor.offset;
state->my.list.insert(
end(state->my.list),
std::make_move_iterator(begin(descriptor.list)),
std::make_move_iterator(end(descriptor.list)));
state->myUpdated.fire({});
});
}
}));
return result;
}
@ -1919,7 +2155,8 @@ void AddBlock(
void GiftBox(
not_null<GenericBox*> box,
not_null<Window::SessionController*> window,
not_null<PeerData*> peer) {
not_null<PeerData*> peer,
MyGiftsDescriptor my) {
box->setWidth(st::boxWideWidth);
box->setStyle(st::creditsGiftBox);
box->setNoContentMargin(true);
@ -1935,6 +2172,24 @@ void GiftBox(
AddSkip(content, st::defaultVerticalListSkip * 5);
// Check disallowed gift types
const auto user = peer->asUser();
using Type = Api::DisallowedGiftType;
const auto disallowedTypes = user
? user->disallowedGiftTypes()
: Type::Premium;
const auto premiumDisallowed = peer->isSelf()
|| (disallowedTypes & Type::Premium);
const auto limitedDisallowed = !peer->isSelf()
&& (disallowedTypes & Type::Limited);
const auto unlimitedDisallowed = !peer->isSelf()
&& (disallowedTypes & Type::Unlimited);
const auto uniqueDisallowed = !peer->isSelf()
&& (disallowedTypes & Type::Unique);
const auto allStarsDisallowed = limitedDisallowed
&& unlimitedDisallowed
&& uniqueDisallowed;
content->add(
object_ptr<CenterWrap<>>(
content,
@ -1956,11 +2211,12 @@ void GiftBox(
window->showSettings(Settings::CreditsId());
return false;
};
if (peer->isUser() && !peer->isSelf()) {
if (peer->isUser() && !peer->isSelf() && !premiumDisallowed) {
const auto premiumClickHandlerFilter = [=](const auto &...) {
Settings::ShowPremium(window, u"gift_send"_q);
return false;
};
AddBlock(content, window, {
.subtitle = tr::lng_gift_premium_subtitle(),
.about = tr::lng_gift_premium_about(
@ -1973,6 +2229,9 @@ void GiftBox(
.content = MakePremiumGifts(window, peer),
});
}
// Only add star gifts if at least one type is allowed
if (!allStarsDisallowed) {
AddBlock(content, window, {
.subtitle = (peer->isSelf()
? tr::lng_gift_self_title()
@ -1993,8 +2252,9 @@ void GiftBox(
tr::lng_gift_stars_link() | Text::ToLink(),
Text::WithEntities)),
.aboutFilter = starsClickHandlerFilter,
.content = MakeStarsGifts(window, peer),
.content = MakeStarsGifts(window, peer, std::move(my)),
});
}
}
struct SelfOption {
@ -2184,11 +2444,31 @@ void ChooseStarGiftRecipient(
void ShowStarGiftBox(
not_null<Window::SessionController*> controller,
not_null<PeerData*> peer) {
if (controller->showFrozenError()) {
return;
}
struct Session {
PeerData *peer = nullptr;
MyGiftsDescriptor my;
bool premiumGiftsReady = false;
bool starsGiftsReady = false;
bool fullReady = false;
bool myReady = false;
bool hasPremium = false;
bool hasUpgradable = false;
bool hasLimited = false;
bool hasUnlimited = false;
rpl::lifetime lifetime;
[[nodiscard]] bool ready() const {
return premiumGiftsReady
&& starsGiftsReady
&& fullReady
&& myReady;
}
};
static auto Map = base::flat_map<not_null<Main::Session*>, Session>();
@ -2203,46 +2483,91 @@ void ShowStarGiftBox(
i->second = Session{ .peer = peer };
const auto weak = base::make_weak(controller);
const auto show = [=] {
Map[session] = Session();
const auto checkReady = [=] {
auto &entry = Map[session];
if (!entry.ready()) {
return;
}
auto was = std::move(entry);
entry = Session();
if (const auto strong = weak.get()) {
strong->show(Box(GiftBox, strong, peer));
if (const auto user = peer->asUser()) {
using Type = Api::DisallowedGiftType;
const auto disallowedTypes = user->disallowedGiftTypes();
const auto premium = (disallowedTypes & Type::Premium)
|| peer->isSelf();
const auto limited = (disallowedTypes & Type::Limited);
const auto unlimited = (disallowedTypes & Type::Unlimited);
const auto unique = (disallowedTypes & Type::Unique);
if ((unique || (!was.hasUpgradable && was.my.list.empty()))
&& (premium || !was.hasPremium)
&& (limited || !was.hasLimited)
&& (unlimited || !was.hasUnlimited)) {
strong->showToast(
tr::lng_edit_privacy_gifts_restricted(tr::now));
return;
}
}
strong->show(Box(GiftBox, strong, peer, std::move(was.my)));
}
};
base::timer_once(
kGiftsPreloadTimeout
) | rpl::start_with_next(show, i->second.lifetime);
const auto user = peer->asUser();
if (user && !user->isSelf()) {
GiftsPremium(
session,
peer
) | rpl::start_with_next([=](PremiumGiftsDescriptor &&gifts) {
if (!gifts.list.empty()) {
auto &entry = Map[session];
entry.premiumGiftsReady = true;
if (entry.starsGiftsReady) {
show();
}
}
entry.hasPremium = !gifts.list.empty();
checkReady();
}, i->second.lifetime);
} else {
i->second.hasPremium = false;
i->second.premiumGiftsReady = true;
}
if (peer->isFullLoaded()) {
i->second.fullReady = true;
} else {
peer->updateFull();
peer->session().changes().peerUpdates(
peer,
Data::PeerUpdate::Flag::FullInfo
) | rpl::take(1) | rpl::start_with_next([=] {
auto &entry = Map[session];
entry.fullReady = true;
checkReady();
}, i->second.lifetime);
}
GiftsStars(
session,
peer
) | rpl::start_with_next([=](std::vector<GiftTypeStars> &&gifts) {
if (!gifts.empty()) {
auto &entry = Map[session];
entry.starsGiftsReady = true;
if (entry.premiumGiftsReady) {
show();
for (const auto &gift : gifts) {
if (gift.info.limitedCount) {
entry.hasLimited = true;
if (gift.info.starsToUpgrade) {
entry.hasUpgradable = true;
}
} else {
entry.hasUnlimited = true;
}
}
checkReady();
}, i->second.lifetime);
UniqueGiftsSlice(
session
) | rpl::start_with_next([=](MyGiftsDescriptor &&gifts) {
auto &entry = Map[session];
entry.my = std::move(gifts);
entry.myReady = true;
checkReady();
}, i->second.lifetime);
}

View file

@ -463,6 +463,8 @@ void TransferGift(
std::move(formDone));
}
} // namespace
void ShowTransferToBox(
not_null<Window::SessionController*> controller,
not_null<PeerData*> peer,
@ -539,8 +541,6 @@ void ShowTransferToBox(
}));
}
} // namespace
void ShowTransferGiftBox(
not_null<Window::SessionController*> window,
std::shared_ptr<Data::UniqueGift> gift,

View file

@ -16,6 +16,13 @@ struct UniqueGift;
class SavedStarGiftId;
} // namespace Data
void ShowTransferToBox(
not_null<Window::SessionController*> controller,
not_null<PeerData*> peer,
std::shared_ptr<Data::UniqueGift> gift,
Data::SavedStarGiftId savedId,
Fn<void()> closeParentBox);
void ShowTransferGiftBox(
not_null<Window::SessionController*> window,
std::shared_ptr<Data::UniqueGift> gift,

View file

@ -715,18 +715,21 @@ void Panel::createRemoteLowBattery() {
}, _remoteLowBattery->lifetime());
constexpr auto kBatterySize = QSize(29, 13);
const auto scaledBatterySize = QSize(
style::ConvertScale(kBatterySize.width()),
style::ConvertScale(kBatterySize.height()));
const auto icon = [&] {
auto svg = QSvgRenderer(
BatterySvg(kBatterySize, st::videoPlayIconFg->c));
auto image = QImage(
kBatterySize * style::DevicePixelRatio(),
scaledBatterySize * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
image.setDevicePixelRatio(style::DevicePixelRatio());
image.fill(Qt::transparent);
{
auto p = QPainter(&image);
svg.render(&p, Rect(kBatterySize));
svg.render(&p, Rect(scaledBatterySize));
}
return image;
}();
@ -745,7 +748,7 @@ void Panel::createRemoteLowBattery() {
p.drawImage(
st::callTooltipMutedIconPosition.x(),
(r.height() - kBatterySize.height()) / 2,
(r.height() - scaledBatterySize.height()) / 2,
icon);
}, _remoteLowBattery->lifetime());

View file

@ -1131,6 +1131,10 @@ historyScheduledToggle: IconButton(historyAttach) {
{ "chat/input_scheduled_dot", attentionButtonFg }
};
}
historyGiftToUser: IconButton(historyAttach) {
icon: icon {{ "chat/input_gift", historyComposeIconFg }};
iconOver: icon {{ "chat/input_gift", historyComposeIconFgOver }};
}
historyAttachEmojiInner: IconButton(historyAttach) {
icon: icon {{ "chat/input_smile_face", historyComposeIconFg }};
@ -1566,3 +1570,27 @@ processingVideoView: RoundButton(defaultActiveButton) {
textBgOver: transparent;
ripple: emptyRippleAnimation;
}
frozenBarTitle: FlatLabel(defaultFlatLabel) {
style: semiboldTextStyle;
textFg: attentionButtonFg;
}
frozenRestrictionTitle: FlatLabel(frozenBarTitle) {
align: align(top);
}
frozenBarSubtitle: FlatLabel(defaultFlatLabel) {
textFg: windowSubTextFg;
}
frozenRestrictionSubtitle: FlatLabel(frozenBarSubtitle) {
align: align(top);
}
frozenInfoBox: Box(defaultBox) {
buttonPadding: margins(16px, 11px, 16px, 16px);
buttonHeight: 42px;
button: RoundButton(defaultActiveButton) {
height: 42px;
textTop: 12px;
style: semiboldTextStyle;
}
shadowIgnoreTopSkip: true;
}

View file

@ -1265,7 +1265,7 @@ void EmojiListWidget::fillEmojiStatusMenu(
int section,
int index) {
const auto chosen = lookupCustomEmoji(index, section);
if (!chosen || chosen.collectible) {
if (!chosen) {
return;
}
const auto selectWith = [=](TimeId scheduled) {

View file

@ -11,38 +11,48 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history.h" // History::session
#include "history/history_item.h" // HistoryItem::originalText
#include "history/history_item_helpers.h" // DropDisallowedCustomEmoji
#include "base/unixtime.h"
#include "base/qthelp_regex.h"
#include "base/qthelp_url.h"
#include "base/event_filter.h"
#include "ui/chat/chat_style.h"
#include "ui/layers/generic_box.h"
#include "ui/basic_click_handlers.h"
#include "ui/rect.h"
#include "core/shortcuts.h"
#include "core/application.h"
#include "core/core_settings.h"
#include "core/ui_integration.h"
#include "lottie/lottie_icon.h"
#include "info/profile/info_profile_icon.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/popup_menu.h"
#include "ui/widgets/shadow.h"
#include "ui/power_saving.h"
#include "ui/vertical_list.h"
#include "ui/ui_utility.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "data/data_document.h"
#include "data/stickers/data_custom_emoji.h"
#include "chat_helpers/emoji_suggestions_widget.h"
#include "history/view/controls/compose_controls_common.h"
#include "window/window_session_controller.h"
#include "lang/lang_keys.h"
#include "mainwindow.h"
#include "main/main_session.h"
#include "settings/settings_common.h"
#include "settings/settings_premium.h"
#include "styles/style_layers.h"
#include "styles/style_boxes.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_credits.h"
#include "styles/style_dialogs.h"
#include "styles/style_menu_icons.h"
#include "styles/style_settings.h"
#include "base/qt/qt_common_adapters.h"
@ -1187,10 +1197,10 @@ base::unique_qptr<Ui::RpWidget> CreateDisabledFieldView(
return result;
}
base::unique_qptr<Ui::RpWidget> TextErrorSendRestriction(
std::unique_ptr<Ui::RpWidget> TextErrorSendRestriction(
QWidget *parent,
const QString &text) {
auto result = base::make_unique_q<Ui::RpWidget>(parent);
auto result = std::make_unique<Ui::RpWidget>(parent);
const auto raw = result.get();
const auto label = CreateChild<Ui::FlatLabel>(
result.get(),
@ -1215,11 +1225,11 @@ base::unique_qptr<Ui::RpWidget> TextErrorSendRestriction(
return result;
}
base::unique_qptr<Ui::RpWidget> PremiumRequiredSendRestriction(
std::unique_ptr<Ui::RpWidget> PremiumRequiredSendRestriction(
QWidget *parent,
not_null<UserData*> user,
not_null<Window::SessionController*> controller) {
auto result = base::make_unique_q<Ui::RpWidget>(parent);
auto result = std::make_unique<Ui::RpWidget>(parent);
const auto raw = result.get();
const auto label = CreateChild<Ui::FlatLabel>(
result.get(),
@ -1254,6 +1264,102 @@ base::unique_qptr<Ui::RpWidget> PremiumRequiredSendRestriction(
return result;
}
std::unique_ptr<Ui::AbstractButton> BoostsToLiftWriteRestriction(
not_null<QWidget*> parent,
std::shared_ptr<ChatHelpers::Show> show,
not_null<PeerData*> peer,
int boosts) {
auto result = std::make_unique<Ui::FlatButton>(
parent,
tr::lng_restricted_boost_group(tr::now),
st::historyComposeButton);
result->setClickedCallback([=] {
const auto window = show->resolveWindow();
window->resolveBoostState(peer->asChannel(), boosts);
});
return result;
}
std::unique_ptr<Ui::AbstractButton> FrozenWriteRestriction(
not_null<QWidget*> parent,
std::shared_ptr<ChatHelpers::Show> show,
FrozenWriteRestrictionType type,
FreezeInfoStyleOverride st) {
using namespace Ui;
auto result = std::make_unique<FlatButton>(
parent,
QString(),
st::historyComposeButton);
const auto raw = result.get();
const auto bar = (type == FrozenWriteRestrictionType::DialogsList);
const auto title = CreateChild<FlatLabel>(
raw,
(bar ? tr::lng_frozen_bar_title : tr::lng_frozen_restrict_title)(
tr::now),
bar ? st::frozenBarTitle : st::frozenRestrictionTitle);
title->setAttribute(Qt::WA_TransparentForMouseEvents);
title->show();
const auto subtitle = CreateChild<FlatLabel>(
raw,
(bar
? tr::lng_frozen_bar_text(
lt_arrow,
rpl::single(Ui::Text::IconEmoji(&st::textMoreIconEmoji)),
Ui::Text::WithEntities)
: tr::lng_frozen_restrict_text(Ui::Text::WithEntities)),
bar ? st::frozenBarSubtitle : st::frozenRestrictionSubtitle);
subtitle->setAttribute(Qt::WA_TransparentForMouseEvents);
subtitle->show();
const auto shadow = bar ? CreateChild<PlainShadow>(raw) : nullptr;
const auto icon = bar ? CreateChild<RpWidget>(raw) : nullptr;
if (icon) {
icon->paintRequest() | rpl::start_with_next([=] {
auto p = QPainter(icon);
st::menuIconDisableAttention.paintInCenter(p, icon->rect());
}, icon->lifetime());
icon->show();
}
raw->sizeValue() | rpl::start_with_next([=](QSize size) {
if (bar) {
const auto toggle = [&](auto &&widget, bool shown) {
if (widget->isHidden() == shown) {
widget->setVisible(shown);
}
};
const auto small = 2 * st::defaultDialogRow.photoSize;
const auto shown = (size.width() > small);
toggle(icon, !shown);
toggle(title, shown);
toggle(subtitle, shown);
icon->setGeometry(0, 0, size.width(), size.height());
}
const auto skip = bar
? st::defaultDialogRow.padding.left()
: 2 * st::normalFont->spacew;
const auto available = size.width() - skip * 2;
title->resizeToWidth(available);
subtitle->resizeToWidth(available);
const auto height = title->height() + subtitle->height();
const auto top = (size.height() - height) / 2;
title->moveToLeft(skip, top, size.width());
subtitle->moveToLeft(skip, top + title->height(), size.width());
const auto line = st::lineWidth;
if (shadow) {
shadow->setGeometry(0, size.height() - line, size.width(), line);
}
}, title->lifetime());
raw->setClickedCallback([=] {
show->show(Box(FrozenInfoBox, &show->session(), st));
});
return result;
}
void SelectTextInFieldWithMargins(
not_null<Ui::InputField*> field,
const TextSelection &selection) {
@ -1301,3 +1407,101 @@ rpl::producer<TextWithEntities> PaidSendButtonText(
return PaidSendButtonText(tr::now, count);
});
}
void FrozenInfoBox(
not_null<Ui::GenericBox*> box,
not_null<Main::Session*> session,
FreezeInfoStyleOverride st) {
box->setWidth(st::boxWideWidth);
box->setStyle(st::frozenInfoBox);
box->setNoContentMargin(true);
box->addTopButton(st::boxTitleClose, [=] {
box->closeBox();
});
const auto info = session->frozen();
const auto content = box->verticalLayout();
auto icon = Settings::CreateLottieIcon(
content,
{
.name = u"media_forbidden"_q,
.sizeOverride = {
st::changePhoneIconSize,
st::changePhoneIconSize,
},
},
st::settingLocalPasscodeIconPadding);
content->add(std::move(icon.widget));
box->setShowFinishedCallback([animate = std::move(icon.animate)] {
animate(anim::repeat::once);
});
Ui::AddSkip(content);
const auto infoRow = [&](
rpl::producer<QString> title,
rpl::producer<TextWithEntities> text,
not_null<const style::icon*> icon) {
auto raw = content->add(
object_ptr<Ui::VerticalLayout>(content));
raw->add(
object_ptr<Ui::FlatLabel>(
raw,
std::move(title) | Ui::Text::ToBold(),
st.infoTitle ? *st.infoTitle : st::defaultFlatLabel),
st::settingsPremiumRowTitlePadding);
raw->add(
object_ptr<Ui::FlatLabel>(
raw,
std::move(text),
st.infoAbout ? *st.infoAbout : st::upgradeGiftSubtext),
st::settingsPremiumRowAboutPadding);
object_ptr<Info::Profile::FloatingIcon>(
raw,
*icon,
st::starrefInfoIconPosition);
};
content->add(
object_ptr<Ui::FlatLabel>(
content,
tr::lng_frozen_title(),
st.title ? *st.title : st::uniqueGiftTitle),
st::settingsPremiumRowTitlePadding);
Ui::AddSkip(content, st::defaultVerticalListSkip * 3);
infoRow(
tr::lng_frozen_subtitle1(),
tr::lng_frozen_text1(Ui::Text::WithEntities),
st.violationIcon ? st.violationIcon : &st::menuIconBlock);
infoRow(
tr::lng_frozen_subtitle2(),
tr::lng_frozen_text2(Ui::Text::WithEntities),
st.readOnlyIcon ? st.readOnlyIcon : &st::menuIconLock);
infoRow(
tr::lng_frozen_subtitle3(),
tr::lng_frozen_text3(
lt_link,
rpl::single(Ui::Text::Link(u"@SpamBot"_q, info.appealUrl)),
lt_date,
rpl::single(TextWithEntities{
langDayOfMonthFull(
base::unixtime::parse(info.until).date()),
}),
Ui::Text::WithEntities),
st.appealIcon ? st.appealIcon : &st::menuIconHourglass);
const auto button = box->addButton(
tr::lng_frozen_appeal_button(),
[url = info.appealUrl] { UrlClickHandler::Open(url); });
const auto buttonPadding = st::frozenInfoBox.buttonPadding;
const auto buttonWidth = st::boxWideWidth
- buttonPadding.left()
- buttonPadding.right();
button->widthValue() | rpl::filter([=] {
return (button->widthNoMargins() != buttonWidth);
}) | rpl::start_with_next([=] {
button->resizeToWidth(buttonWidth);
}, button->lifetime());
}

View file

@ -37,7 +37,12 @@ enum class PauseReason;
class Show;
} // namespace ChatHelpers
namespace HistoryView::Controls {
struct WriteRestriction;
} // namespace HistoryView::Controls
namespace Ui {
class GenericBox;
class PopupMenu;
class Show;
} // namespace Ui
@ -162,13 +167,41 @@ private:
[[nodiscard]] base::unique_qptr<Ui::RpWidget> CreateDisabledFieldView(
QWidget *parent,
not_null<PeerData*> peer);
[[nodiscard]] base::unique_qptr<Ui::RpWidget> TextErrorSendRestriction(
[[nodiscard]] std::unique_ptr<Ui::RpWidget> TextErrorSendRestriction(
QWidget *parent,
const QString &text);
[[nodiscard]] base::unique_qptr<Ui::RpWidget> PremiumRequiredSendRestriction(
[[nodiscard]] std::unique_ptr<Ui::RpWidget> PremiumRequiredSendRestriction(
QWidget *parent,
not_null<UserData*> user,
not_null<Window::SessionController*> controller);
[[nodiscard]] auto BoostsToLiftWriteRestriction(
not_null<QWidget*> parent,
std::shared_ptr<ChatHelpers::Show> show,
not_null<PeerData*> peer,
int boosts)
-> std::unique_ptr<Ui::AbstractButton>;
struct FreezeInfoStyleOverride {
const style::Box *box = nullptr;
const style::FlatLabel *title = nullptr;
const style::FlatLabel *subtitle = nullptr;
const style::icon *violationIcon = nullptr;
const style::icon *readOnlyIcon = nullptr;
const style::icon *appealIcon = nullptr;
const style::FlatLabel *infoTitle = nullptr;
const style::FlatLabel *infoAbout = nullptr;
};
[[nodiscard]] FreezeInfoStyleOverride DarkFreezeInfoStyle();
enum class FrozenWriteRestrictionType {
MessageField,
DialogsList,
};
[[nodiscard]] std::unique_ptr<Ui::AbstractButton> FrozenWriteRestriction(
not_null<QWidget*> parent,
std::shared_ptr<ChatHelpers::Show> show,
FrozenWriteRestrictionType type,
FreezeInfoStyleOverride st = {});
void SelectTextInFieldWithMargins(
not_null<Ui::InputField*> field,
@ -178,3 +211,8 @@ void SelectTextInFieldWithMargins(
[[nodiscard]] rpl::producer<TextWithEntities> PaidSendButtonText(
rpl::producer<int> stars,
rpl::producer<QString> fallback = nullptr);
void FrozenInfoBox(
not_null<Ui::GenericBox*> box,
not_null<Main::Session*> session,
FreezeInfoStyleOverride st);

View file

@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "chat_helpers/stickers_list_widget.h"
#include "chat_helpers/gifs_list_widget.h"
#include "menu/menu_send.h"
#include "ui/controls/swipe_handler.h"
#include "ui/controls/tabbed_search.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/buttons.h"
@ -522,7 +523,6 @@ TabbedSelector::TabbedSelector(
if (hasEmojiTab()) {
emoji()->refreshEmoji();
}
//setAttribute(Qt::WA_AcceptTouchEvents);
setAttribute(Qt::WA_OpaquePaintEvent, false);
showAll();
hide();
@ -530,6 +530,60 @@ TabbedSelector::TabbedSelector(
TabbedSelector::~TabbedSelector() = default;
void TabbedSelector::reinstallSwipe(not_null<Ui::RpWidget*> widget) {
_swipeLifetime.destroy();
auto update = [=](Ui::Controls::SwipeContextData data) {
if (data.translation != 0) {
if (!_swipeBackData.callback) {
_swipeBackData = Ui::Controls::SetupSwipeBack(
this,
[=]() -> std::pair<QColor, QColor> {
return {
st::historyForwardChooseBg->c,
st::historyForwardChooseFg->c,
};
},
data.translation < 0);
}
_swipeBackData.callback(data);
return;
} else if (_swipeBackData.lifetime) {
_swipeBackData = {};
}
};
auto init = [=](int, Qt::LayoutDirection direction) {
if (!_tabsSlider) {
return Ui::Controls::SwipeHandlerFinishData();
}
const auto activeSection = _tabsSlider->activeSection();
const auto isToLeft = direction == Qt::RightToLeft;
if ((isToLeft && activeSection > 0)
|| (!isToLeft && activeSection < _tabs.size() - 1)) {
return Ui::Controls::DefaultSwipeBackHandlerFinishData([=] {
if (_tabsSlider
&& _tabsSlider->activeSection() == activeSection) {
_swipeBackData = {};
_tabsSlider->setActiveSection(isToLeft
? activeSection - 1
: activeSection + 1);
}
});
}
return Ui::Controls::SwipeHandlerFinishData();
};
Ui::Controls::SetupSwipeHandler({
.widget = widget,
.scroll = _scroll.data(),
.update = std::move(update),
.init = std::move(init),
.dontStart = nullptr,
.onLifetime = &_swipeLifetime,
});
}
const style::EmojiPan &TabbedSelector::st() const {
return _st;
}
@ -1302,6 +1356,10 @@ void TabbedSelector::setWidgetToScrollArea() {
inner->moveToLeft(0, 0);
inner->show();
if (_tabs.size() > 1) {
reinstallSwipe(inner);
}
_scroll->disableScroll(false);
scrollToY(currentTab()->getScrollTop());
handleScroll();

View file

@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_common.h"
#include "chat_helpers/compose/compose_features.h"
#include "ui/rp_widget.h"
#include "ui/controls/swipe_handler_data.h"
#include "ui/effects/animations.h"
#include "ui/effects/message_sending_animation_common.h"
#include "ui/effects/panel_animation.h"
@ -287,12 +288,16 @@ private:
not_null<GifsListWidget*> gifs() const;
not_null<StickersListWidget*> masks() const;
void reinstallSwipe(not_null<Ui::RpWidget*> widget);
const style::EmojiPan &_st;
const ComposeFeatures _features;
const std::shared_ptr<Show> _show;
const PauseReason _level = {};
const Fn<QColor()> _customTextColor;
Ui::Controls::SwipeBackResult _swipeBackData;
Mode _mode = Mode::Full;
int _roundRadius = 0;
int _footerTop = 0;
@ -329,6 +334,8 @@ private:
rpl::event_stream<> _showRequests;
rpl::event_stream<> _slideFinished;
rpl::lifetime _swipeLifetime;
};
class TabbedSelector::Inner : public Ui::RpWidget {

View file

@ -93,6 +93,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include <QtGui/QScreen>
#include <QtGui/QWindow>
#include <ksandbox.h>
// AyuGram includes
#include "ayu/ayu_infra.h"
#include "ayu/features/streamer_mode/streamer_mode.h"

View file

@ -244,7 +244,7 @@ QByteArray Settings::serialize() const {
+ Serialize::stringSize(_customFontFamily)
+ sizeof(qint32) * 3
+ Serialize::bytearraySize(_tonsiteStorageToken)
+ sizeof(qint32) * 7;
+ sizeof(qint32) * 8;
auto result = QByteArray();
result.reserve(size);
@ -405,7 +405,8 @@ QByteArray Settings::serialize() const {
<< qint32(_recordVideoMessages ? 1 : 0)
<< SerializeVideoQuality(_videoQuality)
<< qint32(_ivZoom.current())
<< qint32(_systemDarkModeEnabled.current() ? 1 : 0);
<< qint32(_systemDarkModeEnabled.current() ? 1 : 0)
<< qint32(_quickDialogAction);
}
Ensures(result.size() == size);
@ -536,6 +537,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
qint32 recordVideoMessages = _recordVideoMessages ? 1 : 0;
quint32 videoQuality = SerializeVideoQuality(_videoQuality);
quint32 chatFiltersHorizontal = _chatFiltersHorizontal.current() ? 1 : 0;
quint32 quickDialogAction = quint32(_quickDialogAction);
stream >> themesAccentColors;
if (!stream.atEnd()) {
@ -864,6 +866,9 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
if (!stream.atEnd()) {
stream >> systemDarkModeEnabled;
}
if (!stream.atEnd()) {
stream >> quickDialogAction;
}
if (stream.status() != QDataStream::Ok) {
LOG(("App Error: "
"Bad data for Core::Settings::constructFromSerialized()"));
@ -1086,6 +1091,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
_recordVideoMessages = (recordVideoMessages == 1);
_videoQuality = DeserializeVideoQuality(videoQuality);
_chatFiltersHorizontal = (chatFiltersHorizontal == 1);
_quickDialogAction = Dialogs::Ui::QuickDialogAction(quickDialogAction);
}
QString Settings::getSoundPath(const QString &key) const {
@ -1477,6 +1483,7 @@ void Settings::resetOnLastLogout() {
_recordVideoMessages = false;
_videoQuality = {};
_chatFiltersHorizontal = false;
_quickDialogAction = Dialogs::Ui::QuickDialogAction::Disabled;
_recentEmojiPreload.clear();
_recentEmoji.clear();
@ -1664,4 +1671,12 @@ void Settings::setChatFiltersHorizontal(bool value) {
_chatFiltersHorizontal = value;
}
Dialogs::Ui::QuickDialogAction Settings::quickDialogAction() const {
return _quickDialogAction;
}
void Settings::setQuickDialogAction(Dialogs::Ui::QuickDialogAction action) {
_quickDialogAction = action;
}
} // namespace Core

View file

@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "core/core_settings_proxy.h"
#include "media/media_common.h"
#include "dialogs/ui/dialogs_quick_action.h"
#include "window/themes/window_themes_embedded.h"
#include "ui/chat/attach/attach_send_files_way.h"
#include "base/flags.h"
@ -952,6 +953,9 @@ public:
[[nodiscard]] static PlaybackSpeed DeserializePlaybackSpeed(
qint32 speed);
[[nodiscard]] Dialogs::Ui::QuickDialogAction quickDialogAction() const;
void setQuickDialogAction(Dialogs::Ui::QuickDialogAction);
void resetOnLastLogout();
private:
@ -1092,6 +1096,9 @@ private:
bool _recordVideoMessages = false;
Dialogs::Ui::QuickDialogAction _quickDialogAction
= Dialogs::Ui::QuickDialogAction::Disabled;
QByteArray _photoEditorBrush;
};

View file

@ -504,6 +504,22 @@ uint64 Launcher::installationTag() const {
return InstallationTag;
}
QByteArray Launcher::instanceHash() const {
static const auto Result = [&] {
QByteArray h(32, 0);
if (customWorkingDir()) {
const auto d = QFile::encodeName(
QDir(cWorkingDir()).absolutePath());
hashMd5Hex(d.constData(), d.size(), h.data());
} else {
const auto f = QFile::encodeName(cExeDir() + cExeName());
hashMd5Hex(f.constData(), f.size(), h.data());
}
return h;
}();
return Result;
}
void Launcher::processArguments() {
enum class KeyFormat {
NoValues,

View file

@ -33,6 +33,7 @@ public:
bool customWorkingDir() const;
uint64 installationTag() const;
QByteArray instanceHash() const;
bool checkPortableVersionFolder();
bool validateCustomWorkingDir();

View file

@ -306,6 +306,42 @@ void ShowLanguagesBox(Window::SessionController *controller) {
Guard = LanguageBox::Show(controller);
}
void ShowPhonePrivacyBox(Window::SessionController *controller) {
static auto Guard = base::binary_guard();
auto guard = base::binary_guard();
using Privacy = Api::UserPrivacy;
const auto key = Privacy::Key::PhoneNumber;
controller->session().api().userPrivacy().reload(key);
const auto weak = base::make_weak(controller);
auto shared = std::make_shared<base::binary_guard>(
guard.make_guard());
auto lifetime = std::make_shared<rpl::lifetime>();
controller->session().api().userPrivacy().value(
key
) | rpl::take(
1
) | rpl::start_with_next([=](const Privacy::Rule &value) mutable {
using namespace ::Settings;
const auto show = shared->alive();
if (lifetime) {
base::take(lifetime)->destroy();
}
if (show) {
if (const auto controller = weak.get()) {
controller->show(Box<EditPrivacyBox>(
controller,
std::make_unique<PhoneNumberPrivacyController>(
controller),
value));
}
}
}, *lifetime);
Guard = std::move(guard);
}
bool SetLanguage(
Window::SessionController *controller,
const Match &match,
@ -722,6 +758,9 @@ bool ResolveSettings(
if (section == u"language"_q) {
ShowLanguagesBox(controller);
return {};
} else if (section == u"phone_privacy"_q) {
ShowPhonePrivacyBox(controller);
return {};
} else if (section == u"devices"_q) {
return ::Settings::Sessions::Id();
} else if (section == u"folders"_q) {
@ -876,6 +915,8 @@ bool ShowEditBirthday(
const QVariant &context) {
if (!controller) {
return false;
} else if (controller->showFrozenError()) {
return true;
}
const auto user = controller->session().user();
const auto save = [=](Data::Birthday result) {
@ -932,6 +973,8 @@ bool ShowEditPersonalChannel(
const QVariant &context) {
if (!controller) {
return false;
} else if (controller->showFrozenError()) {
return true;
}
auto listController = std::make_unique<PersonalChannelController>(
@ -1466,7 +1509,7 @@ const std::vector<LocalUrlHandler> &LocalUrlHandlers() {
ResolvePrivatePost
},
{
u"^settings(/language|/devices|/folders|/privacy|/themes|/change_number|/auto_delete|/information|/edit_profile)?$"_q,
u"^settings(/language|/devices|/folders|/privacy|/themes|/change_number|/auto_delete|/information|/edit_profile|/phone_privacy)?$"_q,
ResolveSettings
},
{

View file

@ -110,6 +110,7 @@ const auto CommandByName = base::flat_map<QString, Command>{
{ u"read_chat"_q , Command::ReadChat },
{ u"show_chat_menu"_q , Command::ShowChatMenu },
{ u"show_chat_preview"_q , Command::ShowChatPreview },
// Shortcuts that have no default values.
{ u"message"_q , Command::JustSendMessage },
@ -506,6 +507,7 @@ void Manager::fillDefaults() {
set(u"ctrl+r"_q, Command::ReadChat);
set(u"ctrl+\\"_q, Command::ShowChatMenu);
set(u"ctrl+]"_q, Command::ShowChatPreview);
_defaults = keysCurrents();
}

View file

@ -72,6 +72,7 @@ enum class Command {
MediaViewerFullscreen,
ShowChatMenu,
ShowChatPreview,
SupportReloadTemplates,
SupportToggleMuted,

View file

@ -21,10 +21,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include <set>
#include <filesystem>
#if __has_include(<ksandbox.h>)
#include <ksandbox.h>
#endif
#define qsl(s) QStringLiteral(s)
namespace base {
@ -34,15 +30,6 @@ inline bool in_range(Value &&value, From &&from, Till &&till) {
return (value >= from) && (value < till);
}
#if __has_include(<ksandbox.h>)
inline QString IconName() {
static const auto Result = KSandbox::isFlatpak()
? qEnvironmentVariable("FLATPAK_ID")
: u"telegram"_q;
return Result;
}
#endif
inline bool CanReadDirectory(const QString &path) {
#ifndef Q_OS_MAC // directory_iterator since 10.15
std::error_code error;

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 = 5012003;
constexpr auto AppVersionStr = "5.12.3";
constexpr auto AppVersion = 5013001;
constexpr auto AppVersionStr = "5.13.1";
constexpr auto AppBetaVersion = false;
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;

View file

@ -8,10 +8,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/business/data_business_chatbots.h"
#include "apiwrap.h"
#include "boxes/peers/edit_peer_permissions_box.h"
#include "data/business/data_business_common.h"
#include "data/business/data_business_info.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
namespace Data {
@ -41,7 +43,7 @@ void Chatbots::preload() {
_settings = ChatbotsSettings{
.bot = _owner->session().data().user(botId),
.recipients = FromMTP(_owner, bot.vrecipients()),
.repliesAllowed = bot.is_can_reply(),
.permissions = FromMTP(bot.vrights()),
};
} else {
_settings.force_assign(ChatbotsSettings());
@ -81,11 +83,8 @@ void Chatbots::save(
using Flag = MTPaccount_UpdateConnectedBot::Flag;
const auto api = &_owner->session().api();
api->request(MTPaccount_UpdateConnectedBot(
MTP_flags(!settings.bot
? Flag::f_deleted
: settings.repliesAllowed
? Flag::f_can_reply
: Flag()),
MTP_flags(!settings.bot ? Flag::f_deleted : Flag::f_rights),
ToMTP(settings.permissions),
(settings.bot ? settings.bot : was.bot)->inputUser,
ForBotsToMTP(settings.recipients)
)).done([=](const MTPUpdates &result) {
@ -180,4 +179,39 @@ void Chatbots::reload() {
preload();
}
EditFlagsDescriptor<ChatbotsPermissions> ChatbotsPermissionsLabels() {
using Flag = ChatbotsPermission;
using PermissionLabel = EditFlagsLabel<ChatbotsPermissions>;
auto messages = std::vector<PermissionLabel>{
{ Flag::ViewMessages, tr::lng_chatbots_read(tr::now) },
{ Flag::ReplyToMessages, tr::lng_chatbots_reply(tr::now) },
{ Flag::MarkAsRead, tr::lng_chatbots_mark_as_read(tr::now) },
{ Flag::DeleteSent, tr::lng_chatbots_delete_sent(tr::now) },
{ Flag::DeleteReceived, tr::lng_chatbots_delete_received(tr::now) },
};
auto manage = std::vector<PermissionLabel>{
{ Flag::EditName, tr::lng_chatbots_edit_name(tr::now) },
{ Flag::EditBio, tr::lng_chatbots_edit_bio(tr::now) },
{ Flag::EditUserpic, tr::lng_chatbots_edit_userpic(tr::now) },
{ Flag::EditUsername, tr::lng_chatbots_edit_username(tr::now) },
};
auto gifts = std::vector<PermissionLabel>{
{ Flag::ViewGifts, tr::lng_chatbots_view_gifts(tr::now) },
{ Flag::SellGifts, tr::lng_chatbots_sell_gifts(tr::now) },
{ Flag::GiftSettings, tr::lng_chatbots_gift_settings(tr::now) },
{ Flag::TransferGifts, tr::lng_chatbots_transfer_gifts(tr::now) },
{ Flag::TransferStars, tr::lng_chatbots_transfer_stars(tr::now) },
};
auto stories = std::vector<PermissionLabel>{
{ Flag::ManageStories, tr::lng_chatbots_manage_stories(tr::now) },
};
return { .labels = {
{ tr::lng_chatbots_manage_messages(), std::move(messages) },
{ tr::lng_chatbots_manage_profile(), std::move(manage) },
{ tr::lng_chatbots_manage_gifts(), std::move(gifts) },
{ std::nullopt, std::move(stories) },
}, .st = nullptr };
}
} // namespace Data

View file

@ -11,6 +11,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
class UserData;
template <typename Flags>
struct EditFlagsDescriptor;
namespace Data {
class Session;
@ -18,7 +21,7 @@ class Session;
struct ChatbotsSettings {
UserData *bot = nullptr;
BusinessRecipients recipients;
bool repliesAllowed = false;
ChatbotsPermissions permissions;
friend inline bool operator==(
const ChatbotsSettings &,
@ -67,4 +70,7 @@ private:
};
[[nodiscard]] auto ChatbotsPermissionsLabels()
-> EditFlagsDescriptor<ChatbotsPermissions>;
} // namespace Data

View file

@ -159,6 +159,47 @@ BusinessRecipients FromMTP(
return result;
}
ChatbotsPermissions FromMTP(const MTPBusinessBotRights &rights) {
using Flag = ChatbotsPermission;
const auto &data = rights.data();
return Flag::ViewMessages
| (data.is_reply() ? Flag::ReplyToMessages : Flag())
| (data.is_read_messages() ? Flag::MarkAsRead : Flag())
| (data.is_delete_sent_messages() ? Flag::DeleteSent : Flag())
| (data.is_delete_received_messages() ? Flag::DeleteReceived : Flag())
| (data.is_edit_name() ? Flag::EditName : Flag())
| (data.is_edit_bio() ? Flag::EditBio : Flag())
| (data.is_edit_profile_photo() ? Flag::EditUserpic : Flag())
| (data.is_edit_username() ? Flag::EditUsername : Flag())
| (data.is_view_gifts() ? Flag::ViewGifts : Flag())
| (data.is_sell_gifts() ? Flag::SellGifts : Flag())
| (data.is_change_gift_settings() ? Flag::GiftSettings : Flag())
| (data.is_transfer_and_upgrade_gifts() ? Flag::TransferGifts : Flag())
| (data.is_transfer_stars() ? Flag::TransferStars : Flag())
| (data.is_manage_stories() ? Flag::ManageStories : Flag());
}
MTPBusinessBotRights ToMTP(ChatbotsPermissions rights) {
using Flag = MTPDbusinessBotRights::Flag;
using Right = ChatbotsPermission;
return MTP_businessBotRights(MTP_flags(Flag()
| ((rights & Right::ReplyToMessages) ? Flag::f_reply : Flag())
| ((rights & Right::MarkAsRead) ? Flag::f_read_messages : Flag())
| ((rights & Right::DeleteSent) ? Flag::f_delete_sent_messages : Flag())
| ((rights & Right::DeleteReceived) ? Flag::f_delete_received_messages : Flag())
| ((rights & Right::EditName) ? Flag::f_edit_name : Flag())
| ((rights & Right::EditBio) ? Flag::f_edit_bio : Flag())
| ((rights & Right::EditUserpic) ? Flag::f_edit_profile_photo : Flag())
| ((rights & Right::EditUsername) ? Flag::f_edit_username : Flag())
| ((rights & Right::ViewGifts) ? Flag::f_view_gifts : Flag())
| ((rights & Right::SellGifts) ? Flag::f_sell_gifts : Flag())
| ((rights & Right::GiftSettings) ? Flag::f_change_gift_settings : Flag())
| ((rights & Right::TransferGifts) ? Flag::f_transfer_and_upgrade_gifts : Flag())
| ((rights & Right::TransferStars) ? Flag::f_transfer_stars : Flag())
| ((rights & Right::ManageStories) ? Flag::f_manage_stories : Flag())));
}
BusinessDetails FromMTP(
not_null<Session*> owner,
const tl::conditional<MTPBusinessWorkHours> &hours,

View file

@ -57,6 +57,26 @@ enum class BusinessRecipientsType : uchar {
Bots,
};
enum class ChatbotsPermission {
ViewMessages = 0x0001,
ReplyToMessages = 0x0002,
MarkAsRead = 0x0004,
DeleteSent = 0x0008,
DeleteReceived = 0x0010,
EditName = 0x0020,
EditBio = 0x0040,
EditUserpic = 0x0080,
EditUsername = 0x0100,
ViewGifts = 0x0200,
SellGifts = 0x0400,
GiftSettings = 0x0800,
TransferGifts = 0x1000,
TransferStars = 0x2000,
ManageStories = 0x4000,
};
inline constexpr bool is_flag_type(ChatbotsPermission) { return true; }
using ChatbotsPermissions = base::flags<ChatbotsPermission>;
[[nodiscard]] MTPInputBusinessRecipients ForMessagesToMTP(
const BusinessRecipients &data);
[[nodiscard]] MTPInputBusinessBotRecipients ForBotsToMTP(
@ -67,6 +87,9 @@ enum class BusinessRecipientsType : uchar {
[[nodiscard]] BusinessRecipients FromMTP(
not_null<Session*> owner,
const MTPBusinessBotRecipients &recipients);
[[nodiscard]] ChatbotsPermissions FromMTP(
const MTPBusinessBotRights &rights);
[[nodiscard]] MTPBusinessBotRights ToMTP(ChatbotsPermissions rights);
struct Timezone {
QString id;

View file

@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/components/sponsored_messages.h"
#include "api/api_text_entities.h"
#include "api/api_peer_search.h" // SponsoredSearchResult
#include "apiwrap.h"
#include "core/click_handler_types.h"
#include "data/data_channel.h"
@ -37,6 +38,19 @@ constexpr auto kRequestTimeLimit = 5 * 60 * crl::time(1000);
return (received > 0) && (received + kRequestTimeLimit > crl::now());
}
template <typename Fields>
[[nodiscard]] std::vector<TextWithEntities> Prepare(const Fields &fields) {
using InfoList = std::vector<TextWithEntities>;
return (!fields.sponsorInfo.text.isEmpty()
&& !fields.additionalInfo.text.isEmpty())
? InfoList{ fields.sponsorInfo, fields.additionalInfo }
: !fields.sponsorInfo.text.isEmpty()
? InfoList{ fields.sponsorInfo }
: !fields.additionalInfo.text.isEmpty()
? InfoList{ fields.additionalInfo }
: InfoList{};
}
} // namespace
SponsoredMessages::SponsoredMessages(not_null<Main::Session*> session)
@ -523,17 +537,16 @@ void SponsoredMessages::view(const FullMsgId &fullId) {
if (!entryPtr) {
return;
}
const auto randomId = entryPtr->sponsored.randomId;
view(entryPtr->sponsored.randomId);
}
void SponsoredMessages::view(const QByteArray &randomId) {
auto &request = _viewRequests[randomId];
if (request.requestId || TooEarlyForRequest(request.lastReceived)) {
return;
}
request.requestId = _session->api().request(
MTPmessages_ViewSponsoredMessage(
entryPtr->item
? entryPtr->item->history()->peer->input
: _session->data().peer(fullId.peer)->input,
MTP_bytes(randomId))
MTPmessages_ViewSponsoredMessage(MTP_bytes(randomId))
).done([=] {
auto &request = _viewRequests[randomId];
request.lastReceived = crl::now();
@ -550,18 +563,8 @@ SponsoredMessages::Details SponsoredMessages::lookupDetails(
return {};
}
const auto &data = entryPtr->sponsored;
using InfoList = std::vector<TextWithEntities>;
auto info = (!data.sponsorInfo.text.isEmpty()
&& !data.additionalInfo.text.isEmpty())
? InfoList{ data.sponsorInfo, data.additionalInfo }
: !data.sponsorInfo.text.isEmpty()
? InfoList{ data.sponsorInfo }
: !data.additionalInfo.text.isEmpty()
? InfoList{ data.additionalInfo }
: InfoList{};
return {
.info = std::move(info),
.info = Prepare(data),
.link = data.link,
.buttonText = data.from.buttonText,
.photoId = data.from.photoId,
@ -574,6 +577,14 @@ SponsoredMessages::Details SponsoredMessages::lookupDetails(
};
}
SponsoredMessages::Details SponsoredMessages::lookupDetails(
const Api::SponsoredSearchResult &data) const {
return {
.info = Prepare(data),
.canReport = true,
};
}
void SponsoredMessages::clicked(
const FullMsgId &fullId,
bool isMedia,
@ -582,22 +593,45 @@ void SponsoredMessages::clicked(
if (!entryPtr) {
return;
}
const auto randomId = entryPtr->sponsored.randomId;
clicked(entryPtr->sponsored.randomId, isMedia, isFullscreen);
}
void SponsoredMessages::clicked(
const QByteArray &randomId,
bool isMedia,
bool isFullscreen) {
using Flag = MTPmessages_ClickSponsoredMessage::Flag;
_session->api().request(MTPmessages_ClickSponsoredMessage(
MTP_flags(Flag(0)
| (isMedia ? Flag::f_media : Flag(0))
| (isFullscreen ? Flag::f_fullscreen : Flag(0))),
entryPtr->item
? entryPtr->item->history()->peer->input
: _session->data().peer(fullId.peer)->input,
MTP_bytes(randomId)
)).send();
}
SponsoredReportAction SponsoredMessages::createReportCallback(
const FullMsgId &fullId) {
const auto entry = find(fullId);
if (!entry) {
return { .callback = [=](const auto &...) {} };
}
const auto history = _session->data().history(fullId.peer);
const auto erase = [=] {
const auto it = _data.find(history);
if (it != end(_data)) {
auto &list = it->second.entries;
const auto proj = [&](const Entry &e) {
return e.itemFullId == fullId;
};
list.erase(ranges::remove_if(list, proj), end(list));
}
};
return createReportCallback(entry->sponsored.randomId, erase);
}
auto SponsoredMessages::createReportCallback(const FullMsgId &fullId)
-> Fn<void(SponsoredReportResult::Id, Fn<void(SponsoredReportResult)>)> {
SponsoredReportAction SponsoredMessages::createReportCallback(
const QByteArray &randomId,
Fn<void()> erase) {
using TLChoose = MTPDchannels_sponsoredMessageReportResultChooseOption;
using TLAdsHidden = MTPDchannels_sponsoredMessageReportResultAdsHidden;
using TLReported = MTPDchannels_sponsoredMessageReportResultReported;
@ -613,25 +647,7 @@ auto SponsoredMessages::createReportCallback(const FullMsgId &fullId)
};
const auto state = std::make_shared<State>();
return [=](Result::Id optionId, Fn<void(Result)> done) {
const auto entry = find(fullId);
if (!entry) {
return;
}
const auto history = _session->data().history(fullId.peer);
const auto erase = [=] {
const auto it = _data.find(history);
if (it != end(_data)) {
auto &list = it->second.entries;
const auto proj = [&](const Entry &e) {
return e.itemFullId == fullId;
};
list.erase(ranges::remove_if(list, proj), end(list));
}
};
return { .callback = [=](Result::Id optionId, Fn<void(Result)> done) {
if (optionId == Result::Id("-1")) {
erase();
return;
@ -639,8 +655,7 @@ auto SponsoredMessages::createReportCallback(const FullMsgId &fullId)
state->requestId = _session->api().request(
MTPmessages_ReportSponsoredMessage(
history->peer->input,
MTP_bytes(entry->sponsored.randomId),
MTP_bytes(randomId),
MTP_bytes(optionId))
).done([=](
const MTPchannels_SponsoredMessageReportResult &result,
@ -677,7 +692,7 @@ auto SponsoredMessages::createReportCallback(const FullMsgId &fullId)
done({ .error = error.type() });
}
}).send();
};
} };
}
SponsoredMessages::State SponsoredMessages::state(

View file

@ -14,6 +14,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
class History;
namespace Api {
struct SponsoredSearchResult;
} // namespace Api
namespace Main {
class Session;
} // namespace Main
@ -69,6 +73,25 @@ struct SponsoredMessage {
TextWithEntities additionalInfo;
};
struct SponsoredMessageDetails {
std::vector<TextWithEntities> info;
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;
bool canReport = false;
};
struct SponsoredReportAction {
Fn<void(
Data::SponsoredReportResult::Id,
Fn<void(Data::SponsoredReportResult)>)> callback;
};
class SponsoredMessages final {
public:
enum class AppendResult {
@ -82,18 +105,7 @@ public:
InjectToMiddle,
AppendToTopBar,
};
struct Details {
std::vector<TextWithEntities> info;
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;
bool canReport = false;
};
using Details = SponsoredMessageDetails;
using RandomId = QByteArray;
explicit SponsoredMessages(not_null<Main::Session*> session);
~SponsoredMessages();
@ -103,7 +115,13 @@ public:
void request(not_null<History*> history, Fn<void()> done);
void clearItems(not_null<History*> history);
[[nodiscard]] Details lookupDetails(const FullMsgId &fullId) const;
[[nodiscard]] Details lookupDetails(
const Api::SponsoredSearchResult &data) const;
void clicked(const FullMsgId &fullId, bool isMedia, bool isFullscreen);
void clicked(
const QByteArray &randomId,
bool isMedia,
bool isFullscreen);
[[nodiscard]] FullMsgId fillTopBar(
not_null<History*> history,
not_null<Ui::RpWidget*> widget);
@ -117,11 +135,15 @@ public:
int fallbackWidth);
void view(const FullMsgId &fullId);
void view(const QByteArray &randomId);
[[nodiscard]] State state(not_null<History*> history) const;
[[nodiscard]] auto createReportCallback(const FullMsgId &fullId)
-> Fn<void(SponsoredReportResult::Id, Fn<void(SponsoredReportResult)>)>;
[[nodiscard]] SponsoredReportAction createReportCallback(
const FullMsgId &fullId);
[[nodiscard]] SponsoredReportAction createReportCallback(
const QByteArray &randomId,
Fn<void()> erase);
void clear();

View file

@ -96,27 +96,28 @@ struct PeerUpdate {
PersonalChannel = (1ULL << 34),
StarRefProgram = (1ULL << 35),
PaysPerMessage = (1ULL << 36),
GiftSettings = (1ULL << 37),
// For chats and channels
InviteLinks = (1ULL << 37),
Members = (1ULL << 38),
Admins = (1ULL << 39),
BannedUsers = (1ULL << 40),
Rights = (1ULL << 41),
PendingRequests = (1ULL << 42),
Reactions = (1ULL << 43),
InviteLinks = (1ULL << 38),
Members = (1ULL << 39),
Admins = (1ULL << 40),
BannedUsers = (1ULL << 41),
Rights = (1ULL << 42),
PendingRequests = (1ULL << 43),
Reactions = (1ULL << 44),
// For channels
ChannelAmIn = (1ULL << 44),
StickersSet = (1ULL << 45),
EmojiSet = (1ULL << 46),
ChannelLinkedChat = (1ULL << 47),
ChannelLocation = (1ULL << 48),
Slowmode = (1ULL << 49),
GroupCall = (1ULL << 50),
ChannelAmIn = (1ULL << 45),
StickersSet = (1ULL << 46),
EmojiSet = (1ULL << 47),
ChannelLinkedChat = (1ULL << 48),
ChannelLocation = (1ULL << 49),
Slowmode = (1ULL << 50),
GroupCall = (1ULL << 51),
// For iteration
LastUsedBit = (1ULL << 50),
LastUsedBit = (1ULL << 51),
};
using Flags = base::flags<Flag>;
friend inline constexpr auto is_flag_type(Flag) { return true; }

View file

@ -118,7 +118,10 @@ bool CanSendAnyOf(
not_null<const PeerData*> peer,
ChatRestrictions rights,
bool forbidInForums) {
if (const auto user = peer->asUser()) {
if (peer->session().frozen()
&& !peer->isFreezeAppealChat()) {
return false;
} else if (const auto user = peer->asUser()) {
if (user->isInaccessible()
|| user->isRepliesChat()
|| user->isVerifyCodes()) {
@ -178,7 +181,13 @@ SendError RestrictionError(
not_null<PeerData*> peer,
ChatRestriction restriction) {
using Flag = ChatRestriction;
if (const auto restricted = peer->amRestricted(restriction)) {
if (peer->session().frozen()
&& !peer->isFreezeAppealChat()) {
return SendError({
.text = tr::lng_frozen_restrict_title(tr::now),
.frozen = true,
});
} else if (const auto restricted = peer->amRestricted(restriction)) {
if (const auto user = peer->asUser()) {
if (user->requiresPremiumToWrite()
&& !user->session().premium()) {

View file

@ -191,16 +191,19 @@ struct SendError {
QString text;
int boostsToLift = 0;
bool premiumToLift = false;
bool frozen = false;
};
SendError(Args &&args)
: text(std::move(args.text))
, boostsToLift(args.boostsToLift)
, premiumToLift(args.premiumToLift) {
, premiumToLift(args.premiumToLift)
, frozen(args.frozen) {
}
QString text;
int boostsToLift = 0;
bool premiumToLift = false;
bool frozen = false;
[[nodiscard]] SendError value_or(SendError other) const {
return *this ? *this : other;

View file

@ -171,8 +171,19 @@ struct AlbumCounts {
}
template <typename MediaType>
[[nodiscard]] uint64 CountCacheKey(not_null<MediaType*> data, bool spoiler) {
return (reinterpret_cast<uint64>(data.get()) & ~1) | (spoiler ? 1 : 0);
[[nodiscard]] uint64 CountCacheKey(
not_null<MediaType*> data,
ImageRoundRadius radius,
bool spoiler) {
return (reinterpret_cast<uint64>(data.get()) & ~3)
| ((radius == ImageRoundRadius::Ellipse) ? 2 : 0)
| (spoiler ? 1 : 0);
}
[[nodiscard]] uint64 SimpleCacheKey(ImageRoundRadius radius, bool spoiler) {
return uint64()
| ((radius == ImageRoundRadius::Ellipse) ? 2 : 0)
| (spoiler ? 1 : 0);
}
[[nodiscard]] ItemPreviewImage PreparePhotoPreviewImage(
@ -181,7 +192,7 @@ template <typename MediaType>
ImageRoundRadius radius,
bool spoiler) {
const auto photo = media->owner();
const auto counted = CountCacheKey(photo, spoiler);
const auto counted = CountCacheKey(photo, radius, spoiler);
if (const auto small = media->image(PhotoSize::Small)) {
return { PreparePreviewImage(small, radius, spoiler), counted };
} else if (const auto thumbnail = media->image(PhotoSize::Thumbnail)) {
@ -191,15 +202,15 @@ template <typename MediaType>
}
const auto allowedToDownload = media->autoLoadThumbnailAllowed(
item->history()->peer);
const auto spoilered = uint64(spoiler ? 1 : 0);
const auto cacheKey = allowedToDownload ? spoilered : counted;
const auto simple = SimpleCacheKey(radius, spoiler);
const auto cacheKey = allowedToDownload ? simple : counted;
if (allowedToDownload) {
media->owner()->load(PhotoSize::Small, item->fullId());
}
if (const auto blurred = media->thumbnailInline()) {
return { PreparePreviewImage(blurred, radius, spoiler), cacheKey };
}
return { QImage(), allowedToDownload ? spoilered : cacheKey };
return { QImage(), allowedToDownload ? simple : cacheKey };
}
[[nodiscard]] ItemPreviewImage PrepareFilePreviewImage(
@ -210,7 +221,7 @@ template <typename MediaType>
Expects(media->owner()->hasThumbnail());
const auto document = media->owner();
const auto readyCacheKey = CountCacheKey(document, spoiler);
const auto readyCacheKey = CountCacheKey(document, radius, spoiler);
if (const auto thumbnail = media->thumbnail()) {
return {
PreparePreviewImage(thumbnail, radius, spoiler),
@ -218,11 +229,11 @@ template <typename MediaType>
};
}
document->loadThumbnail(item->fullId());
const auto spoilered = uint64(spoiler ? 1 : 0);
const auto simple = SimpleCacheKey(radius, spoiler);
if (const auto blurred = media->thumbnailInline()) {
return { PreparePreviewImage(blurred, radius, spoiler), spoilered };
return { PreparePreviewImage(blurred, radius, spoiler), simple };
}
return { QImage(), spoilered };
return { QImage(), simple };
}
[[nodiscard]] QImage PutPlayIcon(QImage preview) {
@ -274,13 +285,14 @@ template <typename MediaType>
[[nodiscard]] ItemPreviewImage FindCachedPreview(
const std::vector<ItemPreviewImage> *existing,
not_null<MediaType*> data,
ImageRoundRadius radius,
bool spoiler) {
if (!existing) {
return {};
}
const auto i = ranges::find(
*existing,
CountCacheKey(data, spoiler),
CountCacheKey(data, radius, spoiler),
&ItemPreviewImage::cacheKey);
return (i != end(*existing)) ? *i : ItemPreviewImage();
}
@ -856,13 +868,17 @@ ItemPreview MediaPhoto::toPreview(ToPreviewOptions options) const {
}
auto images = std::vector<ItemPreviewImage>();
auto context = std::any();
if (auto found = FindCachedPreview(options.existing, _photo, _spoiler)) {
images.push_back(std::move(found));
} else {
const auto media = _photo->createMediaView();
const auto radius = _chat
? ImageRoundRadius::Ellipse
: ImageRoundRadius::Small;
if (auto found = FindCachedPreview(
options.existing,
_photo,
radius,
_spoiler)) {
images.push_back(std::move(found));
} else {
const auto media = _photo->createMediaView();
if (auto prepared = PreparePhotoPreview(
parent(),
media,
@ -1101,18 +1117,24 @@ ItemPreview MediaFile::toPreview(ToPreviewOptions options) const {
auto images = std::vector<ItemPreviewImage>();
auto context = std::any();
const auto existing = options.existing;
if (auto found = FindCachedPreview(existing, _document, _spoiler)) {
images.push_back(std::move(found));
} else if (TryFilePreview(_document)) {
const auto media = _document->createMediaView();
const auto spoilered = _spoiler
|| (_document->isVideoMessage() && ttlSeconds());
const auto radius = _document->isVideoMessage()
? ImageRoundRadius::Ellipse
: ImageRoundRadius::Small;
if (auto found = FindCachedPreview(
existing,
_document,
radius,
spoilered)) {
images.push_back(std::move(found));
} else if (TryFilePreview(_document)) {
const auto media = _document->createMediaView();
if (auto prepared = PrepareFilePreview(
parent(),
media,
radius,
_spoiler)
spoilered)
; prepared || !prepared.cacheKey) {
images.push_back(std::move(prepared));
if (!prepared.cacheKey) {
@ -1809,13 +1831,17 @@ ItemPreview MediaWebPage::toPreview(ToPreviewOptions options) const {
|| _page->type == WebPageType::Video
|| _page->type == WebPageType::Document;
if (pageTypeWithPreview || !_page->collage.items.empty()) {
if (auto found = FindCachedPreview(options.existing, _page, false)) {
const auto radius = ImageRoundRadius::Small;
if (auto found = FindCachedPreview(
options.existing,
_page,
radius,
false)) {
return { .text = caption, .images = { std::move(found) } };
}
auto context = std::any();
auto images = std::vector<ItemPreviewImage>();
auto prepared = ItemPreviewImage();
const auto radius = ImageRoundRadius::Small;
if (const auto photo = MediaWebPage::photo()) {
const auto media = photo->createMediaView();
prepared = PreparePhotoPreview(parent(), media, radius, false);
@ -2063,10 +2089,18 @@ ItemPreview MediaInvoice::toPreview(ToPreviewOptions options) const {
if (!photo && !document) {
continue;
} else if (images.size() < kMaxPreviewImages) {
auto found = photo
? FindCachedPreview(existing, not_null(photo), spoiler)
: FindCachedPreview(existing, not_null(document), spoiler);
const auto radius = ImageRoundRadius::Small;
auto found = photo
? FindCachedPreview(
existing,
not_null(photo),
radius,
spoiler)
: FindCachedPreview(
existing,
not_null(document),
radius,
spoiler);
if (found) {
images.push_back(std::move(found));
} else if (photo) {

View file

@ -1319,6 +1319,10 @@ bool PeerData::isVerifyCodes() const {
return (id == kVerifyCodesId);
}
bool PeerData::isFreezeAppealChat() const {
return username().compare(u"spambot"_q, Qt::CaseInsensitive) == 0;
}
bool PeerData::sharedMediaInfo() const {
return isSelf() || isRepliesChat();
}
@ -1453,6 +1457,7 @@ bool PeerData::canRevokeFullHistory() const {
if (const auto user = asUser()) {
return !isSelf()
&& (!user->isBot() || user->isSupport())
&& !user->isInaccessible()
&& session().serverConfig().revokePrivateInbox
&& (session().serverConfig().revokePrivateTimeLimit == 0x7FFFFFFF);
} else if (const auto chat = asChat()) {

View file

@ -235,6 +235,7 @@ public:
[[nodiscard]] bool isGigagroup() const;
[[nodiscard]] bool isRepliesChat() const;
[[nodiscard]] bool isVerifyCodes() const;
[[nodiscard]] bool isFreezeAppealChat() const;
[[nodiscard]] bool sharedMediaInfo() const;
[[nodiscard]] bool savedSublistsInfo() const;
[[nodiscard]] bool hasStoriesHidden() const;

View file

@ -69,6 +69,7 @@ struct StarGift {
TimeId lastSaleDate = 0;
bool upgradable = false;
bool birthday = false;
bool soldOut = false;
friend inline bool operator==(
const StarGift &,

View file

@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_user.h"
#include "api/api_credits.h"
#include "api/api_global_privacy.h"
#include "api/api_sensitive_content.h"
#include "api/api_statistics.h"
#include "storage/localstorage.h"
@ -673,6 +674,13 @@ bool UserData::hasCalls() const {
&& (callsStatus() != CallsStatus::Unknown);
}
void UserData::setDisallowedGiftTypes(Api::DisallowedGiftTypes types) {
if (_disallowedGiftTypes != types) {
_disallowedGiftTypes = types;
session().changes().peerUpdated(this, UpdateFlag::GiftSettings);
}
}
namespace Data {
void ApplyUserUpdate(not_null<UserData*> user, const MTPDuserFull &update) {
@ -829,6 +837,31 @@ void ApplyUserUpdate(not_null<UserData*> user, const MTPDuserFull &update) {
user->setBotVerifyDetails(
ParseBotVerifyDetails(update.vbot_verification()));
if (const auto gifts = update.vdisallowed_gifts()) {
const auto &data = gifts->data();
user->setDisallowedGiftTypes(Api::DisallowedGiftType()
| (data.is_disallow_unlimited_stargifts()
? Api::DisallowedGiftType::Unlimited
: Api::DisallowedGiftType())
| (data.is_disallow_limited_stargifts()
? Api::DisallowedGiftType::Limited
: Api::DisallowedGiftType())
| (data.is_disallow_unique_stargifts()
? Api::DisallowedGiftType::Unique
: Api::DisallowedGiftType())
| (data.is_disallow_premium_gifts()
? Api::DisallowedGiftType::Premium
: Api::DisallowedGiftType())
| (update.is_display_gifts_button()
? Api::DisallowedGiftType::SendHide
: Api::DisallowedGiftType()));
} else {
user->setDisallowedGiftTypes(Api::DisallowedGiftTypes()
| (update.is_display_gifts_button()
? Api::DisallowedGiftType::SendHide
: Api::DisallowedGiftType()));
}
user->owner().stories().apply(user, update.vstories());
user->fullUpdated();

View file

@ -15,12 +15,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_lastseen_status.h"
#include "data/data_user_names.h"
#include "dialogs/dialogs_key.h"
#include "base/flags.h"
namespace Data {
struct BotCommand;
struct BusinessDetails;
} // namespace Data
namespace Api {
enum class DisallowedGiftType : uchar;
using DisallowedGiftTypes = base::flags<DisallowedGiftType>;
} // namespace Api
struct StarRefProgram {
StarsAmount revenuePerUser;
TimeId endDate = 0;
@ -262,6 +268,11 @@ public:
std::unique_ptr<BotInfo> botInfo;
[[nodiscard]] Api::DisallowedGiftTypes disallowedGiftTypes() const {
return _disallowedGiftTypes;
}
void setDisallowedGiftTypes(Api::DisallowedGiftTypes types);
private:
auto unavailableReasons() const
-> const std::vector<Data::UnavailableReason> & override;
@ -293,6 +304,8 @@ private:
static constexpr auto kInaccessibleAccessHashOld
= 0xFFFFFFFFFFFFFFFFULL;
Api::DisallowedGiftTypes _disallowedGiftTypes;
};
namespace Data {

View file

@ -24,6 +24,10 @@ DialogRow {
unreadMarkDiameter: pixels;
tagTop: pixels;
}
DialogRightButton {
button: RoundButton;
margin: margins;
}
ThreeStateIcon {
icon: icon;
@ -115,11 +119,16 @@ dialogRowFilterTagSkip: 4px;
dialogRowFilterTagStyle: TextStyle(defaultTextStyle) {
font: font(10px);
}
dialogRowOpenBotTextStyle: semiboldTextStyle;
dialogRowOpenBotHeight: 20px;
dialogRowOpenBotRight: 10px;
dialogRowOpenBotTop: 32px;
dialogRowOpenBotRecentTop: 28px;
dialogRowOpenBot: DialogRightButton {
button: RoundButton(defaultActiveButton) {
height: 20px;
textTop: 1px;
}
margin: margins(0px, 32px, 10px, 0px);
}
dialogRowOpenBotRecent: DialogRightButton(dialogRowOpenBot) {
margin: margins(0px, 32px, 28px, 0px);
}
forumDialogJumpArrow: icon{{ "dialogs/dialogs_topic_arrow", dialogsTextFg }};
forumDialogJumpArrowOver: icon{{ "dialogs/dialogs_topic_arrow", dialogsTextFgOver }};
@ -789,3 +798,18 @@ dialogsPopularAppsPadding: margins(10px, 8px, 10px, 12px);
dialogsPopularAppsAbout: FlatLabel(boxDividerLabel) {
minWidth: 128px;
}
dialogsQuickActionSize: 20px;
dialogsQuickActionRippleSize: 80px;
dialogsSponsoredButton: DialogRightButton(dialogRowOpenBot) {
button: RoundButton(defaultLightButton) {
textFg: windowActiveTextFg;
textFgOver: windowActiveTextFg;
textBg: lightButtonBgOver;
textBgOver: lightButtonBgOver;
height: 20px;
textTop: 1px;
}
margin: margins(0px, 9px, 10px, 0px);
}

View file

@ -7,6 +7,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
namespace style {
struct DialogRightButton;
} // namespace style
namespace Ui {
class RippleAnimation;
} // namespace Ui
@ -114,11 +118,16 @@ struct RowsByLetter {
};
struct RightButton final {
const style::DialogRightButton *st = nullptr;
QImage bg;
QImage selectedBg;
QImage activeBg;
Ui::Text::String text;
std::unique_ptr<Ui::RippleAnimation> ripple;
explicit operator bool() const {
return st != nullptr;
}
};
} // namespace Dialogs

View file

@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "dialogs/dialogs_widget.h"
#include "dialogs/dialogs_search_from_controllers.h"
#include "dialogs/dialogs_search_tags.h"
#include "dialogs/dialogs_quick_action.h"
#include "history/view/history_view_context_menu.h"
#include "history/history.h"
#include "history/history_item.h"
@ -32,6 +33,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/ui_utility.h"
#include "data/components/sponsored_messages.h"
#include "data/data_drafts.h"
#include "data/data_folder.h"
#include "data/data_forum.h"
@ -54,12 +56,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/unixtime.h"
#include "base/options.h"
#include "lang/lang_keys.h"
#include "lottie/lottie_icon.h"
#include "mainwindow.h"
#include "mainwidget.h"
#include "storage/storage_account.h"
#include "apiwrap.h"
#include "main/main_session.h"
#include "main/main_session_settings.h"
#include "menu/menu_sponsored.h"
#include "window/notifications_manager.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
@ -242,10 +246,17 @@ struct InnerWidget::HashtagResult {
BasicRow row;
};
struct InnerWidget::SponsoredSearchResult {
Api::SponsoredSearchResult data;
RightButton button;
};
struct InnerWidget::PeerSearchResult {
explicit PeerSearchResult(not_null<PeerData*> peer) : peer(peer) {
}
not_null<PeerData*> peer;
std::unique_ptr<SponsoredSearchResult> sponsored;
mutable Ui::Text::String name;
mutable Ui::PeerBadge badge;
BasicRow row;
@ -286,6 +297,12 @@ InnerWidget::InnerWidget(
_topicJumpCache = nullptr;
_chatsFilterTags.clear();
_rightButtons.clear();
_pressedRightButtonData = nullptr;
for (const auto &result : _peerSearchResults) {
if (const auto sponsored = result->sponsored.get()) {
sponsored->button = {};
}
}
}, lifetime());
session().downloaderTaskFinished(
@ -833,13 +850,36 @@ void InnerWidget::paintEvent(QPaintEvent *e) {
bool mayBeActive) {
const auto &key = row->key();
const auto active = mayBeActive && isRowActive(row, activeEntry);
const auto forum = key.history() && key.history()->isForum();
const auto history = key.history();
const auto forum = history && history->isForum();
if (forum && !_topicJumpCache) {
_topicJumpCache = std::make_unique<Ui::TopicJumpCache>();
}
const auto expanding = forum
&& (key.history()->peer->id == childListShown.peerId);
&& (history->peer->id == childListShown.peerId);
context.rightButton = maybeCacheRightButton(row);
if (history) {
if (_activeQuickAction
&& (_activeQuickAction->data.msgBareId
== history->peer->id.value)) {
context.quickActionContext = _activeQuickAction.get();
} else if (!_inactiveQuickActions.empty()) {
auto it = _inactiveQuickActions.begin();
while (it != _inactiveQuickActions.end()) {
const auto raw = it->get();
if (raw->finishedAt
&& (ms - raw->finishedAt
> st::defaultRippleAnimation.hideDuration)) {
_inactiveQuickActions.erase(it);
} else {
if (raw->data.msgBareId == history->peer->id.value) {
context.quickActionContext = raw;
}
++it;
}
}
}
}
context.st = (forum ? &st::forumDialogRow : _st.get());
@ -864,7 +904,7 @@ void InnerWidget::paintEvent(QPaintEvent *e) {
}
if (active
&& (filter.flags() & Data::ChatFilter::Flag::NoRead)
&& !filter.contains(key.history(), true)) {
&& !filter.contains(history, true)) {
// Hack for History::fakeUnreadWhileOpened().
continue;
}
@ -919,6 +959,9 @@ void InnerWidget::paintEvent(QPaintEvent *e) {
&& _selectedTopicJump
&& (!_pressed || _pressedTopicJump);
Ui::RowPainter::Paint(p, row, validateVideoUserpic(row), context);
if (context.quickActionContext) {
context.quickActionContext = nullptr;
}
};
if (_state == WidgetState::Default) {
const auto collapsedSkip = collapsedRowsOffset();
@ -1108,15 +1151,36 @@ void InnerWidget::paintEvent(QPaintEvent *e) {
const auto activePeer = activeEntry.key.peer();
for (; from < to; ++from) {
const auto &result = _peerSearchResults[from];
if (result->sponsored
&& r.y() <= (skip + from * st::dialogsRowHeight)
&& r.y() + r.height() >= (skip + (from + 1) * st::dialogsRowHeight)) {
session().sponsoredMessages().view(
result->sponsored->data.randomId);
}
const auto peer = result->peer;
const auto active = !activeEntry.fullId
&& activePeer
&& ((peer == activePeer)
|| (peer->migrateTo() == activePeer));
const auto selected = (from == (isPressed()
const auto selected = (from == ((_peerSearchMenu >= 0)
? _peerSearchMenu
: isPressed()
? _peerSearchPressed
: _peerSearchSelected));
if (result->sponsored
&& result->sponsored->button.text.isEmpty()) {
fillRightButton(
result->sponsored->button,
tr::lng_search_sponsored_button(
tr::now,
Ui::Text::WithEntities),
st::dialogsSponsoredButton);
}
paintPeerSearchResult(p, result.get(), {
.rightButton = (result->sponsored
? &result->sponsored->button
: nullptr),
.st = &st::defaultDialogRow,
.currentBg = currentBg(),
.now = ms,
@ -1274,17 +1338,15 @@ void InnerWidget::paintEvent(QPaintEvent *e) {
}
}
[[nodiscard]] RightButton *InnerWidget::maybeCacheRightButton(Row *row) {
if (const auto user = MaybeBotWithApp(row)) {
const auto it = _rightButtons.find(user->id);
if (it == _rightButtons.end()) {
auto rightButton = RightButton();
const auto text = tr::lng_profile_open_app_short(tr::now);
rightButton.text.setText(st::dialogRowOpenBotTextStyle, text);
void InnerWidget::fillRightButton(
RightButton &button,
const TextWithEntities &text,
const style::DialogRightButton &st) {
button.st = &st;
button.text.setMarkedText(st.button.style, text);
const auto size = QSize(
rightButton.text.maxWidth()
+ rightButton.text.minHeight(),
st::dialogRowOpenBotHeight);
button.text.maxWidth() + button.text.minHeight(),
st.button.height);
const auto generateBg = [&](const style::color &c) {
auto bg = QImage(
style::DevicePixelRatio() * size,
@ -1301,9 +1363,22 @@ void InnerWidget::paintEvent(QPaintEvent *e) {
}
return bg;
};
rightButton.bg = generateBg(st::activeButtonBg);
rightButton.selectedBg = generateBg(st::activeButtonBgOver);
rightButton.activeBg = generateBg(st::activeButtonFg);
button.bg = generateBg(st.button.textBg);
button.selectedBg = generateBg(st.button.textBgOver);
button.activeBg = generateBg(st.button.textFg);
}
[[nodiscard]] RightButton *InnerWidget::maybeCacheRightButton(Row *row) {
if (const auto user = MaybeBotWithApp(row)) {
const auto it = _rightButtons.find(user->id);
if (it == _rightButtons.end()) {
auto rightButton = RightButton();
fillRightButton(
rightButton,
tr::lng_profile_open_app_short(
tr::now,
Ui::Text::WithEntities),
st::dialogRowOpenBot);
return &(_rightButtons.emplace(
user->id,
std::move(rightButton)).first->second);
@ -1426,7 +1501,11 @@ void InnerWidget::paintPeerSearchResult(
context.st->photoSize);
auto nameleft = context.st->nameLeft;
auto namewidth = context.width - nameleft - context.st->padding.right();
auto available = context.width - nameleft - context.st->padding.right();
auto namewidth = available;
if (const auto used = Ui::PaintRightButton(p, context)) {
namewidth -= used - st::dialogsUnreadPadding;
}
QRect rectForName(nameleft, context.st->nameTop, namewidth, st::semiboldFont->height);
if (result->name.isEmpty()) {
@ -1586,7 +1665,7 @@ void InnerWidget::clearIrrelevantState() {
_filteredSelected = -1;
setFilteredPressed(-1, false, false);
_peerSearchSelected = -1;
setPeerSearchPressed(-1);
setPeerSearchPressed(-1, false);
_previewSelected = -1;
setPreviewPressed(-1);
_searchedSelected = -1;
@ -1605,20 +1684,28 @@ bool InnerWidget::lookupIsInBotAppButton(
if (const auto user = MaybeBotWithApp(row)) {
const auto it = _rightButtons.find(user->id);
if (it != _rightButtons.end()) {
const auto s = it->second.bg.size() / style::DevicePixelRatio();
const auto r = QRect(
width() - s.width() - st::dialogRowOpenBotRight,
st::dialogRowOpenBotTop,
s.width(),
s.height());
if (r.contains(localPosition)) {
return true;
}
return lookupIsInRightButton(it->second, localPosition);
}
}
return false;
}
bool InnerWidget::lookupIsInRightButton(
const RightButton &button,
QPoint localPosition) {
if (!button.st) {
return false;
}
const auto s = button.bg.size() / style::DevicePixelRatio();
const auto r = QRect(
width() - s.width() - button.st->margin.right(),
button.st->margin.top(),
s.width(),
s.height());
return r.contains(localPosition);
}
void InnerWidget::selectByMouse(QPoint globalPosition) {
const auto local = mapFromGlobal(globalPosition);
if (updateReorderPinned(local)) {
@ -1662,16 +1749,16 @@ void InnerWidget::selectByMouse(QPoint globalPosition) {
const auto mappedY = selected ? mouseY - offset - selected->top() : 0;
const auto selectedTopicJump = selected
&& selected->lookupIsInTopicJump(local.x(), mappedY);
const auto selectedBotApp = selected
const auto selectedRightButton = selected
&& lookupIsInBotAppButton(selected, QPoint(local.x(), mappedY));
if (_collapsedSelected != collapsedSelected
|| _selected != selected
|| _selectedTopicJump != selectedTopicJump
|| _selectedBotApp != selectedBotApp) {
|| _selectedRightButton != selectedRightButton) {
updateSelectedRow();
_selected = selected;
_selectedTopicJump = selectedTopicJump;
_selectedBotApp = selectedBotApp;
_selectedRightButton = selectedRightButton;
_collapsedSelected = collapsedSelected;
updateSelectedRow();
setCursor((_selected || _collapsedSelected >= 0)
@ -1711,29 +1798,39 @@ void InnerWidget::selectByMouse(QPoint globalPosition) {
&& _filterResults[filteredSelected].row->lookupIsInTopicJump(
local.x(),
mappedY);
const auto selectedBotApp = (filteredSelected >= 0)
const auto selectedRightButton = (filteredSelected >= 0)
&& lookupIsInBotAppButton(
_filterResults[filteredSelected].row,
QPoint(local.x(), mappedY));
if (_filteredSelected != filteredSelected
|| _selectedTopicJump != selectedTopicJump
|| _selectedBotApp != selectedBotApp) {
|| _selectedRightButton != selectedRightButton) {
updateSelectedRow();
_filteredSelected = filteredSelected;
_selectedTopicJump = selectedTopicJump;
_selectedBotApp = selectedBotApp;
_selectedRightButton = selectedRightButton;
updateSelectedRow();
}
}
if (!_peerSearchResults.empty()) {
auto skip = peerSearchOffset();
const auto skip = peerSearchOffset();
auto peerSearchSelected = (mouseY >= skip) ? ((mouseY - skip) / st::dialogsRowHeight) : -1;
if (peerSearchSelected < 0 || peerSearchSelected >= _peerSearchResults.size()) {
peerSearchSelected = -1;
}
if (_peerSearchSelected != peerSearchSelected) {
const auto mappedY = (peerSearchSelected >= 0)
? mouseY - skip - (peerSearchSelected * st::dialogsRowHeight)
: 0;
const auto selectedRightButton = (peerSearchSelected >= 0)
&& _peerSearchResults[peerSearchSelected]->sponsored
&& lookupIsInRightButton(
_peerSearchResults[peerSearchSelected]->sponsored->button,
QPoint(local.x(), mappedY));
if (_peerSearchSelected != peerSearchSelected
|| _selectedRightButton != selectedRightButton) {
updateSelectedRow();
_peerSearchSelected = peerSearchSelected;
_selectedRightButton = selectedRightButton;
updateSelectedRow();
}
}
@ -1820,12 +1917,15 @@ void InnerWidget::mousePressEvent(QMouseEvent *e) {
selectByMouse(e->globalPos());
_pressButton = e->button();
setPressed(_selected, _selectedTopicJump, _selectedBotApp);
setPressed(_selected, _selectedTopicJump, _selectedRightButton);
setCollapsedPressed(_collapsedSelected);
setHashtagPressed(_hashtagSelected);
_hashtagDeletePressed = _hashtagDeleteSelected;
setFilteredPressed(_filteredSelected, _selectedTopicJump, _selectedBotApp);
setPeerSearchPressed(_peerSearchSelected);
setFilteredPressed(
_filteredSelected,
_selectedTopicJump,
_selectedRightButton);
setPeerSearchPressed(_peerSearchSelected, _selectedRightButton);
setPreviewPressed(_previewSelected);
setSearchedPressed(_searchedSelected);
_pressedMorePosts = _selectedMorePosts;
@ -1854,7 +1954,9 @@ void InnerWidget::mousePressEvent(QMouseEvent *e) {
};
const auto origin = e->pos()
- QPoint(0, dialogsOffset() + _pressed->top());
if (addBotAppRipple(origin, updateCallback)) {
if ((_pressButton == Qt::MiddleButton)
&& addQuickActionRipple(row, updateCallback)) {
} else if (addRightButtonRipple(origin, updateCallback)) {
} else if (_pressedTopicJump) {
row->addTopicJumpRipple(
origin,
@ -1881,7 +1983,7 @@ void InnerWidget::mousePressEvent(QMouseEvent *e) {
const auto origin = e->pos()
- QPoint(0, filteredOffset() + result.top);
const auto updateCallback = [=] { repaintDialogRow(filterId, row); };
if (addBotAppRipple(origin, updateCallback)) {
if (addRightButtonRipple(origin, updateCallback)) {
} else if (_pressedTopicJump) {
row->addTopicJumpRipple(
origin,
@ -1896,11 +1998,19 @@ void InnerWidget::mousePressEvent(QMouseEvent *e) {
}
} else if (base::in_range(_peerSearchPressed, 0, _peerSearchResults.size())) {
auto &result = _peerSearchResults[_peerSearchPressed];
auto row = &result->row;
const auto row = &result->row;
const auto origin = e->pos()
- QPoint(0, peerSearchOffset() + _peerSearchPressed * st::dialogsRowHeight);
const auto updateCallback = [this, peer = result->peer] {
updateSearchResult(peer);
};
if (addRightButtonRipple(origin, updateCallback)) {
} else {
row->addRipple(
e->pos() - QPoint(0, peerSearchOffset() + _peerSearchPressed * st::dialogsRowHeight),
origin,
QSize(width(), st::dialogsRowHeight),
[this, peer = result->peer] { updateSearchResult(peer); });
updateCallback);
}
} else if (base::in_range(_searchedPressed, 0, _searchResults.size())) {
auto &row = _searchResults[_searchedPressed];
row->addRipple(
@ -1916,22 +2026,96 @@ void InnerWidget::mousePressEvent(QMouseEvent *e) {
}
}
bool InnerWidget::addBotAppRipple(QPoint origin, Fn<void()> updateCallback) {
if (!(_pressedBotApp && _pressedBotAppData)) {
bool InnerWidget::addRightButtonRipple(QPoint origin, Fn<void()> updateCallback) {
if (!(_pressedRightButton && _pressedRightButtonData)) {
return false;
}
const auto size = _pressedBotAppData->bg.size()
const auto size = _pressedRightButtonData->bg.size()
/ style::DevicePixelRatio();
if (!_pressedBotAppData->ripple) {
_pressedBotAppData->ripple = std::make_unique<Ui::RippleAnimation>(
st::defaultRippleAnimation,
if (!_pressedRightButtonData->ripple) {
_pressedRightButtonData->ripple = std::make_unique<Ui::RippleAnimation>(
_pressedRightButtonData->st->button.ripple,
Ui::RippleAnimation::RoundRectMask(size, size.height() / 2),
updateCallback);
std::move(updateCallback));
}
const auto shift = QPoint(
width() - size.width() - st::dialogRowOpenBotRight,
st::dialogRowOpenBotTop);
_pressedBotAppData->ripple->add(origin - shift);
width() - size.width() - _pressedRightButtonData->st->margin.right(),
_pressedRightButtonData->st->margin.top());
_pressedRightButtonData->ripple->add(origin - shift);
return true;
}
bool InnerWidget::addQuickActionRipple(
not_null<Row*> row,
Fn<void()> updateCallback) {
if (_activeQuickAction) {
return false;
}
const auto action = Core::App().settings().quickDialogAction();
if (action == Dialogs::Ui::QuickDialogAction::Disabled) {
return false;
}
const auto history = row->history();
if (!history) {
return false;
}
const auto type = ResolveQuickDialogLabel(history, action, _filterId);
if (type == Dialogs::Ui::QuickDialogActionLabel::Disabled) {
return false;
}
const auto key = history->peer->id.value;
const auto context = ensureQuickAction(key);
if (context->data) {
return false;
}
auto name = ResolveQuickDialogLottieIconName(type);
context->icon = Lottie::MakeIcon({
.name = std::move(name),
.sizeOverride = Size(st::dialogsQuickActionSize),
});
context->action = action;
context->icon->jumpTo(context->icon->framesCount() - 1, [=] {
const auto size = QSize(
st::dialogsQuickActionRippleSize,
row->height());
const auto isRemovingFromList
= (action == Dialogs::Ui::QuickDialogAction::Archive);
if (!context->ripple) {
context->ripple = std::make_unique<Ui::RippleAnimation>(
st::defaultRippleAnimation,
Ui::RippleAnimation::RectMask(size),
isRemovingFromList
? Fn<void()>([=] { update(); })
: updateCallback);
}
if (!context->rippleFg) {
context->rippleFg = std::make_unique<Ui::RippleAnimation>(
st::defaultRippleAnimation,
Ui::RippleAnimation::MaskByDrawer(
size,
true,
[&](QPainter &p) {
p.setCompositionMode(
QPainter::CompositionMode_Source);
p.fillRect(Rect(size), Qt::transparent);
DrawQuickAction(
p,
Rect(size),
context->icon.get(),
ResolveQuickDialogLabel(
row->history(),
action,
_filterId));
}),
isRemovingFromList
? Fn<void()>([=] { update(); })
: std::move(updateCallback));
}
context->ripple->add(QPoint(size.width() / 2, size.height() / 2));
context->rippleFg->add(QPoint(size.width() / 2, size.height() / 2));
});
return true;
}
@ -1950,12 +2134,17 @@ void InnerWidget::checkReorderPinnedStart(QPoint localPosition) {
if (!_pressed
|| _dragging
|| (_state != WidgetState::Default)
|| _pressedBotApp) {
|| _pressedRightButtonData) {
return;
} else if (qAbs(localPosition.y() - _dragStart.y())
< style::ConvertScale(kStartReorderThreshold)) {
return;
}
if ((_pressButton == Qt::MiddleButton)
&& (Core::App().settings().quickDialogAction()
!= Dialogs::Ui::QuickDialogAction::Disabled)) {
return;
}
_dragging = _pressed;
startReorderPinned(localPosition);
}
@ -2215,7 +2404,7 @@ void InnerWidget::mousePressReleased(
setCollapsedPressed(-1);
const auto pressedTopicRootId = _pressedTopicJumpRootId;
const auto pressedTopicJump = _pressedTopicJump;
const auto pressedBotApp = _pressedBotApp;
const auto pressedRightButton = _pressedRightButton;
auto pressed = _pressed;
clearPressed();
auto hashtagPressed = _hashtagPressed;
@ -2225,7 +2414,7 @@ void InnerWidget::mousePressReleased(
auto filteredPressed = _filteredPressed;
setFilteredPressed(-1, false, false);
auto peerSearchPressed = _peerSearchPressed;
setPeerSearchPressed(-1);
setPeerSearchPressed(-1, false);
auto previewPressed = _previewPressed;
setPreviewPressed(-1);
auto searchedPressed = _searchedPressed;
@ -2237,8 +2426,28 @@ void InnerWidget::mousePressReleased(
if (wasDragging) {
selectByMouse(globalPosition);
}
if (_pressedBotAppData && _pressedBotAppData->ripple) {
_pressedBotAppData->ripple->lastStop();
if (_pressedRightButtonData && _pressedRightButtonData->ripple) {
_pressedRightButtonData->ripple->lastStop();
}
if (_activeQuickAction && pressed && !_activeQuickAction->data) {
if (const auto history = pressed->history()) {
const auto raw = _activeQuickAction.get();
if (raw->ripple) {
raw->ripple->lastStop();
}
if (raw->rippleFg) {
raw->rippleFg->lastStop();
}
if (pressed == _selected) {
PerformQuickDialogAction(
_controller,
history->peer,
raw->action,
_filterId);
}
deactivateQuickAction();
}
}
updateSelectedRow();
if (!wasDragging && button == Qt::LeftButton) {
@ -2246,13 +2455,14 @@ void InnerWidget::mousePressReleased(
|| (pressed
&& pressed == _selected
&& pressedTopicJump == _selectedTopicJump
&& pressedBotApp == _selectedBotApp)
&& pressedRightButton == _selectedRightButton)
|| (hashtagPressed >= 0
&& hashtagPressed == _hashtagSelected
&& hashtagDeletePressed == _hashtagDeleteSelected)
|| (filteredPressed >= 0 && filteredPressed == _filteredSelected)
|| (peerSearchPressed >= 0
&& peerSearchPressed == _peerSearchSelected)
&& peerSearchPressed == _peerSearchSelected
&& pressedRightButton == _selectedRightButton)
|| (previewPressed >= 0
&& previewPressed == _previewSelected)
|| (searchedPressed >= 0
@ -2261,13 +2471,15 @@ void InnerWidget::mousePressReleased(
&& pressedMorePosts == _selectedMorePosts)
|| (pressedChatTypeFilter
&& pressedChatTypeFilter == _selectedChatTypeFilter)) {
if (pressedBotApp && (pressed || filteredPressed >= 0)) {
if (pressedRightButton && (pressed || filteredPressed >= 0)) {
const auto &row = pressed
? pressed
: _filterResults[filteredPressed].row.get();
if (const auto user = MaybeBotWithApp(row)) {
_openBotMainAppRequests.fire(peerToUser(user->id));
}
} else if (pressedRightButton && peerSearchPressed >= 0) {
showSponsoredMenu(peerSearchPressed, globalPosition);
} else {
chooseRow(modifiers, pressedTopicRootId);
}
@ -2294,25 +2506,25 @@ void InnerWidget::setCollapsedPressed(int pressed) {
void InnerWidget::setPressed(
Row *pressed,
bool pressedTopicJump,
bool pressedBotApp) {
bool pressedRightButton) {
if ((_pressed != pressed)
|| (pressed && _pressedTopicJump != pressedTopicJump)
|| (pressed && _pressedBotApp != pressedBotApp)) {
|| (pressed && _pressedRightButton != pressedRightButton)) {
if (_pressed) {
_pressed->stopLastRipple();
}
if (_pressedBotAppData && _pressedBotAppData->ripple) {
_pressedBotAppData->ripple->lastStop();
if (_pressedRightButtonData && _pressedRightButtonData->ripple) {
_pressedRightButtonData->ripple->lastStop();
}
_pressed = pressed;
if (pressed || !pressedTopicJump || !pressedBotApp) {
if (pressed || !pressedTopicJump || !pressedRightButton) {
_pressedTopicJump = pressedTopicJump;
_pressedBotApp = pressedBotApp;
if (pressedBotApp) {
_pressedRightButton = pressedRightButton;
if (pressedRightButton) {
if (const auto user = MaybeBotWithApp(pressed)) {
const auto it = _rightButtons.find(user->id);
if (it != _rightButtons.end()) {
_pressedBotAppData = &(it->second);
_pressedRightButtonData = &(it->second);
}
}
}
@ -2339,26 +2551,26 @@ void InnerWidget::setHashtagPressed(int pressed) {
void InnerWidget::setFilteredPressed(
int pressed,
bool pressedTopicJump,
bool pressedBotApp) {
bool pressedRightButton) {
if (_filteredPressed != pressed
|| (pressed >= 0 && _pressedTopicJump != pressedTopicJump)
|| (pressed >= 0 && _pressedBotApp != pressedBotApp)) {
|| (pressed >= 0 && _pressedRightButton != pressedRightButton)) {
if (base::in_range(_filteredPressed, 0, _filterResults.size())) {
_filterResults[_filteredPressed].row->stopLastRipple();
}
if (_pressedBotAppData && _pressedBotAppData->ripple) {
_pressedBotAppData->ripple->lastStop();
if (_pressedRightButtonData && _pressedRightButtonData->ripple) {
_pressedRightButtonData->ripple->lastStop();
}
_filteredPressed = pressed;
if (pressed >= 0 || !pressedTopicJump || !pressedBotApp) {
if (pressed >= 0 || !pressedTopicJump || !pressedRightButton) {
_pressedTopicJump = pressedTopicJump;
_pressedBotApp = pressedBotApp;
if (pressed >= 0 && pressedBotApp) {
_pressedRightButton = pressedRightButton;
if (pressed >= 0 && pressedRightButton) {
const auto &row = _filterResults[pressed].row;
if (const auto history = row->history()) {
const auto it = _rightButtons.find(history->peer->id);
if (it != _rightButtons.end()) {
_pressedBotAppData = &(it->second);
_pressedRightButtonData = &(it->second);
}
}
}
@ -2371,11 +2583,26 @@ void InnerWidget::setFilteredPressed(
}
}
void InnerWidget::setPeerSearchPressed(int pressed) {
void InnerWidget::setPeerSearchPressed(int pressed, bool pressedRightButton) {
if (_peerSearchPressed != pressed
|| (pressed >= 0 && _pressedRightButton != pressedRightButton)) {
if (base::in_range(_peerSearchPressed, 0, _peerSearchResults.size())) {
_peerSearchResults[_peerSearchPressed]->row.stopLastRipple();
}
if (_pressedRightButtonData && _pressedRightButtonData->ripple) {
_pressedRightButtonData->ripple->lastStop();
}
_peerSearchPressed = pressed;
if (pressed >= 0 || !pressedRightButton) {
_pressedRightButton = pressedRightButton;
if (pressed >= 0 && pressedRightButton) {
const auto &entry = _peerSearchResults[pressed];
if (entry->sponsored) {
_pressedRightButtonData = &entry->sponsored->button;
}
}
}
}
}
void InnerWidget::setPreviewPressed(int pressed) {
@ -2438,7 +2665,7 @@ void InnerWidget::dialogRowReplaced(
_selected = newRow;
}
if (_pressed == oldRow) {
setPressed(newRow, _pressedTopicJump, _pressedBotApp);
setPressed(newRow, _pressedTopicJump, _pressedRightButton);
}
if (_dragging == oldRow) {
if (newRow) {
@ -2836,6 +3063,11 @@ bool InnerWidget::showChatPreview() {
void InnerWidget::chatPreviewShown(bool shown, RowDescriptor row) {
_chatPreviewScheduled = false;
if (shown) {
const auto chosen = computeChosenRow();
if (!chosen.sponsoredRandomId.isEmpty() && row.key == chosen.key) {
auto &messages = session().sponsoredMessages();
messages.clicked(chosen.sponsoredRandomId, false, false);
}
_chatPreviewRow = row;
if (base::take(_chatPreviewTouchGlobal)) {
_touchCancelRequests.fire({});
@ -2950,6 +3182,62 @@ void InnerWidget::contextMenuEvent(QContextMenuEvent *e) {
}
}
void InnerWidget::showSponsoredMenu(int peerSearchIndex, QPoint globalPos) {
_menu = nullptr;
const auto count = int(_peerSearchResults.size());
const auto entry = (peerSearchIndex >= 0 && peerSearchIndex < count)
? _peerSearchResults[peerSearchIndex].get()
: nullptr;
if (!entry || !entry->sponsored) {
return;
}
_peerSearchMenu = peerSearchIndex;
_menu = base::make_unique_q<Ui::PopupMenu>(
this,
st::popupMenuExpandedSeparator);
const auto peer = entry->peer;
const auto remove = crl::guard(this, [=] {
_sponsoredRemoved.emplace(peer);
_peerSearchResults.erase(
ranges::remove(
_peerSearchResults,
peer,
&PeerSearchResult::peer),
end(_peerSearchResults));
refresh();
});
Menu::FillSponsored(
this,
Ui::Menu::CreateAddActionCallback(_menu),
_controller->uiShow(),
Menu::SponsoredPhrases::Search,
session().sponsoredMessages().lookupDetails(entry->sponsored->data),
session().sponsoredMessages().createReportCallback(
entry->sponsored->data.randomId,
remove),
false,
false);
QObject::connect(_menu.get(), &QObject::destroyed, [=] {
if (_peerSearchMenu >= 0
&& _peerSearchMenu < _peerSearchResults.size()) {
const auto index = std::exchange(_peerSearchMenu, -1);
updateSearchResult(_peerSearchResults[index]->peer);
}
const auto globalPosition = QCursor::pos();
if (rect().contains(mapFromGlobal(globalPosition))) {
setMouseTracking(true);
selectByMouse(globalPosition);
}
});
if (_menu->empty()) {
_menu = nullptr;
} else {
_menu->popup(globalPos);
}
}
void InnerWidget::parentGeometryChanged() {
const auto globalPosition = QCursor::pos();
if (rect().contains(mapFromGlobal(globalPosition))) {
@ -2984,7 +3272,7 @@ bool InnerWidget::processTouchEvent(not_null<QTouchEvent*> e) {
}
if (_chatPreviewTouchGlobal) {
const auto delta = (*_chatPreviewTouchGlobal - *point);
if (delta.manhattanLength() > _st->photoSize) {
if (delta.manhattanLength() >= QApplication::startDragDistance()) {
cancelChatPreview();
}
}
@ -2993,7 +3281,7 @@ bool InnerWidget::processTouchEvent(not_null<QTouchEvent*> e) {
return _dragging != nullptr;
} else if (_touchDragStartGlobal) {
const auto delta = (*_touchDragStartGlobal - *point);
if (delta.manhattanLength() > QApplication::startDragDistance()) {
if (delta.manhattanLength() >= QApplication::startDragDistance()) {
if (_touchDragPinnedTimer.isActive()) {
_touchDragPinnedTimer.cancel();
_touchDragStartGlobal = {};
@ -3231,14 +3519,23 @@ InnerWidget::~InnerWidget() {
clearSearchResults();
}
void InnerWidget::clearSearchResults(bool clearPeerSearchResults) {
if (clearPeerSearchResults) {
_peerSearchResults.clear();
void InnerWidget::clearSearchResults(bool alsoPeerSearchResults) {
if (alsoPeerSearchResults) {
clearPeerSearchResults();
}
_searchResults.clear();
_searchedCount = _searchedMigratedCount = 0;
}
void InnerWidget::clearPeerSearchResults() {
_peerSearchResults.clear();
if (_pressedRightButtonSponsored) {
_pressedRightButtonData = nullptr;
_pressedRightButtonSponsored = false;
_pressedRightButton = false;
}
}
void InnerWidget::clearPreviewResults() {
_previewResults.clear();
_previewCount = 0;
@ -3554,40 +3851,45 @@ void InnerWidget::searchReceived(
refresh();
}
void InnerWidget::peerSearchReceived(
const QString &query,
const QVector<MTPPeer> &my,
const QVector<MTPPeer> &result) {
void InnerWidget::peerSearchReceived(Api::PeerSearchResult result) {
if (_state != WidgetState::Filtered) {
return;
}
_peerSearchQuery = query.toLower().trimmed();
_peerSearchResults.clear();
_peerSearchResults.reserve(result.size());
for (const auto &mtpPeer : my) {
if (const auto peer = session().data().peerLoaded(peerFromMTP(mtpPeer))) {
_peerSearchQuery = result.query.toLower().trimmed();
clearPeerSearchResults();
_peerSearchResults.reserve(result.peers.size()
+ result.sponsored.size());
for (const auto &peer : result.my) {
appendToFiltered(peer->owner().history(peer));
} else {
LOG(("API Error: "
"user %1 was not loaded in InnerWidget::peopleReceived()"
).arg(peerFromMTP(mtpPeer).value));
}
}
for (const auto &mtpPeer : result) {
if (const auto peer = session().data().peerLoaded(peerFromMTP(mtpPeer))) {
const auto inlist = [&](not_null<PeerData*> peer) {
if (const auto history = peer->owner().historyLoaded(peer)) {
if (history->inChatList()) {
continue; // skip existing chats
// Skip existing chats.
return history->inChatList();
}
return false;
};
auto added = base::flat_set<not_null<PeerData*>>();
for (const auto &sponsored : result.sponsored) {
const auto peer = sponsored.peer;
if (inlist(peer) || _sponsoredRemoved.contains(peer)) {
continue;
}
_peerSearchResults.push_back(std::make_unique<PeerSearchResult>(
peer));
} else {
LOG(("API Error: "
"user %1 was not loaded in InnerWidget::peopleReceived()"
).arg(peerFromMTP(mtpPeer).value));
_peerSearchResults.push_back(
std::make_unique<PeerSearchResult>(peer));
_peerSearchResults.back()->sponsored
= std::make_unique<SponsoredSearchResult>(SponsoredSearchResult{
.data = sponsored,
});
added.emplace(peer);
}
for (const auto &peer : result.peers) {
if (added.contains(peer) || inlist(peer)) {
continue;
}
_peerSearchResults.push_back(
std::make_unique<PeerSearchResult>(peer));
}
refresh();
}
@ -3921,7 +4223,7 @@ void InnerWidget::clearFilter() {
_hashtagResults.clear();
_filterResults.clear();
_filterResultsGlobal.clear();
_peerSearchResults.clear();
clearPeerSearchResults();
_searchResults.clear();
_previewResults.clear();
_trackedHistories.clear();
@ -4377,10 +4679,13 @@ ChosenRow InnerWidget::computeChosenRow() const {
.filteredRow = true,
};
} else if (base::in_range(_peerSearchSelected, 0, _peerSearchResults.size())) {
const auto peer = _peerSearchResults[_peerSearchSelected]->peer;
const auto row = _peerSearchResults[_peerSearchSelected].get();
return {
.key = session().data().history(peer),
.message = Data::UnreadMessagePosition
.key = session().data().history(row->peer),
.message = Data::UnreadMessagePosition,
.sponsoredRandomId = (row->sponsored
? row->sponsored->data.randomId
: QByteArray()),
};
} else if (base::in_range(_previewSelected, 0, _previewResults.size())) {
const auto result = _previewResults[_previewSelected].get();
@ -5004,4 +5309,100 @@ rpl::producer<UserId> InnerWidget::openBotMainAppRequests() const {
return _openBotMainAppRequests.events();
}
void InnerWidget::setSwipeContextData(
int64 key,
std::optional<Ui::Controls::SwipeContextData> data) {
if (!key) {
return;
}
if (!data) {
_activeQuickAction = nullptr;
return;
}
const auto context = ensureQuickAction(key);
context->data = base::take(*data);
if (context->data.msgBareId) {
constexpr auto kStartAnimateThreshold = 0.32;
constexpr auto kResetAnimateThreshold = 0.24;
if (context->data.ratio > kStartAnimateThreshold) {
if (context->icon
&& !context->icon->frameIndex()
&& !context->icon->animating()) {
context->icon->animate(
[=] { update(); },
0,
context->icon->framesCount());
}
} else if (context->data.ratio < kResetAnimateThreshold) {
if (context->icon
&& context->icon->frameIndex()) {
context->icon->jumpTo(0, [=] { update(); });
}
}
update();
}
}
not_null<Ui::QuickActionContext*> InnerWidget::ensureQuickAction(int64 key) {
Expects(key != 0);
if (_activeQuickAction) {
if (_activeQuickAction->data.msgBareId == key) {
return _activeQuickAction.get();
} else {
deactivateQuickAction();
}
}
_activeQuickAction = std::make_unique<Ui::QuickActionContext>();
_activeQuickAction->data.msgBareId = key;
return _activeQuickAction.get();
}
int64 InnerWidget::calcSwipeKey(int top) {
top -= dialogsOffset();
for (auto it = _shownList->begin(); it != _shownList->end(); ++it) {
const auto row = it->get();
const auto from = row->top();
const auto to = from + row->height();
if (top >= from && top < to) {
if (const auto peer = row->key().peer()) {
return peer->id.value;
}
return 0;
}
}
return 0;
}
void InnerWidget::prepareQuickAction(
int64 key,
Dialogs::Ui::QuickDialogAction action) {
Expects(key != 0);
const auto context = ensureQuickAction(key);
auto name = ResolveQuickDialogLottieIconName(
ResolveQuickDialogLabel(
session().data().history(PeerId(key)),
action,
_filterId));
context->icon = Lottie::MakeIcon({
.name = std::move(name),
.sizeOverride = Size(st::dialogsQuickActionSize),
});
context->action = action;
}
void InnerWidget::clearQuickActions() {
_inactiveQuickActions.clear();
}
void InnerWidget::deactivateQuickAction() {
if (_activeQuickAction) {
_activeQuickAction->finishedAt = crl::now();
_inactiveQuickActions.push_back(
QuickActionPtr{ _activeQuickAction.release() });
}
}
} // namespace Dialogs

View file

@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/object_ptr.h"
#include "base/timer.h"
#include "dialogs/dialogs_key.h"
#include "dialogs/ui/dialogs_quick_action_context.h"
#include "data/data_messages.h"
#include "ui/dragging_scroll_manager.h"
#include "ui/effects/animations.h"
@ -19,12 +20,21 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace style {
struct DialogRow;
struct DialogRightButton;
} // namespace style
namespace Api {
struct PeerSearchResult;
} // namespace Api
namespace MTP {
class Error;
} // namespace MTP
namespace Lottie {
class Icon;
} // namespace Lottie
namespace Main {
class Session;
} // namespace Main
@ -34,6 +44,9 @@ class IconButton;
class PopupMenu;
class FlatLabel;
struct ScrollToRequest;
namespace Controls {
enum class QuickDialogAction;
} // namespace Controls
} // namespace Ui
namespace Window {
@ -70,6 +83,7 @@ enum class ChatTypeFilter : uchar;
struct ChosenRow {
Key key;
Data::MessagePosition message;
QByteArray sponsoredRandomId;
bool userpicClick : 1 = false;
bool filteredRow : 1 = false;
bool newWindow : 1 = false;
@ -118,10 +132,7 @@ public:
HistoryItem *inject,
SearchRequestType type,
int fullCount);
void peerSearchReceived(
const QString &query,
const QVector<MTPPeer> &my,
const QVector<MTPPeer> &result);
void peerSearchReceived(Api::PeerSearchResult result);
[[nodiscard]] FilterId filterId() const;
@ -208,6 +219,13 @@ public:
[[nodiscard]] rpl::producer<UserId> openBotMainAppRequests() const;
void setSwipeContextData(
int64 key,
std::optional<Ui::Controls::SwipeContextData> data);
[[nodiscard]] int64 calcSwipeKey(int top);
void prepareQuickAction(int64 key, Dialogs::Ui::QuickDialogAction);
void clearQuickActions();
protected:
void visibleTopBottomUpdated(
int visibleTop,
@ -225,6 +243,7 @@ protected:
private:
struct CollapsedRow;
struct HashtagResult;
struct SponsoredSearchResult;
struct PeerSearchResult;
struct TagCache;
@ -282,6 +301,7 @@ private:
void repaintDialogRow(RowDescriptor row);
void refreshDialogRow(RowDescriptor row);
bool updateEntryHeight(not_null<Entry*> entry);
void showSponsoredMenu(int peerSearchIndex, QPoint globalPos);
void clearMouseSelection(bool clearSelection = false);
void mousePressReleased(
@ -295,14 +315,17 @@ private:
void scrollToItem(int top, int height);
void scrollToDefaultSelected();
void setCollapsedPressed(int pressed);
void setPressed(Row *pressed, bool pressedTopicJump, bool pressedBotApp);
void setPressed(
Row *pressed,
bool pressedTopicJump,
bool pressedRightButton);
void clearPressed();
void setHashtagPressed(int pressed);
void setFilteredPressed(
int pressed,
bool pressedTopicJump,
bool pressedBotApp);
void setPeerSearchPressed(int pressed);
bool pressedRightButton);
void setPeerSearchPressed(int pressed, bool pressedRightButton);
void setPreviewPressed(int pressed);
void setSearchedPressed(int pressed);
bool isPressed() const {
@ -338,6 +361,9 @@ private:
void repaintDialogRowCornerStatus(not_null<History*> history);
bool addBotAppRipple(QPoint origin, Fn<void()> updateCallback);
bool addQuickActionRipple(not_null<Row*> row, Fn<void()> updateCallback);
bool addRightButtonRipple(QPoint origin, Fn<void()> updateCallback);
void setupShortcuts();
RowDescriptor computeJump(
@ -441,7 +467,8 @@ private:
Ui::VideoUserpic *validateVideoUserpic(not_null<History*> history);
Row *shownRowByKey(Key key);
void clearSearchResults(bool clearPeerSearchResults = true);
void clearSearchResults(bool alsoPeerSearchResults = true);
void clearPeerSearchResults();
void clearPreviewResults();
void updateSelectedRow(Key key = Key());
void trackResultsHistory(not_null<History*> history);
@ -468,10 +495,21 @@ private:
void saveChatsFilterScrollState(FilterId filterId);
void restoreChatsFilterScrollState(FilterId filterId);
[[nodiscard]] not_null<Ui::QuickActionContext*> ensureQuickAction(
int64 key);
void deactivateQuickAction();
[[nodiscard]] bool lookupIsInBotAppButton(
Row *row,
QPoint localPosition);
[[nodiscard]] bool lookupIsInRightButton(
const RightButton &button,
QPoint localPosition);
[[nodiscard]] RightButton *maybeCacheRightButton(Row *row);
void fillRightButton(
RightButton &button,
const TextWithEntities &text,
const style::DialogRightButton &st);
[[nodiscard]] QImage *cacheChatsFilterTag(
const Data::ChatFilter &filter,
@ -507,9 +545,10 @@ private:
bool _selectedTopicJump = false;
bool _pressedTopicJump = false;
RightButton *_pressedBotAppData = nullptr;
bool _selectedBotApp = false;
bool _pressedBotApp = false;
RightButton *_pressedRightButtonData = nullptr;
bool _pressedRightButtonSponsored = false;
bool _selectedRightButton = false;
bool _pressedRightButton = false;
Row *_dragging = nullptr;
int _draggingIndex = -1;
@ -544,9 +583,11 @@ private:
rpl::lifetime _trackedLifetime;
QString _peerSearchQuery;
base::flat_set<not_null<PeerData*>> _sponsoredRemoved;
std::vector<std::unique_ptr<PeerSearchResult>> _peerSearchResults;
int _peerSearchSelected = -1;
int _peerSearchPressed = -1;
int _peerSearchMenu = -1;
std::vector<std::unique_ptr<FakeRow>> _previewResults;
int _previewCount = 0;
@ -611,6 +652,10 @@ private:
rpl::event_stream<> _refreshHashtagsRequests;
rpl::event_stream<UserId> _openBotMainAppRequests;
using QuickActionPtr = std::unique_ptr<Ui::QuickActionContext>;
QuickActionPtr _activeQuickAction;
std::vector<QuickActionPtr> _inactiveQuickActions;
RowDescriptor _chatPreviewRow;
bool _chatPreviewScheduled = false;
std::optional<QPoint> _chatPreviewTouchGlobal;

View file

@ -198,7 +198,7 @@ Row *List::rowAtY(int y) const {
List::iterator List::findByY(int y) const {
return ranges::lower_bound(_rows, y, ranges::less(), [](const Row *row) {
return row->top() + row->height();
return row->top() + row->height() - 1;
});
}

View file

@ -0,0 +1,245 @@
/*
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 "dialogs/dialogs_quick_action.h"
#include "dialogs/ui/dialogs_quick_action_context.h"
#include "apiwrap.h"
#include "data/data_histories.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "dialogs/dialogs_entry.h"
#include "history/history.h"
#include "lang/lang_instance.h"
#include "lang/lang_keys.h"
#include "lottie/lottie_icon.h"
#include "main/main_session.h"
#include "menu/menu_mute.h"
#include "window/window_peer_menu.h"
#include "window/window_session_controller.h"
#include "styles/style_dialogs.h"
namespace Dialogs {
namespace {
const style::font &SwipeActionFont(
Dialogs::Ui::QuickDialogActionLabel action,
int availableWidth) {
struct Entry final {
Dialogs::Ui::QuickDialogActionLabel action;
QString langId;
style::font font;
};
static auto Fonts = std::vector<Entry>();
for (auto &entry : Fonts) {
if (entry.action == action) {
if (entry.langId == Lang::GetInstance().id()) {
return entry.font;
}
}
}
constexpr auto kNormalFontSize = 13;
constexpr auto kMinFontSize = 5;
for (auto i = kNormalFontSize; i >= kMinFontSize; --i) {
auto font = style::font(
style::ConvertScale(i, style::Scale()),
st::semiboldFont->flags(),
st::semiboldFont->family());
if (font->width(ResolveQuickDialogLabel(action)) <= availableWidth
|| i == kMinFontSize) {
Fonts.emplace_back(Entry{
.action = action,
.langId = Lang::GetInstance().id(),
.font = std::move(font),
});
return Fonts.back().font;
}
}
Unexpected("SwipeActionFont: can't find font.");
}
} // namespace
void PerformQuickDialogAction(
not_null<Window::SessionController*> controller,
not_null<PeerData*> peer,
Ui::QuickDialogAction action,
FilterId filterId) {
const auto history = peer->owner().history(peer);
if (action == Dialogs::Ui::QuickDialogAction::Mute) {
const auto isMuted = rpl::variable<bool>(
MuteMenu::ThreadDescriptor(history).isMutedValue()).current();
MuteMenu::ThreadDescriptor(history).updateMutePeriod(isMuted
? 0
: std::numeric_limits<TimeId>::max());
} else if (action == Dialogs::Ui::QuickDialogAction::Pin) {
const auto entry = (Dialogs::Entry*)(history);
Window::TogglePinnedThread(controller, entry, filterId);
} else if (action == Dialogs::Ui::QuickDialogAction::Read) {
if (Window::IsUnreadThread(history)) {
Window::MarkAsReadThread(history);
} else if (history) {
peer->owner().histories().changeDialogUnreadMark(history, true);
}
} else if (action == Dialogs::Ui::QuickDialogAction::Archive) {
history->session().api().toggleHistoryArchived(
history,
!Window::IsArchived(history),
[] {});
} else if (action == Dialogs::Ui::QuickDialogAction::Delete) {
Window::DeleteAndLeaveHandler(controller, peer)();
}
}
QString ResolveQuickDialogLottieIconName(Ui::QuickDialogActionLabel action) {
switch (action) {
case Ui::QuickDialogActionLabel::Mute:
return u"swipe_mute"_q;
case Ui::QuickDialogActionLabel::Unmute:
return u"swipe_unmute"_q;
case Ui::QuickDialogActionLabel::Pin:
return u"swipe_pin"_q;
case Ui::QuickDialogActionLabel::Unpin:
return u"swipe_unpin"_q;
case Ui::QuickDialogActionLabel::Read:
return u"swipe_read"_q;
case Ui::QuickDialogActionLabel::Unread:
return u"swipe_unread"_q;
case Ui::QuickDialogActionLabel::Archive:
return u"swipe_archive"_q;
case Ui::QuickDialogActionLabel::Unarchive:
return u"swipe_unarchive"_q;
case Ui::QuickDialogActionLabel::Delete:
return u"swipe_delete"_q;
default:
return u"swipe_disabled"_q;
}
}
Ui::QuickDialogActionLabel ResolveQuickDialogLabel(
not_null<History*> history,
Ui::QuickDialogAction action,
FilterId filterId) {
if (action == Dialogs::Ui::QuickDialogAction::Mute) {
if (history->peer->isSelf()) {
return Ui::QuickDialogActionLabel::Disabled;
}
const auto isMuted = rpl::variable<bool>(
MuteMenu::ThreadDescriptor(history).isMutedValue()).current();
return isMuted
? Ui::QuickDialogActionLabel::Unmute
: Ui::QuickDialogActionLabel::Mute;
} else if (action == Dialogs::Ui::QuickDialogAction::Pin) {
const auto entry = (Dialogs::Entry*)(history);
return entry->isPinnedDialog(filterId)
? Ui::QuickDialogActionLabel::Unpin
: Ui::QuickDialogActionLabel::Pin;
} else if (action == Dialogs::Ui::QuickDialogAction::Read) {
const auto unread = Window::IsUnreadThread(history);
if (history->isForum() && !unread) {
return Ui::QuickDialogActionLabel::Disabled;
}
return unread
? Ui::QuickDialogActionLabel::Read
: Ui::QuickDialogActionLabel::Unread;
} else if (action == Dialogs::Ui::QuickDialogAction::Archive) {
if (!Window::CanArchive(history, history->peer)) {
return Ui::QuickDialogActionLabel::Disabled;
}
return Window::IsArchived(history)
? Ui::QuickDialogActionLabel::Unarchive
: Ui::QuickDialogActionLabel::Archive;
} else if (action == Dialogs::Ui::QuickDialogAction::Delete) {
return Ui::QuickDialogActionLabel::Delete;
}
return Ui::QuickDialogActionLabel::Disabled;
}
QString ResolveQuickDialogLabel(Ui::QuickDialogActionLabel action) {
switch (action) {
case Ui::QuickDialogActionLabel::Mute:
return tr::lng_settings_quick_dialog_action_mute(tr::now);
case Ui::QuickDialogActionLabel::Unmute:
return tr::lng_settings_quick_dialog_action_unmute(tr::now);
case Ui::QuickDialogActionLabel::Pin:
return tr::lng_settings_quick_dialog_action_pin(tr::now);
case Ui::QuickDialogActionLabel::Unpin:
return tr::lng_settings_quick_dialog_action_unpin(tr::now);
case Ui::QuickDialogActionLabel::Read:
return tr::lng_settings_quick_dialog_action_read(tr::now);
case Ui::QuickDialogActionLabel::Unread:
return tr::lng_settings_quick_dialog_action_unread(tr::now);
case Ui::QuickDialogActionLabel::Archive:
return tr::lng_settings_quick_dialog_action_archive(tr::now);
case Ui::QuickDialogActionLabel::Unarchive:
return tr::lng_settings_quick_dialog_action_unarchive(tr::now);
case Ui::QuickDialogActionLabel::Delete:
return tr::lng_settings_quick_dialog_action_delete(tr::now);
default:
return tr::lng_settings_quick_dialog_action_disabled(tr::now);
};
}
const style::color &ResolveQuickActionBg(
Ui::QuickDialogActionLabel action) {
switch (action) {
case Ui::QuickDialogActionLabel::Delete:
return st::attentionButtonFg;
case Ui::QuickDialogActionLabel::Disabled:
return st::windowSubTextFgOver;
case Ui::QuickDialogActionLabel::Mute:
case Ui::QuickDialogActionLabel::Unmute:
case Ui::QuickDialogActionLabel::Pin:
case Ui::QuickDialogActionLabel::Unpin:
case Ui::QuickDialogActionLabel::Read:
case Ui::QuickDialogActionLabel::Unread:
case Ui::QuickDialogActionLabel::Archive:
case Ui::QuickDialogActionLabel::Unarchive:
default:
return st::windowBgActive;
};
}
const style::color &ResolveQuickActionBgActive(
Ui::QuickDialogActionLabel action) {
return st::windowSubTextFgOver;
}
void DrawQuickAction(
QPainter &p,
const QRect &rect,
not_null<Lottie::Icon*> icon,
Ui::QuickDialogActionLabel label,
float64 iconRatio,
bool twoLines) {
const auto iconSize = st::dialogsQuickActionSize * iconRatio;
const auto innerHeight = iconSize * 2;
const auto top = (rect.height() - innerHeight) / 2;
icon->paint(p, rect.x() + (rect.width() - iconSize) / 2, top);
p.setPen(st::premiumButtonFg);
p.setBrush(Qt::NoBrush);
const auto availableWidth = rect.width();
p.setFont(SwipeActionFont(label, availableWidth));
if (twoLines) {
auto text = ResolveQuickDialogLabel(label);
const auto index = text.indexOf(' ');
if (index != -1) {
text = text.replace(index, 1, '\n');
}
p.drawText(
QRect(rect.x(), top, availableWidth, innerHeight),
std::move(text),
style::al_bottom);
} else {
p.drawText(
QRect(rect.x(), top, availableWidth, innerHeight),
ResolveQuickDialogLabel(label),
style::al_bottom);
}
}
} // namespace Dialogs

View file

@ -0,0 +1,57 @@
/*
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
class History;
class PeerData;
namespace Dialogs::Ui {
enum class QuickDialogAction;
enum class QuickDialogActionLabel;
} // namespace Dialogs::Ui
namespace Lottie {
class Icon;
} // namespace Lottie
namespace Window {
class SessionController;
} // namespace Window
namespace Dialogs {
void PerformQuickDialogAction(
not_null<Window::SessionController*> controller,
not_null<PeerData*> peer,
Ui::QuickDialogAction action,
FilterId filterId);
[[nodiscard]] QString ResolveQuickDialogLottieIconName(
Ui::QuickDialogActionLabel action);
[[nodiscard]] Ui::QuickDialogActionLabel ResolveQuickDialogLabel(
not_null<History*> history,
Ui::QuickDialogAction action,
FilterId filterId);
[[nodiscard]] QString ResolveQuickDialogLabel(Ui::QuickDialogActionLabel);
[[nodiscard]] const style::color &ResolveQuickActionBg(
Ui::QuickDialogActionLabel);
[[nodiscard]] const style::color &ResolveQuickActionBgActive(
Ui::QuickDialogActionLabel);
void DrawQuickAction(
QPainter &p,
const QRect &rect,
not_null<Lottie::Icon*> icon,
Ui::QuickDialogActionLabel label,
float64 iconRatio = 1.,
bool twoLines = false);
} // namespace Dialogs

View file

@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "dialogs/dialogs_widget.h"
#include "base/call_delayed.h"
#include "base/qt/qt_key_modifiers.h"
#include "base/options.h"
#include "dialogs/ui/chat_search_in.h"
@ -15,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "dialogs/ui/dialogs_suggestions.h"
#include "dialogs/dialogs_inner_widget.h"
#include "dialogs/dialogs_search_from_controllers.h"
#include "dialogs/dialogs_quick_action.h"
#include "dialogs/dialogs_key.h"
#include "history/history.h"
#include "history/history_item.h"
@ -35,6 +37,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/chat/more_chats_bar.h"
#include "ui/controls/download_bar.h"
#include "ui/controls/jump_down_button.h"
#include "ui/controls/swipe_handler.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/ui_utility.h"
@ -46,6 +49,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "main/main_session_settings.h"
#include "api/api_chat_filters.h"
#include "apiwrap.h"
#include "chat_helpers/message_field.h"
#include "core/application.h"
#include "core/ui_integration.h"
#include "core/update_checker.h"
@ -59,6 +63,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "storage/storage_account.h"
#include "storage/storage_domain.h"
#include "data/components/recent_peers.h"
#include "data/components/sponsored_messages.h"
#include "data/data_session.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
@ -366,6 +371,7 @@ Widget::Widget(
_storiesContents.events() | rpl::flatten_latest())
: nullptr)
, _searchTimer([=] { search(); })
, _peerSearch(&controller->session(), Api::PeerSearch::Type::WithSponsored)
, _singleMessageSearch(&controller->session()) {
const auto makeChildListShown = [](PeerId peerId, float64 shown) {
return InnerWidget::ChildListShown{ peerId, shown };
@ -381,6 +387,10 @@ Widget::Widget(
_childListPeerId.value(),
_childListShown.value(),
makeChildListShown)));
_scroll->heightValue() | rpl::start_with_next([=](int height) {
_inner->setMinimumHeight(height);
_inner->refresh();
}, _inner->lifetime());
_scrollToTop->raise();
_lockUnlock->toggle(false, anim::type::instant);
@ -671,18 +681,169 @@ Widget::Widget(
setupMoreChatsBar();
setupDownloadBar();
}
setupSwipeBack();
if (session().settings().dialogsFiltersEnabled()
&& (Core::App().settings().chatFiltersHorizontal()
|| !controller->enoughSpaceForFilters())) {
toggleFiltersMenu(true);
}
setupFrozenAccountBar();
}
void Widget::setupSwipeBack() {
const auto isMainList = [=] {
const auto current = controller()->activeChatsFilterCurrent();
const auto &chatsFilters = session().data().chatsFilters();
if (chatsFilters.has()) {
return chatsFilters.defaultId() == current;
}
return !current;
};
auto update = [=](Ui::Controls::SwipeContextData data) {
if (data.translation != 0) {
if (data.translation < 0
&& _inner
&& (Core::App().settings().quickDialogAction()
!= Ui::QuickDialogAction::Disabled)) {
_inner->setSwipeContextData(data.msgBareId, std::move(data));
} else {
if (!_swipeBackData.callback) {
_swipeBackData = Ui::Controls::SetupSwipeBack(
this,
[]() -> std::pair<QColor, QColor> {
return {
st::historyForwardChooseBg->c,
st::historyForwardChooseFg->c,
};
},
_swipeBackMirrored,
_swipeBackIconMirrored);
}
_swipeBackData.callback(data);
}
return;
} else {
if (_swipeBackData.lifetime) {
_swipeBackData = {};
}
if (_inner) {
_inner->setSwipeContextData(data.msgBareId, std::nullopt);
_inner->update();
}
}
};
auto init = [=](int top, Qt::LayoutDirection direction) {
_swipeBackIconMirrored = false;
_swipeBackMirrored = false;
if (_childListShown.current()) {
return Ui::Controls::SwipeHandlerFinishData();
}
const auto isRightToLeft = direction == Qt::RightToLeft;
const auto action = Core::App().settings().quickDialogAction();
const auto isDisabled = action == Ui::QuickDialogAction::Disabled;
if (_inner) {
_inner->clearQuickActions();
if (!isRightToLeft) {
if (const auto key = _inner->calcSwipeKey(top);
key && !isDisabled) {
_inner->prepareQuickAction(key, action);
return Ui::Controls::SwipeHandlerFinishData{
.callback = [=, session = &session()] {
auto callback = [=, peerId = PeerId(key)] {
PerformQuickDialogAction(
controller(),
session->data().peer(peerId),
action,
_inner->filterId());
};
base::call_delayed(
st::slideWrapDuration,
session,
std::move(callback));
},
.msgBareId = key,
.speedRatio = 1.,
.reachRatioDuration = crl::time(
st::slideWrapDuration),
.provideReachOutRatio = true,
};
}
}
}
if (controller()->openedFolder().current()) {
if (!isRightToLeft) {
return Ui::Controls::SwipeHandlerFinishData();
}
return Ui::Controls::DefaultSwipeBackHandlerFinishData([=] {
_swipeBackData = {};
if (controller()->openedFolder().current()) {
if (!controller()->windowId().folder()) {
controller()->closeFolder();
}
}
});
}
if (controller()->shownForum().current()) {
if (!isRightToLeft) {
return Ui::Controls::SwipeHandlerFinishData();
}
const auto id = controller()->windowId();
const auto initial = id.forum();
if (initial) {
return Ui::Controls::SwipeHandlerFinishData();
}
return Ui::Controls::DefaultSwipeBackHandlerFinishData([=] {
_swipeBackData = {};
if (const auto forum = controller()->shownForum().current()) {
controller()->closeForum();
}
});
}
if (isRightToLeft && isMainList()) {
_swipeBackIconMirrored = true;
return Ui::Controls::DefaultSwipeBackHandlerFinishData([=] {
_swipeBackIconMirrored = false;
_swipeBackData = {};
if (isMainList()) {
showMainMenu();
}
});
}
if (session().data().chatsFilters().has() && isDisabled) {
_swipeBackMirrored = !isRightToLeft;
using namespace Window;
const auto next = !isRightToLeft;
if (CheckAndJumpToNearChatsFilter(controller(), next, false)) {
return Ui::Controls::DefaultSwipeBackHandlerFinishData([=] {
_swipeBackData = {};
CheckAndJumpToNearChatsFilter(controller(), next, true);
});
}
}
return Ui::Controls::SwipeHandlerFinishData();
};
Ui::Controls::SetupSwipeHandler({
.widget = _inner,
.scroll = _scroll.data(),
.update = std::move(update),
.init = std::move(init),
});
}
void Widget::chosenRow(const ChosenRow &row) {
storiesToggleExplicitExpand(false);
if (!_searchState.query.isEmpty()) {
if (!row.sponsoredRandomId.isEmpty()) {
auto &messages = session().sponsoredMessages();
messages.clicked(row.sponsoredRandomId, false, false);
} else if (!_searchState.query.isEmpty()) {
if (const auto history = row.key.history()) {
session().recentPeers().bump(history->peer);
}
@ -692,6 +853,7 @@ void Widget::chosenRow(const ChosenRow &row) {
const auto topicJump = history
? history->peer->forumTopicFor(row.message.fullId.msg)
: nullptr;
if (topicJump) {
if (controller()->shownForum().current() == topicJump->forum()) {
controller()->closeForum();
@ -843,6 +1005,29 @@ void Widget::setupTouchChatPreview() {
}, _inner->lifetime());
}
void Widget::setupFrozenAccountBar() {
session().frozenValue(
) | rpl::start_with_next([=] {
updateFrozenAccountBar();
updateControlsGeometry();
}, lifetime());
}
void Widget::updateFrozenAccountBar() {
if (_layout == Layout::Child
|| _openedForum
|| _openedFolder
|| !session().frozen()) {
_frozenAccountBar = nullptr;
} else if (!_frozenAccountBar) {
_frozenAccountBar = FrozenWriteRestriction(
this,
controller()->uiShow(),
FrozenWriteRestrictionType::DialogsList);
_frozenAccountBar->show();
}
}
void Widget::setupMoreChatsBar() {
if (_layout == Layout::Child) {
return;
@ -1220,6 +1405,14 @@ void Widget::setupShortcuts() {
}
return true;
});
request->check(Command::ShowChatPreview, 1)
&& request->handle([=] {
if (_inner) {
Window::ActivateWindow(controller());
return _inner->showChatPreview();
}
return true;
});
}
}, lifetime());
}
@ -1258,6 +1451,9 @@ void Widget::updateControlsVisibility(bool fast) {
if (_moreChatsBar) {
_moreChatsBar->show();
}
if (_frozenAccountBar) {
_frozenAccountBar->show();
}
if (_chatFilters) {
_chatFilters->show();
}
@ -1541,7 +1737,7 @@ void Widget::changeOpenedSubsection(
change();
refreshTopBars();
updateControlsVisibility(true);
_peerSearchRequest = 0;
_peerSearch.clear();
_api.request(base::take(_topicSearchRequest)).cancel();
if (animated == anim::type::normal) {
if (_connecting) {
@ -1576,6 +1772,7 @@ void Widget::changeOpenedFolder(Data::Folder *folder, anim::type animated) {
if (_stories) {
storiesExplicitCollapse();
}
updateFrozenAccountBar();
}, (folder != nullptr), animated);
}
@ -1632,6 +1829,7 @@ void Widget::changeOpenedForum(Data::Forum *forum, anim::type animated) {
_api.request(base::take(_topicSearchRequest)).cancel();
_inner->changeOpenedForum(forum);
storiesToggleExplicitExpand(false);
updateFrozenAccountBar();
updateStoriesVisibility();
}, (forum != nullptr), animated);
}
@ -1963,6 +2161,9 @@ void Widget::startWidthAnimation() {
}
_widthAnimationCache = grabNonNarrowScrollFrame();
_scroll->hide();
if (_frozenAccountBar) {
_frozenAccountBar->hide();
}
if (_chatFilters) {
_chatFilters->hide();
}
@ -1973,6 +2174,9 @@ void Widget::stopWidthAnimation() {
_widthAnimationCache = QPixmap();
if (!_showAnimation) {
_scroll->setVisible(!_suggestions);
if (_frozenAccountBar) {
_frozenAccountBar->setVisible(!_suggestions);
}
if (_chatFilters) {
_chatFilters->setVisible(!_suggestions);
}
@ -2077,6 +2281,9 @@ void Widget::startSlideAnimation(
if (_moreChatsBar) {
_moreChatsBar->hide();
}
if (_frozenAccountBar) {
_frozenAccountBar->hide();
}
if (_chatFilters) {
_chatFilters->hide();
}
@ -2241,10 +2448,9 @@ bool Widget::search(bool inCache, SearchRequestDelay delay) {
{ .posts = true, .start = true },
&_postsProcess);
}
_api.request(base::take(_peerSearchRequest)).cancel();
_peerSearchQuery = QString();
peerSearchApplyEmpty(0);
_peerSearch.clear();
_api.request(base::take(_topicSearchRequest)).cancel();
peerSearchReceived({});
return true;
} else if (inCache) {
const auto success = _singleMessageSearch.lookup(query, [=] {
@ -2349,35 +2555,18 @@ bool Widget::search(bool inCache, SearchRequestDelay delay) {
} else {
_inner->searchRequested(false);
}
const auto peerQuery = Api::ConvertPeerSearchQuery(query);
if (searchForPeersRequired(peerQuery)) {
if (inCache) {
auto i = _peerSearchCache.find(peerQuery);
if (i != _peerSearchCache.end()) {
_peerSearchQuery = peerQuery;
_peerSearchRequest = 0;
peerSearchReceived(i->second, 0);
}
} else if (_peerSearchQuery != peerQuery) {
_peerSearchQuery = peerQuery;
_peerSearchFull = false;
_peerSearchRequest = _api.request(MTPcontacts_Search(
MTP_string(_peerSearchQuery),
MTP_int(SearchPeopleLimit)
)).done([=](
const MTPcontacts_Found &result,
mtpRequestId requestId) {
peerSearchReceived(result, requestId);
}).fail([=](const MTP::Error &error, mtpRequestId requestId) {
peerSearchFailed(error, requestId);
}).send();
_peerSearchQueries.emplace(_peerSearchRequest, _peerSearchQuery);
}
if (peerSearchRequired()) {
const auto requestType = inCache
? Api::PeerSearch::RequestType::CacheOnly
: Api::PeerSearch::RequestType::CacheOrRemote;
_peerSearch.request(query, [=](Api::PeerSearchResult result) {
peerSearchReceived(result);
}, requestType);
} else {
_api.request(base::take(_peerSearchRequest)).cancel();
_peerSearchQuery = peerQuery;
peerSearchApplyEmpty(0);
_peerSearch.clear();
peerSearchReceived({});
}
const auto peerQuery = Api::ConvertPeerSearchQuery(query);
if (searchForTopicsRequired(peerQuery)) {
if (inCache) {
if (_topicSearchQuery != peerQuery) {
@ -2396,11 +2585,8 @@ bool Widget::search(bool inCache, SearchRequestDelay delay) {
return result;
}
bool Widget::searchForPeersRequired(const QString &query) const {
return _searchState.filterChatsList()
&& !_openedForum
&& !query.isEmpty()
&& (IsHashOrCashtagSearchQuery(query) == HashOrCashtag::None);
bool Widget::peerSearchRequired() const {
return _searchState.filterChatsList() && !_openedForum;
}
bool Widget::searchForTopicsRequired(const QString &query) const {
@ -2800,31 +2986,10 @@ void Widget::searchReceived(
update();
}
void Widget::peerSearchReceived(
const MTPcontacts_Found &result,
mtpRequestId requestId) {
const auto state = _inner->state();
auto q = _peerSearchQuery;
if (state == WidgetState::Filtered) {
auto i = _peerSearchQueries.find(requestId);
if (i != _peerSearchQueries.end()) {
_peerSearchCache[i->second] = result;
_peerSearchQueries.erase(i);
}
}
if (_peerSearchRequest == requestId) {
switch (result.type()) {
case mtpc_contacts_found: {
auto &d = result.c_contacts_found();
session().data().processUsers(d.vusers());
session().data().processChats(d.vchats());
_inner->peerSearchReceived(q, d.vmy_results().v, d.vresults().v);
} break;
}
_peerSearchRequest = 0;
void Widget::peerSearchReceived(Api::PeerSearchResult result) {
_inner->peerSearchReceived(std::move(result));
listScrollUpdated();
}
update();
}
void Widget::searchApplyEmpty(
@ -2840,17 +3005,6 @@ void Widget::searchApplyEmpty(
process);
}
void Widget::peerSearchApplyEmpty(mtpRequestId id) {
_peerSearchFull = true;
peerSearchReceived(
MTP_contacts_found(
MTP_vector<MTPPeer>(0),
MTP_vector<MTPPeer>(0),
MTP_vector<MTPChat>(0),
MTP_vector<MTPUser>(0)),
id);
}
void Widget::searchFailed(
SearchRequestType type,
const MTP::Error &error,
@ -2863,13 +3017,6 @@ void Widget::searchFailed(
}
}
void Widget::peerSearchFailed(const MTP::Error &error, mtpRequestId id) {
if (_peerSearchRequest == id) {
_peerSearchRequest = 0;
_peerSearchFull = true;
}
}
void Widget::dragEnterEvent(QDragEnterEvent *e) {
using namespace Storage;
@ -3294,12 +3441,7 @@ bool Widget::applySearchState(SearchState state) {
clearSearchCache(searchCleared);
}
if (state.query.isEmpty()) {
_peerSearchCache.clear();
const auto queries = base::take(_peerSearchQueries);
for (const auto &[requestId, query] : queries) {
_api.request(requestId).cancel();
}
_peerSearchQuery = QString();
_peerSearch.clear();
}
if (_searchState.query != currentSearchQuery()) {
@ -3357,8 +3499,8 @@ void Widget::clearSearchCache(bool clearPosts) {
_topicSearchQuery = QString();
_topicSearchOffsetDate = 0;
_topicSearchOffsetId = _topicSearchOffsetTopicId = 0;
_api.request(base::take(_peerSearchRequest)).cancel();
_api.request(base::take(_topicSearchRequest)).cancel();
_peerSearch.clear();
cancelSearchRequest();
}
@ -3660,9 +3802,17 @@ void Widget::updateControlsGeometry() {
if (_chatFilters) {
_chatFilters->resizeToWidth(barw);
}
if (_frozenAccountBar) {
_frozenAccountBar->resize(barw, _frozenAccountBar->height());
}
_updateScrollGeometryCached = [=] {
const auto moreChatsBarTop = expandedStoriesTop
const auto frozenBarTop = expandedStoriesTop
+ ((!_stories || _stories->isHidden()) ? 0 : _aboveScrollAdded);
if (_frozenAccountBar) {
_frozenAccountBar->move(0, frozenBarTop);
}
const auto moreChatsBarTop = frozenBarTop
+ (_frozenAccountBar ? _frozenAccountBar->height() : 0);
if (_moreChatsBar) {
_moreChatsBar->move(0, moreChatsBarTop);
}

View file

@ -7,9 +7,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "api/api_peer_search.h"
#include "base/timer.h"
#include "dialogs/dialogs_key.h"
#include "window/section_widget.h"
#include "ui/controls/swipe_handler_data.h"
#include "ui/effects/animations.h"
#include "ui/userpic_view.h"
#include "mtproto/sender.h"
@ -187,9 +189,7 @@ private:
const MTPmessages_Messages &result,
not_null<SearchProcessState*> process,
bool cacheResults = false);
void peerSearchReceived(
const MTPcontacts_Found &result,
mtpRequestId requestId);
void peerSearchReceived(Api::PeerSearchResult result);
void escape();
void submit();
void cancelSearchRequest();
@ -200,17 +200,19 @@ private:
void setupSupportMode();
void setupTouchChatPreview();
void setupFrozenAccountBar();
void setupConnectingWidget();
void setupMainMenuToggle();
void setupMoreChatsBar();
void setupDownloadBar();
void setupShortcuts();
void setupStories();
void setupSwipeBack();
void storiesExplicitCollapse();
void collectStoriesUserpicsViews(Data::StorySourcesList list);
void storiesToggleExplicitExpand(bool expand);
void trackScroll(not_null<Ui::RpWidget*> widget);
[[nodiscard]] bool searchForPeersRequired(const QString &query) const;
[[nodiscard]] bool peerSearchRequired() const;
[[nodiscard]] bool searchForTopicsRequired(const QString &query) const;
// Child list may be unable to set specific search state.
@ -221,6 +223,7 @@ private:
void showMainMenu();
void clearSearchCache(bool clearPosts);
void setSearchQuery(const QString &query, int cursorPosition = -1);
void updateFrozenAccountBar();
void updateControlsVisibility(bool fast = false);
void updateLockUnlockVisibility(
anim::type animated = anim::type::instant);
@ -263,11 +266,9 @@ private:
SearchRequestType type,
const MTP::Error &error,
not_null<SearchProcessState*> process);
void peerSearchFailed(const MTP::Error &error, mtpRequestId requestId);
void searchApplyEmpty(
SearchRequestType type,
not_null<SearchProcessState*> process);
void peerSearchApplyEmpty(mtpRequestId id);
void updateForceDisplayWide();
void scrollToDefault(bool verytop = false);
@ -298,6 +299,9 @@ private:
const Layout _layout = Layout::Main;
int _narrowWidth = 0;
std::unique_ptr<Ui::AbstractButton> _frozenAccountBar;
object_ptr<Ui::RpWidget> _searchControls;
object_ptr<HistoryView::TopBarWidget> _subsectionTopBar = { nullptr };
struct {
@ -366,10 +370,6 @@ private:
base::Timer _searchTimer;
QString _peerSearchQuery;
bool _peerSearchFull = false;
mtpRequestId _peerSearchRequest = 0;
QString _topicSearchQuery;
TimeId _topicSearchOffsetDate = 0;
MsgId _topicSearchOffsetId = 0;
@ -383,14 +383,17 @@ private:
ChatSearchTab _searchQueryTab = {};
ChatTypeFilter _searchQueryFilter = {};
Ui::Controls::SwipeBackResult _swipeBackData;
bool _swipeBackMirrored = false;
bool _swipeBackIconMirrored = false;
SearchProcessState _searchProcess;
SearchProcessState _migratedProcess;
SearchProcessState _postsProcess;
int _historiesRequest = 0; // Not real mtpRequestId.
Api::PeerSearch _peerSearch;
Api::SingleMessageSearch _singleMessageSearch;
base::flat_map<QString, MTPcontacts_Found> _peerSearchCache;
base::flat_map<mtpRequestId, QString> _peerSearchQueries;
QPixmap _widthAnimationCache;

View file

@ -20,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/stickers/data_custom_emoji.h"
#include "dialogs/dialogs_list.h"
#include "dialogs/dialogs_three_state_icon.h"
#include "dialogs/dialogs_quick_action.h"
#include "dialogs/ui/dialogs_video_userpic.h"
#include "history/history.h"
#include "history/history_item.h"
@ -28,12 +29,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history_unread_things.h"
#include "history/view/history_view_item_preview.h"
#include "history/view/history_view_send_action.h"
#include "lang/lang_instance.h"
#include "lang/lang_keys.h"
#include "lottie/lottie_icon.h"
#include "main/main_session.h"
#include "storage/localstorage.h"
#include "support/support_helper.h"
#include "ui/empty_userpic.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/power_saving.h"
#include "ui/text/format_values.h"
#include "ui/text/text_options.h"
@ -89,16 +93,18 @@ void PaintRowTopRight(
text);
}
int PaintRightButton(QPainter &p, const PaintContext &context) {
int PaintRightButtonImpl(QPainter &p, const PaintContext &context) {
if (context.width < st::columnMinimalWidthLeft) {
return 0;
}
if (const auto rightButton = context.rightButton) {
Assert(rightButton->st != nullptr);
const auto size = rightButton->bg.size() / style::DevicePixelRatio();
const auto left = context.width
- size.width()
- st::dialogRowOpenBotRight;
const auto top = st::dialogRowOpenBotTop;
- rightButton->st->margin.right();
const auto top = rightButton->st->margin.top();
p.drawImage(
left,
top,
@ -113,22 +119,22 @@ int PaintRightButton(QPainter &p, const PaintContext &context) {
left,
top,
size.width() - size.height() / 2,
context.active
(context.active
? &st::universalRippleAnimation.color->c
: &st::activeButtonBgRipple->c);
: &rightButton->st->button.ripple.color->c));
if (rightButton->ripple->empty()) {
rightButton->ripple.reset();
}
}
p.setPen(context.active
? st::activeButtonBg
? rightButton->st->button.textBg
: context.selected
? st::activeButtonFgOver
: st::activeButtonFg);
? rightButton->st->button.textFgOver
: rightButton->st->button.textFg);
rightButton->text.draw(p, {
.position = QPoint(
left + size.height() / 2,
top + (st::dialogRowOpenBotHeight - rightButton->text.minHeight()) / 2),
top + rightButton->st->button.textTop),
.outerWidth = size.width() - size.height() / 2,
.availableWidth = size.width() - size.height() / 2,
.elisionLines = 1,
@ -348,11 +354,29 @@ void PaintRow(
draft = nullptr;
}
const auto history = entry->asHistory();
const auto thread = entry->asThread();
const auto sublist = entry->asSublist();
auto bg = context.active
? st::dialogsBgActive
: context.selected
? st::dialogsBgOver
: context.currentBg;
auto swipeTranslation = 0;
if (history
&& context.quickActionContext
&& !context.quickActionContext->ripple
&& (history->peer->id.value
== context.quickActionContext->data.msgBareId)) {
if (context.quickActionContext->data.translation != 0) {
swipeTranslation = context.quickActionContext->data.translation
* -2;
}
}
if (swipeTranslation) {
p.translate(-swipeTranslation, 0);
}
p.fillRect(geometry, bg);
if (!(flags & Flag::TopicJumpRipple)) {
auto ripple = context.active
@ -361,10 +385,6 @@ void PaintRow(
row->paintRipple(p, 0, 0, context.width, &ripple->c);
}
const auto history = entry->asHistory();
const auto thread = entry->asThread();
const auto sublist = entry->asSublist();
if (flags & Flag::SavedMessages) {
EmptyUserpic::PaintSavedMessages(
p,
@ -833,6 +853,60 @@ void PaintRow(
+ (tag->width() / style::DevicePixelRatio());
}
}
if (swipeTranslation) {
p.translate(swipeTranslation, 0);
const auto swipeActionRect = QRect(
rect::right(geometry) - swipeTranslation,
geometry.y(),
swipeTranslation,
geometry.height());
p.setClipRegion(swipeActionRect);
const auto labelType = ResolveQuickDialogLabel(
history,
context.quickActionContext->action,
context.filter);
p.fillRect(swipeActionRect, ResolveQuickActionBg(labelType));
if (context.quickActionContext->data.reachRatio) {
p.setPen(Qt::NoPen);
p.setBrush(ResolveQuickActionBgActive(labelType));
const auto r = swipeTranslation
* context.quickActionContext->data.reachRatio;
const auto offset = st::dialogsQuickActionSize
+ st::dialogsQuickActionSize / 2.;
p.drawEllipse(QPointF(geometry.width() - offset, offset), r, r);
}
const auto quickWidth = st::dialogsQuickActionSize * 3;
if (context.quickActionContext->icon) {
DrawQuickAction(
p,
QRect(
rect::right(geometry) - quickWidth,
geometry.y(),
quickWidth,
geometry.height()),
context.quickActionContext->icon.get(),
labelType);
}
p.setClipping(false);
}
if (const auto quick = context.quickActionContext;
quick && quick->ripple && quick->rippleFg) {
const auto labelType = ResolveQuickDialogLabel(
history,
context.quickActionContext->action,
context.filter);
const auto ripple = ResolveQuickActionBg(labelType);
const auto size = st::dialogsQuickActionRippleSize;
const auto x = geometry.width() - size;
quick->ripple->paint(p, x, 0, size, &ripple->c);
quick->rippleFg->paint(p, x, 0, size, &st::premiumButtonFg->c);
if (quick->ripple->empty()) {
quick->ripple.reset();
}
if (quick->rippleFg->empty()) {
quick->rippleFg.reset();
}
}
}
} // namespace
@ -1189,4 +1263,8 @@ void PaintCollapsedRow(
}
}
int PaintRightButton(QPainter &p, const PaintContext &context) {
return PaintRightButtonImpl(p, context);
}
} // namespace Dialogs::Ui

View file

@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "dialogs/ui/dialogs_quick_action_context.h"
#include "ui/cached_round_corners.h"
namespace style {
@ -18,9 +19,6 @@ namespace st {
extern const style::DialogRow &defaultDialogRow;
} // namespace st
namespace Ui {
} // namespace Ui
namespace Data {
class Forum;
class Folder;
@ -57,6 +55,7 @@ struct TopicJumpCache {
struct PaintContext {
RightButton *rightButton = nullptr;
std::vector<QImage*> *chatsFilterTags = nullptr;
QuickActionContext *quickActionContext = nullptr;
not_null<const style::DialogRow*> st;
TopicJumpCache *topicJumpCache = nullptr;
Data::Folder *folder = nullptr;
@ -111,4 +110,6 @@ void PaintCollapsedRow(
int unread,
const PaintContext &context);
int PaintRightButton(QPainter &p, const PaintContext &context);
} // namespace Dialogs::Ui

View file

@ -150,8 +150,7 @@ void MessageView::prepare(
not_null<const HistoryItem*> item,
Data::Forum *forum,
Fn<void()> customEmojiRepaint,
ToPreviewOptions options,
Fn<void()> customLoadingFinishCallback) {
ToPreviewOptions options) {
if (!forum) {
_topics = nullptr;
} else if (!_topics || _topics->forum() != forum) {
@ -213,11 +212,9 @@ void MessageView::prepare(
if (!_loadingContext) {
_loadingContext = std::make_unique<LoadingContext>();
item->history()->session().downloaderTaskFinished(
) | rpl::start_with_next(
customLoadingFinishCallback
? customLoadingFinishCallback
: Fn<void()>([=] { _textCachedFor = nullptr; }),
_loadingContext->lifetime);
) | rpl::start_with_next([=] {
_textCachedFor = nullptr;
}, _loadingContext->lifetime);
}
_loadingContext->context = std::move(preview.loadingContext);
} else {
@ -370,9 +367,20 @@ void MessageView::paint(
if (image.hasSpoiler()) {
const auto frame = DefaultImageSpoiler().frame(
_spoiler->index(context.now, pausedSpoiler));
if (image.isEllipse()) {
const auto radius = st::dialogsMiniPreview / 2;
static auto mask = Images::CornersMask(radius);
FillSpoilerRect(
p,
mini,
Images::CornersMaskRef(mask),
frame,
_cornersCache);
} else {
FillSpoilerRect(p, mini, frame);
}
}
}
rect.setLeft(rect.x() + w);
}
if (!_imagesCache.empty()) {

View file

@ -61,8 +61,7 @@ public:
not_null<const HistoryItem*> item,
Data::Forum *forum,
Fn<void()> customEmojiRepaint,
ToPreviewOptions options,
Fn<void()> customLoadingFinishCallback = nullptr);
ToPreviewOptions options);
void paint(
Painter &p,
@ -95,6 +94,7 @@ private:
mutable std::unique_ptr<SpoilerAnimation> _spoiler;
mutable std::unique_ptr<LoadingContext> _loadingContext;
mutable const style::DialogsMiniIcon *_leftIcon = nullptr;
mutable QImage _cornersCache;
mutable bool _hasPlainLinkAtBegin = false;
};

View file

@ -7,14 +7,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
namespace HistoryView {
namespace Dialogs::Ui {
struct ChatPaintGestureHorizontalData {
float64 ratio = 0.;
float64 reachRatio = 0.;
int64 msgBareId = 0;
int translation = 0;
int cursorTop = 0;
using namespace ::Ui;
enum class QuickDialogAction {
Mute,
Pin,
Read,
Archive,
Delete,
Disabled,
};
} // namespace HistoryView
} // namespace Dialogs::Ui

View file

@ -0,0 +1,47 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "dialogs/ui/dialogs_quick_action.h"
#include "ui/controls/swipe_handler_data.h"
namespace Lottie {
class Icon;
} // namespace Lottie
namespace Ui {
class RippleAnimation;
} // namespace Ui
namespace Dialogs::Ui {
using namespace ::Ui;
enum class QuickDialogActionLabel {
Mute,
Unmute,
Pin,
Unpin,
Read,
Unread,
Archive,
Unarchive,
Delete,
Disabled,
};
struct QuickActionContext {
::Ui::Controls::SwipeContextData data;
std::unique_ptr<Lottie::Icon> icon;
std::unique_ptr<Ui::RippleAnimation> ripple;
std::unique_ptr<Ui::RippleAnimation> rippleFg;
QuickDialogAction action;
crl::time finishedAt = 0;
};
} // namespace Dialogs::Ui

View file

@ -36,6 +36,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "settings/settings_common.h"
#include "storage/storage_shared_media.h"
#include "ui/boxes/confirm_box.h"
#include "ui/controls/swipe_handler.h"
#include "ui/effects/ripple_animation.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/menu/menu_add_action_callback_factory.h"
@ -203,7 +204,7 @@ RecentRow::RecentRow(not_null<PeerData*> peer)
if (const auto user = peer->asUser()) {
if (user->botInfo && user->botInfo->hasMainApp) {
return std::make_unique<Ui::Text::String>(
st::dialogRowOpenBotTextStyle,
st::dialogRowOpenBotRecent.button.style,
tr::lng_profile_open_app_short(tr::now));
}
}
@ -274,20 +275,15 @@ QSize RecentRow::rightActionSize() const {
if (_mainAppText && _badgeSize.isEmpty()) {
return QSize(
_mainAppText->maxWidth() + _mainAppText->minHeight(),
st::dialogRowOpenBotHeight);
st::dialogRowOpenBotRecent.button.height);
}
return _badgeSize;
}
QMargins RecentRow::rightActionMargins() const {
if (_mainAppText && _badgeSize.isEmpty()) {
return QMargins(
0,
st::dialogRowOpenBotRecentTop,
st::dialogRowOpenBotRight,
0);
}
if (_badgeSize.isEmpty()) {
return st::dialogRowOpenBotRecent.margin;
} else if (_badgeSize.isEmpty()) {
return {};
}
const auto x = st::recentPeersItem.photoPosition.x();
@ -320,8 +316,7 @@ void RecentRow::rightActionPaint(
p.setPen(actionSelected
? st::activeButtonFgOver
: st::activeButtonFg);
const auto top = 0
+ (st::dialogRowOpenBotHeight - _mainAppText->minHeight()) / 2;
const auto top = st::dialogRowOpenBotRecent.button.textTop;
_mainAppText->draw(p, {
.position = QPoint(x + size.height() / 2, y + top),
.outerWidth = outerWidth,
@ -1552,6 +1547,61 @@ void Suggestions::setupApps() {
});
}
Ui::Controls::SwipeHandlerArgs Suggestions::generateIncompleteSwipeArgs() {
_swipeLifetime.destroy();
auto update = [=](Ui::Controls::SwipeContextData data) {
if (data.translation != 0) {
if (!_swipeBackData.callback) {
_swipeBackData = Ui::Controls::SetupSwipeBack(
this,
[=]() -> std::pair<QColor, QColor> {
return {
st::historyForwardChooseBg->c,
st::historyForwardChooseFg->c,
};
},
data.translation < 0);
}
_swipeBackData.callback(data);
return;
} else if (_swipeBackData.lifetime) {
_swipeBackData = {};
}
};
auto init = [=](int, Qt::LayoutDirection direction) {
if (!_tabs) {
return Ui::Controls::SwipeHandlerFinishData();
}
const auto activeSection = _tabs->activeSection();
const auto isToLeft = direction == Qt::RightToLeft;
if ((isToLeft && activeSection > 0)
|| (!isToLeft && activeSection < _tabKeys.size() - 1)) {
return Ui::Controls::DefaultSwipeBackHandlerFinishData([=] {
if (_tabs
&& _tabs->activeSection() == activeSection) {
_swipeBackData = {};
_tabs->setActiveSection(isToLeft
? activeSection - 1
: activeSection + 1);
}
});
}
return Ui::Controls::SwipeHandlerFinishData();
};
return { .widget = this, .update = update, .init = init };
}
void Suggestions::reinstallSwipe(not_null<Ui::ElasticScroll*> scroll) {
_swipeLifetime.destroy();
auto args = generateIncompleteSwipeArgs();
args.scroll = scroll;
args.onLifetime = &_swipeLifetime;
Ui::Controls::SetupSwipeHandler(std::move(args));
}
void Suggestions::selectJump(Qt::Key direction, int pageSize) {
switch (_key.current().tab) {
case Tab::Chats: selectJumpChats(direction, pageSize); return;
@ -1977,6 +2027,18 @@ void Suggestions::finishShow() {
_appsScroll->setVisible(key == Key{ Tab::Apps });
for (const auto &[mediaKey, list] : _mediaLists) {
list.wrap->setVisible(key == mediaKey);
if (key == mediaKey) {
_swipeLifetime.destroy();
auto incomplete = generateIncompleteSwipeArgs();
list.wrap->replaceSwipeHandler(&incomplete);
}
}
if (key == Key{ Tab::Chats }) {
reinstallSwipe(_chatsScroll.get());
} else if (key == Key{ Tab::Channels }) {
reinstallSwipe(_channelsScroll.get());
} else if (key == Key{ Tab::Apps }) {
reinstallSwipe(_appsScroll.get());
}
}

View file

@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/object_ptr.h"
#include "base/timer.h"
#include "dialogs/ui/top_peers_strip.h"
#include "ui/controls/swipe_handler_data.h"
#include "ui/effects/animations.h"
#include "ui/rp_widget.h"
@ -32,6 +33,9 @@ enum class SharedMediaType : signed char;
} // namespace Storage
namespace Ui {
namespace Controls {
struct SwipeHandlerArgs;
} // namespace Controls
class BoxContent;
class ScrollArea;
class ElasticScroll;
@ -157,6 +161,9 @@ private:
void setupChats();
void setupChannels();
void setupApps();
void reinstallSwipe(not_null<Ui::ElasticScroll*>);
[[nodiscard]] auto generateIncompleteSwipeArgs()
-> Ui::Controls::SwipeHandlerArgs;
void selectJumpChats(Qt::Key direction, int pageSize);
void selectJumpChannels(Qt::Key direction, int pageSize);
@ -253,6 +260,9 @@ private:
QPixmap _slideLeft;
QPixmap _slideRight;
Ui::Controls::SwipeBackResult _swipeBackData;
rpl::lifetime _swipeLifetime;
int _slideLeftTop = 0;
int _slideRightTop = 0;

View file

@ -1704,6 +1704,15 @@ ServiceAction ParseServiceAction(
.giftId = uint64(gift.vid().v),
};
});
}, [&](const MTPDmessageActionPaidMessagesRefunded &data) {
result.content = ActionPaidMessagesRefunded{
.messages = data.vcount().v,
.stars = int64(data.vstars().v),
};
}, [&](const MTPDmessageActionPaidMessagesPrice &data) {
result.content = ActionPaidMessagesPrice{
.stars = int(data.vstars().v),
};
}, [](const MTPDmessageActionEmpty &data) {});
return result;
}

View file

@ -662,6 +662,15 @@ struct ActionStarGift {
bool limited = false;
};
struct ActionPaidMessagesRefunded {
int messages = 0;
int64 stars = 0;
};
struct ActionPaidMessagesPrice {
int stars = 0;
};
struct ServiceAction {
std::variant<
v::null_t,
@ -707,7 +716,9 @@ struct ServiceAction {
ActionPaymentRefunded,
ActionGiftStars,
ActionPrizeStars,
ActionStarGift> content;
ActionStarGift,
ActionPaidMessagesRefunded,
ActionPaidMessagesPrice> content;
};
ServiceAction ParseServiceAction(

View file

@ -1367,6 +1367,26 @@ auto HtmlWriter::Wrap::pushMessage(
+ " sent you a gift of "
+ QByteArray::number(data.stars)
+ " Telegram Stars.";
}, [&](const ActionPaidMessagesRefunded &data) {
auto result = message.out
? ("You refunded "
+ QString::number(data.stars).toUtf8()
+ " Stars for "
+ QString::number(data.messages).toUtf8()
+ " messages to "
+ peers.wrapPeerName(dialog.peerId))
: (peers.wrapPeerName(dialog.peerId)
+ " refunded "
+ QString::number(data.stars).toUtf8()
+ " Stars for "
+ QString::number(data.messages).toUtf8()
+ " messages to you");
return result;
}, [&](const ActionPaidMessagesPrice &data) {
auto result = "Price per messages changed to "
+ QString::number(data.stars).toUtf8()
+ " Telegram Stars.";
return result;
}, [](v::null_t) { return QByteArray(); });
if (!serviceText.isEmpty()) {
@ -1560,7 +1580,9 @@ auto HtmlWriter::Wrap::pushMessage(
block.append(popTag());
}
if (!message.reactions.empty()) {
block.append(pushDiv("reactions"));
block.append(pushTag("span", {
{ "class", "reactions" },
}));
for (const auto &reaction : message.reactions) {
auto reactionClass = QByteArray("reaction");
for (const auto &recent : reaction.recent) {
@ -1574,10 +1596,10 @@ auto HtmlWriter::Wrap::pushMessage(
reactionClass += " paid";
}
block.append(pushTag("div", {
block.append(pushTag("span", {
{ "class", reactionClass },
}));
block.append(pushTag("div", {
block.append(pushTag("span", {
{ "class", "emoji" },
}));
switch (reaction.type) {
@ -1596,7 +1618,7 @@ auto HtmlWriter::Wrap::pushMessage(
}
block.append(popTag());
if (!reaction.recent.empty()) {
block.append(pushTag("div", {
block.append(pushTag("span", {
{ "class", "userpics" },
}));
for (const auto &recent : reaction.recent) {
@ -1617,7 +1639,7 @@ auto HtmlWriter::Wrap::pushMessage(
}
if (reaction.recent.empty()
|| (reaction.count > reaction.recent.size())) {
block.append(pushTag("div", {
block.append(pushTag("span", {
{ "class", "count" },
}));
block.append(NumberToString(reaction.count));
@ -2321,10 +2343,12 @@ MediaData HtmlWriter::Wrap::prepareMediaData(
} else if (data.isVideoFile) {
// At least try to pushVideoFileMedia.
} else if (data.isAudioFile) {
result.title = (data.songPerformer.isEmpty()
|| data.songTitle.isEmpty())
? QByteArray("Audio file")
: data.songPerformer + " \xe2\x80\x93 " + data.songTitle;
result.title = (!data.songPerformer.isEmpty()
&& !data.songTitle.isEmpty())
? (data.songPerformer + " \xe2\x80\x93 " + data.songTitle)
: !data.name.isEmpty()
? data.name
: QByteArray("Audio file");
result.status = FormatDuration(data.duration);
if (!hasFile) {
result.status += ", " + FormatFileSize(data.file.size);

View file

@ -663,6 +663,15 @@ QByteArray SerializeMessage(
push("is_limited", data.limited);
push("is_anonymous", data.anonymous);
pushBare("gift_text", SerializeText(context, data.text));
}, [&](const ActionPaidMessagesRefunded &data) {
pushActor();
pushAction("paid_messages_refund");
push("messages_count", data.messages);
push("stars_count", data.stars);
}, [&](const ActionPaidMessagesPrice &data) {
pushActor();
pushAction("paid_messages_price_change");
push("price_stars", data.stars);
}, [](v::null_t) {});
if (v::is_null(message.action.content)) {

View file

@ -1039,6 +1039,16 @@ void InnerWidget::restoreScrollPosition() {
_scrollToSignal.fire_copy(newVisibleTop);
}
Ui::ChatPaintContext InnerWidget::preparePaintContext(QRect clip) const {
return _controller->preparePaintContext({
.theme = _theme.get(),
.clip = clip,
.visibleAreaPositionGlobal = mapToGlobal(QPoint(0, _visibleTop)),
.visibleAreaTop = _visibleTop,
.visibleAreaWidth = width(),
});
}
void InnerWidget::paintEvent(QPaintEvent *e) {
if (_controller->contentOverlapped(this, e)) {
return;
@ -1051,13 +1061,7 @@ void InnerWidget::paintEvent(QPaintEvent *e) {
Painter p(this);
auto clip = e->rect();
auto context = _controller->preparePaintContext({
.theme = _theme.get(),
.clip = clip,
.visibleAreaPositionGlobal = mapToGlobal(QPoint(0, _visibleTop)),
.visibleAreaTop = _visibleTop,
.visibleAreaWidth = width(),
});
auto context = preparePaintContext(clip);
if (_items.empty() && _upLoaded && _downLoaded) {
paintEmpty(p, context.st);
} else {

View file

@ -36,6 +36,7 @@ class PopupMenu;
class ChatStyle;
class ChatTheme;
struct PeerUserpicView;
struct ChatPaintContext;
} // namespace Ui
namespace Window {
@ -69,6 +70,8 @@ public:
return _channel;
}
Ui::ChatPaintContext preparePaintContext(QRect clip) const;
// Set the correct scroll position after being resized.
void restoreScrollPosition();

View file

@ -11,6 +11,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/admin_log/history_admin_log_filter.h"
#include "profile/profile_back_button.h"
#include "core/shortcuts.h"
#include "ui/chat/chat_style.h"
#include "ui/controls/swipe_handler.h"
#include "ui/effects/animations.h"
#include "ui/widgets/scroll_area.h"
#include "ui/widgets/shadow.h"
@ -341,6 +343,7 @@ Widget::Widget(
});
setupShortcuts();
setupSwipeReply();
}
void Widget::showFilter() {
@ -416,6 +419,44 @@ void Widget::setupShortcuts() {
}, lifetime());
}
void Widget::setupSwipeReply() {
auto update = [=](Ui::Controls::SwipeContextData data) {
if (data.translation > 0) {
if (!_swipeBackData.callback) {
_swipeBackData = Ui::Controls::SetupSwipeBack(
this,
[=]() -> std::pair<QColor, QColor> {
auto context = _inner->preparePaintContext({});
return {
context.st->msgServiceBg()->c,
context.st->msgServiceFg()->c,
};
});
}
_swipeBackData.callback(data);
return;
} else if (_swipeBackData.lifetime) {
_swipeBackData = {};
}
};
auto init = [=](int, Qt::LayoutDirection direction) {
if (direction == Qt::RightToLeft) {
return Ui::Controls::DefaultSwipeBackHandlerFinishData([=] {
controller()->showBackFromStack();
});
}
return Ui::Controls::SwipeHandlerFinishData();
};
Ui::Controls::SetupSwipeHandler({
.widget = _inner.data(),
.scroll = _scroll.data(),
.update = std::move(update),
.init = std::move(init),
});
}
std::shared_ptr<Window::SectionMemento> Widget::createMemento() {
auto result = std::make_shared<SectionMemento>(channel());
saveState(result.get());

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