Compare commits

...

82 Commits

Author SHA1 Message Date
John Preston
1c4f663941 Version 6.3.6.
- Fix crash in media viewer.
- Fix crash in experimental settings.
2025-12-06 00:07:39 +04:00
John Preston
94f9321db9 Fix crash in experimental settings opening. 2025-12-06 00:05:31 +04:00
John Preston
ae70b10cea Fix crash in media viewer. 2025-12-05 23:58:08 +04:00
John Preston
4f685552e7 Version 6.3.5.
- Offer stars or TON for unique gifts.
- Preview gift auctions before they start.
- Support passkey login on Windows.
2025-12-05 23:11:53 +04:00
John Preston
e085a76165 Fix build with GCC. 2025-12-05 23:11:32 +04:00
23rd
30bd3ed013 Removed WebAuthn support from entitlements. 2025-12-05 23:11:32 +04:00
23rd
25edab4c94 Fixed build with Xcode. 2025-12-05 23:11:32 +04:00
23rd
3aa241d825 Removed WebAuthn support for macOS for now. 2025-12-05 23:11:32 +04:00
John Preston
9b867af7fd Use known gift number in some places. 2025-12-05 20:11:54 +04:00
John Preston
0df3be8630 Use better ratio stars/usd/ton. 2025-12-05 20:11:54 +04:00
John Preston
542326af8f Show offer value diff percent. 2025-12-05 20:11:54 +04:00
John Preston
ea5052e69e Add "Ban Users" channel admin right. 2025-12-05 20:11:54 +04:00
John Preston
2dd96b2269 Better phrases for upcoming auctions. 2025-12-05 20:11:54 +04:00
John Preston
627152e2a9 Nice gifts promo box. 2025-12-05 20:11:54 +04:00
John Preston
f01c93ed58 Update API scheme on layer 220. 2025-12-05 20:11:54 +04:00
John Preston
6fe61ed58a Add gifts premium promo. 2025-12-05 20:11:54 +04:00
John Preston
43347f671c Show rarity in gift variants preview. 2025-12-05 20:11:54 +04:00
John Preston
0b67fa65f2 Full upgradable variants preview. 2025-12-05 20:11:54 +04:00
John Preston
65b3a36984 Implement upcoming auction preview box. 2025-12-05 20:11:54 +04:00
John Preston
b08cf75f0b Start auction preview display. 2025-12-05 20:11:54 +04:00
John Preston
0cc21e5ca2 Update API scheme on layer 220. 2025-12-05 20:11:54 +04:00
John Preston
48f9a92cc3 Improve Checkbox / Button accessibility. 2025-12-05 20:11:54 +04:00
John Preston
939882ef68 Show profile design gift wear promo. 2025-12-05 20:11:54 +04:00
John Preston
52084cf0ae Apply correct min/max offer values. 2025-12-05 20:11:54 +04:00
John Preston
f06f654191 Confirm making an offer. 2025-12-05 20:11:53 +04:00
John Preston
356d20542e Use nice radius in offer with buttons. 2025-12-05 20:11:53 +04:00
John Preston
31ea4cfe80 Process offers with accept / reject. 2025-12-05 20:11:53 +04:00
John Preston
1e89ee4e50 Show Reject/Accept buttons for offers. 2025-12-05 20:11:53 +04:00
John Preston
6fccbf036c Rename HistoryMessage[SuggestedPost->Suggestion]. 2025-12-05 20:11:53 +04:00
John Preston
41d206e354 Rename SuggestPostOptions to SuggestOptions. 2025-12-05 20:11:53 +04:00
John Preston
23880ac6c1 Update API scheme to layer 220.
Allow offering to buy gifts.
2025-12-05 20:11:53 +04:00
John Preston
4439cbf553 Pass effect_id to forward message requests. 2025-12-05 20:11:53 +04:00
23rd
f506f1b830 Added read availability of passkeys from appConfig. 2025-12-05 20:11:53 +04:00
23rd
feb1ea6502 Wrapped text recognition on macOS with experimental toggle. 2025-12-05 20:11:53 +04:00
23rd
fea80b4919 Wrapped passkeys on macOS with experimental toggle due to instability. 2025-12-05 20:11:53 +04:00
23rd
373bb8d74c Improved display of ripple on release in buttons from subsection tabs. 2025-12-05 20:11:53 +04:00
23rd
3ddefd78ba Fixed display of tabbed selector on installed set for read-only peers. 2025-12-05 20:11:53 +04:00
23rd
d62e4da163 Added simple cache of recognized text to media view overlay. 2025-12-05 20:11:53 +04:00
23rd
8c60863e11 Added ability to copy recognized text from media view overlay. 2025-12-05 20:11:53 +04:00
23rd
d5be8c8989 Added initial ability to recognize text to media view overlay. 2025-12-05 20:11:53 +04:00
23rd
255b30e88a Added initial implementation of text recognition for macOS. 2025-12-05 20:11:53 +04:00
23rd
d2dd124be0 Added dummy platform files for text recognition. 2025-12-05 20:11:53 +04:00
23rd
1053b30a6d Added api support for max count of passkeys for accounts. 2025-12-05 18:15:54 +04:00
23rd
e7c1073e13 Added initial error handler to passkeys processing. 2025-12-05 18:15:54 +04:00
23rd
0480c6a4af Added initial ability to login with passkey. 2025-12-05 18:15:54 +04:00
23rd
c70a49c0f3 Added initial api support to login with passkey. 2025-12-05 18:15:54 +04:00
John Preston
7840fd481a Use correct icon for passkey in Settings. 2025-12-05 18:15:54 +04:00
23rd
2a8b491c95 Replaced pure text with Text::String in list of passkeys in settings. 2025-12-05 18:15:54 +04:00
23rd
39c4344047 Added divider with description to section of settings for passkeys. 2025-12-05 18:15:54 +04:00
23rd
cdb58e4ebd Added button for new passkeys to section of settings. 2025-12-05 18:15:54 +04:00
23rd
972325fb6d Added menu to simple list of passkeys in section of settings. 2025-12-05 18:15:54 +04:00
23rd
933b6bedc9 Added icons to simple list of passkeys in section of settings. 2025-12-05 18:15:54 +04:00
23rd
2db8a5d00a Added initial simple list of passkeys to section of settings. 2025-12-05 18:15:54 +04:00
23rd
be043ea349 Added initial api support to delete passkey. 2025-12-05 18:15:54 +04:00
23rd
e531abf31b Added header with lottie to section of settings for passkeys. 2025-12-05 18:15:54 +04:00
23rd
a6af680e59 Added initial entry point to section of settings for passkeys. 2025-12-05 18:15:54 +04:00
23rd
8292334c9b Moved out Settings::Passkeys class from header. 2025-12-05 18:15:54 +04:00
23rd
f20c5a1d3c Added initial box as entry point for passkey creation. 2025-12-05 18:15:53 +04:00
23rd
d1e2ec0309 Added initial api support to request of passkeys for account. 2025-12-05 18:15:53 +04:00
23rd
fe91cae8bc Added initial api support to register passkey. 2025-12-05 18:15:53 +04:00
23rd
7bb30bc4a8 Added initial dummy files for component of passkeys. 2025-12-05 18:15:53 +04:00
23rd
2d41d5903b Added initial dummy files for settings section of passkeys. 2025-12-05 18:15:53 +04:00
23rd
df672ffaf5 Added phrases for settings section of passkeys. 2025-12-05 18:15:53 +04:00
23rd
cb100623fb Added initial macOS API support for passkeys. 2025-12-05 18:15:53 +04:00
23rd
30ef7270b3 Added initial Win API support for passkeys. 2025-12-05 18:15:53 +04:00
23rd
91694a69eb Added initial implementation of deserialization data for passkeys. 2025-12-05 18:15:53 +04:00
23rd
cb07bcf0db Added initial dummy platform files for passkeys. 2025-12-05 18:15:53 +04:00
23rd
a31e384409 Added dummy files for credential data of passkeys. 2025-12-05 18:15:53 +04:00
John Preston
4829c6d028 Update API scheme to layer 219. 2025-12-05 18:15:53 +04:00
Ilya Fedin
ccf6a3fb97 Free more disk space in mac action 2025-12-05 09:58:57 +04:00
Ilya Fedin
2005814fca Actually free space in macOS action
Looks like the command was broken all the time but it was unnoticable due to `|| true`
2025-12-05 09:58:57 +04:00
Ilya Fedin
7319665bda Free more disk space in snap action 2025-12-05 09:58:57 +04:00
Ilya Fedin
d74074a21b Stop creating empty TCP log 2025-12-01 19:39:47 +04:00
John Preston
113115f58c Fix selecting gifts for a collection. 2025-12-01 12:20:33 +04:00
John Preston
e84799283d Simplify and fix link wrapping. 2025-12-01 12:20:33 +04:00
John Preston
71272ed2ec Improve view of finished auctions. 2025-12-01 11:38:35 +04:00
Ilya Fedin
6787c338ac Fix Russian bounds
No way it occupies 360° of latitude
2025-12-01 11:30:47 +04:00
Ilya Fedin
774a44ac7e Stop using geocode-glib
It doesn't seem to work properly anymore, it ignores the passed location doing reversed geocoding based on GeoIP only
2025-12-01 11:30:47 +04:00
23rd
29cdd358cc Added regional English/Portuguese spell check support for macOS. 2025-11-29 09:42:12 +03:00
23rd
b9b1bd5e58 Improved once again display of original date in self.
Related commits are 417d151f2c and cbc03d1e45.
2025-11-29 09:17:39 +03:00
23rd
59814aaeb0 Added ability to view stories anonymously from info stories section. 2025-11-29 09:04:42 +03:00
23rd
fd52b3c23b Fixed once again unwanted reset filter on creation of tabs strip.
Regression was introduced in c0bbb669e0.
2025-11-29 08:27:45 +03:00
178 changed files with 6732 additions and 1168 deletions

View File

@@ -74,6 +74,15 @@ jobs:
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
- name: Free up some disk space.
uses: hugoalh/disk-space-optimizer-ghaction@271735125a1b35180620eae7e45c2e9d470c31b0
with:
general_include: ".+"
homebrew_prune: "True"
homebrew_clean: "True"
npm_prune: "True"
npm_clean: "True"
- name: ThirdParty cache.
id: cache-third-party
uses: actions/cache@v4
@@ -95,9 +104,7 @@ jobs:
./$REPO_NAME/Telegram/build/prepare/mac.sh skip-release silent
- name: Free up some disk space.
run: |
cd Libraries
find . -iname "*.dir" -exec rm -rf {} || true \;
run: find Libraries -iwholename "*.dir/*" -delete
- name: Telegram Desktop build.
if: env.ONLY_CACHE == 'false'

View File

@@ -61,16 +61,11 @@ jobs:
sudo lxd waitready
- name: Free up some disk space.
uses: endersonmenezes/free-disk-space@6c4664f43348c8c7011b53488d5ca65e9fc5cd1a
uses: samueldr/more-space-action@97048bd0df83fb05b5257887bdbaffc848887673
with:
remove_android: true
remove_dotnet: true
remove_haskell: true
remove_tool_cache: true
remove_swap: true
remove_packages: "azure-cli google-cloud-cli microsoft-edge-stable google-chrome-stable firefox postgresql* temurin-* *llvm* mysql* dotnet-sdk-*"
remove_packages_one_command: true
remove_folders: "/usr/share/swift /usr/share/miniconda /usr/share/az* /usr/share/glade* /usr/local/lib/node_modules /usr/local/share/chromium /usr/local/share/powershell"
enable-remove-default-apt-patterns: false
enable-lvm-span: true
lvm-span-mountpoint: /var/snap/lxd/common/lxd/storage-pools/default/containers
- name: Telegram Desktop snap build.
run: sudo -u $USER snap run snapcraft --verbosity=debug

View File

@@ -337,6 +337,8 @@ PRIVATE
boxes/star_gift_auction_box.h
boxes/star_gift_box.cpp
boxes/star_gift_box.h
boxes/star_gift_preview_box.cpp
boxes/star_gift_preview_box.h
boxes/star_gift_resale_box.cpp
boxes/star_gift_resale_box.h
boxes/sticker_set_box.cpp
@@ -523,6 +525,8 @@ PRIVATE
data/components/gift_auctions.h
data/components/location_pickers.cpp
data/components/location_pickers.h
data/components/passkeys.cpp
data/components/passkeys.h
data/components/promo_suggestions.cpp
data/components/promo_suggestions.h
data/components/recent_peers.cpp
@@ -1401,6 +1405,7 @@ PRIVATE
platform/linux/specific_linux.h
platform/linux/tray_linux.cpp
platform/linux/tray_linux.h
platform/linux/webauthn_linux.cpp
platform/mac/file_utilities_mac.mm
platform/mac/file_utilities_mac.h
platform/mac/launcher_mac.mm
@@ -1420,6 +1425,7 @@ PRIVATE
platform/mac/specific_mac_p.h
platform/mac/tray_mac.mm
platform/mac/tray_mac.h
platform/mac/webauthn_mac.mm
platform/mac/window_title_mac.mm
platform/mac/touchbar/items/mac_formatter_item.h
platform/mac/touchbar/items/mac_formatter_item.mm
@@ -1454,6 +1460,7 @@ PRIVATE
platform/win/specific_win.h
platform/win/tray_win.cpp
platform/win/tray_win.h
platform/win/webauthn_win.cpp
platform/win/windows_app_user_model_id.cpp
platform/win/windows_app_user_model_id.h
platform/win/windows_dlls.cpp
@@ -1472,6 +1479,7 @@ PRIVATE
platform/platform_overlay_widget.h
platform/platform_specific.h
platform/platform_tray.h
platform/platform_webauthn.h
platform/platform_window_title.h
profile/profile_back_button.cpp
profile/profile_back_button.h
@@ -1557,6 +1565,8 @@ PRIVATE
settings/settings_notifications.h
settings/settings_notifications_type.cpp
settings/settings_notifications_type.h
settings/settings_passkeys.cpp
settings/settings_passkeys.h
settings/settings_power_saving.cpp
settings/settings_power_saving.h
settings/settings_premium.cpp

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 428 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 677 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1019 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 840 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 876 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -378,6 +378,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_intro_qr_step2" = "Go to Settings > Devices > Link Desktop Device";
"lng_intro_qr_step3" = "Scan this image to Log In";
"lng_intro_qr_skip" = "Or log in using your phone number";
"lng_intro_qr_phone" = "Log in using phone number";
"lng_intro_qr_passkey" = "Log in using passkey";
"lng_intro_fragment_title" = "Enter code";
"lng_intro_fragment_about" = "Get the code for {phone_number} in the Anonymous Numbers section on Fragment.";
@@ -1252,6 +1254,27 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_settings_restart_now" = "Restart";
"lng_settings_restart_later" = "Later";
"lng_settings_passkeys_title" = "Passkeys";
"lng_settings_passkeys_about" = "Manage your passkey, stored safely in the cloud service you choose.";
"lng_settings_passkeys_button" = "Add Passkey";
"lng_settings_passkeys_button_about" = "Your passkey is stored securely in your password manager. {link}";
"lng_settings_passkeys_delete_sure_title" = "Delete Passkey";
"lng_settings_passkeys_delete_sure_about" = "Once deleted, this passkey can't be used to log in.";
"lng_settings_passkeys_delete_sure_about2" = "Don't forget to remove it from your password manager too.";
"lng_settings_passkeys_none_title" = "Protect your account";
"lng_settings_passkeys_none_about" = "Log in safely and keep your account secure.";
"lng_settings_passkeys_none_info1_title" = "Create a Passkey";
"lng_settings_passkeys_none_info1_about" = "Make a passkey to sign in easily and safely.";
"lng_settings_passkeys_none_info2_title" = "Log in with face recognition";
"lng_settings_passkeys_none_info2_about" = "Use your face, fingerprint, or screen lock to sign in.";
"lng_settings_passkeys_none_info3_title" = "Store Passkey securely";
"lng_settings_passkeys_none_info3_about" = "Your passkey is stored safely in the cloud service you choose.";
"lng_settings_passkeys_none_button" = "Create Passkey";
"lng_settings_passkeys_created" = "Created {date}";
"lng_settings_passkeys_last_used" = "Last used {date}";
"lng_settings_passkeys_unsigned_error" = "Passkeys are not available in unsigned builds. Please use an official signed version of Telegram Desktop.";
"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";
@@ -2268,8 +2291,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_action_proximity_distance_km#other" = "{count} km";
"lng_action_webview_data_done" = "Data from the \"{text}\" button was transferred to the bot.";
"lng_action_gift_received" = "{user} sent you a gift for {cost}";
"lng_action_gift_received_sold" = "{user} sold you a gift for {cost}";
"lng_action_gift_unique_received" = "{user} sent you a unique collectible item";
"lng_action_gift_sent" = "You sent a gift for {cost}";
"lng_action_gift_sent_sold" = "You sold a gift for {cost}";
"lng_action_gift_unique_sent" = "You sent a unique collectible item";
"lng_action_gift_upgraded" = "{user} turned the gift from you into a unique collectible";
"lng_action_gift_upgraded_channel" = "{user} turned this gift to {channel} into a unique collectible";
@@ -2325,6 +2350,23 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_action_gift_premium_about" = "Subscription for exclusive Telegram features.";
"lng_action_gift_refunded" = "This gift was downgraded because a request to refund the payment related to this gift was made, and the money was returned.";
"lng_action_gift_got_ton" = "Use TON to suggest posts to channels.";
"lng_action_gift_offer" = "{user} offered you {cost} for {name}.";
"lng_action_gift_offer_you" = "You offered {cost} for {name}.";
"lng_action_gift_offer_state_expires" = "This offer expires in {time}.";
"lng_action_gift_offer_time_large" = "{hours} h";
"lng_action_gift_offer_time_medium" = "{hours} h {minutes} m";
"lng_action_gift_offer_time_small" = "{minutes} m";
"lng_action_gift_offer_state_accepted" = "This offer was accepted.";
"lng_action_gift_offer_state_rejected" = "This offer was rejected.";
"lng_action_gift_offer_state_expired" = "This offer has expired.";
"lng_action_gift_offer_sold" = "{user} sold {name} for {cost}.";
"lng_action_gift_offer_sold_you" = "You sold {name} for {cost}.";
"lng_action_gift_offer_decline" = "Reject";
"lng_action_gift_offer_accept" = "Accept";
"lng_action_gift_offer_expired" = "The offer from {user} to buy your {name} for {cost} has expired.";
"lng_action_gift_offer_expired_your" = "Your offer to buy {name} for {cost} has expired.";
"lng_action_gift_offer_declined" = "{user} rejected your offer to buy {name} for {cost}.";
"lng_action_gift_offer_declined_you" = "You rejected {user}'s offer to buy your {name} for {cost}.";
"lng_action_suggested_photo_me" = "You suggested this photo for {user}'s Telegram profile.";
"lng_action_suggested_photo" = "{user} suggests this photo for your Telegram profile.";
"lng_action_suggested_photo_button" = "View Photo";
@@ -2808,6 +2850,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_premium_summary_about_todo_lists" = "Plan, assign, and complete tasks - seamlessly and efficiently.";
"lng_premium_summary_subtitle_peer_colors" = "Name and Profile Colors";
"lng_premium_summary_about_peer_colors" = "Choose a color and logo for your profile and replies to your messages.";
"lng_premium_summary_subtitle_gifts" = "Telegram Gifts";
"lng_premium_summary_about_gifts" = "Gifts are collectible items you can trade or showcase on your profile.";
"lng_premium_summary_bottom_subtitle" = "About Telegram Premium";
"lng_premium_summary_bottom_about" = "While the free version of Telegram already gives its users more than any other messaging application, **Telegram Premium** pushes its capabilities even further.\n\n**Telegram Premium** is a paid option, because most Premium Features require additional expenses from Telegram to third parties such as data center providers and server manufacturers. Contributions from **Telegram Premium** users allow us to cover such costs and also help Telegram stay free for everyone.";
"lng_premium_summary_button" = "Subscribe for {cost} per month";
@@ -3096,6 +3140,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_credits_small_balance_for_message" = "Buy **Stars** to send messages to {user}.";
"lng_credits_small_balance_for_messages" = "Buy **Stars** to send messages.";
"lng_credits_small_balance_for_suggest" = "Buy **Stars** to suggest post to {channel}.";
"lng_credits_small_balance_for_offer" = "Buy **Stars** to offer for this gift.";
"lng_credits_small_balance_for_search" = "Buy **Stars** to search through public posts.";
"lng_credits_small_balance_fallback" = "Buy **Stars** to unlock content and services on Telegram.";
"lng_credits_purchase_blocked" = "Sorry, you can't purchase this item with Telegram Stars.";
@@ -3692,6 +3737,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_gift_stars_premium" = "premium";
"lng_gift_stars_auction" = "auction";
"lng_gift_stars_auction_join" = "Join";
"lng_gift_stars_auction_view" = "View";
"lng_gift_stars_auction_soon" = "soon";
"lng_gift_stars_auction_upgraded" = "upgraded";
"lng_gift_stars_your_left#one" = "{count} left";
"lng_gift_stars_your_left#other" = "{count} left";
"lng_gift_stars_your_finished" = "none left";
@@ -3860,6 +3908,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_gift_upgrade_tradable_about" = "Sell or auction your gift on third-party NFT marketplaces.";
"lng_gift_upgrade_tradable_about_user" = "{name} will be able to sell the gift on Telegram and NFT marketplaces.";
"lng_gift_upgrade_tradable_about_channel" = "Admins of {name} will be able to sell the gift on Telegram and NFT marketplaces.";
"lng_gift_upgrade_wearable_title" = "Wearable";
"lng_gift_upgrade_wearable_about" = "Display gifts on your page and set them as profile covers or statuses.";
"lng_gift_upgrade_button" = "Upgrade for {price}";
"lng_gift_upgrade_decreases" = "Price decreases in {time}";
"lng_gift_upgrade_see_table" = "See how this price will decrease {arrow}";
@@ -3912,6 +3962,25 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_gift_transfer_unlist" = "Unlist";
"lng_gift_transfer_locked_title" = "Action Locked";
"lng_gift_transfer_locked_text" = "Transfer this gift to your Telegram account on Fragment to unlock this action.";
"lng_gift_offer_button" = "Offer to Buy";
"lng_gift_offer_title" = "Offer to Buy";
"lng_gift_offer_stars_about" = "Choose how many Stars you'd like to offer for {name}.";
"lng_gift_offer_ton_about" = "Choose how many TON you'd like to offer for {name}.";
"lng_gift_offer_duration" = "Offer Duration";
"lng_gift_offer_duration_about" = "Choose how long {user} can accept your offer. When the time expires, the amount will be refunded.";
"lng_gift_offer_cost_button" = "Offer {cost}";
"lng_gift_offer_reject_title" = "Reject Offer";
"lng_gift_offer_confirm_reject" = "Are you sure you want to reject the offer from {user}?";
"lng_gift_offer_confirm_accept" = "Do you want to sell {name} to {user} for {cost}?";
"lng_gift_offer_you_get" = "You will receive {cost} after fees.";
"lng_gift_offer_higher" = "The price you are offered is {percent} higher than the average price for {name}.";
"lng_gift_offer_lower" = "The price you are offered is {percent} lower than the average price for {name}.";
"lng_gift_offer_sell_for" = "Sell for {price}";
"lng_gift_offer_confirm_title" = "Confirm Offer";
"lng_gift_offer_confirm_text" = "Do you want to offer {cost} to {user} for {name}?";
"lng_gift_offer_table_offer" = "Offer";
"lng_gift_offer_table_fee" = "Fee";
"lng_gift_offer_table_duration" = "Duration";
"lng_gift_sell_unlist_title" = "Unlist {name}";
"lng_gift_sell_unlist_sure" = "Are you sure you want to unlist your gift?";
"lng_gift_sell_title" = "Price in Stars";
@@ -3939,6 +4008,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_gift_wear_badge_title" = "Radiant Badge";
"lng_gift_wear_badge_about" = "The glittering icon of this item will be displayed next to your name.";
"lng_gift_wear_badge_about_channel" = "The glittering icon of this item will be displayed next to channel's name.";
"lng_gift_wear_design_title" = "Unique Profile Design";
"lng_gift_wear_design_about" = "Your profile page will get the color and the symbol of this item.";
"lng_gift_wear_design_about_channel" = "Your channel page will get the color and the symbol of this item.";
"lng_gift_wear_proof_title" = "Proof of Ownership";
"lng_gift_wear_proof_about" = "Clicking the icon of this item next to your name will show its info and owner.";
"lng_gift_wear_proof_about_channel" = "Clicking the icon of this item next to channel's name will show its info and owner.";
@@ -4018,6 +4090,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_auction_text_link" = "Learn more {arrow}";
"lng_auction_text_ended" = "Auction ended.";
"lng_auction_start_label" = "Started";
"lng_auction_starts_label" = "Starts";
"lng_auction_rounds_label" = "Rounds";
"lng_auction_rounds_exact" = "Round {n}";
"lng_auction_rounds_range" = "Rounds {n}-{last}";
"lng_auction_rounds_seconds#one" = "{count} minute each";
"lng_auction_rounds_seconds#other" = "{count} minutes each";
"lng_auction_rounds_minutes#one" = "{count} minute each";
"lng_auction_rounds_minutes#other" = "{count} minutes each";
"lng_auction_rounds_hours#one" = "{count} hour each";
"lng_auction_rounds_hours#other" = "{count} hours each";
"lng_auction_rounds_extended" = "{duration} + {increase} for late bids in top {n}";
"lng_auction_end_label" = "Ends";
"lng_auction_round_label" = "Current Round";
"lng_auction_round_value" = "{n} of {amount}";
@@ -4032,10 +4115,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_auction_join_time_left" = "{time} left";
"lng_auction_join_time_medium" = "{hours} h {minutes} m";
"lng_auction_join_time_small" = "{minutes} m";
"lng_auction_join_starts_in" = "starts in {time}";
"lng_auction_join_early_bid" = "Place an Early Bid";
"lng_auction_menu_about" = "About";
"lng_auction_menu_copy_link" = "Copy Link";
"lng_auction_menu_share" = "Share";
"lng_auction_bid_title" = "Place a Bid";
"lng_auction_bid_title_early" = "Place an Early Bid";
"lng_auction_bid_subtitle#one" = "Top {count} bidder will win";
"lng_auction_bid_subtitle#other" = "Top {count} bidders will win";
"lng_auction_bid_your" = "your bid";
@@ -4047,6 +4133,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_auction_bid_until" = "until next round";
"lng_auction_bid_left#one" = "left";
"lng_auction_bid_left#other" = "left";
"lng_auction_bid_before_start" = "before start";
"lng_auction_bid_your_title" = "Your bid will be";
"lng_auction_bid_your_outbid" = "You've been outbid";
"lng_auction_bid_your_winning" = "You're winning";
@@ -4083,13 +4170,34 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_auction_bought_title#other" = "{count} Items Bought";
"lng_auction_bought_date" = "Date";
"lng_auction_bought_bid" = "Accepted Bid";
"lng_auction_bought_round" = "Round #{n}";
"lng_auction_bought_in_round" = "{name} in round {n}";
"lng_auction_change_title" = "Change Recipient";
"lng_auction_change_button" = "Change";
"lng_auction_change_already" = "You've already placed a bid on this gift for {name}.";
"lng_auction_change_to" = "Do you want to raise your bid and change the recipient to {name}?";
"lng_auction_change_already_me" = "You've already placed a bid on this gift for yourself.";
"lng_auction_change_to_me" = "Do you want to raise your bid and change the recipient to yourself?";
"lng_auction_preview_name" = "Upcoming Auction";
"lng_auction_preview_learn_gifts" = "Learn more about Telegram Gifts {arrow}";
"lng_auction_preview_variants#one" = "View {emoji} {count} Variant {arrow}";
"lng_auction_preview_variants#other" = "View {emoji} {count} Variants {arrow}";
"lng_auction_preview_random" = "Random Traits";
"lng_auction_preview_selected" = "Selected Traits";
"lng_auction_preview_randomize" = "Randomize Traits";
"lng_auction_preview_model" = "model";
"lng_auction_preview_models#one" = "This collectible features **{count}** unique model.";
"lng_auction_preview_models#other" = "This collectible features **{count}** unique models.";
"lng_auction_preview_models_button" = "Models";
"lng_auction_preview_backdrop" = "backdrop";
"lng_auction_preview_backdrops#one" = "This collectible features **{count}** unique backdrop.";
"lng_auction_preview_backdrops#other" = "This collectible features **{count}** unique backdrops.";
"lng_auction_preview_backdrops_button" = "Backdrops";
"lng_auction_preview_symbol" = "symbol";
"lng_auction_preview_symbols#one" = "This collectible features **{count}** unique symbol.";
"lng_auction_preview_symbols#other" = "This collectible features **{count}** unique symbols.";
"lng_auction_preview_symbols_button" = "Symbols";
"lng_auction_preview_wear" = "More about wearing gifts {arrow}";
"lng_auction_preview_free_upgrade" = "You can upgrade your gift for free, sell it on the market, or set it as your profile cover.";
"lng_accounts_limit_title" = "Limit Reached";
"lng_accounts_limit1#one" = "You have reached the limit of **{count}** connected account.";

View File

@@ -50,6 +50,7 @@
<file alias="rtmp.tgs">../../animations/rtmp.tgs</file>
<file alias="show_or_premium_lastseen.tgs">../../animations/show_or_premium_lastseen.tgs</file>
<file alias="show_or_premium_readtime.tgs">../../animations/show_or_premium_readtime.tgs</file>
<file alias="passkeys.tgs">../../animations/passkeys.tgs</file>
<file alias="profile_muting.tgs">../../animations/profile/profile_muting.tgs</file>
<file alias="profile_unmuting.tgs">../../animations/profile/profile_unmuting.tgs</file>

View File

@@ -10,7 +10,7 @@
<Identity Name="TelegramMessengerLLP.TelegramDesktop"
ProcessorArchitecture="ARCHITECTURE"
Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
Version="6.3.4.0" />
Version="6.3.6.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 6,3,4,0
PRODUCTVERSION 6,3,4,0
FILEVERSION 6,3,6,0
PRODUCTVERSION 6,3,6,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
@@ -62,10 +62,10 @@ BEGIN
BEGIN
VALUE "CompanyName", "Telegram FZ-LLC"
VALUE "FileDescription", "Telegram Desktop"
VALUE "FileVersion", "6.3.4.0"
VALUE "FileVersion", "6.3.6.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2025"
VALUE "ProductName", "Telegram Desktop"
VALUE "ProductVersion", "6.3.4.0"
VALUE "ProductVersion", "6.3.6.0"
END
END
BLOCK "VarFileInfo"

View File

@@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
//
VS_VERSION_INFO VERSIONINFO
FILEVERSION 6,3,4,0
PRODUCTVERSION 6,3,4,0
FILEVERSION 6,3,6,0
PRODUCTVERSION 6,3,6,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
@@ -53,10 +53,10 @@ BEGIN
BEGIN
VALUE "CompanyName", "Telegram FZ-LLC"
VALUE "FileDescription", "Telegram Desktop Updater"
VALUE "FileVersion", "6.3.4.0"
VALUE "FileVersion", "6.3.6.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2025"
VALUE "ProductName", "Telegram Desktop"
VALUE "ProductVersion", "6.3.4.0"
VALUE "ProductVersion", "6.3.6.0"
END
END
BLOCK "VarFileInfo"

View File

@@ -400,7 +400,7 @@ void ActivateBotCommand(ClickHandlerContext context, int row, int column) {
}
}
const auto replyTo = FullReplyTo();
const auto suggest = SuggestPostOptions();
const auto suggest = SuggestOptions();
Window::PeerMenuCreatePoll(
controller,
item->history()->peer,

View File

@@ -14,7 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Api {
MTPSuggestedPost SuggestToMTP(SuggestPostOptions suggest) {
MTPSuggestedPost SuggestToMTP(SuggestOptions suggest) {
using Flag = MTPDsuggestedPost::Flag;
return suggest.exists
? MTP_suggestedPost(

View File

@@ -19,7 +19,7 @@ namespace Api {
inline constexpr auto kScheduledUntilOnlineTimestamp = TimeId(0x7FFFFFFE);
[[nodiscard]] MTPSuggestedPost SuggestToMTP(SuggestPostOptions suggest);
[[nodiscard]] MTPSuggestedPost SuggestToMTP(SuggestOptions suggest);
struct SendOptions {
uint64 price = 0;
@@ -34,7 +34,7 @@ struct SendOptions {
bool invertCaption = false;
bool hideViaBot = false;
crl::time ttlSeconds = 0;
SuggestPostOptions suggest;
SuggestOptions suggest;
friend inline bool operator==(
const SendOptions &,

View File

@@ -848,8 +848,22 @@ std::optional<Data::StarGift> FromTL(
const auto releasedBy = releasedById
? session->data().peer(releasedById).get()
: nullptr;
const auto background = [&] {
if (!data.vbackground()) {
return std::shared_ptr<Data::StarGiftBackground>();
}
const auto &fields = data.vbackground()->data();
using namespace Ui;
return std::make_shared<Data::StarGiftBackground>(
Data::StarGiftBackground{
.center = ColorFromSerialized(fields.vcenter_color()),
.edge = ColorFromSerialized(fields.vedge_color()),
.text = ColorFromSerialized(fields.vtext_color()),
});
};
return std::optional<Data::StarGift>(Data::StarGift{
.id = uint64(data.vid().v),
.background = background(),
.stars = int64(data.vstars().v),
.starsConverted = int64(data.vconvert_stars().v),
.starsToUpgrade = int64(data.vupgrade_stars().value_or_empty()),
@@ -860,10 +874,12 @@ std::optional<Data::StarGift> FromTL(
.resellCount = int(data.vavailability_resale().value_or_empty()),
.auctionSlug = qs(data.vauction_slug().value_or_empty()),
.auctionGiftsPerRound = data.vgifts_per_round().value_or_empty(),
.auctionStartDate = data.vauction_start_date().value_or_empty(),
.limitedLeft = remaining.value_or_empty(),
.limitedCount = total.value_or_empty(),
.perUserTotal = data.vper_user_total().value_or_empty(),
.perUserRemains = data.vper_user_remains().value_or_empty(),
.upgradeVariants = data.vupgrade_variants().value_or_empty(),
.firstSaleDate = data.vfirst_sale_date().value_or_empty(),
.lastSaleDate = data.vlast_sale_date().value_or_empty(),
.lockedUntilDate = data.vlocked_until_date().value_or_empty(),
@@ -930,6 +946,7 @@ std::optional<Data::StarGift> FromTL(
.themeUser = themeUser,
.nanoTonForResale = FindTonForResale(data.vresell_amount()),
.starsForResale = FindStarsForResale(data.vresell_amount()),
.starsMinOffer = data.voffer_min_stars().value_or(-1),
.number = data.vnum().v,
.onlyAcceptTon = data.is_resale_ton_only(),
.canBeTheme = data.is_theme_available(),
@@ -942,6 +959,8 @@ std::optional<Data::StarGift> FromTL(
data.vvalue_currency().value_or_empty()),
.valuePrice = int64(
data.vvalue_amount().value_or_empty()),
.valuePriceUsd = int64(
data.vvalue_usd_amount().value_or_empty()),
})
: nullptr),
.peerColor = colorCollectible,
@@ -963,7 +982,7 @@ std::optional<Data::StarGift> FromTL(
unique->originalDetails = FromTL(session, data);
});
}
return std::make_optional(result);
return std::make_optional(std::move(result));
});
}
@@ -1009,6 +1028,7 @@ std::optional<Data::SavedStarGift> FromTL(
? peerFromMTP(*data.vfrom_id())
: PeerId()),
.date = data.vdate().v,
.giftNum = data.vgift_num().value_or_empty(),
.upgradeSeparate = data.is_upgrade_separate(),
.upgradable = data.is_can_upgrade(),
.anonymous = data.is_name_hidden(),

View File

@@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "apiwrap.h"
#include "base/unixtime.h"
#include "boxes/transfer_gift_box.h"
#include "chat_helpers/message_field.h"
#include "core/click_handler_types.h"
#include "data/components/credits.h"
@@ -44,7 +45,7 @@ void SendApproval(
not_null<HistoryItem*> item,
TimeId scheduleDate = 0) {
using Flag = MTPmessages_ToggleSuggestedPostApproval::Flag;
const auto suggestion = item->Get<HistoryMessageSuggestedPost>();
const auto suggestion = item->Get<HistoryMessageSuggestion>();
if (!suggestion
|| suggestion->accepted
|| suggestion->rejected
@@ -56,7 +57,7 @@ void SendApproval(
const auto session = &show->session();
const auto finish = [=] {
if (const auto item = session->data().message(id)) {
const auto suggestion = item->Get<HistoryMessageSuggestedPost>();
const auto suggestion = item->Get<HistoryMessageSuggestion>();
if (suggestion) {
suggestion->requestId = 0;
}
@@ -83,7 +84,7 @@ void ConfirmApproval(
not_null<HistoryItem*> item,
TimeId scheduleDate = 0,
Fn<void()> accepted = nullptr) {
const auto suggestion = item->Get<HistoryMessageSuggestedPost>();
const auto suggestion = item->Get<HistoryMessageSuggestion>();
if (!suggestion
|| suggestion->accepted
|| suggestion->rejected
@@ -244,7 +245,7 @@ void SendDecline(
not_null<HistoryItem*> item,
const QString &comment) {
using Flag = MTPmessages_ToggleSuggestedPostApproval::Flag;
const auto suggestion = item->Get<HistoryMessageSuggestedPost>();
const auto suggestion = item->Get<HistoryMessageSuggestion>();
if (!suggestion
|| suggestion->accepted
|| suggestion->rejected
@@ -256,7 +257,7 @@ void SendDecline(
const auto session = &show->session();
const auto finish = [=] {
if (const auto item = session->data().message(id)) {
const auto suggestion = item->Get<HistoryMessageSuggestedPost>();
const auto suggestion = item->Get<HistoryMessageSuggestion>();
if (suggestion) {
suggestion->requestId = 0;
}
@@ -365,10 +366,10 @@ void SendSuggest(
std::shared_ptr<Main::SessionShow> show,
not_null<HistoryItem*> item,
std::shared_ptr<SendSuggestState> state,
Fn<void(SuggestPostOptions&)> modify,
Fn<void(SuggestOptions&)> modify,
Fn<void()> done = nullptr,
int starsApproved = 0) {
const auto suggestion = item->Get<HistoryMessageSuggestedPost>();
const auto suggestion = item->Get<HistoryMessageSuggestion>();
const auto id = item->fullId();
const auto withPaymentApproved = [=](int stars) {
if (const auto item = show->session().data().message(id)) {
@@ -416,7 +417,7 @@ void SendSuggest(
void SuggestApprovalDate(
std::shared_ptr<Main::SessionShow> show,
not_null<HistoryItem*> item) {
const auto suggestion = item->Get<HistoryMessageSuggestedPost>();
const auto suggestion = item->Get<HistoryMessageSuggestion>();
if (!suggestion) {
return;
}
@@ -437,7 +438,7 @@ void SuggestApprovalDate(
show,
item,
state,
[=](SuggestPostOptions &options) { options.date = result; },
[=](SuggestOptions &options) { options.date = result; },
close);
};
using namespace HistoryView;
@@ -454,12 +455,12 @@ void SuggestApprovalDate(
void SuggestOfferForMessage(
std::shared_ptr<Main::SessionShow> show,
not_null<HistoryItem*> item,
SuggestPostOptions values,
SuggestOptions values,
HistoryView::SuggestMode mode) {
const auto id = item->fullId();
const auto state = std::make_shared<SendSuggestState>();
const auto weak = std::make_shared<base::weak_qptr<Ui::BoxContent>>();
const auto done = [=](SuggestPostOptions result) {
const auto done = [=](SuggestOptions result) {
const auto item = show->session().data().message(id);
if (!item) {
return;
@@ -473,7 +474,7 @@ void SuggestOfferForMessage(
show,
item,
state,
[=](SuggestPostOptions &options) { options = result; },
[=](SuggestOptions &options) { options = result; },
close);
};
using namespace HistoryView;
@@ -490,7 +491,7 @@ void SuggestOfferForMessage(
void SuggestApprovalPrice(
std::shared_ptr<Main::SessionShow> show,
not_null<HistoryItem*> item) {
const auto suggestion = item->Get<HistoryMessageSuggestedPost>();
const auto suggestion = item->Get<HistoryMessageSuggestion>();
if (!suggestion) {
return;
}
@@ -504,6 +505,20 @@ void SuggestApprovalPrice(
}, SuggestMode::Change);
}
void ConfirmGiftSaleAccept(
not_null<Window::SessionController*> window,
not_null<HistoryItem*> item,
not_null<HistoryMessageSuggestion*> suggestion) {
ShowGiftSaleAcceptBox(window, item, suggestion);
}
void ConfirmGiftSaleDecline(
not_null<Window::SessionController*> window,
not_null<HistoryItem*> item,
not_null<HistoryMessageSuggestion*> suggestion) {
ShowGiftSaleRejectBox(window, item, suggestion);
}
} // namespace
std::shared_ptr<ClickHandler> AcceptClickHandler(
@@ -521,9 +536,11 @@ std::shared_ptr<ClickHandler> AcceptClickHandler(
return;
}
const auto show = controller->uiShow();
const auto suggestion = item->Get<HistoryMessageSuggestedPost>();
const auto suggestion = item->Get<HistoryMessageSuggestion>();
if (!suggestion) {
return;
} else if (suggestion->gift) {
ConfirmGiftSaleAccept(controller, item, suggestion);
} else if (!suggestion->date) {
RequestApprovalDate(show, item);
} else {
@@ -546,7 +563,12 @@ std::shared_ptr<ClickHandler> DeclineClickHandler(
if (!item) {
return;
}
RequestDeclineComment(controller->uiShow(), item);
const auto suggestion = item->Get<HistoryMessageSuggestion>();
if (suggestion && suggestion->gift) {
ConfirmGiftSaleDecline(controller, item, suggestion);
} else {
RequestDeclineComment(controller->uiShow(), item);
}
});
}
@@ -573,7 +595,7 @@ std::shared_ptr<ClickHandler> SuggestChangesClickHandler(
if (!item) {
return;
}
const auto suggestion = item->Get<HistoryMessageSuggestedPost>();
const auto suggestion = item->Get<HistoryMessageSuggestion>();
if (!suggestion) {
return;
}
@@ -594,7 +616,7 @@ std::shared_ptr<ClickHandler> SuggestChangesClickHandler(
.messageId = FullMsgId(history->peer->id, item->id),
.monoforumPeerId = monoforumPeerId,
},
SuggestPostOptions{
SuggestOptions{
.exists = uint32(1),
.priceWhole = uint32(suggestion->price.whole()),
.priceNano = uint32(suggestion->price.nano()),

View File

@@ -24,7 +24,7 @@ void ToggleExistingMedia(
Data::FileOrigin origin,
ToggleRequestCallback toggleRequest,
DoneCallback &&done) {
const auto api = &document->owner().session().api();
const auto api = &document->session().api();
auto performRequest = [=](const auto &repeatRequest) -> void {
const auto usedFileReference = document->fileReference();

View File

@@ -3483,6 +3483,9 @@ void ApiWrap::forwardMessages(
flags |= MessageFlag::ShortcutMessage;
sendFlags |= SendFlag::f_quick_reply_shortcut;
}
if (action.options.effectId) {
sendFlags |= SendFlag::f_effect;
}
if (draft.options != Data::ForwardOptions::PreserveInfo) {
sendFlags |= SendFlag::f_drop_author;
}
@@ -3548,6 +3551,7 @@ void ApiWrap::forwardMessages(
MTP_int(action.options.scheduleRepeatPeriod),
(sendAs ? sendAs->input : MTP_inputPeerEmpty()),
Data::ShortcutIdToMTP(_session, action.options.shortcutId),
MTP_long(action.options.effectId),
MTPint(), // video_timestamp
MTP_long(starsPaid),
Api::SuggestToMTP(action.options.suggest)

View File

@@ -232,7 +232,7 @@ EditCaptionBox::EditCaptionBox(
not_null<Window::SessionController*> controller,
not_null<HistoryItem*> item,
TextWithTags &&text,
SuggestPostOptions suggest,
SuggestOptions suggest,
bool spoilered,
bool invertCaption,
Ui::PreparedList &&list,
@@ -273,7 +273,7 @@ void EditCaptionBox::StartMediaReplace(
not_null<Window::SessionController*> controller,
FullMsgId itemId,
TextWithTags text,
SuggestPostOptions suggest,
SuggestOptions suggest,
bool spoilered,
bool invertCaption,
Fn<void()> saved) {
@@ -304,7 +304,7 @@ void EditCaptionBox::StartMediaReplace(
FullMsgId itemId,
Ui::PreparedList &&list,
TextWithTags text,
SuggestPostOptions suggest,
SuggestOptions suggest,
bool spoilered,
bool invertCaption,
Fn<void()> saved) {
@@ -353,7 +353,7 @@ void EditCaptionBox::StartPhotoEdit(
std::shared_ptr<Data::PhotoMedia> media,
FullMsgId itemId,
TextWithTags text,
SuggestPostOptions suggest,
SuggestOptions suggest,
bool spoilered,
bool invertCaption,
Fn<void()> saved) {

View File

@@ -39,7 +39,7 @@ public:
not_null<Window::SessionController*> controller,
not_null<HistoryItem*> item,
TextWithTags &&text,
SuggestPostOptions suggest,
SuggestOptions suggest,
bool spoilered,
bool invertCaption,
Ui::PreparedList &&list,
@@ -50,7 +50,7 @@ public:
not_null<Window::SessionController*> controller,
FullMsgId itemId,
TextWithTags text,
SuggestPostOptions suggest,
SuggestOptions suggest,
bool spoilered,
bool invertCaption,
Fn<void()> saved);
@@ -59,7 +59,7 @@ public:
FullMsgId itemId,
Ui::PreparedList &&list,
TextWithTags text,
SuggestPostOptions suggest,
SuggestOptions suggest,
bool spoilered,
bool invertCaption,
Fn<void()> saved);
@@ -68,7 +68,7 @@ public:
std::shared_ptr<Data::PhotoMedia> media,
FullMsgId itemId,
TextWithTags text,
SuggestPostOptions suggest,
SuggestOptions suggest,
bool spoilered,
bool invertCaption,
Fn<void()> saved);
@@ -117,7 +117,7 @@ private:
const not_null<Window::SessionController*> _controller;
const not_null<HistoryItem*> _historyItem;
const SuggestPostOptions _suggest;
const SuggestOptions _suggest;
const bool _isAllowedEditMedia;
const Ui::AlbumType _albumType;

View File

@@ -97,6 +97,7 @@ using RightsMap = std::vector<std::pair<ChatAdminRight, tr::phrase<>>>;
{ Flag::ManageCall, tr::lng_request_channel_manage_livestreams },
{ Flag::ManageDirect, tr::lng_request_channel_manage_direct },
{ Flag::AddAdmins, tr::lng_request_channel_add_admins },
{ Flag::BanUsers, tr::lng_request_group_ban_users },
};
}

View File

@@ -246,7 +246,8 @@ ChatAdminRightsInfo EditAdminBox::defaultRights() const {
| Flag::DeleteStories
| Flag::InviteByLinkOrAdd
| Flag::ManageCall
| Flag::ManageDirect) };
| Flag::ManageDirect
| Flag::BanUsers) };
}
void EditAdminBox::prepare() {

View File

@@ -117,13 +117,7 @@ base::unique_qptr<Ui::RpWidget> CreateEmptyPlaceholder(
tr::lng_gift_stars_tabs_my_empty_next(
lt_emoji,
rpl::single(Ui::Text::IconEmoji(&st::textMoreIconEmoji)),
TextWithEntities::Simple
) | rpl::map([](TextWithEntities t) {
return Ui::Text::Wrapped(
std::move(t),
EntityType::Url,
u"internal:"_q);
}),
tr::link),
st::giftBoxGiftEmptyLabel)
: nullptr;
if (emptyNextLabel) {

View File

@@ -174,6 +174,7 @@ constexpr auto kDefaultChargeStars = 10;
{ Flag::ManageCall, tr::lng_rights_channel_manage_calls(tr::now) },
{ Flag::ManageDirect, tr::lng_rights_channel_manage_direct(tr::now) },
{ Flag::AddAdmins, tr::lng_rights_add_admins(tr::now) },
{ Flag::BanUsers, tr::lng_rights_group_ban(tr::now) },
};
return {
{ std::nullopt, std::move(first) },

View File

@@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_streaming.h"
#include "data/data_peer_values.h"
#include "data/data_premium_limits.h"
#include "info/profile/info_profile_icon.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "main/main_domain.h" // kMaxAccounts
@@ -43,7 +44,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "window/window_session_controller.h"
#include "api/api_premium.h"
#include "apiwrap.h"
#include "styles/style_credits.h" // upgradeGiftSubtext
#include "styles/style_info.h" // infoStarsUnderstood
#include "styles/style_layers.h"
#include "styles/style_menu_icons.h"
#include "styles/style_premium.h"
#include "styles/style_settings.h"
@@ -137,6 +141,8 @@ void PreloadSticker(const std::shared_ptr<Data::DocumentMedia> &media) {
return tr::lng_premium_summary_subtitle_todo_lists();
case PremiumFeature::PeerColors:
return tr::lng_premium_summary_subtitle_peer_colors();
case PremiumFeature::Gifts:
return tr::lng_premium_summary_subtitle_gifts();
case PremiumFeature::BusinessLocation:
return tr::lng_business_subtitle_location();
@@ -206,6 +212,8 @@ void PreloadSticker(const std::shared_ptr<Data::DocumentMedia> &media) {
return tr::lng_premium_summary_about_todo_lists();
case PremiumFeature::PeerColors:
return tr::lng_premium_summary_about_peer_colors();
case PremiumFeature::Gifts:
return tr::lng_premium_summary_about_gifts();
case PremiumFeature::BusinessLocation:
return tr::lng_business_about_location();
@@ -548,6 +556,7 @@ struct VideoPreviewDocument {
case PremiumFeature::Effects: return "effects";
case PremiumFeature::TodoLists: return "todo";
case PremiumFeature::PeerColors: return "peer_colors";
case PremiumFeature::Gifts: return "gifts";
case PremiumFeature::BusinessLocation: return "business_location";
case PremiumFeature::BusinessHours: return "business_hours";
@@ -894,6 +903,45 @@ struct VideoPreviewDocument {
return result;
}
void AddGiftsInfoRows(not_null<Ui::VerticalLayout*> container) {
const auto infoRow = [&](
rpl::producer<QString> title,
rpl::producer<QString> text,
not_null<const style::icon*> icon) {
auto raw = container->add(
object_ptr<Ui::VerticalLayout>(container));
raw->add(
object_ptr<Ui::FlatLabel>(
raw,
std::move(title) | Ui::Text::ToBold(),
st::defaultFlatLabel),
st::settingsPremiumRowTitlePadding);
raw->add(
object_ptr<Ui::FlatLabel>(
raw,
std::move(text),
st::upgradeGiftSubtext),
st::settingsPremiumRowAboutPadding);
object_ptr<Info::Profile::FloatingIcon>(
raw,
*icon,
st::starrefInfoIconPosition);
};
infoRow(
tr::lng_gift_upgrade_unique_title(),
tr::lng_gift_upgrade_unique_about(),
&st::menuIconUnique);
infoRow(
tr::lng_gift_upgrade_tradable_title(),
tr::lng_gift_upgrade_tradable_about(),
&st::menuIconTradable);
infoRow(
tr::lng_gift_upgrade_wearable_title(),
tr::lng_gift_upgrade_wearable_about(),
&st::menuIconNftWear);
}
void PreviewBox(
not_null<Ui::GenericBox*> box,
std::shared_ptr<ChatHelpers::Show> show,
@@ -956,22 +1004,32 @@ void PreviewBox(
st::settingsPremiumTopBarClose);
close->setClickedCallback([=] { box->closeBox(); });
const auto left = Ui::CreateChild<Ui::IconButton>(
const auto gifts = (state->selected.current() == PremiumFeature::Gifts);
const auto left = gifts ? nullptr : Ui::CreateChild<Ui::IconButton>(
buttonsParent,
st::settingsPremiumMoveLeft);
left->setClickedCallback([=] { move(-1); });
if (left) {
left->setClickedCallback([=] { move(-1); });
}
const auto right = Ui::CreateChild<Ui::IconButton>(
const auto right = gifts ? nullptr : Ui::CreateChild<Ui::IconButton>(
buttonsParent,
st::settingsPremiumMoveRight);
right->setClickedCallback([=] { move(1); });
if (right) {
right->setClickedCallback([=] { move(1); });
}
buttonsParent->widthValue(
) | rpl::start_with_next([=](int width) {
const auto outerHeight = st::premiumPreviewHeight;
close->moveToRight(0, 0, width);
left->moveToLeft(0, (outerHeight - left->height()) / 2, width);
right->moveToRight(0, (outerHeight - right->height()) / 2, width);
if (left) {
left->moveToLeft(0, (outerHeight - left->height()) / 2, width);
}
if (right) {
right->moveToRight(0, (outerHeight - right->height()) / 2, width);
}
}, close->lifetime());
state->preload = [=] {
@@ -1099,16 +1157,30 @@ void PreviewBox(
st::premiumPreviewAboutPadding,
style::al_top
)->setTryMakeSimilarLines(true);
box->addRow(
CreateSwitch(box->verticalLayout(), &state->selected, state->order),
st::premiumDotsMargin);
if (gifts) {
box->setStyle(st::giftBox);
AddGiftsInfoRows(box->verticalLayout());
} else {
box->addRow(
CreateSwitch(box->verticalLayout(), &state->selected, state->order),
st::premiumDotsMargin);
}
const auto showFinished = [=] {
state->showFinished = true;
if (base::take(state->preloadScheduled)) {
state->preload();
}
};
if ((descriptor.fromSettings && show->session().premium())
if (gifts) {
box->setShowFinishedCallback(showFinished);
box->addButton(
rpl::single(QString()),
[=] { box->closeBox(); }
)->setText(rpl::single(Ui::Text::IconEmoji(
&st::infoStarsUnderstood
).append(' ').append(tr::lng_auction_about_understood(tr::now))));
} else if ((descriptor.fromSettings && show->session().premium())
|| descriptor.hideSubscriptionButton) {
box->setShowFinishedCallback(showFinished);
box->addButton(tr::lng_close(), [=] { box->closeBox(); });

View File

@@ -74,6 +74,7 @@ enum class PremiumFeature {
FilterTags,
TodoLists,
PeerColors,
Gifts,
// Business features.
BusinessLocation,

View File

@@ -121,7 +121,7 @@ namespace {
return true;
};
if (!updateThumbnail()) {
document->owner().session().downloaderTaskFinished(
document->session().downloaderTaskFinished(
) | rpl::start_with_next([=] {
if (updateThumbnail()) {
state->loadingLifetime.destroy();

View File

@@ -1731,14 +1731,14 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback(
}
return result;
};
auto &api = history->owner().session().api();
auto &api = history->session().api();
auto &histories = history->owner().histories();
const auto donePhraseArgs = CreateForwardedMessagePhraseArgs(
result,
msgIds);
const auto showRecentForwardsToSelf = result.size() == 1
&& result.front()->peer()->isSelf()
&& history->owner().session().premium();
&& history->session().premium();
const auto requestType = Data::Histories::RequestType::Send;
for (const auto thread : result) {
if (!comment.text.isEmpty()) {
@@ -1776,7 +1776,8 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback(
: Flag(0))
| (starsPaid ? Flag::f_allow_paid_stars : Flag())
| (sublistPeer ? Flag::f_reply_to : Flag())
| (options.suggest ? Flag::f_suggested_post : Flag());
| (options.suggest ? Flag::f_suggested_post : Flag())
| (options.effectId ? Flag::f_effect : Flag());
threadHistory->sendRequestId = api.request(
MTPmessages_ForwardMessages(
MTP_flags(sendFlags),
@@ -1792,6 +1793,7 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback(
MTP_int(options.scheduleRepeatPeriod),
MTP_inputPeerEmpty(), // send_as
Data::ShortcutIdToMTP(session, options.shortcutId),
MTP_long(options.effectId),
MTP_int(videoTimestamp.value_or(0)),
MTP_long(starsPaid),
Api::SuggestToMTP(options.suggest)

View File

@@ -8,12 +8,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/star_gift_auction_box.h"
#include "api/api_text_entities.h"
#include "base/random.h"
#include "base/timer_rpl.h"
#include "base/unixtime.h"
#include "boxes/peers/replace_boost_box.h"
#include "boxes/premium_preview_box.h"
#include "boxes/send_credits_box.h" // CreditsEmojiSmall
#include "boxes/share_box.h"
#include "boxes/star_gift_box.h"
#include "boxes/star_gift_preview_box.h"
#include "boxes/star_gift_resale_box.h"
#include "calls/group/calls_group_common.h"
#include "core/application.h"
#include "core/credits_amount.h"
@@ -74,6 +78,7 @@ namespace {
constexpr auto kAuctionAboutShownPref = "gift_auction_about_shown"_cs;
constexpr auto kBidPlacedToastDuration = 5 * crl::time(1000);
constexpr auto kSwitchPreviewCoverInterval = 3 * crl::time(1000);
constexpr auto kMaxShownBid = 30'000;
constexpr auto kShowTopPlaces = 3;
@@ -288,33 +293,39 @@ struct BidSliderValues {
return result;
}
Fn<void()> MakeAuctionMenuCallback(
not_null<QWidget*> parent,
Fn<void(not_null<Ui::PopupMenu*>)> MakeAuctionFillMenuCallback(
std::shared_ptr<ChatHelpers::Show> show,
const Data::GiftAuctionState &state) {
const auto url = show->session().createInternalLinkFull(
u"auction/"_q + state.gift->auctionSlug);
const auto rounds = state.totalRounds;
const auto perRound = state.gift->auctionGiftsPerRound;;
const auto menu = std::make_shared<base::unique_qptr<PopupMenu>>();
return [=] {
*menu = base::make_unique_q<Ui::PopupMenu>(
parent,
st::popupMenuWithIcons);
(*menu)->addAction(tr::lng_auction_menu_about(tr::now), [=] {
return [=](not_null<Ui::PopupMenu*> menu) {
menu->addAction(tr::lng_auction_menu_about(tr::now), [=] {
show->show(Box(AuctionAboutBox, rounds, perRound, nullptr));
}, &st::menuIconInfo);
(*menu)->addAction(tr::lng_auction_menu_copy_link(tr::now), [=] {
menu->addAction(tr::lng_auction_menu_copy_link(tr::now), [=] {
QApplication::clipboard()->setText(url);
show->showToast(tr::lng_username_copied(tr::now));
}, &st::menuIconLink);
(*menu)->addAction(tr::lng_auction_menu_share(tr::now), [=] {
menu->addAction(tr::lng_auction_menu_share(tr::now), [=] {
FastShareLink(show, url);
}, &st::menuIconShare);
};
}
Fn<void()> MakeAuctionMenuCallback(
not_null<QWidget*> parent,
std::shared_ptr<ChatHelpers::Show> show,
const Data::GiftAuctionState &state) {
const auto menu = std::make_shared<base::unique_qptr<PopupMenu>>();
return [=, fill = MakeAuctionFillMenuCallback(show, state)] {
*menu = base::make_unique_q<Ui::PopupMenu>(
parent,
st::popupMenuWithIcons);
fill(menu->get());
(*menu)->popup(QCursor::pos());
};
}
@@ -397,11 +408,23 @@ object_ptr<RpWidget> MakeAuctionInfoBlocks(
auto untilTitle = rpl::duplicate(
stateValue
) | rpl::map([=](const Data::GiftAuctionState &state) {
return SecondsLeftTillValue(state.nextRoundAt
? state.nextRoundAt
: state.endDate);
return SecondsLeftTillValue(state.startDate) | rpl::then(
SecondsLeftTillValue(state.nextRoundAt
? state.nextRoundAt
: state.endDate));
}) | rpl::flatten_latest(
) | rpl::map(NiceCountdownText) | rpl::map(tr::marked);
auto untilSubtext = rpl::duplicate(
stateValue
) | rpl::map([=](const Data::GiftAuctionState &state) {
auto preview = SecondsLeftTillValue(
state.startDate
) | rpl::map(rpl::mappers::_1 > 0) | rpl::distinct_until_changed();
return rpl::conditional(
std::move(preview),
tr::lng_auction_bid_before_start(),
tr::lng_auction_bid_until());
}) | rpl::flatten_latest();
auto leftTitle = rpl::duplicate(
stateValue
) | rpl::map([=](const Data::GiftAuctionState &state) {
@@ -424,7 +447,7 @@ object_ptr<RpWidget> MakeAuctionInfoBlocks(
},
{
.title = std::move(untilTitle),
.subtext = tr::lng_auction_bid_until(),
.subtext = std::move(untilSubtext),
},
{
.title = std::move(leftTitle),
@@ -446,6 +469,7 @@ void AddBidPlaces(
};
struct State {
rpl::variable<My> my;
rpl::variable<bool> started;
rpl::variable<std::vector<BidRowData>> top;
std::vector<Ui::PeerUserpicView> cache;
int winners = 0;
@@ -462,6 +486,9 @@ void AddBidPlaces(
}
state->winners = value.gift->auctionGiftsPerRound;
state->cache = std::move(cache);
state->started = SecondsLeftTillValue(
value.startDate
) | rpl::map(!rpl::mappers::_1);
}, box->lifetime());
state->my = rpl::combine(
@@ -513,7 +540,13 @@ void AddBidPlaces(
top.push_back({ show->session().user(), chosen });
return finishWith((levels.empty() ? 0 : levels.back().position) + 1);
});
auto myLabelText = state->my.value() | rpl::map([](My my) {
auto myLabelText = rpl::combine(
state->my.value(),
state->started.value()
) | rpl::map([](My my, bool started) {
if (!started) {
return tr::lng_auction_bid_your_title();
}
switch (my.type) {
case BidType::Setting: return tr::lng_auction_bid_your_title();
case BidType::Winning: return tr::lng_auction_bid_your_winning();
@@ -609,10 +642,22 @@ void AuctionBidBox(not_null<GenericBox*> box, AuctionBidBoxArgs &&args) {
rpl::variable<BidSliderValues> sliderValues;
rpl::variable<int> chosen;
rpl::variable<QString> subtext;
rpl::variable<bool> started;
bool placing = false;
};
const auto state = box->lifetime().make_state<State>(
std::move(args.state));
state->started = state->value.value(
) | rpl::map([=](const Data::GiftAuctionState &value) {
return value.startDate;
}) | rpl::distinct_until_changed(
) | rpl::map([=](TimeId startTime) {
return SecondsLeftTillValue(
startTime
) | rpl::map([=](int seconds) {
return !seconds;
});
}) | rpl::flatten_latest();
state->sliderValues = state->value.value(
) | rpl::map([=](const Data::GiftAuctionState &value) {
const auto mine = int(value.my.bid);
@@ -780,7 +825,10 @@ void AuctionBidBox(not_null<GenericBox*> box, AuctionBidBoxArgs &&args) {
box->addRow(
object_ptr<FlatLabel>(
box,
tr::lng_auction_bid_title(),
rpl::conditional(
state->started.value(),
tr::lng_auction_bid_title(),
tr::lng_auction_bid_title_early()),
st::boostCenteredTitle),
st::boxRowPadding + QMargins(0, skip / 2, 0, 0),
style::al_top);
@@ -906,6 +954,30 @@ void AuctionBidBox(not_null<GenericBox*> box, AuctionBidBoxArgs &&args) {
helper.context());
}
[[nodiscard]] std::vector<int> RandomIndicesSubset(int total, int subset) {
const auto take = std::min(total, subset);
if (!take) {
return {};
}
auto result = std::vector<int>();
auto taken = base::flat_set<int>();
result.reserve(take);
taken.reserve(take);
for (auto i = 0; i < take; ++i) {
auto index = base::RandomIndex(total - i);
for (const auto already : taken) {
if (index >= already) {
++index;
} else {
break;
}
}
taken.emplace(index);
result.push_back(index);
}
return result;
}
[[nodiscard]] object_ptr<TableLayout> AuctionInfoTable(
not_null<QWidget*> parent,
not_null<VerticalLayout*> container,
@@ -921,6 +993,7 @@ void AuctionBidBox(not_null<GenericBox*> box, AuctionBidBoxArgs &&args) {
state->value = std::move(value);
const auto &now = state->value.current();
const auto preview = (now.startDate > base::unixtime::now());
const auto name = now.gift->resellTitle;
state->finished = now.finished()
? (rpl::single(true) | rpl::type_erased())
@@ -932,10 +1005,12 @@ void AuctionBidBox(not_null<GenericBox*> box, AuctionBidBoxArgs &&args) {
};
AddTableRow(
raw,
rpl::conditional(
state->finished.value(),
tr::lng_gift_link_label_first_sale(),
tr::lng_auction_start_label()),
(preview
? tr::lng_auction_starts_label()
: rpl::conditional(
state->finished.value(),
tr::lng_gift_link_label_first_sale(),
tr::lng_auction_start_label())),
date(now.startDate));
AddTableRow(
raw,
@@ -944,65 +1019,126 @@ void AuctionBidBox(not_null<GenericBox*> box, AuctionBidBoxArgs &&args) {
tr::lng_gift_link_label_last_sale(),
tr::lng_auction_end_label()),
date(now.endDate));
auto roundText = state->value.value(
) | rpl::map([](const Data::GiftAuctionState &state) {
const auto wrapped = [](int count) {
return rpl::single(tr::marked(Lang::FormatCountDecimal(count)));
if (preview) {
AddTableRow(
raw,
tr::lng_gift_unique_availability_label(),
rpl::single(tr::marked(
Lang::FormatCountDecimal(now.gift->limitedCount))));
AddTableRow(
raw,
tr::lng_auction_rounds_label(),
rpl::single(tr::marked(
Lang::FormatCountDecimal(now.totalRounds))));
const auto formatDuration = [&](TimeId value, bool exact) {
return (!(value % 3600))
? (exact ? tr::lng_hours : tr::lng_auction_rounds_hours)(
tr::now,
lt_count,
value / 3600)
: (!(value % 60))
? (exact ? tr::lng_minutes : tr::lng_auction_rounds_minutes)(
tr::now,
lt_count,
value / 60)
: (exact ? tr::lng_seconds : tr::lng_auction_rounds_seconds)(
tr::now,
lt_count,
value);
};
return tr::lng_auction_round_value(
lt_n,
wrapped(state.currentRound),
lt_amount,
wrapped(state.totalRounds),
tr::marked);
}) | rpl::flatten_latest();
const auto round = AddTableRow(
raw,
tr::lng_auction_round_label(),
std::move(roundText));
auto availabilityText = state->value.value(
) | rpl::map([](const Data::GiftAuctionState &state) {
const auto wrapped = [](int count) {
return rpl::single(tr::marked(Lang::FormatCountDecimal(count)));
};
return tr::lng_auction_availability_value(
lt_n,
wrapped(state.giftsLeft),
lt_amount,
wrapped(state.gift->limitedCount),
tr::marked);
}) | rpl::flatten_latest();
AddTableRow(
raw,
tr::lng_auction_availability_label(),
std::move(availabilityText));
const auto tooltip = std::make_shared<TableRowTooltipData>(
TableRowTooltipData{ .parent = container });
state->value.value(
) | rpl::map([](const Data::GiftAuctionState &state) {
return state.averagePrice;
}) | rpl::filter(
rpl::mappers::_1 != 0
) | rpl::take(
1
) | rpl::start_with_next([=](int64 price) {
delete round;
raw->insertRow(
2,
object_ptr<FlatLabel>(
for (auto i = 0, n = int(now.roundParameters.size()); i != n; ++i) {
const auto &that = now.roundParameters[i];
const auto next = (i + 1 < n)
? now.roundParameters[i + 1]
: Data::GiftAuctionRound{ now.totalRounds + 1 };
const auto exact = (next.number == that.number + 1);
const auto extended = that.extendTop && that.extendDuration;
const auto duration = formatDuration(that.duration, exact);
const auto value = extended
? tr::lng_auction_rounds_extended(
tr::now,
lt_duration,
duration,
lt_increase,
formatDuration(that.extendDuration, true),
lt_n,
QString::number(that.extendTop))
: duration;
AddTableRow(
raw,
tr::lng_auction_average_label(),
raw->st().defaultLabel),
MakeAveragePriceValue(raw, tooltip, name, price),
st::giveawayGiftCodeLabelMargin,
st::giveawayGiftCodeValueMargin);
raw->resizeToWidth(raw->widthNoMargins());
}, raw->lifetime());
(exact
? tr::lng_auction_rounds_exact(
lt_n,
rpl::single(QString::number(that.number)))
: tr::lng_auction_rounds_range(
lt_n,
rpl::single(QString::number(that.number)),
lt_last,
rpl::single(QString::number(next.number - 1)))),
object_ptr<FlatLabel>(
raw,
value,
st::auctionInfoValueMultiline));
}
} else {
auto roundText = state->value.value(
) | rpl::map([](const Data::GiftAuctionState &state) {
const auto wrapped = [](int count) {
return rpl::single(tr::marked(Lang::FormatCountDecimal(count)));
};
return tr::lng_auction_round_value(
lt_n,
wrapped(state.currentRound),
lt_amount,
wrapped(state.totalRounds),
tr::marked);
}) | rpl::flatten_latest();
const auto round = AddTableRow(
raw,
tr::lng_auction_round_label(),
std::move(roundText));
auto availabilityText = state->value.value(
) | rpl::map([](const Data::GiftAuctionState &state) {
const auto wrapped = [](int count) {
return rpl::single(tr::marked(Lang::FormatCountDecimal(count)));
};
return tr::lng_auction_availability_value(
lt_n,
wrapped(state.giftsLeft),
lt_amount,
wrapped(state.gift->limitedCount),
tr::marked);
}) | rpl::flatten_latest();
AddTableRow(
raw,
tr::lng_auction_availability_label(),
std::move(availabilityText));
const auto tooltip = std::make_shared<TableRowTooltipData>(
TableRowTooltipData{ .parent = container });
state->value.value(
) | rpl::map([](const Data::GiftAuctionState &state) {
return state.averagePrice;
}) | rpl::filter(
rpl::mappers::_1 != 0
) | rpl::take(
1
) | rpl::start_with_next([=](int64 price) {
delete round;
raw->insertRow(
2,
object_ptr<FlatLabel>(
raw,
tr::lng_auction_average_label(),
raw->st().defaultLabel),
MakeAveragePriceValue(raw, tooltip, name, price),
st::giveawayGiftCodeLabelMargin,
st::giveawayGiftCodeValueMargin);
raw->resizeToWidth(raw->widthNoMargins());
}, raw->lifetime());
}
return result;
}
@@ -1046,14 +1182,17 @@ void AuctionGotGiftsBox(
st::giveawayGiftCodeValueMargin);
};
// Title "Round #n"
addFullWidth(tr::lng_auction_bought_round(
// Title "Gift #number in round #n"
addFullWidth(tr::lng_auction_bought_in_round(
lt_name,
rpl::single(tr::marked(
emoji
).append(' ').append(
Data::UniqueGiftName(gift.resellTitle, entry.number)
)),
lt_n,
rpl::single(tr::marked(QString::number(entry.round))),
tr::bold
) | rpl::map([=](const TextWithEntities &text) {
return TextWithEntities{ emoji }.append(' ').append(text);
}));
tr::bold));
// Recipient
AddTableRow(
@@ -1092,6 +1231,93 @@ void AuctionGotGiftsBox(
}
}
[[nodiscard]] rpl::producer<UniqueGiftCover> MakePreviewAuctionStream(
const Data::StarGift &info,
rpl::producer<Data::UniqueGiftAttributes> attributes) {
Expects(attributes);
const auto cover = [](Data::UniqueGift gift) {
return UniqueGiftCover{ std::move(gift) };
};
auto initial = Data::UniqueGift{
.title = info.resellTitle,
.model = Data::UniqueGiftModel{
.document = info.document,
},
.pattern = Data::UniqueGiftPattern{
.document = info.document,
},
.backdrop = (info.background
? info.background->backdrop()
: Data::UniqueGiftBackdrop()),
};
return rpl::single(cover(initial)) | rpl::then(std::move(
attributes
) | rpl::map([=](const Data::UniqueGiftAttributes &values)
-> rpl::producer<UniqueGiftCover> {
if (values.backdrops.empty()
|| values.models.empty()
|| values.patterns.empty()) {
return rpl::never<UniqueGiftCover>();
}
return [=](auto consumer) {
auto lifetime = rpl::lifetime();
struct State {
Data::UniqueGiftAttributes data;
std::vector<int> modelIndices;
std::vector<int> patternIndices;
std::vector<int> backdropIndices;
};
const auto state = lifetime.make_state<State>(State{
.data = values,
});
const auto put = [=] {
const auto index = [](
std::vector<int> &indices,
const auto &v) {
const auto fill = [&] {
if (!indices.empty()) {
return;
}
indices = ranges::views::ints(
0
) | ranges::views::take(
v.size()
) | ranges::to_vector;
ranges::shuffle(indices);
};
fill();
const auto result = indices.back();
indices.pop_back();
fill();
if (indices.back() == result) {
std::swap(indices.front(), indices.back());
}
return result;
};
auto &models = state->data.models;
auto &patterns = state->data.patterns;
auto &backdrops = state->data.backdrops;
consumer.put_next(cover({
.title = info.resellTitle,
.model = models[index(state->modelIndices, models)],
.pattern = patterns[index(state->patternIndices, patterns)],
.backdrop = backdrops[index(state->backdropIndices, backdrops)],
}));
};
put();
base::timer_each(
kSwitchPreviewCoverInterval / 3
) | rpl::start_with_next(put, lifetime);
return lifetime;
};
}) | rpl::flatten_latest());
}
void AuctionInfoBox(
not_null<GenericBox*> box,
not_null<Window::SessionController*> window,
@@ -1101,105 +1327,76 @@ void AuctionInfoBox(
struct State {
explicit State(not_null<Main::Session*> session)
: delegate(session, GiftButtonMode::Minimal) {
: delegate(session, GiftButtonMode::Minimal) {
}
Delegate delegate;
rpl::variable<Data::GiftAuctionState> value;
rpl::variable<int> minutesLeft;
rpl::variable<int> minutesTillEnd;
rpl::variable<int> secondsTillStart;
rpl::variable<Data::UniqueGiftAttributes> attributes;
std::vector<Data::GiftAcquired> acquired;
bool acquiredRequested = false;
base::unique_qptr<PopupMenu> menu;
rpl::lifetime previewLifetime;
bool previewRequested = false;
};
const auto show = window->uiShow();
const auto state = box->lifetime().make_state<State>(&show->session());
state->value = std::move(value);
const auto &now = state->value.current();
state->minutesLeft = MinutesLeftTillValue(now.endDate);
const auto auctions = &show->session().giftAuctions();
const auto giftId = now.gift->id;
if (auto attributes = auctions->attributes(giftId)) {
state->attributes = std::move(*attributes);
} else {
auctions->requestAttributes(giftId, crl::guard(box, [=] {
state->attributes.force_assign(*auctions->attributes(giftId));
}));
}
state->minutesTillEnd = MinutesLeftTillValue(now.endDate);
state->secondsTillStart = SecondsLeftTillValue(now.startDate);
const auto started = !state->secondsTillStart.current();
box->setStyle(st::giftBox);
box->setNoContentMargin(true);
const auto name = now.gift->resellTitle;
const auto extend = st::defaultDropdownMenu.wrap.shadow.extend;
const auto side = st::giftBoxGiftSmall;
const auto size = QSize(side, side).grownBy(extend);
const auto preview = box->addRow(
object_ptr<FixedHeightWidget>(box, size.height()),
st::auctionInfoPreviewMargin);
const auto gift = CreateChild<GiftButton>(preview, &state->delegate);
gift->setAttribute(Qt::WA_TransparentForMouseEvents);
gift->setDescriptor(GiftTypeStars{
.info = *now.gift,
}, GiftButtonMode::Minimal);
preview->widthValue() | rpl::start_with_next([=](int width) {
const auto left = (width - size.width()) / 2;
gift->setGeometry(
QRect(QPoint(left, 0), size).marginsRemoved(extend),
extend);
}, gift->lifetime());
const auto rounds = state->value.current().totalRounds;
const auto perRound = state->value.current().gift->auctionGiftsPerRound;
auto aboutText = state->value.value(
) | rpl::map([=](const Data::GiftAuctionState &state) {
if (state.finished()) {
return tr::lng_auction_text_ended(tr::now, tr::marked);
}
return tr::lng_auction_text(
tr::now,
lt_count,
perRound,
lt_name,
tr::bold(name),
lt_link,
tr::lng_auction_text_link(
tr::now,
lt_arrow,
Text::IconEmoji(&st::textMoreIconEmoji),
tr::link),
tr::rich);
const auto container = box->verticalLayout();
auto gift = MakePreviewAuctionStream(
*now.gift,
state->attributes.value());
AddUniqueGiftCover(container, std::move(gift), {
.pretitle = started ? nullptr : tr::lng_auction_preview_name(),
.subtitle = tr::lng_auction_preview_learn_gifts(
lt_arrow,
rpl::single(Text::IconEmoji(&st::textMoreIconEmoji)),
tr::link),
.subtitleClick = [=] {
ShowPremiumPreviewBox(window, PremiumFeature::Gifts);
},
.subtitleLinkColored = true,
});
box->addRow(
object_ptr<FlatLabel>(
box,
name,
st::uniqueGiftTitle),
style::al_top);
const auto about = box->addRow(
object_ptr<FlatLabel>(
box,
std::move(aboutText),
st::uniqueGiftSubtitle),
st::boxRowPadding + QMargins(0, st::auctionInfoSubtitleSkip, 0, 0),
style::al_top);
about->setTryMakeSimilarLines(true);
box->resizeToWidth(box->widthNoMargins());
AddSkip(container, st::defaultVerticalListSkip * 2);
about->setClickHandlerFilter([=](const auto &...) {
show->show(Box(AuctionAboutBox, rounds, perRound, nullptr));
return false;
});
AddUniqueCloseButton(
box,
{},
now.finished() ? nullptr : MakeAuctionFillMenuCallback(show, now));
box->addRow(
AuctionInfoTable(box, box->verticalLayout(), state->value.value()),
st::boxRowPadding + st::auctionInfoTableMargin);
state->value.value(
) | rpl::map([=](const Data::GiftAuctionState &value) {
return value.my.gotCount;
}) | rpl::filter(
rpl::mappers::_1 > 0
) | rpl::take(1) | rpl::start_with_next([=](int count) {
if (const auto got = now.my.gotCount) {
box->addRow(
object_ptr<FlatLabel>(
box,
tr::lng_auction_bought(
lt_count_decimal,
rpl::single(count * 1.),
rpl::single(1. * got),
lt_emoji,
rpl::single(Data::SingleCustomEmoji(
state->value.current().gift->document)),
@@ -1224,7 +1421,7 @@ void AuctionInfoBox(
state->acquired));
} else if (!state->acquiredRequested) {
state->acquiredRequested = true;
show->session().giftAuctions().requestAcquired(
auctions->requestAcquired(
value.gift->id,
crl::guard(box, [=](
std::vector<Data::GiftAcquired> result) {
@@ -1241,11 +1438,45 @@ void AuctionInfoBox(
}
return false;
});
}, box->lifetime());
} else if (const auto variants = now.gift->upgradeVariants) {
using namespace Data;
state->attributes.value(
) | rpl::filter([](const UniqueGiftAttributes &list) {
return !list.models.empty();
}) | rpl::take(
1
) | rpl::start_with_next([=](const UniqueGiftAttributes &list) {
auto emoji = tr::marked();
const auto indices = RandomIndicesSubset(list.models.size(), 3);
for (const auto index : indices) {
emoji.append(Data::SingleCustomEmoji(
list.models[index].document));
}
box->addRow(
object_ptr<FlatLabel>(
box,
tr::lng_auction_preview_variants(
lt_count_decimal,
rpl::single(1. * variants),
lt_emoji,
rpl::single(emoji),
lt_arrow,
rpl::single(Text::IconEmoji(&st::textMoreIconEmoji)),
tr::link),
st::uniqueGiftValueAvailableLink,
st::defaultPopupMenu,
Core::TextContext({ .session = &show->session() })),
st::boxRowPadding + st::uniqueGiftValueAvailableMargin,
style::al_top
)->setClickHandlerFilter([=](const auto &...) {
show->show(Box(StarGiftPreviewBox, window, *now.gift, list));
return false;
});
}, box->lifetime());
}
const auto button = box->addButton(rpl::single(QString()), [=] {
if (state->value.current().finished()
|| !state->minutesLeft.current()) {
|| !state->minutesTillEnd.current()) {
box->closeBox();
return;
}
@@ -1266,40 +1497,6 @@ void AuctionInfoBox(
button,
AuctionButtonCountdownType::Join,
state->value.value());
box->setNoContentMargin(true);
const auto close = CreateChild<IconButton>(
box->verticalLayout(),
st::boxTitleClose);
close->setClickedCallback([=] { box->closeBox(); });
const auto menu = CreateChild<IconButton>(
box->verticalLayout(),
st::boxTitleMenu);
menu->setClickedCallback(MakeAuctionMenuCallback(menu, show, now));
const auto weakMenu = base::make_weak(menu);
box->verticalLayout()->widthValue() | rpl::start_with_next([=](int) {
close->moveToRight(0, 0);
if (const auto strong = weakMenu.get()) {
strong->moveToRight(close->width(), 0);
}
}, close->lifetime());
rpl::combine(
state->value.value(),
state->minutesLeft.value()
) | rpl::start_with_next([=](
const Data::GiftAuctionState &state,
int minutes) {
const auto finished = state.finished() || (minutes <= 0);
about->setTextColorOverride(finished
? st::attentionButtonFg->c
: std::optional<QColor>());
if (const auto strong = finished ? weakMenu.get() : nullptr) {
delete strong;
}
}, box->lifetime());
}
base::weak_qptr<BoxContent> ChooseAndShowAuctionBox(
@@ -1308,16 +1505,21 @@ base::weak_qptr<BoxContent> ChooseAndShowAuctionBox(
std::shared_ptr<rpl::variable<Data::GiftAuctionState>> state,
Fn<void()> boxClosed) {
const auto local = &peer->session().local();
const auto &now = state->current();
const auto finished = now.finished()
|| (now.endDate <= base::unixtime::now());
const auto showBidBox = now.my.bid
const auto &current = state->current();
const auto now = base::unixtime::now();
const auto started = (current.startDate <= now);
const auto finished = current.finished() || (current.endDate <= now);
const auto showBidBox = current.my.bid
&& !finished
&& (!now.my.to || now.my.to == peer);
const auto showChangeRecipient = !showBidBox && now.my.bid && !finished;
&& (!current.my.to || current.my.to == peer);
const auto showChangeRecipient = !showBidBox
&& current.my.bid
&& !finished;
const auto showInfoBox = !showBidBox
&& !showChangeRecipient
&& (local->readPref<bool>(kAuctionAboutShownPref) || finished);
&& (!started
|| finished
|| local->readPref<bool>(kAuctionAboutShownPref));
auto box = base::weak_qptr<BoxContent>();
if (showBidBox) {
box = window->show(MakeAuctionBidBox({
@@ -1333,13 +1535,13 @@ base::weak_qptr<BoxContent> ChooseAndShowAuctionBox(
peer,
nullptr,
Info::PeerGifts::GiftTypeStars{
.info = *now.gift,
.info = *current.gift,
},
state->value()));
sendBox->boxClosing(
) | rpl::start_with_next(close, sendBox->lifetime());
};
const auto from = now.my.to;
const auto from = current.my.to;
const auto text = (from->isSelf()
? tr::lng_auction_change_already_me(tr::now, tr::rich)
: tr::lng_auction_change_already(
@@ -1383,8 +1585,8 @@ base::weak_qptr<BoxContent> ChooseAndShowAuctionBox(
};
box = window->show(Box(
AuctionAboutBox,
now.totalRounds,
now.gift->auctionGiftsPerRound,
current.totalRounds,
current.gift->auctionGiftsPerRound,
understood));
}
if (const auto strong = box.get()) {
@@ -1457,35 +1659,62 @@ void SetAuctionButtonCountdownText(
rpl::producer<Data::GiftAuctionState> value) {
struct State {
rpl::variable<Data::GiftAuctionState> value;
rpl::variable<int> minutesLeft;
rpl::variable<int> minutesTillEnd;
rpl::variable<int> secondsTillStart;
};
const auto state = button->lifetime().make_state<State>();
state->value = std::move(value);
state->minutesLeft = MinutesLeftTillValue(
state->value.current().endDate);
const auto &now = state->value.current();
const auto preview = (now.startDate > base::unixtime::now());
if (preview) {
state->secondsTillStart = SecondsLeftTillValue(now.startDate);
} else {
state->minutesTillEnd = MinutesLeftTillValue(now.endDate);
}
auto buttonTitle = rpl::combine(
state->value.value(),
state->minutesLeft.value()
) | rpl::map([=](const Data::GiftAuctionState &state, int minutes) {
return (state.finished() || minutes <= 0)
(preview
? state->secondsTillStart.value()
: state->minutesTillEnd.value())
) | rpl::map([=](const Data::GiftAuctionState &state, int leftTill) {
return (state.finished() || (!preview && leftTill <= 0))
? tr::lng_box_ok(tr::marked)
: (type == AuctionButtonCountdownType::Join)
: preview
? tr::lng_auction_join_early_bid(tr::marked)
: (type != AuctionButtonCountdownType::Place)
? tr::lng_auction_join_button(tr::marked)
: tr::lng_auction_join_bid(tr::marked);
}) | rpl::flatten_latest();
auto buttonSubtitle = rpl::combine(
state->value.value(),
state->minutesLeft.value()
(preview
? state->secondsTillStart.value()
: state->minutesTillEnd.value())
) | rpl::map([=](
const Data::GiftAuctionState &state,
int minutes) -> rpl::producer<TextWithEntities> {
if (state.finished() || minutes <= 0) {
const Data::GiftAuctionState &state,
int leftTill
) -> rpl::producer<TextWithEntities> {
if (state.finished() || leftTill <= 0) {
return rpl::single(TextWithEntities());
} else if (preview) {
const auto hours = (leftTill / 3600);
const auto minutes = (leftTill % 3600) / 60;
const auto seconds = (leftTill % 60);
const auto time = hours
? u"%1:%2:%3"_q
.arg(hours).arg(minutes, 2, 10, QChar('0'))
.arg(seconds, 2, 10, QChar('0'))
: u"%1:%2"_q.arg(minutes).arg(seconds, 2, 10, QChar('0'));
return tr::lng_auction_join_starts_in(
lt_time,
rpl::single(tr::marked(time)),
tr::marked);
}
const auto hours = (minutes / 60);
minutes -= (hours * 60);
const auto hours = (leftTill / 60);
const auto minutes = leftTill % 60;
auto value = [](int count) {
return rpl::single(tr::marked(QString::number(count)));

View File

@@ -14,6 +14,7 @@ class Show;
namespace Data {
struct GiftAuctionState;
struct ActiveAuctions;
struct StarGift;
} // namespace Data
namespace Info::PeerGifts {
@@ -49,6 +50,7 @@ struct AuctionBidBoxArgs {
enum class AuctionButtonCountdownType {
Join,
Place,
Preview,
};
void SetAuctionButtonCountdownText(
not_null<RoundButton*> button,

View File

@@ -137,6 +137,7 @@ constexpr auto kCrossfadeDuration = crl::time(400);
constexpr auto kUpgradeDoneToastDuration = 4 * crl::time(1000);
constexpr auto kGiftsPreloadTimeout = 3 * crl::time(1000);
constexpr auto kResellPriceCacheLifetime = 60 * crl::time(1000);
constexpr auto kGradientButtonBgOpacity = 0.6;
using namespace HistoryView;
using namespace Info::PeerGifts;
@@ -2797,53 +2798,90 @@ void SetupResalePriceButton(
void AddUniqueGiftCover(
not_null<VerticalLayout*> container,
rpl::producer<Data::UniqueGift> data,
rpl::producer<QString> subtitleOverride,
rpl::producer<CreditsAmount> resalePrice,
Fn<void()> resaleClick) {
rpl::producer<UniqueGiftCover> data,
UniqueGiftCoverArgs &&args) {
const auto cover = container->add(object_ptr<RpWidget>(container));
struct Released {
Released() : white(QColor(255, 255, 255)) {
Released() : link(QColor(255, 255, 255)) {
}
rpl::variable<TextWithEntities> subtitleText;
style::owned_color white;
std::optional<Ui::Premium::ColoredMiniStars> stars;
style::owned_color link;
style::FlatLabel st;
PeerData *by = nullptr;
QColor bg;
QColor fg;
};
const auto released = cover->lifetime().make_state<Released>();
released->st = st::uniqueGiftReleasedBy;
released->st.palette.linkFg = released->white.color();
released->st.palette.linkFg = released->link.color();
if (resalePrice) {
const auto repaintedHook = args.repaintedHook;
if (args.resalePrice) {
auto background = rpl::duplicate(
data
) | rpl::map([=](const Data::UniqueGift &unique) {
return unique.backdrop.patternColor;
) | rpl::map([=](const UniqueGiftCover &cover) {
auto result = cover.values.backdrop.patternColor;
result.setAlphaF(kGradientButtonBgOpacity * result.alphaF());
return result;
});
SetupResalePriceButton(
cover,
std::move(background),
std::move(resalePrice),
std::move(resaleClick));
std::move(args.resalePrice),
std::move(args.resaleClick));
}
const auto pretitle = args.pretitle
? CreateChild<FlatLabel>(
cover,
std::move(args.pretitle),
st::uniqueGiftPretitle)
: nullptr;
if (pretitle) {
released->stars.emplace(
cover,
true,
Ui::Premium::MiniStarsType::SlowStars);
const auto white = QColor(255, 255, 255);
released->stars->setColorOverride(QGradientStops{
{ 0., anim::with_alpha(white, .3) },
{ 1., white },
});
pretitle->geometryValue() | rpl::start_with_next([=](QRect rect) {
const auto half = rect.height() / 2;
released->stars->setCenter(rect - QMargins(half, 0, half, 0));
}, pretitle->lifetime());
pretitle->setAttribute(Qt::WA_TransparentForMouseEvents);
pretitle->setTextColorOverride(QColor(255, 255, 255));
pretitle->paintOn([=](QPainter &p) {
auto hq = PainterHighQualityEnabler(p);
const auto radius = pretitle->height() / 2.;
p.setPen(Qt::NoPen);
auto bg = released->bg;
bg.setAlphaF(kGradientButtonBgOpacity * bg.alphaF());
p.setBrush(bg);
p.drawRoundedRect(pretitle->rect(), radius, radius);
p.translate(-pretitle->pos());
released->stars->paint(p);
});
}
const auto title = CreateChild<FlatLabel>(
cover,
rpl::duplicate(
data
) | rpl::map([](const Data::UniqueGift &now) { return now.title; }),
) | rpl::map([](const UniqueGiftCover &now) {
return now.values.title;
}),
st::uniqueGiftTitle);
title->setTextColorOverride(QColor(255, 255, 255));
released->subtitleText = subtitleOverride
? std::move(
subtitleOverride
) | Ui::Text::ToWithEntities() | rpl::type_erased()
: rpl::duplicate(data) | rpl::map([=](const Data::UniqueGift &gift) {
released->subtitleText = args.subtitle
? std::move(args.subtitle)
: rpl::duplicate(data) | rpl::map([=](const UniqueGiftCover &cover) {
const auto &gift = cover.values;
released->by = gift.releasedBy;
released->bg = gift.backdrop.patternColor;
return gift.releasedBy
? tr::lng_gift_unique_number_by(
tr::now,
@@ -2860,13 +2898,18 @@ void AddUniqueGiftCover(
});
if (!released->by) {
released->st = st::uniqueGiftSubtitle;
released->st.palette.linkFg = released->white.color();
released->st.palette.linkFg = released->link.color();
}
const auto subtitle = CreateChild<FlatLabel>(
cover,
released->subtitleText.value(),
released->st);
if (released->by) {
if (const auto handler = args.subtitleClick) {
subtitle->setClickHandlerFilter([=](const auto &...) {
handler();
return false;
});
} else if (released->by) {
const auto button = CreateChild<AbstractButton>(cover);
subtitle->raise();
subtitle->setAttribute(Qt::WA_TransparentForMouseEvents);
@@ -2902,6 +2945,7 @@ void AddUniqueGiftCover(
std::unique_ptr<Lottie::SinglePlayer> lottie;
std::unique_ptr<Text::CustomEmoji> emoji;
base::flat_map<float64, QImage> emojis;
bool forced = false;
rpl::lifetime lifetime;
};
struct State {
@@ -2909,25 +2953,44 @@ void AddUniqueGiftCover(
GiftView next;
Animations::Simple crossfade;
bool animating = false;
bool updateAttributesPending = false;
};
const auto state = cover->lifetime().make_state<State>();
const auto lottieSize = st::creditsHistoryEntryStarGiftSize;
const auto updateLinkFg = args.subtitleLinkColored;
const auto updateColors = [=](float64 progress) {
subtitle->setTextColorOverride((progress == 0.)
if (repaintedHook) {
repaintedHook(state->now.gift, state->next.gift, progress);
}
released->bg = (progress == 0.)
? state->now.gift->backdrop.patternColor
: (progress == 1.)
? state->next.gift->backdrop.patternColor
: anim::color(
state->now.gift->backdrop.patternColor,
state->next.gift->backdrop.patternColor,
progress);
const auto color = (progress == 0.)
? state->now.gift->backdrop.textColor
: (progress == 1.)
? state->next.gift->backdrop.textColor
: anim::color(
state->now.gift->backdrop.textColor,
state->next.gift->backdrop.textColor,
progress));
progress);
if (updateLinkFg) {
released->link.update(color);
}
released->fg = color;
subtitle->setTextColorOverride(color);
};
std::move(
rpl::duplicate(
data
) | rpl::start_with_next([=](const Data::UniqueGift &gift) {
) | rpl::start_with_next([=](const UniqueGiftCover &now) {
const auto setup = [&](GiftView &to) {
to.gift = gift;
const auto document = gift.model.document;
to.gift = now.values;
to.forced = now.force;
const auto document = now.values.model.document;
to.media = document->createMediaView();
to.media->automaticLoad({}, nullptr);
rpl::single() | rpl::then(
@@ -2952,7 +3015,7 @@ void AddUniqueGiftCover(
}, to.lifetime);
}, to.lifetime);
to.emoji = document->owner().customEmojiManager().create(
gift.pattern.document,
now.values.pattern.document,
[=] { cover->update(); },
Data::CustomEmojiSizeTag::Large);
[[maybe_unused]] const auto preload = to.emoji->ready();
@@ -2962,11 +3025,122 @@ void AddUniqueGiftCover(
setup(state->now);
cover->update();
updateColors(0.);
} else if (!state->next.gift) {
} else if (!state->next.gift || now.force) {
setup(state->next);
}
}, cover->lifetime());
const auto attrs = args.attributesInfo
? CreateChild<RpWidget>(cover)
: nullptr;
auto updateAttrs = Fn<void(const Data::UniqueGift &)>([](const auto &) {
});
if (attrs) {
struct AttributeState {
Ui::Text::String name;
Ui::Text::String type;
Ui::Text::String percent;
};
struct AttributesState {
AttributeState model;
AttributeState pattern;
AttributeState backdrop;
};
const auto astate = cover->lifetime().make_state<AttributesState>();
const auto setType = [&](AttributeState &state, tr::phrase<> text) {
state.type = Ui::Text::String(
st::uniqueAttributeType,
text(tr::now));
};
setType(astate->model, tr::lng_auction_preview_model);
setType(astate->pattern, tr::lng_auction_preview_symbol);
setType(astate->backdrop, tr::lng_auction_preview_backdrop);
updateAttrs = [=](const Data::UniqueGift &gift) {
const auto set = [&](
AttributeState &state,
const Data::UniqueGiftAttribute &value) {
state.name = Ui::Text::String(
st::uniqueAttributeName,
value.name);
state.percent = Ui::Text::String(
st::uniqueAttributePercent,
QString::number(value.rarityPermille / 10.) + '%');
};
set(astate->model, gift.model);
set(astate->pattern, gift.pattern);
set(astate->backdrop, gift.backdrop);
attrs->update();
};
const auto height = st::uniqueAttributeTop
+ st::uniqueAttributePadding.top()
+ st::uniqueAttributeName.font->height
+ st::uniqueAttributeType.font->height
+ st::uniqueAttributePadding.bottom();
attrs->resize(attrs->width(), height);
attrs->paintOn([=](QPainter &p) {
const auto skip = st::giftBoxGiftSkip.x();
const auto available = attrs->width() - 2 * skip;
const auto single = available / 3;
if (single <= 0) {
return;
}
auto hq = PainterHighQualityEnabler(p);
auto bg = released->bg;
bg.setAlphaF(kGradientButtonBgOpacity * bg.alphaF());
const auto innert = st::uniqueAttributeTop;
const auto innerh = height - innert;
const auto radius = innerh / 3.;
const auto paint = [&](int x, const AttributeState &state) {
p.setPen(Qt::NoPen);
p.setBrush(bg);
p.drawRoundedRect(x, innert, single, innerh, radius, radius);
p.setPen(QColor(255, 255, 255));
const auto padding = st::uniqueAttributePadding;
const auto inner = single - padding.left() - padding.right();
const auto namew = std::min(inner, state.name.maxWidth());
state.name.draw(p, {
.position = QPoint(
x + (single - namew) / 2,
innert + padding.top()),
.availableWidth = namew,
.elisionLines = 1,
});
p.setPen(released->fg);
const auto typew = std::min(inner, state.type.maxWidth());
state.type.draw(p, {
.position = QPoint(
x + (single - typew) / 2,
innert + padding.top() + state.name.minHeight()),
.availableWidth = typew,
});
p.setPen(Qt::NoPen);
p.setBrush(anim::color(released->bg, released->fg, 0.3));
const auto r = st::uniqueAttributePercent.font->height / 2.;
const auto left = x + single - state.percent.maxWidth();
const auto top = st::uniqueAttributePercentPadding.top();
const auto percent = QRect(
left,
top,
state.percent.maxWidth(),
st::uniqueAttributeType.font->height);
p.drawRoundedRect(
percent.marginsAdded(st::uniqueAttributePercentPadding),
r,
r);
p.setPen(QColor(255, 255, 255));
state.percent.draw(p, {
.position = percent.topLeft(),
});
};
auto left = 0;
paint(left, astate->model);
paint(left + single + skip, astate->backdrop);
paint(attrs->width() - single - left, astate->pattern);
});
}
updateAttrs(*state->now.gift);
cover->widthValue() | rpl::start_with_next([=](int width) {
const auto skip = st::uniqueGiftBottom;
if (width <= 3 * skip) {
@@ -2974,18 +3148,52 @@ void AddUniqueGiftCover(
}
const auto available = width - 2 * skip;
title->resizeToWidth(available);
title->moveToLeft(skip, st::uniqueGiftTitleTop);
subtitle->resizeToWidth(available);
subtitle->moveToLeft(skip, st::uniqueGiftSubtitleTop);
cover->resize(width, subtitle->y() + subtitle->height() + skip);
auto top = st::uniqueGiftTitleTop;
if (pretitle) {
pretitle->move((width - pretitle->width()) / 2, top);
top += pretitle->height()
+ (st::uniqueGiftSubtitleTop - st::uniqueGiftTitleTop)
- title->height();
}
title->moveToLeft(skip, top);
if (pretitle) {
top += title->height() + st::defaultVerticalListSkip;
} else {
top += st::uniqueGiftSubtitleTop - st::uniqueGiftTitleTop;
}
subtitle->moveToLeft(skip, top);
top += subtitle->height() + (skip / 2);
if (attrs) {
attrs->resizeToWidth(width
- st::giftBoxPadding.left()
- st::giftBoxPadding.right());
attrs->moveToLeft(st::giftBoxPadding.left(), top);
top += attrs->height() + (skip / 2);
} else {
top += (skip / 2);
}
cover->resize(width, top);
}, cover->lifetime());
cover->paintRequest() | rpl::start_with_next([=] {
auto p = QPainter(cover);
auto progress = state->crossfade.value(state->animating ? 1. : 0.);
if (state->updateAttributesPending && progress >= 0.5) {
state->updateAttributesPending = false;
updateAttrs(*state->next.gift);
} else if (state->updateAttributesPending
&& !state->animating
&& !state->crossfade.animating()) {
state->updateAttributesPending = false;
updateAttrs(*state->now.gift);
}
if (state->animating) {
updateColors(progress);
}
@@ -3007,15 +3215,16 @@ void AddUniqueGiftCover(
}
p.drawImage(0, 0, gift.gradient);
Ui::PaintBgPoints(
p,
Ui::PatternBgPoints(),
gift.emojis,
gift.emoji.get(),
*gift.gift,
QRect(0, 0, width, pointsHeight),
shown);
if (gift.gift->pattern.document != gift.gift->model.document) {
Ui::PaintBgPoints(
p,
Ui::PatternBgPoints(),
gift.emojis,
gift.emoji.get(),
*gift.gift,
QRect(0, 0, width, pointsHeight),
shown);
}
const auto lottie = gift.lottie.get();
const auto factor = style::DevicePixelRatio();
const auto request = Lottie::FrameRequest{
@@ -3039,10 +3248,13 @@ void AddUniqueGiftCover(
};
if (progress < 1.) {
const auto finished = paint(state->now, 1. - progress);
const auto finished = paint(state->now, 1. - progress)
|| (state->next.forced
&& (!state->animating || !state->crossfade.animating()));
const auto next = finished ? state->next.lottie.get() : nullptr;
if (next && next->ready()) {
state->animating = true;
state->updateAttributesPending = true;
state->crossfade.start([=] {
cover->update();
}, 0., 1., kCrossfadeDuration);
@@ -3201,10 +3413,12 @@ void ShowUniqueGiftWearBox(
? tr::lng_gift_wear_badge_about_channel()
: tr::lng_gift_wear_badge_about()),
st.radiantIcon ? st.radiantIcon : &st::menuIconUnique);
//infoRow(
// tr::lng_gift_wear_design_title(),
// tr::lng_gift_wear_design_about(),
// &st::menuIconUniqueProfile);
infoRow(
tr::lng_gift_wear_design_title(),
(channel
? tr::lng_gift_wear_design_about_channel()
: tr::lng_gift_wear_design_about()),
st.profileIcon ? st.profileIcon : &st::menuIconUniqueProfile);
infoRow(
tr::lng_gift_wear_proof_title(),
(channel
@@ -3560,6 +3774,155 @@ void ShowUniqueGiftSellBox(
});
}
void SendOfferBuyGift(
std::shared_ptr<ChatHelpers::Show> show,
std::shared_ptr<Data::UniqueGift> unique,
SuggestOptions options,
int starsPerMessage,
Fn<void(bool)> done) {
const auto randomId = base::RandomValue<uint64>();
const auto owner = show->session().data().peer(unique->ownerId);
using Flag = MTPpayments_SendStarGiftOffer::Flag;
show->session().api().request(MTPpayments_SendStarGiftOffer(
MTP_flags(starsPerMessage ? Flag::f_allow_paid_stars : Flag()),
owner->input,
MTP_string(unique->slug),
StarsAmountToTL(options.price()),
MTP_int(options.offerDuration),
MTP_long(randomId),
MTP_long(starsPerMessage)
)).done([=](const MTPUpdates &result) {
show->session().api().applyUpdates(result);
done(true);
}).fail([=](const MTP::Error &error) {
if (error.type() == u""_q) {
} else {
show->showToast(error.type());
}
done(false);
}).send();
}
void ConfirmOfferBuyGift(
std::shared_ptr<ChatHelpers::Show> show,
std::shared_ptr<Data::UniqueGift> unique,
SuggestOptions options,
Fn<void()> done) {
const auto owner = show->session().data().peer(unique->ownerId);
const auto fee = owner->starsPerMessageChecked();
const auto price = options.price();
const auto sent = std::make_shared<bool>();
const auto send = [=](Fn<void()> close) {
if (*sent) {
return;
}
*sent = true;
SendOfferBuyGift(show, unique, options, fee, [=](bool ok) {
*sent = false;
if (ok) {
if (const auto window = show->resolveWindow()) {
window->showPeerHistory(owner->id);
}
done();
close();
}
});
};
show->show(Box([=](not_null<Ui::GenericBox*> box) {
Ui::ConfirmBox(box, {
.text = tr::lng_gift_offer_confirm_text(
tr::now,
lt_cost,
tr::bold(PrepareCreditsAmountText(options.price())),
lt_user,
tr::bold(owner->shortName()),
lt_name,
tr::bold(Data::UniqueGiftName(*unique)),
tr::marked),
.confirmed = send,
.confirmText = tr::lng_payments_pay_amount(
tr::now,
lt_amount,
Ui::Text::IconEmoji(price.ton()
? &st::buttonTonIconEmoji
: &st::buttonStarIconEmoji
).append(Lang::FormatCreditsAmountDecimal(price.ton()
? price
: CreditsAmount(price.whole() + fee))),
tr::marked),
.title = tr::lng_gift_offer_confirm_title(),
});
auto helper = Ui::Text::CustomEmojiHelper();
const auto starIcon = helper.paletteDependent(
Ui::Earn::IconCreditsEmoji());
const auto tonIcon = helper.paletteDependent(
Ui::Earn::IconCurrencyEmoji());
const auto context = helper.context();
const auto table = box->addRow(
object_ptr<Ui::TableLayout>(box, st::defaultTable),
st::boxPadding);
const auto add = [&](tr::phrase<> label, TextWithEntities value) {
table->addRow(
object_ptr<Ui::FlatLabel>(
table,
label(),
st::defaultTable.defaultLabel),
object_ptr<Ui::FlatLabel>(
table,
rpl::single(value),
st::defaultTable.defaultValue,
st::defaultPopupMenu,
context),
st::giveawayGiftCodeLabelMargin,
st::giveawayGiftCodeValueMargin);
};
add(tr::lng_gift_offer_table_offer, tr::marked(price.ton()
? tonIcon
: starIcon).append(Lang::FormatCreditsAmountDecimal(price)));
if (fee) {
add(tr::lng_gift_offer_table_fee, tr::marked(starIcon).append(
Lang::FormatCreditsAmountDecimal(CreditsAmount(fee))));
}
const auto hours = options.offerDuration / 3600;
const auto duration = hours
? tr::lng_hours(tr::now, lt_count, hours)
: tr::lng_minutes(tr::now, lt_count, options.offerDuration / 60);
add(tr::lng_gift_offer_table_duration, tr::marked(duration));
}));
}
void ShowOfferBuyBox(
std::shared_ptr<ChatHelpers::Show> show,
std::shared_ptr<Data::UniqueGift> unique) {
Expects(unique->starsMinOffer >= 0);
const auto weak = std::make_shared<base::weak_qptr<Ui::BoxContent>>();
const auto done = [=](SuggestOptions result) {
ConfirmOfferBuyGift(show, unique, result, [=] {
if (const auto strong = weak->get()) {
strong->closeBox();
}
});
};
using namespace HistoryView;
const auto options = SuggestOptions{
.exists = 1,
.priceWhole = uint32(unique->starsMinOffer),
};
auto priceBox = Box(ChooseSuggestPriceBox, SuggestPriceBoxArgs{
.peer = show->session().data().peer(unique->ownerId),
.done = done,
.value = options,
.mode = SuggestMode::Gift,
.giftName = UniqueGiftName(*unique),
});
*weak = priceBox.data();
show->show(std::move(priceBox));
}
void GiftReleasedByHandler(not_null<PeerData*> peer) {
const auto session = &peer->session();
const auto window = session->tryResolveWindow(peer);
@@ -3586,12 +3949,12 @@ struct UpgradeArgs : StarGiftUpgradeArgs {
std::vector<UpgradePrice> nextPrices;
};
[[nodiscard]] rpl::producer<Data::UniqueGift> MakeUpgradeGiftStream(
[[nodiscard]] rpl::producer<UniqueGiftCover> MakeUpgradeGiftStream(
const UpgradeArgs &args) {
if (args.models.empty()
|| args.patterns.empty()
|| args.backdrops.empty()) {
return rpl::never<Data::UniqueGift>();
return rpl::never<UniqueGiftCover>();
}
return [=](auto consumer) {
auto lifetime = rpl::lifetime();
@@ -3629,14 +3992,14 @@ struct UpgradeArgs : StarGiftUpgradeArgs {
auto &models = state->data.models;
auto &patterns = state->data.patterns;
auto &backdrops = state->data.backdrops;
consumer.put_next(Data::UniqueGift{
consumer.put_next(UniqueGiftCover{ Data::UniqueGift{
.title = (state->data.savedId
? tr::lng_gift_upgrade_title(tr::now)
: tr::lng_gift_upgrade_preview_title(tr::now)),
.model = models[index(state->modelIndices, models)],
.pattern = patterns[index(state->patternIndices, patterns)],
.backdrop = backdrops[index(state->backdropIndices, backdrops)],
});
} });
};
put();
@@ -3651,16 +4014,16 @@ struct UpgradeArgs : StarGiftUpgradeArgs {
void AddUpgradeGiftCover(
not_null<VerticalLayout*> container,
const UpgradeArgs &args) {
AddUniqueGiftCover(
container,
MakeUpgradeGiftStream(args),
(args.savedId
? tr::lng_gift_upgrade_about()
AddUniqueGiftCover(container, MakeUpgradeGiftStream(args), {
.subtitle = (args.savedId
? tr::lng_gift_upgrade_about(tr::marked)
: (args.peer->isBroadcast()
? tr::lng_gift_upgrade_preview_about_channel
: tr::lng_gift_upgrade_preview_about)(
lt_name,
rpl::single(args.peer->shortName()))));
rpl::single(tr::marked(args.peer->shortName())),
tr::marked)),
});
}
class UpgradePriceValue final {
@@ -4409,8 +4772,8 @@ CreditsAmount StarsFromTon(
not_null<Main::Session*> session,
CreditsAmount ton) {
const auto appConfig = &session->appConfig();
const auto starsRate = appConfig->starsWithdrawRate() / 100.;
const auto tonRate = appConfig->currencyWithdrawRate();
const auto starsRate = appConfig->starsSellRate() / 100.;
const auto tonRate = appConfig->currencySellRate();
if (!starsRate) {
return {};
}
@@ -4422,8 +4785,8 @@ CreditsAmount TonFromStars(
not_null<Main::Session*> session,
CreditsAmount stars) {
const auto appConfig = &session->appConfig();
const auto starsRate = appConfig->starsWithdrawRate() / 100.;
const auto tonRate = appConfig->currencyWithdrawRate();
const auto starsRate = appConfig->starsSellRate() / 100.;
const auto tonRate = appConfig->currencySellRate();
if (!tonRate) {
return {};
}

View File

@@ -65,12 +65,28 @@ void ShowStarGiftBox(
not_null<Window::SessionController*> controller,
not_null<PeerData*> peer);
struct UniqueGiftCoverArgs {
rpl::producer<QString> pretitle;
rpl::producer<TextWithEntities> subtitle;
Fn<void()> subtitleClick;
bool subtitleLinkColored = false;
rpl::producer<CreditsAmount> resalePrice;
Fn<void()> resaleClick;
bool attributesInfo = false;
Fn<void(
std::optional<Data::UniqueGift> now,
std::optional<Data::UniqueGift> next,
float64 progress)> repaintedHook;
};
struct UniqueGiftCover {
Data::UniqueGift values;
bool force = false;
};
void AddUniqueGiftCover(
not_null<VerticalLayout*> container,
rpl::producer<Data::UniqueGift> data,
rpl::producer<QString> subtitleOverride = nullptr,
rpl::producer<CreditsAmount> resalePrice = nullptr,
Fn<void()> resaleClick = nullptr);
rpl::producer<UniqueGiftCover> data,
UniqueGiftCoverArgs &&args);
void AddWearGiftCover(
not_null<VerticalLayout*> container,
const Data::UniqueGift &data,
@@ -95,6 +111,10 @@ void ShowUniqueGiftSellBox(
Data::SavedStarGiftId savedId,
Settings::GiftWearBoxStyleOverride st);
void ShowOfferBuyBox(
std::shared_ptr<ChatHelpers::Show> show,
std::shared_ptr<Data::UniqueGift> unique);
void GiftReleasedByHandler(not_null<PeerData*> peer);
struct StarGiftUpgradeArgs {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
namespace Data {
struct StarGift;
struct UniqueGiftAttributes;
} // namespace Data
namespace Window {
class SessionController;
} // namespace Window
namespace Ui {
class GenericBox;
void StarGiftPreviewBox(
not_null<GenericBox*> box,
not_null<Window::SessionController*> controller,
const Data::StarGift &gift,
const Data::UniqueGiftAttributes &attributes);
} // namespace Ui

View File

@@ -1548,7 +1548,7 @@ void StickerSetBox::Inner::fillDeleteStickerBox(
const auto buttonWidth = state->saveButton
? state->saveButton->width()
: 0;
state->requestId = document->owner().session().api().request(
state->requestId = document->session().api().request(
MTPstickers_RemoveStickerFromSet(document->mtpInput()
)).done([=](const TLStickerSet &result) {
result.match([&](const MTPDmessages_stickerSet &d) {
@@ -1590,7 +1590,7 @@ void StickerSetBox::Inner::fillDeleteStickerBox(
state->requestId.value() | rpl::map(rpl::mappers::_1 > 0));
}
box->addButton(tr::lng_close(), [=] {
document->owner().session().api().request(
document->session().api().request(
state->requestId.current()).cancel();
box->closeBox();
});

View File

@@ -24,13 +24,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_star_gift.h"
#include "data/data_thread.h"
#include "data/data_user.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/history_item_components.h"
#include "lang/lang_keys.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
#include "payments/payments_checkout_process.h"
#include "ui/boxes/confirm_box.h"
#include "ui/controls/sub_tabs.h"
#include "ui/controls/ton_common.h"
#include "ui/layers/generic_box.h"
#include "ui/text/format_values.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
#include "ui/basic_click_handlers.h"
@@ -492,6 +497,26 @@ void TransferGift(
}
}
void ResolveGiftSaleOffer(
not_null<Window::SessionController*> window,
MsgId id,
bool accept,
Fn<void(bool)> done) {
using Flag = MTPpayments_ResolveStarGiftOffer::Flag;
const auto session = &window->session();
const auto show = window->uiShow();
session->api().request(MTPpayments_ResolveStarGiftOffer(
MTP_flags(accept ? Flag() : Flag::f_decline),
MTP_int(id.bare)
)).done([=](const MTPUpdates &result) {
session->api().applyUpdates(result);
done(true);
}).fail([=](const MTP::Error &error) {
show->showToast(error.type());
done(false);
}).send();
}
void BuyResaleGift(
std::shared_ptr<ChatHelpers::Show> show,
not_null<PeerData*> to,
@@ -687,6 +712,164 @@ void ShowTransferGiftBox(
Ui::LayerOption::KeepOther);
}
void ShowGiftSaleAcceptBox(
not_null<Window::SessionController*> controller,
not_null<HistoryItem*> item,
not_null<HistoryMessageSuggestion*> suggestion) {
const auto id = item->id;
const auto peer = item->history()->peer;
const auto gift = suggestion->gift;
const auto price = suggestion->price;
const auto &appConfig = controller->session().appConfig();
const auto starsThousandths = appConfig.giftResaleStarsThousandths();
const auto nanoTonThousandths = appConfig.giftResaleNanoTonThousandths();
controller->show(Box([=](not_null<Ui::GenericBox*> box) {
struct State {
bool sent = false;
};
const auto state = std::make_shared<State>();
auto callback = [=] {
if (state->sent) {
return;
}
state->sent = true;
const auto weak = base::make_weak(controller);
const auto weakBox = base::make_weak(box);
ResolveGiftSaleOffer(controller, id, true, [=](bool ok) {
state->sent = false;
if (ok) {
if (const auto strong = weak.get()) {
strong->showPeerHistory(peer->id);
}
if (const auto strong = weakBox.get()) {
strong->closeBox();
}
}
});
};
const auto receive = price.ton()
? ((price.value() * nanoTonThousandths) / 1000.)
: ((int64(price.value()) * starsThousandths) / 1000);
auto button = tr::lng_gift_offer_sell_for(
lt_price,
rpl::single(Ui::Text::IconEmoji(price.ton()
? &st::buttonTonIconEmoji
: &st::buttonStarIconEmoji
).append(Lang::FormatExactCountDecimal(receive))),
tr::marked);
box->addRow(
CreateGiftTransfer(box->verticalLayout(), gift, peer),
QMargins(0, st::boxPadding.top(), 0, 0));
Ui::ConfirmBox(box, {
.text = tr::lng_gift_offer_confirm_accept(
tr::now,
lt_name,
tr::bold(UniqueGiftName(*gift)),
lt_user,
tr::bold(peer->shortName()),
lt_cost,
tr::bold(PrepareCreditsAmountText(price)),
tr::marked
).append(u"\n\n"_q).append(tr::lng_gift_offer_you_get(
tr::now,
lt_cost,
tr::bold(price.stars()
? tr::lng_action_gift_for_stars(
tr::now,
lt_count_decimal,
receive)
: tr::lng_action_gift_for_ton(
tr::now,
lt_count_decimal,
receive)),
tr::marked)),
.confirmed = std::move(callback),
.confirmText = std::move(button),
});
const auto show = controller->uiShow();
auto taken = base::take(gift->value);
AddTransferGiftTable(show, box->verticalLayout(), gift);
gift->value = std::move(taken);
if (gift->value.get()) {
const auto appConfig = &show->session().appConfig();
const auto rule = Ui::LookupCurrencyRule(u"USD"_q);
const auto value = (gift->value->valuePriceUsd > 0 ? 1 : -1)
* std::abs(gift->value->valuePriceUsd)
/ std::pow(10., rule.exponent);
if (std::abs(value) >= 0.01) {
const auto rate = price.ton()
? appConfig->currencySellRate()
: (appConfig->starsSellRate() / 100.);
const auto offered = receive * rate;
const auto diff = offered - value;
const auto percent = std::abs(diff / value * 100.);
if (percent >= 1) {
const auto higher = (diff > 0.);
const auto good = higher || (percent < 10);
const auto number = int(base::SafeRound(percent));
const auto percentText = QString::number(number) + '%';
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
(higher
? tr::lng_gift_offer_higher
: tr::lng_gift_offer_lower)(
lt_percent,
rpl::single(tr::bold(percentText)),
lt_name,
rpl::single(tr::marked(gift->title)),
tr::marked),
(good ? st::offerValueGood : st::offerValueBad)),
st::boxRowPadding + st::offerValuePadding
)->setTryMakeSimilarLines(true);
}
}
}
}));
}
void ShowGiftSaleRejectBox(
not_null<Window::SessionController*> controller,
not_null<HistoryItem*> item,
not_null<HistoryMessageSuggestion*> suggestion) {
struct State {
bool sent = false;
};
const auto id = item->id;
const auto state = std::make_shared<State>();
auto callback = [=](Fn<void()> close) {
if (state->sent) {
return;
}
state->sent = true;
const auto weak = base::make_weak(controller);
ResolveGiftSaleOffer(controller, id, false, [=](bool ok) {
state->sent = false;
if (ok) {
close();
}
});
};
controller->show(Ui::MakeConfirmBox({
.text = tr::lng_gift_offer_confirm_reject(
lt_user,
rpl::single(tr::bold(item->history()->peer->shortName())),
tr::marked),
.confirmed = std::move(callback),
.confirmText = tr::lng_action_gift_offer_decline(),
.confirmStyle = &st::attentionBoxButton,
.title = tr::lng_gift_offer_reject_title(),
}));
}
void SetThemeFromUniqueGift(
not_null<Window::SessionController*> window,
std::shared_ptr<Data::UniqueGift> unique) {

View File

@@ -7,6 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
struct HistoryMessageSuggestion;
namespace Window {
class SessionController;
} // namespace Window
@@ -36,6 +38,15 @@ void ShowTransferGiftBox(
std::shared_ptr<Data::UniqueGift> gift,
Data::SavedStarGiftId savedId);
void ShowGiftSaleAcceptBox(
not_null<Window::SessionController*> controller,
not_null<HistoryItem*> item,
not_null<HistoryMessageSuggestion*> suggestion);
void ShowGiftSaleRejectBox(
not_null<Window::SessionController*> controller,
not_null<HistoryItem*> item,
not_null<HistoryMessageSuggestion*> suggestion);
void ShowBuyResaleGiftBox(
std::shared_ptr<ChatHelpers::Show> show,
std::shared_ptr<Data::UniqueGift> gift,

View File

@@ -554,6 +554,7 @@ void SetupFingerprintTooltip(not_null<Ui::RpWidget*> widget) {
std::unique_ptr<Ui::ImportantTooltip> tooltip;
Fn<void()> updateGeometry;
Fn<void(bool)> toggleTooltip;
bool tooltipShown = false;
};
const auto state = widget->lifetime().make_state<State>();
state->updateGeometry = [=] {
@@ -620,12 +621,18 @@ void SetupFingerprintTooltip(not_null<Ui::RpWidget*> widget) {
// Enter events may come from widget destructors,
// in that case sync-showing tooltip (calling Grab)
// crashes the whole thing.
state->tooltipShown = true;
crl::on_main(widget, [=] {
state->toggleTooltip(true);
if (state->tooltipShown) {
state->toggleTooltip(true);
}
});
} else if (type == QEvent::Leave) {
state->tooltipShown = false;
crl::on_main(widget, [=] {
state->toggleTooltip(false);
if (!state->tooltipShown) {
state->toggleTooltip(false);
}
});
}
}, widget->lifetime());

View File

@@ -2109,6 +2109,7 @@ void Panel::trackControl(Ui::RpWidget *widget, rpl::lifetime &lifetime) {
if (!widget) {
return;
}
const auto over = std::make_shared<bool>();
widget->events(
) | rpl::start_with_next([=](not_null<QEvent*> e) {
const auto type = e->type();
@@ -2116,12 +2117,18 @@ void Panel::trackControl(Ui::RpWidget *widget, rpl::lifetime &lifetime) {
// Enter events may come from widget destructors,
// in that case sync-showing tooltip (calling Grab)
// crashes the whole thing.
*over = true;
crl::on_main(widget, [=] {
trackControlOver(widget, true);
if (*over) {
trackControlOver(widget, true);
}
});
} else if (type == QEvent::Leave) {
*over = false;
crl::on_main(widget, [=] {
trackControlOver(widget, false);
if (!*over) {
trackControlOver(widget, false);
}
});
}
}, lifetime);

View File

@@ -1742,7 +1742,7 @@ void StickersListWidget::showStickerSetBox(
}
lifetime->destroy();
}, *lifetime);
document->owner().session().api().requestSpecialStickersForce(
document->session().api().requestSpecialStickersForce(
setId == Data::Stickers::FavedSetId,
setId == Data::Stickers::RecentSetId,
false);

View File

@@ -489,7 +489,12 @@ TabbedSelector::TabbedSelector(
) | rpl::start_with_next([=](uint64 setId) {
_tabsSlider->setActiveSection(indexByType(SelectorTab::Stickers));
stickers()->showStickerSet(setId);
_showRequests.fire({});
if (_currentPeer
&& Data::CanSend(
_currentPeer,
ChatRestriction::SendStickers)) {
_showRequests.fire({});
}
}, lifetime());
rpl::merge(
@@ -517,7 +522,9 @@ TabbedSelector::TabbedSelector(
) | rpl::start_with_next([=](uint64 setId) {
_tabsSlider->setActiveSection(indexByType(SelectorTab::Emoji));
emoji()->showSet(setId);
_showRequests.fire({});
if (_currentPeer && Data::CanSendTexts(_currentPeer)) {
_showRequests.fire({});
}
}, lifetime());
}
if (hasEmojiTab()) {

View File

@@ -181,3 +181,5 @@ private:
[[nodiscard]] CreditsAmount CreditsAmountFromTL(
const MTPStarsAmount *amount);
[[nodiscard]] MTPStarsAmount StarsAmountToTL(CreditsAmount amount);
[[nodiscard]] QString PrepareCreditsAmountText(CreditsAmount amount);

View File

@@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D1ED}"_cs;
constexpr auto AppNameOld = "Telegram Win (Unofficial)"_cs;
constexpr auto AppName = "Telegram Desktop"_cs;
constexpr auto AppFile = "Telegram"_cs;
constexpr auto AppVersion = 6003004;
constexpr auto AppVersionStr = "6.3.4";
constexpr auto AppVersion = 6003006;
constexpr auto AppVersionStr = "6.3.6";
constexpr auto AppBetaVersion = false;
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;

View File

@@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "apiwrap.h"
#include "api/api_credits.h"
#include "data/data_user.h"
#include "lang/lang_keys.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
@@ -256,3 +257,15 @@ MTPStarsAmount StarsAmountToTL(CreditsAmount amount) {
MTP_long(amount.whole() * uint64(1'000'000'000) + amount.nano())
) : MTP_starsAmount(MTP_long(amount.whole()), MTP_int(amount.nano()));
}
QString PrepareCreditsAmountText(CreditsAmount amount) {
return amount.stars()
? tr::lng_action_gift_for_stars(
tr::now,
lt_count_decimal,
amount.value())
: tr::lng_action_gift_for_ton(
tr::now,
lt_count_decimal,
amount.value());
}

View File

@@ -112,6 +112,7 @@ void GiftAuctions::requestAcquired(
.date = data.vdate().v,
.bidAmount = int64(data.vbid_amount().v),
.round = data.vround().v,
.number = data.vgift_num().value_or_empty(),
.position = data.vpos().v,
.nameHidden = data.is_name_hidden(),
});
@@ -129,6 +130,49 @@ void GiftAuctions::requestAcquired(
}).send();
}
std::optional<Data::UniqueGiftAttributes> GiftAuctions::attributes(
uint64 giftId) const {
const auto i = _attributes.find(giftId);
return (i != end(_attributes) && i->second.waiters.empty())
? i->second.lists
: std::optional<Data::UniqueGiftAttributes>();
}
void GiftAuctions::requestAttributes(uint64 giftId, Fn<void()> ready) {
auto &entry = _attributes[giftId];
entry.waiters.push_back(std::move(ready));
if (entry.waiters.size() > 1) {
return;
}
_session->api().request(MTPpayments_GetStarGiftUpgradeAttributes(
MTP_long(giftId)
)).done([=](const MTPpayments_StarGiftUpgradeAttributes &result) {
const auto &attributes = result.data().vattributes().v;
auto &entry = _attributes[giftId];
auto &info = entry.lists;
info.models.reserve(attributes.size());
info.patterns.reserve(attributes.size());
info.backdrops.reserve(attributes.size());
for (const auto &attribute : attributes) {
attribute.match([&](const MTPDstarGiftAttributeModel &data) {
info.models.push_back(Api::FromTL(_session, data));
}, [&](const MTPDstarGiftAttributePattern &data) {
info.patterns.push_back(Api::FromTL(_session, data));
}, [&](const MTPDstarGiftAttributeBackdrop &data) {
info.backdrops.push_back(Api::FromTL(data));
}, [](const MTPDstarGiftAttributeOriginalDetails &data) {
});
}
for (const auto &ready : base::take(entry.waiters)) {
ready();
}
}).fail([=] {
for (const auto &ready : base::take(_attributes[giftId].waiters)) {
ready();
}
}).send();
}
rpl::producer<ActiveAuctions> GiftAuctions::active() const {
return _activeChanged.events_starting_with_copy(
rpl::empty
@@ -233,6 +277,7 @@ void GiftAuctions::requestActive() {
result.match([=](const MTPDpayments_starGiftActiveAuctions &data) {
const auto owner = &_session->data();
owner->processUsers(data.vusers());
owner->processChats(data.vchats());
auto giftsFound = base::flat_set<QString>();
const auto &list = data.vauctions().v;
@@ -294,6 +339,7 @@ void GiftAuctions::request(const QString &slug) {
const auto &data = result.data();
_session->data().processUsers(data.vusers());
_session->data().processChats(data.vchats());
raw->state.gift = Api::FromTL(_session, data.vgift());
if (!raw->state.gift) {
@@ -367,6 +413,24 @@ void GiftAuctions::apply(
entry->giftsLeft = data.vgifts_left().v;
entry->currentRound = data.vcurrent_round().v;
entry->totalRounds = data.vtotal_rounds().v;
const auto &rounds = data.vrounds().v;
entry->roundParameters.clear();
entry->roundParameters.reserve(rounds.size());
for (const auto &round : rounds) {
round.match([&](const MTPDstarGiftAuctionRound &data) {
entry->roundParameters.push_back({
.number = data.vnum().v,
.duration = data.vduration().v,
});
}, [&](const MTPDstarGiftAuctionRoundExtendable &data) {
entry->roundParameters.push_back({
.number = data.vnum().v,
.duration = data.vduration().v,
.extendTop = data.vextend_top().v,
.extendDuration = data.vextend_window().v,
});
});
}
entry->averagePrice = 0;
}, [&](const MTPDstarGiftAuctionStateFinished &data) {
entry->averagePrice = data.vaverage_price().v;

View File

@@ -31,11 +31,19 @@ struct StarGiftAuctionMyState {
bool returned = false;
};
struct GiftAuctionRound {
int number = 0;
TimeId duration = 0;
int extendTop = 0;
TimeId extendDuration = 0;
};
struct GiftAuctionState {
std::optional<StarGift> gift;
StarGiftAuctionMyState my;
std::vector<GiftAuctionBidLevel> bidLevels;
std::vector<not_null<UserData*>> topBidders;
std::vector<GiftAuctionRound> roundParameters;
crl::time subscribedTill = 0;
int64 minBidAmount = 0;
int64 averagePrice = 0;
@@ -58,6 +66,7 @@ struct GiftAcquired {
TimeId date = 0;
int64 bidAmount = 0;
int round = 0;
int number = 0;
int position = 0;
bool nameHidden = false;
};
@@ -80,6 +89,10 @@ public:
uint64 giftId,
Fn<void(std::vector<Data::GiftAcquired>)> done);
[[nodiscard]] std::optional<Data::UniqueGiftAttributes> attributes(
uint64 giftId) const;
void requestAttributes(uint64 giftId, Fn<void()> ready);
[[nodiscard]] rpl::producer<ActiveAuctions> active() const;
[[nodiscard]] rpl::producer<bool> hasActiveChanges() const;
[[nodiscard]] bool hasActive() const;
@@ -100,6 +113,10 @@ private:
}
friend inline bool operator==(MyStateKey, MyStateKey) = default;
};
struct Attributes {
Data::UniqueGiftAttributes lists;
std::vector<Fn<void()>> waiters;
};
void request(const QString &slug);
Entry *find(uint64 giftId) const;
@@ -126,6 +143,7 @@ private:
base::Timer _timer;
base::flat_map<QString, std::unique_ptr<Entry>> _map;
base::flat_map<uint64, Attributes> _attributes;
rpl::event_stream<> _activeChanged;
mtpRequestId _activeRequestId = 0;

View File

@@ -0,0 +1,186 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "data/components/passkeys.h"
#include "apiwrap.h"
#include "data/data_passkey_deserialize.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
#include "platform/platform_webauthn.h"
namespace Data {
namespace {
constexpr auto kTimeoutMs = 5000;
[[nodiscard]] PasskeyEntry FromTL(const MTPDpasskey &data) {
return PasskeyEntry{
.id = qs(data.vid()),
.name = qs(data.vname()),
.date = data.vdate().v,
.softwareEmojiId = data.vsoftware_emoji_id().value_or(0),
.lastUsageDate = data.vlast_usage_date().value_or(0),
};
}
} // namespace
Passkeys::Passkeys(not_null<Main::Session*> session)
: _session(session) {
}
Passkeys::~Passkeys() = default;
void Passkeys::initRegistration(
Fn<void(const Data::Passkey::RegisterData&)> done) {
_session->api().request(MTPaccount_InitPasskeyRegistration(
)).done([=](const MTPaccount_PasskeyRegistrationOptions &result) {
const auto &data = result.data();
const auto jsonData = data.voptions().data().vdata().v;
if (const auto p = Data::Passkey::DeserializeRegisterData(jsonData)) {
done(*p);
}
}).send();
}
void Passkeys::registerPasskey(
const Platform::WebAuthn::RegisterResult &result,
Fn<void()> done) {
const auto credentialIdBase64 = QString::fromUtf8(
result.credentialId.toBase64(QByteArray::Base64UrlEncoding));
_session->api().request(MTPaccount_RegisterPasskey(
MTP_inputPasskeyCredentialPublicKey(
MTP_string(credentialIdBase64),
MTP_string(credentialIdBase64),
MTP_inputPasskeyResponseRegister(
MTP_dataJSON(MTP_bytes(result.clientDataJSON)),
MTP_bytes(result.attestationObject)))
)).done([=](const MTPPasskey &result) {
_passkeys.emplace_back(FromTL(result.data()));
_listUpdated.fire({});
done();
}).send();
}
void Passkeys::deletePasskey(
const QString &id,
Fn<void()> done,
Fn<void(QString)> fail) {
_session->api().request(MTPaccount_DeletePasskey(
MTP_string(id)
)).done([=] {
_lastRequestTime = 0;
_listKnown = false;
loadList();
done();
}).fail([=](const MTP::Error &error) {
fail(error.type());
}).send();
}
rpl::producer<> Passkeys::requestList() {
if (crl::now() - _lastRequestTime > kTimeoutMs) {
if (!_listRequestId) {
loadList();
}
return _listUpdated.events();
} else {
return _listUpdated.events_starting_with(rpl::empty_value());
}
}
const std::vector<PasskeyEntry> &Passkeys::list() const {
return _passkeys;
}
bool Passkeys::listKnown() const {
return _listKnown;
}
void Passkeys::loadList() {
_lastRequestTime = crl::now();
_listRequestId = _session->api().request(MTPaccount_GetPasskeys(
)).done([=](const MTPaccount_Passkeys &result) {
_listRequestId = 0;
_listKnown = true;
const auto &data = result.data();
_passkeys.clear();
_passkeys.reserve(data.vpasskeys().v.size());
for (const auto &passkey : data.vpasskeys().v) {
_passkeys.emplace_back(FromTL(passkey.data()));
}
_listUpdated.fire({});
}).fail([=] {
_listRequestId = 0;
}).send();
}
bool Passkeys::canRegister() const {
const auto max = _session->appConfig().passkeysAccountPasskeysMax();
return Platform::WebAuthn::IsSupported() && _passkeys.size() < max;
}
bool Passkeys::possible() const {
return _session->appConfig().settingsDisplayPasskeys();
}
void InitPasskeyLogin(
MTP::Sender &api,
Fn<void(const Data::Passkey::LoginData&)> done) {
api.request(MTPauth_InitPasskeyLogin(
MTP_int(ApiId),
MTP_string(ApiHash)
)).done([=](const MTPauth_PasskeyLoginOptions &result) {
const auto &data = result.data();
if (const auto p = Passkey::DeserializeLoginData(
data.voptions().data().vdata().v)) {
done(*p);
}
}).send();
}
void FinishPasskeyLogin(
MTP::Sender &api,
int initialDc,
const Platform::WebAuthn::LoginResult &result,
Fn<void(const MTPauth_Authorization&)> done,
Fn<void(QString)> fail) {
const auto userHandleStr = QString::fromUtf8(result.userHandle);
const auto parts = userHandleStr.split(':');
if (parts.size() != 2) {
return;
}
const auto userDc = parts[0].toInt();
const auto credentialIdBase64 = result.credentialId.toBase64(
QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
const auto credential = MTP_inputPasskeyCredentialPublicKey(
MTP_string(credentialIdBase64.toStdString()),
MTP_string(credentialIdBase64.toStdString()),
MTP_inputPasskeyResponseLogin(
MTP_dataJSON(MTP_bytes(result.clientDataJSON)),
MTP_bytes(result.authenticatorData),
MTP_bytes(result.signature),
MTP_string(userHandleStr.toStdString())
)
);
const auto flags = (userDc != initialDc)
? MTPauth_finishPasskeyLogin::Flag::f_from_dc_id
: MTPauth_finishPasskeyLogin::Flags(0);
api.request(MTPauth_FinishPasskeyLogin(
MTP_flags(flags),
credential,
MTP_int(initialDc),
MTP_long(0)
)).toDC(
userDc
).done(done).fail([=](const MTP::Error &error) {
fail(error.type());
}).send();
}
} // namespace Data

View File

@@ -0,0 +1,79 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
namespace Data::Passkey {
struct RegisterData;
struct LoginData;
} // namespace Data::Passkey
namespace Platform::WebAuthn {
struct RegisterResult;
struct LoginResult;
} // namespace Platform::WebAuthn
namespace Main {
class Session;
} // namespace Main
namespace MTP {
class Sender;
} // namespace MTP
namespace Data {
struct PasskeyEntry {
QString id;
QString name;
TimeId date = 0;
DocumentId softwareEmojiId = 0;
TimeId lastUsageDate = 0;
};
class Passkeys final {
public:
explicit Passkeys(not_null<Main::Session*> session);
~Passkeys();
void initRegistration(Fn<void(const Data::Passkey::RegisterData&)> done);
void registerPasskey(
const Platform::WebAuthn::RegisterResult &result,
Fn<void()> done);
void deletePasskey(
const QString &id,
Fn<void()> done,
Fn<void(QString)> fail);
[[nodiscard]] rpl::producer<> requestList();
[[nodiscard]] const std::vector<PasskeyEntry> &list() const;
[[nodiscard]] bool listKnown() const;
[[nodiscard]] bool canRegister() const;
[[nodiscard]] bool possible() const;
private:
void loadList();
const not_null<Main::Session*> _session;
std::vector<PasskeyEntry> _passkeys;
rpl::event_stream<> _listUpdated;
crl::time _lastRequestTime = 0;
mtpRequestId _listRequestId = 0;
bool _listKnown = false;
};
void InitPasskeyLogin(
MTP::Sender &api,
Fn<void(const Data::Passkey::LoginData&)> done);
void FinishPasskeyLogin(
MTP::Sender &api,
int initialDc,
const Platform::WebAuthn::LoginResult &result,
Fn<void(const MTPauth_Authorization&)> done,
Fn<void(QString)> fail);
} // namespace Data

View File

@@ -78,6 +78,7 @@ struct CreditsHistoryEntry final {
uint64 giftChannelSavedId = 0;
uint64 stargiftId = 0;
QString giftPrepayUpgradeHash;
QString giftTitle;
std::shared_ptr<UniqueGift> uniqueGift;
Fn<std::vector<CreditsHistoryEntry>()> pinnedSavedGifts;
uint64 nextToUpgradeStickerId = 0;
@@ -105,6 +106,7 @@ struct CreditsHistoryEntry final {
int starsForDetailsRemove = 0;
int premiumMonthsForStars = 0;
int floodSkip = 0;
int giftNumber = 0;
bool converted : 1 = false;
bool anonymous : 1 = false;
bool stargift : 1 = false;

View File

@@ -431,7 +431,7 @@ void DocumentMedia::GenerateGoodThumbnail(
document->setGoodThumbnailChecked(false);
return;
}
const auto guard = base::make_weak(&document->owner().session());
const auto guard = base::make_weak(&document->session());
crl::async([=, location = std::move(location)] {
const auto filepath = (location && location->accessEnable())
? location->name()

View File

@@ -45,7 +45,7 @@ WebPageDraft WebPageDraft::FromItem(not_null<HistoryItem*> item) {
Draft::Draft(
const TextWithTags &textWithTags,
FullReplyTo reply,
SuggestPostOptions suggest,
SuggestOptions suggest,
const MessageCursor &cursor,
WebPageDraft webpage,
mtpRequestId saveRequestId)
@@ -60,7 +60,7 @@ Draft::Draft(
Draft::Draft(
not_null<const Ui::InputField*> field,
FullReplyTo reply,
SuggestPostOptions suggest,
SuggestOptions suggest,
WebPageDraft webpage,
mtpRequestId saveRequestId)
: textWithTags(field->getTextWithTags())
@@ -110,7 +110,7 @@ void ApplyPeerCloudDraft(
}
}, [](const auto &) {});
}
auto suggest = SuggestPostOptions();
auto suggest = SuggestOptions();
if (!history->suggestDraftAllowed()) {
// Don't apply suggest options in unsupported chats.
} else if (const auto suggested = draft.vsuggested_post()) {
@@ -173,7 +173,7 @@ void SetChatLinkDraft(not_null<PeerData*> peer, TextWithEntities draft) {
.topicRootId = topicRootId,
.monoforumPeerId = monoforumPeerId,
},
SuggestPostOptions(),
SuggestOptions(),
cursor,
WebPageDraft()));
history->clearLocalEditDraft(topicRootId, monoforumPeerId);

View File

@@ -52,21 +52,21 @@ struct Draft {
Draft(
const TextWithTags &textWithTags,
FullReplyTo reply,
SuggestPostOptions suggest,
SuggestOptions suggest,
const MessageCursor &cursor,
WebPageDraft webpage,
mtpRequestId saveRequestId = 0);
Draft(
not_null<const Ui::InputField*> field,
FullReplyTo reply,
SuggestPostOptions suggest,
SuggestOptions suggest,
WebPageDraft webpage,
mtpRequestId saveRequestId = 0);
TimeId date = 0;
TextWithTags textWithTags;
FullReplyTo reply; // reply.messageId.msg is editMsgId for edit draft.
SuggestPostOptions suggest;
SuggestOptions suggest;
MessageCursor cursor;
WebPageDraft webpage;
mtpRequestId saveRequestId = 0;

View File

@@ -2606,10 +2606,11 @@ std::unique_ptr<HistoryView::Media> MediaGiftBox::createView(
.service = true,
.hideServiceText = true,
});
} else if (_data.type == GiftType::ChatTheme) {
} else if (_data.type == GiftType::ChatTheme
|| _data.type == GiftType::GiftOffer) {
return std::make_unique<HistoryView::ServiceBox>(
message,
std::make_unique<HistoryView::GiftThemeBox>(message, this));
std::make_unique<HistoryView::GiftServiceBox>(message, this));
} else if (const auto &unique = _data.unique) {
return std::make_unique<HistoryView::MediaGeneric>(
message,

View File

@@ -140,6 +140,7 @@ enum class GiftType : uchar {
StarGift, // count - stars
ChatTheme,
BirthdaySuggest,
GiftOffer,
};
struct GiftCode {
@@ -154,6 +155,7 @@ struct GiftCode {
PeerData *channelFrom = nullptr;
uint64 channelSavedId = 0;
QString giftPrepayUpgradeHash;
QString giftTitle;
MsgId giveawayMsgId = 0;
MsgId realGiftMsgId = 0;
int starsConverted = 0;
@@ -161,6 +163,7 @@ struct GiftCode {
int starsUpgradedBySender = 0;
int starsForDetailsRemove = 0;
int starsBid = 0;
int giftNum = 0;
int limitedCount = 0;
int limitedLeft = 0;
int64 count = 0;

View File

@@ -207,12 +207,13 @@ struct FullReplyTo {
friend inline bool operator==(FullReplyTo, FullReplyTo) = default;
};
struct SuggestPostOptions {
struct SuggestOptions {
uint32 exists : 1 = 0;
uint32 priceWhole : 31 = 0;
uint32 priceNano : 31 = 0;
uint32 ton : 1 = 0;
TimeId date = 0;
TimeId offerDuration = 0;
[[nodiscard]] CreditsAmount price() const {
return CreditsAmount(
@@ -226,11 +227,11 @@ struct SuggestPostOptions {
}
friend inline auto operator<=>(
SuggestPostOptions,
SuggestPostOptions) = default;
SuggestOptions,
SuggestOptions) = default;
friend inline bool operator==(
SuggestPostOptions,
SuggestPostOptions) = default;
SuggestOptions,
SuggestOptions) = default;
};
struct GlobalMsgId {

View File

@@ -0,0 +1,121 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "data/data_passkey_deserialize.h"
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
namespace Data::Passkey {
namespace {
[[nodiscard]] std::string SerializeClientData(
const QByteArray &challenge,
const QString &type) {
auto obj = QJsonObject();
obj["type"] = type;
obj["challenge"] = QString::fromUtf8(
challenge.toBase64(QByteArray::Base64UrlEncoding
| QByteArray::OmitTrailingEquals));
obj["origin"] = "https://telegram.org";
obj["crossOrigin"] = false;
return QJsonDocument(obj).toJson(QJsonDocument::Compact).toStdString();
}
} // namespace
std::optional<RegisterData> DeserializeRegisterData(
const QByteArray &jsonData) {
auto doc = QJsonDocument::fromJson(jsonData);
if (!doc.isObject()) {
return std::nullopt;
}
auto root = doc.object();
auto publicKey = root["publicKey"].toObject();
if (publicKey.isEmpty()) {
return std::nullopt;
}
auto data = RegisterData();
auto rp = publicKey["rp"].toObject();
data.rp.id = rp["id"].toString();
data.rp.name = rp["name"].toString();
auto user = publicKey["user"].toObject();
data.user.id = QByteArray::fromBase64(
user["id"].toString().toUtf8());
data.user.name = user["name"].toString();
data.user.displayName = user["displayName"].toString();
data.challenge = QByteArray::fromBase64(
publicKey["challenge"].toString().toUtf8(),
QByteArray::Base64UrlEncoding);
auto params = publicKey["pubKeyCredParams"].toArray();
for (const auto &param : params) {
auto obj = param.toObject();
CredentialParameter cp;
cp.type = obj["type"].toString();
cp.alg = obj["alg"].toInt();
data.pubKeyCredParams.push_back(cp);
}
data.timeout = publicKey["timeout"].toInt(60000);
return data;
}
std::string SerializeClientDataCreate(const QByteArray &challenge) {
return SerializeClientData(challenge, "webauthn.create");
}
std::string SerializeClientDataGet(const QByteArray &challenge) {
return SerializeClientData(challenge, "webauthn.get");
}
std::optional<LoginData> DeserializeLoginData(
const QByteArray &jsonData) {
auto doc = QJsonDocument::fromJson(jsonData);
if (!doc.isObject()) {
return std::nullopt;
}
auto root = doc.object();
auto publicKey = root["publicKey"].toObject();
if (publicKey.isEmpty()) {
return std::nullopt;
}
auto data = LoginData();
data.challenge = QByteArray::fromBase64(
publicKey["challenge"].toString().toUtf8(),
QByteArray::Base64UrlEncoding);
data.rpId = publicKey["rpId"].toString();
data.timeout = publicKey["timeout"].toInt(60000);
data.userVerification = publicKey["userVerification"].toString();
if (publicKey.contains("allowCredentials")) {
auto allowList = publicKey["allowCredentials"].toArray();
for (const auto &cred : allowList) {
auto credObj = cred.toObject();
Credential credential;
credential.id = QByteArray::fromBase64(
credObj["id"].toString().toUtf8(),
QByteArray::Base64UrlEncoding);
credential.type = credObj["type"].toString();
data.allowCredentials.push_back(credential);
}
}
return data;
}
} // namespace Data::Passkey

View File

@@ -0,0 +1,62 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
namespace Data::Passkey {
struct RelyingParty {
QString id;
QString name;
};
struct User {
QByteArray id;
QString name;
QString displayName;
};
struct CredentialParameter {
QString type;
int alg = 0;
};
struct RegisterData {
RelyingParty rp;
User user;
QByteArray challenge;
std::vector<CredentialParameter> pubKeyCredParams;
int timeout = 60000;
};
struct Credential {
QByteArray id;
QString type;
};
struct LoginData {
QByteArray challenge;
QString rpId;
std::vector<Credential> allowCredentials;
QString userVerification;
int timeout = 60000;
};
[[nodiscard]] std::optional<RegisterData> DeserializeRegisterData(
const QByteArray &jsonData);
[[nodiscard]] std::optional<LoginData> DeserializeLoginData(
const QByteArray &jsonData);
[[nodiscard]] std::string SerializeClientDataCreate(
const QByteArray &challenge);
[[nodiscard]] std::string SerializeClientDataGet(
const QByteArray &challenge);
} // namespace Data::Passkey

View File

@@ -3972,9 +3972,6 @@ void Session::webpageApplyFields(
using WebPageAuctionPtr = std::unique_ptr<WebPageAuction>;
const auto lookupAuction = [&]() -> WebPageAuctionPtr {
const auto toUint = [](const MTPint &c) {
return (uint32(1) << 24) | uint32(c.v);
};
if (const auto attributes = data.vattributes()) {
for (const auto &attribute : attributes->v) {
return attribute.match([&](
@@ -3986,12 +3983,6 @@ void Session::webpageApplyFields(
auto auction = std::make_unique<WebPageAuction>();
auction->auctionGift = std::make_shared<StarGift>(*gift);
auction->endDate = data.vend_date().v;
auction->centerColor = Ui::ColorFromSerialized(
toUint(data.vcenter_color()));
auction->edgeColor = Ui::ColorFromSerialized(
toUint(data.vedge_color()));
auction->textColor = Ui::ColorFromSerialized(
toUint(data.vtext_color()));
return auction;
}, [](const auto &) -> WebPageAuctionPtr { return nullptr; });
}

View File

@@ -59,7 +59,11 @@ constexpr auto kResaleGiftsPerPage = 50;
} // namespace
QString UniqueGiftName(const UniqueGift &gift) {
return gift.title + u" #"_q + QString::number(gift.number);
return UniqueGiftName(gift.title, gift.number);
}
QString UniqueGiftName(const QString &title, int number) {
return title + u" #"_q + QString::number(number);
}
CreditsAmount UniqueGiftResaleStars(const UniqueGift &gift) {

View File

@@ -20,14 +20,26 @@ namespace Data {
struct UniqueGiftAttribute {
QString name;
int rarityPermille = 0;
friend inline bool operator==(
const UniqueGiftAttribute &,
const UniqueGiftAttribute &) = default;
};
struct UniqueGiftModel : UniqueGiftAttribute {
not_null<DocumentData*> document;
friend inline bool operator==(
const UniqueGiftModel &,
const UniqueGiftModel &) = default;
};
struct UniqueGiftPattern : UniqueGiftAttribute {
not_null<DocumentData*> document;
friend inline bool operator==(
const UniqueGiftPattern &,
const UniqueGiftPattern &) = default;
};
struct UniqueGiftBackdrop : UniqueGiftAttribute {
@@ -36,6 +48,16 @@ struct UniqueGiftBackdrop : UniqueGiftAttribute {
QColor patternColor;
QColor textColor;
int id = 0;
friend inline bool operator==(
const UniqueGiftBackdrop &,
const UniqueGiftBackdrop &) = default;
};
struct UniqueGiftAttributes {
std::vector<UniqueGiftModel> models;
std::vector<UniqueGiftBackdrop> backdrops;
std::vector<UniqueGiftPattern> patterns;
};
struct UniqueGiftOriginalDetails {
@@ -48,6 +70,7 @@ struct UniqueGiftOriginalDetails {
struct UniqueGiftValue {
QString currency;
int64 valuePrice = 0;
int64 valuePriceUsd = 0;
CreditsAmount initialPriceStars;
int64 initialSalePrice = 0;
TimeId initialSaleDate = 0;
@@ -76,6 +99,7 @@ struct UniqueGift {
int64 nanoTonForResale = -1;
int starsForResale = -1;
int starsForTransfer = -1;
int starsMinOffer = -1;
int number = 0;
bool onlyAcceptTon = false;
bool canBeTheme = false;
@@ -91,6 +115,7 @@ struct UniqueGift {
};
[[nodiscard]] QString UniqueGiftName(const UniqueGift &gift);
[[nodiscard]] QString UniqueGiftName(const QString &title, int number);
[[nodiscard]] CreditsAmount UniqueGiftResaleStars(const UniqueGift &gift);
[[nodiscard]] CreditsAmount UniqueGiftResaleTon(const UniqueGift &gift);
@@ -100,9 +125,25 @@ struct UniqueGift {
[[nodiscard]] TextWithEntities FormatGiftResaleTon(const UniqueGift &gift);
[[nodiscard]] TextWithEntities FormatGiftResaleAsked(const UniqueGift &gift);
struct StarGiftBackground {
QColor center;
QColor edge;
QColor text;
[[nodiscard]] UniqueGiftBackdrop backdrop() const {
return {
.centerColor = center,
.edgeColor = edge,
.patternColor = edge,
.textColor = text,
};
}
};
struct StarGift {
uint64 id = 0;
std::shared_ptr<UniqueGift> unique;
std::shared_ptr<StarGiftBackground> background;
int64 stars = 0;
int64 starsConverted = 0;
int64 starsToUpgrade = 0;
@@ -113,10 +154,12 @@ struct StarGift {
int resellCount = 0;
QString auctionSlug;
int auctionGiftsPerRound = 0;
TimeId auctionStartDate = 0;
int limitedLeft = 0;
int limitedCount = 0;
int perUserTotal = 0;
int perUserRemains = 0;
int upgradeVariants = 0;
TimeId firstSaleDate = 0;
TimeId lastSaleDate = 0;
TimeId lockedUntilDate = 0;
@@ -197,6 +240,7 @@ struct SavedStarGift {
QString giftPrepayUpgradeHash;
PeerId fromId = 0;
TimeId date = 0;
int giftNum = 0;
bool upgradeSeparate = false;
bool upgradable = false;
bool anonymous = false;

View File

@@ -727,7 +727,7 @@ void Story::applyFields(
auto caption = TextWithEntities{
data.vcaption().value_or_empty(),
Api::EntitiesFromMTP(
&owner().session(),
&session(),
data.ventities().value_or_empty()),
};
if (const auto user = _peer->asUser()) {

View File

@@ -90,9 +90,6 @@ struct WebPageStickerSet {
struct WebPageAuction {
std::shared_ptr<Data::StarGift> auctionGift;
TimeId endDate = 0;
QColor centerColor;
QColor edgeColor;
QColor textColor;
};
struct WebPageData {

View File

@@ -145,7 +145,7 @@ const base::flat_map<QString, GeoBounds> &CountryBounds() {
{ u"PY"_q, GeoBounds{ -62.69, -27.55, -54.29, -19.34 } },
{ u"QA"_q, GeoBounds{ 50.74, 24.56, 51.61, 26.11 } },
{ u"RO"_q, GeoBounds{ 20.22, 43.69, 29.63, 48.22 } },
{ u"RU"_q, GeoBounds{ -180.0, 41.15, 180.0, 81.25 } },
{ u"RU"_q, GeoBounds{ -18.0, 41.15, 180.0, 81.25 } },
{ u"RW"_q, GeoBounds{ 29.02, -2.92, 30.82, -1.13 } },
{ u"SA"_q, GeoBounds{ 34.63, 16.35, 55.67, 32.16 } },
{ u"SD"_q, GeoBounds{ 21.94, 8.62, 38.41, 22.0 } },

View File

@@ -121,7 +121,7 @@ struct EntryState {
Section section = Section::History;
FilterId filterId = 0;
FullReplyTo currentReplyTo;
SuggestPostOptions currentSuggest;
SuggestOptions currentSuggest;
friend inline auto operator<=>(
const EntryState&,

View File

@@ -1677,6 +1677,7 @@ void Widget::toggleFiltersMenu(bool enabled) {
_chatFilters = base::make_unique_q<NoScrollPropagationWidget>(this);
const auto raw = _chatFilters.get();
const auto idBeforeTabs = controller()->activeChatsFilterCurrent();
const auto inner = Ui::AddChatFiltersTabsStrip(
_chatFilters.get(),
&session(),
@@ -1689,6 +1690,9 @@ void Widget::toggleFiltersMenu(bool enabled) {
Window::GifPauseReason::Any,
controller(),
true);
if (controller()->activeChatsFilterCurrent() != idBeforeTabs) {
controller()->setActiveChatsFilter(idBeforeTabs);
}
raw->show();
raw->stackUnder(_scroll);
raw->resizeToWidth(width());

View File

@@ -247,22 +247,8 @@ void FillSourceMenu(
add(viewProfileText, [=] {
controller->showPeerInfo(peer);
}, channel ? &st::menuIconInfo : &st::menuIconProfile);
if (peer->session().premiumPossible()
&& peer->isUser()
&& !peer->hasActiveVideoStream()
&& peer->hasUnreadStories()) {
const auto now = base::unixtime::now();
const auto stealth = owner->stories().stealthMode();
add(tr::lng_stories_view_anonymously(tr::now), [=] {
Media::Stories::SetupStealthMode(
controller->uiShow(),
Media::Stories::StealthModeDescriptor{
[=] { controller->openPeerStories(peer->id); },
&st::storiesStealthStyleDefault,
});
}, ((peer->session().premium() || (stealth.enabledTill > now))
? &st::menuIconStealth
: &st::menuIconStealthLocked));
if (!peer->hasActiveVideoStream() && peer->hasUnreadStories()) {
Media::Stories::AddStealthModeMenu(add, peer, controller);
}
const auto in = [&](Data::StorySourcesList list) {
return ranges::contains(

View File

@@ -84,7 +84,7 @@ ItemSticker::ItemSticker(
return true;
};
if (!updateThumbnail()) {
_document->owner().session().downloaderTaskFinished(
_document->session().downloaderTaskFinished(
) | rpl::start_with_next([=] {
if (updateThumbnail()) {
_loadingLifetime.destroy();

View File

@@ -905,6 +905,20 @@ UserpicsSlice ParseUserpicsSlice(
return result;
}
[[nodiscard]] ActionStarGift ParseStarGift(const MTPStarGift &gift) {
return gift.match([&](const MTPDstarGift &gift) {
return ActionStarGift{
.giftId = uint64(gift.vid().v),
.stars = int64(gift.vstars().v),
.limited = gift.is_limited(),
};
}, [&](const MTPDstarGiftUnique &gift) {
return ActionStarGift{
.giftId = uint64(gift.vid().v),
};
});
}
File &Story::file() {
return media.file();
}
@@ -1744,41 +1758,16 @@ ServiceAction ParseServiceAction(
.isUnclaimed = data.is_unclaimed(),
};
}, [&](const MTPDmessageActionStarGift &data) {
data.vgift().match([&](const MTPDstarGift &gift) {
result.content = ActionStarGift{
.giftId = uint64(gift.vid().v),
.stars = int64(gift.vstars().v),
.text = (data.vmessage()
? ParseText(
data.vmessage()->data().vtext(),
data.vmessage()->data().ventities().v)
: std::vector<TextPart>()),
.anonymous = data.is_name_hidden(),
.limited = gift.is_limited(),
};
}, [&](const MTPDstarGiftUnique &gift) {
result.content = ActionStarGift{
.giftId = uint64(gift.vid().v),
.text = (data.vmessage()
? ParseText(
data.vmessage()->data().vtext(),
data.vmessage()->data().ventities().v)
: std::vector<TextPart>()),
.anonymous = data.is_name_hidden(),
};
});
auto content = ParseStarGift(data.vgift());
content.text = (data.vmessage()
? ParseText(
data.vmessage()->data().vtext(),
data.vmessage()->data().ventities().v)
: std::vector<TextPart>());
content.anonymous = data.is_name_hidden();
result.content = content;
}, [&](const MTPDmessageActionStarGiftUnique &data) {
data.vgift().match([&](const MTPDstarGift &gift) {
result.content = ActionStarGift{
.giftId = uint64(gift.vid().v),
.stars = int64(gift.vstars().v),
.limited = gift.is_limited(),
};
}, [&](const MTPDstarGiftUnique &gift) {
result.content = ActionStarGift{
.giftId = uint64(gift.vid().v),
};
});
result.content = ParseStarGift(data.vgift());
}, [&](const MTPDmessageActionPaidMessagesRefunded &data) {
result.content = ActionPaidMessagesRefunded{
.messages = data.vcount().v,
@@ -1844,6 +1833,21 @@ ServiceAction ParseServiceAction(
fields.vmonth().v,
fields.vyear().value_or_empty());
result.content = content;
}, [&](const MTPDmessageActionStarGiftPurchaseOffer &data) {
auto content = ParseStarGift(data.vgift());
content.offer = true;
content.offerPrice = CreditsAmountFromTL(data.vprice());
content.offerExpireAt = data.vexpires_at().v;
content.offerAccepted = data.is_accepted();
content.offerDeclined = data.is_declined();
result.content = content;
}, [&](const MTPDmessageActionStarGiftPurchaseOfferDeclined &data) {
auto content = ParseStarGift(data.vgift());
content.offer = true;
content.offerDeclined = true;
content.offerExpired = data.is_expired();
content.offerPrice = CreditsAmountFromTL(data.vprice());
result.content = content;
}, [](const MTPDmessageActionEmpty &data) {});
return result;
}

View File

@@ -690,6 +690,13 @@ struct ActionStarGift {
std::vector<TextPart> text;
bool anonymous = false;
bool limited = false;
CreditsAmount offerPrice;
TimeId offerExpireAt = 0;
bool offer = false;
bool offerAccepted = false;
bool offerDeclined = false;
bool offerExpired = false;
};
struct ActionPaidMessagesRefunded {

View File

@@ -296,12 +296,6 @@ TextWithEntities GenerateAdminChangeText(
phraseMap[Flag::InviteByLinkOrAdd] = invitePhrase;
phraseMap[Flag::ManageCall] = callPhrase;
if (!channel->isMegagroup()) {
// Don't display "Ban users" changes in channels.
newRights.flags &= ~Flag::BanUsers;
prevRights.flags &= ~Flag::BanUsers;
}
const auto changes = CollectChanges(
phraseMap,
newRights.flags,

View File

@@ -244,7 +244,7 @@ void History::createLocalDraftFromCloud(
draft->reply.topicRootId = topicRootId;
draft->reply.monoforumPeerId = monoforumPeerId;
if (!suggestDraftAllowed()) {
draft->suggest = SuggestPostOptions();
draft->suggest = SuggestOptions();
}
auto existing = localDraft(topicRootId, monoforumPeerId);
if (Data::DraftIsNull(existing)
@@ -334,7 +334,7 @@ Data::Draft *History::createCloudDraft(
.topicRootId = topicRootId,
.monoforumPeerId = monoforumPeerId,
},
SuggestPostOptions(),
SuggestOptions(),
MessageCursor(),
Data::WebPageDraft()));
cloudDraft(topicRootId, monoforumPeerId)->date = TimeId(0);
@@ -362,7 +362,7 @@ Data::Draft *History::createCloudDraft(
existing->reply.topicRootId = topicRootId;
existing->reply.monoforumPeerId = monoforumPeerId;
if (!suggestDraftAllowed()) {
existing->suggest = SuggestPostOptions();
existing->suggest = SuggestOptions();
}
}

View File

@@ -1092,19 +1092,32 @@ bool HistoryItem::checkDiscussionLink(ChannelId id) const {
}
SuggestionActions HistoryItem::computeSuggestionActions() const {
return computeSuggestionActions(Get<HistoryMessageSuggestedPost>());
return computeSuggestionActions(Get<HistoryMessageSuggestion>());
}
SuggestionActions HistoryItem::computeSuggestionActions(
const HistoryMessageSuggestedPost *suggest) const {
const HistoryMessageSuggestion *suggest) const {
return suggest
? computeSuggestionActions(suggest->accepted, suggest->rejected)
? computeSuggestionActions(
suggest->accepted,
suggest->rejected,
suggest->gift ? suggest->date: 0)
: SuggestionActions::None;
}
SuggestionActions HistoryItem::computeSuggestionActions(
bool accepted,
bool rejected) const {
bool rejected,
TimeId giftOfferExpiresAt) const {
if (giftOfferExpiresAt) {
const auto can = isRegular()
&& !(accepted || rejected)
&& !out()
&& (giftOfferExpiresAt > base::unixtime::now());
return can
? SuggestionActions::GiftOfferActions
: SuggestionActions::None;
}
const auto channelIsAuthor = from()->isChannel();
const auto amMonoforumAdmin = history()->peer->amMonoforumAdmin();
const auto broadcast = history()->peer->monoforumBroadcast();
@@ -1125,7 +1138,7 @@ SuggestionActions HistoryItem::computeSuggestionActions(
}
void HistoryItem::updateSuggestControls(
const HistoryMessageSuggestedPost *suggest) {
const HistoryMessageSuggestion *suggest) {
if (const auto markup = Get<HistoryMessageReplyMarkup>()) {
markup->updateSuggestControls(computeSuggestionActions(suggest));
}
@@ -2034,17 +2047,17 @@ void HistoryItem::applyEdition(HistoryMessageEdition &&edition) {
if (!edition.useSameSuggest) {
if (edition.suggest.exists) {
if (!Has<HistoryMessageSuggestedPost>()) {
AddComponents(HistoryMessageSuggestedPost::Bit());
if (!Has<HistoryMessageSuggestion>()) {
AddComponents(HistoryMessageSuggestion::Bit());
}
auto suggest = Get<HistoryMessageSuggestedPost>();
auto suggest = Get<HistoryMessageSuggestion>();
suggest->price = edition.suggest.price;
suggest->date = edition.suggest.date;
suggest->accepted = edition.suggest.accepted;
suggest->rejected = edition.suggest.rejected;
updateSuggestControls(suggest);
} else {
RemoveComponents(HistoryMessageSuggestedPost::Bit());
RemoveComponents(HistoryMessageSuggestion::Bit());
updateSuggestControls(nullptr);
}
}
@@ -4074,10 +4087,11 @@ void HistoryItem::createComponents(CreateConfig &&config) {
}
}
if (config.suggest.exists) {
mask |= HistoryMessageSuggestedPost::Bit();
mask |= HistoryMessageSuggestion::Bit();
if (computeSuggestionActions(
config.suggest.accepted,
config.suggest.rejected
config.suggest.rejected,
TimeId()
) != SuggestionActions::None) {
mask |= HistoryMessageReplyMarkup::Bit();
}
@@ -4179,7 +4193,7 @@ void HistoryItem::createComponents(CreateConfig &&config) {
flagSensitiveContent();
}
if (const auto suggest = Get<HistoryMessageSuggestedPost>()) {
if (const auto suggest = Get<HistoryMessageSuggestion>()) {
suggest->price = config.suggest.price;
suggest->date = config.suggest.date;
suggest->accepted = config.suggest.accepted;
@@ -4827,7 +4841,7 @@ void HistoryItem::createServiceFromMtp(const MTPDmessageService &message) {
const auto &data = action.c_messageActionSuggestedPostSuccess();
UpdateComponents(HistoryServiceSuggestFinish::Bit());
const auto finish = Get<HistoryServiceSuggestFinish>();
finish->successPrice = CreditsAmountFromTL(data.vprice());
finish->price = CreditsAmountFromTL(data.vprice());
} else if (type == mtpc_messageActionSuggestedPostRefund) {
const auto &data = action.c_messageActionSuggestedPostRefund();
UpdateComponents(HistoryServiceSuggestFinish::Bit());
@@ -4835,6 +4849,43 @@ void HistoryItem::createServiceFromMtp(const MTPDmessageService &message) {
finish->refundType = data.is_payer_initiated()
? SuggestRefundType::User
: SuggestRefundType::Admin;
} else if (type == mtpc_messageActionStarGiftPurchaseOffer) {
const auto &data = action.c_messageActionStarGiftPurchaseOffer();
const auto accepted = data.is_accepted();
const auto rejected = data.is_declined();
const auto expiresAt = data.vexpires_at().v;
const auto gift = Api::FromTL(&history()->session(), data.vgift());
if (const auto unique = gift ? gift->unique : nullptr) {
auto mask = HistoryMessageSuggestion::Bit();
const auto actions = computeSuggestionActions(
accepted,
rejected,
expiresAt);
if (actions != SuggestionActions::None) {
mask |= HistoryMessageReplyMarkup::Bit();
}
UpdateComponents(mask);
const auto suggestion = Get<HistoryMessageSuggestion>();
suggestion->price = CreditsAmountFromTL(data.vprice());
suggestion->date = expiresAt;
suggestion->accepted = accepted;
suggestion->rejected = rejected;
suggestion->gift = unique;
if (actions != SuggestionActions::None) {
const auto markup = Get<HistoryMessageReplyMarkup>();
markup->updateSuggestControls(actions);
}
}
} else if (type == mtpc_messageActionStarGiftPurchaseOfferDeclined) {
const auto &data = action.c_messageActionStarGiftPurchaseOfferDeclined();
UpdateComponents(HistoryServiceSuggestFinish::Bit());
const auto finish = Get<HistoryServiceSuggestFinish>();
finish->refundType = data.is_expired()
? SuggestRefundType::Expired
: SuggestRefundType::User;
finish->price = CreditsAmountFromTL(data.vprice());
}
if (const auto replyTo = message.vreply_to()) {
replyTo->match([&](const MTPDmessageReplyHeader &data) {
@@ -6151,17 +6202,10 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) {
auto result = PreparedServiceText();
const auto isSelf = _from->isSelf();
const auto resale = CreditsAmountFromTL(action.vresale_amount());
const auto fromOffer = action.is_from_offer();
const auto resaleCost = !resale
? TextWithEntities()
: resale.stars()
? TextWithEntities{ tr::lng_action_gift_for_stars(
tr::now,
lt_count,
resale.value()) }
: TextWithEntities{ tr::lng_action_gift_for_ton(
tr::now,
lt_count,
resale.value()) };
? tr::marked()
: tr::marked(PrepareCreditsAmountText(resale));
const auto giftPeer = action.vpeer()
? peerFromMTP(*action.vpeer())
: PeerId();
@@ -6240,7 +6284,21 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) {
if (!resale || !isSelf) {
result.links.push_back(from->createOpenLink());
}
result.text = resale
result.text = fromOffer
? (isSelf
? tr::lng_action_gift_sent_sold(
tr::now,
lt_cost,
resaleCost,
Ui::Text::WithEntities)
: tr::lng_action_gift_received_sold(
tr::now,
lt_user,
Ui::Text::Link(peer->shortName(), 1), // Link 1.
lt_cost,
resaleCost,
Ui::Text::WithEntities))
: resale
? (isSelf
? tr::lng_action_gift_sent(
tr::now,
@@ -6393,7 +6451,7 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) {
Unexpected("PhoneCall type in setServiceMessageFromMtp.");
};
auto prepareSuggestBirthday = [this](const MTPDmessageActionSuggestBirthday &action) {
auto prepareSuggestBirthday = [&](const MTPDmessageActionSuggestBirthday &action) {
auto result = PreparedServiceText{};
const auto isSelf = (_from->id == _from->session().userPeerId());
const auto peer = isSelf ? history()->peer : _from;
@@ -6412,6 +6470,96 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) {
return result;
};
auto prepareStarGiftPurchaseOffer = [&](const MTPDmessageActionStarGiftPurchaseOffer &action) {
auto result = PreparedServiceText{};
action.vgift().match([&](const MTPDstarGiftUnique &data) {
const auto amount = CreditsAmountFromTL(action.vprice());
const auto cost = tr::marked(PrepareCreditsAmountText(amount));
const auto giftName = tr::bold(qs(data.vtitle())
+ u" #"_q
+ QString::number(data.vnum().v));
if (_from->isSelf()) {
result.text = tr::lng_action_gift_offer_you(
tr::now,
lt_cost,
cost,
lt_name,
giftName,
tr::marked);
} else {
result.links.push_back(fromLink());
result.text = tr::lng_action_gift_offer(
tr::now,
lt_user,
fromLinkText(),
lt_cost,
cost,
lt_name,
giftName,
tr::marked);
}
}, [](const MTPDstarGift &) {
});
return result;
};
auto prepareStarGiftPurchaseOfferDeclined = [&](const MTPDmessageActionStarGiftPurchaseOfferDeclined &action) {
auto result = PreparedServiceText{};
action.vgift().match([&](const MTPDstarGiftUnique &data) {
const auto amount = CreditsAmountFromTL(action.vprice());
const auto cost = tr::marked(PrepareCreditsAmountText(amount));
const auto giftName = Ui::Text::Bold(qs(data.vtitle())
+ u" #"_q
+ QString::number(data.vnum().v));
const auto expired = action.is_expired();
if (_from->isSelf()) {
result.links.push_back(_history->peer->createOpenLink());
result.text = expired
? tr::lng_action_gift_offer_expired(
tr::now,
lt_user,
tr::link(st::wrap_rtl(_history->peer->name()), 1),
lt_name,
giftName,
lt_cost,
cost,
tr::marked)
: tr::lng_action_gift_offer_declined_you(
tr::now,
lt_user,
tr::link(st::wrap_rtl(_history->peer->name()), 1),
lt_name,
giftName,
lt_cost,
cost,
tr::marked);
} else {
if (!expired) {
result.links.push_back(fromLink());
}
result.text = expired
? tr::lng_action_gift_offer_expired_your(
tr::now,
lt_name,
giftName,
lt_cost,
cost,
tr::marked)
: tr::lng_action_gift_offer_declined(
tr::now,
lt_user,
fromLinkText(),
lt_name,
giftName,
lt_cost,
cost,
tr::marked);
}
}, [](const MTPDstarGift &) {
});
return result;
};
setServiceText(action.match(
prepareChatAddUserText,
prepareChatJoinedByLink,
@@ -6469,6 +6617,8 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) {
prepareSuggestedPostSuccess,
prepareSuggestedPostRefund,
prepareSuggestBirthday,
prepareStarGiftPurchaseOffer,
prepareStarGiftPurchaseOfferDeclined,
PrepareEmptyText<MTPDmessageActionRequestedPeerSentMe>,
PrepareErrorText<MTPDmessageActionEmpty>));
@@ -6622,6 +6772,11 @@ void HistoryItem::applyAction(const MTPMessageAction &action) {
: PeerId();
const auto upgradeMsgId = data.vupgrade_msg_id().value_or_empty();
const auto realGiftMsgId = data.vgift_msg_id().value_or_empty();
const auto title = data.vgift().match([&](const MTPDstarGift &gift) {
return qs(gift.vtitle().value_or_empty());
}, [](const MTPDstarGiftUnique &) {
return QString();
});
const auto bid = data.vgift().match([&](const MTPDstarGift &gift) {
return data.is_auction_acquired()
? (int(gift.vstars().v)
@@ -6652,11 +6807,13 @@ void HistoryItem::applyAction(const MTPMessageAction &action) {
.channelSavedId = data.vsaved_id().value_or_empty(),
.giftPrepayUpgradeHash = qs(
data.vprepaid_upgrade_hash().value_or_empty()),
.giftTitle = title,
.realGiftMsgId = (upgradeMsgId ? upgradeMsgId : realGiftMsgId),
.starsConverted = int(data.vconvert_stars().value_or_empty()),
.starsUpgradedBySender = int(
data.vupgrade_stars().value_or_empty()),
.starsBid = bid,
.giftNum = data.vgift_num().value_or_empty(),
.type = Data::GiftType::StarGift,
.upgradeSeparate = data.is_upgrade_separate(),
.upgradeGifted = data.is_prepaid_upgrade(),
@@ -6737,6 +6894,18 @@ void HistoryItem::applyAction(const MTPMessageAction &action) {
fields.vyear().value_or_empty()).serialize(),
.type = Data::GiftType::BirthdaySuggest,
});
}, [&](const MTPDmessageActionStarGiftPurchaseOffer &data) {
if (const auto suggestion = Get<HistoryMessageSuggestion>()) {
Assert(suggestion->gift != nullptr);
_media = std::make_unique<Data::MediaGiftBox>(
this,
_from,
Data::GiftCode{
.unique = suggestion->gift,
.type = Data::GiftType::GiftOffer,
});
}
}, [](const auto &) {
});
}

View File

@@ -22,7 +22,7 @@ struct HistoryMessageMarkupData;
struct HistoryMessageReplyMarkup;
struct HistoryMessageTranslation;
struct HistoryMessageForwarded;
struct HistoryMessageSuggestedPost;
struct HistoryMessageSuggestion;
struct HistoryServiceDependentData;
struct HistoryServiceTodoCompletions;
enum class HistorySelfDestructType;
@@ -577,10 +577,11 @@ public:
[[nodiscard]] SuggestionActions computeSuggestionActions() const;
[[nodiscard]] SuggestionActions computeSuggestionActions(
const HistoryMessageSuggestedPost *suggest) const;
const HistoryMessageSuggestion *suggest) const;
[[nodiscard]] SuggestionActions computeSuggestionActions(
bool accepted,
bool rejected) const;
bool rejected,
TimeId giftOfferExpiresAt) const;
[[nodiscard]] bool needsUpdateForVideoQualities(const MTPMessage &data);
@@ -620,7 +621,7 @@ private:
void setReplyMarkup(
HistoryMessageMarkupData &&markup,
bool ignoreSuggestButtons = false);
void updateSuggestControls(const HistoryMessageSuggestedPost *suggest);
void updateSuggestControls(const HistoryMessageSuggestion *suggest);
void changeReplyToTopCounter(
not_null<HistoryMessageReply*> reply,

View File

@@ -1219,7 +1219,8 @@ bool HistoryMessageReplyMarkup::hiddenBy(Data::Media *media) const {
void HistoryMessageReplyMarkup::updateSuggestControls(
SuggestionActions actions) {
if (actions == SuggestionActions::AcceptAndDecline) {
if (actions == SuggestionActions::AcceptAndDecline
|| actions == SuggestionActions::GiftOfferActions) {
data.flags |= ReplyMarkupFlag::SuggestionAccept;
} else {
data.flags &= ~ReplyMarkupFlag::SuggestionAccept;
@@ -1238,7 +1239,21 @@ void HistoryMessageReplyMarkup::updateSuggestControls(
type,
&HistoryMessageMarkupButton::type);
};
if (actions == SuggestionActions::AcceptAndDecline) {
if (actions == SuggestionActions::GiftOfferActions) {
if (has(Type::SuggestAccept)) {
// Nothing changed.
}
data.rows.push_back({
{
Type::SuggestDecline,
tr::lng_action_gift_offer_decline(tr::now),
},
{
Type::SuggestAccept,
tr::lng_action_gift_offer_accept(tr::now),
},
});
} else if (actions == SuggestionActions::AcceptAndDecline) {
// ... rows ...
// [decline] | [accept]
// [suggestchanges]

View File

@@ -62,6 +62,7 @@ enum class SuggestionActions : uchar {
None,
Decline,
AcceptAndDecline,
GiftOfferActions,
};
struct HistoryMessageVia : RuntimeComponent<HistoryMessageVia, HistoryItem> {
@@ -622,8 +623,9 @@ struct HistoryMessageFactcheck
bool requested = false;
};
struct HistoryMessageSuggestedPost
: RuntimeComponent<HistoryMessageSuggestedPost, HistoryItem> {
struct HistoryMessageSuggestion
: RuntimeComponent<HistoryMessageSuggestion, HistoryItem> {
std::shared_ptr<Data::UniqueGift> gift;
CreditsAmount price;
TimeId date = 0;
mtpRequestId requestId = 0;
@@ -713,12 +715,13 @@ enum class SuggestRefundType {
None,
User,
Admin,
Expired,
};
struct HistoryServiceSuggestFinish
: RuntimeComponent<HistoryServiceSuggestFinish, HistoryItem>
, HistoryServiceDependentData {
CreditsAmount successPrice;
CreditsAmount price;
SuggestRefundType refundType = SuggestRefundType::None;
};

View File

@@ -223,7 +223,7 @@ std::optional<SendPaymentDetails> ComputePaymentDetails(
bool SuggestPaymentDataReady(
not_null<PeerData*> peer,
SuggestPostOptions suggest) {
SuggestOptions suggest) {
if (!suggest.exists || !suggest.price() || peer->amMonoforumAdmin()) {
return true;
} else if (suggest.ton && !peer->session().credits().tonLoaded()) {

View File

@@ -151,7 +151,7 @@ struct SendPaymentDetails {
[[nodiscard]] bool SuggestPaymentDataReady(
not_null<PeerData*> peer,
SuggestPostOptions suggest);
SuggestOptions suggest);
struct PaidConfirmStyles {
const style::FlatLabel *label = nullptr;

View File

@@ -346,7 +346,7 @@ HistoryMessageSuggestInfo::HistoryMessageSuggestInfo(
}
HistoryMessageSuggestInfo::HistoryMessageSuggestInfo(
SuggestPostOptions options) {
SuggestOptions options) {
if (!options.exists) {
return;
}

View File

@@ -152,7 +152,7 @@ struct HistoryMessageSuggestInfo {
HistoryMessageSuggestInfo() = default;
explicit HistoryMessageSuggestInfo(const MTPSuggestedPost *data);
explicit HistoryMessageSuggestInfo(const Api::SendOptions &options);
explicit HistoryMessageSuggestInfo(SuggestPostOptions options);
explicit HistoryMessageSuggestInfo(SuggestOptions options);
CreditsAmount price;
TimeId date = 0;

View File

@@ -3238,12 +3238,12 @@ void HistoryWidget::refreshSendGiftToggle() {
}
void HistoryWidget::applySuggestOptions(
SuggestPostOptions suggest,
SuggestOptions suggest,
HistoryView::SuggestMode mode) {
Expects(suggest.exists);
using namespace HistoryView;
_suggestOptions = std::make_unique<SuggestOptions>(
_suggestOptions = std::make_unique<SuggestOptionsBar>(
controller()->uiShow(),
_peer,
suggest,
@@ -6796,13 +6796,13 @@ FullReplyTo HistoryWidget::replyTo() const {
: FullReplyTo();
}
SuggestPostOptions HistoryWidget::suggestOptions(
SuggestOptions HistoryWidget::suggestOptions(
bool skipNoAdminCheck) const {
const auto checked = skipNoAdminCheck
|| (_history && _history->suggestDraftAllowed());
return (checked && _suggestOptions)
? _suggestOptions->values()
: SuggestPostOptions();
: SuggestOptions();
}
bool HistoryWidget::hasSavedScroll() const {
@@ -8757,12 +8757,12 @@ void HistoryWidget::setReplyFieldsFromProcessing() {
if (_editMsgId) {
if (const auto localDraft = _history->localDraft({}, {})) {
localDraft->reply = id;
localDraft->suggest = SuggestPostOptions();
localDraft->suggest = SuggestOptions();
} else {
_history->setLocalDraft(std::make_unique<Data::Draft>(
TextWithTags(),
id,
SuggestPostOptions(),
SuggestOptions(),
MessageCursor(),
Data::WebPageDraft()));
}
@@ -8828,7 +8828,7 @@ void HistoryWidget::editMessage(
_history->setLocalEditDraft(std::make_unique<Data::Draft>(
editData,
FullReplyTo{ item->fullId() },
SuggestPostOptions(),
SuggestOptions(),
cursor,
previewDraft));
applyDraft();

View File

@@ -111,7 +111,7 @@ class TranslateBar;
class ComposeSearch;
class SubsectionTabs;
struct SelectedQuote;
class SuggestOptions;
class SuggestOptionsBar;
enum class SuggestMode;
} // namespace HistoryView
@@ -217,7 +217,7 @@ public:
not_null<PeerData*> peer);
[[nodiscard]] FullReplyTo replyTo() const;
[[nodiscard]] SuggestPostOptions suggestOptions(
[[nodiscard]] SuggestOptions suggestOptions(
bool skipNoAdminCheck = false) const;
bool lastForceReplyReplied(const FullMsgId &replyTo) const;
bool lastForceReplyReplied() const;
@@ -686,7 +686,7 @@ private:
void refreshSendGiftToggle();
void refreshSuggestPostToggle();
void applySuggestOptions(
SuggestPostOptions suggest,
SuggestOptions suggest,
HistoryView::SuggestMode mode);
void setupSendAsToggle();
void refreshSendAsToggle();
@@ -721,7 +721,7 @@ private:
std::unique_ptr<Ui::SpoilerAnimation> _replySpoiler;
mutable base::Timer _updateEditTimeLeftDisplay;
std::unique_ptr<HistoryView::SuggestOptions> _suggestOptions;
std::unique_ptr<HistoryView::SuggestOptionsBar> _suggestOptions;
object_ptr<Ui::IconButton> _fieldBarCancel;

View File

@@ -154,7 +154,7 @@ public:
void editMessage(
FullMsgId id,
SuggestPostOptions suggest,
SuggestOptions suggest,
bool photoEditAllowed = false);
void replyToMessage(FullReplyTo id);
void updateForwarding(
@@ -180,7 +180,7 @@ public:
[[nodiscard]] SendMenu::Details saveMenuDetails(bool hasSendText) const;
[[nodiscard]] FullReplyTo getDraftReply() const;
[[nodiscard]] SuggestPostOptions suggestOptions() const;
[[nodiscard]] SuggestOptions suggestOptions() const;
[[nodiscard]] rpl::producer<> editCancelled() const {
return _editCancelled.events();
}
@@ -213,7 +213,7 @@ private:
bool hasPreview() const;
void applySuggestOptions(SuggestPostOptions suggest, SuggestMode mode);
void applySuggestOptions(SuggestOptions suggest, SuggestMode mode);
void cancelSuggestPost();
struct Preview {
@@ -240,7 +240,7 @@ private:
rpl::variable<FullMsgId> _editMsgId;
rpl::variable<FullReplyTo> _replyTo;
std::unique_ptr<ForwardPanel> _forwardPanel;
std::unique_ptr<SuggestOptions> _suggestOptions;
std::unique_ptr<SuggestOptionsBar> _suggestOptions;
rpl::producer<> _toForwardUpdated;
HistoryItem *_shownMessage = nullptr;
@@ -773,10 +773,10 @@ FullReplyTo FieldHeader::getDraftReply() const {
: _replyTo.current();
}
SuggestPostOptions FieldHeader::suggestOptions() const {
SuggestOptions FieldHeader::suggestOptions() const {
return _suggestOptions
? _suggestOptions->values()
: SuggestPostOptions();
: SuggestOptions();
}
void FieldHeader::updateControlsGeometry(QSize size) {
@@ -795,7 +795,7 @@ void FieldHeader::updateControlsGeometry(QSize size) {
void FieldHeader::editMessage(
FullMsgId id,
SuggestPostOptions suggest,
SuggestOptions suggest,
bool photoEditAllowed) {
_photoEditAllowed = photoEditAllowed;
_editMsgId = id;
@@ -817,12 +817,12 @@ void FieldHeader::editMessage(
}
void FieldHeader::applySuggestOptions(
SuggestPostOptions suggest,
SuggestOptions suggest,
SuggestMode mode) {
Expects(suggest.exists);
using namespace HistoryView;
_suggestOptions = std::make_unique<SuggestOptions>(
_suggestOptions = std::make_unique<SuggestOptionsBar>(
_show,
_history->peer,
suggest,
@@ -1296,7 +1296,7 @@ void ComposeControls::setCurrentDialogsEntryState(
unregisterDraftSources();
state.currentReplyTo.topicRootId = _topicRootId;
state.currentReplyTo.monoforumPeerId = _monoforumPeerId;
state.currentSuggest = SuggestPostOptions();
state.currentSuggest = SuggestOptions();
_currentDialogsEntryState = state;
updateForwarding();
registerDraftSource();
@@ -1952,7 +1952,7 @@ void ComposeControls::saveFieldToHistoryLocalDraft() {
std::make_unique<Data::Draft>(
_field,
id,
SuggestPostOptions(),
SuggestOptions(),
_preview->draft()));
} else {
_history->clearDraft(draftKeyCurrent());
@@ -2067,7 +2067,7 @@ void ComposeControls::init() {
const auto topicRootId = _topicRootId;
const auto monoforumPeerId = _monoforumPeerId;
const auto reply = _header->replyingToMessage();
const auto suggest = SuggestPostOptions();
const auto suggest = SuggestOptions();
const auto webpage = _preview->draft();
const auto done = [=](
@@ -2635,7 +2635,7 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) {
: FullMsgId();
const auto editingSuggest = (draft && draft == editDraft)
? draft->suggest
: SuggestPostOptions();
: SuggestOptions();
InvokeQueued(_autocomplete.get(), [=] {
if (_autocomplete) {
@@ -2717,7 +2717,7 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) {
}
_canReplaceMedia = _canAddMedia = false;
_photoEditMedia = nullptr;
_header->editMessage(editingId, SuggestPostOptions(), false);
_header->editMessage(editingId, SuggestOptions(), false);
return false;
};
if (!resolve()) {
@@ -3762,7 +3762,7 @@ void ComposeControls::editMessage(not_null<HistoryItem*> item) {
.topicRootId = key.topicRootId(),
.monoforumPeerId = key.monoforumPeerId(),
},
SuggestPostOptions(),
SuggestOptions(),
cursor,
Data::WebPageDraft::FromItem(item)));
applyDraft();
@@ -3857,7 +3857,7 @@ void ComposeControls::replyToMessage(FullReplyTo id) {
std::make_unique<Data::Draft>(
TextWithTags(),
id,
SuggestPostOptions(),
SuggestOptions(),
MessageCursor(),
Data::WebPageDraft()));
}

View File

@@ -161,7 +161,7 @@ ListController::ListController(not_null<History*> history)
}
Main::Session &ListController::session() const {
return _history->owner().session();
return _history->session();
}
void ListController::prepare() {

View File

@@ -1387,7 +1387,7 @@ void ShowReplyToChatBox(
history->setLocalDraft(std::make_unique<Data::Draft>(
textWithTags,
reply,
SuggestPostOptions(),
SuggestOptions(),
cursor,
Data::WebPageDraft()));
history->clearLocalEditDraft(topicRootId, monoforumPeerId);

View File

@@ -353,7 +353,7 @@ void ClearDraftReplyTo(
.topicRootId = topicRootId,
.monoforumPeerId = monoforumPeerId,
};
draft.suggest = SuggestPostOptions();
draft.suggest = SuggestOptions();
if (Data::DraftIsNull(&draft)) {
history->clearLocalDraft(topicRootId, monoforumPeerId);
} else {

View File

@@ -20,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history.h"
#include "history/history_item.h"
#include "history/history_item_components.h"
#include "history/history_item_helpers.h"
#include "info/channel_statistics/earn/earn_format.h"
#include "info/channel_statistics/earn/earn_icons.h"
#include "lang/lang_keys.h"
@@ -31,6 +32,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/layers/generic_box.h"
#include "ui/text/text_utilities.h"
#include "ui/boxes/choose_date_time.h"
#include "ui/boxes/single_choice_box.h"
#include "ui/controls/ton_common.h"
#include "ui/widgets/fields/number_input.h"
#include "ui/widgets/fields/input_field.h"
@@ -131,8 +133,8 @@ void AddApproximateUsd(
}
const auto appConfig = &session->appConfig();
const auto rate = amount.ton()
? appConfig->currencyWithdrawRate()
: (appConfig->starsWithdrawRate() / 100.);
? appConfig->currencySellRate()
: (appConfig->starsSellRate() / 100.);
return Info::ChannelEarn::ToUsd(amount, rate, 2);
});
const auto usd = Ui::CreateChild<Ui::FlatLabel>(
@@ -401,6 +403,7 @@ void ChooseSuggestPriceBox(
rpl::event_stream<> fieldsChanges;
rpl::variable<CreditsAmount> price;
rpl::variable<TimeId> date;
rpl::variable<TimeId> offerDuration;
rpl::variable<bool> ton;
Fn<std::optional<CreditsAmount>()> computePrice;
Fn<void()> save;
@@ -413,6 +416,10 @@ void ChooseSuggestPriceBox(
state->price = args.value.price();
const auto peer = args.peer;
[[maybe_unused]] const auto details = ComputePaymentDetails(peer, 1);
const auto mode = args.mode;
const auto gift = (mode == SuggestMode::Gift);
const auto admin = peer->amMonoforumAdmin();
const auto broadcast = peer->monoforumBroadcast();
const auto usePeer = broadcast ? broadcast : peer;
@@ -426,7 +433,9 @@ void ChooseSuggestPriceBox(
box->setStyle(st::suggestPriceBox);
auto title = (args.mode == SuggestMode::New)
auto title = gift
? tr::lng_gift_offer_title()
: (mode == SuggestMode::New)
? tr::lng_suggest_options_title()
: tr::lng_suggest_options_change();
if (admin) {
@@ -567,25 +576,43 @@ void ChooseSuggestPriceBox(
auto starsAbout = admin
? rpl::combine(
youGet(StarsPriceValue(state->price.value()), true),
tr::lng_suggest_options_stars_warning(Ui::Text::RichLangValue)
tr::lng_suggest_options_stars_warning(tr::rich)
) | rpl::map([=](const QString &t1, const TextWithEntities &t2) {
return TextWithEntities{ t1 }.append("\n\n").append(t2);
})
: tr::lng_suggest_options_stars_price_about(Ui::Text::WithEntities);
: gift
? tr::lng_gift_offer_stars_about(
lt_name,
rpl::single(tr::marked(args.giftName)),
tr::rich)
: tr::lng_suggest_options_stars_price_about(tr::rich);
auto tonAbout = admin
? youGet(
TonPriceValue(state->price.value()),
false
) | Ui::Text::ToWithEntities()
: tr::lng_suggest_options_ton_price_about(Ui::Text::WithEntities);
) | rpl::map(tr::rich)
: gift
? tr::lng_gift_offer_ton_about(
lt_name,
rpl::single(tr::marked(args.giftName)),
tr::rich)
: tr::lng_suggest_options_ton_price_about(tr::rich);
auto priceInput = AddStarsTonPriceInput(container, {
.session = session,
.showTon = state->ton.value(),
.price = args.value.price(),
.starsMin = appConfig.suggestedPostStarsMin(),
.starsMax = appConfig.suggestedPostStarsMax(),
.nanoTonMin = appConfig.suggestedPostNanoTonMin(),
.nanoTonMax = appConfig.suggestedPostNanoTonMax(),
.starsMin = (gift
? appConfig.giftResaleStarsMin()
: appConfig.suggestedPostStarsMin()),
.starsMax = (gift
? appConfig.giftResaleStarsMax()
: appConfig.suggestedPostStarsMax()),
.nanoTonMin = (gift
? appConfig.giftResaleNanoTonMin()
: appConfig.suggestedPostNanoTonMin()),
.nanoTonMax = (gift
? appConfig.giftResaleNanoTonMax()
: appConfig.suggestedPostNanoTonMax()),
.starsAbout = std::move(starsAbout),
.tonAbout = std::move(tonAbout),
});
@@ -595,39 +622,94 @@ void ChooseSuggestPriceBox(
Ui::AddSkip(container);
const auto time = Settings::AddButtonWithLabel(
container,
tr::lng_suggest_options_date(),
state->date.value() | rpl::map([](TimeId date) {
return date
? langDateTime(base::unixtime::parse(date))
: tr::lng_suggest_options_date_any(tr::now);
}),
st::settingsButtonNoIcon);
time->setClickedCallback([=] {
const auto weak = std::make_shared<base::weak_qptr<Ui::BoxContent>>();
const auto parentWeak = base::make_weak(box);
const auto done = [=](TimeId result) {
if (parentWeak) {
state->date = result;
}
if (const auto strong = weak->get()) {
strong->closeBox();
}
if (gift) {
const auto day = 86400;
auto durations = std::vector{
day / 4,
day / 2,
day,
day + day / 2,
day * 2,
day * 3,
};
auto dateBox = Box(ChooseSuggestTimeBox, SuggestTimeBoxArgs{
.session = session,
.done = done,
.value = state->date.current(),
.mode = args.mode,
});
*weak = dateBox.data();
box->uiShow()->show(std::move(dateBox));
});
if (peer->session().isTestMode()) {
durations.insert(begin(durations), 120);
}
const auto durationToText = [](TimeId date) {
return (date >= 3600)
? tr::lng_hours(tr::now, lt_count, date / 3600)
: tr::lng_minutes(tr::now, lt_count, date / 60);
};
state->offerDuration = day;
const auto duration = Settings::AddButtonWithLabel(
container,
tr::lng_gift_offer_duration(),
state->offerDuration.value() | rpl::map(durationToText),
st::settingsButtonNoIcon);
Ui::AddSkip(container);
Ui::AddDividerText(container, tr::lng_suggest_options_date_about());
duration->setClickedCallback([=] {
box->uiShow()->show(Box([=](not_null<Ui::GenericBox*> box) {
const auto save = [=](int index) {
state->offerDuration = durations[index];
};
auto options = durations
| ranges::views::transform(durationToText)
| ranges::to_vector;
const auto selected = ranges::find(
durations,
state->offerDuration.current());
SingleChoiceBox(box, {
.title = tr::lng_gift_offer_duration(),
.options = options,
.initialSelection = int(selected - begin(durations)),
.callback = save,
});
}));
});
Ui::AddSkip(container);
Ui::AddDividerText(
container,
tr::lng_gift_offer_duration_about(
lt_user,
rpl::single(peer->shortName())));
} else {
const auto time = Settings::AddButtonWithLabel(
container,
tr::lng_suggest_options_date(),
state->date.value() | rpl::map([](TimeId date) {
return date
? langDateTime(base::unixtime::parse(date))
: tr::lng_suggest_options_date_any(tr::now);
}),
st::settingsButtonNoIcon);
time->setClickedCallback([=] {
const auto weak = std::make_shared<
base::weak_qptr<Ui::BoxContent>
>();
const auto parentWeak = base::make_weak(box);
const auto done = [=](TimeId result) {
if (parentWeak) {
state->date = result;
}
if (const auto strong = weak->get()) {
strong->closeBox();
}
};
auto dateBox = Box(ChooseSuggestTimeBox, SuggestTimeBoxArgs{
.session = session,
.done = done,
.value = state->date.current(),
.mode = args.mode,
});
*weak = dateBox.data();
box->uiShow()->show(std::move(dateBox));
});
Ui::AddSkip(container);
Ui::AddDividerText(container, tr::lng_suggest_options_date_about());
}
state->save = [=] {
const auto ton = uint32(state->ton.current() ? 1 : 0);
@@ -645,14 +727,15 @@ void ChooseSuggestPriceBox(
box->uiShow()->show(Box(InsufficientTonBox, usePeer, value));
return;
}
} else if (!admin) {
}
const auto requiredStars = peer->starsPerMessageChecked()
+ (ton ? 0 : int(base::SafeRound(value.value())));
if (!admin && requiredStars) {
if (!credits->loaded()) {
state->savePending = true;
return;
}
const auto required = peer->starsPerMessageChecked()
+ int(base::SafeRound(value.value()));
if (credits->balance() < CreditsAmount(required)) {
if (credits->balance() < CreditsAmount(requiredStars)) {
using namespace Settings;
const auto done = [=](SmallBalanceResult result) {
if (result == SmallBalanceResult::Success
@@ -660,10 +743,13 @@ void ChooseSuggestPriceBox(
state->save();
}
};
const auto source = gift
? Settings::SmallBalanceSource(SmallBalanceForOffer())
: SmallBalanceForSuggest{ usePeer->id };
MaybeRequestBalanceIncrease(
Main::MakeSessionShow(box->uiShow(), session),
required,
SmallBalanceForSuggest{ usePeer->id },
requiredStars,
source,
done);
return;
}
@@ -674,6 +760,7 @@ void ChooseSuggestPriceBox(
.priceNano = uint32(value.nano()),
.ton = ton,
.date = state->date.current(),
.offerDuration = state->offerDuration.current(),
});
};
@@ -777,7 +864,7 @@ bool CanAddOfferToMessage(not_null<HistoryItem*> item) {
const auto broadcast = history->peer->monoforumBroadcast();
return broadcast
&& !history->amMonoforumAdmin()
&& !item->Get<HistoryMessageSuggestedPost>()
&& !item->Get<HistoryMessageSuggestion>()
&& !item->groupId()
&& item->isRegular()
&& !item->isService()
@@ -862,10 +949,10 @@ void InsufficientTonBox(
}, button->lifetime());
}
SuggestOptions::SuggestOptions(
SuggestOptionsBar::SuggestOptionsBar(
std::shared_ptr<ChatHelpers::Show> show,
not_null<PeerData*> peer,
SuggestPostOptions values,
SuggestOptions values,
SuggestMode mode)
: _show(std::move(show))
, _peer(peer)
@@ -874,21 +961,29 @@ SuggestOptions::SuggestOptions(
updateTexts();
}
SuggestOptions::~SuggestOptions() = default;
SuggestOptionsBar::~SuggestOptionsBar() = default;
void SuggestOptions::paintIcon(QPainter &p, int x, int y, int outerWidth) {
void SuggestOptionsBar::paintIcon(
QPainter &p,
int x,
int y,
int outerWidth) {
st::historySuggestIconActive.paint(
p,
QPoint(x, y) + st::historySuggestIconPosition,
outerWidth);
}
void SuggestOptions::paintBar(QPainter &p, int x, int y, int outerWidth) {
void SuggestOptionsBar::paintBar(QPainter &p, int x, int y, int outerWidth) {
paintIcon(p, x, y, outerWidth);
paintLines(p, x + st::historyReplySkip, y, outerWidth);
}
void SuggestOptions::paintLines(QPainter &p, int x, int y, int outerWidth) {
void SuggestOptionsBar::paintLines(
QPainter &p,
int x,
int y,
int outerWidth) {
auto available = outerWidth
- x
- st::historyReplyCancel.width
@@ -907,9 +1002,9 @@ void SuggestOptions::paintLines(QPainter &p, int x, int y, int outerWidth) {
});
}
void SuggestOptions::edit() {
void SuggestOptionsBar::edit() {
const auto weak = std::make_shared<base::weak_qptr<Ui::BoxContent>>();
const auto apply = [=](SuggestPostOptions values) {
const auto apply = [=](SuggestOptions values) {
_values = values;
updateTexts();
_updates.fire({});
@@ -925,7 +1020,7 @@ void SuggestOptions::edit() {
}));
}
void SuggestOptions::updateTexts() {
void SuggestOptionsBar::updateTexts() {
_title.setText(
st::semiboldTextStyle,
((_mode == SuggestMode::New)
@@ -938,7 +1033,7 @@ void SuggestOptions::updateTexts() {
Core::TextContext({ .session = &_peer->session() }));
}
TextWithEntities SuggestOptions::composeText() const {
TextWithEntities SuggestOptionsBar::composeText() const {
auto helper = Ui::Text::CustomEmojiHelper();
const auto amount = _values.price().ton()
? helper.paletteDependent(Ui::Earn::IconCurrencyEmoji({
@@ -971,17 +1066,17 @@ TextWithEntities SuggestOptions::composeText() const {
).append(date);
}
SuggestPostOptions SuggestOptions::values() const {
SuggestOptions SuggestOptionsBar::values() const {
auto result = _values;
result.exists = 1;
return result;
}
rpl::producer<> SuggestOptions::updates() const {
rpl::producer<> SuggestOptionsBar::updates() const {
return _updates.events();
}
rpl::lifetime &SuggestOptions::lifetime() {
rpl::lifetime &SuggestOptionsBar::lifetime() {
return _lifetime;
}

View File

@@ -34,6 +34,7 @@ enum class SuggestMode {
New,
Change,
Publish,
Gift,
};
struct SuggestTimeBoxArgs {
@@ -88,9 +89,10 @@ struct StarsTonPriceArgs {
struct SuggestPriceBoxArgs {
not_null<PeerData*> peer;
bool updating = false;
Fn<void(SuggestPostOptions)> done;
SuggestPostOptions value;
Fn<void(SuggestOptions)> done;
SuggestOptions value;
SuggestMode mode = SuggestMode::New;
QString giftName;
};
void ChooseSuggestPriceBox(
not_null<Ui::GenericBox*> box,
@@ -112,14 +114,14 @@ void InsufficientTonBox(
not_null<PeerData*> peer,
CreditsAmount required);
class SuggestOptions final {
class SuggestOptionsBar final {
public:
SuggestOptions(
SuggestOptionsBar(
std::shared_ptr<ChatHelpers::Show> show,
not_null<PeerData*> peer,
SuggestPostOptions values,
SuggestOptions values,
SuggestMode mode);
~SuggestOptions();
~SuggestOptionsBar();
void paintBar(QPainter &p, int x, int y, int outerWidth);
void edit();
@@ -127,7 +129,7 @@ public:
void paintIcon(QPainter &p, int x, int y, int outerWidth);
void paintLines(QPainter &p, int x, int y, int outerWidth);
[[nodiscard]] SuggestPostOptions values() const;
[[nodiscard]] SuggestOptions values() const;
[[nodiscard]] rpl::producer<> updates() const;
@@ -145,7 +147,7 @@ private:
Ui::Text::String _title;
Ui::Text::String _text;
SuggestPostOptions _values;
SuggestOptions _values;
rpl::event_stream<> _updates;
rpl::lifetime _lifetime;

View File

@@ -657,10 +657,10 @@ BottomInfo::Data BottomInfoDataFromMessage(not_null<Message*> message) {
result.scheduleRepeatPeriod = item->scheduleRepeatPeriod();
}
if (forwarded
&& ((forwarded->savedFromPeer && forwarded->savedFromMsgId)
|| forwarded->savedFromHiddenSenderInfo
|| forwarded->originalHiddenSenderInfo)
&& !item->externalReply()) {
&& forwarded->originalDate
&& (message->context() == Context::SavedSublist
|| item->history()->peer->isSelf())
&& !item->externalReply()) {
result.date = base::unixtime::parse(forwarded->originalDate);
result.flags |= Flag::ForwardedDate;
}

View File

@@ -1858,7 +1858,7 @@ void ChatWidget::refreshTopBarActiveChat() {
? EntryState::Section::SavedSublist
: EntryState::Section::Replies,
.currentReplyTo = replyTo(),
.currentSuggest = SuggestPostOptions(),
.currentSuggest = SuggestOptions(),
};
_topBar->setActiveChat(state, _sendAction.get());
_composeControls->setCurrentDialogsEntryState(state);

View File

@@ -35,6 +35,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "chat_helpers/stickers_emoji_pack.h"
#include "payments/payments_reaction_process.h" // TryAddingPaidReaction.
#include "window/window_session_controller.h"
#include "ui/effects/glare.h"
#include "ui/effects/path_shift_gradient.h"
#include "ui/effects/reaction_fly_animation.h"
#include "ui/toast/toast.h"
@@ -42,6 +43,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/item_text_options.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/round_rect.h"
#include "data/components/sponsored_messages.h"
#include "data/data_channel.h"
#include "data/data_saved_sublist.h"
@@ -53,6 +55,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_user.h"
#include "lang/lang_keys.h"
#include "styles/style_chat.h"
#include "styles/style_dialogs.h"
namespace HistoryView {
namespace {
@@ -66,6 +69,287 @@ Element *HoveredLinkElement/* = nullptr*/;
Element *PressedLinkElement/* = nullptr*/;
Element *MousedElement/* = nullptr*/;
class KeyboardStyle : public ReplyKeyboard::Style {
public:
KeyboardStyle(const style::BotKeyboardButton &st, Fn<void()> repaint);
Images::CornersMaskRef buttonRounding(
Ui::BubbleRounding outer,
RectParts sides) const override;
void startPaint(
QPainter &p,
const Ui::ChatStyle *st) const override;
const style::TextStyle &textStyle() const override;
void repaint(not_null<const HistoryItem*> item) const override;
protected:
void paintButtonBg(
QPainter &p,
const Ui::ChatStyle *st,
const QRect &rect,
Ui::BubbleRounding rounding,
float64 howMuchOver) const override;
void paintButtonIcon(
QPainter &p,
const Ui::ChatStyle *st,
const QRect &rect,
int outerWidth,
HistoryMessageMarkupButton::Type type) const override;
void paintButtonLoading(
QPainter &p,
const Ui::ChatStyle *st,
const QRect &rect,
int outerWidth,
Ui::BubbleRounding rounding) const override;
int minButtonWidth(HistoryMessageMarkupButton::Type type) const override;
private:
struct CachedBg {
QImage image;
QColor color;
};
using BubbleRoundingKey = uchar;
mutable base::flat_map<BubbleRoundingKey, CachedBg> _cachedBg;
mutable base::flat_map<BubbleRoundingKey, QPainterPath> _cachedOutline;
mutable std::unique_ptr<Ui::GlareEffect> _glare;
Fn<void()> _repaint;
};
KeyboardStyle::KeyboardStyle(
const style::BotKeyboardButton &st,
Fn<void()> repaint)
: ReplyKeyboard::Style(st)
, _repaint(std::move(repaint)) {
}
void KeyboardStyle::startPaint(
QPainter &p,
const Ui::ChatStyle *st) const {
Expects(st != nullptr);
p.setPen(st->msgServiceFg());
}
const style::TextStyle &KeyboardStyle::textStyle() const {
return st::serviceTextStyle;
}
void KeyboardStyle::repaint(not_null<const HistoryItem*> item) const {
item->history()->owner().requestItemRepaint(item);
}
Images::CornersMaskRef KeyboardStyle::buttonRounding(
Ui::BubbleRounding outer,
RectParts sides) const {
using namespace Images;
using namespace Ui;
using Radius = CachedCornerRadius;
using Corner = BubbleCornerRounding;
auto result = CornersMaskRef(CachedCornersMasks(Radius::BubbleSmall));
if (sides & RectPart::Bottom) {
const auto &large = CachedCornersMasks(Radius::BubbleLarge);
auto round = [&](RectPart side, int index) {
if ((sides & side) && (outer[index] == Corner::Large)) {
result.p[index] = &large[index];
}
};
round(RectPart::Left, kBottomLeft);
round(RectPart::Right, kBottomRight);
}
return result;
}
void KeyboardStyle::paintButtonBg(
QPainter &p,
const Ui::ChatStyle *st,
const QRect &rect,
Ui::BubbleRounding rounding,
float64 howMuchOver) const {
Expects(st != nullptr);
using Corner = Ui::BubbleCornerRounding;
auto &cachedBg = _cachedBg[rounding.key()];
const auto sti = &st->imageStyle(false);
const auto ratio = style::DevicePixelRatio();
if (cachedBg.image.isNull()
|| cachedBg.image.width() != (rect.width() * ratio)
|| cachedBg.color != sti->msgServiceBg->c) {
cachedBg.color = sti->msgServiceBg->c;
cachedBg.image = QImage(
rect.size() * ratio,
QImage::Format_ARGB32_Premultiplied);
cachedBg.image.setDevicePixelRatio(ratio);
cachedBg.image.fill(Qt::transparent);
{
auto painter = QPainter(&cachedBg.image);
const auto &small = sti->msgServiceBgCornersSmall;
const auto &large = sti->msgServiceBgCornersLarge;
auto corners = Ui::CornersPixmaps();
int radiuses[4];
for (auto i = 0; i != 4; ++i) {
const auto isLarge = (rounding[i] == Corner::Large);
corners.p[i] = (isLarge ? large : small).p[i];
radiuses[i] = Ui::CachedCornerRadiusValue(isLarge
? Ui::CachedCornerRadius::BubbleLarge
: Ui::CachedCornerRadius::BubbleSmall);
}
const auto r = Rect(rect.size());
_cachedOutline[rounding.key()] = Ui::ComplexRoundedRectPath(
r - Margins(st::lineWidth),
radiuses[0],
radiuses[1],
radiuses[2],
radiuses[3]);
Ui::FillRoundRect(painter, r, sti->msgServiceBg, corners);
}
}
p.drawImage(rect.topLeft(), cachedBg.image);
if (howMuchOver > 0) {
auto o = p.opacity();
p.setOpacity(o * howMuchOver);
const auto &small = st->msgBotKbOverBgAddCornersSmall();
const auto &large = st->msgBotKbOverBgAddCornersLarge();
auto over = Ui::CornersPixmaps();
for (auto i = 0; i != 4; ++i) {
over.p[i] = (rounding[i] == Corner::Large ? large : small).p[i];
}
Ui::FillRoundRect(p, rect, st->msgBotKbOverBgAdd(), over);
p.setOpacity(o);
}
}
void KeyboardStyle::paintButtonIcon(
QPainter &p,
const Ui::ChatStyle *st,
const QRect &rect,
int outerWidth,
HistoryMessageMarkupButton::Type type) const {
Expects(st != nullptr);
using Type = HistoryMessageMarkupButton::Type;
const auto icon = [&]() -> const style::icon* {
switch (type) {
case Type::Url:
case Type::Auth: return &st->msgBotKbUrlIcon();
case Type::Buy: return &st->msgBotKbPaymentIcon();
case Type::SwitchInlineSame:
case Type::SwitchInline: return &st->msgBotKbSwitchPmIcon();
case Type::WebView:
case Type::SimpleWebView: return &st->msgBotKbWebviewIcon();
case Type::CopyText: return &st->msgBotKbCopyIcon();
}
return nullptr;
}();
if (icon) {
icon->paint(p, rect.x() + rect.width() - icon->width() - st::msgBotKbIconPadding, rect.y() + st::msgBotKbIconPadding, outerWidth);
}
}
void KeyboardStyle::paintButtonLoading(
QPainter &p,
const Ui::ChatStyle *st,
const QRect &rect,
int outerWidth,
Ui::BubbleRounding rounding) const {
Expects(st != nullptr);
if (anim::Disabled()) {
const auto &icon = st->historySendingInvertedIcon();
icon.paint(
p,
rect::right(rect) - icon.width() - st::msgBotKbIconPadding,
rect::bottom(rect) - icon.height() - st::msgBotKbIconPadding,
rect.x() * 2 + rect.width());
return;
}
const auto cacheKey = rounding.key();
auto &cachedBg = _cachedBg[cacheKey];
if (!cachedBg.image.isNull()) {
if (_glare && _glare->glare.birthTime) {
const auto progress = _glare->progress(crl::now());
const auto w = _glare->width;
const auto h = rect.height();
const auto x = (-w) + (w * 2) * progress;
auto frame = cachedBg.image;
frame.fill(Qt::transparent);
{
auto painter = QPainter(&frame);
auto hq = PainterHighQualityEnabler(painter);
painter.setPen(Qt::NoPen);
painter.drawTiledPixmap(x, 0, w, h, _glare->pixmap, 0, 0);
auto path = QPainterPath();
path.addRect(Rect(rect.size()));
path -= _cachedOutline[cacheKey];
constexpr auto kBgOutlineAlpha = 0.5;
constexpr auto kFgOutlineAlpha = 0.8;
const auto &c = st::premiumButtonFg->c;
painter.setPen(Qt::NoPen);
painter.setBrush(c);
painter.setOpacity(kBgOutlineAlpha);
painter.drawPath(path);
auto gradient = QLinearGradient(-w, 0, w * 2, 0);
{
constexpr auto kShiftLeft = 0.01;
constexpr auto kShiftRight = 0.99;
auto stops = _glare->computeGradient(c).stops();
stops[1] = {
std::clamp(progress, kShiftLeft, kShiftRight),
QColor(c.red(), c.green(), c.blue(), kFgOutlineAlpha),
};
gradient.setStops(std::move(stops));
}
painter.setBrush(QBrush(gradient));
painter.setOpacity(1);
painter.drawPath(path);
painter.setCompositionMode(
QPainter::CompositionMode_DestinationIn);
painter.drawImage(0, 0, cachedBg.image);
}
p.drawImage(rect.x(), rect.y(), frame);
} else {
_glare = std::make_unique<Ui::GlareEffect>();
_glare->width = outerWidth;
constexpr auto kTimeout = crl::time(0);
constexpr auto kDuration = crl::time(1100);
const auto color = st::premiumButtonFg->c;
_glare->validate(color, _repaint, kTimeout, kDuration);
}
}
}
int KeyboardStyle::minButtonWidth(
HistoryMessageMarkupButton::Type type) const {
using Type = HistoryMessageMarkupButton::Type;
int result = 2 * buttonPadding(), iconWidth = 0;
switch (type) {
case Type::Url:
case Type::Auth: iconWidth = st::msgBotKbUrlIcon.width(); break;
case Type::Buy: iconWidth = st::msgBotKbPaymentIcon.width(); break;
case Type::SwitchInlineSame:
case Type::SwitchInline: iconWidth = st::msgBotKbSwitchPmIcon.width(); break;
case Type::Callback:
case Type::CallbackWithPassword:
case Type::Game: iconWidth = st::historySendingInvertedIcon.width(); break;
case Type::WebView:
case Type::SimpleWebView: iconWidth = st::msgBotKbWebviewIcon.width(); break;
case Type::CopyText: return st::msgBotKbCopyIcon.width(); break;
}
if (iconWidth > 0) {
result = std::max(result, 2 * iconWidth + 4 * int(st::msgBotKbIconPadding));
}
return result;
}
[[nodiscard]] bool IsAttachedToPreviousInSavedMessages(
not_null<HistoryItem*> previous,
HistoryMessageForwarded *prevForwarded,
@@ -1463,6 +1747,24 @@ void Element::validateTextSkipBlock(bool has, int width, int height) {
}
}
void Element::validateInlineKeyboard(HistoryMessageReplyMarkup *markup) {
if (!markup
|| markup->inlineKeyboard
|| markup->hiddenBy(data()->media())) {
return;
}
const auto item = data();
//if (item->hideLinks()) {
// item->setHasHiddenLinks(true);
// return;
//}
markup->inlineKeyboard = std::make_unique<ReplyKeyboard>(
item,
std::make_unique<KeyboardStyle>(
st::msgBotKbButton,
[=] { item->history()->owner().requestItemRepaint(item); }));
}
void Element::previousInBlocksChanged() {
recountThreadBarInBlocks();
recountDisplayDateInBlocks();

View File

@@ -18,6 +18,7 @@ class HistoryBlock;
class HistoryItem;
struct HistoryMessageReply;
struct PreparedServiceText;
struct HistoryMessageReplyMarkup;
namespace Data {
class Thread;
@@ -693,6 +694,7 @@ protected:
[[nodiscard]] int textHeightFor(int textWidth);
void validateText();
void validateTextSkipBlock(bool has, int width, int height);
void validateInlineKeyboard(HistoryMessageReplyMarkup *markup);
void clearSpecialOnlyEmoji();
void checkSpecialOnlyEmoji();

View File

@@ -26,13 +26,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history.h"
#include "boxes/premium_preview_box.h"
#include "boxes/share_box.h"
#include "ui/effects/glare.h"
#include "ui/effects/reaction_fly_animation.h"
#include "ui/text/text_utilities.h"
#include "ui/text/text_extended_data.h"
#include "ui/power_saving.h"
#include "ui/rect.h"
#include "ui/round_rect.h"
//#include "ui/round_rect.h"
#include "data/components/factchecks.h"
#include "data/components/sponsored_messages.h"
#include "data/data_session.h"
@@ -59,286 +58,6 @@ namespace {
constexpr auto kPlayStatusLimit = 2;
const auto kPsaTooltipPrefix = "cloud_lng_tooltip_psa_";
class KeyboardStyle : public ReplyKeyboard::Style {
public:
KeyboardStyle(const style::BotKeyboardButton &st, Fn<void()> repaint);
Images::CornersMaskRef buttonRounding(
Ui::BubbleRounding outer,
RectParts sides) const override;
void startPaint(
QPainter &p,
const Ui::ChatStyle *st) const override;
const style::TextStyle &textStyle() const override;
void repaint(not_null<const HistoryItem*> item) const override;
protected:
void paintButtonBg(
QPainter &p,
const Ui::ChatStyle *st,
const QRect &rect,
Ui::BubbleRounding rounding,
float64 howMuchOver) const override;
void paintButtonIcon(
QPainter &p,
const Ui::ChatStyle *st,
const QRect &rect,
int outerWidth,
HistoryMessageMarkupButton::Type type) const override;
void paintButtonLoading(
QPainter &p,
const Ui::ChatStyle *st,
const QRect &rect,
int outerWidth,
Ui::BubbleRounding rounding) const override;
int minButtonWidth(HistoryMessageMarkupButton::Type type) const override;
private:
using BubbleRoundingKey = uchar;
mutable base::flat_map<BubbleRoundingKey, QImage> _cachedBg;
mutable base::flat_map<BubbleRoundingKey, QPainterPath> _cachedOutline;
mutable std::unique_ptr<Ui::GlareEffect> _glare;
Fn<void()> _repaint;
rpl::lifetime _lifetime;
};
KeyboardStyle::KeyboardStyle(
const style::BotKeyboardButton &st,
Fn<void()> repaint)
: ReplyKeyboard::Style(st)
, _repaint(std::move(repaint)) {
style::PaletteChanged(
) | rpl::start_with_next([=] {
_cachedBg = {};
_cachedOutline = {};
}, _lifetime);
}
void KeyboardStyle::startPaint(
QPainter &p,
const Ui::ChatStyle *st) const {
Expects(st != nullptr);
p.setPen(st->msgServiceFg());
}
const style::TextStyle &KeyboardStyle::textStyle() const {
return st::serviceTextStyle;
}
void KeyboardStyle::repaint(not_null<const HistoryItem*> item) const {
item->history()->owner().requestItemRepaint(item);
}
Images::CornersMaskRef KeyboardStyle::buttonRounding(
Ui::BubbleRounding outer,
RectParts sides) const {
using namespace Images;
using namespace Ui;
using Radius = CachedCornerRadius;
using Corner = BubbleCornerRounding;
auto result = CornersMaskRef(CachedCornersMasks(Radius::BubbleSmall));
if (sides & RectPart::Bottom) {
const auto &large = CachedCornersMasks(Radius::BubbleLarge);
auto round = [&](RectPart side, int index) {
if ((sides & side) && (outer[index] == Corner::Large)) {
result.p[index] = &large[index];
}
};
round(RectPart::Left, kBottomLeft);
round(RectPart::Right, kBottomRight);
}
return result;
}
void KeyboardStyle::paintButtonBg(
QPainter &p,
const Ui::ChatStyle *st,
const QRect &rect,
Ui::BubbleRounding rounding,
float64 howMuchOver) const {
Expects(st != nullptr);
using Corner = Ui::BubbleCornerRounding;
auto &cachedBg = _cachedBg[rounding.key()];
if (cachedBg.isNull()
|| cachedBg.width() != (rect.width() * style::DevicePixelRatio())) {
cachedBg = QImage(
rect.size() * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
cachedBg.setDevicePixelRatio(style::DevicePixelRatio());
cachedBg.fill(Qt::transparent);
{
auto painter = QPainter(&cachedBg);
const auto sti = &st->imageStyle(false);
const auto &small = sti->msgServiceBgCornersSmall;
const auto &large = sti->msgServiceBgCornersLarge;
auto corners = Ui::CornersPixmaps();
int radiuses[4];
for (auto i = 0; i != 4; ++i) {
const auto isLarge = (rounding[i] == Corner::Large);
corners.p[i] = (isLarge ? large : small).p[i];
radiuses[i] = Ui::CachedCornerRadiusValue(isLarge
? Ui::CachedCornerRadius::BubbleLarge
: Ui::CachedCornerRadius::BubbleSmall);
}
const auto r = Rect(rect.size());
_cachedOutline[rounding.key()] = Ui::ComplexRoundedRectPath(
r - Margins(st::lineWidth),
radiuses[0],
radiuses[1],
radiuses[2],
radiuses[3]);
Ui::FillRoundRect(painter, r, sti->msgServiceBg, corners);
}
}
p.drawImage(rect.topLeft(), cachedBg);
if (howMuchOver > 0) {
auto o = p.opacity();
p.setOpacity(o * howMuchOver);
const auto &small = st->msgBotKbOverBgAddCornersSmall();
const auto &large = st->msgBotKbOverBgAddCornersLarge();
auto over = Ui::CornersPixmaps();
for (auto i = 0; i != 4; ++i) {
over.p[i] = (rounding[i] == Corner::Large ? large : small).p[i];
}
Ui::FillRoundRect(p, rect, st->msgBotKbOverBgAdd(), over);
p.setOpacity(o);
}
}
void KeyboardStyle::paintButtonIcon(
QPainter &p,
const Ui::ChatStyle *st,
const QRect &rect,
int outerWidth,
HistoryMessageMarkupButton::Type type) const {
Expects(st != nullptr);
using Type = HistoryMessageMarkupButton::Type;
const auto icon = [&]() -> const style::icon* {
switch (type) {
case Type::Url:
case Type::Auth: return &st->msgBotKbUrlIcon();
case Type::Buy: return &st->msgBotKbPaymentIcon();
case Type::SwitchInlineSame:
case Type::SwitchInline: return &st->msgBotKbSwitchPmIcon();
case Type::WebView:
case Type::SimpleWebView: return &st->msgBotKbWebviewIcon();
case Type::CopyText: return &st->msgBotKbCopyIcon();
}
return nullptr;
}();
if (icon) {
icon->paint(p, rect.x() + rect.width() - icon->width() - st::msgBotKbIconPadding, rect.y() + st::msgBotKbIconPadding, outerWidth);
}
}
void KeyboardStyle::paintButtonLoading(
QPainter &p,
const Ui::ChatStyle *st,
const QRect &rect,
int outerWidth,
Ui::BubbleRounding rounding) const {
Expects(st != nullptr);
if (anim::Disabled()) {
const auto &icon = st->historySendingInvertedIcon();
icon.paint(
p,
rect::right(rect) - icon.width() - st::msgBotKbIconPadding,
rect::bottom(rect) - icon.height() - st::msgBotKbIconPadding,
rect.x() * 2 + rect.width());
return;
}
const auto cacheKey = rounding.key();
auto &cachedBg = _cachedBg[cacheKey];
if (!cachedBg.isNull()) {
if (_glare && _glare->glare.birthTime) {
const auto progress = _glare->progress(crl::now());
const auto w = _glare->width;
const auto h = rect.height();
const auto x = (-w) + (w * 2) * progress;
auto frame = cachedBg;
frame.fill(Qt::transparent);
{
auto painter = QPainter(&frame);
auto hq = PainterHighQualityEnabler(painter);
painter.setPen(Qt::NoPen);
painter.drawTiledPixmap(x, 0, w, h, _glare->pixmap, 0, 0);
auto path = QPainterPath();
path.addRect(Rect(rect.size()));
path -= _cachedOutline[cacheKey];
constexpr auto kBgOutlineAlpha = 0.5;
constexpr auto kFgOutlineAlpha = 0.8;
const auto &c = st::premiumButtonFg->c;
painter.setPen(Qt::NoPen);
painter.setBrush(c);
painter.setOpacity(kBgOutlineAlpha);
painter.drawPath(path);
auto gradient = QLinearGradient(-w, 0, w * 2, 0);
{
constexpr auto kShiftLeft = 0.01;
constexpr auto kShiftRight = 0.99;
auto stops = _glare->computeGradient(c).stops();
stops[1] = {
std::clamp(progress, kShiftLeft, kShiftRight),
QColor(c.red(), c.green(), c.blue(), kFgOutlineAlpha),
};
gradient.setStops(std::move(stops));
}
painter.setBrush(QBrush(gradient));
painter.setOpacity(1);
painter.drawPath(path);
painter.setCompositionMode(
QPainter::CompositionMode_DestinationIn);
painter.drawImage(0, 0, cachedBg);
}
p.drawImage(rect.x(), rect.y(), frame);
} else {
_glare = std::make_unique<Ui::GlareEffect>();
_glare->width = outerWidth;
constexpr auto kTimeout = crl::time(0);
constexpr auto kDuration = crl::time(1100);
const auto color = st::premiumButtonFg->c;
_glare->validate(color, _repaint, kTimeout, kDuration);
}
}
}
int KeyboardStyle::minButtonWidth(
HistoryMessageMarkupButton::Type type) const {
using Type = HistoryMessageMarkupButton::Type;
int result = 2 * buttonPadding(), iconWidth = 0;
switch (type) {
case Type::Url:
case Type::Auth: iconWidth = st::msgBotKbUrlIcon.width(); break;
case Type::Buy: iconWidth = st::msgBotKbPaymentIcon.width(); break;
case Type::SwitchInlineSame:
case Type::SwitchInline: iconWidth = st::msgBotKbSwitchPmIcon.width(); break;
case Type::Callback:
case Type::CallbackWithPassword:
case Type::Game: iconWidth = st::historySendingInvertedIcon.width(); break;
case Type::WebView:
case Type::SimpleWebView: iconWidth = st::msgBotKbWebviewIcon.width(); break;
case Type::CopyText: return st::msgBotKbCopyIcon.width(); break;
}
if (iconWidth > 0) {
result = std::max(result, 2 * iconWidth + 4 * int(st::msgBotKbIconPadding));
}
return result;
}
QString FastForwardText() {
return u"Forward"_q;
}
@@ -422,7 +141,7 @@ Message::Message(
, _bottomInfo(
&data->history()->owner().reactions(),
BottomInfoDataFromMessage(this)) {
if (data->Get<HistoryMessageSuggestedPost>()) {
if (data->Get<HistoryMessageSuggestion>()) {
_hideReply = 1;
} else if (const auto media = data->media()) {
if (media->giveawayResults()) {
@@ -460,7 +179,7 @@ Message::~Message() {
void Message::refreshSuggestedInfo(
not_null<HistoryItem*> item,
not_null<const HistoryMessageSuggestedPost*> suggest,
not_null<const HistoryMessageSuggestion*> suggest,
const HistoryMessageReply *replyData) {
const auto link = (replyData && replyData->resolvedMessage)
? JumpToMessageClickHandler(
@@ -481,7 +200,7 @@ void Message::refreshSuggestedInfo(
void Message::initPaidInformation() {
const auto item = data();
if (item->history()->peer->isMonoforum()) {
if (const auto suggest = item->Get<HistoryMessageSuggestedPost>()) {
if (const auto suggest = item->Get<HistoryMessageSuggestion>()) {
const auto replyData = item->Get<HistoryMessageReply>();
refreshSuggestedInfo(item, suggest, replyData);
}
@@ -753,7 +472,7 @@ QSize Message::performCountOptimalSize() {
}
if (item->history()->peer->isMonoforum()) {
if (const auto suggest = item->Get<HistoryMessageSuggestedPost>()) {
if (const auto suggest = item->Get<HistoryMessageSuggestion>()) {
if (const auto service = Get<ServicePreMessage>()) {
// Ok, we didn't have the message, but now we have.
// That means this is not a plain post suggestion,
@@ -3507,24 +3226,6 @@ bool Message::embedReactionsInBubble() const {
return needInfoDisplay();
}
void Message::validateInlineKeyboard(HistoryMessageReplyMarkup *markup) {
if (!markup
|| markup->inlineKeyboard
|| markup->hiddenBy(data()->media())) {
return;
}
const auto item = data();
//if (item->hideLinks()) {
// item->setHasHiddenLinks(true);
// return;
//}
markup->inlineKeyboard = std::make_unique<ReplyKeyboard>(
item,
std::make_unique<KeyboardStyle>(
st::msgBotKbButton,
[=] { item->history()->owner().requestItemRepaint(item); }));
}
void Message::validateFromNameText(PeerData *from) const {
if (!from) {
if (_fromNameStatus) {

View File

@@ -15,7 +15,7 @@ class HistoryItem;
struct HistoryMessageEdited;
struct HistoryMessageForwarded;
struct HistoryMessageReplyMarkup;
struct HistoryMessageSuggestedPost;
struct HistoryMessageSuggestion;
struct HistoryMessageReply;
namespace Data {
@@ -177,7 +177,7 @@ private:
void initPaidInformation();
void refreshSuggestedInfo(
not_null<HistoryItem*> item,
not_null<const HistoryMessageSuggestedPost*> suggest,
not_null<const HistoryMessageSuggestion*> suggest,
const HistoryMessageReply *reply);
void initLogEntryOriginal();
void initPsa();
@@ -292,7 +292,6 @@ private:
void refreshInfoSkipBlock(HistoryItem *textItem);
[[nodiscard]] int monospaceMaxWidth() const;
void validateInlineKeyboard(HistoryMessageReplyMarkup *markup);
void updateViewButtonExistence();
[[nodiscard]] int viewButtonHeight() const;

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