Compare commits
82 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c4f663941 | ||
|
|
94f9321db9 | ||
|
|
ae70b10cea | ||
|
|
4f685552e7 | ||
|
|
e085a76165 | ||
|
|
30bd3ed013 | ||
|
|
25edab4c94 | ||
|
|
3aa241d825 | ||
|
|
9b867af7fd | ||
|
|
0df3be8630 | ||
|
|
542326af8f | ||
|
|
ea5052e69e | ||
|
|
2dd96b2269 | ||
|
|
627152e2a9 | ||
|
|
f01c93ed58 | ||
|
|
6fe61ed58a | ||
|
|
43347f671c | ||
|
|
0b67fa65f2 | ||
|
|
65b3a36984 | ||
|
|
b08cf75f0b | ||
|
|
0cc21e5ca2 | ||
|
|
48f9a92cc3 | ||
|
|
939882ef68 | ||
|
|
52084cf0ae | ||
|
|
f06f654191 | ||
|
|
356d20542e | ||
|
|
31ea4cfe80 | ||
|
|
1e89ee4e50 | ||
|
|
6fccbf036c | ||
|
|
41d206e354 | ||
|
|
23880ac6c1 | ||
|
|
4439cbf553 | ||
|
|
f506f1b830 | ||
|
|
feb1ea6502 | ||
|
|
fea80b4919 | ||
|
|
373bb8d74c | ||
|
|
3ddefd78ba | ||
|
|
d62e4da163 | ||
|
|
8c60863e11 | ||
|
|
d5be8c8989 | ||
|
|
255b30e88a | ||
|
|
d2dd124be0 | ||
|
|
1053b30a6d | ||
|
|
e7c1073e13 | ||
|
|
0480c6a4af | ||
|
|
c70a49c0f3 | ||
|
|
7840fd481a | ||
|
|
2a8b491c95 | ||
|
|
39c4344047 | ||
|
|
cdb58e4ebd | ||
|
|
972325fb6d | ||
|
|
933b6bedc9 | ||
|
|
2db8a5d00a | ||
|
|
be043ea349 | ||
|
|
e531abf31b | ||
|
|
a6af680e59 | ||
|
|
8292334c9b | ||
|
|
f20c5a1d3c | ||
|
|
d1e2ec0309 | ||
|
|
fe91cae8bc | ||
|
|
7bb30bc4a8 | ||
|
|
2d41d5903b | ||
|
|
df672ffaf5 | ||
|
|
cb100623fb | ||
|
|
30ef7270b3 | ||
|
|
91694a69eb | ||
|
|
cb07bcf0db | ||
|
|
a31e384409 | ||
|
|
4829c6d028 | ||
|
|
ccf6a3fb97 | ||
|
|
2005814fca | ||
|
|
7319665bda | ||
|
|
d74074a21b | ||
|
|
113115f58c | ||
|
|
e84799283d | ||
|
|
71272ed2ec | ||
|
|
6787c338ac | ||
|
|
774a44ac7e | ||
|
|
29cdd358cc | ||
|
|
b9b1bd5e58 | ||
|
|
59814aaeb0 | ||
|
|
fd52b3c23b |
13
.github/workflows/mac.yml
vendored
@@ -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'
|
||||
|
||||
13
.github/workflows/snap.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
BIN
Telegram/Resources/animations/passkeys.tgs
Normal file
BIN
Telegram/Resources/icons/mediaview/recognize.png
Normal file
|
After Width: | Height: | Size: 428 B |
BIN
Telegram/Resources/icons/mediaview/recognize@2x.png
Normal file
|
After Width: | Height: | Size: 677 B |
BIN
Telegram/Resources/icons/mediaview/recognize@3x.png
Normal file
|
After Width: | Height: | Size: 1019 B |
BIN
Telegram/Resources/icons/menu/2sv_off.png
Normal file
|
After Width: | Height: | Size: 840 B |
BIN
Telegram/Resources/icons/menu/2sv_off@2x.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
Telegram/Resources/icons/menu/2sv_off@3x.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
Telegram/Resources/icons/menu/2sv_on.png
Normal file
|
After Width: | Height: | Size: 876 B |
BIN
Telegram/Resources/icons/menu/2sv_on@2x.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
Telegram/Resources/icons/menu/2sv_on@3x.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
@@ -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.";
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 &,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 },
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -246,7 +246,8 @@ ChatAdminRightsInfo EditAdminBox::defaultRights() const {
|
||||
| Flag::DeleteStories
|
||||
| Flag::InviteByLinkOrAdd
|
||||
| Flag::ManageCall
|
||||
| Flag::ManageDirect) };
|
||||
| Flag::ManageDirect
|
||||
| Flag::BanUsers) };
|
||||
}
|
||||
|
||||
void EditAdminBox::prepare() {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) },
|
||||
|
||||
@@ -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(); });
|
||||
|
||||
@@ -74,6 +74,7 @@ enum class PremiumFeature {
|
||||
FilterTags,
|
||||
TodoLists,
|
||||
PeerColors,
|
||||
Gifts,
|
||||
|
||||
// Business features.
|
||||
BusinessLocation,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 ¤t = 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)));
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {};
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
1420
Telegram/SourceFiles/boxes/star_gift_preview_box.cpp
Normal file
29
Telegram/SourceFiles/boxes/star_gift_preview_box.h
Normal 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
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -181,3 +181,5 @@ private:
|
||||
[[nodiscard]] CreditsAmount CreditsAmountFromTL(
|
||||
const MTPStarsAmount *amount);
|
||||
[[nodiscard]] MTPStarsAmount StarsAmountToTL(CreditsAmount amount);
|
||||
|
||||
[[nodiscard]] QString PrepareCreditsAmountText(CreditsAmount amount);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
186
Telegram/SourceFiles/data/components/passkeys.cpp
Normal 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
|
||||
79
Telegram/SourceFiles/data/components/passkeys.h
Normal 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
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
121
Telegram/SourceFiles/data/data_passkey_deserialize.cpp
Normal 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 ¶m : 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
|
||||
62
Telegram/SourceFiles/data/data_passkey_deserialize.h
Normal 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
|
||||
@@ -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; });
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 } },
|
||||
|
||||
@@ -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&,
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 &) {
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -151,7 +151,7 @@ struct SendPaymentDetails {
|
||||
|
||||
[[nodiscard]] bool SuggestPaymentDataReady(
|
||||
not_null<PeerData*> peer,
|
||||
SuggestPostOptions suggest);
|
||||
SuggestOptions suggest);
|
||||
|
||||
struct PaidConfirmStyles {
|
||||
const style::FlatLabel *label = nullptr;
|
||||
|
||||
@@ -346,7 +346,7 @@ HistoryMessageSuggestInfo::HistoryMessageSuggestInfo(
|
||||
}
|
||||
|
||||
HistoryMessageSuggestInfo::HistoryMessageSuggestInfo(
|
||||
SuggestPostOptions options) {
|
||||
SuggestOptions options) {
|
||||
if (!options.exists) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -1387,7 +1387,7 @@ void ShowReplyToChatBox(
|
||||
history->setLocalDraft(std::make_unique<Data::Draft>(
|
||||
textWithTags,
|
||||
reply,
|
||||
SuggestPostOptions(),
|
||||
SuggestOptions(),
|
||||
cursor,
|
||||
Data::WebPageDraft()));
|
||||
history->clearLocalEditDraft(topicRootId, monoforumPeerId);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||