Compare commits

..

136 commits
v5.11.1 ... dev

Author SHA1 Message Date
AlexeyZavar
b3552d8c2c fix: set deleted mark for service message 2025-03-15 00:10:52 +03:00
AlexeyZavar
206a15191f fix: repeated deleted text 2025-03-15 00:01:53 +03:00
Alexey
22de3980d1
chore: update bug report template 2025-03-14 22:19:10 +03:00
AlexeyZavar
9502120ee3 chore: update sqlite_orm 2025-03-13 13:26:43 +03:00
AlexeyZavar
7f1a3320dd chore: update sqlite3 2025-03-13 13:23:40 +03:00
AlexeyZavar
8a4fd7f182 fix: update submodule 2025-03-13 12:57:39 +03:00
AlexeyZavar
b0a4d15d64 Merge tag 'v5.12.3' into dev 2025-03-13 12:57:30 +03:00
John Preston
11d0f9db03 Try fixing crashes on Linux. 2025-03-10 13:32:36 +04:00
John Preston
f024ceecdd Version 5.12.3.
- Fix a couple more crashes.
- Fix gift disappearing on unpin.
- Fix country emoji for Fragment numbers.
2025-03-10 11:58:20 +04:00
John Preston
08c07a0785 Don't remove last gift on unpin if all loaded. 2025-03-10 11:57:20 +04:00
John Preston
b843f91b3c Support 'FT' "country" flag emoji. 2025-03-10 11:52:42 +04:00
23rd
4b2c5b3321 Removed chat filters strip from folders and forums in separated windows. 2025-03-10 11:20:33 +04:00
23rd
2b43f2682a Fixed open of locked chats filter with shortcuts to open near filters. 2025-03-10 11:20:28 +04:00
23rd
7da0124286 Moved out lambda to jump to near chats filters to static function. 2025-03-10 11:18:43 +04:00
23rd
feaeef6482 Fixed repaint of send button when it has slowmode countdown. 2025-03-10 11:11:10 +04:00
23rd
15bcfeec1d Added pause to animation of custom emoji in status info. 2025-03-10 11:11:10 +04:00
23rd
5fb002ab4c Added ability to specify time when export data. 2025-03-10 11:11:10 +04:00
23rd
f56ddbb1e0 Added recursive animation to lock hint while recording voice. 2025-03-10 11:11:10 +04:00
John Preston
6bd2a7c962 Fix possible crash in MediaGenericTextPart. 2025-03-10 11:00:49 +04:00
John Preston
9d591ae806 Fix infinite recursion in channel reactions edit. 2025-03-10 10:59:39 +04:00
Ilya Fedin
7d30e3913c Add lcms2 dependency for Qt6 on Windows 2025-03-10 10:58:10 +04:00
Kaiyang Wu
19d7dd7aa3 fix(integration_linux): include core_settings.h with Qt version <= 6.5.0
Signed-off-by: Kaiyang Wu <self@origincode.me>
2025-03-09 20:33:20 +04:00
John Preston
45444253fd Version 5.12.2.
- Fix some crashes.
2025-03-09 09:44:47 +04:00
John Preston
308ade6a7e Fix crash in some custom emoji cases. 2025-03-09 06:55:25 +04:00
AlexeyZavar
66d54ccd54 feat: open channel in discussion group 2025-03-09 02:59:45 +03:00
AlexeyZavar
ccf8d91f01 feat: burn ttl message & display expired status 2025-03-09 02:33:22 +03:00
AlexeyZavar
7c940a0480 fix: ttl messages hint 2025-03-09 01:28:07 +03:00
AlexeyZavar
277a2edb57 fix: read stories crashes 2025-03-09 00:53:32 +03:00
AlexeyZavar
bd742689e9 fix: weird crash 2025-03-09 00:40:42 +03:00
AlexeyZavar
9ca0c3c41d fix: more checks for disposal 2025-03-09 00:30:18 +03:00
AlexeyZavar
ce8650b4de fix: create a copy of selected TTL messages 2025-03-08 23:58:27 +03:00
AlexeyZavar
4bce9d440e fix: make it build 2025-03-08 23:37:48 +03:00
AlexeyZavar
adc691f516 Merge tag 'v5.12.1' into dev 2025-03-08 23:02:34 +03:00
John Preston
fc67a801e3 Version 5.12.1: Fix build with Xcode. 2025-03-08 07:09:15 +04:00
John Preston
6a3657ca87 Version 5.12.1.
- Fix a crash in some chat switchings.
- Fix crashes in empty repaint callbacks.
2025-03-08 07:06:37 +04:00
John Preston
0537c5f273 Fix crashes in empty repaint callbacks. 2025-03-08 07:05:45 +04:00
John Preston
cc4a5f30b6 Fix a crash in some chat switchings. 2025-03-07 23:56:17 +04:00
John Preston
b0d7c3e9b1 Revert "Re-enable ffmpeg optimizations on Linux"
This reverts commit bd28ac6e1f.

It fails to link in Release mode with LTO.
2025-03-07 22:20:18 +04:00
John Preston
3bf7c44fc9 Version 5.12.
- Set a fee for incoming messages from unknown users.
- Set a fee for messages in groups or channel comments.
- Show some information about who's messaging you.
- Pin gifts on your profile.
2025-03-07 20:50:29 +04:00
John Preston
4ff4e63a11 Fix sending paid intro sticker. 2025-03-07 19:03:41 +04:00
John Preston
72a35ba58b Show new-peer-info photo/name change. 2025-03-07 19:03:41 +04:00
John Preston
b6a31979f2 Add the same enter/leave check to ListWidget. 2025-03-07 19:03:40 +04:00
John Preston
7c710e22cc Always process mouse input in active window.
Fixes #29008.
2025-03-07 19:03:40 +04:00
John Preston
ab58e7a225 Show common groups userpics in new-peer-info. 2025-03-07 19:03:40 +04:00
John Preston
c9fb97cd7c Simplify marked text context logic. 2025-03-07 19:03:40 +04:00
John Preston
789f3e1584 Add non-official account info icon. 2025-03-07 19:03:39 +04:00
John Preston
0fc8229be1 Initial new peer information display. 2025-03-07 19:03:39 +04:00
John Preston
a1e555267e Fix sending invite links to paid. 2025-03-07 19:03:39 +04:00
John Preston
0ac88c0cb5 Show special blank chat for paid messages. 2025-03-07 19:03:39 +04:00
John Preston
d43a6da62b Fix sending bot commands in paid chats. 2025-03-07 19:03:39 +04:00
John Preston
940455f786 Preload gifts for the gift to user layer. 2025-03-07 19:03:39 +04:00
John Preston
0f74456f30 Support gifts pinning. 2025-03-07 19:03:39 +04:00
John Preston
7840fa6d90 Add context menu for gifts in list. 2025-03-07 19:03:39 +04:00
John Preston
95ccc99fee Hide reply in notification for paid peers. 2025-03-07 19:03:39 +04:00
23rd
7b0a156bba Added lottie icon when have no enough info for earn stats. 2025-03-07 19:03:39 +04:00
23rd
0d8ae7bb37 Renamed creditsAmount in invoice for premium gift with represented name. 2025-03-07 19:03:39 +04:00
23rd
9491cff1df Fixed display credits in list of gift options. 2025-03-07 19:03:39 +04:00
23rd
51dc5d6e37 Fixed discount calculation for gifts options with different currencies. 2025-03-07 19:03:39 +04:00
John Preston
f4c739ab92 Improve transactions history for new stuff. 2025-03-07 19:03:39 +04:00
John Preston
0dd8ae3d77 Update submodules. 2025-03-07 19:03:39 +04:00
John Preston
7d2878d81c Support gifting premium for stars. 2025-03-07 19:03:39 +04:00
23rd
bd70a05861 Removed button to turn off sponsored messages in megagroups. 2025-03-07 19:03:39 +04:00
23rd
0605c7b2bc Added ability to display possible currency earn in megagroups in future. 2025-03-07 19:03:39 +04:00
23rd
8e83a55143 Added ability to request earn stats without currency earn in megagroups. 2025-03-07 19:03:39 +04:00
John Preston
4ab4eb8ef2 Pause voice in pay-to-send chats. 2025-03-07 19:03:39 +04:00
John Preston
d1e6150874 Don't suggest userpics to paid-restricted. 2025-03-07 19:03:38 +04:00
John Preston
4121c99f36 Allow folders submenu to have a scroll. 2025-03-07 19:03:38 +04:00
John Preston
827040f487 Use paid messages values from appConfig. 2025-03-07 19:03:38 +04:00
John Preston
9032489786 Add pays-me status bar in chat. 2025-03-07 19:03:38 +04:00
John Preston
8ea7bd4913 Simplify paid message button labeling. 2025-03-07 19:03:38 +04:00
John Preston
97b021efaf Star-count button in multi-Forward/CreatePoll. 2025-03-07 19:03:38 +04:00
John Preston
b3f9a77ba7 Star-count button in SendFilesBox/ShareBox. 2025-03-07 19:03:38 +04:00
John Preston
63fdc1f876 Update API scheme on layer 200. 2025-03-07 19:03:38 +04:00
John Preston
17cf354c58 Support custom send button for paid. 2025-03-07 19:03:38 +04:00
John Preston
c6fd8bcb99 Edit paid messages exceptions. 2025-03-07 19:03:38 +04:00
John Preston
1684465e04 Add sending paid stories replies. 2025-03-07 19:03:38 +04:00
John Preston
fe2df96953 Improve paid peer-box multi-send. 2025-03-07 19:03:38 +04:00
John Preston
ee9d0cfd99 Update API scheme, disable scheduled paid. 2025-03-07 19:03:38 +04:00
John Preston
7b7e18e752 Support paid sending in ShareBox. 2025-03-07 19:03:38 +04:00
John Preston
928be4151b Update API scheme, parse premium gifts for stars. 2025-03-07 19:03:38 +04:00
John Preston
37dd648686 Implement paid location sending. 2025-03-07 19:03:38 +04:00
John Preston
93a590774e Send paid stickers/gifs/inline-results. 2025-03-07 19:03:38 +04:00
John Preston
22b99b6d3e Send paid shared contacts. 2025-03-07 19:03:37 +04:00
John Preston
101d626d4f Support paid files-with-comment and polls. 2025-03-07 19:03:37 +04:00
John Preston
3633c19208 Update API scheme, improve service messages. 2025-03-07 19:03:37 +04:00
John Preston
e302f328f7 Don't cut confirming emoji status. 2025-03-07 19:03:37 +04:00
John Preston
f74ba95e95 Don't track caption in fullscreen video view.
Fixes #28981.
2025-03-07 19:03:37 +04:00
John Preston
c38982d286 Simplify paid stars check. 2025-03-07 19:03:37 +04:00
John Preston
fe9bac096b Refresh balance after paid message sending. 2025-03-07 19:03:37 +04:00
John Preston
5b809c4fc6 Show paid stars information above a message. 2025-03-07 19:03:37 +04:00
John Preston
4729e51e14 Improve phrases for paid messages. 2025-03-07 19:03:37 +04:00
John Preston
960cf7a34b Update API scheme, new paid. 2025-03-07 19:03:37 +04:00
John Preston
852ab19760 Update API scheme, track stars-per-message. 2025-03-07 19:03:37 +04:00
John Preston
2e45d9fc6b Allow replacing default shortcuts. 2025-03-07 19:03:37 +04:00
John Preston
74b71b92b6 Update API scheme on layer 200. 2025-03-07 19:03:37 +04:00
John Preston
bbc14ba74f Allow sending paid messages. 2025-03-07 19:03:37 +04:00
John Preston
45c7829cd8 Track stars-per-message for users and channels. 2025-03-07 19:03:37 +04:00
John Preston
f2aa3afbbb Allow editing charge-for-message privacy. 2025-03-07 19:03:37 +04:00
John Preston
909b01241b Update API scheme to layer 200. 2025-03-07 19:03:36 +04:00
Ilya Fedin
abb58c58a0 QPlatformKeyMapper -> QKeyMapper 2025-03-06 17:54:54 +04:00
Ilya Fedin
700e10d32c Use flat_map::remove in clearFrom{Topic,Item}
Now that notification closing happens in destructor, the iterator is no more needed
2025-03-06 17:30:26 +04:00
Ilya Fedin
4ac48d0e4a Fix clearFromTopic on Linux
Looks like it was broken since addition
2025-03-06 16:41:34 +04:00
Ilya Fedin
345b2cb835 Move notification closing to NotificationData destruction 2025-03-04 14:17:43 +04:00
Ilya Fedin
b962309498 Move hints.lookup_value() out of xdg_notifications_notifications_call_notify
Or it gets executed after hints.end() which clears hints
2025-03-04 12:28:37 +04:00
Eugene
e99cb9bfb8 Ensure policy check before creating Zone.Identifier for downloaded files on Windows 2025-03-04 10:47:31 +04:00
Ilya Fedin
9e12e18f90 Clean up unnecessary calls to Manager::Private::clearNotification
It was added to replicate NotificationData::close but lots of places call it after the notification is already cleared
2025-03-03 18:40:22 +04:00
Ilya Fedin
66fc9b38df Fix a race condition with asynchronous notification sending 2025-03-03 18:40:06 +04:00
Ilya Fedin
5dbe429e6b Fix NotificationData initialization 2025-03-03 18:39:55 +04:00
GitHub Action
b2481ea6c1 Update User-Agent for DNS to Chrome 133.0.0.0. 2025-03-03 18:37:13 +04:00
Ilya Fedin
86a294ce4b Subscribe to XdgNotifications signals on Manager initialization 2025-03-01 09:08:10 +04:00
Ilya Fedin
a8d1eadfbf Turn NotificationData into a struct 2025-02-28 13:20:53 +04:00
Ilya Fedin
b07d3c5403 Decouple GNotification from NotificationData 2025-02-28 12:05:09 +04:00
Ilya Fedin
892db55ae1 Get rid of NotificationData::init 2025-02-28 11:12:08 +04:00
Ilya Fedin
93615fef65 Revert "Check whether notification image has alpha channel"
This reverts commit cee593c423.

Avatars couldn't be opaque anyway while this simplifies porting out of NotificationData
2025-02-28 11:12:08 +04:00
Ilya Fedin
87452706ef Remove unused has_weak_ptr from Manager::Private 2025-02-28 11:12:08 +04:00
Ilya Fedin
3569615b21 Use gi::cstring for notification actions 2025-02-28 11:12:08 +04:00
Ilya Fedin
86f7d09d31 Pass notification icon name inline 2025-02-28 11:12:08 +04:00
Ilya Fedin
d5d1254393 Destroy NotificationData signal connections with rpl::lifetme 2025-02-28 11:12:08 +04:00
davidholio
4df90cfb9e Make sure caption items can only be interacted if not in video fullscreen. 2025-02-25 11:40:05 +04:00
Ilya Fedin
ec6862d31a Communicate PiP window margins to the OS 2025-02-25 10:15:33 +04:00
Ilya Fedin
6f23010382 Fix IconGraphic::counterSlice for Window::WithSmallCounter 2025-02-24 13:25:59 +04:00
Ilya Fedin
c672f105d3 IconGraphic::isCounterNeeded helper for Linux tray 2025-02-24 13:25:59 +04:00
Ilya Fedin
e60d501e4a Have a state struct in Linux tray 2025-02-24 13:25:59 +04:00
jovaska
0d7175058b Fix compilation with ffmpeg-4.x 2025-02-24 13:15:37 +04:00
Ilya Fedin
3b0bd9d1d1 Remove duplicate entry in qt snap part 2025-02-20 17:48:37 +04:00
Ilya Fedin
bd28ac6e1f Re-enable ffmpeg optimizations on Linux
They were disabled due to -Ofast but Dockerfile is switched to -O3 since a long time
2025-02-20 11:34:55 +04:00
Ilya Fedin
0c2d00c792 More clean up in qt snap part 2025-02-20 11:34:47 +04:00
Ilya Fedin
140ba653b9 More clean up in libjxl snap part 2025-02-20 11:34:33 +04:00
Ilya Fedin
f64f008f77 Remove fmt from snap
It's not really needed for a long time
2025-02-20 11:34:24 +04:00
Ilya Fedin
a6315bef05 Move GNotiftcation action handlers to Manager 2025-02-19 14:42:11 +04:00
Ilya Fedin
f810d7c82a Fix spaces on end of lines 2025-02-18 21:26:38 +04:00
Ilya Fedin
cf61dedc79 Simplify GNotification actions 2025-02-18 21:21:37 +04:00
Ilya Fedin
2ab9587f5f Don't wrap QByteArray into std::shared_ptr
This has no sense as QByteArray is CoW
2025-02-18 21:20:53 +04:00
Ilya Fedin
4950b52359 Don't set CMAKE_EXPORT_COMPILE_COMMANDS via cmake.configureSettings
It's controlled by cmake.exportCompileCommandsFile and defaults to true
2025-02-14 20:19:29 +04:00
Nikolai Nechaev
03af444735 Notifications: stop fading in before starting to fade out
When a notification is to start hiding (i.e., fade out), it is supposed
to start fading out from the maximum opacity, even if it was not fully
restored (which only happens if the cursor passed through the
notification too quickly). Thus, call `.stop()` for the previous
animation, if any, before `.start()`ing the next animation.
2025-02-14 20:17:02 +04:00
Nikolai Nechaev
7f6221b409 Manager::startAllHiding: don't treat fading in notifications specially
Previously, `Window::Notifications::Default::Manager` would not start
hiding notifications that are fading in when other notifications should
hide. This would lead to some notifications never hiding, e.g., when the
cursor passes through the notification too quickly and there was not
enough time for the notification to fade in completely.

Also renamed `Widget::isShowing` -> `Widget::isFadingIn` for clarity.

Fixes #28811.
2025-02-14 20:17:02 +04:00
292 changed files with 15895 additions and 10118 deletions

View file

@ -9,10 +9,7 @@
"--compile-commands-dir=${workspaceFolder}/out"
],
"cmake.generator": "Ninja Multi-Config",
"cmake.buildDirectory": "${workspaceFolder}/out",
"cmake.configureSettings": {
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
}
"cmake.buildDirectory": "${workspaceFolder}/out"
},
"extensions": [
"ms-vscode.cpptools-extension-pack",

View file

@ -5,7 +5,7 @@ body:
- type: markdown
attributes:
value: |
Thanks for reporting issues of Telegram Desktop!
Thanks for reporting issues of AyuGram Desktop!
To make it easier for us to help you please enter detailed information below.
- type: textarea
@ -39,12 +39,9 @@ body:
required: true
- type: input
attributes:
label: Version of Telegram Desktop
label: Version of AyuGram Desktop
description: >
Please note we don't support versions from Linux distro repositories.
If you need support for these versions, **please contact your distro maintainer**
or your distro bugtracker.
**Don't use 'latest'**, specify actual version, **that's a reason to close your issue**.
**Don't use 'latest'**, specify actual version.
validations:
required: true
- type: dropdown
@ -52,11 +49,7 @@ body:
label: Installation source
multiple: false
options:
- Static binary from official website
- Microsoft Store
- Mac App Store
- Flatpak
- Snap
- Binary from GitHub / official Telegram source
- Other (unofficial) source
validations:
required: true
@ -65,9 +58,7 @@ body:
label: Crash ID
description: >
If you're reporting a crash, please enter the crash ID from the crash reporter
opening on the next launch after crash. **You have to enable beta versions
installation in Settings -> Advanced for the reporter to appear.**
You don't have to wait for a beta version to arrive.
opening on the next launch after crash.
- type: textarea
attributes:
label: Logs

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 578 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 829 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 712 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 926 B

View file

@ -1218,6 +1218,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_edit_privacy_contacts" = "My contacts";
"lng_edit_privacy_close_friends" = "Close friends";
"lng_edit_privacy_contacts_and_premium" = "Contacts & Premium";
"lng_edit_privacy_paid" = "Paid";
"lng_edit_privacy_contacts_and_miniapps" = "Contacts & Mini Apps";
"lng_edit_privacy_nobody" = "Nobody";
"lng_edit_privacy_premium" = "Premium users";
@ -1356,6 +1357,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_messages_privacy_premium_about" = "Subscribe now to change this setting and get access to other exclusive features of Telegram Premium.";
"lng_messages_privacy_premium" = "Only subscribers of {link} can select this option.";
"lng_messages_privacy_premium_link" = "Telegram Premium";
"lng_messages_privacy_charge" = "Charge for messages";
"lng_messages_privacy_charge_about" = "Charge a fee for messages from people outside your contacts or those you haven't messaged first.";
"lng_messages_privacy_price" = "Set your price per message";
"lng_messages_privacy_price_about" = "You will receive {percent} of the selected fee ({amount}) for each incoming message.";
"lng_messages_privacy_exceptions" = "Exceptions";
"lng_messages_privacy_remove_fee" = "Remove Fee";
"lng_messages_privacy_remove_about" = "Add users or entire groups who won't be charged for sending messages to you.";
"lng_self_destruct_title" = "Account self-destruction";
"lng_self_destruct_description" = "If you don't come online at least once within this period, your account will be deleted along with all groups, messages and contacts.";
@ -2158,6 +2166,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_action_boost_apply#other" = "{from} boosted the group {count} times";
"lng_action_set_chat_intro" = "{from} added the message below for all empty chats. How?";
"lng_action_payment_refunded" = "{peer} refunded {amount}";
"lng_action_paid_message_sent#one" = "You paid {count} Star to {action}";
"lng_action_paid_message_sent#other" = "You paid {count} Stars to {action}";
"lng_action_paid_message_one" = "send a message";
"lng_action_paid_message_some#one" = "send {count} message";
"lng_action_paid_message_some#other" = "send {count} messages";
"lng_action_paid_message_got#one" = "You received {count} Star from {name}";
"lng_action_paid_message_got#other" = "You received {count} Stars from {name}";
"lng_you_paid_stars#one" = "You paid {count} Star.";
"lng_you_paid_stars#other" = "You paid {count} Stars.";
"lng_similar_channels_title" = "Similar channels";
"lng_similar_channels_view_all" = "View all";
@ -2664,6 +2681,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_credits_summary_history_entry_inner_in" = "In-App Purchase";
"lng_credits_summary_balance" = "Balance";
"lng_credits_commission" = "{amount} commission";
"lng_credits_paid_messages_fee#one" = "Fee for {count} Message";
"lng_credits_paid_messages_fee#other" = "Fee for {count} Messages";
"lng_credits_paid_messages_fee_about" = "You receive {percent} of the price that you charge for each incoming message. {link}";
"lng_credits_paid_messages_fee_about_link" = "Change Fee {emoji}";
"lng_credits_paid_messages_full" = "Full Price";
"lng_credits_premium_gift_duration" = "Duration";
"lng_credits_more_options" = "More Options";
"lng_credits_balance_me" = "your balance";
"lng_credits_buy_button" = "Buy More Stars";
@ -2777,6 +2800,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_credits_small_balance_reaction" = "Buy **Stars** and send them to {channel} to support their posts.";
"lng_credits_small_balance_subscribe" = "Buy **Stars** and subscribe to **{channel}** and other channels.";
"lng_credits_small_balance_star_gift" = "Buy **Stars** to send gifts to {user} and other contacts.";
"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_fallback" = "Buy **Stars** to unlock content and services on Telegram.";
"lng_credits_purchase_blocked" = "Sorry, you can't purchase this item with Telegram Stars.";
"lng_credits_enough" = "You have enough stars at the moment. {link}";
@ -3310,6 +3335,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_gift_premium_about" = "Give {name} access to exclusive features with Telegram Premium. {features}";
"lng_gift_premium_features" = "See Features >";
"lng_gift_premium_label" = "Premium";
"lng_gift_premium_by_stars" = "or {amount}";
"lng_gift_stars_subtitle" = "Gift Stars";
"lng_gift_stars_about" = "Give {name} gifts that can be kept on your profile or converted to Stars. {link}";
"lng_gift_stars_link" = "What are Stars >";
@ -3321,8 +3347,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_gift_send_title" = "Send a Gift";
"lng_gift_send_message" = "Enter Message";
"lng_gift_send_anonymous" = "Hide My Name";
"lng_gift_send_pay_with_stars" = "Pay with {amount}";
"lng_gift_send_stars_balance" = "Your balance is {amount}. {link}";
"lng_gift_send_stars_balance_link" = "Get More Stars >";
"lng_gift_send_anonymous_self" = "Hide my name and message from visitors to my profile.";
"lng_gift_send_anonymous_about" = "You can hide your name and message from visitors to {user}'s profile. {recipient} will still see your name and message.";
"lng_gift_send_anonymous_about_paid" = "You can hide your name from visitors to {user}'s profile. {recipient} will still see your name.";
"lng_gift_send_anonymous_about_channel" = "You can hide your name and message from all visitors of this channel except its admins.";
"lng_gift_send_unique" = "Make Unique for {price}";
"lng_gift_send_unique_about" = "Enable this to let {user} turn your gift into a unique collectible. {link}";
@ -3398,6 +3428,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_gift_display_done_channel" = "The gift is now shown in channel's Gifts.";
"lng_gift_display_done_hide" = "The gift is now hidden from your profile page.";
"lng_gift_display_done_hide_channel" = "The gift is now hidden from channel's Gifts.";
"lng_gift_pinned_done" = "The gift will always be shown on top.";
"lng_gift_got_stars#one" = "You got **{count} Star** for this gift.";
"lng_gift_got_stars#other" = "You got **{count} Stars** for this gift.";
"lng_gift_channel_got#one" = "Channel got **{count} Star** for this gift.";
@ -3456,6 +3487,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_gift_transfer_button_for" = "Transfer for {price}";
"lng_gift_transfer_wear" = "Wear";
"lng_gift_transfer_take_off" = "Take Off";
"lng_gift_menu_show" = "Show";
"lng_gift_menu_hide" = "Hide";
"lng_gift_wear_title" = "Wear {name}";
"lng_gift_wear_about" = "and get these benefits:";
"lng_gift_wear_badge_title" = "Radiant Badge";
@ -3609,6 +3642,22 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_new_contact_from_request_group" = "{user} is an admin of {name}, a group you requested to join.";
"lng_new_contact_about_status" = "This account uses {emoji} as a custom status next to its\nname. Such emoji statuses are available to all\nsubscribers of {link}.";
"lng_new_contact_about_status_link" = "Telegram Premium";
"lng_new_contact_not_contact" = "Not a contact";
"lng_new_contact_phone_number" = "Phone number";
"lng_new_contact_registration" = "Registration";
"lng_new_contact_common_groups" = "Common groups";
"lng_new_contact_groups#one" = "{count} group {emoji} {arrow}";
"lng_new_contact_groups#other" = "{count} groups {emoji} {arrow}";
"lng_new_contact_not_official" = "Not an official account";
"lng_new_contact_updated_name" = "User updated name {when}";
"lng_new_contact_updated_photo" = "User updated photo {when}";
"lng_new_contact_updated_now" = "less than an hour ago";
"lng_new_contact_updated_hours#one" = "{count} hour ago";
"lng_new_contact_updated_hours#other" = "{count} hours ago";
"lng_new_contact_updated_days#one" = "{count} day ago";
"lng_new_contact_updated_days#other" = "{count} days ago";
"lng_new_contact_updated_months#one" = "{count} month ago";
"lng_new_contact_updated_months#other" = "{count} months ago";
"lng_from_request_title_channel" = "Response to your join request";
"lng_from_request_title_group" = "Response to your join request";
"lng_from_request_body" = "You received this message because you requested to join {name} on {date}.";
@ -3637,6 +3686,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_send_anonymous_ph" = "Send anonymously...";
"lng_story_reply_ph" = "Reply privately...";
"lng_story_comment_ph" = "Comment story...";
"lng_message_paid_ph" = "Message for {amount}";
"lng_send_text_no" = "Text not allowed.";
"lng_send_text_no_about" = "The admins of this group only allow sending {types}.";
"lng_send_text_type_and_last" = "{types} and {last}";
@ -4798,6 +4848,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_rights_boosts_no_restrict" = "Do not restrict boosters";
"lng_rights_boosts_about" = "Turn this on to always allow users who boosted your group to send messages and media.";
"lng_rights_boosts_about_on" = "Choose how many boosts a user must give to the group to bypass restrictions on sending messages.";
"lng_rights_charge_stars" = "Charge Stars for Messages";
"lng_rights_charge_stars_about" = "If you turn this on, regular members of the group will have to pay Stars to send messages.";
"lng_rights_charge_price" = "Set price per message";
"lng_rights_charge_price_about" = "Your group will receive {percent} of the selected fee ({amount}) for each incoming message.";
"lng_slowmode_enabled" = "Slow Mode is active.\nYou can send your next message in {left}.";
"lng_slowmode_no_many" = "Slow mode is enabled. You can't send more than one message at a time.";
@ -4805,6 +4859,28 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_slowmode_seconds#one" = "{count} second";
"lng_slowmode_seconds#other" = "{count} seconds";
"lng_payment_confirm_title" = "Confirm payment";
"lng_payment_confirm_text#one" = "{name} charges **{count}** Star per message.";
"lng_payment_confirm_text#other" = "{name} charges **{count}** Stars per message.";
"lng_payment_confirm_amount#one" = "**{count}** Star";
"lng_payment_confirm_amount#other" = "**{count}** Stars";
"lng_payment_confirm_users#one" = "You selected **{count}** user who charge Stars for messages.";
"lng_payment_confirm_users#other" = "You selected **{count}** users who charge Stars for messages.";
"lng_payment_confirm_chats#one" = "You selected **{count}** chat where you pay Stars for messages.";
"lng_payment_confirm_chats#other" = "You selected **{count}** chats where you pay Stars for messages.";
"lng_payment_confirm_sure#one" = "Would you like to pay {amount} to send **{count}** message?";
"lng_payment_confirm_sure#other" = "Would you like to pay {amount} to send **{count}** messages?";
"lng_payment_confirm_dont_ask" = "Don't ask me again";
"lng_payment_confirm_button#one" = "Pay for {count} Message";
"lng_payment_confirm_button#other" = "Pay for {count} Messages";
"lng_payment_bar_text" = "{name} must pay {cost} for each message to you.";
"lng_payment_bar_button" = "Remove Fee";
"lng_payment_refund_title" = "Remove Fee";
"lng_payment_refund_text" = "Are you sure you want to allow {name} to message you for free?";
"lng_payment_refund_also#one" = "Refund already paid {count} Star";
"lng_payment_refund_also#other" = "Refund already paid {count} Stars";
"lng_payment_refund_confirm" = "Confirm";
"lng_rights_gigagroup_title" = "Broadcast group";
"lng_rights_gigagroup_convert" = "Convert to Broadcast Group";
"lng_rights_gigagroup_about" = "Broadcast groups can have over 200,000 members, but only admins can send messages in them.";
@ -4936,6 +5012,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_send_non_premium_message_toast" = "**{user}** only accepts messages from contacts and {link} subscribers.";
"lng_send_non_premium_message_toast_link" = "Telegram Premium";
"lng_send_charges_stars_text" = "{user} charges {amount} for each message.";
"lng_send_charges_stars_go" = "Buy Stars";
"lng_exceptions_list_title" = "Exceptions";
"lng_removed_list_title" = "Removed users";

View file

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

View file

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

View file

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

View file

@ -149,18 +149,14 @@ void InitFilterLinkHeader(
iconEmoji
).value_or(Ui::FilterIcon::Custom)).active;
const auto isStatic = title.isStatic;
const auto makeContext = [=](Fn<void()> repaint) {
return Core::MarkedTextContext{
.session = &box->peerListUiShow()->session(),
.customEmojiRepaint = std::move(repaint),
.customEmojiLoopLimit = isStatic ? -1 : 0,
};
};
auto header = Ui::MakeFilterLinkHeader(box, {
.type = type,
.title = TitleText(type)(tr::now),
.about = AboutText(type, title.text),
.makeAboutContext = makeContext,
.aboutContext = Core::TextContext({
.session = &box->peerListUiShow()->session(),
.customEmojiLoopLimit = isStatic ? -1 : 0,
}),
.folderTitle = title.text,
.folderIcon = icon,
.badge = (type == Ui::FilterLinkHeaderType::AddingChats
@ -560,16 +556,12 @@ void ShowImportToast(
text.append('\n').append(phrase(tr::now, lt_count, added));
}
const auto isStatic = title.isStatic;
const auto makeContext = [=](not_null<QWidget*> widget) {
return Core::MarkedTextContext{
.session = &strong->session(),
.customEmojiRepaint = [=] { widget->update(); },
.customEmojiLoopLimit = isStatic ? -1 : 0,
};
};
strong->showToast({
.text = std::move(text),
.textContext = makeContext,
.textContext = Core::TextContext({
.session = &strong->session(),
.customEmojiLoopLimit = isStatic ? -1 : 0,
})
});
}
@ -640,18 +632,14 @@ void ProcessFilterInvite(
raw->setRealContentHeight(box->heightValue());
const auto isStatic = title.isStatic;
const auto makeContext = [=](Fn<void()> update) {
return Core::MarkedTextContext{
.session = &strong->session(),
.customEmojiRepaint = update,
.customEmojiLoopLimit = isStatic ? -1 : 0,
};
};
auto owned = Ui::FilterLinkProcessButton(
box,
type,
title.text,
makeContext,
Core::TextContext({
.session = &strong->session(),
.customEmojiLoopLimit = isStatic ? -1 : 0,
}),
std::move(badge));
const auto button = owned.data();
@ -873,18 +861,14 @@ void ProcessFilterRemove(
}, type, title, iconEmoji, rpl::single(0), horizontalFilters);
const auto isStatic = title.isStatic;
const auto makeContext = [=](Fn<void()> update) {
return Core::MarkedTextContext{
.session = &strong->session(),
.customEmojiRepaint = update,
.customEmojiLoopLimit = isStatic ? -1 : 0,
};
};
auto owned = Ui::FilterLinkProcessButton(
box,
type,
title.text,
makeContext,
Core::TextContext({
.session = &strong->session(),
.customEmojiLoopLimit = isStatic ? -1 : 0,
}),
std::move(badge));
const auto button = owned.data();

View file

@ -25,6 +25,7 @@ struct SendOptions {
TimeId scheduled = 0;
BusinessShortcutId shortcutId = 0;
EffectId effectId = 0;
int starsApproved = 0;
bool silent = false;
bool handleSupportSwitch = false;
bool invertCaption = false;

View file

@ -90,7 +90,13 @@ constexpr auto kTransactionsLimit = 100;
? peerFromMTP(*tl.data().vstarref_peer()).value
: 0;
const auto incoming = (amount >= StarsAmount());
const auto saveActorId = (reaction || !extended.empty()) && incoming;
const auto paidMessagesCount
= tl.data().vpaid_messages().value_or_empty();
const auto premiumMonthsForStars
= tl.data().vpremium_gift_months().value_or_empty();
const auto saveActorId = (reaction
|| !extended.empty()
|| paidMessagesCount) && incoming;
const auto parsedGift = stargift
? FromTL(&peer->session(), *stargift)
: std::optional<Data::StarGift>();
@ -110,9 +116,9 @@ constexpr auto kTransactionsLimit = 100;
.bareGiftStickerId = giftStickerId,
.bareActorId = saveActorId ? barePeerId : uint64(0),
.uniqueGift = parsedGift ? parsedGift->unique : nullptr,
.starrefAmount = starrefAmount,
.starrefCommission = starrefCommission,
.starrefRecipientId = starrefBarePeerId,
.starrefAmount = paidMessagesCount ? StarsAmount() : starrefAmount,
.starrefCommission = paidMessagesCount ? 0 : starrefCommission,
.starrefRecipientId = paidMessagesCount ? 0 : starrefBarePeerId,
.peerType = tl.data().vpeer().match([](const HistoryPeerTL &) {
return Data::CreditsHistoryEntry::PeerType::Peer;
}, [](const MTPDstarsTransactionPeerPlayMarket &) {
@ -138,9 +144,15 @@ constexpr auto kTransactionsLimit = 100;
? base::unixtime::parse(tl.data().vtransaction_date()->v)
: QDateTime(),
.successLink = qs(tl.data().vtransaction_url().value_or_empty()),
.paidMessagesCount = paidMessagesCount,
.paidMessagesAmount = (paidMessagesCount
? starrefAmount
: StarsAmount()),
.paidMessagesCommission = paidMessagesCount ? starrefCommission : 0,
.starsConverted = int(nonUniqueGift
? nonUniqueGift->vconvert_stars().v
: 0),
.premiumMonthsForStars = premiumMonthsForStars,
.floodSkip = int(tl.data().vfloodskip_number().value_or(0)),
.converted = stargift && incoming,
.stargift = stargift.has_value(),

View file

@ -114,7 +114,8 @@ void GlobalPrivacy::updateHideReadTime(bool hide) {
archiveAndMuteCurrent(),
unarchiveOnNewMessageCurrent(),
hide,
newRequirePremiumCurrent());
newRequirePremiumCurrent(),
newChargeStarsCurrent());
}
bool GlobalPrivacy::hideReadTimeCurrent() const {
@ -125,14 +126,6 @@ rpl::producer<bool> GlobalPrivacy::hideReadTime() const {
return _hideReadTime.value();
}
void GlobalPrivacy::updateNewRequirePremium(bool value) {
update(
archiveAndMuteCurrent(),
unarchiveOnNewMessageCurrent(),
hideReadTimeCurrent(),
value);
}
bool GlobalPrivacy::newRequirePremiumCurrent() const {
return _newRequirePremium.current();
}
@ -141,6 +134,25 @@ rpl::producer<bool> GlobalPrivacy::newRequirePremium() const {
return _newRequirePremium.value();
}
int GlobalPrivacy::newChargeStarsCurrent() const {
return _newChargeStars.current();
}
rpl::producer<int> GlobalPrivacy::newChargeStars() const {
return _newChargeStars.value();
}
void GlobalPrivacy::updateMessagesPrivacy(
bool requirePremium,
int chargeStars) {
update(
archiveAndMuteCurrent(),
unarchiveOnNewMessageCurrent(),
hideReadTimeCurrent(),
requirePremium,
chargeStars);
}
void GlobalPrivacy::loadPaidReactionShownPeer() {
if (_paidReactionShownPeerLoaded) {
return;
@ -169,7 +181,8 @@ void GlobalPrivacy::updateArchiveAndMute(bool value) {
value,
unarchiveOnNewMessageCurrent(),
hideReadTimeCurrent(),
newRequirePremiumCurrent());
newRequirePremiumCurrent(),
newChargeStarsCurrent());
}
void GlobalPrivacy::updateUnarchiveOnNewMessage(
@ -178,14 +191,16 @@ void GlobalPrivacy::updateUnarchiveOnNewMessage(
archiveAndMuteCurrent(),
value,
hideReadTimeCurrent(),
newRequirePremiumCurrent());
newRequirePremiumCurrent(),
newChargeStarsCurrent());
}
void GlobalPrivacy::update(
bool archiveAndMute,
UnarchiveOnNewMessage unarchiveOnNewMessage,
bool hideReadTime,
bool newRequirePremium) {
bool newRequirePremium,
int newChargeStars) {
using Flag = MTPDglobalPrivacySettings::Flag;
_api.request(_requestId).cancel();
@ -204,35 +219,44 @@ void GlobalPrivacy::update(
| (hideReadTime ? Flag::f_hide_read_marks : Flag())
| ((newRequirePremium && newRequirePremiumAllowed)
? Flag::f_new_noncontact_peers_require_premium
: Flag());
: Flag())
| Flag::f_noncontact_peers_paid_stars;
_requestId = _api.request(MTPaccount_SetGlobalPrivacySettings(
MTP_globalPrivacySettings(MTP_flags(flags))
MTP_globalPrivacySettings(
MTP_flags(flags),
MTP_long(newChargeStars))
)).done([=](const MTPGlobalPrivacySettings &result) {
_requestId = 0;
apply(result);
}).fail([=](const MTP::Error &error) {
_requestId = 0;
if (error.type() == u"PREMIUM_ACCOUNT_REQUIRED"_q) {
update(archiveAndMute, unarchiveOnNewMessage, hideReadTime, {});
update(
archiveAndMute,
unarchiveOnNewMessage,
hideReadTime,
false,
0);
}
}).send();
_archiveAndMute = archiveAndMute;
_unarchiveOnNewMessage = unarchiveOnNewMessage;
_hideReadTime = hideReadTime;
_newRequirePremium = newRequirePremium;
_newChargeStars = newChargeStars;
}
void GlobalPrivacy::apply(const MTPGlobalPrivacySettings &data) {
data.match([&](const MTPDglobalPrivacySettings &data) {
_archiveAndMute = data.is_archive_and_mute_new_noncontact_peers();
_unarchiveOnNewMessage = data.is_keep_archived_unmuted()
? UnarchiveOnNewMessage::None
: data.is_keep_archived_folders()
? UnarchiveOnNewMessage::NotInFoldersUnmuted
: UnarchiveOnNewMessage::AnyUnmuted;
_hideReadTime = data.is_hide_read_marks();
_newRequirePremium = data.is_new_noncontact_peers_require_premium();
});
void GlobalPrivacy::apply(const MTPGlobalPrivacySettings &settings) {
const auto &data = settings.data();
_archiveAndMute = data.is_archive_and_mute_new_noncontact_peers();
_unarchiveOnNewMessage = data.is_keep_archived_unmuted()
? UnarchiveOnNewMessage::None
: data.is_keep_archived_folders()
? UnarchiveOnNewMessage::NotInFoldersUnmuted
: UnarchiveOnNewMessage::AnyUnmuted;
_hideReadTime = data.is_hide_read_marks();
_newRequirePremium = data.is_new_noncontact_peers_require_premium();
_newChargeStars = data.vnoncontact_peers_paid_stars().value_or_empty();
}
} // namespace Api

View file

@ -49,23 +49,28 @@ public:
[[nodiscard]] bool hideReadTimeCurrent() const;
[[nodiscard]] rpl::producer<bool> hideReadTime() const;
void updateNewRequirePremium(bool value);
[[nodiscard]] bool newRequirePremiumCurrent() const;
[[nodiscard]] rpl::producer<bool> newRequirePremium() const;
[[nodiscard]] int newChargeStarsCurrent() const;
[[nodiscard]] rpl::producer<int> newChargeStars() const;
void updateMessagesPrivacy(bool requirePremium, int chargeStars);
void loadPaidReactionShownPeer();
void updatePaidReactionShownPeer(PeerId shownPeer);
[[nodiscard]] PeerId paidReactionShownPeerCurrent() const;
[[nodiscard]] rpl::producer<PeerId> paidReactionShownPeer() const;
private:
void apply(const MTPGlobalPrivacySettings &data);
void apply(const MTPGlobalPrivacySettings &settings);
void update(
bool archiveAndMute,
UnarchiveOnNewMessage unarchiveOnNewMessage,
bool hideReadTime,
bool newRequirePremium);
bool newRequirePremium,
int newChargeStars);
const not_null<Main::Session*> _session;
MTP::Sender _api;
@ -76,6 +81,7 @@ private:
rpl::variable<bool> _showArchiveAndMute = false;
rpl::variable<bool> _hideReadTime = false;
rpl::variable<bool> _newRequirePremium = false;
rpl::variable<int> _newChargeStars = 0;
rpl::variable<PeerId> _paidReactionShownPeer = false;
std::vector<Fn<void()>> _callbacks;
bool _paidReactionShownPeerLoaded = false;

View file

@ -42,7 +42,7 @@ Polls::Polls(not_null<ApiWrap*> api)
void Polls::create(
const PollData &data,
const SendAction &action,
SendAction action,
Fn<void()> done,
Fn<void()> fail) {
_session->api().sendAction(action);
@ -64,6 +64,9 @@ void Polls::create(
history->startSavingCloudDraft(topicRootId);
}
const auto silentPost = ShouldSendSilent(peer, action.options);
const auto starsPaid = std::min(
peer->starsPerMessageChecked(),
action.options.starsApproved);
if (silentPost) {
sendFlags |= MTPmessages_SendMedia::Flag::f_silent;
}
@ -76,6 +79,10 @@ void Polls::create(
if (action.options.effectId) {
sendFlags |= MTPmessages_SendMedia::Flag::f_effect;
}
if (starsPaid) {
action.options.starsApproved -= starsPaid;
sendFlags |= MTPmessages_SendMedia::Flag::f_allow_paid_stars;
}
const auto sendAs = action.options.sendAs;
if (sendAs) {
sendFlags |= MTPmessages_SendMedia::Flag::f_send_as;
@ -98,7 +105,8 @@ void Polls::create(
MTP_int(action.options.scheduled),
(sendAs ? sendAs->input : MTP_inputPeerEmpty()),
Data::ShortcutIdToMTP(_session, action.options.shortcutId),
MTP_long(action.options.effectId)
MTP_long(action.options.effectId),
MTP_long(starsPaid)
), [=](const MTPUpdates &result, const MTP::Response &response) {
if (clearCloudDraft) {
history->finishSavingCloudDraft(

View file

@ -27,7 +27,7 @@ public:
void create(
const PollData &data,
const SendAction &action,
SendAction action,
Fn<void()> done,
Fn<void()> fail);
void sendVotes(

View file

@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_text_entities.h"
#include "apiwrap.h"
#include "base/random.h"
#include "data/data_channel.h"
#include "data/data_document.h"
#include "data/data_peer.h"
#include "data/data_peer_values.h"
@ -377,15 +378,15 @@ const Data::PremiumSubscriptionOptions &Premium::subscriptionOptions() const {
return _subscriptionOptions;
}
rpl::producer<> Premium::somePremiumRequiredResolved() const {
return _somePremiumRequiredResolved.events();
rpl::producer<> Premium::someMessageMoneyRestrictionsResolved() const {
return _someMessageMoneyRestrictionsResolved.events();
}
void Premium::resolvePremiumRequired(not_null<UserData*> user) {
_resolvePremiumRequiredUsers.emplace(user);
if (!_premiumRequiredRequestScheduled
&& _resolvePremiumRequestedUsers.empty()) {
_premiumRequiredRequestScheduled = true;
void Premium::resolveMessageMoneyRestrictions(not_null<UserData*> user) {
_resolveMessageMoneyRequiredUsers.emplace(user);
if (!_messageMoneyRequestScheduled
&& _resolveMessageMoneyRequestedUsers.empty()) {
_messageMoneyRequestScheduled = true;
crl::on_main(_session, [=] {
requestPremiumRequiredSlice();
});
@ -393,50 +394,65 @@ void Premium::resolvePremiumRequired(not_null<UserData*> user) {
}
void Premium::requestPremiumRequiredSlice() {
_premiumRequiredRequestScheduled = false;
if (!_resolvePremiumRequestedUsers.empty()
|| _resolvePremiumRequiredUsers.empty()) {
_messageMoneyRequestScheduled = false;
if (!_resolveMessageMoneyRequestedUsers.empty()
|| _resolveMessageMoneyRequiredUsers.empty()) {
return;
}
constexpr auto kPerRequest = 100;
auto users = MTP_vector_from_range(_resolvePremiumRequiredUsers
auto users = MTP_vector_from_range(_resolveMessageMoneyRequiredUsers
| ranges::views::transform(&UserData::inputUser));
if (users.v.size() > kPerRequest) {
auto shortened = users.v;
shortened.resize(kPerRequest);
users = MTP_vector<MTPInputUser>(std::move(shortened));
const auto from = begin(_resolvePremiumRequiredUsers);
_resolvePremiumRequestedUsers = { from, from + kPerRequest };
_resolvePremiumRequiredUsers.erase(from, from + kPerRequest);
const auto from = begin(_resolveMessageMoneyRequiredUsers);
_resolveMessageMoneyRequestedUsers = { from, from + kPerRequest };
_resolveMessageMoneyRequiredUsers.erase(from, from + kPerRequest);
} else {
_resolvePremiumRequestedUsers
= base::take(_resolvePremiumRequiredUsers);
_resolveMessageMoneyRequestedUsers
= base::take(_resolveMessageMoneyRequiredUsers);
}
const auto finish = [=](const QVector<MTPBool> &list) {
constexpr auto me = UserDataFlag::MeRequiresPremiumToWrite;
constexpr auto known = UserDataFlag::RequirePremiumToWriteKnown;
constexpr auto mask = me | known;
const auto finish = [=](const QVector<MTPRequirementToContact> &list) {
auto index = 0;
for (const auto &user : base::take(_resolvePremiumRequestedUsers)) {
const auto require = (index < list.size())
&& mtpIsTrue(list[index++]);
user->setFlags((user->flags() & ~mask)
| known
| (require ? me : UserDataFlag()));
for (const auto &user : base::take(_resolveMessageMoneyRequestedUsers)) {
const auto set = [&](bool requirePremium, int stars) {
using Flag = UserDataFlag;
constexpr auto me = Flag::RequiresPremiumToWrite;
constexpr auto known = Flag::MessageMoneyRestrictionsKnown;
constexpr auto hasPrem = Flag::HasRequirePremiumToWrite;
constexpr auto hasStars = Flag::HasStarsPerMessage;
user->setStarsPerMessage(stars);
user->setFlags((user->flags() & ~(me | hasPrem | hasStars))
| known
| (requirePremium ? (me | hasPrem) : Flag())
| (stars ? hasStars : Flag()));
};
if (index >= list.size()) {
set(false, 0);
continue;
}
list[index++].match([&](const MTPDrequirementToContactEmpty &) {
set(false, 0);
}, [&](const MTPDrequirementToContactPremium &) {
set(true, 0);
}, [&](const MTPDrequirementToContactPaidMessages &data) {
set(false, data.vstars_amount().v);
});
}
if (!_premiumRequiredRequestScheduled
&& !_resolvePremiumRequiredUsers.empty()) {
_premiumRequiredRequestScheduled = true;
if (!_messageMoneyRequestScheduled
&& !_resolveMessageMoneyRequiredUsers.empty()) {
_messageMoneyRequestScheduled = true;
crl::on_main(_session, [=] {
requestPremiumRequiredSlice();
});
}
_somePremiumRequiredResolved.fire({});
_someMessageMoneyRestrictionsResolved.fire({});
};
_session->api().request(
MTPusers_GetIsPremiumRequiredToContact(std::move(users))
).done([=](const MTPVector<MTPBool> &result) {
MTPusers_GetRequirementsToContact(std::move(users))
).done([=](const MTPVector<MTPRequirementToContact> &result) {
finish(result.v);
}).fail([=] {
finish({});
@ -463,10 +479,14 @@ rpl::producer<rpl::no_value, QString> PremiumGiftCodeOptions::request() {
for (const auto &tlOption : result.v) {
const auto &data = tlOption.data();
tlMapOptions[data.vusers().v].push_back(tlOption);
if (qs(data.vcurrency()) == Ui::kCreditsCurrency) {
continue;
}
const auto token = Token{ data.vusers().v, data.vmonths().v };
_stores[token] = Store{
.amount = data.vamount().v,
.currency = qs(data.vcurrency()),
.product = qs(data.vstore_product().value_or_empty()),
.quantity = data.vstore_quantity().value_or_empty(),
};
@ -475,14 +495,14 @@ rpl::producer<rpl::no_value, QString> PremiumGiftCodeOptions::request() {
}
}
for (const auto &[amount, tlOptions] : tlMapOptions) {
if (amount == 1 && _optionsForOnePerson.currency.isEmpty()) {
_optionsForOnePerson.currency = qs(
tlOptions.front().data().vcurrency());
if (amount == 1 && _optionsForOnePerson.currencies.empty()) {
for (const auto &option : tlOptions) {
_optionsForOnePerson.months.push_back(
option.data().vmonths().v);
_optionsForOnePerson.totalCosts.push_back(
option.data().vamount().v);
_optionsForOnePerson.currencies.push_back(
qs(option.data().vcurrency()));
}
}
_subscriptionOptions[amount] = GiftCodesFromTL(tlOptions);
@ -509,7 +529,7 @@ rpl::producer<rpl::no_value, QString> PremiumGiftCodeOptions::applyPrepaid(
_api.request(MTPpayments_LaunchPrepaidGiveaway(
_peer->input,
MTP_long(prepaidId),
invoice.creditsAmount
invoice.giveawayCredits
? Payments::InvoiceCreditsGiveawayToTL(invoice)
: Payments::InvoicePremiumGiftCodeGiveawayToTL(invoice)
)).done([=](const MTPUpdates &result) {
@ -540,7 +560,7 @@ Payments::InvoicePremiumGiftCode PremiumGiftCodeOptions::invoice(
const auto token = Token{ users, months };
const auto &store = _stores[token];
return Payments::InvoicePremiumGiftCode{
.currency = _optionsForOnePerson.currency,
.currency = store.currency,
.storeProduct = store.product,
.randomId = randomId,
.amount = store.amount,
@ -553,14 +573,15 @@ Payments::InvoicePremiumGiftCode PremiumGiftCodeOptions::invoice(
std::vector<GiftOptionData> PremiumGiftCodeOptions::optionsForPeer() const {
auto result = std::vector<GiftOptionData>();
if (!_optionsForOnePerson.currency.isEmpty()) {
if (!_optionsForOnePerson.currencies.empty()) {
const auto count = int(_optionsForOnePerson.months.size());
result.reserve(count);
for (auto i = 0; i != count; ++i) {
Assert(i < _optionsForOnePerson.totalCosts.size());
Assert(i < _optionsForOnePerson.currencies.size());
result.push_back({
.cost = _optionsForOnePerson.totalCosts[i],
.currency = _optionsForOnePerson.currency,
.currency = _optionsForOnePerson.currencies[i],
.months = _optionsForOnePerson.months[i],
});
}
@ -581,7 +602,7 @@ Data::PremiumSubscriptionOptions PremiumGiftCodeOptions::options(int amount) {
MTP_int(_optionsForOnePerson.months[i]),
MTPstring(),
MTPint(),
MTP_string(_optionsForOnePerson.currency),
MTP_string(_optionsForOnePerson.currencies[i]),
MTP_long(_optionsForOnePerson.totalCosts[i] * amount)));
}
_subscriptionOptions[amount] = GiftCodesFromTL(tlOptions);
@ -694,28 +715,38 @@ rpl::producer<rpl::no_value, QString> SponsoredToggle::setToggled(bool v) {
};
}
RequirePremiumState ResolveRequiresPremiumToWrite(
MessageMoneyRestriction ResolveMessageMoneyRestrictions(
not_null<PeerData*> peer,
History *maybeHistory) {
const auto user = peer->asUser();
if (!user
|| !user->someRequirePremiumToWrite()
|| user->session().premium()) {
return RequirePremiumState::No;
} else if (user->requirePremiumToWriteKnown()) {
return user->meRequiresPremiumToWrite()
? RequirePremiumState::Yes
: RequirePremiumState::No;
} else if (user->flags() & UserDataFlag::MutualContact) {
return RequirePremiumState::No;
} else if (!maybeHistory) {
return RequirePremiumState::Unknown;
if (const auto channel = peer->asChannel()) {
return {
.starsPerMessage = channel->starsPerMessageChecked(),
.known = true,
};
}
const auto user = peer->asUser();
if (!user) {
return { .known = true };
} else if (user->messageMoneyRestrictionsKnown()) {
return {
.starsPerMessage = user->starsPerMessageChecked(),
.premiumRequired = (user->requiresPremiumToWrite()
&& !user->session().premium()),
.known = true,
};
} else if (user->hasStarsPerMessage()) {
return {};
} else if (!user->hasRequirePremiumToWrite()) {
return { .known = true };
} else if (user->flags() & UserDataFlag::MutualContact) {
return { .known = true };
} else if (!maybeHistory) {
return {};
}
const auto update = [&](bool require) {
using Flag = UserDataFlag;
constexpr auto known = Flag::RequirePremiumToWriteKnown;
constexpr auto me = Flag::MeRequiresPremiumToWrite;
constexpr auto known = Flag::MessageMoneyRestrictionsKnown;
constexpr auto me = Flag::RequiresPremiumToWrite;
user->setFlags((user->flags() & ~me)
| known
| (require ? me : Flag()));
@ -727,16 +758,19 @@ RequirePremiumState ResolveRequiresPremiumToWrite(
const auto item = view->data();
if (!item->out() && !item->isService()) {
update(false);
return RequirePremiumState::No;
return { .known = true };
}
}
}
if (user->isContact() // Here we know, that we're not in his contacts.
&& maybeHistory->loadedAtTop() // And no incoming messages.
&& maybeHistory->loadedAtBottom()) {
update(true);
return {
.premiumRequired = !user->session().premium(),
.known = true,
};
}
return RequirePremiumState::Unknown;
return {};
}
rpl::producer<DocumentData*> RandomHelloStickerValue(
@ -870,6 +904,7 @@ std::optional<Data::SavedStarGift> FromTL(
.date = data.vdate().v,
.upgradable = data.is_can_upgrade(),
.anonymous = data.is_name_hidden(),
.pinned = data.is_pinned_to_top(),
.hidden = data.is_unsaved(),
.mine = to->isSelf(),
};

View file

@ -116,8 +116,9 @@ public:
[[nodiscard]] auto subscriptionOptions() const
-> const Data::PremiumSubscriptionOptions &;
[[nodiscard]] rpl::producer<> somePremiumRequiredResolved() const;
void resolvePremiumRequired(not_null<UserData*> user);
[[nodiscard]] auto someMessageMoneyRestrictionsResolved() const
-> rpl::producer<>;
void resolveMessageMoneyRestrictions(not_null<UserData*> user);
private:
void reloadPromo();
@ -166,10 +167,10 @@ private:
Data::PremiumSubscriptionOptions _subscriptionOptions;
rpl::event_stream<> _somePremiumRequiredResolved;
base::flat_set<not_null<UserData*>> _resolvePremiumRequiredUsers;
base::flat_set<not_null<UserData*>> _resolvePremiumRequestedUsers;
bool _premiumRequiredRequestScheduled = false;
rpl::event_stream<> _someMessageMoneyRestrictionsResolved;
base::flat_set<not_null<UserData*>> _resolveMessageMoneyRequiredUsers;
base::flat_set<not_null<UserData*>> _resolveMessageMoneyRequestedUsers;
bool _messageMoneyRequestScheduled = false;
};
@ -208,6 +209,7 @@ private:
};
struct Store final {
uint64 amount = 0;
QString currency;
QString product;
int quantity = 0;
};
@ -218,7 +220,7 @@ private:
struct {
std::vector<int> months;
std::vector<int64> totalCosts;
QString currency;
std::vector<QString> currencies;
} _optionsForOnePerson;
std::vector<int> _availablePresets;
@ -244,12 +246,20 @@ private:
};
enum class RequirePremiumState {
Unknown,
Yes,
No,
struct MessageMoneyRestriction {
int starsPerMessage = 0;
bool premiumRequired = false;
bool known = false;
explicit operator bool() const {
return starsPerMessage != 0 || premiumRequired;
}
friend inline bool operator==(
const MessageMoneyRestriction &,
const MessageMoneyRestriction &) = default;
};
[[nodiscard]] RequirePremiumState ResolveRequiresPremiumToWrite(
[[nodiscard]] MessageMoneyRestriction ResolveMessageMoneyRestrictions(
not_null<PeerData*> peer,
History *maybeHistory);

View file

@ -26,7 +26,7 @@ Data::PremiumSubscriptionOption CreateSubscriptionOption(
}();
return {
.duration = Ui::FormatTTL(months * 86400 * 31),
.discount = discount
.discount = (discount > 0)
? QString::fromUtf8("\xe2\x88\x92%1%").arg(discount)
: QString(),
.costPerMonth = Ui::FillAmountAndCurrency(

View file

@ -24,15 +24,26 @@ template<typename Option>
if (tlOpts.isEmpty()) {
return {};
}
auto monthlyAmountPerCurrency = base::flat_map<QString, int>();
auto result = Data::PremiumSubscriptionOptions();
const auto monthlyAmount = [&] {
const auto monthlyAmount = [&](const QString &currency) -> int {
const auto it = monthlyAmountPerCurrency.find(currency);
if (it != end(monthlyAmountPerCurrency)) {
return it->second;
}
const auto &min = ranges::min_element(
tlOpts,
ranges::less(),
[](const Option &o) { return o.data().vamount().v; }
[&](const Option &o) {
return currency == qs(o.data().vcurrency())
? o.data().vamount().v
: std::numeric_limits<int64_t>::max();
}
)->data();
return min.vamount().v / float64(min.vmonths().v);
}();
const auto monthly = min.vamount().v / float64(min.vmonths().v);
monthlyAmountPerCurrency.emplace(currency, monthly);
return monthly;
};
result.reserve(tlOpts.size());
for (const auto &tlOption : tlOpts) {
const auto &option = tlOption.data();
@ -45,7 +56,7 @@ template<typename Option>
const auto currency = qs(option.vcurrency());
result.push_back(CreateSubscriptionOption(
months,
monthlyAmount,
monthlyAmount(currency),
amount,
currency,
botUrl));

View file

@ -95,7 +95,9 @@ void SendSimpleMedia(SendAction action, MTPInputMedia inputMedia) {
const auto messagePostAuthor = peer->isBroadcast()
? session->user()->name()
: QString();
const auto starsPaid = std::min(
peer->starsPerMessageChecked(),
action.options.starsApproved);
if (action.options.scheduled) {
flags |= MessageFlag::IsOrWasScheduled;
sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date;
@ -111,6 +113,10 @@ void SendSimpleMedia(SendAction action, MTPInputMedia inputMedia) {
flags |= MessageFlag::InvertMedia;
sendFlags |= MTPmessages_SendMedia::Flag::f_invert_media;
}
if (starsPaid) {
action.options.starsApproved -= starsPaid;
sendFlags |= MTPmessages_SendMedia::Flag::f_allow_paid_stars;
}
auto &histories = history->owner().histories();
histories.sendPreparedMessage(
@ -129,7 +135,8 @@ void SendSimpleMedia(SendAction action, MTPInputMedia inputMedia) {
MTP_int(action.options.scheduled),
(sendAs ? sendAs->input : MTP_inputPeerEmpty()),
Data::ShortcutIdToMTP(session, action.options.shortcutId),
MTP_long(action.options.effectId)
MTP_long(action.options.effectId),
MTP_long(starsPaid)
), [=](const MTPUpdates &result, const MTP::Response &response) {
}, [=](const MTP::Error &error, const MTP::Response &response) {
api->sendMessageFail(error, peer, randomId);
@ -160,7 +167,7 @@ void SendExistingMedia(
? (*localMessageId)
: session->data().nextLocalMessageId());
const auto randomId = base::RandomValue<uint64>();
const auto &action = message.action;
auto &action = message.action;
auto flags = NewMessageFlags(peer);
auto sendFlags = MTPmessages_SendMedia::Flags(0);
@ -190,7 +197,9 @@ void SendExistingMedia(
sendFlags |= MTPmessages_SendMedia::Flag::f_entities;
}
const auto captionText = caption.text;
const auto starsPaid = std::min(
peer->starsPerMessageChecked(),
action.options.starsApproved);
if (action.options.scheduled) {
flags |= MessageFlag::IsOrWasScheduled;
sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date;
@ -206,6 +215,10 @@ void SendExistingMedia(
flags |= MessageFlag::InvertMedia;
sendFlags |= MTPmessages_SendMedia::Flag::f_invert_media;
}
if (starsPaid) {
action.options.starsApproved -= starsPaid;
sendFlags |= MTPmessages_SendMedia::Flag::f_allow_paid_stars;
}
session->data().registerMessageRandomId(randomId, newId);
@ -216,6 +229,7 @@ void SendExistingMedia(
.replyTo = action.replyTo,
.date = NewMessageDate(action.options),
.shortcutId = action.options.shortcutId,
.starsPaid = starsPaid,
.postAuthor = NewMessagePostAuthor(action),
.effectId = action.options.effectId,
}, media, caption);
@ -240,7 +254,8 @@ void SendExistingMedia(
MTP_int(action.options.scheduled),
(sendAs ? sendAs->input : MTP_inputPeerEmpty()),
Data::ShortcutIdToMTP(session, action.options.shortcutId),
MTP_long(action.options.effectId)
MTP_long(action.options.effectId),
MTP_long(starsPaid)
), [=](const MTPUpdates &result, const MTP::Response &response) {
}, [=](const MTP::Error &error, const MTP::Response &response) {
if (error.code() == 400
@ -341,7 +356,7 @@ bool SendDice(MessageToSend &message) {
message.action.generateLocal = true;
const auto &action = message.action;
auto &action = message.action;
api->sendAction(action);
const auto newId = FullMsgId(
@ -380,6 +395,13 @@ bool SendDice(MessageToSend &message) {
flags |= MessageFlag::InvertMedia;
sendFlags |= MTPmessages_SendMedia::Flag::f_invert_media;
}
const auto starsPaid = std::min(
peer->starsPerMessageChecked(),
action.options.starsApproved);
if (starsPaid) {
action.options.starsApproved -= starsPaid;
sendFlags |= MTPmessages_SendMedia::Flag::f_allow_paid_stars;
}
session->data().registerMessageRandomId(randomId, newId);
@ -390,6 +412,7 @@ bool SendDice(MessageToSend &message) {
.replyTo = action.replyTo,
.date = NewMessageDate(action.options),
.shortcutId = action.options.shortcutId,
.starsPaid = starsPaid,
.postAuthor = NewMessagePostAuthor(action),
.effectId = action.options.effectId,
}, TextWithEntities(), MTP_messageMediaDice(
@ -411,7 +434,8 @@ bool SendDice(MessageToSend &message) {
MTP_int(action.options.scheduled),
(sendAs ? sendAs->input : MTP_inputPeerEmpty()),
Data::ShortcutIdToMTP(session, action.options.shortcutId),
MTP_long(action.options.effectId)
MTP_long(action.options.effectId),
MTP_long(starsPaid)
), [=](const MTPUpdates &result, const MTP::Response &response) {
}, [=](const MTP::Error &error, const MTP::Response &response) {
api->sendMessageFail(error, peer, randomId, newId);
@ -610,6 +634,9 @@ void SendConfirmedFile(
.replyTo = file->to.replyTo,
.date = NewMessageDate(file->to.options),
.shortcutId = file->to.options.shortcutId,
.starsPaid = std::min(
history->peer->starsPerMessageChecked(),
file->to.options.starsApproved),
.postAuthor = NewMessagePostAuthor(action),
.groupedId = groupId,
.effectId = file->to.options.effectId,

View file

@ -1227,7 +1227,8 @@ void Updates::applyUpdatesNoPtsCheck(const MTPUpdates &updates) {
MTPint(), // quick_reply_shortcut_id
MTPlong(), // effect
MTPFactCheck(),
MTPint()), // report_delivery_until_date
MTPint(), // report_delivery_until_date
MTPlong()), // paid_message_stars
MessageFlags(),
NewMessageType::Unread);
} break;
@ -1265,7 +1266,8 @@ void Updates::applyUpdatesNoPtsCheck(const MTPUpdates &updates) {
MTPint(), // quick_reply_shortcut_id
MTPlong(), // effect
MTPFactCheck(),
MTPint()), // report_delivery_until_date
MTPint(), // report_delivery_until_date
MTPlong()), // paid_message_stars
MessageFlags(),
NewMessageType::Unread);
} break;

View file

@ -210,6 +210,7 @@ MTPInputPrivacyKey KeyToTL(UserPrivacy::Key key) {
case Key::About: return MTP_inputPrivacyKeyAbout();
case Key::Birthday: return MTP_inputPrivacyKeyBirthday();
case Key::GiftsAutoSave: return MTP_inputPrivacyKeyStarGiftsAutoSave();
case Key::NoPaidMessages: return MTP_inputPrivacyKeyNoPaidMessages();
}
Unexpected("Key in Api::UserPrivacy::KetToTL.");
}
@ -241,6 +242,8 @@ std::optional<UserPrivacy::Key> TLToKey(mtpTypeId type) {
case mtpc_inputPrivacyKeyBirthday: return Key::Birthday;
case mtpc_privacyKeyStarGiftsAutoSave:
case mtpc_inputPrivacyKeyStarGiftsAutoSave: return Key::GiftsAutoSave;
case mtpc_privacyKeyNoPaidMessages:
case mtpc_inputPrivacyKeyNoPaidMessages: return Key::NoPaidMessages;
}
return std::nullopt;
}

View file

@ -32,6 +32,7 @@ public:
About,
Birthday,
GiftsAutoSave,
NoPaidMessages,
};
enum class Option {
Everyone,

View file

@ -546,6 +546,7 @@ void ApiWrap::sendMessageFail(
uint64 randomId,
FullMsgId itemId) {
const auto show = ShowForPeer(peer);
const auto paidStarsPrefix = u"ALLOW_PAYMENT_REQUIRED_"_q;
if (show && error == u"PEER_FLOOD"_q) {
show->showBox(
Ui::MakeInformBox(
@ -600,6 +601,19 @@ void ApiWrap::sendMessageFail(
if (show) {
show->showToast(tr::lng_error_schedule_limit(tr::now));
}
} else if (error.startsWith(paidStarsPrefix)) {
if (show) {
show->showToast(
u"Payment requirements changed. Please, try again."_q);
}
if (const auto stars = error.mid(paidStarsPrefix.size()).toInt()) {
if (const auto user = peer->asUser()) {
user->setStarsPerMessage(stars);
} else if (const auto channel = peer->asChannel()) {
channel->setStarsPerMessage(stars);
}
}
peer->updateFull();
}
if (const auto item = _session->data().message(itemId)) {
Assert(randomId != 0);
@ -3342,7 +3356,7 @@ void ApiWrap::finishForwarding(const SendAction &action) {
void ApiWrap::forwardMessages(
Data::ResolvedForwardDraft &&draft,
const SendAction &action,
SendAction action,
FnMut<void()> &&successCallback) {
Expects(!draft.items.empty());
@ -3417,9 +3431,17 @@ void ApiWrap::forwardMessages(
const auto requestType = Data::Histories::RequestType::Send;
const auto idsCopy = localIds;
const auto scheduled = action.options.scheduled;
const auto starsPaid = std::min(
action.options.starsApproved,
int(ids.size() * peer->starsPerMessageChecked()));
auto oneFlags = sendFlags;
if (starsPaid) {
action.options.starsApproved -= starsPaid;
oneFlags |= SendFlag::f_allow_paid_stars;
}
histories.sendRequest(history, requestType, [=](Fn<void()> finish) {
history->sendRequestId = request(MTPmessages_ForwardMessages(
MTP_flags(sendFlags),
MTP_flags(oneFlags),
forwardFrom->input,
MTP_vector<MTPint>(ids),
MTP_vector<MTPlong>(randomIds),
@ -3428,7 +3450,8 @@ void ApiWrap::forwardMessages(
MTP_int(action.options.scheduled),
(sendAs ? sendAs->input : MTP_inputPeerEmpty()),
Data::ShortcutIdToMTP(_session, action.options.shortcutId),
MTPint() // video_timestamp
MTPint(), // video_timestamp
MTP_long(starsPaid)
)).done([=](const MTPUpdates &result) {
if (!scheduled) {
this->updates().checkForSentToScheduled(result);
@ -3480,6 +3503,7 @@ void ApiWrap::forwardMessages(
.replyTo = { .topicRootId = topMsgId },
.date = NewMessageDate(action.options),
.shortcutId = action.options.shortcutId,
.starsPaid = action.options.starsApproved,
.postAuthor = NewMessagePostAuthor(action),
// forwarded messages don't have effects
@ -3573,6 +3597,7 @@ void ApiWrap::sendSharedContact(
.replyTo = action.replyTo,
.date = NewMessageDate(action.options),
.shortcutId = action.options.shortcutId,
.starsPaid = action.options.starsApproved,
.postAuthor = NewMessagePostAuthor(action),
.effectId = action.options.effectId,
}, TextWithEntities(), MTP_messageMediaContact(
@ -3944,6 +3969,14 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
sendFlags |= MTPmessages_SendMessage::Flag::f_effect;
mediaFlags |= MTPmessages_SendMedia::Flag::f_effect;
}
const auto starsPaid = std::min(
peer->starsPerMessageChecked(),
action.options.starsApproved);
if (starsPaid) {
action.options.starsApproved -= starsPaid;
sendFlags |= MTPmessages_SendMessage::Flag::f_allow_paid_stars;
mediaFlags |= MTPmessages_SendMedia::Flag::f_allow_paid_stars;
}
lastMessage = history->addNewLocalMessage({
.id = newId.msg,
.flags = flags,
@ -3951,6 +3984,7 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
.replyTo = action.replyTo,
.date = NewMessageDate(action.options),
.shortcutId = action.options.shortcutId,
.starsPaid = starsPaid,
.postAuthor = NewMessagePostAuthor(action),
.effectId = action.options.effectId,
}, sending, media);
@ -4001,7 +4035,8 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
MTP_int(action.options.scheduled),
(sendAs ? sendAs->input : MTP_inputPeerEmpty()),
mtpShortcut,
MTP_long(action.options.effectId)
MTP_long(action.options.effectId),
MTP_long(starsPaid)
), done, fail);
} else {
histories.sendPreparedMessage(
@ -4019,7 +4054,8 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
MTP_int(action.options.scheduled),
(sendAs ? sendAs->input : MTP_inputPeerEmpty()),
mtpShortcut,
MTP_long(action.options.effectId)
MTP_long(action.options.effectId),
MTP_long(starsPaid)
), done, fail);
}
isFirst = false;
@ -4084,7 +4120,7 @@ void ApiWrap::sendBotStart(
void ApiWrap::sendInlineResult(
not_null<UserData*> bot,
not_null<InlineBots::Result*> data,
const SendAction &action,
SendAction action,
std::optional<MsgId> localMessageId,
Fn<void(bool)> done) {
sendAction(action);
@ -4124,6 +4160,13 @@ void ApiWrap::sendInlineResult(
if (action.options.hideViaBot) {
sendFlags |= SendFlag::f_hide_via;
}
const auto starsPaid = std::min(
peer->starsPerMessageChecked(),
action.options.starsApproved);
if (starsPaid) {
action.options.starsApproved -= starsPaid;
sendFlags |= SendFlag::f_allow_paid_stars;
}
const auto sendAs = action.options.sendAs;
if (sendAs) {
@ -4138,6 +4181,7 @@ void ApiWrap::sendInlineResult(
.replyTo = action.replyTo,
.date = NewMessageDate(action.options),
.shortcutId = action.options.shortcutId,
.starsPaid = starsPaid,
.viaBotId = ((bot && !action.options.hideViaBot)
? peerToUser(bot->id)
: UserId()),
@ -4161,7 +4205,8 @@ void ApiWrap::sendInlineResult(
MTP_string(data->getId()),
MTP_int(action.options.scheduled),
(sendAs ? sendAs->input : MTP_inputPeerEmpty()),
Data::ShortcutIdToMTP(_session, action.options.shortcutId)
Data::ShortcutIdToMTP(_session, action.options.shortcutId),
MTP_long(starsPaid)
), [=](const MTPUpdates &result, const MTP::Response &response) {
history->finishSavingCloudDraft(
topicRootId,
@ -4298,6 +4343,7 @@ void ApiWrap::sendMediaWithRandomId(
const auto history = item->history();
const auto replyTo = item->replyTo();
const auto peer = history->peer;
auto caption = item->originalText();
TextUtilities::Trim(caption);
@ -4307,6 +4353,12 @@ void ApiWrap::sendMediaWithRandomId(
Api::ConvertOption::SkipLocal);
const auto updateRecentStickers = Api::HasAttachedStickers(media);
const auto starsPaid = std::min(
peer->starsPerMessageChecked(),
options.starsApproved);
if (starsPaid) {
options.starsApproved -= starsPaid;
}
using Flag = MTPmessages_SendMedia::Flag;
const auto flags = Flag(0)
@ -4319,10 +4371,10 @@ void ApiWrap::sendMediaWithRandomId(
| (options.sendAs ? Flag::f_send_as : Flag(0))
| (options.shortcutId ? Flag::f_quick_reply_shortcut : Flag(0))
| (options.effectId ? Flag::f_effect : Flag(0))
| (options.invertCaption ? Flag::f_invert_media : Flag(0));
| (options.invertCaption ? Flag::f_invert_media : Flag(0))
| (starsPaid ? Flag::f_allow_paid_stars : Flag(0));
auto &histories = history->owner().histories();
const auto peer = history->peer;
const auto itemId = item->fullId();
histories.sendPreparedMessage(
history,
@ -4346,7 +4398,8 @@ void ApiWrap::sendMediaWithRandomId(
MTP_int(options.scheduled),
(options.sendAs ? options.sendAs->input : MTP_inputPeerEmpty()),
Data::ShortcutIdToMTP(_session, options.shortcutId),
MTP_long(options.effectId)
MTP_long(options.effectId),
MTP_long(starsPaid)
), [=](const MTPUpdates &result, const MTP::Response &response) {
if (done) done(true);
if (updateRecentStickers) {
@ -4367,7 +4420,7 @@ void ApiWrap::sendMultiPaidMedia(
Expects(album->options.price > 0);
const auto groupId = album->groupId;
const auto &options = album->options;
auto &options = album->options;
const auto randomId = album->items.front().randomId;
auto medias = album->items | ranges::view::transform([](
const SendingAlbum::Item &part) {
@ -4377,6 +4430,7 @@ void ApiWrap::sendMultiPaidMedia(
const auto history = item->history();
const auto replyTo = item->replyTo();
const auto peer = history->peer;
auto caption = item->originalText();
TextUtilities::Trim(caption);
@ -4384,6 +4438,12 @@ void ApiWrap::sendMultiPaidMedia(
_session,
caption.entities,
Api::ConvertOption::SkipLocal);
const auto starsPaid = std::min(
peer->starsPerMessageChecked(),
options.starsApproved);
if (starsPaid) {
options.starsApproved -= starsPaid;
}
using Flag = MTPmessages_SendMedia::Flag;
const auto flags = Flag(0)
@ -4396,10 +4456,10 @@ void ApiWrap::sendMultiPaidMedia(
| (options.sendAs ? Flag::f_send_as : Flag(0))
| (options.shortcutId ? Flag::f_quick_reply_shortcut : Flag(0))
| (options.effectId ? Flag::f_effect : Flag(0))
| (options.invertCaption ? Flag::f_invert_media : Flag(0));
| (options.invertCaption ? Flag::f_invert_media : Flag(0))
| (starsPaid ? Flag::f_allow_paid_stars : Flag(0));
auto &histories = history->owner().histories();
const auto peer = history->peer;
const auto itemId = item->fullId();
album->sent = true;
histories.sendPreparedMessage(
@ -4422,7 +4482,8 @@ void ApiWrap::sendMultiPaidMedia(
MTP_int(options.scheduled),
(options.sendAs ? options.sendAs->input : MTP_inputPeerEmpty()),
Data::ShortcutIdToMTP(_session, options.shortcutId),
MTP_long(options.effectId)
MTP_long(options.effectId),
MTP_long(starsPaid)
), [=](const MTPUpdates &result, const MTP::Response &response) {
if (const auto album = _sendingAlbums.take(groupId)) {
const auto copy = (*album)->items;
@ -4518,6 +4579,12 @@ void ApiWrap::sendAlbumIfReady(not_null<SendingAlbum*> album) {
const auto history = sample->history();
const auto replyTo = sample->replyTo();
const auto sendAs = album->options.sendAs;
const auto starsPaid = std::min(
history->peer->starsPerMessageChecked() * int(medias.size()),
album->options.starsApproved);
if (starsPaid) {
album->options.starsApproved -= starsPaid;
}
using Flag = MTPmessages_SendMultiMedia::Flag;
const auto flags = Flag(0)
| (replyTo ? Flag::f_reply_to : Flag(0))
@ -4530,7 +4597,8 @@ void ApiWrap::sendAlbumIfReady(not_null<SendingAlbum*> album) {
? Flag::f_quick_reply_shortcut
: Flag(0))
| (album->options.effectId ? Flag::f_effect : Flag(0))
| (album->options.invertCaption ? Flag::f_invert_media : Flag(0));
| (album->options.invertCaption ? Flag::f_invert_media : Flag(0))
| (starsPaid ? Flag::f_allow_paid_stars : Flag(0));
auto &histories = history->owner().histories();
const auto peer = history->peer;
album->sent = true;
@ -4546,7 +4614,8 @@ void ApiWrap::sendAlbumIfReady(not_null<SendingAlbum*> album) {
MTP_int(album->options.scheduled),
(sendAs ? sendAs->input : MTP_inputPeerEmpty()),
Data::ShortcutIdToMTP(_session, album->options.shortcutId),
MTP_long(album->options.effectId)
MTP_long(album->options.effectId),
MTP_long(starsPaid)
), [=](const MTPUpdates &result, const MTP::Response &response) {
_sendingAlbums.remove(groupId);

View file

@ -306,7 +306,7 @@ public:
void finishForwarding(const SendAction &action);
void forwardMessages(
Data::ResolvedForwardDraft &&draft,
const SendAction &action,
SendAction action,
FnMut<void()> &&successCallback = nullptr);
void shareContact(
const QString &phone,
@ -368,7 +368,7 @@ public:
void sendInlineResult(
not_null<UserData*> bot,
not_null<InlineBots::Result*> data,
const SendAction &action,
SendAction action,
std::optional<MsgId> localMessageId,
Fn<void(bool)> done = nullptr);
void sendMessageFail(

View file

@ -43,18 +43,20 @@ bool ResolveUser(
return true;
}
searchById(userId,
&controller->session(),
[=](const QString &title, UserData *data)
{
if (data) {
controller->showPeerInfo(data);
return;
}
searchById(
userId,
&controller->session(),
[=](const QString &title, UserData *data)
{
if (data) {
controller->showPeerInfo(data);
return;
}
Core::App().hideMediaView();
Ui::show(Ui::MakeInformBox(tr::ayu_UserNotFoundMessage()));
});
Core::App().hideMediaView();
Ui::show(Ui::MakeInformBox(tr::ayu_UserNotFoundMessage()));
}
);
return true;
}

File diff suppressed because it is too large Load diff

View file

@ -146,9 +146,9 @@ extern "C" {
** [sqlite3_libversion_number()], [sqlite3_sourceid()],
** [sqlite_version()] and [sqlite_source_id()].
*/
#define SQLITE_VERSION "3.48.0"
#define SQLITE_VERSION_NUMBER 3048000
#define SQLITE_SOURCE_ID "2025-01-14 11:05:00 d2fe6b05f38d9d7cd78c5d252e99ac59f1aea071d669830c1ffe4e8966e84010"
#define SQLITE_VERSION "3.49.1"
#define SQLITE_VERSION_NUMBER 3049001
#define SQLITE_SOURCE_ID "2025-02-18 13:38:58 873d4e274b4988d260ba8354a9718324a1c26187a4ab4c1cc0227c03d0f10e70"
/*
** CAPI3REF: Run-Time Library Version Numbers
@ -2211,7 +2211,15 @@ struct sqlite3_mem_methods {
** CAPI3REF: Database Connection Configuration Options
**
** These constants are the available integer configuration options that
** can be passed as the second argument to the [sqlite3_db_config()] interface.
** can be passed as the second parameter to the [sqlite3_db_config()] interface.
**
** The [sqlite3_db_config()] interface is a var-args functions. It takes a
** variable number of parameters, though always at least two. The number of
** parameters passed into sqlite3_db_config() depends on which of these
** constants is given as the second parameter. This documentation page
** refers to parameters beyond the second as "arguments". Thus, when this
** page says "the N-th argument" it means "the N-th parameter past the
** configuration option" or "the (N+2)-th parameter to sqlite3_db_config()".
**
** New configuration options may be added in future releases of SQLite.
** Existing configuration options might be discontinued. Applications
@ -2223,8 +2231,14 @@ struct sqlite3_mem_methods {
** <dl>
** [[SQLITE_DBCONFIG_LOOKASIDE]]
** <dt>SQLITE_DBCONFIG_LOOKASIDE</dt>
** <dd> ^This option takes three additional arguments that determine the
** [lookaside memory allocator] configuration for the [database connection].
** <dd> The SQLITE_DBCONFIG_LOOKASIDE option is used to adjust the
** configuration of the lookaside memory allocator within a database
** connection.
** The arguments to the SQLITE_DBCONFIG_LOOKASIDE option are <i>not</i>
** in the [DBCONFIG arguments|usual format].
** The SQLITE_DBCONFIG_LOOKASIDE option takes three arguments, not two,
** so that a call to [sqlite3_db_config()] that uses SQLITE_DBCONFIG_LOOKASIDE
** should have a total of five parameters.
** ^The first argument (the third parameter to [sqlite3_db_config()] is a
** pointer to a memory buffer to use for lookaside memory.
** ^The first argument after the SQLITE_DBCONFIG_LOOKASIDE verb
@ -2247,7 +2261,8 @@ struct sqlite3_mem_methods {
** [[SQLITE_DBCONFIG_ENABLE_FKEY]]
** <dt>SQLITE_DBCONFIG_ENABLE_FKEY</dt>
** <dd> ^This option is used to enable or disable the enforcement of
** [foreign key constraints]. There should be two additional arguments.
** [foreign key constraints]. This is the same setting that is
** enabled or disabled by the [PRAGMA foreign_keys] statement.
** The first argument is an integer which is 0 to disable FK enforcement,
** positive to enable FK enforcement or negative to leave FK enforcement
** unchanged. The second parameter is a pointer to an integer into which
@ -2269,13 +2284,13 @@ struct sqlite3_mem_methods {
** <p>Originally this option disabled all triggers. ^(However, since
** SQLite version 3.35.0, TEMP triggers are still allowed even if
** this option is off. So, in other words, this option now only disables
** triggers in the main database schema or in the schemas of ATTACH-ed
** triggers in the main database schema or in the schemas of [ATTACH]-ed
** databases.)^ </dd>
**
** [[SQLITE_DBCONFIG_ENABLE_VIEW]]
** <dt>SQLITE_DBCONFIG_ENABLE_VIEW</dt>
** <dd> ^This option is used to enable or disable [CREATE VIEW | views].
** There should be two additional arguments.
** There must be two additional arguments.
** The first argument is an integer which is 0 to disable views,
** positive to enable views or negative to leave the setting unchanged.
** The second parameter is a pointer to an integer into which
@ -2294,7 +2309,7 @@ struct sqlite3_mem_methods {
** <dd> ^This option is used to enable or disable the
** [fts3_tokenizer()] function which is part of the
** [FTS3] full-text search engine extension.
** There should be two additional arguments.
** There must be two additional arguments.
** The first argument is an integer which is 0 to disable fts3_tokenizer() or
** positive to enable fts3_tokenizer() or negative to leave the setting
** unchanged.
@ -2309,7 +2324,7 @@ struct sqlite3_mem_methods {
** interface independently of the [load_extension()] SQL function.
** The [sqlite3_enable_load_extension()] API enables or disables both the
** C-API [sqlite3_load_extension()] and the SQL function [load_extension()].
** There should be two additional arguments.
** There must be two additional arguments.
** When the first argument to this interface is 1, then only the C-API is
** enabled and the SQL function remains disabled. If the first argument to
** this interface is 0, then both the C-API and the SQL function are disabled.
@ -2323,23 +2338,30 @@ struct sqlite3_mem_methods {
**
** [[SQLITE_DBCONFIG_MAINDBNAME]] <dt>SQLITE_DBCONFIG_MAINDBNAME</dt>
** <dd> ^This option is used to change the name of the "main" database
** schema. ^The sole argument is a pointer to a constant UTF8 string
** which will become the new schema name in place of "main". ^SQLite
** does not make a copy of the new main schema name string, so the application
** must ensure that the argument passed into this DBCONFIG option is unchanged
** until after the database connection closes.
** schema. This option does not follow the
** [DBCONFIG arguments|usual SQLITE_DBCONFIG argument format].
** This option takes exactly one additional argument so that the
** [sqlite3_db_config()] call has a total of three parameters. The
** extra argument must be a pointer to a constant UTF8 string which
** will become the new schema name in place of "main". ^SQLite does
** not make a copy of the new main schema name string, so the application
** must ensure that the argument passed into SQLITE_DBCONFIG MAINDBNAME
** is unchanged until after the database connection closes.
** </dd>
**
** [[SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE]]
** <dt>SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE</dt>
** <dd> Usually, when a database in wal mode is closed or detached from a
** database handle, SQLite checks if this will mean that there are now no
** connections at all to the database. If so, it performs a checkpoint
** operation before closing the connection. This option may be used to
** override this behavior. The first parameter passed to this operation
** is an integer - positive to disable checkpoints-on-close, or zero (the
** default) to enable them, and negative to leave the setting unchanged.
** The second parameter is a pointer to an integer
** <dd> Usually, when a database in [WAL mode] is closed or detached from a
** database handle, SQLite checks if if there are other connections to the
** same database, and if there are no other database connection (if the
** connection being closed is the last open connection to the database),
** then SQLite performs a [checkpoint] before closing the connection and
** deletes the WAL file. The SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE option can
** be used to override that behavior. The first argument passed to this
** operation (the third parameter to [sqlite3_db_config()]) is an integer
** which is positive to disable checkpoints-on-close, or zero (the default)
** to enable them, and negative to leave the setting unchanged.
** The second argument (the fourth parameter) is a pointer to an integer
** into which is written 0 or 1 to indicate whether checkpoints-on-close
** have been disabled - 0 if they are not disabled, 1 if they are.
** </dd>
@ -2500,7 +2522,7 @@ struct sqlite3_mem_methods {
** statistics. For statistics to be collected, the flag must be set on
** the database handle both when the SQL statement is prepared and when it
** is stepped. The flag is set (collection of statistics is enabled)
** by default. This option takes two arguments: an integer and a pointer to
** by default. <p>This option takes two arguments: an integer and a pointer to
** an integer.. The first argument is 1, 0, or -1 to enable, disable, or
** leave unchanged the statement scanstatus option. If the second argument
** is not NULL, then the value of the statement scanstatus setting after
@ -2514,7 +2536,7 @@ struct sqlite3_mem_methods {
** in which tables and indexes are scanned so that the scans start at the end
** and work toward the beginning rather than starting at the beginning and
** working toward the end. Setting SQLITE_DBCONFIG_REVERSE_SCANORDER is the
** same as setting [PRAGMA reverse_unordered_selects]. This option takes
** same as setting [PRAGMA reverse_unordered_selects]. <p>This option takes
** two arguments which are an integer and a pointer to an integer. The first
** argument is 1, 0, or -1 to enable, disable, or leave unchanged the
** reverse scan order flag, respectively. If the second argument is not NULL,
@ -2523,7 +2545,76 @@ struct sqlite3_mem_methods {
** first argument.
** </dd>
**
** [[SQLITE_DBCONFIG_ENABLE_ATTACH_CREATE]]
** <dt>SQLITE_DBCONFIG_ENABLE_ATTACH_CREATE</dt>
** <dd>The SQLITE_DBCONFIG_ENABLE_ATTACH_CREATE option enables or disables
** the ability of the [ATTACH DATABASE] SQL command to create a new database
** file if the database filed named in the ATTACH command does not already
** exist. This ability of ATTACH to create a new database is enabled by
** default. Applications can disable or reenable the ability for ATTACH to
** create new database files using this DBCONFIG option.<p>
** This option takes two arguments which are an integer and a pointer
** to an integer. The first argument is 1, 0, or -1 to enable, disable, or
** leave unchanged the attach-create flag, respectively. If the second
** argument is not NULL, then 0 or 1 is written into the integer that the
** second argument points to depending on if the attach-create flag is set
** after processing the first argument.
** </dd>
**
** [[SQLITE_DBCONFIG_ENABLE_ATTACH_WRITE]]
** <dt>SQLITE_DBCONFIG_ENABLE_ATTACH_WRITE</dt>
** <dd>The SQLITE_DBCONFIG_ENABLE_ATTACH_WRITE option enables or disables the
** ability of the [ATTACH DATABASE] SQL command to open a database for writing.
** This capability is enabled by default. Applications can disable or
** reenable this capability using the current DBCONFIG option. If the
** the this capability is disabled, the [ATTACH] command will still work,
** but the database will be opened read-only. If this option is disabled,
** then the ability to create a new database using [ATTACH] is also disabled,
** regardless of the value of the [SQLITE_DBCONFIG_ENABLE_ATTACH_CREATE]
** option.<p>
** This option takes two arguments which are an integer and a pointer
** to an integer. The first argument is 1, 0, or -1 to enable, disable, or
** leave unchanged the ability to ATTACH another database for writing,
** respectively. If the second argument is not NULL, then 0 or 1 is written
** into the integer to which the second argument points, depending on whether
** the ability to ATTACH a read/write database is enabled or disabled
** after processing the first argument.
** </dd>
**
** [[SQLITE_DBCONFIG_ENABLE_COMMENTS]]
** <dt>SQLITE_DBCONFIG_ENABLE_COMMENTS</dt>
** <dd>The SQLITE_DBCONFIG_ENABLE_COMMENTS option enables or disables the
** ability to include comments in SQL text. Comments are enabled by default.
** An application can disable or reenable comments in SQL text using this
** DBCONFIG option.<p>
** This option takes two arguments which are an integer and a pointer
** to an integer. The first argument is 1, 0, or -1 to enable, disable, or
** leave unchanged the ability to use comments in SQL text,
** respectively. If the second argument is not NULL, then 0 or 1 is written
** into the integer that the second argument points to depending on if
** comments are allowed in SQL text after processing the first argument.
** </dd>
**
** </dl>
**
** [[DBCONFIG arguments]] <h3>Arguments To SQLITE_DBCONFIG Options</h3>
**
** <p>Most of the SQLITE_DBCONFIG options take two arguments, so that the
** overall call to [sqlite3_db_config()] has a total of four parameters.
** The first argument (the third parameter to sqlite3_db_config()) is a integer.
** The second argument is a pointer to an integer. If the first argument is 1,
** then the option becomes enabled. If the first integer argument is 0, then the
** option is disabled. If the first argument is -1, then the option setting
** is unchanged. The second argument, the pointer to an integer, may be NULL.
** If the second argument is not NULL, then a value of 0 or 1 is written into
** the integer to which the second argument points, depending on whether the
** setting is disabled or enabled after applying any changes specified by
** the first argument.
**
** <p>While most SQLITE_DBCONFIG options use the argument format
** described in the previous paragraph, the [SQLITE_DBCONFIG_MAINDBNAME]
** and [SQLITE_DBCONFIG_LOOKASIDE] options are different. See the
** documentation of those exceptional options for details.
*/
#define SQLITE_DBCONFIG_MAINDBNAME 1000 /* const char* */
#define SQLITE_DBCONFIG_LOOKASIDE 1001 /* void* int int */
@ -2545,7 +2636,10 @@ struct sqlite3_mem_methods {
#define SQLITE_DBCONFIG_TRUSTED_SCHEMA 1017 /* int int* */
#define SQLITE_DBCONFIG_STMT_SCANSTATUS 1018 /* int int* */
#define SQLITE_DBCONFIG_REVERSE_SCANORDER 1019 /* int int* */
#define SQLITE_DBCONFIG_MAX 1019 /* Largest DBCONFIG */
#define SQLITE_DBCONFIG_ENABLE_ATTACH_CREATE 1020 /* int int* */
#define SQLITE_DBCONFIG_ENABLE_ATTACH_WRITE 1021 /* int int* */
#define SQLITE_DBCONFIG_ENABLE_COMMENTS 1022 /* int int* */
#define SQLITE_DBCONFIG_MAX 1022 /* Largest DBCONFIG */
/*
** CAPI3REF: Enable Or Disable Extended Result Codes
@ -10748,8 +10842,9 @@ SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_recover(sqlite3 *db, const c
/*
** CAPI3REF: Serialize a database
**
** The sqlite3_serialize(D,S,P,F) interface returns a pointer to memory
** that is a serialization of the S database on [database connection] D.
** The sqlite3_serialize(D,S,P,F) interface returns a pointer to
** memory that is a serialization of the S database on
** [database connection] D. If S is a NULL pointer, the main database is used.
** If P is not a NULL pointer, then the size of the database in bytes
** is written into *P.
**

File diff suppressed because it is too large Load diff

View file

@ -34,6 +34,7 @@
#include "data/data_session.h"
#include "history/view/history_view_context_menu.h"
#include "history/view/history_view_element.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
namespace AyuUi {
@ -58,13 +59,14 @@ void AddDeletedMessagesActions(PeerData *peerData,
return;
}
addCallback(tr::ayu_ViewDeletedMenuText(tr::now),
[=]
{
sessionController->session().tryResolveWindow()
->showSection(std::make_shared<MessageHistory::SectionMemento>(peerData, nullptr, topicId));
},
&st::menuIconArchive);
addCallback(
tr::ayu_ViewDeletedMenuText(tr::now),
[=]
{
sessionController->session().tryResolveWindow()
->showSection(std::make_shared<MessageHistory::SectionMemento>(peerData, nullptr, topicId));
},
&st::menuIconArchive);
}
void AddJumpToBeginningAction(PeerData *peerData,
@ -147,16 +149,38 @@ void AddJumpToBeginningAction(PeerData *peerData,
&st::ayuMenuIconToBeginning);
}
void AddOpenChannelAction(PeerData *peerData,
not_null<Window::SessionController*> sessionController,
const Window::PeerMenuCallback &addCallback) {
if (!peerData || !peerData->isMegagroup()) {
return;
}
const auto chat = peerData->asMegagroup()->linkedChat();
if (!chat) {
return;
}
addCallback(
tr::lng_context_open_channel(tr::now),
[=]
{
sessionController->showPeerHistory(chat, Window::SectionShow::Way::Forward);
},
&st::menuIconChannel);
}
void AddHistoryAction(not_null<Ui::PopupMenu*> menu, HistoryItem *item) {
if (AyuMessages::hasRevisions(item)) {
menu->addAction(tr::ayu_EditsHistoryMenuText(tr::now),
[=]
{
item->history()->session().tryResolveWindow()
->showSection(
std::make_shared<MessageHistory::SectionMemento>(item->history()->peer, item, 0));
},
&st::ayuEditsHistoryIcon);
menu->addAction(
tr::ayu_EditsHistoryMenuText(tr::now),
[=]
{
item->history()->session().tryResolveWindow()
->showSection(
std::make_shared<MessageHistory::SectionMemento>(item->history()->peer, item, 0));
},
&st::ayuEditsHistoryIcon);
}
}
@ -171,15 +195,16 @@ void AddHideMessageAction(not_null<Ui::PopupMenu*> menu, HistoryItem *item) {
}
const auto history = item->history();
menu->addAction(tr::ayu_ContextHideMessage(tr::now),
[=]()
{
item->destroy();
history->requestChatListMessage();
menu->addAction(
tr::ayu_ContextHideMessage(tr::now),
[=]()
{
item->destroy();
history->requestChatListMessage();
AyuState::hide(item);
},
&st::menuIconClear);
AyuState::hide(item);
},
&st::menuIconClear);
}
void AddUserMessagesAction(not_null<Ui::PopupMenu*> menu, HistoryItem *item) {
@ -432,31 +457,77 @@ void AddReadUntilAction(not_null<Ui::PopupMenu*> menu, HistoryItem *item) {
return;
}
menu->addAction(tr::ayu_ReadUntilMenuText(tr::now),
[=]()
menu->addAction(
tr::ayu_ReadUntilMenuText(tr::now),
[=]()
{
readHistory(item);
if (item->media() && item->media()->ttlSeconds() <= 0 && item->unsupportedTTL() <= 0 && !item->out() && item
->isUnreadMedia()) {
const auto ids = MTP_vector<MTPint>(1, MTP_int(item->id));
if (const auto channel = item->history()->peer->asChannel()) {
item->history()->session().api().request(MTPchannels_ReadMessageContents(
channel->inputChannel,
ids
)).send();
} else {
item->history()->session().api().request(MTPmessages_ReadMessageContents(
ids
)).done([=](const MTPmessages_AffectedMessages &result)
{
readHistory(item);
if (item->media() && !item->media()->ttlSeconds()) {
const auto ids = MTP_vector<MTPint>(1, MTP_int(item->id));
if (const auto channel = item->history()->peer->asChannel()) {
item->history()->session().api().request(MTPchannels_ReadMessageContents(
channel->inputChannel,
ids
)).send();
} else {
item->history()->session().api().request(MTPmessages_ReadMessageContents(
ids
)).done([=](const MTPmessages_AffectedMessages &result)
{
item->history()->session().api().applyAffectedMessages(
item->history()->peer,
result);
}).send();
}
item->markContentsRead();
}
},
&st::menuIconShowInChat);
item->history()->session().api().applyAffectedMessages(
item->history()->peer,
result);
}).send();
}
item->markContentsRead();
}
},
&st::menuIconShowInChat);
}
void AddBurnAction(not_null<Ui::PopupMenu*> menu, HistoryItem *item) {
if (!item->media() || item->media()->ttlSeconds() <= 0 && item->unsupportedTTL() <= 0 || item->out() ||
!item->isUnreadMedia()) {
return;
}
menu->addAction(
tr::ayu_ExpireMediaContextMenuText(tr::now),
[=]()
{
const auto ids = MTP_vector<MTPint>(1, MTP_int(item->id));
const auto callback = [=]()
{
if (const auto window = Core::App().activeWindow()) {
if (const auto controller = window->sessionController()) {
controller->showToast(tr::lng_box_ok(tr::now));
}
}
};
if (const auto channel = item->history()->peer->asChannel()) {
item->history()->session().api().request(MTPchannels_ReadMessageContents(
channel->inputChannel,
ids
)).done([=]()
{
callback();
}).send();
} else {
item->history()->session().api().request(MTPmessages_ReadMessageContents(
ids
)).done([=](const MTPmessages_AffectedMessages &result)
{
item->history()->session().api().applyAffectedMessages(
item->history()->peer,
result);
callback();
}).send();
}
item->markContentsRead();
},
&st::menuIconTTLAny);
}
} // namespace AyuUi

View file

@ -25,10 +25,15 @@ void AddJumpToBeginningAction(PeerData *peerData,
not_null<Window::SessionController*> sessionController,
const Window::PeerMenuCallback &addCallback);
void AddOpenChannelAction(PeerData *peerData,
not_null<Window::SessionController*> sessionController,
const Window::PeerMenuCallback &addCallback);
void AddHistoryAction(not_null<Ui::PopupMenu*> menu, HistoryItem *item);
void AddHideMessageAction(not_null<Ui::PopupMenu*> menu, HistoryItem *item);
void AddUserMessagesAction(not_null<Ui::PopupMenu*> menu, HistoryItem *item);
void AddMessageDetailsAction(not_null<Ui::PopupMenu*> menu, HistoryItem *item);
void AddReadUntilAction(not_null<Ui::PopupMenu*> menu, HistoryItem *item);
void AddBurnAction(not_null<Ui::PopupMenu*> menu, HistoryItem *item);
}

View file

@ -218,59 +218,73 @@ ActionStickerPackAuthor::ActionStickerPackAuthor(not_null<Menu::Menu*> menu,
}
void ActionStickerPackAuthor::searchAuthor(ID authorId) {
const auto pointer = Ui::MakeWeak(this);
searchById(authorId,
_session,
[=](const QString &username, UserData *user)
{
if (!pointer) {
LOG(("ContextActionStickerAuthor: searchById callback after destruction"));
return;
}
if (username.isEmpty() && !user) {
_subText = QString(tr::ayu_MessageDetailsPackOwnerNotFoundPC(tr::now));
setClickedCallback(
[=]
{
QGuiApplication::clipboard()->setText(QString::number(authorId));
if (const auto window = _session->tryResolveWindow()) {
if (const auto mainWidget = window->widget()->sessionController()) {
mainWidget->showToast(tr::ayu_IDCopiedToast(tr::now));
}
}
});
const auto session = _session;
const auto weak = Ui::MakeWeak(this);
crl::on_main(
[=]
{
update();
});
return;
}
searchById(
authorId,
session,
[session, weak, authorId](const QString &username, UserData *user)
{
if (!weak) {
LOG(("ContextActionStickerAuthor: searchById callback after destruction"));
return;
}
const auto title = username.isEmpty() ? user ? user->name() : QString() : username;
const auto callback = [=]
{
if (user) {
if (const auto window = _session->tryResolveWindow()) {
if (const auto mainWidget = window->widget()->sessionController()) {
mainWidget->showPeer(user);
}
}
} else {
QGuiApplication::clipboard()->setText(title);
}
};
const auto strong = weak.data();
if (!strong) {
LOG(("ContextActionStickerAuthor: weak.data() returned null"));
return;
}
setClickedCallback(callback);
if (username.isEmpty() && !user) {
strong->_subText = QString(tr::ayu_MessageDetailsPackOwnerNotFoundPC(tr::now));
strong->setClickedCallback(
[authorId, session]
{
QGuiApplication::clipboard()->setText(QString::number(authorId));
if (const auto window = session->tryResolveWindow()) {
if (const auto mainWidget = window->widget()->sessionController()) {
mainWidget->showToast(tr::ayu_IDCopiedToast(tr::now));
}
}
});
_subText = QString(title);
crl::on_main(
[=]
{
update();
});
});
crl::on_main(
[weak]
{
if (const auto strongInner = weak.data()) {
strongInner->update();
}
});
return;
}
const auto title = username.isEmpty() ? (user ? user->name() : QString()) : username;
const auto callback = [user, title, session]
{
if (user) {
if (const auto window = session->tryResolveWindow()) {
if (const auto mainWidget = window->widget()->sessionController()) {
mainWidget->showPeer(user);
}
}
} else {
QGuiApplication::clipboard()->setText(title);
}
};
strong->setClickedCallback(callback);
strong->_subText = QString(title);
crl::on_main(
[weak]
{
if (const auto strongInner = weak.data()) {
strongInner->update();
}
});
}
);
}
} // namespace

View file

@ -1078,7 +1078,9 @@ void BackgroundPreviewBox::updateServiceBg(const std::vector<QColor> &bg) {
? tr::lng_background_other_group(tr::now)
: forChannel()
? tr::lng_background_other_channel(tr::now)
: (_forPeer && !_fromMessageId)
: (_forPeer
&& !_fromMessageId
&& !_forPeer->starsPerMessageChecked())
? tr::lng_background_other_info(
tr::now,
lt_user,

View file

@ -1122,3 +1122,10 @@ profileQrBackgroundRadius: 12px;
profileQrIcon: icon{{ "qr_mini", windowActiveTextFg }};
profileQrBackgroundMargins: margins(36px, 12px, 36px, 12px);
profileQrBackgroundPadding: margins(0px, 24px, 0px, 24px);
foldersMenu: PopupMenu(popupMenuWithIcons) {
maxHeight: 320px;
menu: Menu(menuWithIcons) {
itemPadding: margins(54px, 8px, 44px, 8px);
}
}

View file

@ -172,13 +172,6 @@ void ChangeFilterById(
const auto account = not_null(&history->session().account());
if (const auto controller = Core::App().windowFor(account)) {
const auto isStatic = name.isStatic;
const auto textContext = [=](not_null<QWidget*> widget) {
return Core::MarkedTextContext{
.session = &history->session(),
.customEmojiRepaint = [=] { widget->update(); },
.customEmojiLoopLimit = isStatic ? -1 : 0,
};
};
controller->showToast({
.text = (add
? tr::lng_filters_toast_add
@ -189,7 +182,10 @@ void ChangeFilterById(
lt_folder,
Ui::Text::Wrapped(name.text, EntityType::Bold),
Ui::Text::WithEntities),
.textContext = textContext,
.textContext = Core::TextContext({
.session = &history->session(),
.customEmojiLoopLimit = isStatic ? -1 : 0,
}),
});
}
}).fail([=](const MTP::Error &error) {
@ -290,19 +286,17 @@ void FillChooseFilterMenu(
const auto title = filter.title();
auto item = base::make_unique_q<FilterAction>(
menu.get(),
st::foldersMenu,
menu->st().menu,
Ui::Menu::CreateAction(
menu.get(),
Ui::Text::FixAmpersandInAction(title.text.text),
std::move(callback)),
contains ? &st::mediaPlayerMenuCheck : nullptr,
contains ? &st::mediaPlayerMenuCheck : nullptr);
const auto context = Core::MarkedTextContext{
item->setMarkedText(title.text, QString(), Core::TextContext({
.session = &history->session(),
.customEmojiRepaint = [raw = item.get()] { raw->update(); },
.customEmojiLoopLimit = title.isStatic ? -1 : 0,
};
item->setMarkedText(title.text, QString(), context);
}));
item->setIcon(Icon(showColors ? filter : filter.withColorIndex({})));
const auto action = menu->addAction(std::move(item));

View file

@ -817,13 +817,15 @@ CreatePollBox::CreatePollBox(
not_null<Window::SessionController*> controller,
PollData::Flags chosen,
PollData::Flags disabled,
rpl::producer<int> starsRequired,
Api::SendType sendType,
SendMenu::Details sendMenuDetails)
: _controller(controller)
, _chosen(chosen)
, _disabled(disabled)
, _sendType(sendType)
, _sendMenuDetails([result = sendMenuDetails] { return result; }) {
, _sendMenuDetails([result = sendMenuDetails] { return result; })
, _starsRequired(std::move(starsRequired)) {
}
rpl::producer<CreatePollBox::Result> CreatePollBox::submitRequests() const {
@ -1226,10 +1228,11 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
_sendMenuDetails());
};
const auto submit = addButton(
(isNormal
? tr::lng_polls_create_button()
: tr::lng_schedule_button()),
tr::lng_polls_create_button(),
[=] { isNormal ? send({}) : schedule(); });
submit->setText(PaidSendButtonText(_starsRequired.value(), isNormal
? tr::lng_polls_create_button()
: tr::lng_schedule_button()));
const auto sendMenuDetails = [=] {
collectError();
return (*error) ? SendMenu::Details() : _sendMenuDetails();

View file

@ -42,6 +42,7 @@ public:
not_null<Window::SessionController*> controller,
PollData::Flags chosen,
PollData::Flags disabled,
rpl::producer<int> starsRequired,
Api::SendType sendType,
SendMenu::Details sendMenuDetails);
@ -76,6 +77,7 @@ private:
const PollData::Flags _disabled = PollData::Flags();
const Api::SendType _sendType = Api::SendType();
const Fn<SendMenu::Details()> _sendMenuDetails;
rpl::variable<int> _starsRequired;
base::unique_qptr<ChatHelpers::TabbedPanel> _emojiPanel;
Fn<void()> _setInnerFocus;
Fn<rpl::producer<bool>()> _dataIsValidValue;

View file

@ -12,7 +12,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/effects/premium_graphics.h"
#include "ui/layers/generic_box.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/continuous_sliders.h"
#include "ui/widgets/shadow.h"
#include "ui/text/format_values.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
#include "ui/wrap/slide_wrap.h"
@ -21,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history.h"
#include "boxes/peer_list_controllers.h"
#include "settings/settings_premium.h"
#include "settings/settings_privacy_controllers.h"
#include "settings/settings_privacy_security.h"
#include "calls/calls_instance.h"
#include "lang/lang_keys.h"
@ -42,6 +45,8 @@ namespace {
constexpr auto kPremiumsRowId = PeerId(FakeChatId(BareId(1))).value;
constexpr auto kMiniAppsRowId = PeerId(FakeChatId(BareId(2))).value;
constexpr auto kStarsMin = 1;
constexpr auto kDefaultChargeStars = 10;
using Exceptions = Api::UserPrivacy::Exceptions;
@ -452,6 +457,143 @@ auto PrivacyExceptionsBoxController::createRow(not_null<History*> history)
return result;
}
[[nodiscard]] object_ptr<Ui::RpWidget> MakeChargeStarsSlider(
QWidget *parent,
not_null<const style::MediaSlider*> sliderStyle,
not_null<const style::FlatLabel*> labelStyle,
int valuesCount,
Fn<int(int)> valueByIndex,
int value,
int maxValue,
Fn<void(int)> valueProgress,
Fn<void(int)> valueFinished) {
auto result = object_ptr<Ui::VerticalLayout>(parent);
const auto raw = result.data();
const auto labels = raw->add(object_ptr<Ui::RpWidget>(raw));
const auto min = Ui::CreateChild<Ui::FlatLabel>(
raw,
QString::number(kStarsMin),
*labelStyle);
const auto max = Ui::CreateChild<Ui::FlatLabel>(
raw,
QString::number(maxValue),
*labelStyle);
const auto current = Ui::CreateChild<Ui::FlatLabel>(
raw,
QString::number(value),
*labelStyle);
min->setTextColorOverride(st::windowSubTextFg->c);
max->setTextColorOverride(st::windowSubTextFg->c);
const auto slider = raw->add(object_ptr<Ui::MediaSliderWheelless>(
raw,
*sliderStyle));
labels->resize(
labels->width(),
current->height() + st::defaultVerticalListSkip);
struct State {
int indexMin = 0;
int index = 0;
};
const auto state = raw->lifetime().make_state<State>();
const auto updateByIndex = [=] {
const auto outer = labels->width();
const auto minWidth = min->width();
const auto maxWidth = max->width();
const auto currentWidth = current->width();
if (minWidth + maxWidth + currentWidth > outer) {
return;
}
min->moveToLeft(0, 0, outer);
max->moveToRight(0, 0, outer);
current->moveToLeft((outer - current->width()) / 2, 0, outer);
};
const auto updateByValue = [=](int value) {
current->setText(
tr::lng_action_gift_for_stars(tr::now, lt_count, value));
state->index = 0;
auto maxIndex = valuesCount - 1;
while (state->index < maxIndex) {
const auto mid = (state->index + maxIndex) / 2;
const auto midValue = valueByIndex(mid);
if (midValue == value) {
state->index = mid;
break;
} else if (midValue < value) {
state->index = mid + 1;
} else {
maxIndex = mid - 1;
}
}
updateByIndex();
};
const auto progress = [=](int value) {
updateByValue(value);
valueProgress(value);
};
const auto finished = [=](int value) {
updateByValue(value);
valueFinished(value);
};
style::PaletteChanged() | rpl::start_with_next([=] {
min->setTextColorOverride(st::windowSubTextFg->c);
max->setTextColorOverride(st::windowSubTextFg->c);
}, raw->lifetime());
updateByValue(value);
state->indexMin = 0;
slider->setPseudoDiscrete(
valuesCount,
valueByIndex,
value,
progress,
finished,
state->indexMin);
slider->resize(slider->width(), sliderStyle->seekSize.height());
raw->widthValue() | rpl::start_with_next([=](int width) {
labels->resizeToWidth(width);
updateByIndex();
}, slider->lifetime());
return result;
}
void EditNoPaidMessagesExceptions(
not_null<Window::SessionController*> window,
const Api::UserPrivacy::Rule &value) {
auto controller = std::make_unique<PrivacyExceptionsBoxController>(
&window->session(),
tr::lng_messages_privacy_remove_fee(),
value.always,
std::optional<SpecialRowType>());
auto initBox = [=, controller = controller.get()](
not_null<PeerListBox*> box) {
box->addButton(tr::lng_settings_save(), [=] {
auto copy = value;
auto &setTo = copy.always;
setTo.peers = box->collectSelectedRows();
setTo.premiums = false;
setTo.miniapps = false;
auto &removeFrom = copy.never;
for (const auto peer : setTo.peers) {
removeFrom.peers.erase(
ranges::remove(removeFrom.peers, peer),
end(removeFrom.peers));
}
window->session().api().userPrivacy().save(
Api::UserPrivacy::Key::NoPaidMessages,
copy);
box->closeBox();
});
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
};
window->show(
Box<PeerListBox>(std::move(controller), std::move(initBox)));
}
} // namespace
bool EditPrivacyController::hasOption(Option option) const {
@ -812,19 +954,27 @@ void EditMessagesPrivacyBox(
constexpr auto kOptionAll = 0;
constexpr auto kOptionPremium = 1;
constexpr auto kOptionCharge = 2;
const auto session = &controller->session();
const auto allowed = [=] {
return controller->session().premium()
|| controller->session().appConfig().newRequirePremiumFree();
return session->premium()
|| session->appConfig().newRequirePremiumFree();
};
const auto privacy = &controller->session().api().globalPrivacy();
const auto privacy = &session->api().globalPrivacy();
const auto inner = box->verticalLayout();
inner->add(object_ptr<Ui::PlainShadow>(box));
Ui::AddSkip(inner, st::messagePrivacyTopSkip);
Ui::AddSubsectionTitle(inner, tr::lng_messages_privacy_subtitle());
const auto group = std::make_shared<Ui::RadiobuttonGroup>(
privacy->newRequirePremiumCurrent() ? kOptionPremium : kOptionAll);
(!allowed()
? kOptionAll
: privacy->newRequirePremiumCurrent()
? kOptionPremium
: privacy->newChargeStarsCurrent()
? kOptionCharge
: kOptionAll));
inner->add(
object_ptr<Ui::Radiobutton>(
inner,
@ -846,6 +996,92 @@ void EditMessagesPrivacyBox(
0,
st::messagePrivacyBottomSkip));
Ui::AddDividerText(inner, tr::lng_messages_privacy_about());
const auto available = session->appConfig().paidMessagesAvailable();
const auto charged = available
? inner->add(
object_ptr<Ui::Radiobutton>(
inner,
group,
kOptionCharge,
tr::lng_messages_privacy_charge(tr::now),
st::messagePrivacyCheck),
st::settingsSendTypePadding + style::margins(
0,
st::messagePrivacyBottomSkip,
0,
st::messagePrivacyBottomSkip))
: nullptr;
struct State {
rpl::variable<int> stars;
};
const auto state = std::make_shared<State>();
const auto savedValue = privacy->newChargeStarsCurrent();
if (available) {
Ui::AddDividerText(inner, tr::lng_messages_privacy_charge_about());
const auto chargeWrap = inner->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
inner,
object_ptr<Ui::VerticalLayout>(inner)));
const auto chargeInner = chargeWrap->entity();
Ui::AddSkip(chargeInner);
state->stars = SetupChargeSlider(
chargeInner,
session->user(),
savedValue);
Ui::AddSkip(chargeInner);
Ui::AddSubsectionTitle(
chargeInner,
tr::lng_messages_privacy_exceptions());
const auto key = Api::UserPrivacy::Key::NoPaidMessages;
session->api().userPrivacy().reload(key);
auto label = session->api().userPrivacy().value(
key
) | rpl::map([=](const Api::UserPrivacy::Rule &value) {
using namespace Settings;
const auto always = ExceptionUsersCount(value.always.peers);
return always
? tr::lng_edit_privacy_exceptions_count(
tr::now,
lt_count,
always)
: QString();
});
const auto exceptions = Settings::AddButtonWithLabel(
chargeInner,
tr::lng_messages_privacy_remove_fee(),
std::move(label),
st::settingsButtonNoIcon);
const auto shower = exceptions->lifetime().make_state<rpl::lifetime>();
exceptions->setClickedCallback([=] {
*shower = session->api().userPrivacy().value(
key
) | rpl::take(
1
) | rpl::start_with_next([=](const Api::UserPrivacy::Rule &value) {
EditNoPaidMessagesExceptions(controller, value);
});
});
Ui::AddSkip(chargeInner);
Ui::AddDividerText(
chargeInner,
tr::lng_messages_privacy_remove_about());
using namespace rpl::mappers;
chargeWrap->toggleOn(group->value() | rpl::map(_1 == kOptionCharge));
chargeWrap->finishAnimating();
}
using WeakToast = base::weak_ptr<Ui::Toast::Instance>;
const auto toast = std::make_shared<WeakToast>();
const auto showToast = [=] {
@ -875,19 +1111,20 @@ void EditMessagesPrivacyBox(
}),
});
};
if (!allowed()) {
CreateRadiobuttonLock(restricted, st::messagePrivacyCheck);
if (charged) {
CreateRadiobuttonLock(charged, st::messagePrivacyCheck);
}
group->setChangedCallback([=](int value) {
if (value == kOptionPremium) {
if (value == kOptionPremium || value == kOptionCharge) {
group->setValue(kOptionAll);
showToast();
}
});
}
Ui::AddDividerText(inner, tr::lng_messages_privacy_about());
if (!allowed()) {
Ui::AddSkip(inner);
Settings::AddButtonWithIcon(
inner,
@ -907,8 +1144,12 @@ void EditMessagesPrivacyBox(
} else {
box->addButton(tr::lng_settings_save(), [=] {
if (allowed()) {
privacy->updateNewRequirePremium(
group->current() == kOptionPremium);
const auto value = group->current();
const auto premiumRequired = (value == kOptionPremium);
const auto chargeStars = (value == kOptionCharge)
? state->stars.current()
: 0;
privacy->updateMessagesPrivacy(premiumRequired, chargeStars);
box->closeBox();
} else {
showToast();
@ -919,3 +1160,78 @@ void EditMessagesPrivacyBox(
});
}
}
rpl::producer<int> SetupChargeSlider(
not_null<Ui::VerticalLayout*> container,
not_null<PeerData*> peer,
int savedValue) {
struct State {
rpl::variable<int> stars;
};
const auto group = !peer->isUser();
const auto state = container->lifetime().make_state<State>();
const auto chargeStars = savedValue ? savedValue : kDefaultChargeStars;
state->stars = chargeStars;
Ui::AddSubsectionTitle(container, group
? tr::lng_rights_charge_price()
: tr::lng_messages_privacy_price());
auto values = std::vector<int>();
const auto maxStars = peer->session().appConfig().paidMessageStarsMax();
if (chargeStars < kStarsMin) {
values.push_back(chargeStars);
}
for (auto i = kStarsMin; i < std::min(100, maxStars); ++i) {
values.push_back(i);
}
for (auto i = 100; i < std::min(1000, maxStars); i += 10) {
if (i < chargeStars + 10 && chargeStars < i) {
values.push_back(chargeStars);
}
values.push_back(i);
}
for (auto i = 1000; i < maxStars + 1; i += 100) {
if (i < chargeStars + 100 && chargeStars < i) {
values.push_back(chargeStars);
}
values.push_back(i);
}
const auto valuesCount = int(values.size());
const auto setStars = [=](int value) {
state->stars = value;
};
container->add(
MakeChargeStarsSlider(
container,
&st::settingsScale,
&st::settingsScaleLabel,
valuesCount,
[=](int index) { return values[index]; },
chargeStars,
maxStars,
setStars,
setStars),
st::boxRowPadding);
const auto skip = 2 * st::defaultVerticalListSkip;
Ui::AddSkip(container, skip);
auto dollars = state->stars.value() | rpl::map([=](int stars) {
const auto ratio = peer->session().appConfig().starsWithdrawRate();
const auto dollars = int(base::SafeRound(stars * ratio));
return '~' + Ui::FillAmountAndCurrency(dollars, u"USD"_q);
});
const auto percent = peer->session().appConfig().paidMessageCommission();
Ui::AddDividerText(
container,
(group
? tr::lng_rights_charge_price_about
: tr::lng_messages_privacy_price_about)(
lt_percent,
rpl::single(QString::number(percent / 10.) + '%'),
lt_amount,
std::move(dollars)));
return state->stars.value();
}

View file

@ -169,3 +169,8 @@ private:
void EditMessagesPrivacyBox(
not_null<Ui::GenericBox*> box,
not_null<Window::SessionController*> controller);
[[nodiscard]] rpl::producer<int> SetupChargeSlider(
not_null<Ui::VerticalLayout*> container,
not_null<PeerData*> peer,
int savedValue);

View file

@ -441,13 +441,10 @@ void EditFilterBox(
using namespace Window;
return window->isGifPausedAtLeastFor(GifPauseReason::Layer);
};
name->setCustomTextContext([=](Fn<void()> repaint) {
return std::any(Core::MarkedTextContext{
.session = session,
.customEmojiRepaint = std::move(repaint),
.customEmojiLoopLimit = value ? -1 : 0,
});
}, [paused] {
name->setCustomTextContext(Core::TextContext({
.session = session,
.customEmojiLoopLimit = value ? -1 : 0,
}), [paused] {
return On(PowerSaving::kEmojiChat) || paused();
}, [paused] {
return On(PowerSaving::kChatSpoiler) || paused();
@ -609,10 +606,7 @@ void EditFilterBox(
float64 alpha = 1.;
};
const auto tag = preview->lifetime().make_state<TagState>();
tag->context.textContext = Core::MarkedTextContext{
.session = session,
.customEmojiRepaint = [] {},
};
tag->context.textContext = Core::TextContext({ .session = session });
preview->paintRequest() | rpl::start_with_next([=] {
auto p = QPainter(preview);
p.setOpacity(tag->alpha);

View file

@ -163,10 +163,10 @@ ExceptionRow::ExceptionRow(
st::defaultTextStyle,
filters,
kMarkupTextOptions,
Core::MarkedTextContext{
Core::TextContext({
.session = &history->session(),
.customEmojiRepaint = repaint,
});
.repaint = repaint,
}));
} else if (peer()->isSelf()) {
setCustomStatus(tr::lng_saved_forward_here(tr::now));
}

View file

@ -537,13 +537,6 @@ void LinkController::addHeader(not_null<Ui::VerticalLayout*> container) {
verticalLayout->add(std::move(icon.widget));
const auto isStatic = _filterTitle.isStatic;
const auto makeContext = [=](Fn<void()> update) {
return Core::MarkedTextContext{
.session = &_window->session(),
.customEmojiRepaint = update,
.customEmojiLoopLimit = isStatic ? -1 : 0,
};
};
verticalLayout->add(
object_ptr<Ui::CenterWrap<>>(
verticalLayout,
@ -559,7 +552,10 @@ void LinkController::addHeader(not_null<Ui::VerticalLayout*> container) {
Ui::Text::WithEntities)),
st::settingsFilterDividerLabel,
st::defaultPopupMenu,
makeContext)),
Core::TextContext({
.session = &_window->session(),
.customEmojiLoopLimit = isStatic ? -1 : 0,
}))),
st::filterLinkDividerLabelPadding);
verticalLayout->geometryValue(

View file

@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_credits.h"
#include "boxes/peer_list_controllers.h"
#include "core/ui_integration.h" // TextContext.
#include "data/data_peer.h"
#include "data/data_session.h"
#include "data/data_user.h"
@ -67,14 +68,9 @@ void GiftCreditsBox(
2.);
{
Ui::AddSkip(content);
const auto arrow = Ui::Text::SingleCustomEmoji(
peer->owner().customEmojiManager().registerInternalEmoji(
st::topicButtonArrow,
st::channelEarnLearnArrowMargins,
true));
auto link = tr::lng_credits_box_history_entry_gift_about_link(
lt_emoji,
rpl::single(arrow),
rpl::single(Ui::Text::IconEmoji(&st::textMoreIconEmoji)),
Ui::Text::RichLangValue
) | rpl::map([](TextWithEntities text) {
return Ui::Text::Link(
@ -92,7 +88,7 @@ void GiftCreditsBox(
lt_link,
std::move(link),
Ui::Text::RichLangValue),
{ .session = &peer->session() },
Core::TextContext({ .session = &peer->session() }),
st::creditsBoxAbout)),
st::boxRowPadding);
}

View file

@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/premium_preview_box.h" // ShowPremiumPreviewBox.
#include "boxes/star_gift_box.h" // ShowStarGiftBox.
#include "boxes/transfer_gift_box.h" // ShowTransferGiftBox.
#include "core/ui_integration.h"
#include "data/data_boosts.h"
#include "data/data_changes.h"
#include "data/data_channel.h"
@ -58,7 +59,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/toast/toast.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/gradient_round_button.h"
#include "ui/widgets/label_with_custom_emoji.h"
#include "ui/widgets/tooltip.h"
#include "ui/wrap/padding_wrap.h"
#include "ui/wrap/slide_wrap.h"
@ -66,6 +66,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "window/window_peer_menu.h" // ShowChooseRecipientBox.
#include "window/window_session_controller.h"
#include "styles/style_boxes.h"
#include "styles/style_credits.h"
#include "styles/style_giveaway.h"
#include "styles/style_info.h"
#include "styles/style_layers.h"
@ -516,13 +517,13 @@ not_null<Ui::FlatLabel*> AddTableRow(
not_null<Ui::TableLayout*> table,
rpl::producer<QString> label,
rpl::producer<TextWithEntities> value,
const Fn<std::any(Fn<void()>)> &makeContext = nullptr) {
const Ui::Text::MarkedContext &context = {}) {
auto widget = object_ptr<Ui::FlatLabel>(
table,
std::move(value),
table->st().defaultValue,
st::defaultPopupMenu,
std::move(makeContext));
context);
const auto result = widget.data();
AddTableRow(
table,
@ -1272,8 +1273,8 @@ void AddStarGiftTable(
const auto selfBareId = session->userPeerId().value;
const auto giftToSelf = (peerId == session->userPeerId())
&& (entry.in || entry.bareGiftOwnerId == selfBareId);
const auto giftToChannel = entry.giftSavedId
&& peerIsChannel(PeerId(entry.bareGiftListPeerId));
const auto giftToChannel = entry.giftChannelSavedId
&& peerIsChannel(PeerId(entry.bareEntryOwnerId));
const auto raw = std::make_shared<Ui::ImportantTooltip*>(nullptr);
const auto showTooltip = [=](
@ -1394,14 +1395,14 @@ void AddStarGiftTable(
? MakePeerTableValue(table, show, PeerId(entry.bareActorId))
: MakeHiddenPeerTableValue(table)),
st::giveawayGiftCodePeerMargin);
if (entry.bareGiftListPeerId) {
if (entry.bareEntryOwnerId) {
AddTableRow(
table,
tr::lng_credits_box_history_entry_peer(),
MakePeerTableValue(
table,
show,
PeerId(entry.bareGiftListPeerId)),
PeerId(entry.bareEntryOwnerId)),
st::giveawayGiftCodePeerMargin);
}
} else if (peerId && !giftToSelf) {
@ -1526,12 +1527,6 @@ void AddStarGiftTable(
: nullptr;
const auto date = base::unixtime::parse(original.date).date();
const auto dateText = TextWithEntities{ langDayOfMonth(date) };
const auto makeContext = [=](Fn<void()> update) {
return Core::MarkedTextContext{
.session = session,
.customEmojiRepaint = std::move(update),
};
};
auto label = object_ptr<Ui::FlatLabel>(
table,
(from
@ -1573,7 +1568,7 @@ void AddStarGiftTable(
? *st.tableValueMessage
: st::giveawayGiftMessage),
st::defaultPopupMenu,
makeContext);
Core::TextContext({ .session = session }));
const auto showBoxLink = [=](not_null<PeerData*> peer) {
return std::make_shared<LambdaClickHandler>([=] {
show->showBox(PrepareShortInfoBox(peer, show));
@ -1591,12 +1586,6 @@ void AddStarGiftTable(
st::giveawayGiftCodeValueMargin);
}
} else if (!entry.description.empty()) {
const auto makeContext = [=](Fn<void()> update) {
return Core::MarkedTextContext{
.session = session,
.customEmojiRepaint = std::move(update),
};
};
auto label = object_ptr<Ui::FlatLabel>(
table,
rpl::single(entry.description),
@ -1604,7 +1593,7 @@ void AddStarGiftTable(
? *st.tableValueMessage
: st::giveawayGiftMessage),
st::defaultPopupMenu,
makeContext);
Core::TextContext({ .session = session }));
label->setSelectable(true);
table->addRow(
nullptr,
@ -1775,6 +1764,25 @@ void AddCreditsHistoryEntryTable(
tr::lng_credits_box_history_entry_subscription(
Ui::Text::WithEntities));
}
if (entry.paidMessagesAmount) {
auto value = Ui::Text::IconEmoji(&st::starIconEmojiColored);
const auto full = (entry.in ? 1 : -1)
* (entry.credits + entry.paidMessagesAmount);
const auto starsText = Lang::FormatStarsAmountDecimal(full);
AddTableRow(
table,
tr::lng_credits_paid_messages_full(),
rpl::single(value.append(' ' + starsText)));
}
if (const auto months = entry.premiumMonthsForStars) {
AddTableRow(
table,
tr::lng_credits_premium_gift_duration(),
tr::lng_months(
lt_count,
rpl::single(1. * months),
Ui::Text::WithEntities));
}
if (!entry.id.isEmpty()) {
auto label = MakeMaybeMultilineTokenValue(table, entry.id, st);
label->setClickHandlerFilter([=](const auto &...) {

View file

@ -453,10 +453,7 @@ void CreateModerateMessagesBox(
) | rpl::start_with_next([=](const TextWithEntities &text) {
raw->setMarkedText(
Ui::Text::Link(text, u"internal:"_q),
Core::MarkedTextContext{
.session = session,
.customEmojiRepaint = [=] { raw->update(); },
});
Core::TextContext({ .session = session }));
}, label->lifetime());
Ui::AddSkip(inner);

View file

@ -1154,8 +1154,7 @@ RecoverBox::RecoverBox(
rpl::single(Ui::Text::WrapEmailPattern(pattern)),
Ui::Text::WithEntities),
st::termsContent,
st::defaultPopupMenu,
[=](Fn<void()> update) { return CommonTextContext{ std::move(update) }; })
st::defaultPopupMenu)
, _closeParent(std::move(closeParent)) {
_patternLabel->setAttribute(Qt::WA_TransparentForMouseEvents);
if (_cloudFields.pendingResetDate != 0 || !session) {

View file

@ -883,6 +883,7 @@ void PeerListRow::paintUserpic(
} else if (const auto callback = generatePaintUserpicCallback(false)) {
callback(p, x, y, outerWidth, st.photoSize);
}
paintUserpicOverlay(p, st, x, y, outerWidth);
}
// Emulates Ui::RoundImageCheckbox::paint() in a checked state.

View file

@ -95,6 +95,13 @@ public:
[[nodiscard]] virtual QString generateShortName();
[[nodiscard]] virtual auto generatePaintUserpicCallback(
bool forceRound) -> PaintRoundImageCallback;
virtual void paintUserpicOverlay(
Painter &p,
const style::PeerListItem &st,
int x,
int y,
int outerWidth) {
}
[[nodiscard]] virtual auto generateNameFirstLetters() const
-> const base::flat_set<QChar> &;

View file

@ -8,7 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/peer_list_controllers.h"
#include "api/api_chat_participants.h"
#include "api/api_premium.h"
#include "api/api_premium.h" // MessageMoneyRestriction.
#include "base/random.h"
#include "boxes/filters/edit_filter_chats_list.h"
#include "settings/settings_premium.h"
@ -41,6 +41,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history.h"
#include "history/history_item.h"
#include "dialogs/dialogs_main_list.h"
#include "payments/ui/payments_reaction_box.h"
#include "ui/effects/outline_segments.h"
#include "ui/wrap/slide_wrap.h"
#include "window/window_separate_id.h"
@ -275,40 +276,71 @@ bool PeerListGlobalSearchController::isLoading() {
return _timer.isActive() || _requestId;
}
struct RecipientRow::Restriction {
Api::MessageMoneyRestriction value;
RestrictionBadgeCache cache;
};
RecipientRow::RecipientRow(
not_null<PeerData*> peer,
const style::PeerListItem *maybeLockedSt,
History *maybeHistory)
: PeerListRow(peer)
, _maybeHistory(maybeHistory)
, _resolvePremiumRequired(maybeLockedSt != nullptr) {
if (maybeLockedSt
&& (Api::ResolveRequiresPremiumToWrite(peer, maybeHistory)
== Api::RequirePremiumState::Yes)) {
_lockedSt = maybeLockedSt;
, _maybeLockedSt(maybeLockedSt) {
if (_maybeLockedSt) {
setRestriction(Api::ResolveMessageMoneyRestrictions(
peer,
maybeHistory));
}
}
PaintRoundImageCallback RecipientRow::generatePaintUserpicCallback(
bool forceRound) {
auto result = PeerListRow::generatePaintUserpicCallback(forceRound);
if (const auto st = _lockedSt) {
return [=](Painter &p, int x, int y, int outerWidth, int size) {
result(p, x, y, outerWidth, size);
PaintPremiumRequiredLock(p, st, x, y, outerWidth, size);
};
Api::MessageMoneyRestriction RecipientRow::restriction() const {
return _restriction
? _restriction->value
: Api::MessageMoneyRestriction();
}
void RecipientRow::setRestriction(Api::MessageMoneyRestriction restriction) {
if (!restriction) {
_restriction = nullptr;
return;
} else if (!_restriction) {
_restriction = std::make_unique<Restriction>();
}
_restriction->value = restriction;
}
void RecipientRow::paintUserpicOverlay(
Painter &p,
const style::PeerListItem &st,
int x,
int y,
int outerWidth) {
if (const auto &r = _restriction) {
PaintRestrictionBadge(
p,
_maybeLockedSt,
r->value.starsPerMessage,
r->cache,
x,
y,
outerWidth,
st.photoSize);
}
return result;
}
bool RecipientRow::refreshLock(
not_null<const style::PeerListItem*> maybeLockedSt) {
if (const auto user = peer()->asUser()) {
const auto locked = _resolvePremiumRequired
&& (Api::ResolveRequiresPremiumToWrite(user, _maybeHistory)
== Api::RequirePremiumState::Yes);
if (this->locked() != locked) {
setLocked(locked ? maybeLockedSt.get() : nullptr);
using Restriction = Api::MessageMoneyRestriction;
const auto r = _maybeLockedSt
? Api::ResolveMessageMoneyRestrictions(
user,
_maybeHistory)
: Restriction();
if ((_restriction ? _restriction->value : Restriction()) != r) {
setRestriction(r);
return true;
}
}
@ -318,22 +350,30 @@ bool RecipientRow::refreshLock(
void RecipientRow::preloadUserpic() {
PeerListRow::preloadUserpic();
if (!_resolvePremiumRequired) {
if (!_maybeLockedSt) {
return;
} else if (Api::ResolveRequiresPremiumToWrite(peer(), _maybeHistory)
== Api::RequirePremiumState::Unknown) {
const auto user = peer()->asUser();
user->session().api().premium().resolvePremiumRequired(user);
}
const auto peer = this->peer();
const auto known = Api::ResolveMessageMoneyRestrictions(
peer,
_maybeHistory).known;
if (known) {
return;
} else if (const auto user = peer->asUser()) {
const auto api = &user->session().api();
api->premium().resolveMessageMoneyRestrictions(user);
} else if (const auto group = peer->asChannel()) {
group->updateFull();
}
}
void TrackPremiumRequiredChanges(
void TrackMessageMoneyRestrictionsChanges(
not_null<PeerListController*> controller,
rpl::lifetime &lifetime) {
const auto session = &controller->session();
rpl::merge(
Data::AmPremiumValue(session) | rpl::to_empty,
session->api().premium().somePremiumRequiredResolved()
session->api().premium().someMessageMoneyRestrictionsResolved()
) | rpl::start_with_next([=] {
const auto st = &controller->computeListSt().item;
const auto delegate = controller->delegate();
@ -726,7 +766,7 @@ std::unique_ptr<PeerListRow> ContactsBoxController::createRow(
return std::make_unique<PeerListRow>(user);
}
RecipientPremiumRequiredError WritePremiumRequiredError(
RecipientMoneyRestrictionError WriteMoneyRestrictionError(
not_null<UserData*> user) {
return {
.text = tr::lng_send_non_premium_message_toast(
@ -759,7 +799,7 @@ ChooseRecipientBoxController::ChooseRecipientBoxController(
, _session(args.session)
, _callback(std::move(args.callback))
, _filter(std::move(args.filter))
, _premiumRequiredError(std::move(args.premiumRequiredError)) {
, _moneyRestrictionError(std::move(args.moneyRestrictionError)) {
}
Main::Session &ChooseRecipientBoxController::session() const {
@ -769,14 +809,17 @@ Main::Session &ChooseRecipientBoxController::session() const {
void ChooseRecipientBoxController::prepareViewHook() {
delegate()->peerListSetTitle(tr::lng_forward_choose());
if (_premiumRequiredError) {
TrackPremiumRequiredChanges(this, lifetime());
if (_moneyRestrictionError) {
TrackMessageMoneyRestrictionsChanges(this, lifetime());
}
}
bool ChooseRecipientBoxController::showLockedError(
not_null<PeerListRow*> row) {
return RecipientRow::ShowLockedError(this, row, _premiumRequiredError);
return RecipientRow::ShowLockedError(
this,
row,
_moneyRestrictionError);
}
void ChooseRecipientBoxController::rowClicked(not_null<PeerListRow*> row) {
@ -836,8 +879,9 @@ void ChooseRecipientBoxController::rowClicked(not_null<PeerListRow*> row) {
bool RecipientRow::ShowLockedError(
not_null<PeerListController*> controller,
not_null<PeerListRow*> row,
Fn<RecipientPremiumRequiredError(not_null<UserData*>)> error) {
if (!static_cast<RecipientRow*>(row.get())->locked()) {
Fn<RecipientMoneyRestrictionError(not_null<UserData*>)> error) {
const auto recipient = static_cast<RecipientRow*>(row.get());
if (!recipient->restriction().premiumRequired) {
return false;
}
::Settings::ShowPremiumPromoToast(
@ -860,15 +904,15 @@ auto ChooseRecipientBoxController::createRow(
: ((peer->isBroadcast() && !Data::CanSendAnything(peer))
|| peer->isRepliesChat()
|| peer->isVerifyCodes()
|| (peer->isUser() && (_premiumRequiredError
? !peer->asUser()->canSendIgnoreRequirePremium()
|| (peer->isUser() && (_moneyRestrictionError
? !peer->asUser()->canSendIgnoreMoneyRestrictions()
: !Data::CanSendAnything(peer))));
if (skip) {
return nullptr;
}
auto result = std::make_unique<Row>(
history,
_premiumRequiredError ? &computeListSt().item : nullptr);
_moneyRestrictionError ? &computeListSt().item : nullptr);
return result;
}
@ -1093,25 +1137,61 @@ auto ChooseTopicBoxController::createRow(not_null<Data::ForumTopic*> topic)
return skip ? nullptr : std::make_unique<Row>(topic);
};
void PaintPremiumRequiredLock(
void PaintRestrictionBadge(
Painter &p,
not_null<const style::PeerListItem*> st,
int stars,
RestrictionBadgeCache &cache,
int x,
int y,
int outerWidth,
int size) {
auto hq = PainterHighQualityEnabler(p);
const auto paletteVersion = style::PaletteVersion();
const auto good = !cache.badge.isNull()
&& (cache.stars == stars)
&& (cache.paletteVersion == paletteVersion);
const auto &check = st->checkbox.check;
auto pen = check.border->p;
pen.setWidthF(check.width);
p.setPen(pen);
p.setBrush(st::premiumButtonBg2);
const auto &icon = st::stickersPremiumLock;
const auto width = icon.width();
const auto height = icon.height();
const auto rect = QRect(
QPoint(x + size - width, y + size - height),
icon.size());
p.drawEllipse(rect);
icon.paintInCenter(p, rect);
const auto add = check.width;
if (!good) {
cache.stars = stars;
cache.paletteVersion = paletteVersion;
if (stars) {
const auto text = (stars >= 1000)
? (QString::number(stars / 1000) + 'K')
: QString::number(stars);
cache.badge = Ui::GenerateSmallBadgeImage(
text,
st::paidReactTopStarIcon,
check.bgActive->c,
st::premiumButtonFg->c,
&check);
} else {
auto hq = PainterHighQualityEnabler(p);
const auto &icon = st::stickersPremiumLock;
const auto width = icon.width();
const auto height = icon.height();
const auto rect = QRect(
QPoint(x + size - width, y + size - height),
icon.size());
const auto added = QMargins(add, add, add, add);
const auto ratio = style::DevicePixelRatio();
cache.badge = QImage(
(rect + added).size() * ratio,
QImage::Format_ARGB32_Premultiplied);
cache.badge.setDevicePixelRatio(ratio);
cache.badge.fill(Qt::transparent);
const auto inner = QRect(add, add, rect.width(), rect.height());
auto q = QPainter(&cache.badge);
auto pen = check.border->p;
pen.setWidthF(check.width);
q.setPen(pen);
q.setBrush(st::premiumButtonBg2);
q.drawEllipse(inner);
icon.paintInCenter(q, inner);
}
}
const auto cached = cache.badge.size() / cache.badge.devicePixelRatio();
const auto left = x + size + add - cached.width();
const auto top = stars ? (y - add) : (y + size + add - cached.height());
p.drawImage(left, top, cache.badge);
}

View file

@ -19,6 +19,10 @@ namespace style {
struct PeerListItem;
} // namespace style
namespace Api {
struct MessageMoneyRestriction;
} // namespace Api
namespace Data {
class Thread;
class Forum;
@ -93,13 +97,28 @@ private:
};
struct RecipientPremiumRequiredError {
struct RecipientMoneyRestrictionError {
TextWithEntities text;
};
[[nodiscard]] RecipientPremiumRequiredError WritePremiumRequiredError(
[[nodiscard]] RecipientMoneyRestrictionError WriteMoneyRestrictionError(
not_null<UserData*> user);
struct RestrictionBadgeCache {
int paletteVersion = 0;
int stars = 0;
QImage badge;
};
void PaintRestrictionBadge(
Painter &p,
not_null<const style::PeerListItem*> st,
int stars,
RestrictionBadgeCache &cache,
int x,
int y,
int outerWidth,
int size);
class RecipientRow : public PeerListRow {
public:
explicit RecipientRow(
@ -112,30 +131,33 @@ public:
[[nodiscard]] static bool ShowLockedError(
not_null<PeerListController*> controller,
not_null<PeerListRow*> row,
Fn<RecipientPremiumRequiredError(not_null<UserData*>)> error);
Fn<RecipientMoneyRestrictionError(not_null<UserData*>)> error);
[[nodiscard]] History *maybeHistory() const {
return _maybeHistory;
}
[[nodiscard]] bool locked() const {
return _lockedSt != nullptr;
}
void setLocked(const style::PeerListItem *lockedSt) {
_lockedSt = lockedSt;
}
PaintRoundImageCallback generatePaintUserpicCallback(
bool forceRound) override;
void paintUserpicOverlay(
Painter &p,
const style::PeerListItem &st,
int x,
int y,
int outerWidth) override;
void preloadUserpic() override;
[[nodiscard]] Api::MessageMoneyRestriction restriction() const;
void setRestriction(Api::MessageMoneyRestriction restriction);
private:
struct Restriction;
History *_maybeHistory = nullptr;
const style::PeerListItem *_lockedSt = nullptr;
bool _resolvePremiumRequired = false;
const style::PeerListItem *_maybeLockedSt = nullptr;
std::shared_ptr<Restriction> _restriction;
};
void TrackPremiumRequiredChanges(
void TrackMessageMoneyRestrictionsChanges(
not_null<PeerListController*> controller,
rpl::lifetime &lifetime);
@ -261,8 +283,8 @@ struct ChooseRecipientArgs {
FnMut<void(not_null<Data::Thread*>)> callback;
Fn<bool(not_null<Data::Thread*>)> filter;
using PremiumRequiredError = RecipientPremiumRequiredError;
Fn<PremiumRequiredError(not_null<UserData*>)> premiumRequiredError;
using MoneyRestrictionError = RecipientMoneyRestrictionError;
Fn<MoneyRestrictionError(not_null<UserData*>)> moneyRestrictionError;
};
class ChooseRecipientBoxController
@ -290,8 +312,8 @@ private:
const not_null<Main::Session*> _session;
FnMut<void(not_null<Data::Thread*>)> _callback;
Fn<bool(not_null<Data::Thread*>)> _filter;
Fn<RecipientPremiumRequiredError(
not_null<UserData*>)> _premiumRequiredError;
Fn<RecipientMoneyRestrictionError(
not_null<UserData*>)> _moneyRestrictionError;
};
@ -371,11 +393,3 @@ private:
Fn<bool(not_null<Data::ForumTopic*>)> _filter;
};
void PaintPremiumRequiredLock(
Painter &p,
not_null<const style::PeerListItem*> st,
int x,
int y,
int outerWidth,
int size);

View file

@ -9,10 +9,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_chat_participants.h"
#include "api/api_invite_links.h"
#include "api/api_premium.h"
#include "boxes/peers/edit_participant_box.h"
#include "boxes/peers/edit_peer_type_box.h"
#include "boxes/peers/replace_boost_box.h"
#include "boxes/max_invite_box.h"
#include "chat_helpers/message_field.h"
#include "lang/lang_keys.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
@ -22,6 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_changes.h"
#include "data/data_peer_values.h"
#include "history/history.h"
#include "history/history_item_helpers.h"
#include "dialogs/dialogs_indexed_list.h"
#include "ui/boxes/confirm_box.h"
#include "ui/boxes/show_or_premium_box.h"
@ -52,16 +55,39 @@ constexpr auto kUserpicsLimit = 3;
class ForbiddenRow final : public PeerListRow {
public:
ForbiddenRow(not_null<PeerData*> peer, bool locked);
ForbiddenRow(
not_null<PeerData*> peer,
not_null<const style::PeerListItem*> lockSt,
bool locked);
PaintRoundImageCallback generatePaintUserpicCallback(
bool forceRound) override;
Api::MessageMoneyRestriction restriction() const;
void setRestriction(Api::MessageMoneyRestriction restriction);
void preloadUserpic() override;
void paintUserpicOverlay(
Painter &p,
const style::PeerListItem &st,
int x,
int y,
int outerWidth) override;
bool refreshLock();
private:
struct Restriction {
Api::MessageMoneyRestriction value;
RestrictionBadgeCache cache;
};
const bool _locked = false;
const not_null<const style::PeerListItem*> _lockSt;
QImage _disabledFrame;
InMemoryKey _userpicKey;
int _paletteVersion = 0;
std::shared_ptr<Restriction> _restriction;
};
@ -81,6 +107,9 @@ public:
[[nodiscard]] rpl::producer<int> selectedValue() const {
return _selected.value();
}
[[nodiscard]] rpl::producer<int> starsToSend() const {
return _starsToSend.value();
}
void send(
std::vector<not_null<PeerData*>> list,
@ -89,10 +118,16 @@ public:
private:
void appendRow(not_null<UserData*> user);
[[nodiscard]] std::unique_ptr<PeerListRow> createRow(
[[nodiscard]] std::unique_ptr<ForbiddenRow> createRow(
not_null<UserData*> user) const;
[[nodiscard]] bool canInvite(not_null<PeerData*> peer) const;
void send(
std::vector<not_null<PeerData*>> list,
Ui::ShowPtr show,
Fn<void()> close,
Api::SendOptions options);
void setSimpleCover();
void setComplexCover();
@ -101,8 +136,11 @@ private:
const std::vector<not_null<UserData*>> &_users;
const bool _can = false;
rpl::variable<int> _selected;
rpl::variable<int> _starsToSend;
bool _sending = false;
rpl::lifetime _paymentCheckLifetime;
};
base::flat_set<not_null<UserData*>> GetAlreadyInFromPeer(PeerData *peer) {
@ -256,11 +294,17 @@ Main::Session &InviteForbiddenController::session() const {
return _peer->session();
}
ForbiddenRow::ForbiddenRow(not_null<PeerData*> peer, bool locked)
ForbiddenRow::ForbiddenRow(
not_null<PeerData*> peer,
not_null<const style::PeerListItem*> lockSt,
bool locked)
: PeerListRow(peer)
, _locked(locked) {
, _locked(locked)
, _lockSt(lockSt) {
if (_locked) {
setCustomStatus(tr::lng_invite_status_disabled(tr::now));
} else {
setRestriction(Api::ResolveMessageMoneyRestrictions(peer, nullptr));
}
}
@ -339,6 +383,76 @@ PaintRoundImageCallback ForbiddenRow::generatePaintUserpicCallback(
};
}
Api::MessageMoneyRestriction ForbiddenRow::restriction() const {
return _restriction
? _restriction->value
: Api::MessageMoneyRestriction();
}
void ForbiddenRow::setRestriction(Api::MessageMoneyRestriction restriction) {
if (!restriction || !restriction.starsPerMessage) {
_restriction = nullptr;
return;
} else if (!_restriction) {
_restriction = std::make_unique<Restriction>();
}
_restriction->value = restriction;
}
void ForbiddenRow::paintUserpicOverlay(
Painter &p,
const style::PeerListItem &st,
int x,
int y,
int outerWidth) {
if (const auto &r = _restriction) {
PaintRestrictionBadge(
p,
_lockSt,
r->value.starsPerMessage,
r->cache,
x,
y,
outerWidth,
st.photoSize);
}
}
bool ForbiddenRow::refreshLock() {
if (_locked) {
return false;
} else if (const auto user = peer()->asUser()) {
using Restriction = Api::MessageMoneyRestriction;
auto r = Api::ResolveMessageMoneyRestrictions(user, nullptr);
if (!r || !r.starsPerMessage) {
r = Restriction();
}
if ((_restriction ? _restriction->value : Restriction()) != r) {
setRestriction(r);
return true;
}
}
return false;
}
void ForbiddenRow::preloadUserpic() {
PeerListRow::preloadUserpic();
const auto peer = this->peer();
const auto known = Api::ResolveMessageMoneyRestrictions(
peer,
nullptr).known;
if (known) {
return;
} else if (const auto user = peer->asUser()) {
const auto api = &user->session().api();
api->premium().resolveMessageMoneyRestrictions(user);
} else if (const auto group = peer->asChannel()) {
group->updateFull();
}
}
void InviteForbiddenController::setSimpleCover() {
delegate()->peerListSetTitle(
_can ? tr::lng_profile_add_via_link() : tr::lng_via_link_cant());
@ -435,6 +549,30 @@ void InviteForbiddenController::setComplexCover() {
}
void InviteForbiddenController::prepare() {
session().api().premium().someMessageMoneyRestrictionsResolved(
) | rpl::start_with_next([=] {
auto stars = 0;
const auto process = [&](not_null<PeerListRow*> raw) {
const auto row = static_cast<ForbiddenRow*>(raw.get());
if (row->refreshLock()) {
delegate()->peerListUpdateRow(raw);
}
if (const auto r = row->restriction()) {
stars += r.starsPerMessage;
}
};
auto count = delegate()->peerListFullRowsCount();
for (auto i = 0; i != count; ++i) {
process(delegate()->peerListRowAt(i));
}
_starsToSend = stars;
count = delegate()->peerListSearchRowsCount();
for (auto i = 0; i != count; ++i) {
process(delegate()->peerListSearchRowAt(i));
}
}, lifetime());
if (session().premium()
|| (_forbidden.premiumAllowsInvite.empty()
&& _forbidden.premiumAllowsWrite.empty())) {
@ -464,6 +602,11 @@ void InviteForbiddenController::rowClicked(not_null<PeerListRow*> row) {
const auto checked = row->checked();
delegate()->peerListSetRowChecked(row, !checked);
_selected = _selected.current() + (checked ? -1 : 1);
const auto r = static_cast<ForbiddenRow*>(row.get())->restriction();
if (r.starsPerMessage) {
_starsToSend = _starsToSend.current()
+ (checked ? -r.starsPerMessage : r.starsPerMessage);
}
}
void InviteForbiddenController::appendRow(not_null<UserData*> user) {
@ -473,6 +616,9 @@ void InviteForbiddenController::appendRow(not_null<UserData*> user) {
delegate()->peerListAppendRow(std::move(row));
if (canInvite(user)) {
delegate()->peerListSetRowChecked(raw, true);
if (const auto r = raw->restriction()) {
_starsToSend = _starsToSend.current() + r.starsPerMessage;
}
}
}
}
@ -481,7 +627,64 @@ void InviteForbiddenController::send(
std::vector<not_null<PeerData*>> list,
Ui::ShowPtr show,
Fn<void()> close) {
if (_sending || list.empty()) {
send(list, show, close, {});
}
void InviteForbiddenController::send(
std::vector<not_null<PeerData*>> list,
Ui::ShowPtr show,
Fn<void()> close,
Api::SendOptions options) {
if (list.empty()) {
return;
}
_paymentCheckLifetime.destroy();
const auto withPaymentApproved = [=](int approved) {
auto copy = options;
copy.starsApproved = approved;
send(list, show, close, copy);
};
const auto messagesCount = 1;
const auto alreadyApproved = options.starsApproved;
auto paid = std::vector<not_null<PeerData*>>();
auto waiting = base::flat_set<not_null<PeerData*>>();
auto totalStars = 0;
for (const auto &peer : list) {
const auto details = ComputePaymentDetails(peer, messagesCount);
if (!details) {
waiting.emplace(peer);
} else if (details->stars > 0) {
totalStars += details->stars;
paid.push_back(peer);
}
}
if (!waiting.empty()) {
session().changes().peerUpdates(
Data::PeerUpdate::Flag::FullInfo
) | rpl::start_with_next([=](const Data::PeerUpdate &update) {
if (waiting.contains(update.peer)) {
withPaymentApproved(alreadyApproved);
}
}, _paymentCheckLifetime);
if (!session().credits().loaded()) {
session().credits().loadedValue(
) | rpl::filter(
rpl::mappers::_1
) | rpl::take(1) | rpl::start_with_next([=] {
withPaymentApproved(alreadyApproved);
}, _paymentCheckLifetime);
}
return;
} else if (totalStars > alreadyApproved) {
const auto sessionShow = Main::MakeSessionShow(show, &session());
ShowSendPaidConfirm(sessionShow, paid, SendPaymentDetails{
.messages = messagesCount,
.stars = totalStars,
}, [=] { withPaymentApproved(totalStars); });
return;
} else if (_sending) {
return;
}
_sending = true;
@ -492,12 +695,18 @@ void InviteForbiddenController::send(
if (link.isEmpty()) {
return false;
}
auto full = options;
auto &api = _peer->session().api();
auto options = Api::SendOptions();
for (const auto &to : list) {
auto copy = full;
copy.starsApproved = std::min(
to->starsPerMessageChecked(),
full.starsApproved);
full.starsApproved -= copy.starsApproved;
const auto history = to->owner().history(to);
auto message = Api::MessageToSend(
Api::SendAction(history, options));
Api::SendAction(history, copy));
message.textWithTags = { link };
message.action.clearDraft = false;
api.sendMessage(std::move(message));
@ -542,10 +751,11 @@ void InviteForbiddenController::send(
}
}
std::unique_ptr<PeerListRow> InviteForbiddenController::createRow(
std::unique_ptr<ForbiddenRow> InviteForbiddenController::createRow(
not_null<UserData*> user) const {
const auto locked = _can && !canInvite(user);
return std::make_unique<ForbiddenRow>(user, locked);
const auto lockSt = &computeListSt().item;
return std::make_unique<ForbiddenRow>(user, lockSt, locked);
}
} // namespace
@ -584,8 +794,8 @@ void AddParticipantsBoxController::subscribeToMigration() {
}
void AddParticipantsBoxController::rowClicked(not_null<PeerListRow*> row) {
const auto premiumRequiredError = WritePremiumRequiredError;
if (RecipientRow::ShowLockedError(this, row, premiumRequiredError)) {
const auto moneyRestrictionError = WriteMoneyRestrictionError;
if (RecipientRow::ShowLockedError(this, row, moneyRestrictionError)) {
return;
}
const auto &serverConfig = session().serverConfig();
@ -614,7 +824,7 @@ void AddParticipantsBoxController::itemDeselectedHook(
void AddParticipantsBoxController::prepareViewHook() {
updateTitle();
TrackPremiumRequiredChanges(this, lifetime());
TrackMessageMoneyRestrictionsChanges(this, lifetime());
}
int AddParticipantsBoxController::alreadyInCount() const {
@ -929,12 +1139,15 @@ bool ChatInviteForbidden(
) | rpl::start_with_next([=](bool has) {
box->clearButtons();
if (has) {
box->addButton(tr::lng_via_link_send(), [=] {
const auto send = box->addButton(tr::lng_via_link_send(), [=] {
weak->send(
box->collectSelectedRows(),
box->uiShow(),
crl::guard(box, [=] { box->closeBox(); }));
});
send->setText(PaidSendButtonText(
weak->starsToSend(),
tr::lng_via_link_send()));
}
box->addButton(tr::lng_create_group_skip(), [=] {
box->closeBox();

View file

@ -15,7 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/background_box.h"
#include "boxes/stickers_box.h"
#include "chat_helpers/compose/compose_show.h"
#include "core/ui_integration.h" // Core::MarkedTextContext.
#include "core/ui_integration.h" // TextContext
#include "data/stickers/data_custom_emoji.h"
#include "data/stickers/data_stickers.h"
#include "data/data_changes.h"
@ -165,7 +165,7 @@ private:
const uint32 _level;
const TextWithEntities _icon;
const Core::MarkedTextContext _context;
const Ui::Text::MarkedContext _context;
Ui::Text::String _text;
bool _minimal = false;
@ -466,7 +466,10 @@ LevelBadge::LevelBadge(
st::settingsLevelBadgeLock,
QMargins(0, st::settingsLevelBadgeLockSkip, 0, 0),
false)))
, _context({ .session = session }) {
, _context(Core::TextContext({
.session = session,
.repaint = [this] { update(); },
})) {
updateText();
}

View file

@ -219,6 +219,33 @@ void SaveSlowmodeSeconds(
api->registerModifyRequest(key, requestId);
}
void SaveStarsPerMessage(
not_null<ChannelData*> channel,
int starsPerMessage,
Fn<void()> done) {
const auto api = &channel->session().api();
const auto key = Api::RequestKey("stars_per_message", channel->id);
const auto requestId = api->request(MTPchannels_UpdatePaidMessagesPrice(
channel->inputChannel,
MTP_long(starsPerMessage)
)).done([=](const MTPUpdates &result) {
api->clearModifyRequest(key);
api->applyUpdates(result);
channel->setStarsPerMessage(starsPerMessage);
done();
}).fail([=](const MTP::Error &error) {
api->clearModifyRequest(key);
if (error.type() != u"CHAT_NOT_MODIFIED"_q) {
return;
}
channel->setStarsPerMessage(starsPerMessage);
done();
}).send();
api->registerModifyRequest(key, requestId);
}
void SaveBoostsUnrestrict(
not_null<ChannelData*> channel,
int boostsUnrestrict,
@ -271,6 +298,7 @@ void ShowEditPermissions(
channel,
result.boostsUnrestrict,
close);
SaveStarsPerMessage(channel, result.starsPerMessage, close);
}
};
auto done = [=](EditPeerPermissionsBoxResult result) {
@ -282,7 +310,9 @@ void ShowEditPermissions(
const auto saveFor = peer->migrateToOrMe();
const auto chat = saveFor->asChat();
if (!chat
|| (!result.slowmodeSeconds && !result.boostsUnrestrict)) {
|| (!result.slowmodeSeconds
&& !result.boostsUnrestrict
&& !result.starsPerMessage)) {
save(saveFor, result);
return;
}
@ -2689,3 +2719,9 @@ bool EditPeerInfoBox::Available(not_null<PeerData*> peer) {
return false;
}
}
void ShowEditChatPermissions(
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer) {
ShowEditPermissions(navigation, peer);
}

View file

@ -56,3 +56,7 @@ private:
not_null<PeerData*> _peer;
};
void ShowEditChatPermissions(
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer);

View file

@ -15,7 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/peer_list_controllers.h"
#include "boxes/share_box.h"
#include "core/application.h"
#include "core/ui_integration.h" // Core::MarkedTextContext.
#include "core/ui_integration.h" // TextContext
#include "data/components/credits.h"
#include "data/data_changes.h"
#include "data/data_channel.h"
@ -740,10 +740,10 @@ void Controller::setupAboveJoinedWidget() {
{ QString::number(current.subscription.credits) },
Ui::Text::WithEntities),
kMarkupTextOptions,
Core::MarkedTextContext{
Core::TextContext({
.session = &session(),
.customEmojiRepaint = [=] { widget->update(); },
});
.repaint = [=] { widget->update(); },
}));
auto &lifetime = widget->lifetime();
const auto rateValue = lifetime.make_state<rpl::variable<float64>>(
session().credits().rateValue(_peer));
@ -994,10 +994,7 @@ void Controller::rowClicked(not_null<PeerListRow*> row) {
lt_cost,
{ QString::number(data.subscription.credits) },
Ui::Text::WithEntities),
Core::MarkedTextContext{
.session = session,
.customEmojiRepaint = [=] { subtitle1->update(); },
});
Core::TextContext({ .session = session }));
const auto subtitle2 = box->addRow(
object_ptr<Ui::CenterWrap<Ui::FlatLabel>>(
box,
@ -1484,8 +1481,12 @@ object_ptr<Ui::BoxContent> ShareInviteLinkBox(
? tr::lng_group_invite_copied(tr::now)
: copied);
};
auto countMessagesCallback = [=](const TextWithTags &comment) {
return 1;
};
auto submitCallback = [=](
std::vector<not_null<Data::Thread*>> &&result,
Fn<bool()> checkPaid,
TextWithTags &&comment,
Api::SendOptions options,
Data::ForwardOptions) {
@ -1503,6 +1504,8 @@ object_ptr<Ui::BoxContent> ShareInviteLinkBox(
result.size() > 1));
}
return;
} else if (!checkPaid()) {
return;
}
*sending = true;
@ -1530,7 +1533,7 @@ object_ptr<Ui::BoxContent> ShareInviteLinkBox(
};
auto filterCallback = [](not_null<Data::Thread*> thread) {
if (const auto user = thread->peer()->asUser()) {
if (user->canSendIgnoreRequirePremium()) {
if (user->canSendIgnoreMoneyRestrictions()) {
return true;
}
}
@ -1539,9 +1542,10 @@ object_ptr<Ui::BoxContent> ShareInviteLinkBox(
auto object = Box<ShareBox>(ShareBox::Descriptor{
.session = session,
.copyCallback = std::move(copyCallback),
.countMessagesCallback = std::move(countMessagesCallback),
.submitCallback = std::move(submitCallback),
.filterCallback = std::move(filterCallback),
.premiumRequiredError = SharePremiumRequiredError(),
.moneyRestrictionError = ShareMessageMoneyRestrictionError(),
});
*box = Ui::MakeWeak(object.data());
return object;

View file

@ -31,6 +31,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "info/profile/info_profile_values.h"
#include "boxes/peers/edit_participants_box.h"
#include "boxes/peers/edit_peer_info_box.h"
#include "boxes/edit_privacy_box.h"
#include "settings/settings_power_saving.h"
#include "window/window_session_controller.h"
#include "window/window_controller.h"
@ -891,11 +892,10 @@ void AddBoostsUnrestrictLabels(
manager->registerInternalEmoji(
st::boostsMessageIcon,
st::boostsMessageIconPadding));
const auto context = Core::MarkedTextContext{
const auto context = Core::TextContext({
.session = session,
.customEmojiRepaint = [] {},
.customEmojiLoopLimit = 1,
};
});
for (auto i = 0; i != kBoostsUnrestrictValues; ++i) {
const auto label = Ui::CreateChild<Ui::FlatLabel>(
labels,
@ -942,9 +942,7 @@ rpl::producer<int> AddBoostsUnrestrictSlider(
const auto boostsUnrestrict = lifetime.make_state<rpl::variable<int>>(
channel ? channel->boostsUnrestrict() : 0);
container->add(
object_ptr<Ui::BoxContentDivider>(container),
{ 0, st::infoProfileSkip, 0, st::infoProfileSkip });
Ui::AddSkip(container);
auto enabled = boostsUnrestrict->value(
) | rpl::map(_1 > 0);
@ -1008,19 +1006,20 @@ rpl::producer<int> AddBoostsUnrestrictWrapped(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
container,
object_ptr<Ui::VerticalLayout>(container)));
wrap->toggleOn(rpl::duplicate(shown), anim::type::normal);
wrap->toggleOn(std::move(shown), anim::type::normal);
wrap->finishAnimating();
auto result = AddBoostsUnrestrictSlider(wrap->entity(), peer);
const auto divider = container->add(
const auto inner = wrap->entity();
auto result = AddBoostsUnrestrictSlider(inner, peer);
const auto skip = st::defaultVerticalListSkip;
const auto divider = inner->add(
object_ptr<Ui::SlideWrap<Ui::BoxContentDivider>>(
container,
object_ptr<Ui::BoxContentDivider>(container),
QMargins{ 0, st::infoProfileSkip, 0, st::infoProfileSkip }));
divider->toggleOn(rpl::combine(
std::move(shown),
rpl::duplicate(result),
!rpl::mappers::_1 || !rpl::mappers::_2));
inner,
object_ptr<Ui::BoxContentDivider>(inner),
QMargins{ 0, skip, 0, skip }));
divider->toggleOn(rpl::duplicate(result) | rpl::map(!rpl::mappers::_1));
divider->finishAnimating();
return result;
@ -1159,7 +1158,43 @@ void ShowEditPeerPermissionsBox(
rpl::variable<int> slowmodeSeconds;
rpl::variable<int> boostsUnrestrict;
rpl::variable<bool> hasSendRestrictions;
rpl::variable<int> starsPerMessage;
};
const auto state = inner->lifetime().make_state<State>();
const auto channel = peer->asChannel();
const auto available = channel && channel->paidMessagesAvailable();
Ui::AddSkip(inner);
Ui::AddDivider(inner);
auto charging = (Ui::SettingsButton*)nullptr;
if (available) {
Ui::AddSkip(inner);
const auto starsPerMessage = peer->isChannel()
? peer->asChannel()->starsPerMessage()
: 0;
charging = inner->add(object_ptr<Ui::SettingsButton>(
inner,
tr::lng_rights_charge_stars(),
st::settingsButtonNoIcon));
charging->toggleOn(rpl::single(starsPerMessage > 0));
Ui::AddSkip(inner);
Ui::AddDividerText(inner, tr::lng_rights_charge_stars_about());
const auto chargeWrap = inner->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
inner,
object_ptr<Ui::VerticalLayout>(inner)));
chargeWrap->toggleOn(charging->toggledValue());
chargeWrap->finishAnimating();
const auto chargeInner = chargeWrap->entity();
Ui::AddSkip(chargeInner);
state->starsPerMessage = SetupChargeSlider(
chargeInner,
peer,
starsPerMessage);
}
static constexpr auto kSendRestrictions = Flag::EmbedLinks
| Flag::SendGames
| Flag::SendGifs
@ -1173,7 +1208,6 @@ void ShowEditPeerPermissionsBox(
| Flag::SendVoiceMessages
| Flag::SendFiles
| Flag::SendOther;
const auto state = inner->lifetime().make_state<State>();
state->hasSendRestrictions = ((restrictions & kSendRestrictions) != 0)
|| (peer->isChannel() && peer->asChannel()->slowmodeSeconds() > 0);
state->boostsUnrestrict = AddBoostsUnrestrictWrapped(
@ -1214,10 +1248,14 @@ void ShowEditPeerPermissionsBox(
const auto boostsUnrestrict = hasRestrictions
? state->boostsUnrestrict.current()
: 0;
const auto starsPerMessage = (charging && charging->toggled())
? state->starsPerMessage.current()
: 0;
done({
restrictions,
slowmodeSeconds,
boostsUnrestrict,
starsPerMessage,
});
});
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });

View file

@ -39,6 +39,7 @@ struct EditPeerPermissionsBoxResult final {
ChatRestrictions rights;
int slowmodeSeconds = 0;
int boostsUnrestrict = 0;
int starsPerMessage = 0;
};
void ShowEditPeerPermissionsBox(

View file

@ -363,12 +363,17 @@ object_ptr<Ui::RpWidget> AddReactionsSelector(
const auto customEmojiPaused = [controller = args.controller] {
return controller->isGifPausedAtLeastFor(PauseReason::Layer);
};
auto factory = [=](QStringView data, Fn<void()> update)
-> std::unique_ptr<Ui::Text::CustomEmoji> {
auto simpleContext = Core::TextContext({
.session = session,
.repaint = [=] { raw->update(); },
});
auto context = simpleContext;
context.customEmojiFactory = [=](
QStringView data,
const Ui::Text::MarkedContext &context
) -> std::unique_ptr<Ui::Text::CustomEmoji> {
const auto id = Data::ParseCustomEmojiData(data);
auto result = owner->customEmojiManager().create(
data,
std::move(update));
auto result = Ui::Text::MakeCustomEmoji(data, simpleContext);
if (state->unifiedFactoryOwner->lookupReactionId(id).custom()) {
return std::make_unique<MaybeDisabledEmoji>(
std::move(result),
@ -377,12 +382,10 @@ object_ptr<Ui::RpWidget> AddReactionsSelector(
using namespace Ui::Text;
return std::make_unique<FirstFrameEmoji>(std::move(result));
};
raw->setCustomTextContext([=](Fn<void()> repaint) {
return std::any(Core::MarkedTextContext{
.session = session,
.customEmojiRepaint = std::move(repaint),
});
}, customEmojiPaused, customEmojiPaused, std::move(factory));
raw->setCustomTextContext(
std::move(context),
customEmojiPaused,
customEmojiPaused);
const auto callback = args.callback;
const auto isCustom = [=](DocumentId id) {

View file

@ -9,7 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_credits.h"
#include "apiwrap.h"
#include "core/ui_integration.h" // Core::MarkedTextContext.
#include "core/ui_integration.h" // TextContext
#include "data/components/credits.h"
#include "data/data_credits.h"
#include "data/data_photo.h"
@ -511,32 +511,28 @@ TextWithEntities CreditsEmoji(not_null<Main::Session*> session) {
}
TextWithEntities CreditsEmojiSmall(not_null<Main::Session*> session) {
return Ui::Text::SingleCustomEmoji(
session->data().customEmojiManager().registerInternalEmoji(
st::starIconSmall,
st::starIconSmallPadding,
true),
return Ui::Text::IconEmoji(
&st::starIconEmoji,
QString(QChar(0x2B50)));
}
not_null<FlatLabel*> SetButtonMarkedLabel(
not_null<RpWidget*> button,
rpl::producer<TextWithEntities> text,
Fn<std::any(Fn<void()> update)> context,
Text::MarkedContext context,
const style::FlatLabel &st,
const style::color *textFg) {
const auto buttonLabel = Ui::CreateChild<Ui::FlatLabel>(
button,
rpl::single(QString()),
st);
context.repaint = [=] { buttonLabel->update(); };
rpl::duplicate(
text
) | rpl::filter([=](const TextWithEntities &text) {
return !text.text.isEmpty();
}) | rpl::start_with_next([=](const TextWithEntities &text) {
buttonLabel->setMarkedText(
text,
context([=] { buttonLabel->update(); }));
buttonLabel->setMarkedText(text, context);
}, buttonLabel->lifetime());
if (textFg) {
buttonLabel->setTextColorOverride((*textFg)->c);
@ -565,15 +561,12 @@ not_null<FlatLabel*> SetButtonMarkedLabel(
not_null<Main::Session*> session,
const style::FlatLabel &st,
const style::color *textFg) {
return SetButtonMarkedLabel(button, text, [=](Fn<void()> update) {
return Core::MarkedTextContext{
.session = session,
.customEmojiRepaint = update,
};
}, st, textFg);
return SetButtonMarkedLabel(button, text, Core::TextContext({
.session = session,
}), st, textFg);
}
void SendStarGift(
void SendStarsForm(
not_null<Main::Session*> session,
std::shared_ptr<Payments::CreditsFormData> data,
Fn<void(std::optional<QString>)> done) {

View file

@ -41,7 +41,7 @@ void SendCreditsBox(
not_null<FlatLabel*> SetButtonMarkedLabel(
not_null<RpWidget*> button,
rpl::producer<TextWithEntities> text,
Fn<std::any(Fn<void()> update)> context,
Text::MarkedContext context,
const style::FlatLabel &st,
const style::color *textFg = nullptr);
@ -52,7 +52,7 @@ not_null<FlatLabel*> SetButtonMarkedLabel(
const style::FlatLabel &st,
const style::color *textFg = nullptr);
void SendStarGift(
void SendStarsForm(
not_null<Main::Session*> session,
std::shared_ptr<Payments::CreditsFormData> data,
Fn<void(std::optional<QString>)> done);

View file

@ -59,9 +59,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "window/window_session_controller.h"
#include "core/application.h"
#include "core/core_settings.h"
#include "styles/style_layers.h"
#include "styles/style_boxes.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_layers.h"
#include <QtCore/QMimeData>
@ -722,6 +722,18 @@ void SendFilesBox::openDialogToAddFileToAlbum() {
crl::guard(this, callback));
}
void SendFilesBox::refreshMessagesCount() {
const auto way = _sendWay.current();
const auto withCaption = _list.canAddCaption(
way.groupFiles() && way.sendImagesAsPhotos(),
way.sendImagesAsPhotos());
const auto withComment = !withCaption
&& _caption
&& !_caption->isHidden()
&& !_caption->getTextWithTags().text.isEmpty();
_messagesCount = _list.files.size() + (withComment ? 1 : 0);
}
void SendFilesBox::refreshButtons() {
clearButtons();
@ -730,6 +742,15 @@ void SendFilesBox::refreshButtons() {
? tr::lng_send_button()
: tr::lng_create_group_next()),
[=] { send({}); });
refreshMessagesCount();
const auto perMessage = _captionToPeer
? _captionToPeer->starsPerMessageChecked()
: 0;
if (perMessage > 0) {
_send->setText(PaidSendButtonText(_messagesCount.value(
) | rpl::map(rpl::mappers::_1 * perMessage)));
}
if (_sendType == Api::SendType::Normal) {
SendMenu::SetupMenuAndShortcuts(
_send,
@ -846,10 +867,9 @@ void SendFilesBox::refreshPriceTag() {
QString(),
st::paidTagLabel);
std::move(text) | rpl::start_with_next([=](TextWithEntities &&text) {
label->setMarkedText(text, Core::MarkedTextContext{
label->setMarkedText(text, Core::TextContext({
.session = session,
.customEmojiRepaint = [=] { label->update(); },
});
}));
}, label->lifetime());
label->show();
label->sizeValue() | rpl::start_with_next([=](QSize size) {
@ -1489,6 +1509,7 @@ void SendFilesBox::setupCaption() {
_caption->changes()
) | rpl::start_with_next([=] {
checkCharsLimitation();
refreshMessagesCount();
}, _caption->lifetime());
}

View file

@ -246,6 +246,7 @@ private:
void addPreparedAsyncFile(Ui::PreparedFile &&file);
void checkCharsLimitation();
void refreshMessagesCount();
[[nodiscard]] Fn<MenuDetails()> prepareSendMenuDetails(
const SendFilesBoxDescriptor &descriptor);
@ -261,6 +262,7 @@ private:
Ui::PreparedList _list;
std::optional<int> _removingIndex;
rpl::variable<int> _messagesCount;
SendFilesLimits _limits = {};
Fn<MenuDetails()> _sendMenuDetails;

View file

@ -123,12 +123,13 @@ private:
Ui::RoundImageCheckbox checkbox;
Ui::Text::String name;
Ui::Animations::Simple nameActive;
bool locked = false;
Api::MessageMoneyRestriction restriction;
RestrictionBadgeCache badgeCache;
};
void invalidateCache();
bool showLockedError(not_null<Chat*> chat);
void refreshLockedRows();
void refreshRestrictedRows();
[[nodiscard]] int displayedChatsCount() const;
[[nodiscard]] not_null<Data::Thread*> chatThread(
@ -137,7 +138,7 @@ private:
void paintChat(Painter &p, not_null<Chat*> chat, int index);
void updateChat(not_null<PeerData*> peer);
void updateChatName(not_null<Chat*> chat);
void initChatLocked(not_null<Chat*> chat);
void initChatRestriction(not_null<Chat*> chat);
void repaintChat(not_null<PeerData*> peer);
int chatIndex(not_null<PeerData*> peer) const;
void repaintChatAtIndex(int index);
@ -517,9 +518,19 @@ void ShareBox::keyPressEvent(QKeyEvent *e) {
SendMenu::Details ShareBox::sendMenuDetails() const {
const auto selected = _inner->selected();
const auto type = ranges::all_of(
selected | ranges::views::transform(&Data::Thread::peer),
HistoryView::CanScheduleUntilOnline)
const auto hasPaid = [&] {
for (const auto &thread : selected) {
if (thread->peer()->starsPerMessageChecked()) {
return true;
}
}
return false;
}();
const auto type = hasPaid
? SendMenu::Type::SilentOnly
: ranges::all_of(
selected | ranges::views::transform(&Data::Thread::peer),
HistoryView::CanScheduleUntilOnline)
? SendMenu::Type::ScheduledToUser
: (selected.size() == 1 && selected.front()->peer()->isSelf())
? SendMenu::Type::Reminder
@ -614,6 +625,9 @@ void ShareBox::createButtons() {
showMenu(send);
}
}, send->lifetime());
send->setText(PaidSendButtonText(
_starsToSend.value(),
tr::lng_share_confirm()));
} else if (_descriptor.copyCallback) {
addButton(_copyLinkText.value(), [=] { copyLink(); });
}
@ -657,6 +671,73 @@ void ShareBox::innerSelectedChanged(
}
void ShareBox::submit(Api::SendOptions options) {
_submitLifetime.destroy();
auto threads = _inner->selected();
const auto weak = Ui::MakeWeak(this);
const auto field = _comment->entity();
auto comment = field->getTextWithAppliedMarkdown();
const auto checkPaid = [=] {
if (!_descriptor.countMessagesCallback) {
return true;
}
const auto withPaymentApproved = crl::guard(weak, [=](int approved) {
auto copy = options;
copy.starsApproved = approved;
submit(copy);
});
const auto messagesCount = _descriptor.countMessagesCallback(
comment);
const auto alreadyApproved = options.starsApproved;
auto paid = std::vector<not_null<PeerData*>>();
auto waiting = base::flat_set<not_null<PeerData*>>();
auto totalStars = 0;
for (const auto &thread : threads) {
const auto peer = thread->peer();
const auto details = ComputePaymentDetails(peer, messagesCount);
if (!details) {
waiting.emplace(peer);
} else if (details->stars > 0) {
totalStars += details->stars;
paid.push_back(peer);
}
}
if (!waiting.empty()) {
_descriptor.session->changes().peerUpdates(
Data::PeerUpdate::Flag::FullInfo
) | rpl::start_with_next([=](const Data::PeerUpdate &update) {
if (waiting.contains(update.peer)) {
withPaymentApproved(alreadyApproved);
}
}, _submitLifetime);
if (!_descriptor.session->credits().loaded()) {
_descriptor.session->credits().loadedValue(
) | rpl::filter(
rpl::mappers::_1
) | rpl::take(1) | rpl::start_with_next([=] {
withPaymentApproved(alreadyApproved);
}, _submitLifetime);
}
return false;
} else if (totalStars > alreadyApproved) {
const auto show = uiShow();
const auto session = _descriptor.session;
const auto sessionShow = Main::MakeSessionShow(show, session);
const auto scheduleBoxSt = _descriptor.st.scheduleBox.get();
ShowSendPaidConfirm(sessionShow, paid, SendPaymentDetails{
.messages = messagesCount,
.stars = totalStars,
}, [=] { withPaymentApproved(totalStars); }, PaidConfirmStyles{
.label = (scheduleBoxSt
? scheduleBoxSt->chooseDateTimeArgs.labelStyle
: nullptr),
.checkbox = _descriptor.st.checkbox,
});
return false;
}
return true;
};
if (const auto onstack = _descriptor.submitCallback) {
const auto forwardOptions = (_forwardOptions.captionsCount
&& _forwardOptions.dropCaptions)
@ -665,8 +746,9 @@ void ShareBox::submit(Api::SendOptions options) {
? Data::ForwardOptions::NoSenderNames
: Data::ForwardOptions::PreserveInfo;
onstack(
_inner->selected(),
_comment->entity()->getTextWithAppliedMarkdown(),
std::move(threads),
checkPaid,
std::move(comment),
options,
forwardOptions);
}
@ -686,9 +768,23 @@ void ShareBox::selectedChanged() {
_comment->toggle(_hasSelected, anim::type::normal);
_comment->resizeToWidth(st::boxWideWidth);
}
computeStarsCount();
update();
}
void ShareBox::computeStarsCount() {
auto perMessage = 0;
for (const auto &thread : _inner->selected()) {
perMessage += thread->peer()->starsPerMessageChecked();
}
const auto messagesCount = _descriptor.countMessagesCallback
? _descriptor.countMessagesCallback(_comment
? _comment->entity()->getTextWithTags()
: TextWithTags())
: 0;
_starsToSend = perMessage * messagesCount;
}
void ShareBox::scrollTo(Ui::ScrollToRequest request) {
scrollToY(request.ymin, request.ymax);
//auto scrollTop = scrollArea()->scrollTop(), scrollBottom = scrollTop + scrollArea()->height();
@ -726,13 +822,13 @@ ShareBox::Inner::Inner(
_rowHeight = st::shareRowHeight;
setAttribute(Qt::WA_OpaquePaintEvent);
if (_descriptor.premiumRequiredError) {
if (_descriptor.moneyRestrictionError) {
const auto session = _descriptor.session;
rpl::merge(
Data::AmPremiumValue(session) | rpl::to_empty,
session->api().premium().somePremiumRequiredResolved()
session->api().premium().someMessageMoneyRestrictionsResolved()
) | rpl::start_with_next([=] {
refreshLockedRows();
refreshRestrictedRows();
}, lifetime());
}
@ -793,38 +889,36 @@ void ShareBox::Inner::invalidateCache() {
}
bool ShareBox::Inner::showLockedError(not_null<Chat*> chat) {
if (!chat->locked) {
if (!chat->restriction.premiumRequired) {
return false;
}
::Settings::ShowPremiumPromoToast(
Main::MakeSessionShow(_show, _descriptor.session),
ChatHelpers::ResolveWindowDefault(),
_descriptor.premiumRequiredError(chat->peer->asUser()).text,
_descriptor.moneyRestrictionError(chat->peer->asUser()).text,
u"require_premium"_q);
return true;
}
void ShareBox::Inner::refreshLockedRows() {
void ShareBox::Inner::refreshRestrictedRows() {
auto changed = false;
for (const auto &[peer, data] : _dataMap) {
const auto history = data->history;
const auto locked = (Api::ResolveRequiresPremiumToWrite(
const auto restriction = Api::ResolveMessageMoneyRestrictions(
history->peer,
history
) == Api::RequirePremiumState::Yes);
if (data->locked != locked) {
data->locked = locked;
history);
if (data->restriction != restriction) {
data->restriction = restriction;
changed = true;
}
}
for (const auto &data : d_byUsernameFiltered) {
const auto history = data->history;
const auto locked = (Api::ResolveRequiresPremiumToWrite(
const auto restriction = Api::ResolveMessageMoneyRestrictions(
history->peer,
history
) == Api::RequirePremiumState::Yes);
if (data->locked != locked) {
data->locked = locked;
history);
if (data->restriction != restriction) {
data->restriction = restriction;
changed = true;
}
}
@ -891,14 +985,14 @@ void ShareBox::Inner::updateChatName(not_null<Chat*> chat) {
chat->name.setText(_st.item.nameStyle, text, Ui::NameTextOptions());
}
void ShareBox::Inner::initChatLocked(not_null<Chat*> chat) {
if (_descriptor.premiumRequiredError) {
void ShareBox::Inner::initChatRestriction(not_null<Chat*> chat) {
if (_descriptor.moneyRestrictionError) {
const auto history = chat->history;
if (Api::ResolveRequiresPremiumToWrite(
const auto restriction = Api::ResolveMessageMoneyRestrictions(
history->peer,
history
) == Api::RequirePremiumState::Yes) {
chat->locked = true;
history);
if (restriction || restriction.known) {
chat->restriction = restriction;
}
}
}
@ -1024,14 +1118,15 @@ void ShareBox::Inner::loadProfilePhotos() {
void ShareBox::Inner::preloadUserpic(not_null<Dialogs::Entry*> entry) {
entry->chatListPreloadData();
const auto history = entry->asHistory();
if (!_descriptor.premiumRequiredError || !history) {
if (!_descriptor.moneyRestrictionError || !history) {
return;
} else if (Api::ResolveRequiresPremiumToWrite(
history->peer,
history
) == Api::RequirePremiumState::Unknown) {
const auto user = history->peer->asUser();
_descriptor.session->api().premium().resolvePremiumRequired(user);
} else if (!Api::ResolveMessageMoneyRestrictions(
history->peer,
history).known) {
if (const auto user = history->peer->asUser()) {
const auto api = &_descriptor.session->api();
api->premium().resolveMessageMoneyRestrictions(user);
}
}
}
@ -1054,7 +1149,7 @@ auto ShareBox::Inner::getChat(not_null<Dialogs::Row*> row)
repaintChat(peer);
}));
updateChatName(i->second.get());
initChatLocked(i->second.get());
initChatRestriction(i->second.get());
row->attached = i->second.get();
return i->second.get();
}
@ -1088,10 +1183,12 @@ void ShareBox::Inner::paintChat(
auto photoTop = st::sharePhotoTop;
chat->checkbox.paint(p, x + photoLeft, y + photoTop, outerWidth);
if (chat->locked) {
PaintPremiumRequiredLock(
if (chat->restriction) {
PaintRestrictionBadge(
p,
&_st.item,
chat->restriction.starsPerMessage,
chat->badgeCache,
x + photoLeft,
y + photoTop,
outerWidth,
@ -1446,7 +1543,7 @@ void ShareBox::Inner::peopleReceived(
_st.item,
[=] { repaintChat(peer); }));
updateChatName(d_byUsernameFiltered.back().get());
initChatLocked(d_byUsernameFiltered.back().get());
initChatRestriction(d_byUsernameFiltered.back().get());
}
}
};
@ -1499,6 +1596,15 @@ ChatHelpers::ForwardedMessagePhraseArgs CreateForwardedMessagePhraseArgs(
};
}
ShareBox::CountMessagesCallback ShareBox::DefaultForwardCountMessages(
not_null<History*> history,
MessageIdsList msgIds) {
return [=](const TextWithTags &comment) {
const auto items = history->owner().idsToItems(msgIds);
return int(items.size()) + (comment.empty() ? 0 : 1);
};
}
ShareBox::SubmitCallback ShareBox::DefaultForwardCallback(
std::shared_ptr<Ui::Show> show,
not_null<History*> history,
@ -1510,12 +1616,14 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback(
const auto state = std::make_shared<State>();
return [=](
std::vector<not_null<Data::Thread*>> &&result,
TextWithTags &&comment,
Fn<bool()> checkPaid,
TextWithTags comment,
Api::SendOptions options,
Data::ForwardOptions forwardOptions) {
if (!state->requests.empty()) {
return; // Share clicked already.
}
const auto items = history->owner().idsToItems(msgIds);
const auto existingIds = history->owner().itemsToIds(items);
if (existingIds.empty() || result.empty()) {
@ -1528,6 +1636,8 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback(
if (error.error) {
show->showBox(MakeSendErrorBox(error, result.size() > 1));
return;
} else if (!checkPaid()) {
return;
}
using Flag = MTPmessages_ForwardMessages::Flag;
@ -1576,6 +1686,12 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback(
: topicRootId;
const auto peer = thread->peer();
const auto threadHistory = thread->owningHistory();
const auto starsPaid = std::min(
peer->starsPerMessageChecked(),
options.starsApproved);
if (starsPaid) {
options.starsApproved -= starsPaid;
}
histories.sendRequest(threadHistory, requestType, [=](
Fn<void()> finish) {
const auto session = &threadHistory->session();
@ -1587,7 +1703,8 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback(
: Flag(0))
| (options.shortcutId
? Flag::f_quick_reply_shortcut
: Flag(0));
: Flag(0))
| (starsPaid ? Flag::f_allow_paid_stars : Flag());
threadHistory->sendRequestId = api.request(
MTPmessages_ForwardMessages(
MTP_flags(sendFlags),
@ -1599,7 +1716,8 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback(
MTP_int(options.scheduled),
MTP_inputPeerEmpty(), // send_as
Data::ShortcutIdToMTP(session, options.shortcutId),
MTP_int(videoTimestamp.value_or(0))
MTP_int(videoTimestamp.value_or(0)),
MTP_long(starsPaid)
)).done([=](const MTPUpdates &updates, mtpRequestId reqId) {
threadHistory->session().api().applyUpdates(updates);
state->requests.remove(reqId);
@ -1621,7 +1739,11 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback(
finish();
}).fail([=](const MTP::Error &error) {
if (error.type() == u"VOICE_MESSAGES_FORBIDDEN"_q) {
const auto type = error.type();
if (type.startsWith(u"ALLOW_PAYMENT_REQUIRED_"_q)) {
show->showToast(u"Payment requirements changed. "
"Please, try again."_q);
} else if (type == u"VOICE_MESSAGES_FORBIDDEN"_q) {
show->showToast(
tr::lng_restricted_send_voice_messages(
tr::now,
@ -1660,6 +1782,7 @@ ShareBoxStyleOverrides DarkShareBoxStyle() {
.comment = &st::groupCallShareBoxComment,
.peerList = &st::groupCallShareBoxList,
.label = &st::groupCallField,
.checkbox = &st::groupCallCheckbox,
.scheduleBox = std::make_shared<ScheduleBoxStyleArgs>(schedule()),
};
}
@ -1716,7 +1839,7 @@ void FastShareMessage(
const auto requiresInline = item->requiresSendInlineRight();
auto filterCallback = [=](not_null<Data::Thread*> thread) {
if (const auto user = thread->peer()->asUser()) {
if (user->canSendIgnoreRequirePremium()) {
if (user->canSendIgnoreMoneyRestrictions()) {
return true;
}
}
@ -1731,6 +1854,9 @@ void FastShareMessage(
show->show(Box<ShareBox>(ShareBox::Descriptor{
.session = session,
.copyCallback = std::move(copyLinkCallback),
.countMessagesCallback = ShareBox::DefaultForwardCountMessages(
history,
msgIds),
.submitCallback = ShareBox::DefaultForwardCallback(
show,
history,
@ -1742,7 +1868,7 @@ void FastShareMessage(
.captionsCount = ItemsForwardCaptionsCount(items),
.show = !hasOnlyForcedForwardedInfo,
},
.premiumRequiredError = SharePremiumRequiredError(),
.moneyRestrictionError = ShareMessageMoneyRestrictionError(),
}), Ui::LayerOption::CloseOther);
}
@ -1770,8 +1896,12 @@ void FastShareLink(
QGuiApplication::clipboard()->setText(url);
show->showToast(tr::lng_background_link_copied(tr::now));
};
auto countMessagesCallback = [=](const TextWithTags &comment) {
return 1;
};
auto submitCallback = [=](
std::vector<not_null<::Data::Thread*>> &&result,
Fn<bool()> checkPaid,
TextWithTags &&comment,
Api::SendOptions options,
::Data::ForwardOptions) {
@ -1788,6 +1918,8 @@ void FastShareLink(
MakeSendErrorBox(error, result.size() > 1));
}
return;
} else if (!checkPaid()) {
return;
}
*sending = true;
@ -1815,7 +1947,7 @@ void FastShareLink(
};
auto filterCallback = [](not_null<::Data::Thread*> thread) {
if (const auto user = thread->peer()->asUser()) {
if (user->canSendIgnoreRequirePremium()) {
if (user->canSendIgnoreMoneyRestrictions()) {
return true;
}
}
@ -1825,16 +1957,17 @@ void FastShareLink(
Box<ShareBox>(ShareBox::Descriptor{
.session = &show->session(),
.copyCallback = std::move(copyCallback),
.countMessagesCallback = std::move(countMessagesCallback),
.submitCallback = std::move(submitCallback),
.filterCallback = std::move(filterCallback),
.st = st,
.premiumRequiredError = SharePremiumRequiredError(),
.moneyRestrictionError = ShareMessageMoneyRestrictionError(),
}),
Ui::LayerOption::KeepOther,
anim::type::normal);
}
auto SharePremiumRequiredError()
-> Fn<RecipientPremiumRequiredError(not_null<UserData*>)> {
return WritePremiumRequiredError;
auto ShareMessageMoneyRestrictionError()
-> Fn<RecipientMoneyRestrictionError(not_null<UserData*>)> {
return WriteMoneyRestrictionError;
}

View file

@ -66,6 +66,7 @@ struct ShareBoxStyleOverrides {
const style::InputField *comment = nullptr;
const style::PeerList *peerList = nullptr;
const style::InputField *label = nullptr;
const style::Checkbox *checkbox = nullptr;
std::shared_ptr<HistoryView::ScheduleBoxStyleArgs> scheduleBox;
};
[[nodiscard]] ShareBoxStyleOverrides DarkShareBoxStyle();
@ -87,20 +88,25 @@ void FastShareLink(
const QString &url,
ShareBoxStyleOverrides st = {});
struct RecipientPremiumRequiredError;
[[nodiscard]] auto SharePremiumRequiredError()
-> Fn<RecipientPremiumRequiredError(not_null<UserData*>)>;
struct RecipientMoneyRestrictionError;
[[nodiscard]] auto ShareMessageMoneyRestrictionError()
-> Fn<RecipientMoneyRestrictionError(not_null<UserData*>)>;
class ShareBox final : public Ui::BoxContent {
public:
using CopyCallback = Fn<void()>;
using CountMessagesCallback = Fn<int(const TextWithTags&)>;
using SubmitCallback = Fn<void(
std::vector<not_null<Data::Thread*>>&&,
Fn<bool()> checkPaid,
TextWithTags&&,
Api::SendOptions,
Data::ForwardOptions)>;
using FilterCallback = Fn<bool(not_null<Data::Thread*>)>;
[[nodiscard]] static auto DefaultForwardCountMessages(
not_null<History*> history,
MessageIdsList msgIds) -> CountMessagesCallback;
[[nodiscard]] static SubmitCallback DefaultForwardCallback(
std::shared_ptr<Ui::Show> show,
not_null<History*> history,
@ -110,6 +116,7 @@ public:
struct Descriptor {
not_null<Main::Session*> session;
CopyCallback copyCallback;
CountMessagesCallback countMessagesCallback;
SubmitCallback submitCallback;
FilterCallback filterCallback;
object_ptr<Ui::RpWidget> bottomWidget = { nullptr };
@ -123,8 +130,9 @@ public:
bool show = false;
} forwardOptions;
using PremiumRequiredError = RecipientPremiumRequiredError;
Fn<PremiumRequiredError(not_null<UserData*>)> premiumRequiredError;
using MoneyRestrictionError = RecipientMoneyRestrictionError;
Fn<MoneyRestrictionError(
not_null<UserData*>)> moneyRestrictionError;
};
ShareBox(QWidget*, Descriptor &&descriptor);
@ -149,6 +157,7 @@ private:
void needSearchByUsername();
void applyFilterUpdate(const QString &query);
void selectedChanged();
void computeStarsCount();
void createButtons();
int getTopScrollSkip() const;
int getBottomScrollSkip() const;
@ -180,6 +189,7 @@ private:
bool _hasSelected = false;
rpl::variable<QString> _copyLinkText;
rpl::variable<int> _starsToSend;
base::Timer _searchTimer;
QString _peopleQuery;
@ -195,5 +205,6 @@ private:
PeopleQueries _peopleQueries;
Ui::Animations::Simple _scrollAnimation;
rpl::lifetime _submitLifetime;
};

View file

@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "chat_helpers/tabbed_panel.h"
#include "chat_helpers/tabbed_selector.h"
#include "core/ui_integration.h"
#include "data/data_changes.h"
#include "data/data_channel.h"
#include "data/data_credits.h"
#include "data/data_document.h"
@ -80,6 +81,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/widgets/checkbox.h"
#include "ui/widgets/popup_menu.h"
#include "ui/widgets/shadow.h"
#include "ui/wrap/slide_wrap.h"
#include "window/themes/window_theme.h"
#include "window/section_widget.h"
#include "window/window_session_controller.h"
@ -106,6 +108,7 @@ constexpr auto kSentToastDuration = 3 * crl::time(1000);
constexpr auto kSwitchUpgradeCoverInterval = 3 * crl::time(1000);
constexpr auto kCrossfadeDuration = crl::time(400);
constexpr auto kUpgradeDoneToastDuration = 4 * crl::time(1000);
constexpr auto kGiftsPreloadTimeout = 3 * crl::time(1000);
using namespace HistoryView;
using namespace Info::PeerGifts;
@ -126,6 +129,7 @@ struct GiftDetails {
uint64 randomId = 0;
bool anonymous = false;
bool upgraded = false;
bool byStars = false;
};
class PreviewDelegate final : public DefaultElementDelegate {
@ -227,7 +231,7 @@ auto GenerateGiftMedia(
TextWithEntities text,
QMargins margins = {},
const base::flat_map<uint16, ClickHandlerPtr> &links = {},
const std::any &context = {}) {
Ui::Text::MarkedContext context = {}) {
if (text.empty()) {
return;
}
@ -236,7 +240,7 @@ auto GenerateGiftMedia(
margins,
st::defaultTextStyle,
links,
context));
std::move(context)));
};
const auto sticker = [=] {
@ -306,10 +310,10 @@ auto GenerateGiftMedia(
auto description = data.text.empty()
? std::move(textFallback)
: data.text;
const auto context = Core::MarkedTextContext{
const auto context = Core::TextContext({
.session = &parent->history()->session(),
.customEmojiRepaint = [parent] { parent->repaint(); },
};
.repaint = [parent] { parent->repaint(); },
});
pushText(
std::move(title),
st::giftBoxPreviewTitlePadding,
@ -495,7 +499,14 @@ void PreviewWrap::prepare(rpl::producer<GiftDetails> details) {
std::move(details) | rpl::start_with_next([=](GiftDetails details) {
const auto &descriptor = details.descriptor;
const auto cost = v::match(descriptor, [&](GiftTypePremium data) {
return FillAmountAndCurrency(data.cost, data.currency, true);
const auto stars = (details.byStars && data.stars)
? data.stars
: (data.currency == kCreditsCurrency)
? data.cost
: 0;
return stars
? tr::lng_gift_stars_title(tr::now, lt_count, stars)
: FillAmountAndCurrency(data.cost, data.currency, true);
}, [&](GiftTypeStars data) {
const auto stars = data.info.stars
+ (details.upgraded ? data.info.starsToUpgrade : 0);
@ -622,14 +633,27 @@ void PreviewWrap::paintEvent(QPaintEvent *e) {
list.reserve(options.size());
auto minMonthsGift = GiftTypePremium();
for (const auto &option : options) {
list.push_back({
.cost = option.cost,
.currency = option.currency,
.months = option.months,
});
if (!minMonthsGift.months
|| option.months < minMonthsGift.months) {
minMonthsGift = list.back();
if (option.currency != kCreditsCurrency) {
list.push_back({
.cost = option.cost,
.currency = option.currency,
.months = option.months,
});
if (!minMonthsGift.months
|| option.months < minMonthsGift.months) {
minMonthsGift = list.back();
}
}
}
for (const auto &option : options) {
if (option.currency == kCreditsCurrency) {
const auto i = ranges::find(
list,
option.months,
&GiftTypePremium::months);
if (i != end(list)) {
i->stars = option.cost;
}
}
}
for (auto &gift : list) {
@ -735,15 +759,11 @@ void PreviewWrap::paintEvent(QPaintEvent *e) {
}
auto &manager = session->data().customEmojiManager();
auto result = Text::String();
const auto context = Core::MarkedTextContext{
.session = session,
.customEmojiRepaint = [] {},
};
result.setMarkedText(
st::semiboldTextStyle,
manager.creditsEmoji().append(QString::number(price)),
kMarkupTextOptions,
context);
Core::TextContext({ .session = session }));
return result;
}
@ -1103,16 +1123,35 @@ void SendGift(
std::shared_ptr<Api::PremiumGiftCodeOptions> api,
const GiftDetails &details,
Fn<void(Payments::CheckoutResult)> done) {
const auto processNonPanelPaymentFormFactory
= Payments::ProcessNonPanelPaymentFormFactory(window, done);
v::match(details.descriptor, [&](const GiftTypePremium &gift) {
auto invoice = api->invoice(1, gift.months);
invoice.purpose = Payments::InvoicePremiumGiftCodeUsers{
.users = { peer->asUser() },
.message = details.text,
};
Payments::CheckoutProcess::Start(std::move(invoice), done);
if (details.byStars && gift.stars) {
auto invoice = Payments::InvoicePremiumGiftCode{
.purpose = Payments::InvoicePremiumGiftCodeUsers{
.users = { peer->asUser() },
.message = details.text,
},
.currency = Ui::kCreditsCurrency,
.randomId = details.randomId,
.amount = uint64(gift.stars),
.storeQuantity = 1,
.users = 1,
.months = gift.months,
};
Payments::CheckoutProcess::Start(
std::move(invoice),
done,
processNonPanelPaymentFormFactory);
} else {
auto invoice = api->invoice(1, gift.months);
invoice.purpose = Payments::InvoicePremiumGiftCodeUsers{
.users = { peer->asUser() },
.message = details.text,
};
Payments::CheckoutProcess::Start(std::move(invoice), done);
}
}, [&](const GiftTypeStars &gift) {
const auto processNonPanelPaymentFormFactory
= Payments::ProcessNonPanelPaymentFormFactory(window, done);
Payments::CheckoutProcess::Start(Payments::InvoiceStarGift{
.giftId = gift.info.id,
.randomId = details.randomId,
@ -1279,12 +1318,6 @@ void AddUpgradeButton(
button->toggleOn(rpl::single(false))->toggledValue(
) | rpl::start_with_next(toggled, button->lifetime());
const auto makeContext = [session](Fn<void()> update) {
return Core::MarkedTextContext{
.session = session,
.customEmojiRepaint = std::move(update),
};
};
auto star = session->data().customEmojiManager().creditsEmoji();
const auto label = Ui::CreateChild<Ui::FlatLabel>(
button,
@ -1296,7 +1329,7 @@ void AddUpgradeButton(
Text::WithEntities),
st::boxLabel,
st::defaultPopupMenu,
std::move(makeContext));
Core::TextContext({ .session = session }));
label->show();
label->setAttribute(Qt::WA_TransparentForMouseEvents);
button->widthValue() | rpl::start_with_next([=](int outer) {
@ -1424,6 +1457,7 @@ void SendGiftBox(
struct State {
rpl::variable<GiftDetails> details;
rpl::variable<bool> messageAllowed;
std::shared_ptr<Data::DocumentMedia> media;
bool submitting = false;
};
@ -1432,13 +1466,25 @@ void SendGiftBox(
.descriptor = descriptor,
.randomId = base::RandomValue<uint64>(),
};
peer->updateFull();
state->messageAllowed = peer->session().changes().peerFlagsValue(
peer,
Data::PeerUpdate::Flag::StarsPerMessage
) | rpl::map([=] {
return peer->starsPerMessageChecked() == 0;
});
auto cost = state->details.value(
) | rpl::map([session](const GiftDetails &details) {
return v::match(details.descriptor, [&](const GiftTypePremium &data) {
if (data.currency == kCreditsCurrency) {
const auto stars = (details.byStars && data.stars)
? data.stars
: (data.currency == kCreditsCurrency)
? data.cost
: 0;
if (stars) {
return CreditsEmojiSmall(session).append(
Lang::FormatCountDecimal(std::abs(data.cost)));
Lang::FormatCountDecimal(std::abs(stars)));
}
return TextWithEntities{
FillAmountAndCurrency(data.cost, data.currency),
@ -1462,10 +1508,17 @@ void SendGiftBox(
peer,
state->details.value()));
const auto messageWrap = container->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
container,
object_ptr<Ui::VerticalLayout>(container)));
messageWrap->toggleOn(state->messageAllowed.value());
messageWrap->finishAnimating();
const auto messageInner = messageWrap->entity();
const auto limit = StarGiftMessageLimit(session);
const auto text = AddPartInput(
window,
container,
messageInner,
box->getDelegate()->outerContainer(),
tr::lng_gift_send_message(),
QString(),
@ -1509,7 +1562,6 @@ void SendGiftBox(
text,
session,
{ .suggestCustomEmoji = true, .allowCustomWithoutPremium = allow });
if (stars) {
const auto cost = stars->info.starsToUpgrade;
if (cost > 0 && !peer->isSelf()) {
@ -1551,20 +1603,73 @@ void SendGiftBox(
}, container->lifetime());
AddSkip(container);
}
v::match(descriptor, [&](const GiftTypePremium &) {
AddDividerText(container, tr::lng_gift_send_premium_about(
v::match(descriptor, [&](const GiftTypePremium &data) {
AddDividerText(messageInner, tr::lng_gift_send_premium_about(
lt_user,
rpl::single(peer->shortName())));
if (const auto byStars = data.stars) {
const auto star = Ui::Text::IconEmoji(&st::starIconEmojiColored);
AddSkip(container);
container->add(
object_ptr<SettingsButton>(
container,
tr::lng_gift_send_pay_with_stars(
lt_amount,
rpl::single(base::duplicate(star).append(Lang::FormatCountDecimal(byStars))),
Ui::Text::WithEntities),
st::settingsButtonNoIcon)
)->toggleOn(rpl::single(false))->toggledValue(
) | rpl::start_with_next([=](bool toggled) {
auto now = state->details.current();
now.byStars = toggled;
state->details = std::move(now);
}, container->lifetime());
AddSkip(container);
const auto balance = AddDividerText(
container,
tr::lng_gift_send_stars_balance(
lt_amount,
peer->session().credits().balanceValue(
) | rpl::map([=](StarsAmount amount) {
return base::duplicate(star).append(
Lang::FormatStarsAmountDecimal(amount));
}),
lt_link,
tr::lng_gift_send_stars_balance_link(
) | Ui::Text::ToLink(),
Ui::Text::WithEntities));
struct State {
Settings::BuyStarsHandler buyStars;
rpl::variable<bool> loading;
};
const auto state = balance->lifetime().make_state<State>();
state->loading = state->buyStars.loadingValue();
balance->setClickHandlerFilter([=](const auto &...) {
if (!state->loading.current()) {
state->buyStars.handler(window->uiShow())();
}
return false;
});
}
}, [&](const GiftTypeStars &) {
AddDividerText(container, peer->isSelf()
? tr::lng_gift_send_anonymous_self()
: peer->isBroadcast()
? tr::lng_gift_send_anonymous_about_channel()
: tr::lng_gift_send_anonymous_about(
lt_user,
rpl::single(peer->shortName()),
lt_recipient,
rpl::single(peer->shortName())));
: rpl::conditional(
state->messageAllowed.value(),
tr::lng_gift_send_anonymous_about(
lt_user,
rpl::single(peer->shortName()),
lt_recipient,
rpl::single(peer->shortName())),
tr::lng_gift_send_anonymous_about_paid(
lt_user,
rpl::single(peer->shortName()),
lt_recipient,
rpl::single(peer->shortName()))));
});
const auto buttonWidth = st::boxWideWidth
@ -1575,13 +1680,20 @@ void SendGiftBox(
return;
}
state->submitting = true;
const auto details = state->details.current();
auto details = state->details.current();
if (!state->messageAllowed.current()) {
details.text = {};
}
const auto weak = MakeWeak(box);
const auto done = [=](Payments::CheckoutResult result) {
if (result == Payments::CheckoutResult::Paid) {
if (details.byStars
|| v::is<GiftTypeStars>(details.descriptor)) {
window->session().credits().load(true);
}
const auto copy = state->media;
window->showPeerHistory(peer);
ShowSentToast(window, descriptor, details);
ShowSentToast(window, details.descriptor, details);
}
if (const auto strong = weak.data()) {
box->closeBox();
@ -1814,6 +1926,8 @@ void GiftBox(
box->setCustomCornersFilling(RectPart::FullTop);
box->addButton(tr::lng_create_group_back(), [=] { box->closeBox(); });
window->session().credits().load();
FillBg(box);
const auto &stUser = st::premiumGiftsUserpicButton;
@ -2070,7 +2184,66 @@ void ChooseStarGiftRecipient(
void ShowStarGiftBox(
not_null<Window::SessionController*> controller,
not_null<PeerData*> peer) {
controller->show(Box(GiftBox, controller, peer));
struct Session {
PeerData *peer = nullptr;
bool premiumGiftsReady = false;
bool starsGiftsReady = false;
rpl::lifetime lifetime;
};
static auto Map = base::flat_map<not_null<Main::Session*>, Session>();
const auto session = &controller->session();
auto i = Map.find(session);
if (i == end(Map)) {
i = Map.emplace(session).first;
session->lifetime().add([=] { Map.remove(session); });
} else if (i->second.peer == peer) {
return;
}
i->second = Session{ .peer = peer };
const auto weak = base::make_weak(controller);
const auto show = [=] {
Map[session] = Session();
if (const auto strong = weak.get()) {
strong->show(Box(GiftBox, strong, peer));
}
};
base::timer_once(
kGiftsPreloadTimeout
) | rpl::start_with_next(show, i->second.lifetime);
const auto user = peer->asUser();
if (user && !user->isSelf()) {
GiftsPremium(
session,
peer
) | rpl::start_with_next([=](PremiumGiftsDescriptor &&gifts) {
if (!gifts.list.empty()) {
auto &entry = Map[session];
entry.premiumGiftsReady = true;
if (entry.starsGiftsReady) {
show();
}
}
}, i->second.lifetime);
} else {
i->second.premiumGiftsReady = true;
}
GiftsStars(
session,
peer
) | rpl::start_with_next([=](std::vector<GiftTypeStars> &&gifts) {
if (!gifts.empty()) {
auto &entry = Map[session];
entry.starsGiftsReady = true;
if (entry.premiumGiftsReady) {
show();
}
}
}, i->second.lifetime);
}
void AddUniqueGiftCover(

View file

@ -771,30 +771,43 @@ void StickerSetBox::updateButtons() {
const auto addPackOwner = [=](const std::shared_ptr<base::unique_qptr<Ui::PopupMenu>> &menu)
{
if (type == Data::StickersType::Stickers || type == Data::StickersType::Emoji) {
const auto pointer = Ui::MakeWeak(this);
const auto weak = Ui::MakeWeak(this);
const auto session = _session;
const auto innerId = _inner->setId() >> 32;
(*menu)->addAction(
tr::ayu_MessageDetailsPackOwnerPC(tr::now),
[=]
[weak, session, innerId]
{
if (!pointer) {
if (!weak) {
return;
}
const auto strong = weak.data();
if (!strong) {
return;
}
searchById(
_inner->setId() >> 32,
_session,
[=](const QString &username, UserData *user)
innerId,
session,
[session, weak](const QString &username, UserData *user)
{
if (!pointer) {
if (!weak) {
return;
}
const auto strongInner = weak.data();
if (!strongInner) {
return;
}
if (!user) {
showToast(tr::ayu_UserNotFoundMessage(tr::now));
strongInner->showToast(tr::ayu_UserNotFoundMessage(tr::now));
return;
}
if (const auto window = _session->tryResolveWindow()) {
if (const auto window = session->tryResolveWindow()) {
if (const auto mainWidget = window->widget()->sessionController()) {
mainWidget->showPeer(user);
}

View file

@ -150,10 +150,7 @@ void TranslateBox(
original->entity()->setAnimationsPausedCallback(animationsPaused);
original->entity()->setMarkedText(
text,
Core::MarkedTextContext{
.session = &peer->session(),
.customEmojiRepaint = [=] { original->entity()->update(); },
});
Core::TextContext({ .session = &peer->session() }));
original->setMinimalHeight(lineHeight);
original->hide(anim::type::instant);
@ -221,10 +218,7 @@ void TranslateBox(
const auto label = translated->entity();
label->setMarkedText(
text,
Core::MarkedTextContext{
.session = &peer->session(),
.customEmojiRepaint = [=] { label->update(); },
});
Core::TextContext({ .session = &peer->session() }));
translated->show(anim::type::instant);
loading->hide(anim::type::instant);
};

View file

@ -132,8 +132,12 @@ object_ptr<ShareBox> ShareInviteLinkBox(
QGuiApplication::clipboard()->setText(currentLink());
show->showToast(tr::lng_group_invite_copied(tr::now));
};
auto countMessagesCallback = [=](const TextWithTags &comment) {
return 1;
};
auto submitCallback = [=](
std::vector<not_null<Data::Thread*>> &&result,
Fn<bool()> checkPaid,
TextWithTags &&comment,
Api::SendOptions options,
Data::ForwardOptions) {
@ -150,6 +154,8 @@ object_ptr<ShareBox> ShareInviteLinkBox(
MakeSendErrorBox(error, result.size() > 1));
}
return;
} else if (!checkPaid()) {
return;
}
*sending = true;
@ -178,7 +184,7 @@ object_ptr<ShareBox> ShareInviteLinkBox(
};
auto filterCallback = [](not_null<Data::Thread*> thread) {
if (const auto user = thread->peer()->asUser()) {
if (user->canSendIgnoreRequirePremium()) {
if (user->canSendIgnoreMoneyRestrictions()) {
return true;
}
}
@ -189,6 +195,7 @@ object_ptr<ShareBox> ShareInviteLinkBox(
auto result = Box<ShareBox>(ShareBox::Descriptor{
.session = &peer->session(),
.copyCallback = std::move(copyCallback),
.countMessagesCallback = std::move(countMessagesCallback),
.submitCallback = std::move(submitCallback),
.filterCallback = std::move(filterCallback),
.bottomWidget = std::move(bottom),
@ -199,7 +206,7 @@ object_ptr<ShareBox> ShareInviteLinkBox(
tr::lng_group_call_copy_speaker_link(),
tr::lng_group_call_copy_listener_link()),
.st = st.shareBox ? *st.shareBox : ShareBoxStyleOverrides(),
.premiumRequiredError = SharePremiumRequiredError(),
.moneyRestrictionError = ShareMessageMoneyRestrictionError(),
});
*box = result.data();
return result;

View file

@ -149,6 +149,7 @@ EmojiButton {
SendButton {
inner: IconButton;
stars: RoundButton;
record: icon;
recordOver: icon;
round: icon;
@ -855,6 +856,10 @@ historyComposeButton: FlatButton {
color: historyComposeButtonBgRipple;
}
}
historyComposeButtonText: FlatLabel(defaultFlatLabel) {
style: semiboldTextStyle;
textFg: windowActiveTextFg;
}
historyGiftToChannel: IconButton(defaultIconButton) {
width: 46px;
height: 46px;
@ -913,6 +918,10 @@ historyBusinessBotSettings: IconButton(defaultIconButton) {
height: 58px;
width: 48px;
}
paysStatusLabel: FlatLabel(historyBusinessBotStatus) {
align: align(top);
minWidth: 240px;
}
historyReplyCancelIcon: icon {{ "box_button_close", historyReplyCancelFg }};
historyReplyCancelIconOver: icon {{ "box_button_close", historyReplyCancelFgOver }};
@ -1289,6 +1298,12 @@ historySend: SendButton {
icon: historySendIcon;
iconOver: historySendIconOver;
}
stars: RoundButton(defaultActiveButton) {
height: 28px;
padding: margins(0px, 0px, 6px, 0px);
textTop: 5px;
width: -8px;
}
record: historyRecordVoice;
recordOver: historyRecordVoiceOver;
round: historyRecordRound;

View file

@ -1747,7 +1747,8 @@ void InitFieldAutocomplete(
&& peer->isUser()
&& !peer->asUser()->isBot()
&& (!shortcutMessages
|| shortcutMessages->shortcuts().list.empty())) {
|| shortcutMessages->shortcuts().list.empty()
|| peer->starsPerMessageChecked() != 0)) {
parsed = {};
}
raw->showFiltered(peer, parsed.query, parsed.fromStart);

View file

@ -695,14 +695,15 @@ GifsListWidget::LayoutItem *GifsListWidget::layoutPrepareSavedGif(
}
GifsListWidget::LayoutItem *GifsListWidget::layoutPrepareInlineResult(
not_null<InlineResult*> result) {
auto it = _inlineLayouts.find(result);
std::shared_ptr<InlineResult> result) {
const auto raw = result.get();
auto it = _inlineLayouts.find(raw);
if (it == _inlineLayouts.cend()) {
if (auto layout = LayoutItem::createLayout(
this,
result,
std::move(result),
_inlineWithThumb)) {
it = _inlineLayouts.emplace(result, std::move(layout)).first;
it = _inlineLayouts.emplace(raw, std::move(layout)).first;
it->second->initDimensions();
} else {
return nullptr;
@ -775,8 +776,8 @@ int GifsListWidget::refreshInlineRows(const InlineCacheEntry *entry, bool result
from,
count
) | ranges::views::transform([&](
const std::unique_ptr<InlineBots::Result> &r) {
return layoutPrepareInlineResult(r.get());
const std::shared_ptr<InlineBots::Result> &r) {
return layoutPrepareInlineResult(r);
}) | ranges::views::filter([](const LayoutItem *item) {
return item != nullptr;
}) | ranges::to<std::vector<not_null<LayoutItem*>>>;
@ -799,7 +800,7 @@ int GifsListWidget::validateExistingInlineRows(const InlineResults &results) {
const auto until = _mosaic.validateExistingRows([&](
not_null<const LayoutItem*> item,
int untilIndex) {
return item->getResult() != results[untilIndex].get();
return item->getResult().get() != results[untilIndex].get();
}, results.size());
if (_mosaic.empty()) {

View file

@ -131,7 +131,7 @@ private:
};
using InlineResult = InlineBots::Result;
using InlineResults = std::vector<std::unique_ptr<InlineResult>>;
using InlineResults = std::vector<std::shared_ptr<InlineResult>>;
using LayoutItem = InlineBots::Layout::ItemBase;
struct InlineCacheEntry {
@ -162,7 +162,8 @@ private:
void clearInlineRows(bool resultsDeleted);
LayoutItem *layoutPrepareSavedGif(not_null<DocumentData*> document);
LayoutItem *layoutPrepareInlineResult(not_null<InlineResult*> result);
LayoutItem *layoutPrepareInlineResult(
std::shared_ptr<InlineResult> result);
void deleteUnusedGifLayouts();

View file

@ -42,6 +42,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/style_boxes.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_credits.h"
#include "styles/style_settings.h"
#include "base/qt/qt_common_adapters.h"
@ -432,12 +433,9 @@ void InitMessageFieldHandlers(MessageFieldHandlersArgs &&args) {
const auto session = args.session;
field->setTagMimeProcessor(
FieldTagMimeProcessor(session, args.allowPremiumEmoji));
field->setCustomTextContext([=](Fn<void()> repaint) {
return std::any(Core::MarkedTextContext{
.session = session,
.customEmojiRepaint = std::move(repaint),
});
}, [paused] {
field->setCustomTextContext(Core::TextContext({
.session = session
}), [paused] {
return On(PowerSaving::kEmojiChat) || paused();
}, [paused] {
return On(PowerSaving::kChatSpoiler) || paused();
@ -1280,3 +1278,26 @@ void SelectTextInFieldWithMargins(
textCursor.setPosition(selection.to, QTextCursor::KeepAnchor);
field->setTextCursor(textCursor);
}
TextWithEntities PaidSendButtonText(tr::now_t, int stars) {
return Ui::Text::IconEmoji(&st::starIconEmoji).append(
Lang::FormatCountToShort(stars).string);
}
rpl::producer<TextWithEntities> PaidSendButtonText(
rpl::producer<int> stars,
rpl::producer<QString> fallback) {
if (fallback) {
return rpl::combine(
std::move(fallback),
std::move(stars)
) | rpl::map([=](QString zero, int count) {
return count
? PaidSendButtonText(tr::now, count)
: TextWithEntities{ zero };
});
}
return std::move(stars) | rpl::map([=](int count) {
return PaidSendButtonText(tr::now, count);
});
}

View file

@ -19,6 +19,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include <QtGui/QClipboard>
namespace tr {
struct now_t;
} // namespace tr
namespace Main {
class Session;
class SessionShow;
@ -169,3 +173,8 @@ private:
void SelectTextInFieldWithMargins(
not_null<Ui::InputField*> field,
const TextSelection &selection);
[[nodiscard]] TextWithEntities PaidSendButtonText(tr::now_t, int stars);
[[nodiscard]] rpl::producer<TextWithEntities> PaidSendButtonText(
rpl::producer<int> stars,
rpl::producer<QString> fallback = nullptr);

View file

@ -37,7 +37,7 @@ std::map<int, const char*> BetaLogs() {
"- Nice looking code blocks with syntax highlight.\n"
"- Copy full code block by click on its header.\n"
"- Send a highlighted code block using ```language syntax.\n"
}
};

View file

@ -205,13 +205,13 @@ void BotGameUrlClickHandler::onClick(ClickContext context) const {
});
};
if (_bot->isVerified()
|| _bot->session().local().isBotTrustedOpenGame(_bot->id)) {
|| _bot->session().local().isPeerTrustedOpenGame(_bot->id)) {
openGame();
} else {
if (const auto controller = my.sessionWindow.get()) {
const auto callback = [=, bot = _bot](Fn<void()> close) {
close();
bot->session().local().markBotTrustedOpenGame(bot->id);
bot->session().local().markPeerTrustedOpenGame(bot->id);
openGame();
};
controller->show(Ui::MakeConfirmBox({

View file

@ -25,6 +25,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/boxes/edit_birthday_box.h"
#include "ui/integration.h"
#include "payments/payments_non_panel_process.h"
#include "boxes/peers/edit_peer_info_box.h"
#include "boxes/share_box.h"
#include "boxes/connection_box.h"
#include "boxes/gift_premium_box.h"
@ -68,6 +69,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "main/main_domain.h"
#include "main/main_session.h"
#include "main/main_session_settings.h"
#include "info/info_controller.h"
#include "info/info_memento.h"
#include "inline_bots/bot_attach_web_view.h"
#include "history/history.h"
#include "history/history_item.h"
@ -1018,6 +1021,45 @@ bool CopyUsername(
return true;
}
bool EditPaidMessagesFee(
Window::SessionController *controller,
const Match &match,
const QVariant &context) {
if (!controller) {
return false;
}
const auto peerId = PeerId(match->captured(1).toULongLong());
if (const auto id = peerToChannel(peerId)) {
const auto channel = controller->session().data().channelLoaded(id);
if (channel && channel->canEditPermissions()) {
ShowEditChatPermissions(controller, channel);
}
} else {
controller->show(Box(EditMessagesPrivacyBox, controller));
}
return true;
}
bool ShowCommonGroups(
Window::SessionController *controller,
const Match &match,
const QVariant &context) {
if (!controller) {
return false;
}
const auto peerId = PeerId(match->captured(1).toULongLong());
if (const auto id = peerToUser(peerId)) {
const auto user = controller->session().data().userLoaded(id);
if (user) {
controller->showSection(
std::make_shared<Info::Memento>(
user,
Info::Section::Type::CommonGroups));
}
}
return true;
}
bool ShowStarsExamples(
Window::SessionController *controller,
const Match &match,
@ -1529,6 +1571,14 @@ const std::vector<LocalUrlHandler> &InternalUrlHandlers() {
u"^username_regular/([a-zA-Z0-9\\-\\_\\.]+)@([0-9]+)$"_q,
CopyUsername,
},
{
u"^edit_paid_messages_fee/([0-9]+)$"_q,
EditPaidMessagesFee,
},
{
u"^common_groups/([0-9]+)$"_q,
ShowCommonGroups,
},
{
u"^stars_examples$"_q,
ShowStarsExamples,

View file

@ -127,16 +127,30 @@ ResolvePhoneAction::ResolvePhoneAction(
return;
}
searchById(possibleId,
&controller->session(),
[=](const QString &username, UserData *user)
{
if (user) {
_peer = user;
}
const auto weak = Ui::MakeWeak(this);
const auto session = &controller->session();
_loaded.force_assign(true);
});
searchById(
possibleId,
session,
[session, weak, possibleId](const QString &username, UserData *user)
{
if (!weak) {
return;
}
const auto strong = weak.data();
if (!strong) {
return;
}
if (user) {
strong->_peer = user;
}
strong->_loaded.force_assign(true);
}
);
}
}).send();
}

View file

@ -76,6 +76,14 @@ const auto CommandByName = base::flat_map<QString, Command>{
{ u"first_chat"_q , Command::ChatFirst },
{ u"last_chat"_q , Command::ChatLast },
{ u"self_chat"_q , Command::ChatSelf },
{ u"pinned_chat1"_q , Command::ChatPinned1 },
{ u"pinned_chat2"_q , Command::ChatPinned2 },
{ u"pinned_chat3"_q , Command::ChatPinned3 },
{ u"pinned_chat4"_q , Command::ChatPinned4 },
{ u"pinned_chat5"_q , Command::ChatPinned5 },
{ u"pinned_chat6"_q , Command::ChatPinned6 },
{ u"pinned_chat7"_q , Command::ChatPinned7 },
{ u"pinned_chat8"_q , Command::ChatPinned8 },
{ u"previous_folder"_q , Command::FolderPrevious },
{ u"next_folder"_q , Command::FolderNext },
@ -168,6 +176,7 @@ private:
void set(const QKeySequence &result, Command command, bool replace);
void remove(const QString &keys);
void remove(const QKeySequence &keys);
void remove(const QKeySequence &keys, Command command);
void unregister(base::unique_qptr<QAction> shortcut);
void pruneListened();
@ -293,7 +302,7 @@ void Manager::change(
Command command,
std::optional<Command> restore) {
if (!was.isEmpty()) {
remove(was);
remove(was, command);
}
if (!now.isEmpty()) {
set(now, command, true);
@ -397,6 +406,7 @@ bool Manager::readCustomFile() {
const auto entry = (*i).toObject();
const auto keys = entry.constFind(u"keys"_q);
const auto command = entry.constFind(u"command"_q);
const auto removed = entry.constFind(u"removed"_q);
if (keys == entry.constEnd()
|| command == entry.constEnd()
|| !(*keys).isString()
@ -410,7 +420,11 @@ bool Manager::readCustomFile() {
const auto name = (*command).toString();
const auto i = CommandByName.find(name);
if (i != end(CommandByName)) {
set((*keys).toString(), i->second, true);
if (removed != entry.constEnd() && removed->toBool()) {
remove((*keys).toString(), i->second);
} else {
set((*keys).toString(), i->second, true);
}
} else {
LOG(("Shortcut Warning: "
"could not find shortcut command handler '%1'"
@ -565,12 +579,36 @@ void Manager::writeCustomFile() {
}
}
}
for (const auto &[sequence, command] : _defaults) {
if (!_shortcuts.contains(sequence)) {
const auto has = [&](not_null<QObject*> shortcut, Command command) {
for (auto i = _commandByObject.findFirst(shortcut)
; i != end(_commandByObject) && i->first == shortcut
; ++i) {
if (i->second == command) {
return true;
}
}
return false;
};
for (const auto &[sequence, commands] : _defaults) {
const auto i = _shortcuts.find(sequence);
if (i == end(_shortcuts)) {
QJsonObject entry;
entry.insert(u"keys"_q, sequence.toString().toLower());
entry.insert(u"command"_q, QJsonValue());
shortcuts.append(entry);
continue;
}
for (const auto command : commands) {
if (!has(i->second.get(), command)) {
const auto j = CommandNames().find(command);
if (j != CommandNames().end()) {
QJsonObject entry;
entry.insert(u"keys"_q, sequence.toString().toLower());
entry.insert(u"command"_q, j->second);
entry.insert(u"removed"_q, true);
shortcuts.append(entry);
}
}
}
}
@ -669,6 +707,17 @@ void Manager::remove(const QKeySequence &keys) {
}
}
void Manager::remove(const QKeySequence &keys, Command command) {
const auto i = _shortcuts.find(keys);
if (i != end(_shortcuts)) {
_commandByObject.remove(i->second.get(), command);
if (!_commandByObject.contains(i->second.get())) {
unregister(std::move(i->second));
_shortcuts.erase(i);
}
}
}
void Manager::unregister(base::unique_qptr<QAction> shortcut) {
if (shortcut) {
_commandByObject.removeAll(shortcut.get());

View file

@ -60,6 +60,11 @@ public:
normalize();
return *this;
}
inline StarsAmount operator-() const {
auto result = *this;
result *= -1;
return result;
}
friend inline auto operator<=>(StarsAmount, StarsAmount) = default;
friend inline bool operator==(StarsAmount, StarsAmount) = default;
@ -97,3 +102,7 @@ private:
[[nodiscard]] inline StarsAmount operator*(StarsAmount a, int64 b) {
return a *= b;
}
[[nodiscard]] inline StarsAmount operator*(int64 a, StarsAmount b) {
return b *= a;
}

View file

@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_session.h"
#include "iv/iv_instance.h"
#include "ui/text/text_custom_emoji.h"
#include "ui/text/text_utilities.h"
#include "ui/basic_click_handlers.h"
#include "ui/emoji_config.h"
#include "lang/lang_keys.h"
@ -112,6 +113,40 @@ const auto kBadPrefix = u"http://"_q;
} // namespace
Ui::Text::MarkedContext TextContext(TextContextArgs &&args) {
using Context = Ui::Text::MarkedContext;
using Factory = Ui::Text::CustomEmojiFactory;
const auto session = args.session;
auto simple = [session](QStringView data, const Context &context) {
return session->data().customEmojiManager().create(
data,
context.repaint);
};
auto factory = !args.customEmojiLoopLimit
? Factory(simple)
: (args.customEmojiLoopLimit > 0)
? Factory([simple, loop = args.customEmojiLoopLimit](
QStringView data,
const Context &context) {
return std::make_unique<Ui::Text::LimitedLoopsEmoji>(
simple(data, context),
loop);
})
: Factory([simple](
QStringView data,
const Context &context) {
return std::make_unique<Ui::Text::FirstFrameEmoji>(
simple(data, context));
});
args.details.session = session;
return {
.repaint = std::move(args.repaint),
.customEmojiFactory = std::move(factory),
.other = std::move(args.details),
};
}
void UiIntegration::postponeCall(FnMut<void()> &&callable) {
Sandbox::Instance().postponeCall(std::move(callable));
}
@ -152,8 +187,8 @@ bool UiIntegration::screenIsLocked() {
std::shared_ptr<ClickHandler> UiIntegration::createLinkHandler(
const EntityLinkData &data,
const std::any &context) {
const auto my = std::any_cast<MarkedTextContext>(&context);
const Ui::Text::MarkedContext &context) {
const auto my = std::any_cast<Core::TextContextDetails>(&context.other);
switch (data.type) {
case EntityType::Url:
return (!data.data.isEmpty()
@ -170,7 +205,7 @@ std::shared_ptr<ClickHandler> UiIntegration::createLinkHandler(
return std::make_shared<BotCommandClickHandler>(data.data);
case EntityType::Hashtag:
using HashtagMentionType = MarkedTextContext::HashtagMentionType;
using HashtagMentionType = TextContextDetails::HashtagMentionType;
if (my && my->type == HashtagMentionType::Twitter) {
return std::make_shared<UrlClickHandler>(
(u"https://twitter.com/hashtag/"_q
@ -190,7 +225,7 @@ std::shared_ptr<ClickHandler> UiIntegration::createLinkHandler(
return std::make_shared<CashtagClickHandler>(data.data);
case EntityType::Mention:
using HashtagMentionType = MarkedTextContext::HashtagMentionType;
using HashtagMentionType = TextContextDetails::HashtagMentionType;
if (my && my->type == HashtagMentionType::Twitter) {
return std::make_shared<UrlClickHandler>(
u"https://twitter.com/"_q + data.data.mid(1),
@ -222,7 +257,9 @@ std::shared_ptr<ClickHandler> UiIntegration::createLinkHandler(
case EntityType::Pre:
return std::make_shared<MonospaceClickHandler>(data.text, data.type);
case EntityType::Phone:
return std::make_shared<PhoneClickHandler>(my->session, data.text);
return my->session
? std::make_shared<PhoneClickHandler>(my->session, data.text)
: nullptr;
}
return Integration::createLinkHandler(data, context);
}
@ -280,36 +317,6 @@ bool UiIntegration::copyPreOnClick(const QVariant &context) {
return true;
}
std::unique_ptr<Ui::Text::CustomEmoji> UiIntegration::createCustomEmoji(
QStringView data,
const std::any &context) {
const auto my = std::any_cast<MarkedTextContext>(&context);
if (!my || !my->session) {
return nullptr;
}
auto result = my->session->data().customEmojiManager().create(
data,
my->customEmojiRepaint);
if (my->customEmojiLoopLimit > 0) {
return std::make_unique<Ui::Text::LimitedLoopsEmoji>(
std::move(result),
my->customEmojiLoopLimit);
} else if (my->customEmojiLoopLimit) {
return std::make_unique<Ui::Text::FirstFrameEmoji>(
std::move(result));
}
return result;
}
Fn<void()> UiIntegration::createSpoilerRepaint(const std::any &context) {
const auto my = std::any_cast<MarkedTextContext>(&context);
if (my) {
return my->customEmojiRepaint;
}
const auto common = std::any_cast<CommonTextContext>(&context);
return common ? common->repaint : nullptr;
}
rpl::producer<> UiIntegration::forcePopupMenuHideRequests() {
return Core::App().passcodeLockChanges() | rpl::to_empty;
}

View file

@ -19,7 +19,7 @@ class ElementDelegate;
namespace Core {
struct MarkedTextContext {
struct TextContextDetails {
enum class HashtagMentionType : uchar {
Telegram,
Twitter,
@ -28,9 +28,15 @@ struct MarkedTextContext {
Main::Session *session = nullptr;
HashtagMentionType type = HashtagMentionType::Telegram;
Fn<void()> customEmojiRepaint;
};
struct TextContextArgs {
not_null<Main::Session*> session;
TextContextDetails details;
Fn<void()> repaint;
int customEmojiLoopLimit = 0;
};
[[nodiscard]] Ui::Text::MarkedContext TextContext(TextContextArgs &&args);
class UiIntegration final : public Ui::Integration {
public:
@ -49,7 +55,7 @@ public:
std::shared_ptr<ClickHandler> createLinkHandler(
const EntityLinkData &data,
const std::any &context) override;
const Ui::Text::MarkedContext &context) override;
bool handleUrlClick(
const QString &url,
const QVariant &context) override;
@ -57,10 +63,6 @@ public:
rpl::producer<> forcePopupMenuHideRequests() override;
const Ui::Emoji::One *defaultEmojiVariant(
const Ui::Emoji::One *emoji) override;
std::unique_ptr<Ui::Text::CustomEmoji> createCustomEmoji(
QStringView data,
const std::any &context) override;
Fn<void()> createSpoilerRepaint(const std::any &context) override;
QString phraseContextCopyText() override;
QString phraseContextCopyEmail() override;

View file

@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D666}"_cs;
constexpr auto AppNameOld = "AyuGram for Windows"_cs;
constexpr auto AppName = "AyuGram Desktop"_cs;
constexpr auto AppFile = "AyuGram"_cs;
constexpr auto AppVersion = 5011001;
constexpr auto AppVersionStr = "5.11.1";
constexpr auto AppVersion = 5012003;
constexpr auto AppVersionStr = "5.12.3";
constexpr auto AppBetaVersion = false;
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;

View file

@ -322,6 +322,9 @@ QString CountriesInstance::flagEmojiByISO2(const QString &iso) const {
|| iso.back() < 'A'
|| iso.back() > 'Z') {
return QString();
} else if (iso == u"FT"_q) {
return QString::fromUtf8(
"\xF0\x9F\x8F\xB4\xE2\x80\x8D\xE2\x98\xA0\xEF\xB8\x8F");
}
auto result = QString(4, QChar(0xD83C));
result[1] = QChar(iso.front().unicode() - 'A' + 0xDDE6);

View file

@ -91,7 +91,8 @@ constexpr auto kRequestTimeLimit = 60 * crl::time(1000);
MTP_int(shortcutId),
MTP_long(data.veffect().value_or_empty()),
(data.vfactcheck() ? *data.vfactcheck() : MTPFactCheck()),
MTP_int(data.vreport_delivery_until_date().value_or_empty()));
MTP_int(data.vreport_delivery_until_date().value_or_empty()),
MTP_long(data.vpaid_message_stars().value_or_empty()));
});
}

View file

@ -37,10 +37,7 @@ void Credits::apply(const MTPDupdateStarsBalance &data) {
rpl::producer<float64> Credits::rateValue(
not_null<PeerData*> ownedBotOrChannel) {
return rpl::single(
_session->appConfig().get<float64>(
u"stars_usd_withdraw_rate_x1000"_q,
1200) / 1000.);
return rpl::single(_session->appConfig().starsWithdrawRate());
}
void Credits::load(bool force) {

View file

@ -95,7 +95,8 @@ constexpr auto kRequestTimeLimit = 60 * crl::time(1000);
MTPint(), // quick_reply_shortcut_id
MTP_long(data.veffect().value_or_empty()), // effect
data.vfactcheck() ? *data.vfactcheck() : MTPFactCheck(),
MTP_int(data.vreport_delivery_until_date().value_or_empty()));
MTP_int(data.vreport_delivery_until_date().value_or_empty()),
MTP_long(data.vpaid_message_stars().value_or_empty()));
});
}
@ -269,7 +270,8 @@ void ScheduledMessages::sendNowSimpleMessage(
MTPint(), // quick_reply_shortcut_id
MTP_long(local->effectId()), // effect
MTPFactCheck(),
MTPint()), // report_delivery_until_date
MTPint(), // report_delivery_until_date
MTPlong()), // paid_message_stars
localFlags,
NewMessageType::Unread);

View file

@ -75,46 +75,48 @@ struct PeerUpdate {
BackgroundEmoji = (1ULL << 15),
StoriesState = (1ULL << 16),
VerifyInfo = (1ULL << 17),
StarsPerMessage = (1ULL << 18),
// For users
CanShareContact = (1ULL << 18),
IsContact = (1ULL << 19),
PhoneNumber = (1ULL << 20),
OnlineStatus = (1ULL << 21),
BotCommands = (1ULL << 22),
BotCanBeInvited = (1ULL << 23),
BotStartToken = (1ULL << 24),
CommonChats = (1ULL << 25),
PeerGifts = (1ULL << 26),
HasCalls = (1ULL << 27),
SupportInfo = (1ULL << 28),
IsBot = (1ULL << 29),
EmojiStatus = (1ULL << 30),
BusinessDetails = (1ULL << 31),
Birthday = (1ULL << 32),
PersonalChannel = (1ULL << 33),
StarRefProgram = (1ULL << 34),
CanShareContact = (1ULL << 19),
IsContact = (1ULL << 20),
PhoneNumber = (1ULL << 21),
OnlineStatus = (1ULL << 22),
BotCommands = (1ULL << 23),
BotCanBeInvited = (1ULL << 24),
BotStartToken = (1ULL << 25),
CommonChats = (1ULL << 26),
PeerGifts = (1ULL << 27),
HasCalls = (1ULL << 28),
SupportInfo = (1ULL << 29),
IsBot = (1ULL << 30),
EmojiStatus = (1ULL << 31),
BusinessDetails = (1ULL << 32),
Birthday = (1ULL << 33),
PersonalChannel = (1ULL << 34),
StarRefProgram = (1ULL << 35),
PaysPerMessage = (1ULL << 36),
// For chats and channels
InviteLinks = (1ULL << 35),
Members = (1ULL << 36),
Admins = (1ULL << 37),
BannedUsers = (1ULL << 38),
Rights = (1ULL << 39),
PendingRequests = (1ULL << 40),
Reactions = (1ULL << 41),
InviteLinks = (1ULL << 37),
Members = (1ULL << 38),
Admins = (1ULL << 39),
BannedUsers = (1ULL << 40),
Rights = (1ULL << 41),
PendingRequests = (1ULL << 42),
Reactions = (1ULL << 43),
// For channels
ChannelAmIn = (1ULL << 42),
StickersSet = (1ULL << 43),
EmojiSet = (1ULL << 44),
ChannelLinkedChat = (1ULL << 45),
ChannelLocation = (1ULL << 46),
Slowmode = (1ULL << 47),
GroupCall = (1ULL << 48),
ChannelAmIn = (1ULL << 44),
StickersSet = (1ULL << 45),
EmojiSet = (1ULL << 46),
ChannelLinkedChat = (1ULL << 47),
ChannelLocation = (1ULL << 48),
Slowmode = (1ULL << 49),
GroupCall = (1ULL << 50),
// For iteration
LastUsedBit = (1ULL << 48),
LastUsedBit = (1ULL << 50),
};
using Flags = base::flags<Flag>;
friend inline constexpr auto is_flag_type(Flag) { return true; }

View file

@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_channel.h"
#include "api/api_global_privacy.h"
#include "data/components/credits.h"
#include "data/data_changes.h"
#include "data/data_channel_admins.h"
#include "data/data_user.h"
@ -30,6 +31,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_chat_invite.h"
#include "api/api_invite_links.h"
#include "apiwrap.h"
#include "storage/storage_account.h"
#include "ui/unread_badge.h"
#include "window/notifications_manager.h"
@ -859,6 +861,21 @@ void ChannelData::growSlowmodeLastMessage(TimeId when) {
session().changes().peerUpdated(this, UpdateFlag::Slowmode);
}
int ChannelData::starsPerMessage() const {
if (const auto info = mgInfo.get()) {
return info->_starsPerMessage;
}
return 0;
}
void ChannelData::setStarsPerMessage(int stars) {
if (mgInfo && starsPerMessage() != stars) {
mgInfo->_starsPerMessage = stars;
session().changes().peerUpdated(this, UpdateFlag::StarsPerMessage);
}
checkTrustedPayForMessage();
}
int ChannelData::peerGiftsCount() const {
return _peerGiftsCount;
}
@ -1150,7 +1167,8 @@ void ApplyChannelUpdate(
| Flag::CanViewRevenue
| Flag::PaidMediaAllowed
| Flag::CanViewCreditsRevenue
| Flag::StargiftsAvailable;
| Flag::StargiftsAvailable
| Flag::PaidMessagesAvailable;
channel->setFlags((channel->flags() & ~mask)
| (update.is_can_set_username() ? Flag::CanSetUsername : Flag())
| (update.is_can_view_participants()
@ -1174,6 +1192,9 @@ void ApplyChannelUpdate(
: Flag())
| (update.is_stargifts_available()
? Flag::StargiftsAvailable
: Flag())
| (update.is_paid_messages_available()
? Flag::PaidMessagesAvailable
: Flag()));
channel->setUserpicPhoto(update.vchat_photo());
if (const auto migratedFrom = update.vmigrated_from_chat_id()) {

View file

@ -14,6 +14,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_peer_bot_commands.h"
#include "data/data_user_names.h"
class ChannelData;
struct ChannelLocation {
QString address;
Data::LocationPoint point;
@ -70,6 +72,7 @@ enum class ChannelDataFlag : uint64 {
CanViewCreditsRevenue = (1ULL << 34),
SignatureProfiles = (1ULL << 35),
StargiftsAvailable = (1ULL << 36),
PaidMessagesAvailable = (1ULL << 37),
};
inline constexpr bool is_flag_type(ChannelDataFlag) { return true; };
using ChannelDataFlags = base::flags<ChannelDataFlag>;
@ -150,6 +153,9 @@ private:
ChannelLocation _location;
Data::ChatBotCommands _botCommands;
std::unique_ptr<Data::Forum> _forum;
int _starsPerMessage = 0;
friend class ChannelData;
};
@ -257,6 +263,9 @@ public:
[[nodiscard]] bool stargiftsAvailable() const {
return flags() & Flag::StargiftsAvailable;
}
[[nodiscard]] bool paidMessagesAvailable() const {
return flags() & Flag::PaidMessagesAvailable;
}
[[nodiscard]] static ChatRestrictionsInfo KickedRestrictedRights(
not_null<PeerData*> participant);
@ -456,6 +465,9 @@ public:
[[nodiscard]] TimeId slowmodeLastMessage() const;
void growSlowmodeLastMessage(TimeId when);
void setStarsPerMessage(int stars);
[[nodiscard]] int starsPerMessage() const;
[[nodiscard]] int peerGiftsCount() const;
void setPeerGiftsCount(int count);

View file

@ -17,10 +17,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_user.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "ui/boxes/confirm_box.h"
#include "ui/chat/attach/attach_prepare.h"
#include "ui/layers/generic_box.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
#include "window/window_session_controller.h"
#include "styles/style_widgets.h"
namespace {
@ -120,7 +123,7 @@ bool CanSendAnyOf(
|| user->isRepliesChat()
|| user->isVerifyCodes()) {
return false;
} else if (user->meRequiresPremiumToWrite()
} else if (user->requiresPremiumToWrite()
&& !user->session().premium()) {
return false;
} else if (rights
@ -177,7 +180,7 @@ SendError RestrictionError(
using Flag = ChatRestriction;
if (const auto restricted = peer->amRestricted(restriction)) {
if (const auto user = peer->asUser()) {
if (user->meRequiresPremiumToWrite()
if (user->requiresPremiumToWrite()
&& !user->session().premium()) {
return SendError({
.text = tr::lng_restricted_send_non_premium(

View file

@ -66,10 +66,11 @@ struct CreditsHistoryEntry final {
uint64 bareGiftStickerId = 0;
uint64 bareGiftOwnerId = 0;
uint64 bareActorId = 0;
uint64 bareGiftListPeerId = 0;
uint64 giftSavedId = 0;
uint64 bareEntryOwnerId = 0;
uint64 giftChannelSavedId = 0;
uint64 stargiftId = 0;
std::shared_ptr<UniqueGift> uniqueGift;
Fn<std::vector<CreditsHistoryEntry>()> pinnedSavedGifts;
StarsAmount starrefAmount;
int starrefCommission = 0;
uint64 starrefRecipientId = 0;
@ -77,11 +78,15 @@ struct CreditsHistoryEntry final {
QDateTime subscriptionUntil;
QDateTime successDate;
QString successLink;
int paidMessagesCount = 0;
StarsAmount paidMessagesAmount;
int paidMessagesCommission = 0;
int limitedCount = 0;
int limitedLeft = 0;
int starsConverted = 0;
int starsToUpgrade = 0;
int starsUpgradedBySender = 0;
int premiumMonthsForStars = 0;
int floodSkip = 0;
bool converted : 1 = false;
bool anonymous : 1 = false;
@ -89,6 +94,7 @@ struct CreditsHistoryEntry final {
bool giftTransferred : 1 = false;
bool giftRefunded : 1 = false;
bool giftUpgraded : 1 = false;
bool giftPinned : 1 = false;
bool savedToProfile : 1 = false;
bool fromGiftsList : 1 = false;
bool fromGiftSlug : 1 = false;

View file

@ -16,7 +16,10 @@ namespace Data {
struct CreditsEarnStatistics final {
explicit operator bool() const {
return !!usdRate;
return usdRate
&& currentBalance
&& availableBalance
&& overallRevenue;
}
Data::StatisticalGraph revenueGraph;
StarsAmount currentBalance;

View file

@ -50,6 +50,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/history_view_element.h"
#include "history/history_item.h"
#include "storage/file_download.h"
#include "storage/storage_account.h"
#include "storage/storage_facade.h"
#include "storage/storage_shared_media.h"
@ -65,6 +66,28 @@ using UpdateFlag = Data::PeerUpdate::Flag;
return session->appConfig().ignoredRestrictionReasons();
}
[[nodiscard]] int ParseRegistrationDate(const QString &text) {
// MM.YYYY
if (text.size() != 7 || text[2] != '.') {
return 0;
}
const auto month = text.mid(0, 2).toInt();
const auto year = text.mid(3, 4).toInt();
return (year > 2012 && year < 2100 && month > 0 && month <= 12)
? (year * 100) + month
: 0;
}
[[nodiscard]] int RegistrationYear(int date) {
const auto year = date / 100;
return (year > 2012 && year < 2100) ? year : 0;
}
[[nodiscard]] int RegistrationMonth(int date) {
const auto month = date % 100;
return (month > 0 && month <= 12) ? month : 0;
}
} // namespace
namespace Data {
@ -311,6 +334,17 @@ void PeerData::invalidateEmptyUserpic() {
_userpicEmpty = nullptr;
}
void PeerData::checkTrustedPayForMessage() {
if (!_checkedTrustedPayForMessage
&& !starsPerMessage()
&& session().local().peerTrustedPayForMessageRead()) {
_checkedTrustedPayForMessage = 1;
if (session().local().hasPeerTrustedPayForMessageEntry(id)) {
session().local().clearPeerTrustedPayForMessage(id);
}
}
}
ClickHandlerPtr PeerData::createOpenLink() {
return std::make_shared<PeerClickHandler>(this);
}
@ -692,7 +726,9 @@ void PeerData::checkFolder(FolderId folderId) {
void PeerData::clearBusinessBot() {
if (const auto details = _barDetails.get()) {
if (details->requestChatDate) {
if (details->requestChatDate
|| details->paysPerMessage
|| !details->phoneCountryCode.isEmpty()) {
details->businessBot = nullptr;
details->businessBotManageUrl = QString();
} else {
@ -735,12 +771,27 @@ void PeerData::saveTranslationDisabled(bool disabled) {
void PeerData::setBarSettings(const MTPPeerSettings &data) {
data.match([&](const MTPDpeerSettings &data) {
if (!data.vbusiness_bot_id() && !data.vrequest_chat_title()) {
const auto wasPaysPerMessage = paysPerMessage();
if (!data.vbusiness_bot_id()
&& !data.vrequest_chat_title()
&& !data.vcharge_paid_message_stars()
&& !data.vphone_country()
&& !data.vregistration_month()
&& !data.vname_change_date()
&& !data.vphoto_change_date()) {
_barDetails = nullptr;
} else if (!_barDetails) {
_barDetails = std::make_unique<PeerBarDetails>();
}
if (_barDetails) {
_barDetails->phoneCountryCode
= qs(data.vphone_country().value_or_empty());
_barDetails->registrationDate = ParseRegistrationDate(
data.vregistration_month().value_or_empty());
_barDetails->nameChangeDate
= data.vname_change_date().value_or_empty();
_barDetails->photoChangeDate
= data.vphoto_change_date().value_or_empty();
_barDetails->requestChatTitle
= qs(data.vrequest_chat_title().value_or_empty());
_barDetails->requestChatDate
@ -750,6 +801,8 @@ void PeerData::setBarSettings(const MTPPeerSettings &data) {
: nullptr;
_barDetails->businessBotManageUrl
= qs(data.vbusiness_bot_manage_url().value_or_empty());
_barDetails->paysPerMessage
= data.vcharge_paid_message_stars().value_or_empty();
}
using Flag = PeerBarSetting;
setBarSettings((data.is_add_contact() ? Flag::AddContact : Flag())
@ -773,8 +826,35 @@ void PeerData::setBarSettings(const MTPPeerSettings &data) {
| (data.is_business_bot_can_reply()
? Flag::BusinessBotCanReply
: Flag()));
if (wasPaysPerMessage != paysPerMessage()) {
session().changes().peerUpdated(
this,
UpdateFlag::PaysPerMessage);
}
});
}
int PeerData::paysPerMessage() const {
return _barDetails ? _barDetails->paysPerMessage : 0;
}
void PeerData::clearPaysPerMessage() {
if (const auto details = _barDetails.get()) {
if (details->paysPerMessage) {
if (details->businessBot
|| details->requestChatDate
|| !details->phoneCountryCode.isEmpty()) {
details->paysPerMessage = 0;
} else {
_barDetails = nullptr;
}
session().changes().peerUpdated(
this,
UpdateFlag::PaysPerMessage);
}
}
}
QString PeerData::requestChatTitle() const {
return _barDetails ? _barDetails->requestChatTitle : QString();
}
@ -791,6 +871,28 @@ QString PeerData::businessBotManageUrl() const {
return _barDetails ? _barDetails->businessBotManageUrl : QString();
}
QString PeerData::phoneCountryCode() const {
return _barDetails ? _barDetails->phoneCountryCode : QString();
}
int PeerData::registrationMonth() const {
return _barDetails
? RegistrationMonth(_barDetails->registrationDate)
: 0;
}
int PeerData::registrationYear() const {
return _barDetails ? RegistrationYear(_barDetails->registrationDate) : 0;
}
TimeId PeerData::nameChangeDate() const {
return _barDetails ? _barDetails->nameChangeDate : 0;
}
TimeId PeerData::photoChangeDate() const {
return _barDetails ? _barDetails->photoChangeDate : 0;
}
bool PeerData::changeColorIndex(
const tl::conditional<MTPint> &cloudColorIndex) {
return cloudColorIndex
@ -1301,7 +1403,7 @@ Data::RestrictionCheckResult PeerData::amRestricted(
}
};
if (const auto user = asUser()) {
if (user->meRequiresPremiumToWrite() && !user->session().premium()) {
if (user->requiresPremiumToWrite() && !user->session().premium()) {
return Result::Explicit();
}
return (right == ChatRestriction::SendVoiceMessages
@ -1420,6 +1522,24 @@ bool PeerData::canManageGroupCall() const {
return false;
}
int PeerData::starsPerMessage() const {
if (const auto user = asUser()) {
return user->starsPerMessage();
} else if (const auto channel = asChannel()) {
return channel->starsPerMessage();
}
return 0;
}
int PeerData::starsPerMessageChecked() const {
if (const auto channel = asChannel()) {
return (channel->adminRights() || channel->amCreator())
? 0
: channel->starsPerMessage();
}
return starsPerMessage();
}
Data::GroupCall *PeerData::groupCall() const {
if (const auto chat = asChat()) {
return chat->groupCall();

View file

@ -173,10 +173,15 @@ inline constexpr bool is_flag_type(PeerBarSetting) { return true; };
using PeerBarSettings = base::flags<PeerBarSetting>;
struct PeerBarDetails {
QString phoneCountryCode;
int registrationDate = 0; // YYYYMM or 0, YYYY > 2012, MM > 0.
TimeId nameChangeDate = 0;
TimeId photoChangeDate = 0;
QString requestChatTitle;
TimeId requestChatDate;
UserData *businessBot = nullptr;
QString businessBotManageUrl;
int paysPerMessage = 0;
};
class PeerData {
@ -268,6 +273,9 @@ public:
[[nodiscard]] int slowmodeSecondsLeft() const;
[[nodiscard]] bool canManageGroupCall() const;
[[nodiscard]] int starsPerMessage() const;
[[nodiscard]] int starsPerMessageChecked() const;
[[nodiscard]] UserData *asBot();
[[nodiscard]] const UserData *asBot() const;
[[nodiscard]] UserData *asUser();
@ -409,11 +417,18 @@ public:
? _barSettings.changes()
: (_barSettings.value() | rpl::type_erased());
}
[[nodiscard]] int paysPerMessage() const;
void clearPaysPerMessage();
[[nodiscard]] QString requestChatTitle() const;
[[nodiscard]] TimeId requestChatDate() const;
[[nodiscard]] UserData *businessBot() const;
[[nodiscard]] QString businessBotManageUrl() const;
void clearBusinessBot();
[[nodiscard]] QString phoneCountryCode() const;
[[nodiscard]] int registrationMonth() const;
[[nodiscard]] int registrationYear() const;
[[nodiscard]] TimeId nameChangeDate() const;
[[nodiscard]] TimeId photoChangeDate() const;
enum class TranslationFlag : uchar {
Unknown,
@ -501,6 +516,7 @@ protected:
void updateUserpic(PhotoId photoId, MTP::DcId dcId, bool hasVideo);
void clearUserpic();
void invalidateEmptyUserpic();
void checkTrustedPayForMessage();
private:
void fillNames();
@ -535,9 +551,10 @@ private:
crl::time _lastFullUpdate = 0;
QString _name;
uint32 _nameVersion : 30 = 1;
uint32 _nameVersion : 29 = 1;
uint32 _sensitiveContent : 1 = 0;
uint32 _wallPaperOverriden : 1 = 0;
uint32 _checkedTrustedPayForMessage : 1 = 0;
TimeId _ttlPeriod = 0;

View file

@ -228,11 +228,11 @@ inline auto DefaultRestrictionValue(
| ChatRestriction::SendVideoMessages);
auto allowedAny = PeerFlagsValue(
user,
(UserDataFlag::Deleted | UserDataFlag::MeRequiresPremiumToWrite)
(UserDataFlag::Deleted | UserDataFlag::RequiresPremiumToWrite)
) | rpl::map([=](UserDataFlags flags) {
return (flags & UserDataFlag::Deleted)
? rpl::single(false)
: !(flags & UserDataFlag::MeRequiresPremiumToWrite)
: !(flags & UserDataFlag::RequiresPremiumToWrite)
? rpl::single(true)
: AmPremiumValue(&user->session());
}) | rpl::flatten_latest();

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