Compare commits

..

146 Commits

Author SHA1 Message Date
John Preston
aadc81279a Version 6.3: Fix build with GCC. 2025-11-15 21:51:15 +04:00
John Preston
febd6dd18f Version 6.3.
- Support for Gift auctions.
- Support for Live stories.
- New profile pages design.
2025-11-15 18:22:08 +04:00
John Preston
1236b35aaf Fix build with MSVC. 2025-11-15 18:22:08 +04:00
23rd
41a9a25823 Added collectible emoji status preview to chat settings. 2025-11-15 18:19:07 +04:00
23rd
55ae81524d Attempted to fix change color in preview while reset unique status. 2025-11-15 18:19:07 +04:00
John Preston
a9cf5dd6f2 Fix crash in "Copy Topic Link" action. 2025-11-15 17:54:16 +04:00
John Preston
1e45692ba4 Fix build with Xcode. 2025-11-15 17:48:00 +04:00
John Preston
997cff63a8 Update gyp to support Visual Studio 2026. 2025-11-15 17:39:37 +04:00
John Preston
5db513cc55 Fix build with Xcode. 2025-11-15 17:18:30 +04:00
John Preston
4a7280be5e Fix balance alignment in paid reactions box. 2025-11-15 14:28:56 +04:00
John Preston
59826d492f Show sold out instead of premium paywall. 2025-11-15 14:18:36 +04:00
John Preston
85c2a5fd75 Attempt to fix edit profile colors for self. 2025-11-15 14:02:23 +04:00
John Preston
33dd3342e8 Display correct pattern in edit color box. 2025-11-15 12:38:18 +04:00
John Preston
94fc50f793 Fix live story outline in profile cover. 2025-11-15 12:29:34 +04:00
John Preston
1e79f4a9a5 Don't suggest conversion for auction gifts. 2025-11-15 12:00:01 +04:00
John Preston
f56825c526 Replace newlines with spaces for non-admins. 2025-11-15 12:00:01 +04:00
John Preston
d7c0f9dee8 Set correct auction about icons. 2025-11-15 12:00:01 +04:00
John Preston
a71c59beae Disable newlines in stream comments. 2025-11-15 12:00:01 +04:00
John Preston
5f729433f6 Don't attach reactions menu to streams. 2025-11-15 12:00:01 +04:00
John Preston
a1d37475c9 Share just link for live stories. 2025-11-15 12:00:01 +04:00
John Preston
9275dfce70 Fix crash when no top winners yet. 2025-11-15 12:00:01 +04:00
John Preston
ff9afdae3e Simple auction position display. 2025-11-15 12:00:01 +04:00
John Preston
12a2dcf484 Fix custom emoji text badge with spaces. 2025-11-15 12:00:01 +04:00
John Preston
c3f5f69c2a Remove some debug code. 2025-11-15 12:00:01 +04:00
John Preston
1320e7ac2f Allow placing bid with message / anonymous. 2025-11-15 12:00:01 +04:00
John Preston
ae6b0dd753 Implement bought items box. 2025-11-15 12:00:01 +04:00
John Preston
e43830c08e Handle auction links. 2025-11-15 12:00:01 +04:00
John Preston
4a5cdbcfb2 Nice bidding information display. 2025-11-15 12:00:01 +04:00
John Preston
9b599644d9 Improve bidding logic. 2025-11-15 12:00:01 +04:00
John Preston
cab93600dd Add about auction box layout (no icons yet). 2025-11-15 12:00:01 +04:00
John Preston
0e5e4ca7ea Simple KV storage in session().local(). 2025-11-15 12:00:00 +04:00
John Preston
a293134b5e Implement auction info box (active/ended). 2025-11-15 12:00:00 +04:00
John Preston
3831860943 Extract resale star gift box code. 2025-11-15 12:00:00 +04:00
John Preston
1f8214e658 Specify auction phrases for sent auction gifts. 2025-11-15 12:00:00 +04:00
John Preston
58d2e2ece2 Add lang keys for the auctions. 2025-11-15 12:00:00 +04:00
John Preston
424f416fdd Update API scheme to layer 218. 2025-11-15 12:00:00 +04:00
23rd
95c12a0b8e Improved style of slider within paid reactions box for streams. 2025-11-15 12:00:00 +04:00
23rd
45d42c8c31 Added MediaSlider color overrides structure for customizable styling. 2025-11-15 12:00:00 +04:00
23rd
5b94095c78 Added badge to userpic from top bar profile for live stories. 2025-11-15 12:00:00 +04:00
John Preston
605ad0a01c Update API scheme on layer 217. 2025-11-15 12:00:00 +04:00
John Preston
10b8dc3595 Fix star reaction counter in streams. 2025-11-15 12:00:00 +04:00
John Preston
432f6aeae6 Support disallow_stargifts_from_channels right. 2025-11-15 12:00:00 +04:00
John Preston
6702ff3c55 Add SavedMusic privacy editions. 2025-11-15 12:00:00 +04:00
John Preston
8f205f13d8 Show schedule repeat info in BottomInfo. 2025-11-15 11:59:59 +04:00
John Preston
22a133b182 Always allow rescheduling. 2025-11-15 11:59:20 +04:00
23rd
bc61175e79 Changed display of widget for login email setup only for primary window. 2025-11-15 11:59:20 +04:00
23rd
874e344f91 Added accounts list menu to widget for login email setup. 2025-11-15 11:59:20 +04:00
23rd
f832e31c7b Added support of pending suggestion to set up login email. 2025-11-15 11:59:20 +04:00
John Preston
4cdd793e0c Show crowns in stream comments. 2025-11-15 11:59:20 +04:00
John Preston
0d11cb603f Add LIVE badge for outlined userpics. 2025-11-15 11:59:20 +04:00
John Preston
1e197ae66c Hide comments toggle button if no comments. 2025-11-15 11:59:20 +04:00
John Preston
2a88103b5f Support live stories links. 2025-11-15 11:59:20 +04:00
John Preston
49caea416b Close the viewer when call finishes / fails. 2025-11-15 11:59:20 +04:00
John Preston
5bfe270f24 Support story loading state. 2025-11-15 11:59:20 +04:00
John Preston
e8f10f2b45 Use colorings from appconfig. 2025-11-15 11:59:20 +04:00
John Preston
834986410c Update watchers count. 2025-11-15 11:59:20 +04:00
John Preston
deffbcf231 Skip showing cheap messages. 2025-11-15 11:59:20 +04:00
John Preston
2d720a8349 Show LIVE badge and watchers count. 2025-11-15 11:59:20 +04:00
John Preston
182d45b7ea Check limits when sending comments. 2025-11-15 11:59:20 +04:00
John Preston
3cd68842bf Use reaction color for send button. 2025-11-15 11:59:20 +04:00
John Preston
861ada351e Make sure you have enough stars for comment. 2025-11-15 11:59:20 +04:00
John Preston
e32d863daa Support layout of admin comments in streams. 2025-11-15 11:59:20 +04:00
John Preston
81fc652dc1 Ignore price for admins of live story. 2025-11-15 11:59:20 +04:00
John Preston
b72deb1a0e Update client hello algorithm. 2025-11-15 11:59:19 +04:00
John Preston
cab38f00c2 Support initial hidden state of comments. 2025-11-15 11:59:19 +04:00
John Preston
137594ccee Track stars stats by short poll / local updates. 2025-11-15 11:59:19 +04:00
23rd
449f7fb2a3 Added support settings/login_email link. 2025-11-15 11:59:19 +04:00
John Preston
2a5bea7e4e Support comments restriction. 2025-11-15 11:59:19 +04:00
John Preston
ea309caa22 Support min stars for comment. 2025-11-15 11:59:19 +04:00
John Preston
89bc58ab29 Allow moderation of video stream comments. 2025-11-15 11:59:19 +04:00
John Preston
2d754c93a7 Support choosing send_as in video streams. 2025-11-15 11:59:19 +04:00
John Preston
1017d4cda3 Fix build with Xcode. 2025-11-15 11:59:19 +04:00
John Preston
68dab26be5 New layout of star reaction button in streams. 2025-11-15 11:59:19 +04:00
John Preston
8eb0ec9f7a Send stars from star reaction right click. 2025-11-15 11:59:19 +04:00
John Preston
1da47f62fc Update API scheme on layer 217. 2025-11-15 11:59:19 +04:00
John Preston
70939c4b9c Correctly edit video stream comment price. 2025-11-15 11:59:19 +04:00
John Preston
491ad744ea Update API scheme on layer 217. 2025-11-15 11:59:19 +04:00
John Preston
a1565c7fff Allow highlighting pinned messagse. 2025-11-15 11:59:19 +04:00
John Preston
496dbfb2f0 Track mouse in pinned strip. 2025-11-15 11:59:19 +04:00
John Preston
0e44dac208 Use correct lifetime in call messages. 2025-11-15 11:59:19 +04:00
John Preston
2b808933d2 Add some logging to conference participants. 2025-11-15 11:59:19 +04:00
John Preston
ba0f682e3a Improve pinned comments design a bit. 2025-11-15 11:59:19 +04:00
John Preston
ef49652415 Remove old messages correctly. 2025-11-15 11:59:18 +04:00
John Preston
8e30ee1192 Animate pinned duration. 2025-11-15 11:59:18 +04:00
John Preston
74a676026f Show pinned messages. 2025-11-15 11:59:18 +04:00
John Preston
af93c2ee49 Set correct comments area. 2025-11-15 11:59:18 +04:00
John Preston
6b05d253a7 Support different backgrounds for paid comments. 2025-11-15 11:59:18 +04:00
John Preston
797b2a5085 Remove messages by count, not by date. 2025-11-15 11:59:18 +04:00
John Preston
b898cf4fde Integrate paid reaction logic to video streams. 2025-11-15 11:59:18 +04:00
John Preston
1c17432f70 Request and store top donors in video streams. 2025-11-15 11:59:18 +04:00
John Preston
a6c96df51f Update API scheme on layer 217. 2025-11-15 11:59:18 +04:00
John Preston
d7abe73753 Show unread comments dot in video streams. 2025-11-15 11:59:18 +04:00
John Preston
1b84063dd3 Force sending finalizing even without update. 2025-11-15 11:59:18 +04:00
John Preston
ed5b7fe3e6 Improve video stream comment placeholder. 2025-11-15 11:59:18 +04:00
John Preston
699ac83729 Support simple type of send stars reactions. 2025-11-15 11:59:18 +04:00
John Preston
1e1ce492e6 Update API scheme on layer 217. 2025-11-15 11:59:18 +04:00
John Preston
7afb9f1fc8 Make toggle comments button. 2025-11-15 11:59:18 +04:00
John Preston
7bfe418c3e Update API scheme on layer 217. 2025-11-15 11:59:18 +04:00
John Preston
94c1388b6a Start redesign of video stream comments. 2025-11-15 11:59:18 +04:00
John Preston
90f53cba31 Show video stream in borrowed renderer. 2025-11-15 11:59:18 +04:00
John Preston
a675fca607 Don't show story lines in video streams. 2025-11-15 11:59:18 +04:00
John Preston
bf58171f64 Paint video streams outlines in red. 2025-11-15 11:59:18 +04:00
John Preston
e619bd4acc Show progress correctly (empty). 2025-11-15 11:59:18 +04:00
John Preston
5f5b7ffb66 Support pin ttl for paid messages in streams. 2025-11-15 11:59:18 +04:00
John Preston
2216e75cab Initial choose stars in video stream box. 2025-11-15 11:59:18 +04:00
John Preston
a61d73f48a Allow selecting how many stars to send. 2025-11-15 11:59:17 +04:00
John Preston
ecdca38d9e Allow sending comments with stars. 2025-11-15 11:59:17 +04:00
John Preston
a34bc7cc89 Hide like button in video stream. 2025-11-15 11:59:17 +04:00
John Preston
d12a0be66c Support volume changing in video streams. 2025-11-15 11:59:17 +04:00
John Preston
890421e00e Show volume control in video streams. 2025-11-15 11:59:17 +04:00
John Preston
dc57886b68 Start messages layout changes for streams. 2025-11-15 11:59:17 +04:00
John Preston
832fb023e8 Update API scheme on layer 217. 2025-11-15 11:59:17 +04:00
John Preston
af8c171b1a Don't crash on media viewer call without stories. 2025-11-15 11:59:17 +04:00
John Preston
58264f8b57 Initial messages support in video streams. 2025-11-15 11:59:17 +04:00
John Preston
eb7d614566 Update API scheme on layer 217. 2025-11-15 11:59:17 +04:00
John Preston
c11cab858d Initial support for streamed stories. 2025-11-15 11:59:17 +04:00
John Preston
18438b17a5 Update API scheme on layer 217. 2025-11-15 11:59:17 +04:00
John Preston
f2464df96f Update API scheme on layer 217. 2025-11-15 11:59:17 +04:00
John Preston
5ee0a2fea2 Allow repeat scheduling in any chat. 2025-11-15 11:59:17 +04:00
John Preston
1296505d01 Add repeat Biweekly period. 2025-11-15 11:59:17 +04:00
John Preston
332b70a27d Repeated scheduled messages (reminders). 2025-11-15 11:59:17 +04:00
John Preston
c4d5d52b96 Update API scheme to layer 217. 2025-11-15 11:59:17 +04:00
John Preston
7a5554202e Fix build with latest Xcode. 2025-11-15 11:56:01 +04:00
John Preston
df2c426096 Disable fraudulent warning for IV / Location Picker. 2025-11-15 11:56:01 +04:00
23rd
b33cdc581b Slightly improved condition for handler of record voice shortcut. 2025-11-15 02:09:22 +03:00
23rd
3cd213b9bf Added dialog suggestions close on open dialog in new window. 2025-11-14 14:34:47 +03:00
23rd
aebb37b516 Replaced unknown member count with channel type labels in profiles. 2025-11-14 12:25:18 +03:00
23rd
d016c80ba5 Improved style of edit icon from profile top bar. 2025-11-14 12:25:18 +03:00
23rd
882f1c4d1a Improved fix of title width with emoji in profile top bar. 2025-11-14 12:25:18 +03:00
23rd
3ffdf8d281 Made calls check less restricted to avoid redundant repaint in profiles. 2025-11-14 12:25:18 +03:00
23rd
c183243711 Improved photo process on full receive in profile top bar. 2025-11-14 12:25:18 +03:00
23rd
eeb2c953ad Prioritized badge display by truncating title in profile top bar. 2025-11-14 12:25:18 +03:00
23rd
bfb2f3015a Added action button to profile top bar for direct messages. 2025-11-14 12:25:18 +03:00
23rd
9c54f53613 Fixed display of discuss action in discussion group in profile top bar. 2025-11-14 12:25:17 +03:00
23rd
257c7af2f3 Changed pin gifts to end of pinned list instead of begin. 2025-11-14 12:25:17 +03:00
23rd
4ed256814a Added spoiler to box for rtmp calls. 2025-11-14 12:25:17 +03:00
23rd
1f8ea32388 Fixed crash on zero-size resize in MultiSelect during focus animations. 2025-11-14 12:25:17 +03:00
23rd
69403e7967 Fixed display of verified icon in web view for bots.
Regression was introduced in e7fa330215.
2025-11-14 12:25:17 +03:00
23rd
7cc4c76c54 Fixed ripple of QR button in profile info. 2025-11-14 12:25:17 +03:00
23rd
cf74a81d07 Fixed display in reply view of specific replied media from group. 2025-11-10 19:37:18 +03:00
23rd
417d151f2c Added original date of forwarded messages to bottom message in self. 2025-11-10 19:37:18 +03:00
23rd
ff42b7f61a Fixed gift opening when mouse released on different gift than pressed. 2025-11-10 19:37:18 +03:00
23rd
658c2a0a78 Added ability to reorder pinned gifts to view with all peer gifts. 2025-11-10 19:37:18 +03:00
23rd
90d6899577 Removed menu item to pin / unpin gift from collections. 2025-11-10 19:37:18 +03:00
23rd
5db0e53c0c Added api method to reorder pinned gifts. 2025-11-10 19:37:18 +03:00
Ilya Fedin
7dea4f36fb Accept a list of URLs on Linux
It's supported not only via D-Bus but via command line too since 5fbf280e4a
2025-11-10 16:15:41 +04:00
271 changed files with 13230 additions and 3179 deletions

View File

@@ -333,8 +333,12 @@ PRIVATE
boxes/send_files_box.h
boxes/share_box.cpp
boxes/share_box.h
boxes/star_gift_auction_box.cpp
boxes/star_gift_auction_box.h
boxes/star_gift_box.cpp
boxes/star_gift_box.h
boxes/star_gift_resale_box.cpp
boxes/star_gift_resale_box.h
boxes/sticker_set_box.cpp
boxes/sticker_set_box.h
boxes/stickers_box.cpp
@@ -377,6 +381,8 @@ PRIVATE
calls/group/calls_group_rtmp.h
calls/group/calls_group_settings.cpp
calls/group/calls_group_settings.h
calls/group/calls_group_stars_box.cpp
calls/group/calls_group_stars_box.h
calls/group/calls_group_toasts.cpp
calls/group/calls_group_toasts.h
calls/group/calls_group_viewport.cpp
@@ -513,6 +519,8 @@ PRIVATE
data/components/credits.h
data/components/factchecks.cpp
data/components/factchecks.h
data/components/gift_auctions.cpp
data/components/gift_auctions.h
data/components/location_pickers.cpp
data/components/location_pickers.h
data/components/promo_suggestions.cpp
@@ -1302,6 +1310,8 @@ PRIVATE
media/view/media_view_playback_progress.h
media/view/media_view_playback_sponsored.cpp
media/view/media_view_playback_sponsored.h
media/view/media_view_video_stream.cpp
media/view/media_view_video_stream.h
media/system_media_controls_manager.h
media/system_media_controls_manager.cpp
menu/menu_antispam_validator.cpp
@@ -1636,6 +1646,8 @@ PRIVATE
ui/controls/location_picker.h
ui/controls/silent_toggle.cpp
ui/controls/silent_toggle.h
ui/controls/table_rows.cpp
ui/controls/table_rows.h
ui/controls/userpic_button.cpp
ui/controls/userpic_button.h
ui/effects/credits_graphics.cpp
@@ -1721,6 +1733,8 @@ PRIVATE
window/window_session_controller.cpp
window/window_session_controller.h
window/window_session_controller_link_info.h
window/window_setup_email.cpp
window/window_setup_email.h
window/window_top_bar_wrap.h
window/themes/window_theme.cpp
window/themes/window_theme.h

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="72px" height="72px" viewBox="0 0 72 72" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Filled / filled_stream_crown</title>
<g id="Filled-/-filled_stream_crown" stroke="none" fill="none" fill-rule="evenodd">
<path d="M10.5793798,21.3515408 L22.4461242,27.5065085 C23.3919447,27.6775566 24.3643369,27.4213643 25.1079413,26.8052098 L34.6112236,12.5021245 C35.4192073,11.8326252 36.5811617,11.8326252 37.3891453,12.5021245 L46.8924276,26.8052098 C47.6360321,27.4213643 48.6084242,27.6775566 49.5542448,27.5065085 L61.4209891,21.3515408 C62.613521,21.1358757 63.7528909,21.9400986 63.965843,23.1478228 C64.0112154,23.4051449 64.0113863,23.6685576 63.9663481,23.9259399 L57.0245486,49.6650027 C56.2820716,53.9080693 48.2297454,57 36.0001845,57 C23.7706236,57 15.7182973,53.9080693 14.9758203,49.6650027 L8.03402089,23.9259399 C7.8226367,22.7179332 8.61824017,21.5651064 9.81105098,21.3510293 C10.0651955,21.3054173 10.3252947,21.3055904 10.5793798,21.3515408 Z" id="Path" fill="#FFFFFF"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 833 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 839 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 514 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1012 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 853 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 824 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 989 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -759,6 +759,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_settings_bio_privacy" = "Bio";
"lng_settings_gifts_privacy" = "Gifts";
"lng_settings_birthday_privacy" = "Date of Birth";
"lng_settings_saved_music_privacy" = "Saved Music";
"lng_settings_privacy_premium" = "Only subscribers of {link} can restrict receiving voice messages.";
"lng_settings_privacy_premium_link" = "Telegram Premium";
"lng_settings_passcode_disable" = "Disable passcode";
@@ -929,6 +930,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_settings_cloud_login_email_code_title" = "Check Your New Email";
"lng_settings_cloud_login_email_code_about" = "Please enter the code we have sent to your new email {email}";
"lng_settings_cloud_login_email_success" = "Your email has been changed.";
"lng_settings_cloud_login_email_set_success" = "Your login email has been set successfully.";
"lng_settings_cloud_login_email_busy" = "Please set up login email in another window.";
"lng_settings_error_email_not_alowed" = "Sorry, this email is not allowed";
"lng_settings_ttl_title" = "Auto-Delete Messages";
@@ -998,6 +1001,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_settings_color_changed_profile" = "Your profile style has been updated!";
"lng_settings_color_changed_profile_channel" = "Your channel profile style has been updated!";
"lng_settings_color_apply" = "Apply Style";
"lng_settings_color_wear" = "Wear Collectible";
"lng_settings_color_profile_emoji" = "Add icons to Profile";
"lng_settings_color_profile_emoji_channel" = "Profile Logo";
"lng_settings_color_reset" = "Reset Profile Color";
@@ -1406,6 +1410,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_edit_privacy_gifts_unlimited" = "Unlimited";
"lng_edit_privacy_gifts_limited" = "Limited-Edition";
"lng_edit_privacy_gifts_unique" = "Unique";
"lng_edit_privacy_gifts_channels" = "From Channels";
"lng_edit_privacy_gifts_types_about" = "Choose the types of gifts that you accept.";
"lng_edit_privacy_gifts_show_icon" = "Show Gift Icon in Chats";
"lng_edit_privacy_gifts_show_icon_about" = "Display the {emoji}Gift icon in the message input field for both participants in all chats.";
@@ -1465,6 +1470,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_edit_privacy_voices_always_title" = "Always allow";
"lng_edit_privacy_voices_never_title" = "Never Allow";
"lng_edit_privacy_saved_music_title" = "Saved Music";
"lng_edit_privacy_saved_music_header" = "Who can see my saved music in profile";
"lng_edit_privacy_saved_music_always_empty" = "Always allow";
"lng_edit_privacy_saved_music_never_empty" = "Never allow";
"lng_edit_privacy_saved_music_exceptions" = "These users will or will not be able to see your saved music regardless of the settings above.";
"lng_edit_privacy_saved_music_always_title" = "Always allow";
"lng_edit_privacy_saved_music_never_title" = "Never allow";
"lng_messages_privacy_title" = "Messages";
"lng_messages_privacy_subtitle" = "Who can send me messages";
"lng_messages_privacy_everyone" = "Everybody";
@@ -1686,6 +1699,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_profile_changed_photo_link" = "Settings";
"lng_profile_action_short_message" = "Message";
"lng_profile_action_short_channel" = "Channel";
"lng_profile_action_short_mute" = "Mute";
"lng_profile_action_short_unmute" = "Unmute";
"lng_profile_action_short_call" = "Call";
@@ -2275,6 +2289,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_action_gift_sent_channel" = "{user} sent a gift to {name} for {cost}";
"lng_action_gift_sent_self_channel" = "You sent a gift to {name} for {cost}";
"lng_action_gift_self_bought" = "You bought a gift for {cost}";
"lng_action_gift_self_auction" = "You've successfully bought a gift in the auction for {cost}.";
"lng_action_gift_auction" = "You've successfully bought a gift for {name} in the auction for {cost}.";
"lng_action_gift_self_subtitle" = "Saved Gift";
"lng_action_gift_self_about#one" = "Display this gift on your page or convert it to **{count}** Star.";
"lng_action_gift_self_about#other" = "Display this gift on your page or convert it to **{count}** Stars.";
@@ -2430,6 +2446,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_peer_gifts_filter_saved" = "Displayed";
"lng_peer_gifts_filter_unsaved" = "Hidden";
"lng_premium_gift_duration_days#one" = "for {count} day";
"lng_premium_gift_duration_days#other" = "for {count} days";
"lng_premium_gift_duration_months#one" = "for {count} month";
"lng_premium_gift_duration_months#other" = "for {count} months";
"lng_premium_gift_duration_years#one" = "for {count} year";
@@ -2607,6 +2625,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_edited" = "edited";
"lng_commented" = "commented";
"lng_approximate" = "appx.";
"lng_repeated_daily" = "daily";
"lng_repeated_weekly" = "weekly";
"lng_repeated_biweekly" = "biweekly";
"lng_repeated_monthly" = "monthly";
"lng_repeated_every_month#one" = "{count}-monthly";
"lng_repeated_every_month#other" = "{count}-monthly";
"lng_repeated_yearly" = "yearly";
"lng_edited_date" = "Edited: {date}";
"lng_sent_date" = "Sent: {date}";
"lng_approximate_about" = "Estimated date of video publishing.";
@@ -2725,8 +2750,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_premium_summary_title_subscribed" = "You are all set!";
"lng_premium_summary_subtitle_gift#one" = "{user} has gifted you a {count}-month subscription to Telegram Premium.";
"lng_premium_summary_subtitle_gift#other" = "{user} has gifted you a {count}-months subscription to Telegram Premium.";
"lng_premium_summary_subtitle_gift_days#one" = "{user} has gifted you a {count}-day subscription to Telegram Premium.";
"lng_premium_summary_subtitle_gift_days#other" = "{user} has gifted you a {count}-days subscription to Telegram Premium.";
"lng_premium_summary_subtitle_gift_me#one" = "You gifted {user} a {count}-month subscription to Telegram Premium.";
"lng_premium_summary_subtitle_gift_me#other" = "You gifted {user} a {count}-months subscription to Telegram Premium.";
"lng_premium_summary_subtitle_gift_days_me#one" = "You gifted {user} a {count}-month subscription to Telegram Premium.";
"lng_premium_summary_subtitle_gift_days_me#other" = "You gifted {user} a {count}-months subscription to Telegram Premium.";
"lng_premium_summary_subtitle_wallpapers" = "Wallpapers for Both Sides";
"lng_premium_summary_about_wallpapers" = "Set custom wallpapers for you and your chat partner.";
"lng_premium_summary_subtitle_stories" = "Stories";
@@ -3054,6 +3083,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_credits_small_balance_title#other" = "{count} Stars Needed";
"lng_credits_small_balance_about" = "Buy **Stars** and use them on **{bot}** and other miniapps.";
"lng_credits_small_balance_reaction" = "Buy **Stars** and send them to {channel} to support their posts.";
"lng_credits_small_balance_video_stream" = "Buy **Stars** to send them to {name} to support their stream.";
"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}.";
@@ -3653,6 +3683,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_gift_stars_resale" = "resale";
"lng_gift_stars_on_sale" = "on sale";
"lng_gift_stars_premium" = "premium";
"lng_gift_stars_auction" = "auction";
"lng_gift_stars_auction_join" = "Join";
"lng_gift_stars_your_left#one" = "{count} left";
"lng_gift_stars_your_left#other" = "{count} left";
"lng_gift_stars_your_finished" = "none left";
@@ -3956,6 +3988,96 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_gift_collection_remove_from" = "Remove from Collection";
"lng_gift_locked_title" = "Gift Locked";
"lng_auction_about_title" = "Auction";
"lng_auction_about_subtitle" = "Join the battle for exclusive gifts.";
"lng_auction_about_top_title#one" = "Top {count} Bidder";
"lng_auction_about_top_title#other" = "Top {count} Bidders";
"lng_auction_about_top_rounds#one" = "{count} round";
"lng_auction_about_top_rounds#other" = "{count} rounds";
"lng_auction_about_top_bidders#one" = "top {count} bidder";
"lng_auction_about_top_bidders#other" = "top {count} bidders";
"lng_auction_about_top_about#one" = "{count} gift is dropped in {rounds} to the {bidders} by bid amount.";
"lng_auction_about_top_about#other" = "{count} gifts are dropped in {rounds} to the {bidders} by bid amount.";
"lng_auction_about_bid_title" = "Bid Carryover";
"lng_auction_about_bid_about#one" = "If your bid leaves the top {count}, it will automatically join the next round.";
"lng_auction_about_bid_about#other" = "If your bid leaves the top {count}, it will automatically join the next round.";
"lng_auction_about_missed_title" = "Missed Bidders";
"lng_auction_about_missed_about" = "If your bid doesn't win after the final round, your Stars will be fully refunded.";
"lng_auction_about_understood" = "Understood";
"lng_auction_text#one" = "Top **{count}** bidder will get {name} gifts this round. {link}";
"lng_auction_text#other" = "Top **{count}** bidders will get {name} gifts this round. {link}";
"lng_auction_text_link" = "Learn more {arrow}";
"lng_auction_text_ended" = "Auction ended.";
"lng_auction_start_label" = "Started";
"lng_auction_end_label" = "Ends";
"lng_auction_round_label" = "Current Round";
"lng_auction_round_value" = "{n} of {amount}";
"lng_auction_average_label" = "Average Price";
"lng_auction_average_tooltip" = "{amount} is the average sale price for {gift} gifts.";
"lng_auction_availability_label" = "Availability";
"lng_auction_availability_value" = "{n} of {amount} left";
"lng_auction_bought#one" = "{count} {emoji} item bought {arrow}";
"lng_auction_bought#other" = "{count} {emoji} items bought {arrow}";
"lng_auction_join_button" = "Join Auction";
"lng_auction_join_bid" = "Place a Bid";
"lng_auction_join_time_left" = "{time} left";
"lng_auction_join_time_medium" = "{hours} h {minutes} m";
"lng_auction_join_time_small" = "{minutes} m";
"lng_auction_menu_about" = "About";
"lng_auction_menu_copy_link" = "Copy Link";
"lng_auction_menu_share" = "Share";
"lng_auction_bid_title" = "Place a Bid";
"lng_auction_bid_your" = "your bid";
"lng_auction_bid_threshold#one" = "TOP {count}";
"lng_auction_bid_threshold#other" = "TOP {count}";
"lng_auction_bid_minimal#one" = "minimum bid";
"lng_auction_bid_minimal#other" = "minimum bid";
"lng_auction_bid_until" = "until next round";
"lng_auction_bid_left#one" = "left";
"lng_auction_bid_left#other" = "left";
"lng_auction_bid_your_title" = "Your bid will be";
"lng_auction_bid_your_outbid" = "You've been outbid";
"lng_auction_bid_your_winning" = "You're winning";
"lng_auction_bid_winners_title" = "Top winners";
"lng_auction_bid_place" = "Place a {stars} Bid";
"lng_auction_bid_increase" = "Add {stars} to Your Bid";
"lng_auction_bid_placed_title" = "Your bid has been placed";
"lng_auction_bid_increased_title" = "Your bid has been increased";
"lng_auction_bid_done_text#one" = "If you fall below the **top {count}**, your bid will roll over to the next round.";
"lng_auction_bid_done_text#other" = "If you fall below the **top {count}**, your bid will roll over to the next round.";
"lng_auction_preview_left#one" = "{count} left";
"lng_auction_preview_left#other" = "{count} left";
"lng_auction_preview_join" = "Join";
"lng_auctino_preview_finished" = "Finished";
"lng_auction_preview_sold_out" = "Sold Out";
"lng_auction_preview_view_results" = "View Results";
"lng_auction_bar_active" = "Active Auction";
"lng_auction_bar_active_many#one" = "{count} Active Auction";
"lng_auction_bar_active_many#other" = "{count} Active Auctions";
"lng_auction_bar_winning#one" = "You're winning ({count}st place).";
"lng_auction_bar_winning#other" = "You're winning ({count}th place).";
"lng_auction_bar_outbid" = "You've been outbid.";
"lng_auction_bar_winning_all" = "You're winning in all of them.";
"lng_auction_bar_outbid_some#one" = "You've been outbid in {count} of them.";
"lng_auction_bar_outbid_some#other" = "You've been outbid in {count} of them.";
"lng_auction_bar_outbid_all" = "You've been outbid in all of them.";
"lng_auction_bar_view" = "View";
"lng_auction_bar_round" = "Round {n} of {amount}";
"lng_auction_bar_bid_ranked" = "Your bid **{stars}** is ranked **#{n}**";
"lng_auction_bar_bid_outbid" = "Your bid **{stars}** is outbid";
"lng_auction_bar_raise_bid" = "Raise Bid";
"lng_auction_bought_title#one" = "{count} Item Bought";
"lng_auction_bought_title#other" = "{count} Items Bought";
"lng_auction_bought_date" = "Date";
"lng_auction_bought_bid" = "Accepted Bid";
"lng_auction_bought_round" = "Round #{n}";
"lng_auction_change_title" = "Change Recipient";
"lng_auction_change_button" = "Change";
"lng_auction_change_already" = "You've already placed a bid on this gift for {name}.";
"lng_auction_change_to" = "Do you want to raise your bid and change the recipient to {name}?";
"lng_auction_change_already_me" = "You've already placed a bid on this gift for yourself.";
"lng_auction_change_to_me" = "Do you want to raise your bid and change the recipient to yourself?";
"lng_accounts_limit_title" = "Limit Reached";
"lng_accounts_limit1#one" = "You have reached the limit of **{count}** connected account.";
"lng_accounts_limit1#other" = "You have reached the limit of **{count}** connected accounts.";
@@ -4136,6 +4258,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_reminder_message" = "Set a reminder";
"lng_schedule_title" = "Send this message on...";
"lng_remind_title" = "Remind me on...";
"lng_schedule_repeat_label" = "Repeat:";
"lng_schedule_repeat_never" = "Never";
"lng_schedule_repeat_daily" = "Daily";
"lng_schedule_repeat_weekly" = "Weekly";
"lng_schedule_repeat_biweekly" = "Biweekly";
"lng_schedule_repeat_monthly" = "Monthly";
"lng_schedule_repeat_every_month#one" = "Every {count} month";
"lng_schedule_repeat_every_month#other" = "Every {count} month";
"lng_schedule_repeat_yearly" = "Yearly";
"lng_schedule_repeat_promo" = "Subscribe to {link} to schedule repeating messages.";
"lng_schedule_repeat_promo_link" = "Telegram Premium";
"lng_schedule_at" = "at";
"lng_message_ph" = "Write a message...";
"lng_broadcast_ph" = "Broadcast a message...";
@@ -4143,6 +4276,16 @@ 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_video_stream_comment_ph" = "Comment";
"lng_video_stream_comment_paid_ph#one" = "Comment for {count} Star";
"lng_video_stream_comment_paid_ph#other" = "Comment for {count} Stars";
"lng_video_stream_comments_disabled" = "Comments disabled.";
"lng_video_stream_stars" = "Add Stars to highlight your comment";
"lng_video_stream_live" = "LIVE";
"lng_video_stream_watched#one" = "{count} watching";
"lng_video_stream_watched#other" = "{count} watching";
"lng_video_stream_edit_stars" = "Edit Stars";
"lng_video_stream_remove_stars" = "Remove Stars";
"lng_message_stars_ph#one" = "Message for {count} Star";
"lng_message_stars_ph#other" = "Message for {count} Stars";
"lng_send_text_no" = "Text not allowed.";
@@ -4547,6 +4690,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_context_seen_reacted_none" = "Nobody Reacted";
"lng_context_seen_reacted_all" = "Show All Reactions";
"lng_context_sent_by" = "Sent by {user}";
"lng_context_sent_today" = "Sent today at {time}";
"lng_context_sent_yesterday" = "Sent yesterday at {time}";
"lng_context_sent_date" = "Sent {date} at {time}";
"lng_context_set_as_quick" = "Set As Quick";
"lng_context_filter_by_tag" = "Filter by Tag";
"lng_context_tag_add_name" = "Add Name";
@@ -4643,6 +4789,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_paid_react_show_in_top" = "Show me in Top Senders";
"lng_paid_react_anonymous" = "Anonymous";
"lng_paid_comment_title" = "Highlight and Pin";
"lng_paid_comment_about" = "Highlight and pin your message by adding Stars for {name}.";
"lng_paid_comment_button" = "Add {stars}";
"lng_paid_comment_pin_about" = "pin in chat";
"lng_paid_comment_limit_about#one" = "character";
"lng_paid_comment_limit_about#other" = "characters";
"lng_paid_comment_emoji_about#one" = "emoji";
"lng_paid_comment_emoji_about#other" = "emoji";
"lng_paid_reaction_title" = "React with Stars";
"lng_paid_reaction_about" = "Highlight and pin your message by sending Stars to {name}.";
"lng_paid_reaction_button" = "Send {stars}";
"lng_sensitive_tag" = "18+";
"lng_sensitive_title" = "18+";
"lng_sensitive_text" = "This media may contain sensitive content suitable only for adults. Do you still want to view it?";
@@ -6738,6 +6896,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_stealth_mode_already_about" = "The creators of stories you view in the next **{left}** won't see you in the viewers' lists.";
"lng_stories_link_invalid" = "This link is broken or has expired.";
"lng_stories_live_finished" = "The live story has ended.";
"lng_stats_title" = "Statistics";
"lng_stats_message_title" = "Message Statistic";

View File

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

View File

@@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico"
//
VS_VERSION_INFO VERSIONINFO
FILEVERSION 6,2,6,0
PRODUCTVERSION 6,2,6,0
FILEVERSION 6,3,0,0
PRODUCTVERSION 6,3,0,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
@@ -62,10 +62,10 @@ BEGIN
BEGIN
VALUE "CompanyName", "Telegram FZ-LLC"
VALUE "FileDescription", "Telegram Desktop"
VALUE "FileVersion", "6.2.6.0"
VALUE "FileVersion", "6.3.0.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2025"
VALUE "ProductName", "Telegram Desktop"
VALUE "ProductVersion", "6.2.6.0"
VALUE "ProductVersion", "6.3.0.0"
END
END
BLOCK "VarFileInfo"

View File

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

View File

@@ -25,6 +25,7 @@ struct SendOptions {
uint64 price = 0;
PeerData *sendAs = nullptr;
TimeId scheduled = 0;
TimeId scheduleRepeatPeriod = 0;
BusinessShortcutId shortcutId = 0;
EffectId effectId = 0;
int starsApproved = 0;

View File

@@ -154,6 +154,7 @@ mtpRequestId SuggestMedia(
MTPReplyMarkup(),
sentEntities,
MTPint(), // schedule_date
MTPint(), // schedule_repeat_period
MTPInputPeer(), // send_as
MTPInputQuickReplyShortcut(), // quick_reply_shortcut
MTPlong(), // effect
@@ -295,6 +296,9 @@ mtpRequestId EditMessage(
| (options.scheduled
? MTPmessages_EditMessage::Flag::f_schedule_date
: emptyFlag)
| ((options.scheduled && options.scheduleRepeatPeriod)
? MTPmessages_EditMessage::Flag::f_schedule_repeat_period
: emptyFlag)
| (item->isBusinessShortcut()
? MTPmessages_EditMessage::Flag::f_quick_reply_shortcut_id
: emptyFlag);
@@ -313,6 +317,7 @@ mtpRequestId EditMessage(
MTPReplyMarkup(),
sentEntities,
MTP_int(options.scheduled),
MTP_int(options.scheduleRepeatPeriod),
MTP_int(item->shortcutId())
)).done([=](
const MTPUpdates &result,

View File

@@ -262,6 +262,9 @@ void GlobalPrivacy::update(
: DisallowedFlag())
| ((disallowedGiftTypes & DisallowedGiftType::Unique)
? DisallowedFlag::f_disallow_unique_stargifts
: DisallowedFlag())
| ((disallowedGiftTypes & DisallowedGiftType::FromChannels)
? DisallowedFlag::f_disallow_stargifts_from_channels
: DisallowedFlag());
const auto typesWas = _disallowedGiftTypes.current();
const auto typesChanged = (typesWas != disallowedGiftTypes);
@@ -322,6 +325,9 @@ void GlobalPrivacy::apply(const MTPGlobalPrivacySettings &settings) {
| (disallow.is_disallow_premium_gifts()
? DisallowedGiftType::Premium
: DisallowedGiftType())
| (disallow.is_disallow_stargifts_from_channels()
? DisallowedGiftType::FromChannels
: DisallowedGiftType())
| (data.is_display_gifts_button()
? DisallowedGiftType::SendHide
: DisallowedGiftType());

View File

@@ -25,11 +25,12 @@ enum class UnarchiveOnNewMessage {
};
enum class DisallowedGiftType : uchar {
Limited = 0x01,
Unlimited = 0x02,
Unique = 0x04,
Premium = 0x08,
SendHide = 0x10,
Limited = 0x01,
Unlimited = 0x02,
Unique = 0x04,
FromChannels = 0x08,
Premium = 0x10,
SendHide = 0x20,
};
inline constexpr bool is_flag_type(DisallowedGiftType) { return true; }

View File

@@ -61,6 +61,9 @@ void Polls::create(
}
if (action.options.scheduled) {
sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date;
if (action.options.scheduleRepeatPeriod) {
sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_repeat_period;
}
}
if (action.options.shortcutId) {
sendFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut;
@@ -95,6 +98,7 @@ void Polls::create(
MTPReplyMarkup(),
MTPVector<MTPMessageEntity>(),
MTP_int(action.options.scheduled),
MTP_int(action.options.scheduleRepeatPeriod),
(sendAs ? sendAs->input : MTP_inputPeerEmpty()),
Data::ShortcutIdToMTP(_session, action.options.shortcutId),
MTP_long(action.options.effectId),
@@ -191,6 +195,7 @@ void Polls::close(not_null<HistoryItem*> item) {
MTPReplyMarkup(),
MTPVector<MTPMessageEntity>(),
MTP_int(0), // schedule_date
MTP_int(0), // schedule_repeat_period
MTPint() // quick_reply_shortcut_id
)).done([=](const MTPUpdates &result) {
_pollCloseRequestIds.erase(itemId);

View File

@@ -36,7 +36,7 @@ namespace {
.giveawayId = data.vgiveaway_msg_id().value_or_empty(),
.date = data.vdate().v,
.used = data.vused_date().value_or_empty(),
.months = data.vmonths().v,
.days = data.vdays().v,
.giveaway = data.is_via_giveaway(),
};
}
@@ -858,6 +858,8 @@ std::optional<Data::StarGift> FromTL(
.releasedBy = releasedBy,
.resellTitle = qs(data.vtitle().value_or_empty()),
.resellCount = int(data.vavailability_resale().value_or_empty()),
.auctionSlug = qs(data.vauction_slug().value_or_empty()),
.auctionGiftsPerRound = data.vgifts_per_round().value_or_empty(),
.limitedLeft = remaining.value_or_empty(),
.limitedCount = total.value_or_empty(),
.perUserTotal = data.vper_user_total().value_or_empty(),

View File

@@ -30,11 +30,11 @@ struct GiftCode {
MsgId giveawayId = 0;
TimeId date = 0;
TimeId used = 0; // 0 if not used.
int months = 0;
int days = 0;
bool giveaway = false;
explicit operator bool() const {
return months != 0;
return days != 0;
}
friend inline bool operator==(

View File

@@ -101,6 +101,9 @@ void SendSimpleMedia(SendAction action, MTPInputMedia inputMedia) {
if (action.options.scheduled) {
flags |= MessageFlag::IsOrWasScheduled;
sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date;
if (action.options.scheduleRepeatPeriod) {
sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_repeat_period;
}
}
if (action.options.shortcutId) {
flags |= MessageFlag::ShortcutMessage;
@@ -136,6 +139,7 @@ void SendSimpleMedia(SendAction action, MTPInputMedia inputMedia) {
MTPReplyMarkup(),
MTPvector<MTPMessageEntity>(),
MTP_int(action.options.scheduled),
MTP_int(action.options.scheduleRepeatPeriod),
(sendAs ? sendAs->input : MTP_inputPeerEmpty()),
Data::ShortcutIdToMTP(session, action.options.shortcutId),
MTP_long(action.options.effectId),
@@ -207,6 +211,9 @@ void SendExistingMedia(
if (action.options.scheduled) {
flags |= MessageFlag::IsOrWasScheduled;
sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date;
if (action.options.scheduleRepeatPeriod) {
sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_repeat_period;
}
}
if (action.options.shortcutId) {
flags |= MessageFlag::ShortcutMessage;
@@ -260,6 +267,7 @@ void SendExistingMedia(
MTPReplyMarkup(),
sentEntities,
MTP_int(action.options.scheduled),
MTP_int(action.options.scheduleRepeatPeriod),
(sendAs ? sendAs->input : MTP_inputPeerEmpty()),
Data::ShortcutIdToMTP(session, action.options.shortcutId),
MTP_long(action.options.effectId),
@@ -392,6 +400,9 @@ bool SendDice(MessageToSend &message) {
if (action.options.scheduled) {
flags |= MessageFlag::IsOrWasScheduled;
sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date;
if (action.options.scheduleRepeatPeriod) {
sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_repeat_period;
}
}
if (action.options.shortcutId) {
flags |= MessageFlag::ShortcutMessage;
@@ -445,6 +456,7 @@ bool SendDice(MessageToSend &message) {
MTPReplyMarkup(),
MTP_vector<MTPMessageEntity>(),
MTP_int(action.options.scheduled),
MTP_int(action.options.scheduleRepeatPeriod),
(sendAs ? sendAs->input : MTP_inputPeerEmpty()),
Data::ShortcutIdToMTP(session, action.options.shortcutId),
MTP_long(action.options.effectId),

View File

@@ -66,6 +66,9 @@ void TodoLists::create(
}
if (action.options.scheduled) {
sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date;
if (action.options.scheduleRepeatPeriod) {
sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_repeat_period;
}
}
if (action.options.shortcutId) {
sendFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut;
@@ -100,6 +103,7 @@ void TodoLists::create(
MTPReplyMarkup(),
MTPVector<MTPMessageEntity>(),
MTP_int(action.options.scheduled),
MTP_int(action.options.scheduleRepeatPeriod),
(sendAs ? sendAs->input : MTP_inputPeerEmpty()),
Data::ShortcutIdToMTP(_session, action.options.shortcutId),
MTP_long(action.options.effectId),

View File

@@ -23,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "mtproto/mtproto_dc_options.h"
#include "data/business/data_shortcut_messages.h"
#include "data/components/credits.h"
#include "data/components/gift_auctions.h"
#include "data/components/promo_suggestions.h"
#include "data/components/scheduled_messages.h"
#include "data/components/top_peers.h"
@@ -1234,7 +1235,8 @@ void Updates::applyUpdatesNoPtsCheck(const MTPUpdates &updates) {
MTPFactCheck(),
MTPint(), // report_delivery_until_date
MTPlong(), // paid_message_stars
MTPSuggestedPost()),
MTPSuggestedPost(),
MTPint()), // schedule_repeat_period
MessageFlags(),
NewMessageType::Unread);
} break;
@@ -1274,7 +1276,8 @@ void Updates::applyUpdatesNoPtsCheck(const MTPUpdates &updates) {
MTPFactCheck(),
MTPint(), // report_delivery_until_date
MTPlong(), // paid_message_stars
MTPSuggestedPost()),
MTPSuggestedPost(),
MTPint()), // schedule_repeat_period
MessageFlags(),
NewMessageType::Unread);
} break;
@@ -1705,6 +1708,8 @@ void Updates::feedUpdate(const MTPUpdate &update) {
owner.histories().checkTopicCreated(id, newId);
}
session().data().unregisterMessageRandomId(randomId);
} else {
Core::App().calls().handleUpdate(&session(), update);
}
session().data().unregisterMessageSentData(randomId);
} break;
@@ -2141,7 +2146,8 @@ void Updates::feedUpdate(const MTPUpdate &update) {
case mtpc_updateGroupCallConnection:
case mtpc_updateGroupCall:
case mtpc_updateGroupCallMessage:
case mtpc_updateGroupCallEncryptedMessage: {
case mtpc_updateGroupCallEncryptedMessage:
case mtpc_updateDeleteGroupCallMessages: {
Core::App().calls().handleUpdate(&session(), update);
} break;
@@ -2784,6 +2790,15 @@ void Updates::feedUpdate(const MTPUpdate &update) {
Api::ParsePaidReactionShownPeer(_session, data.vprivate()));
} break;
case mtpc_updateStarGiftAuctionState: {
const auto &data = update.c_updateStarGiftAuctionState();
_session->giftAuctions().apply(data);
} break;
case mtpc_updateStarGiftAuctionUserState: {
const auto &data = update.c_updateStarGiftAuctionUserState();
_session->giftAuctions().apply(data);
} break;
}
}

View File

@@ -211,6 +211,7 @@ MTPInputPrivacyKey KeyToTL(UserPrivacy::Key key) {
case Key::Birthday: return MTP_inputPrivacyKeyBirthday();
case Key::GiftsAutoSave: return MTP_inputPrivacyKeyStarGiftsAutoSave();
case Key::NoPaidMessages: return MTP_inputPrivacyKeyNoPaidMessages();
case Key::SavedMusic: return MTP_inputPrivacyKeySavedMusic();
}
Unexpected("Key in Api::UserPrivacy::KetToTL.");
}
@@ -244,6 +245,8 @@ std::optional<UserPrivacy::Key> TLToKey(mtpTypeId type) {
case mtpc_inputPrivacyKeyStarGiftsAutoSave: return Key::GiftsAutoSave;
case mtpc_privacyKeyNoPaidMessages:
case mtpc_inputPrivacyKeyNoPaidMessages: return Key::NoPaidMessages;
case mtpc_privacyKeySavedMusic:
case mtpc_inputPrivacyKeySavedMusic: return Key::SavedMusic;
}
return std::nullopt;
}

View File

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

View File

@@ -787,8 +787,10 @@ QString ApiWrap::exportDirectStoryLink(not_null<Data::Story*> story) {
const auto peer = story->peer();
const auto fallback = [&] {
const auto base = peer->username();
const auto story = QString::number(storyId.story);
const auto query = base + "/s/" + story;
const auto id = story->call()
? u"live"_q
: QString::number(storyId.story);
const auto query = base + "/s/" + id;
return session().createInternalLinkFull(query);
};
const auto i = _unlikelyStoryLinks.find(storyId);
@@ -3473,6 +3475,9 @@ void ApiWrap::forwardMessages(
if (action.options.scheduled) {
flags |= MessageFlag::IsOrWasScheduled;
sendFlags |= SendFlag::f_schedule_date;
if (action.options.scheduleRepeatPeriod) {
sendFlags |= SendFlag::f_schedule_repeat_period;
}
}
if (action.options.shortcutId) {
flags |= MessageFlag::ShortcutMessage;
@@ -3540,6 +3545,7 @@ void ApiWrap::forwardMessages(
? MTP_inputReplyToMonoForum(monoforumPeer->input)
: MTPInputReplyTo()),
MTP_int(action.options.scheduled),
MTP_int(action.options.scheduleRepeatPeriod),
(sendAs ? sendAs->input : MTP_inputPeerEmpty()),
Data::ShortcutIdToMTP(_session, action.options.shortcutId),
MTPint(), // video_timestamp
@@ -4064,6 +4070,10 @@ void ApiWrap::sendMessage(
flags |= MessageFlag::IsOrWasScheduled;
sendFlags |= MTPmessages_SendMessage::Flag::f_schedule_date;
mediaFlags |= MTPmessages_SendMedia::Flag::f_schedule_date;
if (action.options.scheduleRepeatPeriod) {
sendFlags |= MTPmessages_SendMessage::Flag::f_schedule_repeat_period;
mediaFlags |= MTPmessages_SendMedia::Flag::f_schedule_repeat_period;
}
}
if (action.options.shortcutId) {
flags |= MessageFlag::ShortcutMessage;
@@ -4092,6 +4102,7 @@ void ApiWrap::sendMessage(
.from = NewMessageFromId(action),
.replyTo = action.replyTo,
.date = NewMessageDate(action.options),
.scheduleRepeatPeriod = action.options.scheduleRepeatPeriod,
.shortcutId = action.options.shortcutId,
.starsPaid = starsPaid,
.postAuthor = NewMessagePostAuthor(action),
@@ -4143,6 +4154,7 @@ void ApiWrap::sendMessage(
MTPReplyMarkup(),
sentEntities,
MTP_int(action.options.scheduled),
MTP_int(action.options.scheduleRepeatPeriod),
(sendAs ? sendAs->input : MTP_inputPeerEmpty()),
mtpShortcut,
MTP_long(action.options.effectId),
@@ -4163,6 +4175,7 @@ void ApiWrap::sendMessage(
MTPReplyMarkup(),
sentEntities,
MTP_int(action.options.scheduled),
MTP_int(action.options.scheduleRepeatPeriod),
(sendAs ? sendAs->input : MTP_inputPeerEmpty()),
mtpShortcut,
MTP_long(action.options.effectId),
@@ -4470,6 +4483,9 @@ void ApiWrap::sendMediaWithRandomId(
: Flag(0))
| (!sentEntities.v.isEmpty() ? Flag::f_entities : Flag(0))
| (options.scheduled ? Flag::f_schedule_date : Flag(0))
| ((options.scheduled && options.scheduleRepeatPeriod)
? Flag::f_schedule_repeat_period
: Flag(0))
| (options.sendAs ? Flag::f_send_as : Flag(0))
| (options.shortcutId ? Flag::f_quick_reply_shortcut : Flag(0))
| (options.effectId ? Flag::f_effect : Flag(0))
@@ -4499,6 +4515,7 @@ void ApiWrap::sendMediaWithRandomId(
MTPReplyMarkup(),
sentEntities,
MTP_int(options.scheduled),
MTP_int(options.scheduleRepeatPeriod),
(options.sendAs ? options.sendAs->input : MTP_inputPeerEmpty()),
Data::ShortcutIdToMTP(_session, options.shortcutId),
MTP_long(options.effectId),
@@ -4555,6 +4572,9 @@ void ApiWrap::sendMultiPaidMedia(
: Flag(0))
| (!sentEntities.v.isEmpty() ? Flag::f_entities : Flag(0))
| (options.scheduled ? Flag::f_schedule_date : Flag(0))
| (options.scheduleRepeatPeriod
? Flag::f_schedule_repeat_period
: Flag(0))
| (options.sendAs ? Flag::f_send_as : Flag(0))
| (options.shortcutId ? Flag::f_quick_reply_shortcut : Flag(0))
| (options.effectId ? Flag::f_effect : Flag(0))
@@ -4583,6 +4603,7 @@ void ApiWrap::sendMultiPaidMedia(
MTPReplyMarkup(),
sentEntities,
MTP_int(options.scheduled),
MTP_int(options.scheduleRepeatPeriod),
(options.sendAs ? options.sendAs->input : MTP_inputPeerEmpty()),
Data::ShortcutIdToMTP(_session, options.shortcutId),
MTP_long(options.effectId),
@@ -4690,6 +4711,9 @@ void ApiWrap::sendAlbumIfReady(not_null<SendingAlbum*> album) {
? Flag::f_silent
: Flag(0))
| (album->options.scheduled ? Flag::f_schedule_date : Flag(0))
//| (album->options.scheduleRepeatPeriod
// ? Flag::f_schedule_repeat_period
// : Flag(0))
| (sendAs ? Flag::f_send_as : Flag(0))
| (album->options.shortcutId
? Flag::f_quick_reply_shortcut
@@ -4710,6 +4734,7 @@ void ApiWrap::sendAlbumIfReady(not_null<SendingAlbum*> album) {
Data::Histories::ReplyToPlaceholder(),
MTP_vector<MTPInputSingleMedia>(medias),
MTP_int(album->options.scheduled),
//MTP_int(album->options.scheduleRepeatPeriod),
(sendAs ? sendAs->input : MTP_inputPeerEmpty()),
Data::ShortcutIdToMTP(_session, album->options.shortcutId),
MTP_long(album->options.effectId),

View File

@@ -458,10 +458,12 @@ void AddContactBox::save() {
MTP_vector<MTPInputContact>(
1,
MTP_inputPhoneContact(
MTP_flags(0),
MTP_long(_contactId),
MTP_string(phone),
MTP_string(firstName),
MTP_string(lastName)))
MTP_string(lastName),
MTPTextWithEntities())) // note
)).done(crl::guard(weak, [=](
const MTPcontacts_ImportedContacts &result) {
const auto &data = result.data();

View File

@@ -929,6 +929,14 @@ scheduleTimeSeparator: FlatLabel(defaultFlatLabel) {
}
}
scheduleTimeSeparatorPadding: margins(2px, 0px, 2px, 0px);
scheduleRepeatDropdownLock: IconEmoji {
icon: icon {{ "emoji/premium_lock", windowActiveTextFg }};
padding: margins(-2px, 1px, 0px, 0px);
}
scheduleRepeatDropdownArrow: IconEmoji {
icon: icon {{ "intro_country_dropdown", windowActiveTextFg }};
padding: margins(3px, 6px, 3px, 0px);
}
muteBoxTimeField: InputField(scheduleDateField) {
textMargins: margins(0px, 0px, 0px, 0px);
@@ -998,9 +1006,8 @@ contactsWithStories: PeerList(peerListBox) {
statusPosition: point(70px, 27px);
checkbox: RoundImageCheckbox(defaultPeerListCheckbox) {
selectExtendTwice: 1px;
imageRadius: 21px;
imageSmallRadius: 19px;
imageSmallRadius: 18px;
check: RoundCheckbox(defaultPeerListCheck) {
size: 0px;
}

View File

@@ -31,10 +31,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_premium_subscription_option.h"
#include "data/data_user.h"
#include "data/stickers/data_custom_emoji.h"
#include "info/channel_statistics/boosts/giveaway/boost_badge.h" // InfiniteRadialAnimationWidget.
//#include "info/channel_statistics/boosts/giveaway/boost_badge.h" // InfiniteRadialAnimationWidget.
#include "info/channel_statistics/earn/earn_icons.h"
#include "info/profile/info_profile_badge.h"
#include "info/profile/info_profile_values.h"
//#include "info/profile/info_profile_badge.h"
//#include "info/profile/info_profile_values.h"
#include "lang/lang_keys.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
@@ -47,6 +47,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/boxes/boost_box.h" // StartFireworks.
#include "ui/boxes/confirm_box.h"
#include "ui/controls/userpic_button.h"
#include "ui/controls/table_rows.h"
#include "ui/effects/credits_graphics.h"
#include "ui/effects/premium_graphics.h"
#include "ui/effects/premium_stars_colored.h"
@@ -81,79 +82,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace {
constexpr auto kTooltipDuration = 3 * crl::time(1000);
constexpr auto kPriceTooltipDuration = 6 * crl::time(1000);
constexpr auto kHorizontalBar = QChar(0x2015);
struct InfoTooltipData {
not_null<Ui::RpWidget*> parent;
Ui::ImportantTooltip *raw = nullptr;
};
void ShowInfoTooltip(
std::shared_ptr<InfoTooltipData> data,
not_null<QWidget*> target,
rpl::producer<TextWithEntities> text,
int duration) {
if (data->raw) {
data->raw->toggleAnimated(false);
}
const auto parent = data->parent;
const auto tooltip = Ui::CreateChild<Ui::ImportantTooltip>(
parent,
Ui::MakeNiceTooltipLabel(
parent,
std::move(text),
st::boxWideWidth,
st::defaultImportantTooltipLabel),
st::defaultImportantTooltip);
tooltip->toggleFast(false);
base::install_event_filter(tooltip, qApp, [=](not_null<QEvent*> e) {
if (e->type() == QEvent::MouseButtonPress) {
tooltip->toggleAnimated(false);
}
return base::EventFilterResult::Continue;
});
const auto update = [=] {
const auto geometry = Ui::MapFrom(parent, target, target->rect());
const auto countPosition = [=](QSize size) {
const auto left = geometry.x()
+ (geometry.width() - size.width()) / 2;
const auto right = parent->width()
- st::normalFont->spacew;
return QPoint(
std::max(std::min(left, right - size.width()), 0),
geometry.y() - size.height() - st::normalFont->descent);
};
tooltip->pointAt(geometry, RectPart::Top, countPosition);
};
parent->widthValue(
) | rpl::start_with_next(update, tooltip->lifetime());
update();
tooltip->toggleAnimated(true);
data->raw = tooltip;
tooltip->shownValue() | rpl::filter(
!rpl::mappers::_1
) | rpl::start_with_next([=] {
crl::on_main(tooltip, [=] {
if (tooltip->isHidden()) {
if (data->raw == tooltip) {
data->raw = nullptr;
}
delete tooltip;
}
});
}, tooltip->lifetime());
base::timer_once(
duration
) | rpl::start_with_next([=] {
tooltip->toggleAnimated(false);
}, tooltip->lifetime());
}
using Ui::AddTableRow;
using Ui::TableRowTooltipData;
[[nodiscard]] QString CreateMessageLink(
not_null<Main::Session*> session,
@@ -236,8 +168,10 @@ void ShowInfoTooltip(
return result;
}
[[nodiscard]] tr::phrase<lngtag_count> GiftDurationPhrase(int months) {
return (months < 12)
[[nodiscard]] tr::phrase<lngtag_count> GiftDurationPhrase(int days) {
return (days < 30)
? tr::lng_premium_gift_duration_days
: (days < 30 * 12)
? tr::lng_premium_gift_duration_months
: tr::lng_premium_gift_duration_years;
}
@@ -260,230 +194,6 @@ void ShowInfoTooltip(
: st::giveawayGiftCodeValueMultiline));
}
[[nodiscard]] object_ptr<Ui::RpWidget> MakeValueWithSmallButton(
not_null<Ui::TableLayout*> table,
not_null<Ui::RpWidget*> value,
rpl::producer<QString> buttonText,
Fn<void(not_null<Ui::RpWidget*> button)> handler = nullptr,
int topSkip = 0) {
class MarginedWidget final : public Ui::RpWidget {
public:
using RpWidget::RpWidget;
QMargins getMargins() const override {
return { 0, 0, 0, st::giveawayGiftCodePeerMargin.bottom() };
}
};
auto result = object_ptr<MarginedWidget>(table);
const auto raw = result.data();
value->setParent(raw);
value->show();
const auto button = Ui::CreateChild<Ui::RoundButton>(
raw,
std::move(buttonText),
table->st().smallButton);
button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
if (handler) {
button->setClickedCallback([button, handler = std::move(handler)] {
handler(button);
});
} else {
button->setAttribute(Qt::WA_TransparentForMouseEvents);
}
rpl::combine(
raw->widthValue(),
button->widthValue(),
value->naturalWidthValue()
) | rpl::start_with_next([=](int width, int buttonWidth, int) {
const auto buttonSkip = st::normalFont->spacew + buttonWidth;
value->resizeToNaturalWidth(width - buttonSkip);
value->moveToLeft(0, 0, width);
button->moveToLeft(
rect::right(value) + st::normalFont->spacew,
(topSkip
+ (table->st().defaultValue.style.font->ascent
- table->st().smallButton.style.font->ascent)),
width);
}, value->lifetime());
value->heightValue() | rpl::start_with_next([=](int height) {
const auto bottom = st::giveawayGiftCodePeerMargin.bottom();
raw->resize(raw->width(), height + bottom);
}, raw->lifetime());
return result;
}
[[nodiscard]] object_ptr<Ui::RpWidget> MakePeerTableValue(
not_null<Ui::TableLayout*> table,
std::shared_ptr<ChatHelpers::Show> show,
PeerId id,
rpl::producer<QString> button = nullptr,
Fn<void()> handler = nullptr) {
auto result = object_ptr<Ui::AbstractButton>(table);
const auto raw = result.data();
const auto &st = st::giveawayGiftCodeUserpic;
raw->resize(raw->width(), st.photoSize);
const auto peer = show->session().data().peer(id);
const auto userpic = Ui::CreateChild<Ui::UserpicButton>(raw, peer, st);
const auto label = Ui::CreateChild<Ui::FlatLabel>(
raw,
(button && handler) ? peer->shortName() : peer->name(),
table->st().defaultValue);
raw->widthValue() | rpl::start_with_next([=](int width) {
const auto position = st::giveawayGiftCodeNamePosition;
label->resizeToNaturalWidth(width - position.x());
label->moveToLeft(position.x(), position.y(), width);
const auto top = (raw->height() - userpic->height()) / 2;
userpic->moveToLeft(0, top, width);
}, label->lifetime());
label->naturalWidthValue() | rpl::start_with_next([=](int width) {
raw->setNaturalWidth(st::giveawayGiftCodeNamePosition.x() + width);
}, label->lifetime());
userpic->setAttribute(Qt::WA_TransparentForMouseEvents);
label->setAttribute(Qt::WA_TransparentForMouseEvents);
label->setTextColorOverride(table->st().defaultValue.palette.linkFg->c);
raw->setClickedCallback([=] {
show->showBox(PrepareShortInfoBox(peer, show));
});
if (!button || !handler) {
return result;
}
return MakeValueWithSmallButton(
table,
result.release(),
std::move(button),
[=](not_null<Ui::RpWidget*> button) { handler(); },
st::giveawayGiftCodeNamePosition.y());
}
[[nodiscard]] object_ptr<Ui::RpWidget> MakePeerWithStatusValue(
not_null<Ui::TableLayout*> table,
std::shared_ptr<ChatHelpers::Show> show,
PeerId id,
Fn<void(not_null<Ui::RpWidget*>, EmojiStatusId)> pushStatusId) {
auto result = object_ptr<Ui::RpWidget>(table);
const auto raw = result.data();
const auto peerLabel = MakePeerTableValue(table, show, id).release();
peerLabel->setParent(raw);
peerLabel->show();
raw->resize(raw->width(), peerLabel->height());
using namespace Info::Profile;
struct State {
rpl::variable<Badge::Content> content;
};
const auto peer = show->session().data().peer(id);
const auto state = peerLabel->lifetime().make_state<State>();
state->content = EmojiStatusIdValue(
peer
) | rpl::map([=](EmojiStatusId emojiStatusId) {
if (!peer->session().premium()
|| (!peer->isSelf() && !emojiStatusId)) {
return Badge::Content();
}
return Badge::Content{
.badge = BadgeType::Premium,
.emojiStatusId = emojiStatusId,
};
});
const auto badge = peerLabel->lifetime().make_state<Badge>(
raw,
st::infoPeerBadge,
&peer->session(),
state->content.value(),
nullptr,
[=] { return show->paused(ChatHelpers::PauseReason::Layer); });
state->content.value(
) | rpl::start_with_next([=](const Badge::Content &content) {
if (const auto widget = badge->widget()) {
pushStatusId(widget, content.emojiStatusId);
}
}, raw->lifetime());
rpl::combine(
raw->widthValue(),
rpl::single(rpl::empty) | rpl::then(badge->updated())
) | rpl::start_with_next([=](int width, const auto &) {
const auto badgeWidget = badge->widget();
const auto badgeSkip = badgeWidget
? (st::normalFont->spacew + badgeWidget->width())
: 0;
peerLabel->resizeToNaturalWidth(width - badgeSkip);
peerLabel->moveToLeft(0, 0, width);
if (badgeWidget) {
badgeWidget->moveToLeft(
peerLabel->width() + st::normalFont->spacew,
st::giftBoxByStarsStarTop,
width);
}
}, raw->lifetime());
return result;
}
[[nodiscard]] object_ptr<Ui::RpWidget> MakeHiddenPeerTableValue(
not_null<Ui::TableLayout*> table) {
auto result = object_ptr<Ui::RpWidget>(table);
const auto raw = result.data();
const auto &st = st::giveawayGiftCodeUserpic;
raw->resize(raw->width(), st.photoSize);
const auto userpic = Ui::CreateChild<Ui::RpWidget>(raw);
const auto usize = st.photoSize;
userpic->resize(usize, usize);
userpic->paintRequest() | rpl::start_with_next([=] {
auto p = QPainter(userpic);
Ui::EmptyUserpic::PaintHiddenAuthor(p, 0, 0, usize, usize);
}, userpic->lifetime());
const auto label = Ui::CreateChild<Ui::FlatLabel>(
raw,
tr::lng_gift_from_hidden(),
table->st().defaultValue);
raw->widthValue(
) | rpl::start_with_next([=](int width) {
const auto position = st::giveawayGiftCodeNamePosition;
label->resizeToNaturalWidth(width - position.x());
label->moveToLeft(position.x(), position.y(), width);
const auto top = (raw->height() - userpic->height()) / 2;
userpic->moveToLeft(0, top, width);
}, label->lifetime());
userpic->setAttribute(Qt::WA_TransparentForMouseEvents);
label->setAttribute(Qt::WA_TransparentForMouseEvents);
label->setTextColorOverride(st::windowFg->c);
return result;
}
void AddTableRow(
not_null<Ui::TableLayout*> table,
rpl::producer<QString> label,
object_ptr<Ui::RpWidget> value,
style::margins valueMargins = st::giveawayGiftCodeValueMargin) {
table->addRow(
(label
? object_ptr<Ui::FlatLabel>(
table,
std::move(label),
table->st().defaultLabel)
: object_ptr<Ui::FlatLabel>(nullptr)),
std::move(value),
st::giveawayGiftCodeLabelMargin,
valueMargins);
}
[[nodiscard]] object_ptr<Ui::RpWidget> MakePriceWithChangePercentValue(
not_null<Ui::TableLayout*> table,
const std::shared_ptr<Data::UniqueGiftValue> &value) {
@@ -509,35 +219,13 @@ void AddTableRow(
return MakeValueWithSmallButton(table, label, std::move(text));
}
[[nodiscard]] object_ptr<Ui::RpWidget> MakePriceValueWithTooltip(
not_null<Ui::TableLayout*> table,
std::shared_ptr<InfoTooltipData> data,
TextWithEntities price,
TextWithEntities tooltip) {
const auto label = Ui::CreateChild<Ui::FlatLabel>(
table,
rpl::single(price),
table->st().defaultValue);
label->setAttribute(Qt::WA_TransparentForMouseEvents);
const auto handler = [=](not_null<Ui::RpWidget*> button) {
ShowInfoTooltip(
data,
button,
rpl::single(tooltip),
kPriceTooltipDuration);
};
auto text = rpl::single(u"?"_q);
return MakeValueWithSmallButton(table, label, std::move(text), handler);
}
[[nodiscard]] object_ptr<Ui::RpWidget> MakeMinimumPriceValue(
not_null<Ui::TableLayout*> table,
std::shared_ptr<InfoTooltipData> tooltip,
std::shared_ptr<TableRowTooltipData> tooltip,
const std::shared_ptr<Data::UniqueGift> &unique) {
const auto &value = unique->value;
const auto text = FormatValuePrice(value->minimumPrice, value->currency);
return MakePriceValueWithTooltip(
return Ui::MakeTableValueWithTooltip(
table,
std::move(tooltip),
text,
@@ -552,11 +240,11 @@ void AddTableRow(
[[nodiscard]] object_ptr<Ui::RpWidget> MakeAveragePriceValue(
not_null<Ui::TableLayout*> table,
std::shared_ptr<InfoTooltipData> tooltip,
std::shared_ptr<TableRowTooltipData> tooltip,
const std::shared_ptr<Data::UniqueGift> &unique) {
const auto &value = unique->value;
const auto text = FormatValuePrice(value->averagePrice, value->currency);
return MakePriceValueWithTooltip(
return Ui::MakeTableValueWithTooltip(
table,
std::move(tooltip),
text,
@@ -592,13 +280,12 @@ void AddUniqueGiftPropertyRows(
not_null<Ui::RpWidget*> container,
not_null<Ui::TableLayout*> table,
not_null<Data::UniqueGift*> unique) {
const auto tooltip = std::make_shared<InfoTooltipData>(InfoTooltipData{
.parent = container,
});
const auto tooltip = std::make_shared<TableRowTooltipData>(
TableRowTooltipData{ .parent = container });
const auto showTooltip = [=](
not_null<Ui::RpWidget*> widget,
rpl::producer<TextWithEntities> text) {
ShowInfoTooltip(tooltip, widget, std::move(text), kTooltipDuration);
ShowTableRowTooltip(tooltip, widget, std::move(text), kTooltipDuration);
};
const auto showRarity = [=](
not_null<Ui::RpWidget*> widget,
@@ -712,37 +399,6 @@ void AddUniqueGiftPropertyRows(
handler);
}
not_null<Ui::FlatLabel*> AddTableRow(
not_null<Ui::TableLayout*> table,
rpl::producer<QString> label,
rpl::producer<TextWithEntities> value,
const Ui::Text::MarkedContext &context = {}) {
auto widget = object_ptr<Ui::FlatLabel>(
table,
std::move(value),
table->st().defaultValue,
st::defaultPopupMenu,
context);
const auto result = widget.data();
AddTableRow(table, std::move(label), std::move(widget));
return result;
}
void AddTableRow(
not_null<Ui::TableLayout*> table,
rpl::producer<QString> label,
std::shared_ptr<ChatHelpers::Show> show,
PeerId id) {
if (!id) {
return;
}
AddTableRow(
table,
std::move(label),
MakePeerTableValue(table, show, id),
st::giveawayGiftCodePeerMargin);
}
void AddTable(
not_null<Ui::VerticalLayout*> container,
std::shared_ptr<ChatHelpers::Show> show,
@@ -778,7 +434,7 @@ void AddTable(
tr::lng_gift_link_label_gift(),
tr::lng_gift_link_gift_premium(
lt_duration,
GiftDurationValue(current.months) | Ui::Text::ToWithEntities(),
GiftDurationValue(current.days) | Ui::Text::ToWithEntities(),
Ui::Text::WithEntities));
if (!skipReason && current.from) {
const auto reason = AddTableRow(
@@ -863,17 +519,22 @@ void ShowAlreadyPremiumToast(
} // namespace
rpl::producer<QString> GiftDurationValue(int months) {
return GiftDurationPhrase(months)(
rpl::producer<QString> GiftDurationValue(int days) {
return GiftDurationPhrase(days)(
lt_count,
rpl::single(float64((months < 12) ? months : (months / 12))));
rpl::single(float64((days < 30)
? days
: (days < 30 * 12)
? (days / 30)
: (days / (30 * 12)))));
}
QString GiftDuration(int months) {
return GiftDurationPhrase(months)(
tr::now,
lt_count,
(months < 12) ? months : (months / 12));
QString GiftDuration(int days) {
return GiftDurationPhrase(days)(tr::now, lt_count, (days < 30)
? days
: (days < 30 * 12)
? (days / 30)
: (days / (30 * 12)));
}
void GiftCodeBox(
@@ -1121,9 +782,9 @@ void ResolveGiftCode(
code.to = toId;
const auto self = (fromId == selfId);
const auto peer = session->data().peer(self ? toId : fromId);
const auto months = code.months;
const auto days = code.days;
const auto parent = controller->parentController();
Settings::ShowGiftPremium(parent, peer, months, self);
Settings::ShowGiftPremium(parent, peer, days, self);
} else {
controller->uiShow()->showBox(Box(GiftCodeBox, controller, slug));
}
@@ -1242,7 +903,7 @@ void GiveawayInfoBox(
lt_channel,
Ui::Text::Bold(first),
lt_duration,
TextWithEntities{ GiftDuration(months) },
TextWithEntities{ GiftDuration(months * 30) },
Ui::Text::RichLangValue),
Ui::Text::RichLangValue));
const auto many = start
@@ -1550,13 +1211,12 @@ void AddStarGiftTable(
const auto giftToChannel = entry.giftChannelSavedId
&& peerIsChannel(PeerId(entry.bareEntryOwnerId));
const auto tooltip = std::make_shared<InfoTooltipData>(InfoTooltipData{
.parent = container,
});
const auto tooltip = std::make_shared<TableRowTooltipData>(
TableRowTooltipData{ .parent = container });
const auto showTooltip = [=](
not_null<Ui::RpWidget*> widget,
rpl::producer<TextWithEntities> text) {
ShowInfoTooltip(tooltip, widget, std::move(text), kTooltipDuration);
ShowTableRowTooltip(tooltip, widget, std::move(text), kTooltipDuration);
};
if (unique && entry.bareGiftResaleRecipientId) {
@@ -2313,9 +1973,8 @@ void AddUniqueGiftValueTable(
MakePriceWithChangePercentValue(table, value));
}
const auto tooltip = std::make_shared<InfoTooltipData>(InfoTooltipData{
.parent = container,
});
const auto tooltip = std::make_shared<TableRowTooltipData>(
TableRowTooltipData{ .parent = container });
if (value->minimumPrice) {
AddTableRow(
table,

View File

@@ -44,8 +44,8 @@ namespace Window {
class SessionNavigation;
} // namespace Window
[[nodiscard]] rpl::producer<QString> GiftDurationValue(int months);
[[nodiscard]] QString GiftDuration(int months);
[[nodiscard]] rpl::producer<QString> GiftDurationValue(int days);
[[nodiscard]] QString GiftDuration(int days);
void GiftCodeBox(
not_null<Ui::GenericBox*> box,

View File

@@ -983,10 +983,11 @@ void PeerListRow::setCheckedInternal(bool checked, anim::type animated) {
}
void PeerListRow::setCustomizedCheckSegments(
std::vector<Ui::OutlineSegment> segments) {
std::vector<Ui::OutlineSegment> segments,
bool liveBadge) {
Expects(_checkbox != nullptr);
_checkbox->setCustomizedSegments(std::move(segments));
_checkbox->setCustomizedSegments(std::move(segments), liveBadge);
}
void PeerListRow::finishCheckedAnimation() {

View File

@@ -223,7 +223,8 @@ public:
setCheckedInternal(checked, animated);
}
void setCustomizedCheckSegments(
std::vector<Ui::OutlineSegment> segments);
std::vector<Ui::OutlineSegment> segments,
bool liveBadge);
void setHidden(bool hidden) {
_hidden = hidden;
}

View File

@@ -132,27 +132,36 @@ QBrush PeerListStoriesGradient(const style::PeerList &st) {
}
std::vector<Ui::OutlineSegment> PeerListStoriesSegments(
int count,
int unread,
PeerListStoriesCounts counts,
const QBrush &unreadBrush) {
Expects(unread <= count);
Expects(count > 0);
Expects(counts.unread <= counts.count);
Expects(counts.count > 0);
auto result = std::vector<Ui::OutlineSegment>();
const auto add = [&](bool unread) {
result.push_back({
.brush = unread ? unreadBrush : st::dialogsUnreadBgMuted->b,
.brush = (counts.videoStream
? st::attentionButtonFg->b
: unread
? unreadBrush
: st::dialogsUnreadBgMuted->b),
.width = (unread
? st::dialogsStoriesFull.lineTwice / 2.
: st::dialogsStoriesFull.lineReadTwice / 2.),
});
};
result.reserve(count);
for (auto i = 0, till = count - unread; i != till; ++i) {
add(false);
}
for (auto i = 0; i != unread; ++i) {
if (counts.videoStream) {
add(true);
} else {
const auto count = counts.count;
const auto unread = counts.unread;
result.reserve(count);
for (auto i = 0, till = count - unread; i != till; ++i) {
add(false);
}
for (auto i = 0; i != unread; ++i) {
add(true);
}
}
return result;
}
@@ -521,18 +530,15 @@ void PeerListStories::updateColors() {
for (auto i = begin(_counts); i != end(_counts); ++i) {
if (const auto row = _delegate->peerListFindRow(i->first)) {
if (i->second.count >= 0 && i->second.unread >= 0) {
applyForRow(row, i->second.count, i->second.unread, true);
applyForRow(row, i->second, true);
}
}
}
}
void PeerListStories::updateFor(
uint64 id,
int count,
int unread) {
void PeerListStories::updateFor(uint64 id, Counts counts) {
if (const auto row = _delegate->peerListFindRow(id)) {
applyForRow(row, count, unread);
applyForRow(row, counts);
_delegate->peerListUpdateRow(row);
}
}
@@ -550,11 +556,14 @@ void PeerListStories::process(not_null<PeerListRow*> row) {
? 1
: 0;
const auto unread = source
? source->info().unreadCount
? int(source->info().unreadCount)
: user->hasUnreadStories()
? 1
: 0;
applyForRow(row, count, unread, true);
const auto videoStream = source
? bool(source->info().hasVideoStream)
: user->hasActiveVideoStream();
applyForRow(row, { count, unread, videoStream }, true);
}
bool PeerListStories::handleClick(not_null<PeerData*> peer) {
@@ -597,25 +606,28 @@ void PeerListStories::prepare(not_null<PeerListDelegate*> delegate) {
const auto info = source
? source->info()
: Data::StoriesSourceInfo();
updateFor(id.value, info.count, info.unreadCount);
updateFor(id.value, {
int(info.count),
int(info.unreadCount),
bool(info.hasVideoStream),
});
}, _lifetime);
}
void PeerListStories::applyForRow(
not_null<PeerListRow*> row,
int count,
int unread,
Counts counts,
bool force) {
auto &counts = _counts[row->id()];
if (!force && counts.count == count && counts.unread == unread) {
auto &existing = _counts[row->id()];
if (!force && existing == counts) {
return;
}
counts.count = count;
counts.unread = unread;
_delegate->peerListSetRowChecked(row, count > 0);
if (count > 0) {
existing = counts;
_delegate->peerListSetRowChecked(row, counts.count > 0);
if (counts.count > 0) {
row->setCustomizedCheckSegments(
PeerListStoriesSegments(count, unread, _unreadBrush));
PeerListStoriesSegments(counts, _unreadBrush),
counts.videoStream);
}
}

View File

@@ -42,9 +42,18 @@ class SessionController;
[[nodiscard]] object_ptr<Ui::BoxContent> PrepareContactsBox(
not_null<Window::SessionController*> sessionController);
[[nodiscard]] QBrush PeerListStoriesGradient(const style::PeerList &st);
struct PeerListStoriesCounts {
int count = 0;
int unread = 0;
bool videoStream = false;
friend inline bool operator==(
const PeerListStoriesCounts &a,
const PeerListStoriesCounts &b) = default;
};
[[nodiscard]] std::vector<Ui::OutlineSegment> PeerListStoriesSegments(
int count,
int unread,
PeerListStoriesCounts counts,
const QBrush &unreadBrush);
class PeerListRowWithLink : public PeerListRow {
@@ -211,17 +220,13 @@ public:
bool handleClick(not_null<PeerData*> peer);
private:
struct Counts {
int count = 0;
int unread = 0;
};
using Counts = PeerListStoriesCounts;
void updateColors();
void updateFor(uint64 id, int count, int unread);
void updateFor(uint64 id, Counts counts);
void applyForRow(
not_null<PeerListRow*> row,
int count,
int unread,
Counts counts,
bool force = false);
const not_null<PeerListController*> _controller;

View File

@@ -521,11 +521,6 @@ void Set(
} else {
peer->changeColorProfileIndex(index);
}
if (colorCollectible) {
peer->changeColorCollectible(*colorCollectible);
} else {
peer->clearColorCollectible();
}
peer->changeProfileBackgroundEmojiId(emojiId);
} else {
if (index == kUnsetColorIndex) {
@@ -542,8 +537,10 @@ void Set(
}
peer->session().changes().peerUpdated(
peer,
(values.forProfile ? UpdateFlag::ColorProfile : UpdateFlag::Color)
| UpdateFlag::BackgroundEmoji);
(UpdateFlag::BackgroundEmoji
| (values.forProfile
? UpdateFlag::ColorProfile
: UpdateFlag::Color)));
};
setLocal(
values.colorIndex,
@@ -580,10 +577,11 @@ void Set(
using ColorFlag = MTPDpeerColor::Flag;
send(MTPaccount_UpdateColor(
MTP_flags((values.forProfile ? Flag::f_for_profile : Flag(0))
| (values.colorIndex != kUnsetColorIndex
| (((!values.forProfile && values.colorCollectible)
|| (values.colorIndex != kUnsetColorIndex))
? Flag::f_color
: Flag(0))),
(values.colorCollectible
((!values.forProfile && values.colorCollectible)
? MTP_inputPeerColorCollectible(
MTP_long(values.colorCollectible->collectibleId))
: MTP_peerColor(
@@ -594,6 +592,13 @@ void Set(
: ColorFlag(0))),
MTP_int(values.colorIndex),
MTP_long(values.backgroundEmojiId)))));
if (values.statusChanged
&& (values.statusId || peer->emojiStatusId())) {
peer->owner().emojiStatuses().set(
peer,
values.statusId,
values.statusUntil);
}
} else if (const auto channel = peer->asChannel()) {
if (peer->isBroadcast()) {
using Flag = MTPchannels_UpdateColor::Flag;
@@ -648,10 +653,10 @@ void Apply(
const auto colorMatch = (currentColorIndex == values.colorIndex);
const auto emojiMatch = (currentEmojiId == values.backgroundEmojiId);
const auto collectibleMatch = (!peer->colorCollectible()
== !values.colorCollectible)
&& (!peer->colorCollectible()
|| (*peer->colorCollectible() == *values.colorCollectible));
const auto collectibleMatch = values.forProfile
|| ((!peer->colorCollectible() == !values.colorCollectible)
&& (!peer->colorCollectible()
|| (*peer->colorCollectible() == *values.colorCollectible)));
if (colorMatch
&& emojiMatch
@@ -1179,7 +1184,7 @@ void AddGiftSelector(
not_null<Main::Session*> session,
rpl::producer<uint64> showingGiftIdValue,
Fn<void(std::shared_ptr<Data::UniqueGift> selected)> chosen,
rpl::producer<std::optional<Ui::ColorCollectible>> selected,
rpl::producer<uint64> selected,
bool profile,
rpl::producer<uint64> selectedGiftId = rpl::single(uint64(0)),
Fn<void()> switchToNextTab = nullptr) {
@@ -1202,7 +1207,7 @@ void AddGiftSelector(
std::vector<bool> validated;
std::vector<std::unique_ptr<GiftButton>> buttons;
rpl::variable<Ui::VisibleRange> visibleRange;
rpl::variable<std::optional<Ui::ColorCollectible>> selected;
rpl::variable<uint64> selected;
rpl::variable<uint64> selectedGiftId;
int perRow = 1;
base::unique_qptr<Ui::RpWidget> emptyPlaceholder;
@@ -1302,8 +1307,7 @@ void AddGiftSelector(
Assert(rowTill >= rowFrom);
const auto first = rowFrom * perRow;
const auto last = std::min(rowTill * perRow, count);
const auto current = state->selected.current();
const auto selectedCollectibleId = current ? current->collectibleId : 0;
const auto selectedCollectibleId = state->selected.current();
const auto selectedGiftId = state->selectedGiftId.current();
auto checkedFrom = 0;
auto checkedTill = int(buttons.size());
@@ -1396,10 +1400,8 @@ void AddGiftSelector(
state->selected.value(
) | rpl::combine_previous() | rpl::start_with_next([=](
const std::optional<Ui::ColorCollectible> &was,
const std::optional<Ui::ColorCollectible> &now) {
const auto wasCollectibleId = was ? was->collectibleId : 0;
const auto nowCollectibleId = now ? now->collectibleId : 0;
uint64 wasCollectibleId,
uint64 nowCollectibleId) {
if (wasCollectibleId) {
if (const auto button = find(wasCollectibleId)) {
button->toggleSelected(false, GiftSelectionMode::Inset);
@@ -1616,6 +1618,7 @@ not_null<Info::Profile::TopBar*> CreateProfilePreview(
preview->resize(
container->width(),
st::infoProfileTopBarNoActionsHeightMax);
preview->setAttribute(Qt::WA_TransparentForMouseEvents);
return preview;
}
@@ -2062,7 +2065,10 @@ void EditPeerColorSection(
? *selected->peerColor
: std::optional<Ui::ColorCollectible>();
},
state->collectible.value(),
state->collectible.value() | rpl::map([](
const std::optional<Ui::ColorCollectible> &value) {
return value ? value->collectibleId : 0;
}),
false,
rpl::single(uint64(0)),
switchToNextTab);
@@ -2178,6 +2184,8 @@ void EditPeerProfileColorSection(
std::shared_ptr<Ui::ChatStyle> style,
std::shared_ptr<Ui::ChatTheme> theme,
Fn<void()> aboutCallback) {
Expects(peer->isSelf());
ProcessButton(button);
const auto preview = CreateProfilePreview(box, container, show, peer);
@@ -2188,7 +2196,7 @@ void EditPeerProfileColorSection(
struct State {
rpl::variable<uint8> index = kUnsetColorIndex;
rpl::variable<DocumentId> patternEmojiId;
rpl::variable<std::optional<Ui::ColorCollectible>> collectible;
rpl::variable<EmojiStatusId> wearable;
rpl::variable<uint64> showingGiftId;
rpl::variable<uint64> selectedGiftId;
std::shared_ptr<Data::UniqueGift> buyCollectible;
@@ -2196,23 +2204,25 @@ void EditPeerProfileColorSection(
};
const auto state = button->lifetime().make_state<State>();
state->patternEmojiId = peer->profileBackgroundEmojiId();
state->collectible = peer->colorCollectible()
? *peer->colorCollectible()
: std::optional<Ui::ColorCollectible>();
state->wearable = peer->emojiStatusId();
const auto resetUnique = [=] {
preview->setLocalEmojiStatusId({});
state->buyCollectible = nullptr;
state->collectible.force_assign(std::nullopt);
state->wearable = {};
};
const auto setIndex = [=](uint8 index) {
state->index = index;
if (index != kUnsetColorIndex) {
resetUnique();
}
preview->setColorProfileIndex(index == kUnsetColorIndex
? std::nullopt
: std::make_optional(index));
preview->setPatternEmojiId(state->patternEmojiId.current());
resetUnique();
preview->setPatternEmojiId(index == kUnsetColorIndex
? std::nullopt
: std::make_optional(state->patternEmojiId.current()));
};
setIndex(peer->colorProfileIndex().value_or(kUnsetColorIndex));
@@ -2320,17 +2330,16 @@ void EditPeerProfileColorSection(
&& selected->starsForResale > 0)
? selected
: nullptr;
state->collectible = selected->peerColor
? *selected->peerColor
: std::optional<Ui::ColorCollectible>();
const auto statuses = &peer->owner().emojiStatuses();
state->wearable = statuses->fromUniqueGift(*selected);
preview->setColorProfileIndex(std::nullopt);
preview->setPatternEmojiId(selected->pattern.document->id);
preview->setLocalEmojiStatusId(
session->data().emojiStatuses().fromUniqueGift(
*selected));
preview->setLocalEmojiStatusId(state->wearable.current());
resetWrap->toggle(true, anim::type::normal);
},
state->collectible.value(),
state->wearable.value() | rpl::map([=](const EmojiStatusId &value) {
return value.collectible ? value.collectible->id : 0;
}),
true,
state->selectedGiftId.value(),
switchToNextTab);
@@ -2347,13 +2356,19 @@ void EditPeerProfileColorSection(
} else if (ShowPremiumPreview(show, peer)) {
return;
}
const auto statusId = peer->emojiStatusId();
const auto wearable = state->wearable.current();
const auto statusChanged = wearable.collectible
? (!statusId.collectible
|| statusId.collectible->id != wearable.collectible->id)
: (statusId.collectible != nullptr);
const auto values = SetValues{
.colorIndex = state->index.current(),
.backgroundEmojiId = state->patternEmojiId.current(),
.colorCollectible = state->collectible.current(),
.statusId = {},
.colorCollectible = std::nullopt,
.statusId = state->wearable.current(),
.statusUntil = 0,
.statusChanged = false,
.statusChanged = statusChanged,
.forProfile = true,
};
if (const auto buy = state->buyCollectible) {
@@ -2376,15 +2391,17 @@ void EditPeerProfileColorSection(
profileState->applying = false;
}));
});
state->collectible.value(
) | rpl::start_with_next([=] {
state->wearable.value(
) | rpl::start_with_next([=](EmojiStatusId id) {
const auto buy = state->buyCollectible.get();
while (!button->children().isEmpty()) {
delete button->children().first();
}
if (!buy) {
button->setText(rpl::combine(
tr::lng_settings_color_apply(),
(id.collectible
? tr::lng_settings_color_wear()
: tr::lng_settings_color_apply()),
Data::AmPremiumValue(&peer->session())
) | rpl::map([=](const QString &text, bool premium) {
auto result = TextWithEntities();
@@ -2540,6 +2557,12 @@ void SetupPeerColorSample(
) | rpl::map([=] {
return peer->colorProfileIndex();
});
auto emojiStatusIdValue = peer->session().changes().peerFlagsValue(
peer,
Data::PeerUpdate::Flag::EmojiStatus
) | rpl::map([=] {
return peer->emojiStatusId();
});
const auto name = peer->shortName();
const auto sampleSize = st::settingsColorSampleSize;
@@ -2554,36 +2577,77 @@ void SetupPeerColorSample(
name);
sample->show();
struct ProfileSampleState {
Data::ColorProfileSet colorSet;
};
const auto profileState
= button->lifetime().make_state<ProfileSampleState>();
const auto profileSample = Ui::CreateChild<Ui::ColorSample>(
button.get(),
[=, peerColors = &peer->session().api().peerColors()](uint8 index) {
return peerColors->colorProfileFor(peer).value_or(
Data::ColorProfileSet{});
},
[=](uint8 index) { return profileState->colorSet; },
0,
false);
profileSample->hide();
profileSample->resize(sampleSize, sampleSize);
const auto emojiStatusWidget = Ui::CreateChild<Ui::RpWidget>(
button.get());
emojiStatusWidget->hide();
emojiStatusWidget->resize(sampleSize, sampleSize);
button->lifetime().make_state<std::unique_ptr<Ui::Text::CustomEmoji>>();
struct EmojiStatusState {
std::unique_ptr<Ui::Text::CustomEmoji> emoji;
};
const auto emojiState = button->lifetime().make_state<EmojiStatusState>();
rpl::combine(
button->widthValue(),
rpl::duplicate(label),
rpl::duplicate(colorIndexValue),
rpl::duplicate(colorProfileIndexValue)
rpl::duplicate(colorProfileIndexValue),
rpl::duplicate(emojiStatusIdValue)
) | rpl::start_with_next([=](
int width,
const QString &buttonText,
int colorIndex,
std::optional<uint8> profileIndex) {
std::optional<uint8> profileIndex,
EmojiStatusId emojiStatusId) {
const auto available = width
- st::settingsButton.padding.left()
- (st::settingsColorButton.padding.right() - sampleSize)
- st::settingsButton.style.font->width(buttonText)
- st::settingsButtonRightSkip;
const auto hasProfile = profileIndex.has_value();
const auto hasEmojiStatus = emojiStatusId
&& emojiStatusId.collectible;
const auto hasProfile = profileIndex.has_value() || hasEmojiStatus;
if (hasEmojiStatus && emojiStatusId.collectible) {
const auto color = emojiStatusId.collectible->centerColor;
profileState->colorSet.palette = { color };
profileState->colorSet.bg = { color };
profileState->colorSet.story = { color };
} else if (hasProfile) {
const auto peerColors = &peer->session().api().peerColors();
profileState->colorSet
= peerColors->colorProfileFor(peer).value_or(
Data::ColorProfileSet{});
}
profileSample->setVisible(hasProfile);
emojiStatusWidget->setVisible(hasEmojiStatus);
if (hasEmojiStatus && !emojiState->emoji) {
emojiState->emoji
= peer->session().data().customEmojiManager().create(
Data::EmojiStatusCustomId(emojiStatusId),
[raw = emojiStatusWidget] { raw->update(); },
Data::CustomEmojiSizeTag::Normal);
} else if (!hasEmojiStatus) {
emojiState->emoji = nullptr;
}
sample->setForceCircle(hasProfile);
if (style->colorPatternIndex(colorIndex) || hasProfile) {
@@ -2601,18 +2665,21 @@ void SetupPeerColorSample(
? st::settingsColorSampleCutout
: 0);
profileSample->update();
emojiStatusWidget->update();
}, sample->lifetime());
rpl::combine(
button->sizeValue(),
sample->sizeValue(),
rpl::duplicate(colorIndexValue),
rpl::duplicate(colorProfileIndexValue)
rpl::duplicate(colorProfileIndexValue),
rpl::duplicate(emojiStatusIdValue)
) | rpl::start_with_next([=](
QSize outer,
QSize inner,
int colorIndex,
std::optional<uint8> profileIndex) {
std::optional<uint8> profileIndex,
EmojiStatusId emojiStatusId) {
const auto hasColor = (colorIndex != 0);
const auto right = st::settingsColorButton.padding.right()
@@ -2624,18 +2691,34 @@ void SetupPeerColorSample(
sample->move(
outer.width() - right - inner.width(),
(outer.height() - inner.height()) / 2);
profileSample->move(
sample->pos().x()
+ (hasColor
? (st::settingsColorProfileSampleShift
- st::settingsColorSampleSize
- st::lineWidth)
: 0),
sample->pos().y());
const auto profilePos = sample->pos()
+ (hasColor
? QPoint(st::settingsColorProfileSampleShift
- st::settingsColorSampleSize
- st::lineWidth, 0)
: QPoint());
profileSample->move(profilePos);
emojiStatusWidget->move(profilePos);
}, sample->lifetime());
constexpr auto kScale = 0.7;
emojiStatusWidget->paintOn([=](QPainter &p) {
if (!emojiState->emoji) {
return;
}
const auto size = emojiStatusWidget->size();
const auto offset = (size * (1.0 - kScale)) / 2.0;
p.translate(offset.width(), offset.height());
p.scale(kScale, kScale);
emojiState->emoji->paint(p, {
.textColor = st::windowFg->c,
.now = crl::now(),
});
});
sample->setAttribute(Qt::WA_TransparentForMouseEvents);
profileSample->setAttribute(Qt::WA_TransparentForMouseEvents);
emojiStatusWidget->setAttribute(Qt::WA_TransparentForMouseEvents);
}
void AddPeerColorButton(

View File

@@ -747,7 +747,9 @@ void SendFilesBox::refreshButtons() {
_send,
_show,
_sendMenuDetails,
_sendMenuCallback);
_sendMenuCallback,
&_st.tabbed.menu,
&_st.tabbed.icons);
}
addButton(tr::lng_cancel(), [=] { closeBox(); });
_addFile = addLeftButton(

View File

@@ -1707,6 +1707,9 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback(
const auto commonSendFlags = Flag(0)
| Flag::f_with_my_score
| (options.scheduled ? Flag::f_schedule_date : Flag(0))
| ((options.scheduled && options.scheduleRepeatPeriod)
? Flag::f_schedule_repeat_period
: Flag(0))
| ((forwardOptions != Data::ForwardOptions::PreserveInfo)
? Flag::f_drop_author
: Flag(0))
@@ -1786,6 +1789,7 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback(
? MTP_inputReplyToMonoForum(sublistPeer->input)
: MTPInputReplyTo()),
MTP_int(options.scheduled),
MTP_int(options.scheduleRepeatPeriod),
MTP_inputPeerEmpty(), // send_as
Data::ShortcutIdToMTP(session, options.shortcutId),
MTP_int(videoTimestamp.value_or(0)),

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,56 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
namespace ChatHelpers {
class Show;
} // namespace ChatHelpers
namespace Data {
struct GiftAuctionState;
} // namespace Data
namespace Info::PeerGifts {
struct GiftSendDetails;
} // namespace Info::PeerGifts
namespace Window {
class SessionController;
} // namespace Window
namespace Ui {
class BoxContent;
class RoundButton;
[[nodiscard]] rpl::lifetime ShowStarGiftAuction(
not_null<Window::SessionController*> controller,
PeerData *peer,
QString slug,
Fn<void()> finishRequesting,
Fn<void()> boxClosed);
struct AuctionBidBoxArgs {
not_null<PeerData*> peer;
std::shared_ptr<ChatHelpers::Show> show;
rpl::producer<Data::GiftAuctionState> state;
std::unique_ptr<Info::PeerGifts::GiftSendDetails> details;
};
[[nodiscard]] object_ptr<BoxContent> MakeAuctionBidBox(
AuctionBidBoxArgs &&args);
enum class AuctionButtonCountdownType {
Join,
Place,
};
void SetAuctionButtonCountdownText(
not_null<RoundButton*> button,
AuctionButtonCountdownType type,
rpl::producer<Data::GiftAuctionState> value);
} // namespace Ui

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_star_gift.h"
namespace Api {
class PremiumGiftCodeOptions;
} // namespace Api
namespace ChatHelpers {
class Show;
} // namespace ChatHelpers
@@ -18,8 +22,13 @@ struct UniqueGift;
struct GiftCode;
struct CreditsHistoryEntry;
class SavedStarGiftId;
struct GiftAuctionState;
} // namespace Data
namespace Info::PeerGifts {
struct GiftDescriptor;
} // namespace Info::PeerGifts
namespace Main {
class Session;
class SessionShow;
@@ -44,6 +53,7 @@ class CustomEmoji;
namespace Ui {
class RpWidget;
class PopupMenu;
class GenericBox;
class VerticalLayout;
@@ -136,18 +146,6 @@ void ShowGiftTransferredToast(
not_null<PeerData*> to,
const Data::UniqueGift &gift);
void ShowResaleGiftBoughtToast(
std::shared_ptr<Main::SessionShow> show,
not_null<PeerData*> to,
const Data::UniqueGift &gift);
[[nodiscard]] rpl::lifetime ShowStarGiftResale(
not_null<Window::SessionController*> controller,
not_null<PeerData*> peer,
uint64 giftId,
QString title,
Fn<void()> finishRequesting);
[[nodiscard]] CreditsAmount StarsFromTon(
not_null<Main::Session*> session,
CreditsAmount ton);
@@ -155,4 +153,22 @@ void ShowResaleGiftBoughtToast(
not_null<Main::Session*> session,
CreditsAmount stars);
struct GiftsDescriptor {
std::vector<Info::PeerGifts::GiftDescriptor> list;
std::shared_ptr<Api::PremiumGiftCodeOptions> api;
};
[[nodiscard]] object_ptr<RpWidget> MakeGiftsSendList(
not_null<Window::SessionController*> window,
not_null<PeerData*> peer,
rpl::producer<GiftsDescriptor> gifts,
Fn<void()> loadMore);
void SendGiftBox(
not_null<GenericBox*> box,
not_null<Window::SessionController*> window,
not_null<PeerData*> peer,
std::shared_ptr<Api::PremiumGiftCodeOptions> api,
const Info::PeerGifts::GiftDescriptor &descriptor,
rpl::producer<Data::GiftAuctionState> auctionState);
} // namespace Ui

View File

@@ -0,0 +1,673 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "boxes/star_gift_resale_box.h"
#include "boxes/star_gift_box.h"
#include "chat_helpers/compose/compose_show.h"
#include "core/ui_integration.h"
#include "data/stickers/data_custom_emoji.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "data/data_star_gift.h"
#include "lang/lang_keys.h"
#include "info/peer_gifts/info_peer_gifts_common.h"
#include "main/main_session.h"
#include "menu/gift_resale_filter.h"
#include "ui/layers/generic_box.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/popup_menu.h"
#include "ui/painter.h"
#include "ui/ui_utility.h"
#include "window/window_session_controller.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_credits.h"
#include "styles/style_layers.h"
#include "styles/style_menu_icons.h"
#include <QtWidgets/QApplication>
namespace Ui {
namespace {
constexpr auto kFiltersCount = 4;
constexpr auto kResaleBoughtToastDuration = 4 * crl::time(1000);
using Data::GiftAttributeId;
using Data::GiftAttributeIdType;
using Data::ResaleGiftsSort;
using Data::ResaleGiftsFilter;
using Data::ResaleGiftsDescriptor;
//using Data::MyGiftsDescriptor;
[[nodiscard]] Text::String ResaleTabText(QString text) {
auto result = Text::String();
result.setMarkedText(
st::semiboldTextStyle,
TextWithEntities{ text }.append(st::giftBoxResaleTabsDropdown),
kMarkupTextOptions);
return result;
}
[[nodiscard]] Text::String SortModeText(ResaleGiftsSort mode) {
auto text = [&] {
if (mode == ResaleGiftsSort::Number) {
return Ui::Text::IconEmoji(&st::giftBoxResaleMiniNumber).append(
tr::lng_gift_resale_number(tr::now));
} else if (mode == ResaleGiftsSort::Price) {
return Ui::Text::IconEmoji(&st::giftBoxResaleMiniPrice).append(
tr::lng_gift_resale_price(tr::now));
}
return Ui::Text::IconEmoji(&st::giftBoxResaleMiniDate).append(
tr::lng_gift_resale_date(tr::now));
}();
auto result = Text::String();
result.setMarkedText(
st::semiboldTextStyle,
text,
kMarkupTextOptions);
return result;
}
struct ResaleTabs {
rpl::producer<ResaleGiftsFilter> filter;
object_ptr<RpWidget> widget;
};
[[nodiscard]] ResaleTabs MakeResaleTabs(
std::shared_ptr<ChatHelpers::Show> show,
not_null<PeerData*> peer,
const ResaleGiftsDescriptor &info,
rpl::producer<ResaleGiftsFilter> filter) {
auto widget = object_ptr<RpWidget>((QWidget*)nullptr);
const auto raw = widget.data();
struct Button {
QRect geometry;
Text::String text;
};
struct State {
rpl::variable<ResaleGiftsFilter> filter;
rpl::variable<int> fullWidth;
std::vector<Button> buttons;
base::unique_qptr<Ui::PopupMenu> menu;
ResaleGiftsDescriptor lists;
int dragx = 0;
int pressx = 0;
float64 dragscroll = 0.;
float64 scroll = 0.;
int scrollMax = 0;
int selected = -1;
int pressed = -1;
};
const auto state = raw->lifetime().make_state<State>();
state->filter = std::move(filter);
state->lists.backdrops = info.backdrops;
state->lists.models = info.models;
state->lists.patterns = info.patterns;
const auto scroll = [=] {
return QPoint(int(base::SafeRound(state->scroll)), 0);
};
static constexpr auto IndexToType = [](int index) {
Expects(index > 0 && index < 4);
return (index == 1)
? GiftAttributeIdType::Model
: (index == 2)
? GiftAttributeIdType::Backdrop
: GiftAttributeIdType::Pattern;
};
const auto setSelected = [=](int index) {
const auto was = (state->selected >= 0);
const auto now = (index >= 0);
state->selected = index;
if (was != now) {
raw->setCursor(now ? style::cur_pointer : style::cur_default);
}
};
const auto showMenu = [=](int index) {
if (state->menu) {
return;
}
state->menu = base::make_unique_q<Ui::PopupMenu>(
raw,
st::giftBoxResaleFilter);
const auto menu = state->menu.get();
const auto modify = [=](Fn<void(ResaleGiftsFilter&)> modifier) {
auto now = state->filter.current();
modifier(now);
state->filter = now;
};
const auto actionWithIcon = [=](
QString text,
Fn<void()> callback,
not_null<const style::icon*> icon,
bool checked = false) {
auto action = base::make_unique_q<Ui::GiftResaleFilterAction>(
menu,
menu->st().menu,
TextWithEntities{ text },
Ui::Text::MarkedContext(),
QString(),
icon);
action->setChecked(checked);
action->setClickedCallback(std::move(callback));
menu->addAction(std::move(action));
};
auto context = Core::TextContext({ .session = &show->session() });
context.customEmojiFactory = [original = context.customEmojiFactory](
QStringView data,
const Ui::Text::MarkedContext &context) {
return Ui::GiftResaleColorEmoji::Owns(data)
? std::make_unique<Ui::GiftResaleColorEmoji>(data)
: original(data, context);
};
const auto actionWithEmoji = [=](
TextWithEntities text,
Fn<void()> callback,
QString data,
bool checked) {
auto action = base::make_unique_q<Ui::GiftResaleFilterAction>(
menu,
menu->st().menu,
std::move(text),
context,
data,
nullptr);
action->setChecked(checked);
action->setClickedCallback(std::move(callback));
menu->addAction(std::move(action));
};
const auto actionWithDocument = [=](
TextWithEntities text,
Fn<void()> callback,
DocumentId id,
bool checked) {
actionWithEmoji(
std::move(text),
std::move(callback),
Data::SerializeCustomEmojiId(id),
checked);
};
const auto actionWithColor = [=](
TextWithEntities text,
Fn<void()> callback,
const QColor &color,
bool checked) {
actionWithEmoji(
std::move(text),
std::move(callback),
Ui::GiftResaleColorEmoji::DataFor(color),
checked);
};
if (!index) {
const auto sort = [=](ResaleGiftsSort value) {
modify([&](ResaleGiftsFilter &filter) {
filter.sort = value;
});
};
const auto is = [&](ResaleGiftsSort value) {
return state->filter.current().sort == value;
};
actionWithIcon(tr::lng_gift_resale_sort_price(tr::now), [=] {
sort(ResaleGiftsSort::Price);
}, &st::menuIconOrderPrice, is(ResaleGiftsSort::Price));
actionWithIcon(tr::lng_gift_resale_sort_date(tr::now), [=] {
sort(ResaleGiftsSort::Date);
}, &st::menuIconOrderDate, is(ResaleGiftsSort::Date));
actionWithIcon(tr::lng_gift_resale_sort_number(tr::now), [=] {
sort(ResaleGiftsSort::Number);
}, &st::menuIconOrderNumber, is(ResaleGiftsSort::Number));
} else {
const auto now = state->filter.current().attributes;
const auto type = IndexToType(index);
const auto has = ranges::contains(
now,
type,
&GiftAttributeId::type);
if (has) {
actionWithIcon(tr::lng_gift_resale_filter_all(tr::now), [=] {
modify([&](ResaleGiftsFilter &filter) {
auto &list = filter.attributes;
for (auto i = begin(list); i != end(list);) {
if (i->type == type) {
i = list.erase(i);
} else {
++i;
}
}
});
}, &st::menuIconSelect);
}
const auto toggle = [=](GiftAttributeId id) {
modify([&](ResaleGiftsFilter &filter) {
auto &list = filter.attributes;
if (ranges::contains(list, id)) {
list.remove(id);
} else {
list.emplace(id);
}
});
};
const auto checked = [=](GiftAttributeId id) {
return !has || ranges::contains(now, id);
};
if (type == GiftAttributeIdType::Model) {
for (auto &entry : state->lists.models) {
const auto id = IdFor(entry.model);
const auto text = TextWithEntities{
entry.model.name
}.append(' ').append(Ui::Text::Bold(
Lang::FormatCountDecimal(entry.count)
));
actionWithDocument(text, [=] {
toggle(id);
}, id.value, checked(id));
}
} else if (type == GiftAttributeIdType::Backdrop) {
for (auto &entry : state->lists.backdrops) {
const auto id = IdFor(entry.backdrop);
const auto text = TextWithEntities{
entry.backdrop.name
}.append(' ').append(Ui::Text::Bold(
Lang::FormatCountDecimal(entry.count)
));
actionWithColor(text, [=] {
toggle(id);
}, entry.backdrop.centerColor, checked(id));
}
} else if (type == GiftAttributeIdType::Pattern) {
for (auto &entry : state->lists.patterns) {
const auto id = IdFor(entry.pattern);
const auto text = TextWithEntities{
entry.pattern.name
}.append(' ').append(Ui::Text::Bold(
Lang::FormatCountDecimal(entry.count)
));
actionWithDocument(text, [=] {
toggle(id);
}, id.value, checked(id));
}
}
}
menu->popup(QCursor::pos());
};
state->filter.value(
) | rpl::start_with_next([=](const ResaleGiftsFilter &fields) {
auto x = st::giftBoxResaleTabsMargin.left();
auto y = st::giftBoxResaleTabsMargin.top();
setSelected(-1);
state->buttons.resize(kFiltersCount);
const auto &list = fields.attributes;
const auto setForIndex = [&](int i, auto many, auto one) {
const auto type = IndexToType(i);
const auto count = ranges::count(
list,
type,
&GiftAttributeId::type);
state->buttons[i].text = ResaleTabText((count > 0)
? many(tr::now, lt_count, count)
: one(tr::now));
};
state->buttons[0].text = SortModeText(fields.sort);
setForIndex(
1,
tr::lng_gift_resale_models,
tr::lng_gift_resale_model);
setForIndex(
2,
tr::lng_gift_resale_backdrops,
tr::lng_gift_resale_backdrop);
setForIndex(
3,
tr::lng_gift_resale_symbols,
tr::lng_gift_resale_symbol);
const auto padding = st::giftBoxTabPadding;
for (auto &button : state->buttons) {
const auto width = button.text.maxWidth();
const auto height = st::giftBoxTabStyle.font->height;
const auto r = QRect(0, 0, width, height).marginsAdded(padding);
button.geometry = QRect(QPoint(x, y), r.size());
x += r.width() + st::giftBoxResaleTabSkip;
}
state->fullWidth = x
- st::giftBoxTabSkip
+ st::giftBoxTabsMargin.right();
const auto height = state->buttons.empty()
? 0
: (y
+ state->buttons.back().geometry.height()
+ st::giftBoxTabsMargin.bottom());
raw->resize(raw->width(), height);
raw->update();
}, raw->lifetime());
rpl::combine(
raw->widthValue(),
state->fullWidth.value()
) | rpl::start_with_next([=](int outer, int inner) {
state->scrollMax = std::max(0, inner - outer);
}, raw->lifetime());
raw->setMouseTracking(true);
raw->events() | rpl::start_with_next([=](not_null<QEvent*> e) {
const auto type = e->type();
switch (type) {
case QEvent::Leave: setSelected(-1); break;
case QEvent::MouseMove: {
const auto me = static_cast<QMouseEvent*>(e.get());
const auto mousex = me->pos().x();
const auto drag = QApplication::startDragDistance();
if (state->dragx > 0) {
state->scroll = std::clamp(
state->dragscroll + state->dragx - mousex,
0.,
state->scrollMax * 1.);
raw->update();
break;
} else if (state->pressx > 0
&& std::abs(state->pressx - mousex) > drag) {
state->dragx = state->pressx;
state->dragscroll = state->scroll;
}
const auto position = me->pos() + scroll();
for (auto i = 0, c = int(state->buttons.size()); i != c; ++i) {
if (state->buttons[i].geometry.contains(position)) {
setSelected(i);
break;
}
}
} break;
case QEvent::Wheel: {
const auto me = static_cast<QWheelEvent*>(e.get());
state->scroll = std::clamp(
state->scroll - ScrollDeltaF(me).x(),
0.,
state->scrollMax * 1.);
raw->update();
} break;
case QEvent::MouseButtonPress: {
const auto me = static_cast<QMouseEvent*>(e.get());
if (me->button() != Qt::LeftButton) {
break;
}
state->pressed = state->selected;
state->pressx = me->pos().x();
} break;
case QEvent::MouseButtonRelease: {
const auto me = static_cast<QMouseEvent*>(e.get());
if (me->button() != Qt::LeftButton) {
break;
}
const auto dragx = std::exchange(state->dragx, 0);
const auto pressed = std::exchange(state->pressed, -1);
state->pressx = 0;
if (!dragx && pressed >= 0 && state->selected == pressed) {
showMenu(pressed);
}
} break;
}
}, raw->lifetime());
raw->paintRequest() | rpl::start_with_next([=] {
auto p = QPainter(raw);
auto hq = PainterHighQualityEnabler(p);
const auto padding = st::giftBoxTabPadding;
const auto shift = -scroll();
for (const auto &button : state->buttons) {
const auto geometry = button.geometry.translated(shift);
p.setBrush(st::giftBoxTabBgActive);
p.setPen(Qt::NoPen);
const auto radius = geometry.height() / 2.;
p.drawRoundedRect(geometry, radius, radius);
p.setPen(st::giftBoxTabFgActive);
button.text.draw(p, {
.position = geometry.marginsRemoved(padding).topLeft(),
.availableWidth = button.text.maxWidth(),
});
}
{
const auto &icon = st::defaultEmojiSuggestions;
const auto w = icon.fadeRight.width();
const auto &c = st::boxDividerBg->c;
const auto r = QRect(0, 0, w, raw->height());
const auto s = std::abs(float64(shift.x()));
constexpr auto kF = 0.5;
const auto opacityRight = (state->scrollMax - s)
/ (icon.fadeRight.width() * kF);
p.setOpacity(std::clamp(std::abs(opacityRight), 0., 1.));
icon.fadeRight.fill(p, r.translated(raw->width() - w, 0), c);
const auto opacityLeft = s / (icon.fadeLeft.width() * kF);
p.setOpacity(std::clamp(std::abs(opacityLeft), 0., 1.));
icon.fadeLeft.fill(p, r, c);
}
}, raw->lifetime());
return {
.filter = state->filter.value(),
.widget = std::move(widget),
};
}
void GiftResaleBox(
not_null<GenericBox*> box,
not_null<Window::SessionController*> window,
not_null<PeerData*> peer,
ResaleGiftsDescriptor descriptor) {
box->setWidth(st::boxWideWidth);
// Create a proper vertical layout for the title
const auto titleWrap = box->setPinnedToTopContent(
object_ptr<Ui::VerticalLayout>(box.get()));
// Add vertical spacing above the title
titleWrap->add(object_ptr<Ui::FixedHeightWidget>(
titleWrap,
st::defaultVerticalListSkip));
// Add the gift name with semibold style
titleWrap->add(
object_ptr<Ui::FlatLabel>(
titleWrap,
rpl::single(descriptor.title),
st::boxTitle),
QMargins(st::boxRowPadding.left(), 0, st::boxRowPadding.right(), 0));
// Add the count text in gray below with proper translation
const auto countLabel = titleWrap->add(
object_ptr<Ui::FlatLabel>(
titleWrap,
tr::lng_gift_resale_count(tr::now, lt_count, descriptor.count),
st::defaultFlatLabel),
QMargins(
st::boxRowPadding.left(),
0,
st::boxRowPadding.right(),
st::defaultVerticalListSkip));
countLabel->setTextColorOverride(st::windowSubTextFg->c);
const auto content = box->verticalLayout();
content->paintRequest() | rpl::start_with_next([=](QRect clip) {
QPainter(content).fillRect(clip, st::boxDividerBg);
}, content->lifetime());
struct State {
rpl::event_stream<> updated;
ResaleGiftsDescriptor data;
rpl::variable<ResaleGiftsFilter> filter;
rpl::variable<bool> ton;
rpl::lifetime loading;
int lastMinHeight = 0;
};
const auto state = content->lifetime().make_state<State>();
state->data = std::move(descriptor);
box->addButton(tr::lng_create_group_back(), [=] { box->closeBox(); });
#ifndef OS_MAC_STORE
const auto currency = box->addLeftButton(rpl::single(QString()), [=] {
state->ton = !state->ton.current();
state->updated.fire({});
});
currency->setText(rpl::conditional(
state->ton.value(),
tr::lng_gift_resale_switch_to_stars(),
tr::lng_gift_resale_switch_to_ton()));
#endif
box->heightValue() | rpl::start_with_next([=](int height) {
if (height > state->lastMinHeight) {
state->lastMinHeight = height;
box->setMinHeight(height);
}
}, content->lifetime());
auto tabs = MakeResaleTabs(
window->uiShow(),
peer,
state->data,
state->filter.value());
state->filter = std::move(tabs.filter);
content->add(std::move(tabs.widget));
state->filter.changes() | rpl::start_with_next([=](ResaleGiftsFilter value) {
state->data.offset = QString();
state->loading = ResaleGiftsSlice(
&peer->session(),
state->data.giftId,
value,
QString()
) | rpl::start_with_next([=](ResaleGiftsDescriptor &&slice) {
state->loading.destroy();
state->data.offset = slice.list.empty()
? QString()
: slice.offset;
state->data.list = std::move(slice.list);
state->updated.fire({});
});
}, content->lifetime());
peer->owner().giftUpdates(
) | rpl::start_with_next([=](const Data::GiftUpdate &update) {
using Action = Data::GiftUpdate::Action;
const auto action = update.action;
if (action != Action::Transfer && action != Action::ResaleChange) {
return;
}
const auto i = ranges::find(
state->data.list,
update.slug,
[](const Data::StarGift &gift) {
return gift.unique ? gift.unique->slug : QString();
});
if (i == end(state->data.list)) {
return;
} else if (action == Action::Transfer
|| !i->unique->starsForResale) {
state->data.list.erase(i);
}
state->updated.fire({});
}, box->lifetime());
content->add(MakeGiftsSendList(window, peer, rpl::single(
rpl::empty
) | rpl::then(
state->updated.events()
) | rpl::map([=] {
auto result = GiftsDescriptor();
const auto selfId = window->session().userPeerId();
const auto forceTon = state->ton.current();
for (const auto &gift : state->data.list) {
result.list.push_back(Info::PeerGifts::GiftTypeStars{
.info = gift,
.forceTon = forceTon,
.resale = true,
.mine = (gift.unique->ownerId == selfId),
});
}
return result;
}), [=] {
if (!state->data.offset.isEmpty()
&& !state->loading) {
state->loading = ResaleGiftsSlice(
&peer->session(),
state->data.giftId,
state->filter.current(),
state->data.offset
) | rpl::start_with_next([=](ResaleGiftsDescriptor &&slice) {
state->loading.destroy();
state->data.offset = slice.list.empty()
? QString()
: slice.offset;
state->data.list.insert(
end(state->data.list),
std::make_move_iterator(begin(slice.list)),
std::make_move_iterator(end(slice.list)));
state->updated.fire({});
});
}
}));
}
} // namespace
void ShowResaleGiftBoughtToast(
std::shared_ptr<Main::SessionShow> show,
not_null<PeerData*> to,
const Data::UniqueGift &gift) {
show->showToast({
.title = tr::lng_gift_sent_title(tr::now),
.text = TextWithEntities{ (to->isSelf()
? tr::lng_gift_sent_resale_done_self(
tr::now,
lt_gift,
Data::UniqueGiftName(gift))
: tr::lng_gift_sent_resale_done(
tr::now,
lt_user,
to->shortName())),
},
.duration = kResaleBoughtToastDuration,
});
}
rpl::lifetime ShowStarGiftResale(
not_null<Window::SessionController*> controller,
not_null<PeerData*> peer,
uint64 giftId,
QString title,
Fn<void()> finishRequesting) {
const auto weak = base::make_weak(controller);
const auto session = &controller->session();
return Data::ResaleGiftsSlice(
session,
giftId
) | rpl::start_with_next([=](ResaleGiftsDescriptor &&info) {
if (const auto onstack = finishRequesting) {
onstack();
}
if (!info.giftId || !info.count) {
return;
}
info.title = title;
if (const auto strong = weak.get()) {
strong->show(Box(GiftResaleBox, strong, peer, std::move(info)));
}
});
}
} // namespace Ui

View File

@@ -0,0 +1,41 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
namespace Data {
struct UniqueGift;
} // namespace Data
namespace Info::PeerGifts {
struct GiftTypeStars;
} // namespace Info::PeerGifts
namespace Main {
class Session;
class SessionShow;
} // namespace Main
namespace Window {
class SessionController;
} // namespace Window
namespace Ui {
void ShowResaleGiftBoughtToast(
std::shared_ptr<Main::SessionShow> show,
not_null<PeerData*> to,
const Data::UniqueGift &gift);
[[nodiscard]] rpl::lifetime ShowStarGiftResale(
not_null<Window::SessionController*> controller,
not_null<PeerData*> peer,
uint64 giftId,
QString title,
Fn<void()> finishRequesting);
} // namespace Ui

View File

@@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/peer_list_box.h"
#include "boxes/peer_list_controllers.h"
#include "boxes/star_gift_box.h"
#include "boxes/star_gift_resale_box.h"
#include "data/data_cloud_themes.h"
#include "data/data_session.h"
#include "data/data_star_gift.h"

View File

@@ -743,22 +743,6 @@ groupCallJoinAsList: PeerList(groupCallInviteMembersList) {
statusPosition: point(73px, 28px);
}
}
peerListJoinAsList: PeerList(peerListBox) {
item: PeerListItem(peerListBoxItem) {
height: 56px;
checkbox: RoundImageCheckbox(defaultPeerListCheckbox) {
check: RoundCheckbox(defaultRoundCheckbox) {
size: 0px;
}
imageRadius: 19px;
imageSmallRadius: 15px;
}
photoSize: 38px;
photoPosition: point(24px, 9px);
namePosition: point(73px, 9px);
statusPosition: point(73px, 28px);
}
}
groupCallMultiSelect: MultiSelect(defaultMultiSelect) {
bg: groupCallMembersBg;
item: MultiSelectItem(defaultMultiSelectItem) {
@@ -1609,6 +1593,7 @@ confcallLinkButton: RoundButton(defaultActiveButton) {
confcallLinkBoxInitial: Box(defaultBox) {
buttonPadding: margins(12px, 11px, 24px, 96px);
buttonHeight: 42px;
buttonWide: true;
button: confcallLinkButton;
shadowIgnoreTopSkip: true;
}
@@ -1712,15 +1697,35 @@ groupCallInviteLinkIcon: icon {{ "info/edit/group_manage_links", mediaviewTextLi
groupCallMessagesScroll: ScrollArea(defaultScrollArea) {
barHidden: true;
}
groupCallMessagePadding: margins(8px, 2px, 8px, 2px);
groupCallMessagePadding: margins(8px, 3px, 8px, 2px);
groupCallMessageSkip: 8px;
groupCallMessagePalette: TextPalette(defaultTextPalette) {
linkFg: radialFg;
monoFg: radialFg;
spoilerFg: radialFg;
}
groupCallMessageBadge: RoundButton(defaultActiveButton) {
textBg: attentionButtonFg;
textBgOver: attentionButtonFg;
width: -6px;
height: 11px;
radius: 5px;
textTop: 0px;
style: TextStyle(semiboldTextStyle) {
font: font(8px semibold);
}
}
groupCallMessageBadgeMargin: margins(0px, 4px, 0px, 0px);
groupCallUserpic: 20px;
groupCallUserpicPadding: margins(2px, 2px, 4px, 2px);
groupCallPinnedPadding: margins(10px, 4px, 10px, 2px);
groupCallPinnedMaxWidth: 96px;
groupCallPinnedUserpic: 22px;
groupCallTopReactorBadge: RoundCheckbox(defaultRoundCheckbox) {
width: 1px;
border: groupCallMembersBg;
}
confcallLinkMenu: IconButton(boxTitleClose) {
icon: icon {{ "title_menu_dots", boxTitleCloseFg }};

View File

@@ -295,7 +295,7 @@ void Instance::startedConferenceReady(
migrationInfo);
_currentGroupCall = std::move(_startingGroupCall);
_currentGroupCallChanges.fire_copy(call);
const auto real = call->conferenceCall().get();
const auto real = call->sharedCall().get();
const auto link = real->conferenceInviteLink();
const auto slug = Group::ExtractConferenceSlug(link);
finishConferenceInvitations(args);
@@ -600,6 +600,10 @@ void Instance::handleUpdate(
handleGroupCallUpdate(session, update);
}, [&](const MTPDupdateGroupCallEncryptedMessage &data) {
handleGroupCallUpdate(session, update);
}, [&](const MTPDupdateDeleteGroupCallMessages &data) {
handleGroupCallUpdate(session, update);
}, [&](const MTPDupdateMessageID &data) {
handleGroupCallUpdate(session, update);
}, [](const auto &) {
Unexpected("Update type in Calls::Instance::handleUpdate.");
});
@@ -637,6 +641,10 @@ FnMut<void()> Instance::addAsyncWaiter() {
};
}
void Instance::registerVideoStream(not_null<GroupCall*> call) {
_streams[&call->peer()->session()].push_back(call);
}
bool Instance::isSharingScreen() const {
return (_currentCall && _currentCall->isSharingScreen())
|| (_currentGroupCall && _currentGroupCall->isSharingScreen());
@@ -707,6 +715,35 @@ void Instance::handleCallUpdate(
void Instance::handleGroupCallUpdate(
not_null<Main::Session*> session,
const MTPUpdate &update) {
if (const auto i = _streams.find(session); i != end(_streams)) {
for (auto j = begin(i->second); j != end(i->second);) {
if (const auto strong = j->get()) {
update.match([&](const MTPDupdateGroupCall &data) {
strong->handlePossibleCreateOrJoinResponse(data);
strong->handleUpdate(update);
}, [&](const MTPDupdateGroupCallConnection &data) {
strong->handlePossibleCreateOrJoinResponse(data);
}, [&](const MTPDupdateGroupCallMessage &data) {
strong->handleIncomingMessage(data);
}, [&](const MTPDupdateGroupCallEncryptedMessage &data) {
strong->handleIncomingMessage(data);
}, [&](const MTPDupdateDeleteGroupCallMessages &data) {
strong->handleDeleteMessages(data);
}, [&](const MTPDupdateMessageID &data) {
strong->handleMessageSent(data);
}, [&](const MTPDupdateGroupCallParticipants &data) {
strong->handleUpdate(update);
}, [&](const MTPDupdateGroupCallChainBlocks &data) {
strong->handleUpdate(update);
}, [](const auto &) {
});
++j;
} else {
j = i->second.erase(j);
}
}
}
const auto groupCall = _currentGroupCall
? _currentGroupCall.get()
: _startingGroupCall.get();
@@ -719,13 +756,19 @@ void Instance::handleGroupCallUpdate(
groupCall->handleIncomingMessage(data);
}, [&](const MTPDupdateGroupCallEncryptedMessage &data) {
groupCall->handleIncomingMessage(data);
}, [&](const MTPDupdateDeleteGroupCallMessages &data) {
groupCall->handleDeleteMessages(data);
}, [&](const MTPDupdateMessageID &data) {
groupCall->handleMessageSent(data);
}, [](const auto &) {
});
}
if (update.type() == mtpc_updateGroupCallConnection
|| update.type() == mtpc_updateGroupCallMessage
|| update.type() == mtpc_updateGroupCallEncryptedMessage) {
|| update.type() == mtpc_updateGroupCallEncryptedMessage
|| update.type() == mtpc_updateDeleteGroupCallMessages
|| update.type() == mtpc_updateMessageID) {
return;
}
const auto callId = update.match([](const MTPDupdateGroupCall &data) {
@@ -1111,7 +1154,7 @@ void Instance::showConferenceInvite(
return;
} else if (inGroupCall()
&& _currentGroupCall->conference()
&& _currentGroupCall->conferenceCall()->id() == conferenceId) {
&& _currentGroupCall->sharedCall()->id() == conferenceId) {
return;
}

View File

@@ -147,6 +147,8 @@ public:
[[nodiscard]] FnMut<void()> addAsyncWaiter();
void registerVideoStream(not_null<GroupCall*> call);
[[nodiscard]] bool isSharingScreen() const;
[[nodiscard]] bool isQuitPrevent();
@@ -217,6 +219,10 @@ private:
base::flat_set<std::unique_ptr<crl::semaphore>> _asyncWaiters;
base::flat_map<
not_null<Main::Session*>,
std::vector<base::weak_ptr<GroupCall>>> _streams;
};
} // namespace Calls

View File

@@ -584,7 +584,7 @@ void TopBar::subscribeToMembersChanges(not_null<GroupCall*> call) {
const auto group = _groupCall.get();
const auto conference = group && group->conference();
auto realValue = conference
? (rpl::single(group->conferenceCall().get()) | rpl::type_erased())
? (rpl::single(group->sharedCall().get()) | rpl::type_erased())
: peer->session().changes().peerFlagsValue(
peer,
Data::PeerUpdate::Flag::GroupCall

View File

@@ -28,6 +28,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/style_boxes.h"
#include "styles/style_layers.h"
#include "styles/style_calls.h"
#include "styles/style_chat_helpers.h"
namespace Calls::Group {
namespace {
@@ -224,7 +225,7 @@ void ChooseJoinAsBox(
&st::groupCallMultiSelect);
} else {
controller->setStyleOverrides(
&st::peerListJoinAsList,
&st::defaultChooseSendAs.list,
nullptr);
}
const auto content = box->addRow(

View File

@@ -582,7 +582,7 @@ GroupCall::GroupCall(
StartConferenceInfo info)
: GroupCall(delegate, Group::JoinInfo{
.peer = info.call ? info.call->peer() : info.show->session().user(),
.joinAs = info.call ? info.call->peer() : info.show->session().user(),
.joinAs = info.show ? info.show->session().user() : info.call->peer(),
}, info, info.call
? info.call->input()
: MTP_inputGroupCall(MTP_long(0), MTP_long(0))) {
@@ -591,10 +591,10 @@ GroupCall::GroupCall(
GroupCall::GroupCall(
not_null<Delegate*> delegate,
Group::JoinInfo join,
StartConferenceInfo conference,
StartConferenceInfo startInfo,
const MTPInputGroupCall &inputCall)
: _delegate(delegate)
, _conferenceCall(std::move(conference.call))
, _sharedCall(std::move(startInfo.call))
, _peer(join.peer)
, _history(_peer->owner().history(_peer))
, _api(&_peer->session().mtp())
@@ -602,8 +602,8 @@ GroupCall::GroupCall(
, _joinAs(join.joinAs)
, _possibleJoinAs(std::move(join.possibleJoinAs))
, _joinHash(join.joinHash)
, _conferenceLinkSlug(conference.linkSlug)
, _conferenceJoinMessageId(conference.joinMessageId)
, _conferenceLinkSlug(startInfo.linkSlug)
, _conferenceJoinMessageId(startInfo.joinMessageId)
, _rtmpUrl(join.rtmpInfo.url)
, _rtmpKey(join.rtmpInfo.key)
, _canManage(Data::CanManageGroupCallValue(_peer))
@@ -630,7 +630,7 @@ GroupCall::GroupCall(
, _connectingSoundTimer([=] { playConnectingSoundOnce(); })
, _listenersHidden(join.rtmp)
, _rtmp(join.rtmp)
, _rtmpVolume(Group::kDefaultVolume) {
, _singleSourceVolume(Group::kDefaultVolume) {
applyInputCall(inputCall);
_muted.value(
@@ -664,7 +664,7 @@ GroupCall::GroupCall(
if (!canManage() && real->joinMuted()) {
_muted = MuteState::ForceMuted;
}
} else if (!conference.migrating && !conference.show) {
} else if (!startInfo.migrating && !startInfo.show) {
_peer->session().changes().peerFlagsValue(
_peer,
Data::PeerUpdate::Flag::GroupCall
@@ -684,21 +684,21 @@ GroupCall::GroupCall(
setupMediaDevices();
setupOutgoingVideo();
if (_conferenceCall) {
if (_sharedCall && conference()) {
setupConferenceCall();
initConferenceE2E();
} else if (conference.migrating || conference.show) {
} else if (!_sharedCall && (startInfo.migrating || startInfo.show)) {
initConferenceE2E();
}
if (conference.migrating || (conference.show && !_conferenceCall)) {
if (!conference.muted) {
if (startInfo.migrating || (startInfo.show && !_sharedCall)) {
if (!startInfo.muted) {
setMuted(MuteState::Active);
}
_startConferenceInfo = std::make_shared<StartConferenceInfo>(
std::move(conference));
std::move(startInfo));
}
if (_id || (!_conferenceCall && _startConferenceInfo)) {
if (_id || (!_sharedCall && _startConferenceInfo)) {
initialJoin();
} else {
start(join.scheduleDate, join.rtmp);
@@ -750,6 +750,7 @@ GroupCall::~GroupCall() {
if (!_rtmp) {
Core::App().mediaDevices().setCaptureMuteTracker(this, false);
}
_messages->undoScheduledPaidOnDestroy();
}
void GroupCall::initConferenceE2E() {
@@ -788,16 +789,16 @@ void GroupCall::initConferenceE2E() {
}
void GroupCall::setupConferenceCall() {
Expects(_conferenceCall != nullptr);
Expects(_sharedCall != nullptr);
_conferenceCall->staleParticipantIds(
_sharedCall->staleParticipantIds(
) | rpl::start_with_next([=](const base::flat_set<UserId> &staleIds) {
removeConferenceParticipants(staleIds, true);
}, _lifetime);
}
void GroupCall::trackParticipantsWithAccess() {
if (!_conferenceCall || !_e2e) {
if (!_sharedCall || !_e2e) {
return;
}
@@ -808,7 +809,7 @@ void GroupCall::trackParticipantsWithAccess() {
for (const auto &id : set.list) {
users.emplace(UserId(id.v));
}
_conferenceCall->setParticipantsWithAccess(std::move(users));
_sharedCall->setParticipantsWithAccess(std::move(users));
}, _e2e->lifetime());
}
@@ -959,7 +960,7 @@ void GroupCall::setScheduledDate(TimeId date) {
}
void GroupCall::setMessagesEnabled(bool enabled) {
_messagesEnabled = enabled && !_rtmp;
_messagesEnabled = enabled && (!_rtmp || videoStream());
}
void GroupCall::subscribeToReal(not_null<Data::GroupCall*> real) {
@@ -1198,8 +1199,18 @@ void GroupCall::playConnectingSoundOnce() {
_delegate->groupCallPlaySound(Delegate::GroupCallSound::Connecting);
}
not_null<PeerData*> GroupCall::messagesFrom() const {
if (!videoStream()) {
return joinAs();
} else if (const auto real = lookupReal()) {
return real->resolveSendAs();
}
return _peer->session().user();
}
bool GroupCall::showChooseJoinAs() const {
return !_rtmp
&& !videoStream()
&& ((_possibleJoinAs.size() > 1)
|| (_possibleJoinAs.size() == 1
&& !_possibleJoinAs.front()->isSelf()));
@@ -1217,7 +1228,28 @@ bool GroupCall::rtmp() const {
}
bool GroupCall::conference() const {
return _conferenceCall || _startConferenceInfo;
if (const auto raw = _sharedCall.get()) {
return (raw->origin() == Data::GroupCallOrigin::Conference);
} else if (_startConferenceInfo.get()) {
return true;
}
return false;
}
bool GroupCall::videoStream() const {
if (const auto raw = _sharedCall.get()) {
return (raw->origin() == Data::GroupCallOrigin::VideoStream);
}
return false;
}
Data::GroupCallOrigin GroupCall::origin() const {
if (const auto raw = _sharedCall.get()) {
return raw->origin();
} else if (_startConferenceInfo.get()) {
return Data::GroupCallOrigin::Conference;
}
return Data::GroupCallOrigin::Group;
}
bool GroupCall::listenersHidden() const {
@@ -1233,7 +1265,7 @@ rpl::producer<bool> GroupCall::emptyRtmpValue() const {
}
int GroupCall::rtmpVolume() const {
return _rtmpVolume;
return _singleSourceVolume;
}
Calls::Group::RtmpInfo GroupCall::rtmpInfo() const {
@@ -1246,15 +1278,15 @@ void GroupCall::setRtmpInfo(const Calls::Group::RtmpInfo &value) {
}
Data::GroupCall *GroupCall::lookupReal() const {
if (const auto conference = _conferenceCall.get()) {
return conference;
if (const auto shared = _sharedCall.get()) {
return shared;
}
const auto real = _peer->groupCall();
return (real && real->id() == _id) ? real : nullptr;
}
std::shared_ptr<Data::GroupCall> GroupCall::conferenceCall() const {
return _conferenceCall;
std::shared_ptr<Data::GroupCall> GroupCall::sharedCall() const {
return _sharedCall;
}
rpl::producer<not_null<Data::GroupCall*>> GroupCall::real() const {
@@ -1324,13 +1356,16 @@ void GroupCall::initialJoinRequested() {
update.was->ssrc,
GetAdditionalAudioSsrc(update.was->videoParams),
});
} else if (videoStream()) {
const auto value = singleSourceVolumeValue();
_instance->setVolume(update.now->ssrc, value);
} else if (!_rtmp) {
updateInstanceVolume(update.was, *update.now);
}
}, _lifetime);
if (_conferenceCall) {
_canManage = _conferenceCall->canManage();
if (_sharedCall && conference()) {
_canManage = _sharedCall->canManage();
return;
}
_peer->session().updates().addActiveChat(
@@ -1519,7 +1554,7 @@ void GroupCall::startRejoin() {
for (const auto &[task, part] : _broadcastParts) {
_api.request(part.requestId).cancel();
}
if (_conferenceCall || _startConferenceInfo) {
if (conference()) {
initConferenceE2E();
}
setState(State::Joining);
@@ -1589,10 +1624,11 @@ void GroupCall::rejoin(not_null<PeerData*> as) {
};
LOG(("Call Info: Join payload received, joining with ssrc: %1."
).arg(_joinState.payload.ssrc));
if (!_conferenceCall && _startConferenceInfo) {
if (!_sharedCall && _startConferenceInfo) {
startConference();
} else if (_conferenceCall
&& !_conferenceCall->blockchainMayBeEmpty()
} else if (conference()
&& _sharedCall
&& !_sharedCall->blockchainMayBeEmpty()
&& !_e2e->hasLastBlock0()) {
refreshLastBlockAndJoin();
} else {
@@ -1715,13 +1751,13 @@ void GroupCall::startConference() {
const MTPUpdates &result,
const MTP::Response &response) {
_createRequestId = 0;
_conferenceCall = _peer->owner().sharedConferenceCallFind(result);
if (!_conferenceCall) {
_sharedCall = _peer->owner().sharedConferenceCallFind(result);
if (!_sharedCall) {
joinFail(u"Call not found!"_q);
return;
}
applyInputCall(_conferenceCall->input());
_realChanges.fire_copy(_conferenceCall.get());
applyInputCall(_sharedCall->input());
_realChanges.fire_copy(_sharedCall.get());
initialJoinRequested();
joinDone(
@@ -1764,16 +1800,18 @@ void GroupCall::joinDone(
_api.request(base::take(state.requestId)).cancel();
state.inShortPoll = true;
}
_messages->setApplyingInitial(true);
_peer->session().api().applyUpdates(result);
_messages->setApplyingInitial(false);
for (auto &state : _subchains) {
state.inShortPoll = false;
}
if (justCreated) {
subscribeToReal(_conferenceCall.get());
subscribeToReal(_sharedCall.get());
setupConferenceCall();
_conferenceLinkSlug = Group::ExtractConferenceSlug(
_conferenceCall->conferenceInviteLink());
_sharedCall->conferenceInviteLink());
Core::App().calls().startedConferenceReady(
this,
*_startConferenceInfo);
@@ -2068,7 +2106,8 @@ void GroupCall::applyMeInCallLocally() {
MTPstring(), // Don't update about text in local updates.
MTP_long(raisedHandRating),
MTPGroupCallParticipantVideo(),
MTPGroupCallParticipantVideo())),
MTPGroupCallParticipantVideo(),
MTPlong())),
MTP_int(0)).c_updateGroupCallParticipants());
}
@@ -2115,7 +2154,8 @@ void GroupCall::applyParticipantLocally(
MTPstring(), // Don't update about text in local updates.
MTP_long(participant->raisedHandRating),
MTPGroupCallParticipantVideo(),
MTPGroupCallParticipantVideo())),
MTPGroupCallParticipantVideo(),
MTPlong())),
MTP_int(0)).c_updateGroupCallParticipants());
}
@@ -2383,6 +2423,8 @@ void GroupCall::handlePossibleCreateOrJoinResponse(
setCameraEndpoint(endpoint ? endpoint->id : std::string());
_instance->setJoinResponsePayload(json.toStdString());
}
updateInstanceVolumes();
fillActiveVideoEndpoints();
updateRequestedVideoChannels();
checkMediaChannelDescriptions();
});
@@ -2415,6 +2457,26 @@ void GroupCall::handleIncomingMessage(
_messages->received(data);
}
void GroupCall::handleDeleteMessages(
const MTPDupdateDeleteGroupCallMessages &data) {
const auto id = data.vcall().match([&](const MTPDinputGroupCall &data) {
return data.vid().v;
}, [](const auto &) -> CallId {
Unexpected("slug/msg in GroupCall::handleIncomingMessage");
});
if (id != _id || conference()) {
return;
}
_messages->deleted(data);
}
void GroupCall::handleMessageSent(const MTPDupdateMessageID &data) {
if (conference()) {
return;
}
_messages->sent(data);
}
void GroupCall::handlePossibleDiscarded(const MTPDgroupCallDiscarded &data) {
if (data.vid().v == _id) {
LOG(("Call Info: Hangup after groupCallDiscarded."));
@@ -3504,6 +3566,10 @@ void GroupCall::updateInstanceMuteState() {
&& state != MuteState::PushToTalk);
}
float64 GroupCall::singleSourceVolumeValue() const {
return _singleSourceVolume / float64(Group::kDefaultVolume);
}
void GroupCall::updateInstanceVolumes() {
const auto real = lookupReal();
if (!real) {
@@ -3511,8 +3577,12 @@ void GroupCall::updateInstanceVolumes() {
}
if (_rtmp) {
const auto value = _rtmpVolume / float64(Group::kDefaultVolume);
_instance->setVolume(1, value);
_instance->setVolume(1, singleSourceVolumeValue());
} else if (videoStream()) {
const auto value = singleSourceVolumeValue();
for (const auto &participant : real->participants()) {
_instance->setVolume(participant.ssrc, value);
}
} else {
const auto &participants = real->participants();
for (const auto &participant : participants) {
@@ -3905,8 +3975,8 @@ void GroupCall::requestVideoQuality(
}
void GroupCall::toggleMute(const Group::MuteRequest &data) {
if (_rtmp) {
_rtmpVolume = data.mute ? 0 : Group::kDefaultVolume;
if (_rtmp || videoStream()) {
_singleSourceVolume = data.mute ? 0 : Group::kDefaultVolume;
updateInstanceVolumes();
} else if (data.locallyOnly) {
applyParticipantLocally(data.peer, data.mute, std::nullopt);
@@ -3916,8 +3986,8 @@ void GroupCall::toggleMute(const Group::MuteRequest &data) {
}
void GroupCall::changeVolume(const Group::VolumeRequest &data) {
if (_rtmp) {
_rtmpVolume = data.volume;
if (_rtmp || videoStream()) {
_singleSourceVolume = data.volume;
updateInstanceVolumes();
} else if (data.locallyOnly) {
applyParticipantLocally(data.peer, false, data.volume);
@@ -3971,7 +4041,7 @@ void GroupCall::inviteToConference(
inputCall(),
user->inputUser
)).done([=](const MTPUpdates &result) {
const auto call = _conferenceCall.get();
const auto call = _sharedCall.get();
user->owner().registerInvitedToCallUser(_id, call, user, true);
_peer->session().api().applyUpdates(result);
resultAddress()->invited.push_back(user);
@@ -4023,7 +4093,7 @@ void GroupCall::inviteUsers(
}
};
if (_conferenceCall.get()) {
if (_sharedCall.get()) {
for (const auto &request : requests) {
inviteToConference(request, [=] {
return &state->result;
@@ -4144,7 +4214,7 @@ std::function<std::vector<uint8_t>(
}
void GroupCall::sendMessage(TextWithTags message) {
_messages->send(std::move(message));
_messages->send(std::move(message), 0);
}
auto GroupCall::otherParticipantStateValue() const

View File

@@ -41,6 +41,7 @@ namespace Data {
struct LastSpokeTimes;
struct GroupCallParticipant;
class GroupCall;
enum class GroupCallOrigin : uchar;
} // namespace Data
namespace TdE2E {
@@ -48,9 +49,7 @@ class Call;
class EncryptDecrypt;
} // namespace TdE2E
namespace Calls {
namespace Group {
namespace Calls::Group {
struct MuteRequest;
struct VolumeRequest;
struct ParticipantState;
@@ -61,7 +60,9 @@ struct RtmpInfo;
enum class VideoQuality;
enum class Error;
class Messages;
} // namespace Group
} // namespace Calls::Group
namespace Calls {
struct InviteRequest;
struct InviteResult;
@@ -248,6 +249,7 @@ public:
[[nodiscard]] not_null<Group::Messages*> messages() const {
return _messages.get();
}
[[nodiscard]] not_null<PeerData*> messagesFrom() const;
[[nodiscard]] bool showChooseJoinAs() const;
[[nodiscard]] TimeId scheduleDate() const {
return _scheduleDate;
@@ -255,6 +257,8 @@ public:
[[nodiscard]] bool scheduleStartSubscribed() const;
[[nodiscard]] bool rtmp() const;
[[nodiscard]] bool conference() const;
[[nodiscard]] bool videoStream() const;
[[nodiscard]] Data::GroupCallOrigin origin() const;
[[nodiscard]] bool listenersHidden() const;
[[nodiscard]] bool emptyRtmp() const;
[[nodiscard]] rpl::producer<bool> emptyRtmpValue() const;
@@ -265,7 +269,7 @@ public:
void setRtmpInfo(const Group::RtmpInfo &value);
[[nodiscard]] Data::GroupCall *lookupReal() const;
[[nodiscard]] std::shared_ptr<Data::GroupCall> conferenceCall() const;
[[nodiscard]] std::shared_ptr<Data::GroupCall> sharedCall() const;
[[nodiscard]] rpl::producer<not_null<Data::GroupCall*>> real() const;
[[nodiscard]] rpl::producer<QByteArray> emojiHashValue() const;
@@ -285,6 +289,8 @@ public:
void handleIncomingMessage(const MTPDupdateGroupCallMessage &data);
void handleIncomingMessage(
const MTPDupdateGroupCallEncryptedMessage &data);
void handleDeleteMessages(const MTPDupdateDeleteGroupCallMessages &data);
void handleMessageSent(const MTPDupdateMessageID &data);
void changeTitle(const QString &title);
void toggleRecording(
bool enabled,
@@ -666,12 +672,13 @@ private:
Fn<not_null<InviteResult*>()> resultAddress,
Fn<void()> finishRequest);
[[nodiscard]] float64 singleSourceVolumeValue() const;
[[nodiscard]] int activeVideoSendersCount() const;
[[nodiscard]] MTPInputGroupCall inputCallSafe() const;
const not_null<Delegate*> _delegate;
std::shared_ptr<Data::GroupCall> _conferenceCall;
std::shared_ptr<Data::GroupCall> _sharedCall;
std::unique_ptr<TdE2E::Call> _e2e;
std::shared_ptr<TdE2E::EncryptDecrypt> _e2eEncryptDecrypt;
rpl::variable<QByteArray> _emojiHash;
@@ -687,7 +694,7 @@ private:
base::flat_set<uint32> _unresolvedSsrcs;
rpl::event_stream<Error> _errors;
std::vector<Fn<void()>> _rejoinedCallbacks;
std::unique_ptr<Group::Messages> _messages;
const std::unique_ptr<Group::Messages> _messages;
bool _recordingStoppedByMe = false;
bool _requestedVideoChannelsUpdateScheduled = false;
@@ -794,7 +801,7 @@ private:
bool _listenersHidden = false;
bool _rtmp = false;
bool _reloadedStaleCall = false;
int _rtmpVolume = 0;
int _singleSourceVolume = 0;
SubChainState _subchains[kSubChainsCount];

View File

@@ -77,13 +77,16 @@ object_ptr<Ui::GenericBox> ScreenSharingPrivacyRequestBox() {
#endif // Q_OS_MAC
}
object_ptr<Ui::RpWidget> MakeJoinCallLogo(not_null<QWidget*> parent) {
const auto logoSize = st::confcallJoinLogo.size();
const auto logoOuter = logoSize.grownBy(st::confcallJoinLogoPadding);
object_ptr<Ui::RpWidget> MakeRoundActiveLogo(
not_null<QWidget*> parent,
const style::icon &icon,
const style::margins &padding) {
const auto logoSize = icon.size();
const auto logoOuter = logoSize.grownBy(padding);
auto result = object_ptr<Ui::RpWidget>(parent);
const auto logo = result.data();
logo->resize(logo->width(), logoOuter.height());
logo->paintRequest() | rpl::start_with_next([=] {
logo->paintRequest() | rpl::start_with_next([=, &icon] {
if (logo->width() < logoOuter.width()) {
return;
}
@@ -94,11 +97,18 @@ object_ptr<Ui::RpWidget> MakeJoinCallLogo(not_null<QWidget*> parent) {
p.setBrush(st::windowBgActive);
p.setPen(Qt::NoPen);
p.drawEllipse(outer);
st::confcallJoinLogo.paintInCenter(p, outer);
icon.paintInCenter(p, outer);
}, logo->lifetime());
return result;
}
object_ptr<Ui::RpWidget> MakeJoinCallLogo(not_null<QWidget*> parent) {
return MakeRoundActiveLogo(
parent,
st::confcallJoinLogo,
st::confcallJoinLogoPadding);
}
void ConferenceCallJoinConfirm(
not_null<Ui::GenericBox*> box,
std::shared_ptr<Data::GroupCall> call,
@@ -282,7 +292,8 @@ void ShowConferenceCallLinkBox(
MTP_flags(Flag::f_reset_invite_hash),
call->input(),
MTPBool(), // join_muted
MTPBool()) // messages_enabled
MTPBool(), // messages_enabled
MTPlong()) // send_paid_messages_stars
).done([=](const MTPUpdates &result) {
call->session().api().applyUpdates(result);
ShowConferenceCallLinkBox(show, call, args);

View File

@@ -132,6 +132,7 @@ struct JoinInfo {
enum class PanelMode {
Default,
Wide,
VideoStream,
};
enum class VideoQuality {
@@ -161,6 +162,10 @@ using StickedTooltips = base::flags<StickedTooltip>;
[[nodiscard]] object_ptr<Ui::GenericBox> ScreenSharingPrivacyRequestBox();
[[nodiscard]] object_ptr<Ui::RpWidget> MakeRoundActiveLogo(
not_null<QWidget*> parent,
const style::icon &icon,
const style::margins &padding);
[[nodiscard]] object_ptr<Ui::RpWidget> MakeJoinCallLogo(
not_null<QWidget*> parent);

View File

@@ -545,8 +545,8 @@ bool Members::Controller::appendWithAccessUsers() {
}
void Members::Controller::setupWithAccessUsers() {
const auto conference = _call->conferenceCall().get();
if (!conference) {
const auto conference = _call->sharedCall().get();
if (!_call->conference() || !conference) {
return;
}
conference->participantsWithAccessValue(
@@ -1333,7 +1333,7 @@ base::unique_qptr<Ui::PopupMenu> Members::Controller::createRowContextMenu(
}
const auto muted = (muteState == Row::State::Muted)
|| (muteState == Row::State::RaisedHand);
const auto addCover = !_call->rtmp();
const auto addCover = !_call->rtmp() && !_call->videoStream();
const auto addVolumeItem = (!muted || isMe(participantPeer));
const auto admin = IsGroupCallAdmin(_peer, participantPeer);
const auto session = &_peer->session();
@@ -1452,7 +1452,7 @@ base::unique_qptr<Ui::PopupMenu> Members::Controller::createRowContextMenu(
}
}
if (_call->rtmp()) {
if (_call->rtmp() || _call->videoStream()) {
addMuteActionsToContextMenu(
result,
row->peer(),
@@ -1484,8 +1484,9 @@ base::unique_qptr<Ui::PopupMenu> Members::Controller::createRowContextMenu(
} else {
const auto invited = (muteState == Row::State::Invited)
|| (muteState == Row::State::Calling);
const auto conference = _call->conferenceCall().get();
if (conference
const auto conference = _call->sharedCall().get();
if (_call->conference()
&& conference
&& participantPeer->isUser()
&& invited) {
const auto id = conference->id();
@@ -1645,7 +1646,9 @@ void Members::Controller::addMuteActionsToContextMenu(
menu->addAction(std::move(volumeItem));
if (!_call->rtmp() && !isMe(participantPeer)) {
if (!_call->rtmp()
&& !_call->videoStream()
&& !isMe(participantPeer)) {
menu->addSeparator();
}
};
@@ -1655,6 +1658,7 @@ void Members::Controller::addMuteActionsToContextMenu(
|| muteState == Row::State::Calling
|| muteState == Row::State::WithAccess
|| _call->rtmp()
|| _call->videoStream()
|| isMe(participantPeer)
|| (muteState == Row::State::Inactive
&& participantIsCallAdmin

View File

@@ -463,6 +463,12 @@ void MembersRow::paintMuteIcon(
QString MembersRow::generateName() {
const auto result = peer()->name();
if (result.isEmpty()) {
DEBUG_LOG(("UnknownParticipant: %1, Loaded: %2, Name Version: %3"
).arg(peerToUser(peer()->id).bare
).arg(peer()->isLoaded() ? "TRUE" : "FALSE"
).arg(peer()->nameVersion()));
}
return result.isEmpty()
? u"User #%1"_q.arg(peerToUser(peer()->id).bare)
: result;

View File

@@ -8,12 +8,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "calls/group/calls_group_messages.h"
#include "apiwrap.h"
#include "api/api_blocked_peers.h"
#include "api/api_chat_participants.h"
#include "api/api_text_entities.h"
#include "base/random.h"
#include "base/unixtime.h"
#include "calls/group/ui/calls_group_stars_coloring.h"
#include "calls/group/calls_group_call.h"
#include "calls/group/calls_group_message_encryption.h"
#include "data/data_channel.h"
#include "data/data_group_call.h"
#include "data/data_message_reactions.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "data/data_user.h"
@@ -24,12 +29,61 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/ui_utility.h"
namespace Calls::Group {
namespace {
constexpr auto kMaxShownVideoStreamMessages = 100;
constexpr auto kStarsStatsShortPollDelay = 30 * crl::time(1000);
[[nodiscard]] StarsTop ParseStarsTop(
not_null<Data::Session*> owner,
const MTPphone_GroupCallStars &stars) {
const auto &data = stars.data();
const auto &list = data.vtop_donors().v;
auto result = StarsTop{ .total = int(data.vtotal_stars().v) };
result.topDonors.reserve(list.size());
for (const auto &entry : list) {
const auto &fields = entry.data();
result.topDonors.push_back({
.peer = (fields.vpeer_id()
? owner->peer(peerFromMTP(*fields.vpeer_id())).get()
: nullptr),
.stars = int(fields.vstars().v),
.my = fields.is_my(),
});
}
return result;
}
[[nodiscard]] TimeId PinFinishDate(
not_null<PeerData*> peer,
TimeId date,
int stars) {
if (!date || !stars) {
return 0;
}
const auto &colorings = peer->session().appConfig().groupCallColorings();
return date + Ui::StarsColoringForCount(colorings, stars).secondsPin;
}
[[nodiscard]] TimeId PinFinishDate(const Message &message) {
return PinFinishDate(message.peer, message.date, message.stars);
}
[[nodiscard]] std::optional<PeerId> MaybeShownPeer(
uint32 privacySet,
PeerId shownPeer) {
return privacySet ? shownPeer : std::optional<PeerId>();
}
} // namespace
Messages::Messages(not_null<GroupCall*> call, not_null<MTP::Sender*> api)
: _call(call)
, _session(&call->peer()->session())
, _api(api)
, _destroyTimer([=] { checkDestroying(); })
, _ttl(_call->peer()->session().appConfig().groupCallMessageTTL()) {
, _ttl(_session->appConfig().groupCallMessageTTL())
, _starsStatsTimer([=] { requestStarsStats(); }) {
Ui::PostponeCall(_call, [=] {
_call->real(
) | rpl::start_with_next([=](not_null<Data::GroupCall*> call) {
@@ -40,16 +94,57 @@ Messages::Messages(not_null<GroupCall*> call, not_null<MTP::Sender*> api)
Unexpected("Not ready call.");
}
}, _lifetime);
requestStarsStats();
});
}
Messages::~Messages() {
if (_paid.sending > 0) {
finishPaidSending({
.count = int(_paid.sending),
.valid = true,
.shownPeer = MaybeShownPeer(
_paid.sendingPrivacySet,
_paid.sendingShownPeer),
}, false);
}
}
void Messages::requestStarsStats() {
if (!_call->videoStream()) {
return;
}
_starsStatsTimer.cancel();
_starsTopRequestId = _api->request(MTPphone_GetGroupCallStars(
_call->inputCall()
)).done([=](const MTPphone_GroupCallStars &result) {
const auto &data = result.data();
const auto owner = &_session->data();
owner->processUsers(data.vusers());
owner->processChats(data.vchats());
_paid.top = ParseStarsTop(owner, result);
_paidChanges.fire({});
_starsStatsTimer.callOnce(kStarsStatsShortPollDelay);
}).fail([=](const MTP::Error &error) {
[[maybe_unused]] const auto &type = error.type();
_starsStatsTimer.callOnce(kStarsStatsShortPollDelay);
}).send();
}
bool Messages::ready() const {
return _real && (!_call->conference() || _call->e2eEncryptDecrypt());
}
void Messages::send(TextWithTags text) {
if (!ready()) {
_pending.push_back(std::move(text));
void Messages::send(TextWithTags text, int stars) {
if (text.empty() && !stars) {
return;
} else if (!ready()) {
_pending.push_back({ std::move(text), stars });
return;
}
@@ -64,21 +159,38 @@ void Messages::send(TextWithTags text) {
prepared.entities,
Api::ConvertOption::SkipLocal)));
const auto localId = _call->peer()->owner().nextLocalMessageId();
const auto randomId = base::RandomValue<uint64>();
const auto from = _call->joinAs();
_messages.push_back({
.randomId = randomId,
.peer = from,
.text = std::move(prepared),
});
_sendingIdByRandomId.emplace(randomId, localId);
const auto from = _call->messagesFrom();
const auto skip = skipMessage(prepared, stars);
if (skip) {
_skippedIds.emplace(localId);
} else {
_messages.push_back({
.id = localId,
.peer = from,
.text = std::move(prepared),
.stars = stars,
.admin = (from == _call->peer()),
.mine = true,
});
}
if (!_call->conference()) {
using Flag = MTPphone_SendGroupCallMessage::Flag;
_api->request(MTPphone_SendGroupCallMessage(
MTP_flags(Flag::f_send_as
| (stars ? Flag::f_allow_paid_stars : Flag())),
_call->inputCall(),
MTP_long(randomId),
serialized
)).done([=](const MTPBool &, const MTP::Response &response) {
sent(randomId, response);
serialized,
MTP_long(stars),
from->input
)).done([=](
const MTPUpdates &result,
const MTP::Response &response) {
_session->api().applyUpdates(result, randomId);
}).fail([=](const MTP::Error &, const MTP::Response &response) {
failed(randomId, response);
}).send();
@@ -100,15 +212,28 @@ void Messages::send(TextWithTags text) {
failed(randomId, response);
}).send();
}
checkDestroying(true);
addStars(from, stars, true);
if (!skip) {
checkDestroying(true);
}
}
void Messages::setApplyingInitial(bool value) {
_applyingInitial = value;
}
void Messages::received(const MTPDupdateGroupCallMessage &data) {
if (!ready()) {
return;
}
received(data.vrandom_id().v, data.vfrom_id(), data.vmessage());
pushChanges();
const auto &fields = data.vmessage().data();
received(
fields.vid().v,
fields.vfrom_id(),
fields.vmessage(),
fields.vdate().v,
fields.vpaid_message_stars().value_or_empty());
}
void Messages::received(const MTPDupdateGroupCallEncryptedMessage &data) {
@@ -131,23 +256,97 @@ void Messages::received(const MTPDupdateGroupCallEncryptedMessage &data) {
LOG(("API Error: Can't parse decrypted message"));
return;
}
received(deserialized->randomId, fromId, deserialized->message, true);
pushChanges();
const auto realId = ++_conferenceIdAutoIncrement;
const auto randomId = deserialized->randomId;
if (!_conferenceIdByRandomId.emplace(randomId, realId).second) {
// Already received.
return;
}
received(
realId,
fromId,
deserialized->message,
base::unixtime::now(), // date
0, // stars
true); // checkCustomEmoji
}
void Messages::deleted(const MTPDupdateDeleteGroupCallMessages &data) {
const auto was = _messages.size();
for (const auto &id : data.vmessages().v) {
const auto i = ranges::find(_messages, id.v, &Message::id);
if (i != end(_messages)) {
_messages.erase(i);
}
}
if (_messages.size() < was) {
pushChanges();
}
}
void Messages::sent(const MTPDupdateMessageID &data) {
sent(data.vrandom_id().v, data.vid().v);
}
void Messages::sent(uint64 randomId, const MTP::Response &response) {
const auto realId = ++_conferenceIdAutoIncrement;
_conferenceIdByRandomId.emplace(randomId, realId);
sent(randomId, realId);
const auto i = ranges::find(_messages, realId, &Message::id);
if (i != end(_messages) && !i->date) {
i->date = Api::UnixtimeFromMsgId(response.outerMsgId);
i->pinFinishDate = PinFinishDate(*i);
checkDestroying(true);
}
}
void Messages::sent(uint64 randomId, MsgId realId) {
const auto i = _sendingIdByRandomId.find(randomId);
if (i == end(_sendingIdByRandomId)) {
return;
}
const auto localId = i->second;
_sendingIdByRandomId.erase(i);
const auto j = ranges::find(_messages, localId, &Message::id);
if (j == end(_messages)) {
_skippedIds.emplace(realId);
return;
}
j->id = realId;
crl::on_main(this, [=] {
const auto i = ranges::find(_messages, realId, &Message::id);
if (i != end(_messages) && !i->date) {
i->date = base::unixtime::now();
i->pinFinishDate = PinFinishDate(*i);
checkDestroying(true);
}
});
_idUpdates.fire({ .localId = localId, .realId = realId });
}
void Messages::received(
uint64 randomId,
MsgId id,
const MTPPeer &from,
const MTPTextWithEntities &message,
TimeId date,
int stars,
bool checkCustomEmoji) {
const auto peer = _call->peer();
const auto i = ranges::find(_messages, randomId, &Message::randomId);
const auto i = ranges::find(_messages, id, &Message::id);
if (i != end(_messages)) {
if (peerFromMTP(from) == peer->session().userPeerId() && !i->date) {
i->date = base::unixtime::now();
const auto fromId = peerFromMTP(from);
const auto me1 = peer->session().userPeerId();
const auto me2 = _call->messagesFrom()->id;
if (((fromId == me1) || (fromId == me2)) && !i->date) {
i->date = date;
i->pinFinishDate = PinFinishDate(*i);
checkDestroying(true);
}
return;
} else if (_skippedIds.contains(id)) {
return;
}
auto allowedEntityTypes = std::vector<EntityType>{
EntityType::Code,
@@ -162,30 +361,81 @@ void Messages::received(
if (checkCustomEmoji && !peer->isSelf() && !peer->isPremium()) {
allowedEntityTypes.pop_back();
}
_messages.push_back({
.randomId = randomId,
.date = base::unixtime::now(),
.peer = peer->owner().peer(peerFromMTP(from)),
.text = Ui::Text::Filtered(
Api::ParseTextWithEntities(&peer->session(), message),
allowedEntityTypes),
});
checkDestroying(true);
const auto author = peer->owner().peer(peerFromMTP(from));
auto text = Ui::Text::Filtered(
Api::ParseTextWithEntities(&author->session(), message),
allowedEntityTypes);
const auto mine = author->isSelf()
|| (author->isChannel() && author->asChannel()->amCreator());
const auto skip = skipMessage(text, stars);
if (skip) {
_skippedIds.emplace(id);
} else {
// Should check by sendAsPeers() list instead, but it may not be
// loaded here yet.
_messages.push_back({
.id = id,
.date = date,
.pinFinishDate = PinFinishDate(author, date, stars),
.peer = author,
.text = std::move(text),
.stars = stars,
.admin = (author == _call->peer()),
.mine = mine,
});
ranges::sort(_messages, ranges::less(), &Message::id);
}
if (!_applyingInitial) {
addStars(author, stars, mine);
}
if (!skip) {
checkDestroying(true);
}
}
bool Messages::skipMessage(const TextWithEntities &text, int stars) const {
const auto real = _call->lookupReal();
return text.empty()
&& real
&& (stars < real->messagesMinPrice());
}
void Messages::checkDestroying(bool afterChanges) {
auto next = TimeId();
const auto now = base::unixtime::now();
const auto destroyTime = now - _ttl;
const auto initial = _messages.size();
for (auto i = begin(_messages); i != end(_messages);) {
const auto initial = int(_messages.size());
if (_call->videoStream()) {
if (initial > kMaxShownVideoStreamMessages) {
const auto remove = initial - kMaxShownVideoStreamMessages;
auto i = begin(_messages);
for (auto k = 0; k != remove; ++k) {
if (i->date && i->pinFinishDate <= now) {
i = _messages.erase(i);
} else if (!next || next > i->pinFinishDate - now) {
next = i->pinFinishDate - now;
++i;
} else {
++i;
}
}
}
} else for (auto i = begin(_messages); i != end(_messages);) {
const auto date = i->date;
//const auto ttl = i->stars
// ? (Ui::StarsColoringForCount(i->stars).minutesPin * 60)
// : _ttl;
const auto ttl = _ttl;
if (!date) {
++i;
} else if (date <= destroyTime) {
if (i->id < 0) {
++i;
} else {
i = _messages.erase(i);
}
} else if (date + ttl <= now) {
i = _messages.erase(i);
} else if (!next) {
next = date + _ttl - now;
} else if (!next || next > date + ttl - now) {
next = date + ttl - now;
++i;
} else {
++i;
@@ -209,33 +459,286 @@ rpl::producer<std::vector<Message>> Messages::listValue() const {
return _changes.events_starting_with_copy(_messages);
}
rpl::producer<MessageIdUpdate> Messages::idUpdates() const {
return _idUpdates.events();
}
void Messages::sendPending() {
Expects(_real != nullptr);
for (auto &pending : base::take(_pending)) {
send(std::move(pending));
send(std::move(pending.text), pending.stars);
}
if (_paidSendingPending) {
reactionsPaidSend();
}
}
void Messages::pushChanges() {
_changes.fire_copy(_messages);
}
void Messages::sent(uint64 randomId, const MTP::Response &response) {
const auto i = ranges::find(_messages, randomId, &Message::randomId);
if (i != end(_messages) && !i->date) {
i->date = Api::UnixtimeFromMsgId(response.outerMsgId);
checkDestroying(true);
if (_changesScheduled) {
return;
}
_changesScheduled = true;
Ui::PostponeCall(this, [=] {
_changesScheduled = false;
_changes.fire_copy(_messages);
});
}
void Messages::failed(uint64 randomId, const MTP::Response &response) {
const auto i = ranges::find(_messages, randomId, &Message::randomId);
if (i != end(_messages) && !i->date) {
i->date = Api::UnixtimeFromMsgId(response.outerMsgId);
i->failed = true;
const auto i = _sendingIdByRandomId.find(randomId);
if (i == end(_sendingIdByRandomId)) {
return;
}
const auto localId = i->second;
_sendingIdByRandomId.erase(i);
const auto j = ranges::find(_messages, localId, &Message::id);
if (j != end(_messages) && !j->date) {
j->date = Api::UnixtimeFromMsgId(response.outerMsgId);
j->stars = 0;
j->failed = true;
checkDestroying(true);
}
}
int Messages::reactionsPaidScheduled() const {
return _paid.scheduled;
}
PeerId Messages::reactionsLocalShownPeer() const {
const auto minePaidShownPeer = [&] {
for (const auto &entry : _paid.top.topDonors) {
if (entry.my) {
return entry.peer ? entry.peer->id : PeerId();
}
}
return _session->userPeerId();
//const auto api = &_session->api();
//return api->globalPrivacy().paidReactionShownPeerCurrent();
};
return (_paid.scheduledFlag && _paid.scheduledPrivacySet)
? _paid.scheduledShownPeer
: (_paid.sendingFlag && _paid.sendingPrivacySet)
? _paid.sendingShownPeer
: minePaidShownPeer();
}
void Messages::reactionsPaidAdd(int count, std::optional<PeerId> shownPeer) {
Expects(count >= 0);
_paid.scheduled += count;
_paid.scheduledFlag = 1;
if (shownPeer.has_value()) {
_paid.scheduledShownPeer = *shownPeer;
_paid.scheduledPrivacySet = true;
}
if (count > 0) {
_session->credits().lock(CreditsAmount(count));
}
_call->peer()->owner().reactions().schedulePaid(_call);
_paidChanges.fire({});
}
void Messages::reactionsPaidScheduledCancel() {
if (!_paid.scheduledFlag) {
return;
}
if (const auto amount = int(_paid.scheduled)) {
_session->credits().unlock(
CreditsAmount(amount));
}
_paid.scheduled = 0;
_paid.scheduledFlag = 0;
_paid.scheduledShownPeer = 0;
_paid.scheduledPrivacySet = 0;
_paidChanges.fire({});
}
Data::PaidReactionSend Messages::startPaidReactionSending() {
_paidSendingPending = false;
if (!_paid.scheduledFlag || !_paid.scheduled) {
return {};
} else if (_paid.sendingFlag || !ready()) {
_paidSendingPending = true;
return {};
}
_paid.sending = _paid.scheduled;
_paid.sendingFlag = _paid.scheduledFlag;
_paid.sendingShownPeer = _paid.scheduledShownPeer;
_paid.sendingPrivacySet = _paid.scheduledPrivacySet;
_paid.scheduled = 0;
_paid.scheduledFlag = 0;
_paid.scheduledShownPeer = 0;
_paid.scheduledPrivacySet = 0;
return {
.count = int(_paid.sending),
.valid = true,
.shownPeer = MaybeShownPeer(
_paid.sendingPrivacySet,
_paid.sendingShownPeer),
};
}
void Messages::finishPaidSending(
Data::PaidReactionSend send,
bool success) {
Expects(send.count == _paid.sending);
Expects(send.valid == (_paid.sendingFlag == 1));
Expects(send.shownPeer == MaybeShownPeer(
_paid.sendingPrivacySet,
_paid.sendingShownPeer));
_paid.sending = 0;
_paid.sendingFlag = 0;
_paid.sendingShownPeer = 0;
_paid.sendingPrivacySet = 0;
if (const auto amount = send.count) {
if (success) {
_session->credits().withdrawLocked(CreditsAmount(amount));
auto &donors = _paid.top.topDonors;
const auto i = ranges::find(donors, true, &StarsTopDonor::my);
if (i != end(donors)) {
i->stars += amount;
} else {
donors.push_back({
.peer = _session->user(),
.stars = amount,
.my = true,
});
}
} else {
_session->credits().unlock(CreditsAmount(amount));
_paidChanges.fire({});
}
}
if (_paidSendingPending) {
reactionsPaidSend();
}
}
void Messages::reactionsPaidSend() {
const auto send = startPaidReactionSending();
if (!send.valid || !send.count) {
return;
}
const auto localId = _call->peer()->owner().nextLocalMessageId();
const auto randomId = base::RandomValue<uint64>();
_sendingIdByRandomId.emplace(randomId, localId);
const auto from = _call->messagesFrom();
const auto stars = int(send.count);
const auto skip = skipMessage({}, stars);
if (skip) {
_skippedIds.emplace(localId);
} else {
_messages.push_back({
.id = localId,
.peer = from,
.stars = stars,
.admin = (from == _call->peer()),
.mine = true,
});
}
using Flag = MTPphone_SendGroupCallMessage::Flag;
_api->request(MTPphone_SendGroupCallMessage(
MTP_flags(Flag::f_send_as | Flag::f_allow_paid_stars),
_call->inputCall(),
MTP_long(randomId),
MTP_textWithEntities(MTP_string(), MTP_vector<MTPMessageEntity>()),
MTP_long(stars),
from->input
)).done([=](
const MTPUpdates &result,
const MTP::Response &response) {
finishPaidSending(send, true);
_session->api().applyUpdates(result, randomId);
}).fail([=](const MTP::Error &, const MTP::Response &response) {
finishPaidSending(send, false);
failed(randomId, response);
}).send();
addStars(from, stars, true);
if (!skip) {
checkDestroying(true);
}
}
void Messages::undoScheduledPaidOnDestroy() {
_call->peer()->owner().reactions().undoScheduledPaid(_call);
}
Messages::PaidLocalState Messages::starsLocalState() const {
const auto &donors = _paid.top.topDonors;
const auto i = ranges::find(donors, true, &StarsTopDonor::my);
const auto local = int(_paid.scheduled);
const auto my = (i != end(donors) ? i->stars : 0) + local;
const auto total = _paid.top.total + local;
return { .total = total, .my = my };
}
void Messages::deleteConfirmed(MessageDeleteRequest request) {
const auto eraseFrom = [&](auto iterator) {
if (iterator != end(_messages)) {
_messages.erase(iterator, end(_messages));
pushChanges();
}
};
const auto peer = _call->peer();
if (const auto from = request.deleteAllFrom) {
using Flag = MTPphone_DeleteGroupCallParticipantMessages::Flag;
_api->request(MTPphone_DeleteGroupCallParticipantMessages(
MTP_flags(request.reportSpam ? Flag::f_report_spam : Flag()),
_call->inputCall(),
from->input
)).send();
eraseFrom(ranges::remove(_messages, not_null(from), &Message::peer));
} else {
using Flag = MTPphone_DeleteGroupCallMessages::Flag;
_api->request(MTPphone_DeleteGroupCallMessages(
MTP_flags(request.reportSpam ? Flag::f_report_spam : Flag()),
_call->inputCall(),
MTP_vector<MTPint>(1, MTP_int(request.id.bare))
)).send();
eraseFrom(ranges::remove(_messages, request.id, &Message::id));
}
if (const auto ban = request.ban) {
if (const auto channel = peer->asChannel()) {
ban->session().api().chatParticipants().kick(
channel,
ban,
ChatRestrictionsInfo());
} else {
ban->session().api().blockedPeers().block(ban);
}
}
}
void Messages::addStars(not_null<PeerData*> from, int stars, bool mine) {
if (stars <= 0) {
return;
}
_paid.top.total += stars;
const auto i = ranges::find(
_paid.top.topDonors,
from.get(),
&StarsTopDonor::peer);
if (i != end(_paid.top.topDonors)) {
i->stars += stars;
} else {
_paid.top.topDonors.push_back({
.peer = from,
.stars = stars,
.my = mine,
});
}
ranges::stable_sort(
_paid.top.topDonors,
ranges::greater(),
&StarsTopDonor::stars);
_paidChanges.fire({});
}
} // namespace Calls::Group

View File

@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#pragma once
#include "base/timer.h"
#include "base/weak_ptr.h"
namespace Calls {
class GroupCall;
@@ -15,8 +16,13 @@ class GroupCall;
namespace Data {
class GroupCall;
struct PaidReactionSend;
} // namespace Data
namespace Main {
class Session;
} // namespace Main
namespace MTP {
class Sender;
struct Response;
@@ -25,50 +31,163 @@ struct Response;
namespace Calls::Group {
struct Message {
uint64 randomId = 0;
MsgId id = 0;
TimeId date = 0;
TimeId pinFinishDate = 0;
not_null<PeerData*> peer;
TextWithEntities text;
int stars = 0;
bool failed = false;
bool admin = false;
bool mine = false;
};
class Messages final {
struct MessageIdUpdate {
MsgId localId = 0;
MsgId realId = 0;
};
struct MessageDeleteRequest {
MsgId id = 0;
PeerData *deleteAllFrom = nullptr;
PeerData *ban = nullptr;
bool reportSpam = false;
};
struct StarsTopDonor {
PeerData *peer = nullptr;
int stars = 0;
bool my = false;
friend inline bool operator==(
const StarsTopDonor &,
const StarsTopDonor &) = default;
};
struct StarsTop {
std::vector<StarsTopDonor> topDonors;
int total = 0;
friend inline bool operator==(
const StarsTop &,
const StarsTop &) = default;
};
class Messages final : public base::has_weak_ptr {
public:
Messages(not_null<GroupCall*> call, not_null<MTP::Sender*> api);
~Messages();
void send(TextWithTags text);
void send(TextWithTags text, int stars);
void setApplyingInitial(bool value);
void received(const MTPDupdateGroupCallMessage &data);
void received(const MTPDupdateGroupCallEncryptedMessage &data);
void deleted(const MTPDupdateDeleteGroupCallMessages &data);
void sent(const MTPDupdateMessageID &data);
[[nodiscard]] rpl::producer<std::vector<Message>> listValue() const;
[[nodiscard]] rpl::producer<MessageIdUpdate> idUpdates() const;
[[nodiscard]] int reactionsPaidScheduled() const;
[[nodiscard]] PeerId reactionsLocalShownPeer() const;
void reactionsPaidAdd(int count, std::optional<PeerId> shownPeer = {});
void reactionsPaidScheduledCancel();
void reactionsPaidSend();
void undoScheduledPaidOnDestroy();
struct PaidLocalState {
int total = 0;
int my = 0;
};
[[nodiscard]] PaidLocalState starsLocalState() const;
[[nodiscard]] rpl::producer<> starsValueChanges() const {
return _paidChanges.events();
}
[[nodiscard]] const StarsTop &starsTop() const {
return _paid.top;
}
void requestHiddenShow() {
_hiddenShowRequests.fire({});
}
[[nodiscard]] rpl::producer<> hiddenShowRequested() const {
return _hiddenShowRequests.events();
}
void deleteConfirmed(MessageDeleteRequest request);
private:
struct Pending {
TextWithTags text;
int stars = 0;
};
struct Paid {
StarsTop top;
PeerId scheduledShownPeer = 0;
PeerId sendingShownPeer = 0;
uint32 scheduled : 30 = 0;
uint32 scheduledFlag : 1 = 0;
uint32 scheduledPrivacySet : 1 = 0;
uint32 sending : 30 = 0;
uint32 sendingFlag : 1 = 0;
uint32 sendingPrivacySet : 1 = 0;
};
[[nodiscard]] bool ready() const;
void sendPending();
void pushChanges();
void checkDestroying(bool afterChanges = false);
void received(
uint64 randomId,
MsgId id,
const MTPPeer &from,
const MTPTextWithEntities &message,
TimeId date,
int stars,
bool checkCustomEmoji = false);
void sent(uint64 randomId, const MTP::Response &response);
void sent(uint64 randomId, MsgId realId);
void failed(uint64 randomId, const MTP::Response &response);
[[nodiscard]] bool skipMessage(
const TextWithEntities &text,
int stars) const;
[[nodiscard]] Data::PaidReactionSend startPaidReactionSending();
void finishPaidSending(Data::PaidReactionSend send, bool success);
void addStars(not_null<PeerData*> from, int stars, bool mine);
void requestStarsStats();
const not_null<GroupCall*> _call;
const not_null<Main::Session*> _session;
const not_null<MTP::Sender*> _api;
MsgId _conferenceIdAutoIncrement = 0;
base::flat_map<uint64, MsgId> _conferenceIdByRandomId;
base::flat_map<uint64, MsgId> _sendingIdByRandomId;
Data::GroupCall *_real = nullptr;
std::vector<TextWithTags> _pending;
std::vector<Pending> _pending;
base::Timer _destroyTimer;
std::vector<Message> _messages;
base::flat_set<MsgId> _skippedIds;
rpl::event_stream<std::vector<Message>> _changes;
rpl::event_stream<MessageIdUpdate> _idUpdates;
bool _applyingInitial = false;
mtpRequestId _starsTopRequestId = 0;
Paid _paid;
rpl::event_stream<> _paidChanges;
bool _paidSendingPending = false;
TimeId _ttl = 0;
bool _changesScheduled = false;
rpl::event_stream<> _hiddenShowRequests;
base::Timer _starsStatsTimer;
rpl::lifetime _lifetime;

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/unique_qptr.h"
#include "ui/effects/animations.h"
#include "ui/text/custom_emoji_helper.h"
#include "ui/round_rect.h"
struct TextWithTags;
@@ -26,40 +28,79 @@ class ElasticScroll;
class EmojiButton;
class InputField;
class SendButton;
class PopupMenu;
class RpWidget;
} // namespace Ui
namespace Calls::Group::Ui {
using namespace ::Ui;
struct StarsColoring;
} // namespace Calls::Group::Ui
namespace Calls::Group {
struct Message;
struct MessageIdUpdate;
struct MessageDeleteRequest;
enum class MessagesMode {
GroupCall,
VideoStream,
};
class MessagesUi final {
public:
MessagesUi(
not_null<QWidget*> parent,
std::shared_ptr<ChatHelpers::Show> show,
MessagesMode mode,
rpl::producer<std::vector<Message>> messages,
rpl::producer<std::vector<not_null<PeerData*>>> topDonorsValue,
rpl::producer<MessageIdUpdate> idUpdates,
rpl::producer<bool> canManageValue,
rpl::producer<bool> shown);
~MessagesUi();
void move(int left, int bottom, int width, int availableHeight);
void raise();
[[nodiscard]] rpl::producer<> hiddenShowRequested() const;
[[nodiscard]] rpl::producer<MessageDeleteRequest> deleteRequests() const;
[[nodiscard]] rpl::lifetime &lifetime();
private:
struct MessageView;
struct PinnedView;
struct PayedBg {
explicit PayedBg(const Ui::StarsColoring &coloring);
style::owned_color light;
style::owned_color dark;
Ui::RoundRect pinnedLight;
Ui::RoundRect pinnedDark;
Ui::RoundRect messageLight;
Ui::RoundRect badgeDark;
};
void setupBadges();
void setupList(
rpl::producer<std::vector<Message>> messages,
rpl::producer<bool> shown);
void showList(const std::vector<Message> &list);
void handleIdUpdates(rpl::producer<MessageIdUpdate> idUpdates);
void toggleMessage(MessageView &entry, bool shown);
void setContentFailed(MessageView &entry);
void setContent(MessageView &entry, const TextWithEntities &text);
void setContent(MessageView &entry);
void setContent(PinnedView &entry);
void updateMessageSize(MessageView &entry);
bool updateMessageHeight(MessageView &entry);
void updatePinnedSize(PinnedView &entry);
bool updatePinnedWidth(PinnedView &entry);
void animateMessageSent(MessageView &entry);
void repaintMessage(uint64 id);
void repaintMessage(MsgId id);
void highlightMessage(MsgId id);
void startHighlight(MsgId id);
void recountHeights(std::vector<MessageView>::iterator i, int top);
void appendMessage(const Message &data);
void checkReactionContent(
@@ -69,37 +110,89 @@ private:
void updateReactionPosition(MessageView &entry);
void removeReaction(not_null<Ui::RpWidget*> widget);
void setupMessagesWidget();
void applyWidth();
void togglePinned(PinnedView &entry, bool shown);
void repaintPinned(MsgId id);
void recountWidths(std::vector<PinnedView>::iterator i, int left);
void appendPinned(const Message &data, TimeId now);
void setupPinnedWidget();
void applyGeometry();
void applyGeometryToPinned();
void updateGeometries();
[[nodiscard]] int countPinnedScrollSkip(const PinnedView &entry) const;
void setPinnedScrollSkip(int skip);
void updateTopFade();
void updateBottomFade();
void updateLeftFade();
void updateRightFade();
void receiveSomeMouseEvents();
void receiveAllMouseEvents();
void handleClick(const MessageView &entry, QPoint point);
void showContextMenu(const MessageView &entry, QPoint globalPoint);
[[nodiscard]] int donorPlace(not_null<PeerData*> peer) const;
[[nodiscard]] TextWithEntities nameText(
not_null<PeerData*> peer,
int place);
const not_null<QWidget*> _parent;
const std::shared_ptr<ChatHelpers::Show> _show;
const MessagesMode _mode;
std::unique_ptr<Ui::ElasticScroll> _scroll;
Ui::Animations::Simple _scrollToBottomAnimation;
Ui::Animations::Simple _scrollToAnimation;
Ui::RpWidget *_messages = nullptr;
QImage _canvas;
std::unique_ptr<Ui::ElasticScroll> _pinnedScroll;
Ui::RpWidget *_pinned = nullptr;
QImage _pinnedCanvas;
int _pinnedScrollSkip = 0;
base::unique_qptr<Ui::PopupMenu> _menu;
rpl::variable<bool> _canManage;
rpl::event_stream<> _hiddenShowRequested;
rpl::event_stream<MessageDeleteRequest> _deleteRequests;
std::optional<std::vector<Message>> _hidden;
std::vector<MessageView> _views;
style::complex_color _messageBg;
Ui::RoundRect _messageBgRect;
MsgId _delayedHighlightId = 0;
MsgId _highlightId = 0;
Ui::Animations::Simple _highlightAnimation;
std::vector<PinnedView> _pinnedViews;
base::flat_map<uint64, std::unique_ptr<PayedBg>> _bgs;
QPoint _reactionBasePosition;
rpl::lifetime _effectsLifetime;
Ui::Animations::Simple _topFadeAnimation;
Ui::Animations::Simple _bottomFadeAnimation;
Ui::Text::String _liveBadge;
Ui::Text::String _adminBadge;
Ui::Text::CustomEmojiHelper _crownHelper;
base::flat_map<int, QString> _crownEmojiDataCache;
rpl::variable<std::vector<not_null<PeerData*>>> _topDonors;
//Ui::Animations::Simple _topFadeAnimation;
//Ui::Animations::Simple _bottomFadeAnimation;
//Ui::Animations::Simple _leftFadeAnimation;
//Ui::Animations::Simple _rightFadeAnimation;
int _fadeHeight = 0;
int _fadeWidth = 0;
bool _topFadeShown = false;
bool _bottomFadeShown = false;
bool _leftFadeShown = false;
bool _rightFadeShown = false;
bool _streamMode = false;
int _left = 0;
int _bottom = 0;
int _width = 0;
int _availableHeight = 0;
uint64 _revealedSpoilerId = 0;
MsgId _revealedSpoilerId = 0;
rpl::lifetime _lifetime;

View File

@@ -241,7 +241,11 @@ Panel::Panel(not_null<GroupCall*> call, ConferencePanelMigration info)
, _messages(std::make_unique<MessagesUi>(
widget(),
uiShow(),
MessagesMode::GroupCall,
_call->messages()->listValue(),
nullptr,
_call->messages()->idUpdates(),
_call->canManageValue(),
_call->messagesEnabledValue()))
, _toasts(std::make_unique<Toasts>(this))
, _controlsBackgroundColor([] {
@@ -1071,7 +1075,7 @@ Fn<void()> Panel::shareConferenceLinkCallback() {
return [=] {
Expects(_call->conference());
ShowConferenceCallLinkBox(uiShow(), _call->conferenceCall(), {
ShowConferenceCallLinkBox(uiShow(), _call->sharedCall(), {
.st = DarkConferenceCallLinkStyle(),
});
};
@@ -1080,7 +1084,7 @@ Fn<void()> Panel::shareConferenceLinkCallback() {
void Panel::migrationShowShareLink() {
ShowConferenceCallLinkBox(
uiShow(),
_call->conferenceCall(),
_call->sharedCall(),
{ .st = DarkConferenceCallLinkStyle() });
}
@@ -1656,7 +1660,7 @@ void Panel::addMembers() {
const auto &appConfig = _call->peer()->session().appConfig();
const auto conferenceLimit = appConfig.confcallSizeLimit();
if (_call->conference()
&& _call->conferenceCall()->fullCount() >= conferenceLimit) {
&& _call->sharedCall()->fullCount() >= conferenceLimit) {
uiShow()->showToast({ tr::lng_group_call_invite_limit(tr::now) });
}
const auto showToastCallback = [=](TextWithEntities &&text) {

View File

@@ -160,6 +160,7 @@ void StartRtmpProcess::close() {
void StartRtmpProcess::requestUrl(bool revoke) {
const auto session = &_request->peer->session();
_request->id = session->api().request(MTPphone_GetGroupCallStreamRtmpUrl(
MTP_flags(0),
_request->peer->input,
MTP_bool(revoke)
)).done([=](const MTPphone_GroupCallStreamRtmpUrl &result) {
@@ -231,7 +232,6 @@ void StartRtmpProcess::FillRtmpRows(
const style::RoundButton *attentionButtonStyle,
const style::PopupMenu *popupMenuStyle) {
struct State {
rpl::variable<bool> hidden = true;
rpl::variable<QString> key;
rpl::variable<QString> url;
bool warned = false;
@@ -239,8 +239,6 @@ void StartRtmpProcess::FillRtmpRows(
const auto &rowPadding = st::boxRowPadding;
const auto passChar = QChar(container->style()->styleHint(
QStyle::SH_LineEdit_PasswordCharacter));
const auto state = container->lifetime().make_state<State>();
state->key = rpl::duplicate(
data
@@ -280,11 +278,11 @@ void StartRtmpProcess::FillRtmpRows(
return weak;
};
const auto addLabel = [&](rpl::producer<QString> &&text) {
const auto addLabel = [&](v::text::data &&text) {
const auto label = container->add(
object_ptr<Ui::FlatLabel>(
container,
std::move(text),
v::text::take_marked(std::move(text)),
*labelStyle,
*popupMenuStyle),
st::boxRowPadding + QMargins(0, 0, showButtonStyle->width, 0));
@@ -319,47 +317,27 @@ void StartRtmpProcess::FillRtmpRows(
st::groupCallRtmpSubsectionTitleAddPadding,
subsectionTitleStyle);
auto keyLabelContent = rpl::combine(
state->hidden.value(),
state->key.value()
) | rpl::map([passChar](bool hidden, const QString &key) {
return key.isEmpty()
? QString()
: hidden
? QString().fill(passChar, kPasswordCharAmount)
: key;
auto keyLabelContent = state->key.value(
) | rpl::map([](const QString &key) {
const auto size = int(key.size());
auto result = TextWithEntities{ key };
if (size > 0) {
result.entities.push_back({ EntityType::Spoiler, 0, size });
}
return result;
}) | rpl::after_next([=] {
container->resizeToWidth(container->widthNoMargins());
});
const auto streamKeyLabel = addLabel(std::move(keyLabelContent));
streamKeyLabel->setSelectable(false);
const auto streamKeyButton = Ui::CreateChild<Ui::IconButton>(
container.get(),
*showButtonStyle);
streamKeyLabel->topValue(
) | rpl::start_with_next([=, right = rowPadding.right()](int top) {
streamKeyButton->moveToRight(
st::groupCallRtmpShowButtonPosition.x(),
top + st::groupCallRtmpShowButtonPosition.y());
streamKeyButton->raise();
}, container->lifetime());
streamKeyButton->addClickHandler([=] {
const auto toggle = [=] {
const auto newValue = !state->hidden.current();
state->hidden = newValue;
streamKeyLabel->setSelectable(!newValue);
streamKeyLabel->setAttribute(
Qt::WA_TransparentForMouseEvents,
newValue);
};
if (!state->warned && state->hidden.current()) {
streamKeyLabel->setClickHandlerFilter([=](
const ClickHandlerPtr &handler,
Qt::MouseButton button) {
if (button == Qt::LeftButton) {
show->showBox(Ui::MakeConfirmBox({
.text = tr::lng_group_call_rtmp_key_warning(
Ui::Text::RichLangValue),
.confirmed = [=](Fn<void()> &&close) {
state->warned = true;
toggle();
handler->onClick({});
close();
},
.confirmText = tr::lng_from_request_understand(),
@@ -367,9 +345,8 @@ void StartRtmpProcess::FillRtmpRows(
.confirmStyle = attentionButtonStyle,
.labelStyle = labelStyle,
}));
} else {
toggle();
}
return false;
});
addButton(true, tr::lng_group_call_rtmp_key_copy());

View File

@@ -75,6 +75,7 @@ void SaveCallJoinMuted(
const auto call = peer->groupCall();
if (!call
|| call->id() != callId
|| peer->isUser()
|| !peer->canManageGroupCall()
|| !call->canChangeJoinMuted()
|| call->joinMuted() == joinMuted) {
@@ -86,7 +87,8 @@ void SaveCallJoinMuted(
MTP_flags(Flag::f_join_muted),
call->input(),
MTP_bool(joinMuted),
MTPBool() // messages_enabled
MTPBool(), // messages_enabled
MTPlong() // send_paid_messages_stars
)).send();
}
@@ -98,7 +100,7 @@ void SaveCallMessagesEnabled(
if (!call
|| call->id() != callId
|| !peer->canManageGroupCall()
|| !call->canChangeJoinMuted()
|| !call->canChangeMessagesEnabled()
|| call->messagesEnabled() == messagesEnabled) {
return;
}
@@ -108,7 +110,8 @@ void SaveCallMessagesEnabled(
MTP_flags(Flag::f_messages_enabled),
call->input(),
MTPBool(), // join_muted
MTP_bool(messagesEnabled)
MTP_bool(messagesEnabled),
MTPlong() // send_paid_messages_stars
)).send();
}
@@ -690,6 +693,7 @@ void SettingsBox(
const auto session = &peer->session();
state->requestId = session->api().request(
MTPphone_GetGroupCallStreamRtmpUrl(
MTP_flags(0),
peer->input,
MTP_bool(true)
)).done([=](const MTPphone_GroupCallStreamRtmpUrl &result) {
@@ -887,7 +891,7 @@ std::pair<Fn<void()>, rpl::lifetime> ShareInviteLinkAction(
bool generatingLink = false;
};
const auto state = lifetime.make_state<State>(&peer->session());
if (!peer->canManageGroupCall()) {
if (peer->isUser() || !peer->canManageGroupCall()) {
state->linkSpeaker = QString();
}

View File

@@ -0,0 +1,141 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "calls/group/calls_group_stars_box.h"
#include "boxes/send_credits_box.h"
#include "chat_helpers/compose/compose_show.h"
#include "data/data_message_reactions.h"
#include "data/data_peer.h"
#include "data/data_user.h"
#include "lang/lang_keys.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
#include "payments/ui/payments_reaction_box.h"
#include "ui/layers/generic_box.h"
#include "ui/text/text_utilities.h"
#include "ui/dynamic_thumbnails.h"
#include "window/window_session_controller.h"
namespace Calls::Group {
namespace {
constexpr auto kMaxStarsFallback = 10'000;
constexpr auto kDefaultStars = 10;
} // namespace
int MaxVideoStreamStarsCount(not_null<Main::Session*> session) {
const auto appConfig = &session->appConfig();
return std::max(
appConfig->get<int>(
u"stars_groupcall_message_amount_max"_q,
kMaxStarsFallback),
2);
}
void VideoStreamStarsBox(
not_null<Ui::GenericBox*> box,
VideoStreamStarsBoxArgs &&args) {
args.show->session().credits().load();
const auto sending = args.sending;
auto submitText = [=](rpl::producer<int> amount) {
auto nice = std::move(amount) | rpl::map([=](int count) {
return Ui::CreditsEmojiSmall().append(
Lang::FormatCountDecimal(count));
});
return (sending
? tr::lng_paid_reaction_button
: tr::lng_paid_comment_button)(
lt_stars,
std::move(nice),
Ui::Text::RichLangValue);
};
const auto &show = args.show;
const auto session = &show->session();
const auto max = std::max(args.min, MaxVideoStreamStarsCount(session));
const auto chosen = std::clamp(
args.current ? args.current : kDefaultStars,
args.min,
max);
auto top = std::vector<Ui::PaidReactionTop>();
const auto add = [&](const Data::MessageReactionsTopPaid &entry) {
const auto peer = entry.peer;
const auto name = peer
? peer->shortName()
: tr::lng_paid_react_anonymous(tr::now);
const auto open = [=] {
if (const auto controller = show->resolveWindow()) {
controller->showPeerInfo(peer);
}
};
top.push_back({
.name = name,
.photo = (peer
? Ui::MakeUserpicThumbnail(peer)
: Ui::MakeHiddenAuthorThumbnail()),
.barePeerId = peer ? uint64(peer->id.value) : 0,
.count = int(entry.count),
.click = peer ? open : Fn<void()>(),
.my = (entry.my == 1),
});
};
top.reserve(args.top.size() + 1);
for (const auto &entry : args.top) {
add(entry);
}
auto myAdded = base::flat_set<uint64>();
const auto i = ranges::find(top, true, &Ui::PaidReactionTop::my);
if (i != end(top)) {
myAdded.emplace(i->barePeerId);
}
const auto myCount = uint32((i != end(top)) ? i->count : 0);
const auto myAdd = [&](not_null<PeerData*> peer) {
const auto barePeerId = uint64(peer->id.value);
if (!myAdded.emplace(barePeerId).second) {
return;
}
add(Data::MessageReactionsTopPaid{
.peer = peer,
.count = myCount,
.my = true,
});
};
myAdd(session->user());
ranges::stable_sort(top, ranges::greater(), &Ui::PaidReactionTop::count);
const auto weak = base::make_weak(box);
Ui::PaidReactionsBox(box, {
.min = args.min,
.chosen = chosen,
.max = max,
.top = std::move(top),
.session = &show->session(),
.name = args.name,
.submit = std::move(submitText),
.colorings = show->session().appConfig().groupCallColorings(),
.balanceValue = session->credits().balanceValue(),
.send = [weak, save = args.save](int count, uint64 barePeerId) {
save(count);
if (const auto strong = weak.get()) {
strong->closeBox();
}
},
.videoStreamChoosing = !sending,
.videoStreamSending = sending,
.dark = true,
});
}
object_ptr<Ui::BoxContent> MakeVideoStreamStarsBox(
VideoStreamStarsBoxArgs &&args) {
return Box(VideoStreamStarsBox, std::move(args));
}
} // namespace Calls::Group

View File

@@ -0,0 +1,53 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
namespace ChatHelpers {
class Show;
} // namespace ChatHelpers
namespace Data {
struct MessageReactionsTopPaid;
} // namespace Data
namespace Main {
class Session;
} // namespace Main
namespace Ui {
class BoxContent;
class GenericBox;
} // namespace Ui
namespace Calls::Group::Ui {
using namespace ::Ui;
struct StarsColoring;
} // namespace Calls::Group::Ui
namespace Calls::Group {
[[nodiscard]] int MaxVideoStreamStarsCount(not_null<Main::Session*> session);
struct VideoStreamStarsBoxArgs {
std::shared_ptr<ChatHelpers::Show> show;
std::vector<Data::MessageReactionsTopPaid> top;
int min = 0;
int current = 0;
bool sending = false;
Fn<void(int)> save;
QString name;
};
void VideoStreamStarsBox(
not_null<Ui::GenericBox*> box,
VideoStreamStarsBoxArgs &&args);
[[nodiscard]] object_ptr<Ui::BoxContent> MakeVideoStreamStarsBox(
VideoStreamStarsBoxArgs &&args);
} // namespace Calls::Group

View File

@@ -26,8 +26,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "lang/lang_keys.h"
#include "styles/style_calls.h"
#include <QtGui/QtEvents>
#include <QOpenGLShader>
#include <QtGui/QtEvents>
#include <QOpenGLWidget>
namespace Calls::Group {
namespace {
@@ -51,23 +52,43 @@ namespace {
Viewport::Viewport(
not_null<QWidget*> parent,
PanelMode mode,
Ui::GL::Backend backend)
Ui::GL::Backend backend,
Ui::RpWidgetWrap *borrowedRp,
bool borrowedOpenGL)
: _mode(mode)
, _content(Ui::GL::CreateSurface(parent, chooseRenderer(backend))) {
, _opengl(borrowedOpenGL)
, _content(borrowedRp
? nullptr
: Ui::GL::CreateSurface(parent, chooseRenderer(backend)))
, _borrowed(borrowedRp) {
setup();
}
Viewport::~Viewport() = default;
Viewport::~Viewport() {
if (_borrowed && _opengl) {
const auto w = static_cast<QOpenGLWidget*>(widget().get());
w->makeCurrent();
const auto context = w->context();
const auto valid = w->isValid()
&& context
&& (QOpenGLContext::currentContext() == context);
ensureBorrowedCleared(valid ? context->functions() : nullptr);
}
}
not_null<QWidget*> Viewport::widget() const {
return _content->rpWidget();
return _borrowed ? _borrowed->rpWidget() : _content->rpWidget();
}
not_null<Ui::RpWidgetWrap*> Viewport::rp() const {
return _content.get();
return _borrowed ? _borrowed : _content.get();
}
void Viewport::setup() {
if (_borrowed) {
return;
}
const auto raw = widget();
raw->resize(0, 0);
@@ -76,7 +97,7 @@ void Viewport::setup() {
_content->sizeValue(
) | rpl::filter([=] {
return wide();
return wide() || videoStream();
}) | rpl::start_with_next([=] {
updateTilesGeometry();
}, lifetime());
@@ -106,23 +127,29 @@ void Viewport::setup() {
}
void Viewport::setGeometry(bool fullscreen, QRect geometry) {
Expects(wide());
Expects(wide() || videoStream());
if (_borrowed) {
updateMyWidgetPart();
_borrowedGeometry = geometry;
updateMyWidgetPart();
updateTilesGeometry();
return;
}
const auto changed = (_fullscreen != fullscreen);
if (changed) {
_fullscreen = fullscreen;
}
if (widget()->geometry() != geometry) {
_geometryStaleAfterModeChange = false;
widget()->setGeometry(geometry);
} else if (_geometryStaleAfterModeChange || changed) {
_geometryStaleAfterModeChange = false;
} else if (changed) {
updateTilesGeometry();
}
}
void Viewport::resizeToWidth(int width) {
Expects(!wide());
Expects(!wide() && !videoStream());
updateTilesGeometry(width);
}
@@ -139,6 +166,10 @@ bool Viewport::wide() const {
return (_mode == PanelMode::Wide);
}
bool Viewport::videoStream() const {
return (_mode == PanelMode::VideoStream);
}
void Viewport::setMode(PanelMode mode, not_null<QWidget*> parent) {
if (_mode == mode && widget()->parent() == parent) {
return;
@@ -153,7 +184,7 @@ void Viewport::setMode(PanelMode mode, not_null<QWidget*> parent) {
widget()->show();
}
}
if (!wide()) {
if (!wide() && !videoStream()) {
for (const auto &tile : _tiles) {
tile->toggleTopControlsShown(false);
}
@@ -173,7 +204,9 @@ void Viewport::handleMouseRelease(QPoint position, Qt::MouseButton button) {
setPressed({});
if (const auto tile = pressed.tile) {
if (pressed == _selected) {
if (button == Qt::RightButton) {
if (videoStream()) {
return;
} else if (button == Qt::RightButton) {
tile->row()->showContextMenu();
} else if (!wide()
|| (_hasTwoOrMore && !_large)
@@ -191,7 +224,7 @@ void Viewport::handleMouseMove(QPoint position) {
}
void Viewport::updateSelected(QPoint position) {
if (!widget()->rect().contains(position)) {
if (_borrowed || !widget()->rect().contains(position)) {
setSelected({});
return;
}
@@ -219,12 +252,23 @@ void Viewport::updateSelected(QPoint position) {
}
void Viewport::updateSelected() {
if (_borrowed) {
return;
}
updateSelected(widget()->mapFromGlobal(QCursor::pos()));
}
void Viewport::setControlsShown(float64 shown) {
_controlsShownRatio = shown;
widget()->update();
updateMyWidgetPart();
}
void Viewport::updateMyWidgetPart() {
if (!_borrowed) {
widget()->update();
} else if (!_borrowedGeometry.isEmpty()) {
widget()->update(_borrowedGeometry);
}
}
void Viewport::setCursorShown(bool shown) {
@@ -245,7 +289,7 @@ void Viewport::add(
track,
std::move(trackSize),
std::move(pinned),
[=] { widget()->update(); },
[=] { updateMyWidgetPart(); },
self));
_tiles.back()->trackSizeValue(
@@ -325,7 +369,8 @@ void Viewport::prepareLargeChangeAnimation() {
void Viewport::startLargeChangeAnimation() {
Expects(!_largeChangeAnimation.animating());
if (!wide()
if (_borrowed
|| !wide()
|| anim::Disabled()
|| (_startTilesLayout.list.size() < 2)
|| !_opengl
@@ -415,7 +460,7 @@ Viewport::Layout Viewport::applyLarge(Layout layout) const {
}
void Viewport::updateTilesAnimated() {
if (!_largeChangeAnimation.animating()) {
if (_borrowed || !_largeChangeAnimation.animating()) {
updateTilesGeometry();
return;
}
@@ -539,7 +584,11 @@ Viewport::Layout Viewport::countWide(int outerWidth, int outerHeight) const {
}
void Viewport::showLarge(const VideoEndpoint &endpoint) {
// If a video get's switched off, GroupCall first unpins it,
if (_borrowed) {
return;
}
// If a video gets switched off, GroupCall first unpins it,
// then removes it from Large endpoint, then removes from active tracks.
//
// If we want to animate large video removal properly, we need to
@@ -566,7 +615,9 @@ void Viewport::showLarge(const VideoEndpoint &endpoint) {
}
void Viewport::updateTilesGeometry() {
updateTilesGeometry(widget()->width());
updateTilesGeometry(_borrowed
? _borrowedGeometry.width()
: widget()->width());
}
void Viewport::updateTilesGeometry(int outerWidth) {
@@ -575,16 +626,18 @@ void Viewport::updateTilesGeometry(int outerWidth) {
if (mouseInside) {
updateSelected();
}
widget()->update();
updateMyWidgetPart();
});
const auto outerHeight = widget()->height();
const auto outerHeight = _borrowed
? _borrowedGeometry.height()
: widget()->height();
if (_tiles.empty() || !outerWidth) {
_fullHeight = 0;
return;
}
if (wide()) {
if (wide() || videoStream()) {
updateTilesGeometryWide(outerWidth, outerHeight);
refreshHasTwoOrMore();
_fullHeight = 0;
@@ -777,8 +830,10 @@ void Viewport::setTileGeometry(not_null<VideoTile*> tile, QRect geometry) {
const auto kSmall = style::ConvertScale(240);
const auto &endpoint = tile->endpoint();
const auto forceThumbnailQuality = !wide()
&& !videoStream()
&& (ranges::count(_tiles, false, &VideoTile::hidden) > 1);
const auto forceFullQuality = wide() && (tile.get() == _large);
const auto forceFullQuality = videoStream()
|| (wide() && (tile.get() == _large));
const auto quality = forceThumbnailQuality
? VideoQuality::Thumbnail
: (forceFullQuality || min >= kMedium)
@@ -807,6 +862,9 @@ void Viewport::setSelected(Selection value) {
}
void Viewport::updateCursor() {
if (_borrowed) {
return;
}
const auto pointer = _selected.tile && (!wide() || _hasTwoOrMore);
widget()->setCursor(_cursorHidden
? Qt::BlankCursor
@@ -825,14 +883,18 @@ void Viewport::setPressed(Selection value) {
Ui::GL::ChosenRenderer Viewport::chooseRenderer(Ui::GL::Backend backend) {
_opengl = (backend == Ui::GL::Backend::OpenGL);
return {
.renderer = (_opengl
? std::unique_ptr<Ui::GL::Renderer>(
std::make_unique<RendererGL>(this))
: std::make_unique<RendererSW>(this)),
.renderer = makeRenderer(),
.backend = backend,
};
}
std::unique_ptr<Ui::GL::Renderer> Viewport::makeRenderer() {
return _opengl
? std::unique_ptr<Ui::GL::Renderer>(
std::make_unique<RendererGL>(this))
: std::make_unique<RendererSW>(this);
}
bool Viewport::requireARGB32() const {
return !_opengl;
}
@@ -861,8 +923,44 @@ rpl::producer<bool> Viewport::mouseInsideValue() const {
return _mouseInside.value();
}
void Viewport::ensureBorrowedRenderer(QOpenGLFunctions &f) {
Expects(_borrowed != nullptr);
if (_borrowedRenderer) {
return;
}
_borrowedRenderer = makeRenderer();
_borrowedRenderer->init(f);
}
void Viewport::ensureBorrowedCleared(QOpenGLFunctions *f) {
Expects(_borrowed != nullptr);
if (const auto renderer = base::take(_borrowedRenderer)) {
renderer->deinit(f);
}
}
void Viewport::borrowedPaint(QOpenGLFunctions &f) {
Expects(_borrowedRenderer != nullptr);
Expects(_opengl);
_borrowedRenderer->paint(static_cast<QOpenGLWidget*>(widget().get()), f);
}
void Viewport::borrowedPaint(Painter &p, const QRegion &clip) {
Expects(_borrowedRenderer != nullptr);
Expects(!_opengl);
_borrowedRenderer->paintFallback(p, clip, Ui::GL::Backend::Raster);
}
QPoint Viewport::borrowedOrigin() const {
return _borrowed ? _borrowedGeometry.topLeft() : QPoint();
}
rpl::lifetime &Viewport::lifetime() {
return _content->lifetime();
return _lifetime;
}
rpl::producer<QString> MuteButtonTooltip(not_null<GroupCall*> call) {

View File

@@ -10,15 +10,20 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/rp_widget.h"
#include "ui/effects/animations.h"
class Painter;
class QOpenGLFunctions;
namespace Ui {
class AbstractButton;
class RpWidgetWrap;
namespace GL {
} // namespace Ui
namespace Ui::GL {
enum class Backend;
struct Capabilities;
struct ChosenRenderer;
} // namespace GL
} // namespace Ui
class Renderer;
} // namespace Ui::GL
namespace Calls {
class GroupCall;
@@ -63,7 +68,9 @@ public:
Viewport(
not_null<QWidget*> parent,
PanelMode mode,
Ui::GL::Backend backend);
Ui::GL::Backend backend,
Ui::RpWidgetWrap *borrowedRp = nullptr,
bool borrowedOpenGL = false);
~Viewport();
[[nodiscard]] not_null<QWidget*> widget() const;
@@ -93,6 +100,12 @@ public:
[[nodiscard]] rpl::producer<VideoQualityRequest> qualityRequests() const;
[[nodiscard]] rpl::producer<bool> mouseInsideValue() const;
void ensureBorrowedRenderer(QOpenGLFunctions &f);
void ensureBorrowedCleared(QOpenGLFunctions *f);
void borrowedPaint(QOpenGLFunctions &f);
void borrowedPaint(Painter &p, const QRegion &clip);
[[nodiscard]] QPoint borrowedOrigin() const;
[[nodiscard]] rpl::lifetime &lifetime();
static constexpr auto kShadowMaxAlpha = 80;
@@ -140,6 +153,7 @@ private:
void setup();
[[nodiscard]] bool wide() const;
[[nodiscard]] bool videoStream() const;
void updateCursor();
void updateTilesGeometry();
@@ -168,10 +182,11 @@ private:
[[nodiscard]] Ui::GL::ChosenRenderer chooseRenderer(
Ui::GL::Backend backend);
[[nodiscard]] std::unique_ptr<Ui::GL::Renderer> makeRenderer();
void updateMyWidgetPart();
PanelMode _mode = PanelMode();
bool _opengl = false;
bool _geometryStaleAfterModeChange = false;
const std::unique_ptr<Ui::RpWidgetWrap> _content;
std::vector<std::unique_ptr<VideoTile>> _tiles;
std::vector<not_null<VideoTile*>> _tilesForOrder;
@@ -194,6 +209,13 @@ private:
Selection _pressed;
rpl::variable<bool> _mouseInside = false;
Ui::RpWidgetWrap * const _borrowed = nullptr;
QRect _borrowedGeometry;
std::unique_ptr<Ui::GL::Renderer> _borrowedRenderer;
QMetaObject::Connection _borrowedConnection;
rpl::lifetime _lifetime;
};
[[nodiscard]] QImage GenerateShadow(

View File

@@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/painter.h"
#include "data/data_peer.h"
#include "styles/style_calls.h"
#include "styles/style_media_view.h"
#include <QOpenGLShader>
@@ -443,7 +444,7 @@ void Viewport::RendererGL::paint(
}
std::optional<QColor> Viewport::RendererGL::clearColor() {
return st::groupCallBg->c;
return _owner->videoStream() ? st::mediaviewBg->c : st::groupCallBg->c;
}
void Viewport::RendererGL::validateUserpicFrame(
@@ -485,7 +486,8 @@ void Viewport::RendererGL::paintTile(
_rgbaFrame = (data.format == Webrtc::FrameFormat::ARGB32)
|| _userpicFrame;
const auto geometry = tile->geometry();
const auto geometry = tile->geometry().translated(
_owner->borrowedOrigin());
const auto x = geometry.x();
const auto y = geometry.y();
const auto width = geometry.width();
@@ -784,12 +786,15 @@ void Viewport::RendererGL::paintTile(
program->setUniformValue("viewport", uniformViewport);
program->setUniformValue(
"frameBg",
fullscreen ? QColor(0, 0, 0) : st::groupCallBg->c);
fullscreen ? QColor(0, 0, 0) : *clearColor());
const auto radius = _owner->videoStream()
? st::storiesRadius
: st::roundRadiusLarge;
program->setUniformValue("radiusOutline", QVector2D(
GLfloat(st::roundRadiusLarge * _factor * (fullscreen ? 0. : 1.)),
GLfloat(radius * _factor * (fullscreen ? 0. : 1.)),
(outline > 0) ? (st::groupCallOutline * _factor) : 0.f));
program->setUniformValue("roundRect", Uniform(rect));
program->setUniformValue("roundBg", st::groupCallBg->c);
program->setUniformValue("roundBg", *clearColor());
program->setUniformValue("outlineFg", QVector4D(
st::groupCallMemberActiveIcon->c.redF(),
st::groupCallMemberActiveIcon->c.greenF(),

View File

@@ -53,7 +53,9 @@ void Viewport::RendererSW::paintFallback(
paintTile(p, tile.get(), bounding, bg);
}
const auto fullscreen = _owner->_fullscreen;
const auto color = fullscreen ? QColor(0, 0, 0) : st::groupCallBg->c;
const auto color = fullscreen
? QColor(0, 0, 0)
: st::groupCallBg->c;
for (const auto &rect : bg) {
p.fillRect(rect, color);
}
@@ -129,7 +131,8 @@ void Viewport::RendererSW::paintTile(
};
using namespace Media::View;
const auto geometry = tile->geometry();
const auto geometry = tile->geometry().translated(
_owner->borrowedOrigin());
const auto x = geometry.x();
const auto y = geometry.y();
const auto width = geometry.width();

View File

@@ -261,11 +261,13 @@ void MenuVolumeItem::updateSliderColor(float64 value) {
Ui::ColorFromSerialized(0x24CD80),
Ui::ColorFromSerialized(0x3BBCEC),
} };
_slider->setActiveFgOverride((value < 0.25)
? anim::color(colors[0], colors[1], value / 0.25)
: (value < 0.5)
? anim::color(colors[1], colors[2], (value - 0.25) / 0.25)
: anim::color(colors[2], colors[3], (value - 0.5) / 0.5));
_slider->setColorOverrides({
.activeFg = (value < 0.25)
? anim::color(colors[0], colors[1], value / 0.25)
: (value < 0.5)
? anim::color(colors[1], colors[2], (value - 0.25) / 0.25)
: anim::color(colors[2], colors[3], (value - 0.5) / 0.5),
});
}
not_null<QAction*> MenuVolumeItem::action() const {

View File

@@ -0,0 +1,122 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "calls/group/ui/calls_group_stars_coloring.h"
#include "base/object_ptr.h"
#include "lang/lang_keys.h"
#include "payments/ui/payments_reaction_box.h"
#include "ui/widgets/labels.h"
#include "ui/emoji_config.h"
#include "ui/painter.h"
#include "ui/rp_widget.h"
#include "styles/style_credits.h"
#include "styles/style_layers.h"
#include "styles/style_premium.h"
namespace Calls::Group::Ui {
StarsColoring StarsColoringForCount(
const std::vector<StarsColoring> &colorings,
int stars) {
for (auto i = begin(colorings), e = end(colorings); i != e; ++i) {
if (i->fromStars > stars) {
Assert(i != begin(colorings));
return *(std::prev(i));
}
}
return colorings.back();
}
int StarsRequiredForMessage(
const std::vector<StarsColoring> &colorings,
const TextWithTags &text) {
Expects(!colorings.empty());
auto emojis = 0;
auto outLength = 0;
auto view = QStringView(text.text);
const auto length = int(view.size());
while (!view.isEmpty()) {
if (Emoji::Find(view, &outLength)) {
view = view.mid(outLength);
++emojis;
} else {
view = view.mid(1);
}
}
for (const auto &entry : colorings) {
if (emojis <= entry.emojiLimit && length <= entry.charactersMax) {
return entry.fromStars;
}
}
return colorings.back().fromStars + 1;
}
object_ptr<RpWidget> VideoStreamStarsLevel(
not_null<RpWidget*> box,
const std::vector<StarsColoring> &colorings,
rpl::producer<int> starsValue) {
struct State {
rpl::variable<int> stars;
rpl::variable<StarsColoring> coloring;
};
const auto state = box->lifetime().make_state<State>();
state->stars = std::move(starsValue);
state->coloring = state->stars.value(
) | rpl::map([=](int stars) {
return StarsColoringForCount(colorings, stars);
});
auto pinTitle = state->coloring.value(
) | rpl::map([=](const StarsColoring &value) {
const auto seconds = value.secondsPin;
return (seconds >= 3600)
? tr::lng_hours_tiny(tr::now, lt_count, seconds / 3600)
: (seconds >= 60)
? tr::lng_minutes_tiny(tr::now, lt_count, seconds / 60)
: tr::lng_seconds_tiny(tr::now, lt_count, seconds);
});
auto limitTitle = state->coloring.value(
) | rpl::map([=](const StarsColoring &value) {
return QString::number(value.charactersMax);
});
auto limitSubtext = state->coloring.value(
) | rpl::map([=](const StarsColoring &value) {
return tr::lng_paid_comment_limit_about(
tr::now,
lt_count,
value.charactersMax);
});
auto emojiTitle = state->coloring.value(
) | rpl::map([=](const StarsColoring &value) {
return QString::number(value.emojiLimit);
});
auto emojiSubtext = state->coloring.value(
) | rpl::map([=](const StarsColoring &value) {
return tr::lng_paid_comment_emoji_about(
tr::now,
lt_count,
value.emojiLimit);
});
return MakeStarSelectInfoBlocks(box, {
{
.title = std::move(pinTitle) | Text::ToWithEntities(),
.subtext = tr::lng_paid_comment_pin_about(),
},
{
.title = std::move(limitTitle) | Text::ToWithEntities(),
.subtext = std::move(limitSubtext),
},
{
.title = std::move(emojiTitle) | Text::ToWithEntities(),
.subtext = std::move(emojiSubtext),
},
}, {}, true);
}
} // namespace Calls::Group::Ui

View File

@@ -0,0 +1,47 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
namespace Ui {
class RpWidget;
} // namespace Ui
namespace Calls::Group::Ui {
using namespace ::Ui;
struct StarsColoring {
int bgLight = 0;
int bgDark = 0;
int fromStars = 0;
TimeId secondsPin = 0;
int charactersMax = 0;
int emojiLimit = 0;
friend inline auto operator<=>(
const StarsColoring &,
const StarsColoring &) = default;
friend inline bool operator==(
const StarsColoring &,
const StarsColoring &) = default;
};
[[nodiscard]] StarsColoring StarsColoringForCount(
const std::vector<StarsColoring> &colorings,
int stars);
[[nodiscard]] int StarsRequiredForMessage(
const std::vector<StarsColoring> &colorings,
const TextWithTags &text);
[[nodiscard]] object_ptr<Ui::RpWidget> VideoStreamStarsLevel(
not_null<Ui::RpWidget*> box,
const std::vector<StarsColoring> &colorings,
rpl::producer<int> starsValue);
} // namespace Calls::Group::Ui

View File

@@ -73,6 +73,7 @@ ComposeIcons {
menuBelow: icon;
menuAbove: icon;
menuPrice: icon;
menuEditStars: icon;
stripBubble: icon;
stripExpandPanel: icon;
@@ -148,6 +149,8 @@ EmojiButton {
}
SendButton {
sendIconPosition: point;
sendIconFillPadding: pixels;
inner: IconButton;
stars: RoundButton;
record: icon;
@@ -198,16 +201,43 @@ ComposeFiles {
statusFg: color;
}
SendAsButton {
width: pixels;
height: pixels;
size: pixels;
activeBg: color;
activeFg: color;
cross: CrossAnimation;
duration: int;
}
ChooseSendAs {
button: SendAsButton;
label: FlatLabel;
list: PeerList;
}
ComposeControls {
bg: color;
radius: pixels;
padding: margins;
field: InputField;
fieldLeft: pixels;
send: SendButton;
attach: IconButton;
emoji: EmojiButton;
like: IconButton;
liked: icon;
editStars: IconButton;
commentsShow: IconButton;
commentsShown: icon;
commentsSkip: pixels;
commentsUnreadSize: pixels;
commentsUnreadMargin: pixels;
commentsUnreadPosition: point;
starsReactionCounter: RoundButton;
starsSkip: pixels;
suggestions: EmojiSuggestions;
tabbed: EmojiPan;
tabbedHeightMin: pixels;
@@ -218,6 +248,7 @@ ComposeControls {
boxField: InputField;
restrictionLabel: FlatLabel;
premiumRequired: ComposePremiumRequired;
chooseSendAs: ChooseSendAs;
}
ReportBox {
@@ -1104,6 +1135,8 @@ historyComposeField: InputField(defaultInputField) {
}
historyComposeFieldMaxHeight: 224px;
historyComposeFieldFadeHeight: 6px;
historySendRight: 2px;
historySendPadding: 9px;
// historyMinHeight: 56px;
historyAttach: IconButton(defaultIconButton) {
@@ -1223,7 +1256,6 @@ boxAttachEmojiTop: 20px;
historySendIcon: icon {{ "chat/input_send", historySendIconFg }};
historySendIconOver: icon {{ "chat/input_send", historySendIconFgOver }};
historySendIconPosition: point(10px, 11px);
historySendSize: size(44px, 46px);
historyScheduleIcon: icon {{ "chat/input_schedule", historyComposeAreaBg }};
historyScheduleIconPosition: point(7px, 8px);
@@ -1365,6 +1397,7 @@ defaultRecordBar: RecordBar {
}
historySend: SendButton {
sendIconPosition: point(10px, 11px);
inner: IconButton(historyAttach) {
icon: historySendIcon;
iconOver: historySendIconOver;
@@ -1420,9 +1453,46 @@ defaultRestrictionLabel: FlatLabel(defaultFlatLabel) {
textFg: placeholderFg;
align: align(top);
}
defaultSendAsButton: SendAsButton {
width: 44px;
height: 46px;
size: 28px;
activeBg: activeButtonBg;
activeFg: activeButtonFg;
cross: CrossAnimation {
size: 28px;
skip: 10px;
stroke: 1.5;
minScale: 0.3;
}
duration: 150;
}
defaultChooseSendAs: ChooseSendAs {
button: defaultSendAsButton;
label: FlatLabel(defaultFlatLabel) {
minWidth: 272px;
}
list: PeerList(peerListBox) {
item: PeerListItem(peerListBoxItem) {
height: 56px;
checkbox: RoundImageCheckbox(defaultPeerListCheckbox) {
check: RoundCheckbox(defaultRoundCheckbox) {
size: 0px;
}
imageRadius: 19px;
imageSmallRadius: 15px;
}
photoSize: 38px;
photoPosition: point(24px, 9px);
namePosition: point(73px, 9px);
statusPosition: point(73px, 28px);
}
}
}
defaultComposeControls: ComposeControls {
bg: historyComposeAreaBg;
radius: 0px;
padding: margins(historySendRight, historySendPadding, historySendRight, historySendPadding);
field: historyComposeField;
send: historySend;
@@ -1437,6 +1507,7 @@ defaultComposeControls: ComposeControls {
premium: defaultPremiumLimits;
boxField: defaultInputField;
restrictionLabel: defaultRestrictionLabel;
chooseSendAs: defaultChooseSendAs;
}
moreChatsBarHeight: 48px;

View File

@@ -13,6 +13,7 @@ struct ComposeFeatures {
bool likes : 1 = false;
bool sendAs : 1 = true;
bool ttlInfo : 1 = true;
bool attachments : 1 = true;
bool botCommandSend : 1 = true;
bool silentBroadcastToggle : 1 = true;
bool attachBotsMenu : 1 = true;
@@ -26,6 +27,10 @@ struct ComposeFeatures {
bool autocompleteCommands : 1 = true;
bool suggestStickersByEmoji : 1 = true;
bool commonTabbedPanel : 1 = true;
bool recordMediaMessage : 1 = true;
bool editMessageStars : 1 = false;
bool emojiOnlyPanel : 1 = false;
bool videoStream : 1 = false;
};
} // namespace ChatHelpers

View File

@@ -169,6 +169,7 @@ Application::Application()
, _langCloudManager(std::make_unique<Lang::CloudManager>(langpack()))
, _emojiKeywords(std::make_unique<ChatHelpers::EmojiKeywords>())
, _tray(std::make_unique<Tray>())
, _setupEmailLock(false)
, _autoLockTimer([=] { checkAutoLock(); }) {
Ui::Integration::Set(&_private->uiIntegration);
@@ -1245,6 +1246,20 @@ rpl::producer<bool> Application::passcodeLockValue() const {
return _passcodeLock.value();
}
void Application::lockBySetupEmail() {
_setupEmailLock = true;
enumerateWindows([&](not_null<Window::Controller*> w) {
w->setupSetupEmailLock();
});
}
void Application::unlockSetupEmail() {
_setupEmailLock = false;
enumerateWindows([&](not_null<Window::Controller*> w) {
w->clearSetupEmailLock();
});
}
bool Application::someSessionExists() const {
for (const auto &[index, account] : _domain->accounts()) {
if (account->sessionExists()) {

View File

@@ -304,6 +304,9 @@ public:
rpl::producer<bool> passcodeLockChanges() const;
rpl::producer<bool> passcodeLockValue() const;
void lockBySetupEmail();
void unlockSetupEmail();
void checkAutoLock(crl::time lastNonIdleTime = 0);
void checkAutoLockIn(crl::time time);
void localPasscodeChanged();
@@ -444,6 +447,7 @@ private:
bool _floatPlayerGifsPaused = false;
rpl::variable<bool> _passcodeLock;
rpl::variable<bool> _setupEmailLock;
bool _screenIsLocked = false;
crl::time _shouldLockAt = 0;

View File

@@ -315,8 +315,8 @@ CloudPasswordState ParseCloudPasswordState(
ParseSecureSecretAlgo(data.vnew_secure_algo()));
result.unconfirmedPattern = qs(
data.vemail_unconfirmed_pattern().value_or_empty());
result.loginEmailPattern = qs(
data.vlogin_email_pattern().value_or_empty());
const auto pattern = qs(data.vlogin_email_pattern().value_or_empty());
result.loginEmailPattern = pattern.contains(' ') ? QString() : pattern;
result.pendingResetDate = data.vpending_reset_date().value_or_empty();
result.outdatedClient = [&] {

View File

@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "core/local_url_handlers.h"
#include "api/api_authorizations.h"
#include "api/api_cloud_password.h"
#include "api/api_confirm_phone.h"
#include "api/api_chat_filters.h"
#include "api/api_chat_invite.h"
@@ -52,6 +53,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "window/window_peer_menu.h"
#include "window/themes/window_theme_editor_box.h" // GenerateSlug.
#include "payments/payments_checkout_process.h"
#include "settings/cloud_password/settings_cloud_password_login_email.h"
#include "settings/settings_active_sessions.h"
#include "settings/settings_credits.h"
#include "settings/settings_credits_graphics.h"
@@ -497,6 +499,33 @@ bool ShowWallPaper(
return result;
}
void LoginEmailBox(
not_null<Ui::GenericBox*> box,
rpl::producer<QString> email,
Fn<void()> callback) {
{
box->getDelegate()->setTitle(rpl::duplicate(
email
) | rpl::map(Ui::Text::WrapEmailPattern));
for (const auto &child : ranges::views::reverse(
box->parentWidget()->children())) {
if (child && child->isWidgetType()) {
(static_cast<QWidget*>(child))->setAttribute(
Qt::WA_TransparentForMouseEvents);
break;
}
}
}
Ui::ConfirmBox(box, Ui::ConfirmBoxArgs{
.text = tr::lng_settings_cloud_login_email_box_about(),
.confirmed = [=](Fn<void()> close) {
callback();
close();
},
.confirmText = tr::lng_settings_cloud_login_email_box_ok(),
});
}
[[nodiscard]] ChatAdminRights ParseRequestedAdminRights(
const QString &value) {
auto result = ChatAdminRights();
@@ -633,7 +662,6 @@ bool ResolveUsernameOrPhone(
post = postId;
}
const auto storyParam = params.value(u"story"_q);
const auto storyId = storyParam.toInt();
const auto storyAlbumParam = params.value(u"album"_q);
const auto storyAlbumId = storyAlbumParam.toInt();
const auto giftCollectionParam = params.value(u"collection"_q);
@@ -665,7 +693,7 @@ bool ResolveUsernameOrPhone(
.usernameOrId = domain,
.phone = phone,
.messageId = post,
.storyId = storyId,
.storyParam = storyParam,
.storyAlbumId = storyAlbumId,
.giftCollectionId = giftCollectionId,
.videoTimestamp = (!videot.isEmpty()
@@ -759,6 +787,21 @@ bool ResolvePrivatePost(
return true;
}
void ShowLoginEmailSettings(Window::SessionController *controller) {
controller->session().api().cloudPassword().reload();
controller->uiShow()->show(Box(
LoginEmailBox,
controller->session().api().cloudPassword().state(
) | rpl::map([](const Core::CloudPasswordState &state) {
return state.loginEmailPattern;
}),
[=] {
controller->showSettings(
::Settings::CloudLoginEmailId());
controller->window().activate();
}));
}
bool ResolveSettings(
Window::SessionController *controller,
const Match &match,
@@ -787,6 +830,9 @@ bool ResolveSettings(
return ::Settings::GlobalTTLId();
} else if (section == u"information"_q) {
return ::Settings::Information::Id();
} else if (section == u"login_email"_q) {
ShowLoginEmailSettings(controller);
return {};
}
return ::Settings::Main::Id();
}();
@@ -1587,6 +1633,21 @@ bool ResolveUniqueGift(
return true;
}
bool ResolveGiftAuction(
Window::SessionController *controller,
const Match &match,
const QVariant &context) {
if (!controller) {
return false;
}
const auto slug = match->captured(1);
if (slug.isEmpty()) {
return false;
}
controller->showStarGiftAuction(slug);
return true;
}
bool ResolveConferenceCall(
Window::SessionController *controller,
const Match &match,
@@ -1685,7 +1746,7 @@ const std::vector<LocalUrlHandler> &LocalUrlHandlers() {
ResolvePrivatePost
},
{
u"^settings(/language|/devices|/folders|/privacy|/themes|/change_number|/auto_delete|/information|/edit_profile|/phone_privacy)?$"_q,
u"^settings(/language|/devices|/folders|/privacy|/themes|/change_number|/auto_delete|/information|/edit_profile|/phone_privacy|/login_email)?$"_q,
ResolveSettings
},
{
@@ -1724,6 +1785,10 @@ const std::vector<LocalUrlHandler> &LocalUrlHandlers() {
u"^nft/?\\?slug=([a-zA-Z0-9\\.\\_\\-]+)(&|$)"_q,
ResolveUniqueGift
},
{
u"^stargift_auction/?\\?slug=([a-zA-Z0-9\\.\\_\\-]+)(&|$)"_q,
ResolveGiftAuction
},
{
u"^call/?\\?slug=([a-zA-Z0-9\\.\\_\\-]+)(&|$)"_q,
ResolveConferenceCall
@@ -1896,6 +1961,9 @@ QString TryConvertUrlToLocal(QString url) {
} else if (const auto nftMatch = regex_match(u"^nft/([a-zA-Z0-9\\.\\_\\-]+)(\\?|$)"_q, query, matchOptions)) {
const auto slug = nftMatch->captured(1);
return u"tg://nft?slug="_q + slug;
} else if (const auto auctionMatch = regex_match(u"^auction/([a-zA-Z0-9\\.\\_\\-]+)(\\?|$)"_q, query, matchOptions)) {
const auto slug = auctionMatch->captured(1);
return u"tg://stargift_auction?slug="_q + slug;
} else if (const auto callMatch = regex_match(u"^call/([a-zA-Z0-9\\.\\_\\-]+)(\\?|$)"_q, query, matchOptions)) {
const auto slug = callMatch->captured(1);
return u"tg://call?slug="_q + slug;
@@ -1928,7 +1996,7 @@ QString TryConvertUrlToLocal(QString url) {
"/?$|"
"/[a-zA-Z0-9\\.\\_\\-]+/?(\\?|$)|"
"/\\d+/?(\\?|$)|"
"/s/\\d+/?(\\?|$)|"
"/s/(\\d+|live)/?(\\?|$)|"
"/a/\\d+/?(\\?|$)|"
"/c/\\d+/?(\\?|$)|"
"/\\d+/\\d+/?(\\?|$)"
@@ -1951,7 +2019,7 @@ QString TryConvertUrlToLocal(QString url) {
added = u"&topic=%1&post=%2"_q.arg(threadPostMatch->captured(1), threadPostMatch->captured(2));
} else if (const auto postMatch = regex_match(u"^/(\\d+)(/?\\?|/?$)"_q, usernameMatch->captured(2))) {
added = u"&post="_q + postMatch->captured(1);
} else if (const auto storyMatch = regex_match(u"^/s/(\\d+)(/?\\?|/?$)"_q, usernameMatch->captured(2))) {
} else if (const auto storyMatch = regex_match(u"^/s/(\\d+|live)(/?\\?|/?$)"_q, usernameMatch->captured(2))) {
added = u"&story="_q + storyMatch->captured(1);
} else if (const auto albumMatch = regex_match(u"^/a/(\\d+)(/?\\?|/?$)"_q, usernameMatch->captured(2))) {
added = u"&album="_q + albumMatch->captured(1);

View File

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

View File

@@ -96,7 +96,8 @@ constexpr auto kRequestTimeLimit = 60 * crl::time(1000);
MTP_long(data.vpaid_message_stars().value_or_empty()),
(data.vsuggested_post()
? *data.vsuggested_post()
: MTPSuggestedPost()));
: MTPSuggestedPost()),
MTP_int(data.vschedule_repeat_period().value_or_empty()));
});
}

View File

@@ -0,0 +1,241 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "data/components/gift_auctions.h"
#include "api/api_premium.h"
#include "api/api_text_entities.h"
#include "apiwrap.h"
#include "data/data_session.h"
#include "main/main_session.h"
namespace Data {
GiftAuctions::GiftAuctions(not_null<Main::Session*> session)
: _session(session)
, _timer([=] { checkSubscriptions(); }) {
}
GiftAuctions::~GiftAuctions() = default;
rpl::producer<GiftAuctionState> GiftAuctions::state(const QString &slug) {
return [=](auto consumer) {
auto lifetime = rpl::lifetime();
auto &entry = _map[slug];
if (!entry) {
entry = std::make_unique<Entry>();
}
const auto raw = entry.get();
raw->changes.events() | rpl::start_with_next([=] {
consumer.put_next_copy(raw->state);
}, lifetime);
const auto now = crl::now();
if (raw->state.subscribedTill < 0
|| raw->state.subscribedTill >= now) {
consumer.put_next_copy(raw->state);
} else if (raw->state.subscribedTill >= 0) {
request(slug);
}
return lifetime;
};
}
void GiftAuctions::apply(const MTPDupdateStarGiftAuctionState &data) {
if (const auto entry = find(data.vgift_id().v)) {
apply(entry, data.vstate());
}
}
void GiftAuctions::apply(const MTPDupdateStarGiftAuctionUserState &data) {
if (const auto entry = find(data.vgift_id().v)) {
apply(entry, data.vuser_state());
}
}
void GiftAuctions::requestAcquired(
uint64 giftId,
Fn<void(std::vector<Data::GiftAcquired>)> done) {
Expects(done != nullptr);
_session->api().request(MTPpayments_GetStarGiftAuctionAcquiredGifts(
MTP_long(giftId)
)).done([=](const MTPpayments_StarGiftAuctionAcquiredGifts &result) {
const auto &data = result.data();
const auto owner = &_session->data();
owner->processUsers(data.vusers());
owner->processChats(data.vchats());
const auto &list = data.vgifts().v;
auto gifts = std::vector<Data::GiftAcquired>();
gifts.reserve(list.size());
for (const auto &gift : list) {
const auto &data = gift.data();
gifts.push_back({
.to = owner->peer(peerFromMTP(data.vpeer())),
.message = (data.vmessage()
? Api::ParseTextWithEntities(_session, *data.vmessage())
: TextWithEntities()),
.date = data.vdate().v,
.bidAmount = int64(data.vbid_amount().v),
.round = data.vround().v,
.position = data.vpos().v,
.nameHidden = data.is_name_hidden(),
});
}
if (const auto entry = find(giftId)) {
const auto count = int(gifts.size());
if (entry->state.my.gotCount != count) {
entry->state.my.gotCount = count;
entry->changes.fire({});
}
}
done(std::move(gifts));
}).fail([=] {
done({});
}).send();
}
void GiftAuctions::checkSubscriptions() {
const auto now = crl::now();
auto next = crl::time();
for (const auto &[slug, entry] : _map) {
const auto raw = entry.get();
const auto till = raw->state.subscribedTill;
if (till <= 0 || !raw->changes.has_consumers()) {
continue;
} else if (till <= now) {
request(slug);
} else {
const auto timeout = till - now;
if (!next || timeout < next) {
next = timeout;
}
}
}
if (next) {
_timer.callOnce(next);
}
}
void GiftAuctions::request(const QString &slug) {
auto &entry = _map[slug];
Assert(entry != nullptr);
const auto raw = entry.get();
if (raw->requested) {
return;
}
raw->requested = true;
_session->api().request(MTPpayments_GetStarGiftAuctionState(
MTP_inputStarGiftAuctionSlug(MTP_string(slug)),
MTP_int(raw->state.version)
)).done([=](const MTPpayments_StarGiftAuctionState &result) {
raw->requested = false;
const auto &data = result.data();
raw->state.gift = Api::FromTL(_session, data.vgift());
if (!raw->state.gift) {
return;
}
const auto timeout = data.vtimeout().v;
const auto ms = timeout * crl::time(1000);
raw->state.subscribedTill = ms ? (crl::now() + ms) : -1;
_session->data().processUsers(data.vusers());
apply(raw, data.vstate());
apply(raw, data.vuser_state());
if (raw->changes.has_consumers()) {
raw->changes.fire({});
if (ms && (!_timer.isActive() || _timer.remainingTime() > ms)) {
_timer.callOnce(ms);
}
}
}).send();
}
GiftAuctions::Entry *GiftAuctions::find(uint64 giftId) const {
for (const auto &[slug, entry] : _map) {
if (entry->state.gift && entry->state.gift->id == giftId) {
return entry.get();
}
}
return nullptr;
}
void GiftAuctions::apply(
not_null<Entry*> entry,
const MTPStarGiftAuctionState &state) {
Expects(entry->state.gift.has_value());
const auto raw = &entry->state;
state.match([&](const MTPDstarGiftAuctionState &data) {
const auto version = data.vversion().v;
if (raw->version >= version) {
return;
}
const auto owner = &_session->data();
raw->startDate = data.vstart_date().v;
raw->endDate = data.vend_date().v;
raw->minBidAmount = data.vmin_bid_amount().v;
const auto &levels = data.vbid_levels().v;
raw->bidLevels.clear();
raw->bidLevels.reserve(levels.size());
for (const auto &level : levels) {
auto &entry = raw->bidLevels.emplace_back();
const auto &data = level.data();
entry.amount = data.vamount().v;
entry.position = data.vpos().v;
entry.date = data.vdate().v;
}
const auto &top = data.vtop_bidders().v;
raw->topBidders.clear();
raw->topBidders.reserve(top.size());
for (const auto &user : top) {
raw->topBidders.push_back(owner->user(UserId(user.v)));
}
raw->nextRoundAt = data.vnext_round_at().v;
raw->giftsLeft = data.vgifts_left().v;
raw->currentRound = data.vcurrent_round().v;
raw->totalRounds = data.vtotal_rounds().v;
raw->averagePrice = 0;
}, [&](const MTPDstarGiftAuctionStateFinished &data) {
raw->averagePrice = data.vaverage_price().v;
raw->startDate = data.vstart_date().v;
raw->endDate = data.vend_date().v;
raw->minBidAmount = 0;
raw->nextRoundAt
= raw->currentRound
= raw->totalRounds
= raw->giftsLeft
= raw->version
= 0;
}, [&](const MTPDstarGiftAuctionStateNotModified &data) {
});
}
void GiftAuctions::apply(
not_null<Entry*> entry,
const MTPStarGiftAuctionUserState &state) {
const auto &data = state.data();
const auto raw = &entry->state.my;
raw->to = data.vbid_peer()
? _session->data().peer(peerFromMTP(*data.vbid_peer())).get()
: nullptr;
raw->minBidAmount = data.vmin_bid_amount().value_or(0);
raw->bid = data.vbid_amount().value_or(0);
raw->date = data.vbid_date().value_or(0);
raw->gotCount = data.vacquired_count().v;
raw->returned = data.is_returned();
}
} // namespace Data

View File

@@ -0,0 +1,103 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/timer.h"
#include "data/data_star_gift.h"
namespace Main {
class Session;
} // namespace Main
namespace Data {
struct GiftAuctionBidLevel {
int64 amount = 0;
int position = 0;
TimeId date = 0;
};
struct StarGiftAuctionMyState {
PeerData *to = nullptr;
int64 minBidAmount = 0;
int64 bid = 0;
TimeId date = 0;
int gotCount = 0;
bool returned = false;
};
struct GiftAuctionState {
std::optional<StarGift> gift;
StarGiftAuctionMyState my;
std::vector<GiftAuctionBidLevel> bidLevels;
std::vector<not_null<UserData*>> topBidders;
crl::time subscribedTill = 0;
int64 minBidAmount = 0;
int64 averagePrice = 0;
TimeId startDate = 0;
TimeId endDate = 0;
TimeId nextRoundAt = 0;
int currentRound = 0;
int totalRounds = 0;
int giftsLeft = 0;
int version = 0;
[[nodiscard]] bool finished() const {
return (averagePrice != 0);
}
};
struct GiftAcquired {
not_null<PeerData*> to;
TextWithEntities message;
TimeId date = 0;
int64 bidAmount = 0;
int round = 0;
int position = 0;
bool nameHidden = false;
};
class GiftAuctions final {
public:
explicit GiftAuctions(not_null<Main::Session*> session);
~GiftAuctions();
[[nodiscard]] rpl::producer<GiftAuctionState> state(const QString &slug);
void apply(const MTPDupdateStarGiftAuctionState &data);
void apply(const MTPDupdateStarGiftAuctionUserState &data);
void requestAcquired(
uint64 giftId,
Fn<void(std::vector<Data::GiftAcquired>)> done);
private:
struct Entry {
GiftAuctionState state;
rpl::event_stream<> changes;
bool requested = false;
};
void request(const QString &slug);
Entry *find(uint64 giftId) const;
void apply(
not_null<Entry*> entry,
const MTPStarGiftAuctionState &state);
void apply(
not_null<Entry*> entry,
const MTPStarGiftAuctionUserState &state);
void checkSubscriptions();
const not_null<Main::Session*> _session;
base::Timer _timer;
base::flat_map<QString, std::unique_ptr<Entry>> _map;
};
} // namespace Data

View File

@@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_user.h"
#include "history/history.h"
#include "main/main_session.h"
#include "main/main_session_settings.h"
namespace Data {
namespace {
@@ -42,9 +43,12 @@ constexpr auto kTopPromotionMinDelay = TimeId(10);
} // namespace
PromoSuggestions::PromoSuggestions(not_null<Main::Session*> session)
PromoSuggestions::PromoSuggestions(
not_null<Main::Session*> session,
Fn<void()> firstPromoLoaded)
: _session(session)
, _topPromotionTimer([=] { refreshTopPromotion(); }) {
, _topPromotionTimer([=] { refreshTopPromotion(); })
, _firstPromoLoaded(std::move(firstPromoLoaded)) {
Core::App().settings().proxy().connectionTypeValue(
) | rpl::start_with_next([=] {
refreshTopPromotion();
@@ -100,6 +104,16 @@ void PromoSuggestions::refreshTopPromotion() {
) | ranges::views::transform([](const auto &suggestion) {
return qs(suggestion);
}) | ranges::to_vector;
for (const auto &suggestion : pendingSuggestions) {
if (suggestion == u"SETUP_LOGIN_EMAIL_NOSKIP"_q) {
_setupEmailState = SetupEmailState::SetupNoSkip;
break;
}
if (suggestion == u"SETUP_LOGIN_EMAIL"_q) {
_setupEmailState = SetupEmailState::Setup;
break;
}
}
if (!ranges::equal(_pendingSuggestions, pendingSuggestions)) {
_pendingSuggestions = std::move(pendingSuggestions);
changedPendingSuggestions = true;
@@ -146,6 +160,9 @@ void PromoSuggestions::refreshTopPromotion() {
_refreshed.fire({});
}
});
if (_firstPromoLoaded) {
base::take(_firstPromoLoaded)();
}
}).fail([=] {
_topPromotionRequestId = 0;
const auto now = base::unixtime::now();
@@ -228,6 +245,24 @@ void PromoSuggestions::dismiss(const QString &key) {
)).send();
}
void PromoSuggestions::dismissSetupEmail(Fn<void()> done) {
auto key = QString();
if (_setupEmailState == SetupEmailState::SettingUp) {
key = u"SETUP_LOGIN_EMAIL"_q;
} else if (_setupEmailState == SetupEmailState::SettingUpNoSkip) {
key = u"SETUP_LOGIN_EMAIL_NOSKIP"_q;
} else {
return;
}
_session->api().request(MTPhelp_DismissSuggestion(
MTP_inputPeerEmpty(),
MTP_string(key)
)).done([=](const MTPBool &) {
_setupEmailState = SetupEmailState::None;
done();
}).send();
}
void PromoSuggestions::invalidate() {
if (_topPromotionRequestId) {
_session->api().request(_topPromotionRequestId).cancel();
@@ -311,4 +346,19 @@ QString PromoSuggestions::SugValidatePassword() {
return key;
}
void PromoSuggestions::setSetupEmailState(SetupEmailState state) {
if (_setupEmailState != state) {
_setupEmailState = state;
_setupEmailStateChanges.fire_copy(state);
}
}
SetupEmailState PromoSuggestions::setupEmailState() const {
return _setupEmailState;
}
rpl::producer<SetupEmailState> PromoSuggestions::setupEmailStateValue() const {
return _setupEmailStateChanges.events_starting_with_copy(_setupEmailState);
}
} // namespace Data

View File

@@ -17,6 +17,14 @@ class Session;
namespace Data {
enum class SetupEmailState {
None,
Setup,
SetupNoSkip,
SettingUp,
SettingUpNoSkip,
};
struct CustomSuggestion final {
QString suggestion;
TextWithEntities title;
@@ -30,13 +38,16 @@ struct CustomSuggestion final {
class PromoSuggestions final {
public:
explicit PromoSuggestions(not_null<Main::Session*> session);
explicit PromoSuggestions(
not_null<Main::Session*> session,
Fn<void()> firstPromoLoaded = nullptr);
~PromoSuggestions();
[[nodiscard]] bool current(const QString &key) const;
[[nodiscard]] std::optional<CustomSuggestion> custom() const;
[[nodiscard]] rpl::producer<> requested(const QString &key) const;
void dismiss(const QString &key);
void dismissSetupEmail(Fn<void()> done);
void refreshTopPromotion();
@@ -53,6 +64,10 @@ public:
[[nodiscard]] static QString SugValidatePassword();
void setSetupEmailState(SetupEmailState state);
[[nodiscard]] SetupEmailState setupEmailState() const;
[[nodiscard]] rpl::producer<SetupEmailState> setupEmailStateValue() const;
private:
void setTopPromoted(
History *promoted,
@@ -78,7 +93,12 @@ private:
TimeId _topPromotionNextRequestTime = TimeId(0);
base::Timer _topPromotionTimer;
SetupEmailState _setupEmailState = SetupEmailState::None;
rpl::event_stream<> _refreshed;
rpl::event_stream<SetupEmailState> _setupEmailStateChanges;
Fn<void()> _firstPromoLoaded;
rpl::lifetime _lifetime;

View File

@@ -115,6 +115,40 @@ void RecentSharedMediaGifts::clearLastRequestTime(
}
}
void RecentSharedMediaGifts::updatePinnedOrder(
std::shared_ptr<ChatHelpers::Show> show,
not_null<PeerData*> peer,
const std::vector<SavedStarGift> &gifts,
const std::vector<Data::SavedStarGiftId> &manageIds,
Fn<void()> done) {
auto inputs = QVector<MTPInputSavedStarGift>();
inputs.reserve(manageIds.size());
for (const auto &id : manageIds) {
inputs.push_back(Api::InputSavedStarGiftId(id));
}
_session->api().request(MTPpayments_ToggleStarGiftsPinnedToTop(
peer->input,
MTP_vector<MTPInputSavedStarGift>(std::move(inputs))
)).done([=] {
auto result = std::deque<SavedStarGift>();
for (const auto &id : manageIds) {
for (const auto &gift : gifts) {
if (gift.manageId == id) {
result.push_back(gift);
break;
}
}
}
_recent[peer->id].gifts = std::move(result);
if (done) {
done();
}
}).fail([=](const MTP::Error &error) {
show->showToast(error.type());
}).send();
}
void RecentSharedMediaGifts::togglePinned(
std::shared_ptr<ChatHelpers::Show> show,
not_null<PeerData*> peer,
@@ -127,15 +161,15 @@ void RecentSharedMediaGifts::togglePinned(
auto manageIds = std::vector<Data::SavedStarGiftId>();
if (pinned) {
manageIds.push_back(manageId);
for (const auto &gift : gifts) {
if (gift.pinned && gift.manageId != manageId) {
manageIds.push_back(gift.manageId);
if (manageIds.size() >= limit) {
if (manageIds.size() >= limit - 1) {
break;
}
}
}
manageIds.push_back(manageId);
} else {
for (const auto &gift : gifts) {
if (gift.pinned && gift.manageId != manageId) {
@@ -144,17 +178,7 @@ void RecentSharedMediaGifts::togglePinned(
}
}
auto inputs = QVector<MTPInputSavedStarGift>();
inputs.reserve(manageIds.size());
for (const auto &id : manageIds) {
inputs.push_back(Api::InputSavedStarGiftId(id));
}
_session->api().request(MTPpayments_ToggleStarGiftsPinnedToTop(
peer->input,
MTP_vector<MTPInputSavedStarGift>(std::move(inputs))
)).done([=] {
const auto updateLocal = [=] {
const auto updateLocal = [=] {
using GiftAction = Data::GiftUpdate::Action;
_session->data().notifyGiftUpdate({
.id = manageId,
@@ -185,17 +209,7 @@ void RecentSharedMediaGifts::togglePinned(
};
if (!pinned) {
auto result = std::deque<SavedStarGift>();
for (const auto &id : manageIds) {
for (const auto &gift : gifts) {
if (gift.manageId == id) {
result.push_back(gift);
break;
}
}
}
_recent[peer->id].gifts = std::move(result);
updateLocal();
updatePinnedOrder(show, peer, gifts, manageIds, updateLocal);
} else {
_session->api().request(MTPpayments_GetSavedStarGift(
MTP_vector<MTPInputSavedStarGift>(
@@ -204,31 +218,51 @@ void RecentSharedMediaGifts::togglePinned(
)).done([=](const MTPpayments_SavedStarGifts &result) {
const auto &tlGift = result.data().vgifts().v.front();
if (auto parsed = Api::FromTL(peer, tlGift)) {
auto result = std::deque<SavedStarGift>();
for (const auto &id : manageIds) {
if (parsed->manageId == id) {
parsed->pinned = true;
result.push_back(*parsed);
continue;
}
for (const auto &gift : gifts) {
if (gift.manageId == id) {
result.push_back(gift);
break;
}
auto updatedGifts = std::vector<SavedStarGift>();
for (const auto &gift : gifts) {
if (gift.pinned && gift.manageId != manageId) {
updatedGifts.push_back(gift);
}
}
_recent[peer->id].gifts = std::move(result);
updateLocal();
parsed->pinned = true;
updatedGifts.push_back(*parsed);
updatePinnedOrder(
show,
peer,
updatedGifts,
manageIds,
updateLocal);
}
}).send();
}
}).fail([=](const MTP::Error &error) {
show->showToast(error.type());
}).send();
};
request(peer, performToggle, true);
}
void RecentSharedMediaGifts::reorderPinned(
std::shared_ptr<ChatHelpers::Show> show,
not_null<PeerData*> peer,
int oldPosition,
int newPosition) {
const auto performReorder = [=](const std::vector<SavedStarGift> &gifts) {
if (oldPosition < 0 || oldPosition >= gifts.size()
|| newPosition < 0 || newPosition >= gifts.size()
|| oldPosition == newPosition) {
return;
}
auto manageIds = std::vector<Data::SavedStarGiftId>();
manageIds.reserve(gifts.size());
for (const auto &gift : gifts) {
manageIds.push_back(gift.manageId);
}
base::reorder(manageIds, oldPosition, newPosition);
updatePinnedOrder(show, peer, gifts, manageIds, nullptr);
};
request(peer, performReorder, true);
}
} // namespace Data

View File

@@ -39,7 +39,20 @@ public:
std::shared_ptr<Data::UniqueGift> uniqueData,
std::shared_ptr<Data::UniqueGift> replacingData = nullptr);
void reorderPinned(
std::shared_ptr<ChatHelpers::Show> show,
not_null<PeerData*> peer,
int oldPosition,
int newPosition);
private:
void updatePinnedOrder(
std::shared_ptr<ChatHelpers::Show> show,
not_null<PeerData*> peer,
const std::vector<SavedStarGift> &gifts,
const std::vector<Data::SavedStarGiftId> &manageIds,
Fn<void()> done);
[[nodiscard]] std::vector<Data::SavedStarGift> filterGifts(
const std::deque<SavedStarGift> &gifts,
bool onlyPinnedToTop);

View File

@@ -100,7 +100,8 @@ constexpr auto kRequestTimeLimit = 60 * crl::time(1000);
MTP_long(data.vpaid_message_stars().value_or_empty()),
(data.vsuggested_post()
? *data.vsuggested_post()
: MTPSuggestedPost()));
: MTPSuggestedPost()),
MTP_int(data.vschedule_repeat_period().value_or_empty()));
});
}
@@ -276,7 +277,8 @@ void ScheduledMessages::sendNowSimpleMessage(
MTPFactCheck(),
MTPint(), // report_delivery_until_date
MTPlong(), // paid_message_stars
MTPSuggestedPost()),
MTPSuggestedPost(),
MTPint()), // schedule_repeat_period
localFlags,
NewMessageType::Unread);

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