diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index de5b7ab5aa..adfedd45c6 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -482,8 +482,14 @@ 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_preview_box.cpp + boxes/star_gift_preview_box.h + boxes/star_gift_resale_box.cpp + boxes/star_gift_resale_box.h boxes/sticker_set_box.cpp boxes/sticker_set_box.h boxes/stickers_box.cpp @@ -526,6 +532,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 @@ -662,8 +670,12 @@ 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/passkeys.cpp + data/components/passkeys.h data/components/promo_suggestions.cpp data/components/promo_suggestions.h data/components/recent_peers.cpp @@ -1193,6 +1205,8 @@ PRIVATE info/polls/info_polls_results_widget.h info/profile/info_profile_actions.cpp info/profile/info_profile_actions.h + info/profile/info_profile_badge_tooltip.cpp + info/profile/info_profile_badge_tooltip.h info/profile/info_profile_badge.cpp info/profile/info_profile_badge.h info/profile/info_profile_cover.cpp @@ -1207,6 +1221,10 @@ PRIVATE info/profile/info_profile_members_controllers.h info/profile/info_profile_phone_menu.cpp info/profile/info_profile_phone_menu.h + info/profile/info_profile_status_label.cpp + info/profile/info_profile_status_label.h + info/profile/info_profile_top_bar.cpp + info/profile/info_profile_top_bar.h info/profile/info_profile_values.cpp info/profile/info_profile_values.h info/profile/info_profile_widget.cpp @@ -1215,6 +1233,7 @@ PRIVATE info/reactions_list/info_reactions_list_widget.h info/requests_list/info_requests_list_widget.cpp info/requests_list/info_requests_list_widget.h + info/saved/info_saved_music_common.cpp info/saved/info_saved_music_common.h info/saved/info_saved_music_provider.cpp info/saved/info_saved_music_provider.h @@ -1442,6 +1461,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 @@ -1531,6 +1552,7 @@ PRIVATE platform/linux/specific_linux.h platform/linux/tray_linux.cpp platform/linux/tray_linux.h + platform/linux/webauthn_linux.cpp platform/mac/file_utilities_mac.mm platform/mac/file_utilities_mac.h platform/mac/launcher_mac.mm @@ -1550,6 +1572,7 @@ PRIVATE platform/mac/specific_mac_p.h platform/mac/tray_mac.mm platform/mac/tray_mac.h + platform/mac/webauthn_mac.mm platform/mac/window_title_mac.mm platform/mac/touchbar/items/mac_formatter_item.h platform/mac/touchbar/items/mac_formatter_item.mm @@ -1584,6 +1607,7 @@ PRIVATE platform/win/specific_win.h platform/win/tray_win.cpp platform/win/tray_win.h + platform/win/webauthn_win.cpp platform/win/windows_app_user_model_id.cpp platform/win/windows_app_user_model_id.h platform/win/windows_dlls.cpp @@ -1602,6 +1626,7 @@ PRIVATE platform/platform_overlay_widget.h platform/platform_specific.h platform/platform_tray.h + platform/platform_webauthn.h platform/platform_window_title.h profile/profile_back_button.cpp profile/profile_back_button.h @@ -1687,6 +1712,8 @@ PRIVATE settings/settings_notifications.h settings/settings_notifications_type.cpp settings/settings_notifications_type.h + settings/settings_passkeys.cpp + settings/settings_passkeys.h settings/settings_power_saving.cpp settings/settings_power_saving.h settings/settings_premium.cpp @@ -1776,6 +1803,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 @@ -1814,8 +1843,12 @@ PRIVATE ui/item_text_options.cpp ui/item_text_options.h ui/resize_area.h + ui/top_background_gradient.cpp + ui/top_background_gradient.h ui/unread_badge.cpp ui/unread_badge.h + ui/peer/video_userpic_player.cpp + ui/peer/video_userpic_player.h window/main_window.cpp window/main_window.h window/notifications_manager.cpp @@ -1857,6 +1890,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 diff --git a/Telegram/Resources/animations/my_gifts_empty.tgs b/Telegram/Resources/animations/my_gifts_empty.tgs new file mode 100644 index 0000000000..85393b18fe Binary files /dev/null and b/Telegram/Resources/animations/my_gifts_empty.tgs differ diff --git a/Telegram/Resources/animations/passkeys.tgs b/Telegram/Resources/animations/passkeys.tgs new file mode 100644 index 0000000000..62e11b2540 Binary files /dev/null and b/Telegram/Resources/animations/passkeys.tgs differ diff --git a/Telegram/Resources/animations/profile/profile_muting.tgs b/Telegram/Resources/animations/profile/profile_muting.tgs new file mode 100644 index 0000000000..bdae1b5f90 Binary files /dev/null and b/Telegram/Resources/animations/profile/profile_muting.tgs differ diff --git a/Telegram/Resources/animations/profile/profile_unmuting.tgs b/Telegram/Resources/animations/profile/profile_unmuting.tgs new file mode 100644 index 0000000000..a1dd834d6c Binary files /dev/null and b/Telegram/Resources/animations/profile/profile_unmuting.tgs differ diff --git a/Telegram/Resources/animations/rtmp.tgs b/Telegram/Resources/animations/rtmp.tgs new file mode 100644 index 0000000000..53917fd1a0 Binary files /dev/null and b/Telegram/Resources/animations/rtmp.tgs differ diff --git a/Telegram/Resources/animations/show_or_premium_lastseen.tgs b/Telegram/Resources/animations/show_or_premium_lastseen.tgs new file mode 100644 index 0000000000..1048f5806e Binary files /dev/null and b/Telegram/Resources/animations/show_or_premium_lastseen.tgs differ diff --git a/Telegram/Resources/animations/show_or_premium_readtime.tgs b/Telegram/Resources/animations/show_or_premium_readtime.tgs new file mode 100644 index 0000000000..ac3ea3f758 Binary files /dev/null and b/Telegram/Resources/animations/show_or_premium_readtime.tgs differ diff --git a/Telegram/Resources/animations/toast/chats_filter_in.tgs b/Telegram/Resources/animations/toast/chats_filter_in.tgs new file mode 100644 index 0000000000..aba04090d0 Binary files /dev/null and b/Telegram/Resources/animations/toast/chats_filter_in.tgs differ diff --git a/Telegram/Resources/emoji/emoji_1.webp b/Telegram/Resources/emoji/emoji_1.webp index 4669e60222..a46934a411 100644 Binary files a/Telegram/Resources/emoji/emoji_1.webp and b/Telegram/Resources/emoji/emoji_1.webp differ diff --git a/Telegram/Resources/emoji/emoji_2.webp b/Telegram/Resources/emoji/emoji_2.webp index 0c0d7c2956..1ed5db43fb 100644 Binary files a/Telegram/Resources/emoji/emoji_2.webp and b/Telegram/Resources/emoji/emoji_2.webp differ diff --git a/Telegram/Resources/emoji/emoji_3.webp b/Telegram/Resources/emoji/emoji_3.webp index 4d874b37bf..f31b74cbe7 100644 Binary files a/Telegram/Resources/emoji/emoji_3.webp and b/Telegram/Resources/emoji/emoji_3.webp differ diff --git a/Telegram/Resources/emoji/emoji_4.webp b/Telegram/Resources/emoji/emoji_4.webp index bc4eead449..53f65c27a0 100644 Binary files a/Telegram/Resources/emoji/emoji_4.webp and b/Telegram/Resources/emoji/emoji_4.webp differ diff --git a/Telegram/Resources/emoji/emoji_5.webp b/Telegram/Resources/emoji/emoji_5.webp index 9a4e9afd6d..dda7edca30 100644 Binary files a/Telegram/Resources/emoji/emoji_5.webp and b/Telegram/Resources/emoji/emoji_5.webp differ diff --git a/Telegram/Resources/emoji/emoji_6.webp b/Telegram/Resources/emoji/emoji_6.webp index 7d5842d8e6..d8dbd4774c 100644 Binary files a/Telegram/Resources/emoji/emoji_6.webp and b/Telegram/Resources/emoji/emoji_6.webp differ diff --git a/Telegram/Resources/emoji/emoji_7.webp b/Telegram/Resources/emoji/emoji_7.webp index b324a3e832..13e3a97437 100644 Binary files a/Telegram/Resources/emoji/emoji_7.webp and b/Telegram/Resources/emoji/emoji_7.webp differ diff --git a/Telegram/Resources/emoji/emoji_8.webp b/Telegram/Resources/emoji/emoji_8.webp index 5ac85a8a71..585d8c7182 100644 Binary files a/Telegram/Resources/emoji/emoji_8.webp and b/Telegram/Resources/emoji/emoji_8.webp differ diff --git a/Telegram/Resources/export_html/css/style.css b/Telegram/Resources/export_html/css/style.css index f9727d68ac..3189390b4c 100644 --- a/Telegram/Resources/export_html/css/style.css +++ b/Telegram/Resources/export_html/css/style.css @@ -440,6 +440,9 @@ div.toast_shown { .section.stories { background-image: url(../images/section_stories.png); } +.section.music { + background-image: url(../images/section_music.png); +} .section.web { background-image: url(../images/section_web.png); } @@ -481,6 +484,16 @@ div.toast_shown { .media_video .fill { background-image: url(../images/media_video.png) } +.audio_icon { + width: 48px; + height: 48px; + border-radius: 50%; + background-color: #4f9cd9; + background-image: url(../images/media_music.png); + background-repeat: no-repeat; + background-position: 12px 12px; + background-size: 24px 24px; +} @media only screen and (min--moz-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min-device-pixel-ratio: 2) { .section.calls { @@ -504,6 +517,9 @@ div.toast_shown { .section.stories { background-image: url(../images/section_stories@2x.png); } +.section.music { + background-image: url(../images/section_music@2x.png); +} .section.web { background-image: url(../images/section_web@2x.png); } @@ -545,6 +561,9 @@ div.toast_shown { .media_video .fill { background-image: url(../images/media_video@2x.png) } +.audio_icon { + background-image: url(../images/media_music@2x.png); +} } .spoiler { @@ -633,4 +652,101 @@ div.toast_shown { .reactions .reaction .count { margin-right: 8px; line-height: 20px; +} + +@media (prefers-color-scheme: dark) { +html, body { + background-color: #1a2026; /* groupCallBg */ + margin: 0; + padding: 0; +} +.page_wrap { + background-color: #1a2026; /* groupCallBg */ + color: #ffffff; /* groupCallMembersFg */ + min-height: 100vh; +} +.page_wrap a { + color: #4db8ff; /* groupCallActiveFg */ +} +.page_header { + background-color: #1a2026; /* groupCallBg */ + border-bottom: 1px solid #2c333d; /* groupCallMembersBg */ +} +.bold { + color: #ffffff; /* groupCallMembersFg */ +} +.details { + color: #91979e; /* groupCallMemberNotJoinedStatus */ +} +.page_body { + background-color: #1a2026; /* groupCallBg */ +} +code { + color: #ff8aac; /* historyPeer6UserpicBg */ + background-color: #2c333d; /* groupCallMembersBg */ +} +pre { + color: #ffffff; /* groupCallMembersFg */ + background-color: #2c333d; /* groupCallMembersBg */ + border: 1px solid #323a45; /* groupCallMembersBgOver */ +} +.with_divider { + border-top: 1px solid #2c333d; /* groupCallMembersBg */ +} +a.block_link:hover { + background-color: #323a45; /* groupCallMembersBgOver */ +} +.list_page .entry { + color: #ffffff; /* groupCallMembersFg */ +} +.message { + color: #ffffff; /* groupCallMembersFg */ +} +div.selected { + background-color: #323a45; /* groupCallMembersBgOver */ +} +.default .from_name { + color: #4db8ff; /* groupCallActiveFg */ +} +.default .media .description { + color: #ffffff; /* groupCallMembersFg */ +} +msgInBg, +.historyComposeAreaBg { + background-color: #2c333d; /* groupCallMembersBg */ +} +msgOutBg { + background-color: #323a45; /* groupCallMembersBgOver */ +} +msgInBgSelected { + background-color: #39424f; /* groupCallMembersBgRipple */ +} +msgOutBgSelected { + background-color: #39424f; /* groupCallMembersBgRipple */ +} +.spoiler { + background: #323a45; /* groupCallMembersBgOver */ +} +.spoiler.hidden { + background: #61c0ff; /* groupCallMemberInactiveStatus */ +} +.bot_button { + background-color: #4db8ff40; /* groupCallActiveFg with opacity */ +} +.reactions .reaction { + background-color: #2c333d; /* groupCallMembersBg */ + color: #4db8ff; /* groupCallActiveFg */ +} +.reactions .reaction.active { + background-color: #4db8ff; /* groupCallActiveFg */ + color: #1a2026; /* groupCallBg */ +} +.reactions .reaction.paid { + background-color: #323a45; /* groupCallMembersBgOver */ + color: #febb5b; /* historyPeer8UserpicBg */ +} +.reactions .reaction.active.paid { + background-color: #febb5b; /* historyPeer8UserpicBg */ + color: #1a2026; /* groupCallBg */ +} } \ No newline at end of file diff --git a/Telegram/Resources/export_html/images/section_music.png b/Telegram/Resources/export_html/images/section_music.png new file mode 100644 index 0000000000..a76a2449d5 Binary files /dev/null and b/Telegram/Resources/export_html/images/section_music.png differ diff --git a/Telegram/Resources/export_html/images/section_music@2x.png b/Telegram/Resources/export_html/images/section_music@2x.png new file mode 100644 index 0000000000..53dc7022c6 Binary files /dev/null and b/Telegram/Resources/export_html/images/section_music@2x.png differ diff --git a/Telegram/Resources/icons/calls/filled_stream_crown.svg b/Telegram/Resources/icons/calls/filled_stream_crown.svg new file mode 100644 index 0000000000..f1b17ee2be --- /dev/null +++ b/Telegram/Resources/icons/calls/filled_stream_crown.svg @@ -0,0 +1,7 @@ + + + Filled / filled_stream_crown + + + + \ No newline at end of file diff --git a/Telegram/Resources/icons/chat/input_comments_expand.png b/Telegram/Resources/icons/chat/input_comments_expand.png new file mode 100644 index 0000000000..2a67d96318 Binary files /dev/null and b/Telegram/Resources/icons/chat/input_comments_expand.png differ diff --git a/Telegram/Resources/icons/chat/input_comments_expand@2x.png b/Telegram/Resources/icons/chat/input_comments_expand@2x.png new file mode 100644 index 0000000000..5fe041c4ad Binary files /dev/null and b/Telegram/Resources/icons/chat/input_comments_expand@2x.png differ diff --git a/Telegram/Resources/icons/chat/input_comments_expand@3x.png b/Telegram/Resources/icons/chat/input_comments_expand@3x.png new file mode 100644 index 0000000000..4e675a43f0 Binary files /dev/null and b/Telegram/Resources/icons/chat/input_comments_expand@3x.png differ diff --git a/Telegram/Resources/icons/chat/input_comments_hide.png b/Telegram/Resources/icons/chat/input_comments_hide.png new file mode 100644 index 0000000000..606fb2c497 Binary files /dev/null and b/Telegram/Resources/icons/chat/input_comments_hide.png differ diff --git a/Telegram/Resources/icons/chat/input_comments_hide@2x.png b/Telegram/Resources/icons/chat/input_comments_hide@2x.png new file mode 100644 index 0000000000..233fd06490 Binary files /dev/null and b/Telegram/Resources/icons/chat/input_comments_hide@2x.png differ diff --git a/Telegram/Resources/icons/chat/input_comments_hide@3x.png b/Telegram/Resources/icons/chat/input_comments_hide@3x.png new file mode 100644 index 0000000000..e6f6d925c0 Binary files /dev/null and b/Telegram/Resources/icons/chat/input_comments_hide@3x.png differ diff --git a/Telegram/Resources/icons/chat/input_send_round.png b/Telegram/Resources/icons/chat/input_send_round.png new file mode 100644 index 0000000000..7052e9116d Binary files /dev/null and b/Telegram/Resources/icons/chat/input_send_round.png differ diff --git a/Telegram/Resources/icons/chat/input_send_round@2x.png b/Telegram/Resources/icons/chat/input_send_round@2x.png new file mode 100644 index 0000000000..92b3b02c7f Binary files /dev/null and b/Telegram/Resources/icons/chat/input_send_round@2x.png differ diff --git a/Telegram/Resources/icons/chat/input_send_round@3x.png b/Telegram/Resources/icons/chat/input_send_round@3x.png new file mode 100644 index 0000000000..f33a744a62 Binary files /dev/null and b/Telegram/Resources/icons/chat/input_send_round@3x.png differ diff --git a/Telegram/Resources/icons/chat/mini_gift_hidden.png b/Telegram/Resources/icons/chat/mini_gift_hidden.png new file mode 100644 index 0000000000..3c7afb68fb Binary files /dev/null and b/Telegram/Resources/icons/chat/mini_gift_hidden.png differ diff --git a/Telegram/Resources/icons/chat/mini_gift_hidden@2x.png b/Telegram/Resources/icons/chat/mini_gift_hidden@2x.png new file mode 100644 index 0000000000..c212d24dfd Binary files /dev/null and b/Telegram/Resources/icons/chat/mini_gift_hidden@2x.png differ diff --git a/Telegram/Resources/icons/chat/mini_gift_hidden@3x.png b/Telegram/Resources/icons/chat/mini_gift_hidden@3x.png new file mode 100644 index 0000000000..cb21e63b36 Binary files /dev/null and b/Telegram/Resources/icons/chat/mini_gift_hidden@3x.png differ diff --git a/Telegram/Resources/icons/mediaview/recognize.png b/Telegram/Resources/icons/mediaview/recognize.png new file mode 100644 index 0000000000..05941e3246 Binary files /dev/null and b/Telegram/Resources/icons/mediaview/recognize.png differ diff --git a/Telegram/Resources/icons/mediaview/recognize@2x.png b/Telegram/Resources/icons/mediaview/recognize@2x.png new file mode 100644 index 0000000000..bc86664b69 Binary files /dev/null and b/Telegram/Resources/icons/mediaview/recognize@2x.png differ diff --git a/Telegram/Resources/icons/mediaview/recognize@3x.png b/Telegram/Resources/icons/mediaview/recognize@3x.png new file mode 100644 index 0000000000..0be9ab5e32 Binary files /dev/null and b/Telegram/Resources/icons/mediaview/recognize@3x.png differ diff --git a/Telegram/Resources/icons/menu/2sv_off.png b/Telegram/Resources/icons/menu/2sv_off.png new file mode 100644 index 0000000000..78a2886000 Binary files /dev/null and b/Telegram/Resources/icons/menu/2sv_off.png differ diff --git a/Telegram/Resources/icons/menu/2sv_off@2x.png b/Telegram/Resources/icons/menu/2sv_off@2x.png new file mode 100644 index 0000000000..98cae67216 Binary files /dev/null and b/Telegram/Resources/icons/menu/2sv_off@2x.png differ diff --git a/Telegram/Resources/icons/menu/2sv_off@3x.png b/Telegram/Resources/icons/menu/2sv_off@3x.png new file mode 100644 index 0000000000..cf1b017bae Binary files /dev/null and b/Telegram/Resources/icons/menu/2sv_off@3x.png differ diff --git a/Telegram/Resources/icons/menu/2sv_on.png b/Telegram/Resources/icons/menu/2sv_on.png new file mode 100644 index 0000000000..f7089daa1f Binary files /dev/null and b/Telegram/Resources/icons/menu/2sv_on.png differ diff --git a/Telegram/Resources/icons/menu/2sv_on@2x.png b/Telegram/Resources/icons/menu/2sv_on@2x.png new file mode 100644 index 0000000000..021ec634e6 Binary files /dev/null and b/Telegram/Resources/icons/menu/2sv_on@2x.png differ diff --git a/Telegram/Resources/icons/menu/2sv_on@3x.png b/Telegram/Resources/icons/menu/2sv_on@3x.png new file mode 100644 index 0000000000..5e74c1fcc3 Binary files /dev/null and b/Telegram/Resources/icons/menu/2sv_on@3x.png differ diff --git a/Telegram/Resources/icons/menu/auction_carry.png b/Telegram/Resources/icons/menu/auction_carry.png new file mode 100644 index 0000000000..d63bbb745f Binary files /dev/null and b/Telegram/Resources/icons/menu/auction_carry.png differ diff --git a/Telegram/Resources/icons/menu/auction_carry@2x.png b/Telegram/Resources/icons/menu/auction_carry@2x.png new file mode 100644 index 0000000000..dd5367f943 Binary files /dev/null and b/Telegram/Resources/icons/menu/auction_carry@2x.png differ diff --git a/Telegram/Resources/icons/menu/auction_carry@3x.png b/Telegram/Resources/icons/menu/auction_carry@3x.png new file mode 100644 index 0000000000..c8f1888737 Binary files /dev/null and b/Telegram/Resources/icons/menu/auction_carry@3x.png differ diff --git a/Telegram/Resources/icons/menu/auction_drop.png b/Telegram/Resources/icons/menu/auction_drop.png new file mode 100644 index 0000000000..d1b3af2c33 Binary files /dev/null and b/Telegram/Resources/icons/menu/auction_drop.png differ diff --git a/Telegram/Resources/icons/menu/auction_drop@2x.png b/Telegram/Resources/icons/menu/auction_drop@2x.png new file mode 100644 index 0000000000..3ced06af81 Binary files /dev/null and b/Telegram/Resources/icons/menu/auction_drop@2x.png differ diff --git a/Telegram/Resources/icons/menu/auction_drop@3x.png b/Telegram/Resources/icons/menu/auction_drop@3x.png new file mode 100644 index 0000000000..c2f58a2377 Binary files /dev/null and b/Telegram/Resources/icons/menu/auction_drop@3x.png differ diff --git a/Telegram/Resources/icons/menu/auction_refund.png b/Telegram/Resources/icons/menu/auction_refund.png new file mode 100644 index 0000000000..aebc536a0f Binary files /dev/null and b/Telegram/Resources/icons/menu/auction_refund.png differ diff --git a/Telegram/Resources/icons/menu/auction_refund@2x.png b/Telegram/Resources/icons/menu/auction_refund@2x.png new file mode 100644 index 0000000000..5cc6db3dcb Binary files /dev/null and b/Telegram/Resources/icons/menu/auction_refund@2x.png differ diff --git a/Telegram/Resources/icons/menu/auction_refund@3x.png b/Telegram/Resources/icons/menu/auction_refund@3x.png new file mode 100644 index 0000000000..eb9f032df0 Binary files /dev/null and b/Telegram/Resources/icons/menu/auction_refund@3x.png differ diff --git a/Telegram/Resources/icons/profile/call.svg b/Telegram/Resources/icons/profile/call.svg new file mode 100644 index 0000000000..4882bb6228 --- /dev/null +++ b/Telegram/Resources/icons/profile/call.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/Telegram/Resources/icons/profile/gift.svg b/Telegram/Resources/icons/profile/gift.svg new file mode 100644 index 0000000000..0d6880abee --- /dev/null +++ b/Telegram/Resources/icons/profile/gift.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/Telegram/Resources/icons/profile/join.svg b/Telegram/Resources/icons/profile/join.svg new file mode 100644 index 0000000000..7c53bdae00 --- /dev/null +++ b/Telegram/Resources/icons/profile/join.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/Telegram/Resources/icons/profile/leave.svg b/Telegram/Resources/icons/profile/leave.svg new file mode 100644 index 0000000000..a91a104440 --- /dev/null +++ b/Telegram/Resources/icons/profile/leave.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/Telegram/Resources/icons/profile/message.svg b/Telegram/Resources/icons/profile/message.svg new file mode 100644 index 0000000000..685cf25a68 --- /dev/null +++ b/Telegram/Resources/icons/profile/message.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/Telegram/Resources/icons/profile/mute.svg b/Telegram/Resources/icons/profile/mute.svg new file mode 100644 index 0000000000..d9fea015ed --- /dev/null +++ b/Telegram/Resources/icons/profile/mute.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/Telegram/Resources/icons/profile/profile_manage.png b/Telegram/Resources/icons/profile/profile_manage.png new file mode 100644 index 0000000000..38f1a731dd Binary files /dev/null and b/Telegram/Resources/icons/profile/profile_manage.png differ diff --git a/Telegram/Resources/icons/profile/profile_manage@2x.png b/Telegram/Resources/icons/profile/profile_manage@2x.png new file mode 100644 index 0000000000..e5ab30a518 Binary files /dev/null and b/Telegram/Resources/icons/profile/profile_manage@2x.png differ diff --git a/Telegram/Resources/icons/profile/profile_manage@3x.png b/Telegram/Resources/icons/profile/profile_manage@3x.png new file mode 100644 index 0000000000..8bf97111c7 Binary files /dev/null and b/Telegram/Resources/icons/profile/profile_manage@3x.png differ diff --git a/Telegram/Resources/icons/profile/profile_more.png b/Telegram/Resources/icons/profile/profile_more.png new file mode 100644 index 0000000000..a35e32ff95 Binary files /dev/null and b/Telegram/Resources/icons/profile/profile_more.png differ diff --git a/Telegram/Resources/icons/profile/profile_more@2x.png b/Telegram/Resources/icons/profile/profile_more@2x.png new file mode 100644 index 0000000000..c7ceb94e58 Binary files /dev/null and b/Telegram/Resources/icons/profile/profile_more@2x.png differ diff --git a/Telegram/Resources/icons/profile/profile_more@3x.png b/Telegram/Resources/icons/profile/profile_more@3x.png new file mode 100644 index 0000000000..3851073ed9 Binary files /dev/null and b/Telegram/Resources/icons/profile/profile_more@3x.png differ diff --git a/Telegram/Resources/icons/profile/report.svg b/Telegram/Resources/icons/profile/report.svg new file mode 100644 index 0000000000..2dc982d75f --- /dev/null +++ b/Telegram/Resources/icons/profile/report.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/Telegram/Resources/icons/profile/unmute.svg b/Telegram/Resources/icons/profile/unmute.svg new file mode 100644 index 0000000000..80bd6c2fa4 --- /dev/null +++ b/Telegram/Resources/icons/profile/unmute.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/Telegram/Resources/icons/settings/button_auction.png b/Telegram/Resources/icons/settings/button_auction.png new file mode 100644 index 0000000000..6fbaa5e019 Binary files /dev/null and b/Telegram/Resources/icons/settings/button_auction.png differ diff --git a/Telegram/Resources/icons/settings/button_auction@2x.png b/Telegram/Resources/icons/settings/button_auction@2x.png new file mode 100644 index 0000000000..c4c3ef1d8f Binary files /dev/null and b/Telegram/Resources/icons/settings/button_auction@2x.png differ diff --git a/Telegram/Resources/icons/settings/button_auction@3x.png b/Telegram/Resources/icons/settings/button_auction@3x.png new file mode 100644 index 0000000000..c9c6b28561 Binary files /dev/null and b/Telegram/Resources/icons/settings/button_auction@3x.png differ diff --git a/Telegram/Resources/icons/settings/large_auctions.png b/Telegram/Resources/icons/settings/large_auctions.png new file mode 100644 index 0000000000..e567876247 Binary files /dev/null and b/Telegram/Resources/icons/settings/large_auctions.png differ diff --git a/Telegram/Resources/icons/settings/large_auctions@2x.png b/Telegram/Resources/icons/settings/large_auctions@2x.png new file mode 100644 index 0000000000..1a32e7584c Binary files /dev/null and b/Telegram/Resources/icons/settings/large_auctions@2x.png differ diff --git a/Telegram/Resources/icons/settings/large_auctions@3x.png b/Telegram/Resources/icons/settings/large_auctions@3x.png new file mode 100644 index 0000000000..652c413bc8 Binary files /dev/null and b/Telegram/Resources/icons/settings/large_auctions@3x.png differ diff --git a/Telegram/Resources/icons/settings/premium/features/feature_color_profile.png b/Telegram/Resources/icons/settings/premium/features/feature_color_profile.png new file mode 100644 index 0000000000..33a6d47c9f Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/features/feature_color_profile.png differ diff --git a/Telegram/Resources/icons/settings/premium/features/feature_color_profile@2x.png b/Telegram/Resources/icons/settings/premium/features/feature_color_profile@2x.png new file mode 100644 index 0000000000..dcb515f560 Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/features/feature_color_profile@2x.png differ diff --git a/Telegram/Resources/icons/settings/premium/features/feature_color_profile@3x.png b/Telegram/Resources/icons/settings/premium/features/feature_color_profile@3x.png new file mode 100644 index 0000000000..08f0296671 Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/features/feature_color_profile@3x.png differ diff --git a/Telegram/Resources/icons/settings/premium/features/feature_profile_cover.png b/Telegram/Resources/icons/settings/premium/features/feature_profile_cover.png new file mode 100644 index 0000000000..657fd9390a Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/features/feature_profile_cover.png differ diff --git a/Telegram/Resources/icons/settings/premium/features/feature_profile_cover@2x.png b/Telegram/Resources/icons/settings/premium/features/feature_profile_cover@2x.png new file mode 100644 index 0000000000..250be37750 Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/features/feature_profile_cover@2x.png differ diff --git a/Telegram/Resources/icons/settings/premium/features/feature_profile_cover@3x.png b/Telegram/Resources/icons/settings/premium/features/feature_profile_cover@3x.png new file mode 100644 index 0000000000..ae696a8f34 Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/features/feature_profile_cover@3x.png differ diff --git a/Telegram/Resources/icons/settings/premium/large_lastseen.png b/Telegram/Resources/icons/settings/premium/large_lastseen.png deleted file mode 100644 index e47bd7935b..0000000000 Binary files a/Telegram/Resources/icons/settings/premium/large_lastseen.png and /dev/null differ diff --git a/Telegram/Resources/icons/settings/premium/large_lastseen@2x.png b/Telegram/Resources/icons/settings/premium/large_lastseen@2x.png deleted file mode 100644 index 305b6a4919..0000000000 Binary files a/Telegram/Resources/icons/settings/premium/large_lastseen@2x.png and /dev/null differ diff --git a/Telegram/Resources/icons/settings/premium/large_lastseen@3x.png b/Telegram/Resources/icons/settings/premium/large_lastseen@3x.png deleted file mode 100644 index 0cd1e12ab0..0000000000 Binary files a/Telegram/Resources/icons/settings/premium/large_lastseen@3x.png and /dev/null differ diff --git a/Telegram/Resources/icons/settings/premium/large_readtime.png b/Telegram/Resources/icons/settings/premium/large_readtime.png deleted file mode 100644 index c06294a82e..0000000000 Binary files a/Telegram/Resources/icons/settings/premium/large_readtime.png and /dev/null differ diff --git a/Telegram/Resources/icons/settings/premium/large_readtime@2x.png b/Telegram/Resources/icons/settings/premium/large_readtime@2x.png deleted file mode 100644 index f30a355ee8..0000000000 Binary files a/Telegram/Resources/icons/settings/premium/large_readtime@2x.png and /dev/null differ diff --git a/Telegram/Resources/icons/settings/premium/large_readtime@3x.png b/Telegram/Resources/icons/settings/premium/large_readtime@3x.png deleted file mode 100644 index 1e7d022a30..0000000000 Binary files a/Telegram/Resources/icons/settings/premium/large_readtime@3x.png and /dev/null differ diff --git a/Telegram/Resources/icons/settings/premium/peer_colors.svg b/Telegram/Resources/icons/settings/premium/peer_colors.svg new file mode 100644 index 0000000000..0a3c1b0854 --- /dev/null +++ b/Telegram/Resources/icons/settings/premium/peer_colors.svg @@ -0,0 +1,7 @@ + + + Icon / Filled / premium_themes + + + + \ No newline at end of file diff --git a/Telegram/Resources/icons/settings/toast_auction.png b/Telegram/Resources/icons/settings/toast_auction.png new file mode 100644 index 0000000000..41b34d5fe1 Binary files /dev/null and b/Telegram/Resources/icons/settings/toast_auction.png differ diff --git a/Telegram/Resources/icons/settings/toast_auction@2x.png b/Telegram/Resources/icons/settings/toast_auction@2x.png new file mode 100644 index 0000000000..e03d6a987f Binary files /dev/null and b/Telegram/Resources/icons/settings/toast_auction@2x.png differ diff --git a/Telegram/Resources/icons/settings/toast_auction@3x.png b/Telegram/Resources/icons/settings/toast_auction@3x.png new file mode 100644 index 0000000000..8bf2db0ab2 Binary files /dev/null and b/Telegram/Resources/icons/settings/toast_auction@3x.png differ diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index b45a10f2d7..ec27f33636 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -328,6 +328,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_proxy_box_password" = "Password"; "lng_proxy_invalid" = "The proxy link is invalid."; "lng_proxy_unsupported" = "Your Telegram Desktop version doesn't support this proxy type or the proxy link is invalid. Please update Telegram Desktop to the latest version."; +"lng_proxy_incorrect_secret" = "This proxy link uses invalid **secret** parameter. Please contact the proxy provider and ask him to update MTProxy source code and configure it with a correct **secret** value. Then let him provide a new link."; "lng_edit_deleted" = "This message was deleted"; "lng_edit_limit_reached#one" = "You've reached the message text limit. Please make the text shorter by {count} character."; @@ -377,6 +378,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_intro_qr_step2" = "Go to Settings > Devices > Link Desktop Device"; "lng_intro_qr_step3" = "Scan this image to Log In"; "lng_intro_qr_skip" = "Or log in using your phone number"; +"lng_intro_qr_phone" = "Log in using phone number"; +"lng_intro_qr_passkey" = "Log in using passkey"; "lng_intro_fragment_title" = "Enter code"; "lng_intro_fragment_about" = "Get the code for {phone_number} in the Anonymous Numbers section on Fragment."; @@ -742,6 +745,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_settings_language" = "Language"; "lng_settings_default_scale" = "Default interface scale"; +"lng_settings_scale" = "Interface scale"; "lng_settings_connection_type" = "Connection type"; "lng_settings_downloading_update" = "Downloading update {progress}..."; "lng_settings_privacy_title" = "Privacy"; @@ -758,6 +762,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"; @@ -928,6 +933,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"; @@ -976,6 +983,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_settings_font_family" = "Font family"; "lng_settings_color_title" = "Color preview"; +"lng_settings_color_tab_profile" = "Profile"; +"lng_settings_color_tab_name" = "Name"; "lng_settings_color_reply" = "Reply to your message"; "lng_settings_color_reply_channel" = "Reply to your channel message"; "lng_settings_color_text" = "Your name and replies to your messages will be shown in the selected color."; @@ -990,10 +999,22 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_settings_color_emoji_off" = "Off"; "lng_settings_color_emoji_about" = "Make replies to your messages stand out by adding custom patterns to them."; "lng_settings_color_emoji_about_channel" = "Select an icon to create a custom pattern for replies to your messages."; -"lng_settings_color_subscribe" = "Subscribe to {link} to choose a custom color for your name."; "lng_settings_color_changed" = "Your name color has been updated!"; "lng_settings_color_changed_channel" = "Your channel color has been updated!"; +"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"; +"lng_settings_color_profile_about" = "You can change the color of your name and customize replies to you. {link}"; +"lng_settings_color_profile_about_link" = "Change {emoji}"; +"lng_settings_color_choose_channel" = "Choose a color and a logo for your channel's profile"; +"lng_settings_color_choose_group" = "Choose a color and a logo for the group's profile"; +"lng_settings_color_group_boost_footer#one" = "The group has **{count}** boost. {link}"; +"lng_settings_color_group_boost_footer#other" = "The group has **{count}** boosts. {link}"; +"lng_settings_color_group_boost_footer_link" = "What are boosts?"; "lng_suggest_hide_new_title" = "Hide new chats?"; "lng_suggest_hide_new_about" = "You are receiving lots of new chats from users who are not in your Contact List.\n\nDo you want to have such chats **automatically muted** and **archived**?"; @@ -1233,6 +1254,27 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_settings_restart_now" = "Restart"; "lng_settings_restart_later" = "Later"; +"lng_settings_passkeys_title" = "Passkeys"; +"lng_settings_passkeys_about" = "Manage your passkey, stored safely in the cloud service you choose."; +"lng_settings_passkeys_button" = "Add Passkey"; +"lng_settings_passkeys_button_about" = "Your passkey is stored securely in your password manager. {link}"; +"lng_settings_passkeys_delete_sure_title" = "Delete Passkey"; +"lng_settings_passkeys_delete_sure_about" = "Once deleted, this passkey can't be used to log in."; +"lng_settings_passkeys_delete_sure_about2" = "Don't forget to remove it from your password manager too."; +"lng_settings_passkeys_none_title" = "Protect your account"; +"lng_settings_passkeys_none_about" = "Log in safely and keep your account secure."; +"lng_settings_passkeys_none_info1_title" = "Create a Passkey"; +"lng_settings_passkeys_none_info1_about" = "Make a passkey to sign in easily and safely."; +"lng_settings_passkeys_none_info2_title" = "Log in with face recognition"; +"lng_settings_passkeys_none_info2_about" = "Use your face, fingerprint, or screen lock to sign in."; +"lng_settings_passkeys_none_info3_title" = "Store Passkey securely"; +"lng_settings_passkeys_none_info3_about" = "Your passkey is stored safely in the cloud service you choose."; +"lng_settings_passkeys_none_button" = "Create Passkey"; +"lng_settings_passkeys_created" = "Created {date}"; +"lng_settings_passkeys_last_used" = "Last used {date}"; +"lng_settings_passkeys_unsigned_error" = "Passkeys are not available in unsigned builds. Please use an official signed version of Telegram Desktop."; + + "lng_settings_quick_dialog_action_title" = "Chat list quick action"; "lng_settings_quick_dialog_action_about" = "Choose the action you want to perform when you middle-click or swipe left in the chat list."; "lng_settings_quick_dialog_action_both" = "Swipe left and Middle-click"; @@ -1392,6 +1434,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."; @@ -1451,6 +1494,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"; @@ -1564,6 +1615,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_profile_administrators#one" = "{count} administrator"; "lng_profile_administrators#other" = "{count} administrators"; "lng_profile_manage" = "Channel settings"; +"lng_profile_topic_toast" = "This topic contains {name}"; "lng_invite_upgrade_title" = "Upgrade to Premium"; "lng_invite_upgrade_group_invite#one" = "{users} only accepts invitations to groups from Contacts and **Premium** users."; @@ -1618,6 +1670,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_delete_note" = "Delete Note"; "lng_info_bio_label" = "Bio"; "lng_info_link_label" = "Link"; +"lng_info_link_topic_label" = "This topic link will only work for group members"; "lng_info_location_label" = "Location"; "lng_info_about_label" = "Description"; "lng_info_work_open" = "Open"; @@ -1646,6 +1699,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_profile_open_app_short" = "Open"; "lng_profile_open_app_about" = "By launching this mini app, you agree to the {terms}."; "lng_profile_open_app_terms" = "Terms of Service for Mini Apps"; +"lng_profile_open_photo" = "Open Photo"; "lng_profile_bot_permissions_title" = "Allow access to"; "lng_profile_bot_emoji_status_access" = "Emoji Status"; "lng_info_add_as_contact" = "Add to contacts"; @@ -1653,9 +1707,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_profile_suggest_photo" = "Suggest Profile Photo"; "lng_profile_suggest_photo_from_clipboard" = "Suggest From Clipboard"; "lng_profile_set_photo_for" = "Set Profile Photo"; +"lng_profile_set_photo_for_group" = "Set Group Photo"; +"lng_profile_set_photo_for_channel" = "Set Channel Photo"; "lng_profile_set_photo_for_from_clipboard" = "Set From Clipboard"; "lng_profile_set_photo_for_about" = "You can replace {user}'s photo with another photo that only you will see."; "lng_profile_photo_reset" = "Reset to Original"; +"lng_profile_photo_reset_button" = "Reset"; "lng_profile_photo_reset_sure" = "Are you sure you want to reset {user}'s photo to the original?"; "lng_profile_photo_from_clipboard" = "From clipboard"; "lng_profile_suggest_sure" = "You can suggest {user} to set this photo for their Telegram profile."; @@ -1668,6 +1725,20 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_profile_changed_photo_title" = "Photo updated"; "lng_profile_changed_photo_about" = "You can change it in {link}."; "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"; +"lng_profile_action_short_discuss" = "Discuss"; +"lng_profile_action_short_gift" = "Gift"; +"lng_profile_action_short_join" = "Join"; +"lng_profile_action_short_report" = "Report"; +"lng_profile_action_short_leave" = "Leave"; +"lng_profile_action_short_more" = "More"; +"lng_profile_action_short_manage" = "Manage"; + "lng_media_type_photos" = "Photos"; "lng_media_type_gifs" = "GIFs"; "lng_media_type_videos" = "Videos"; @@ -1687,6 +1758,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_profile_block_user" = "Block user"; "lng_profile_unblock_user" = "Unblock user"; "lng_profile_export_chat" = "Export chat history"; +"lng_profile_export_topic" = "Export topic history"; "lng_profile_gift_premium" = "Gift Premium"; "lng_media_selected_photo#one" = "{count} Photo"; "lng_media_selected_photo#other" = "{count} Photos"; @@ -2219,8 +2291,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_action_proximity_distance_km#other" = "{count} km"; "lng_action_webview_data_done" = "Data from the \"{text}\" button was transferred to the bot."; "lng_action_gift_received" = "{user} sent you a gift for {cost}"; +"lng_action_gift_received_sold" = "{user} sold you a gift for {cost}"; "lng_action_gift_unique_received" = "{user} sent you a unique collectible item"; "lng_action_gift_sent" = "You sent a gift for {cost}"; +"lng_action_gift_sent_sold" = "You sold a gift for {cost}"; "lng_action_gift_unique_sent" = "You sent a unique collectible item"; "lng_action_gift_upgraded" = "{user} turned the gift from you into a unique collectible"; "lng_action_gift_upgraded_channel" = "{user} turned this gift to {channel} into a unique collectible"; @@ -2246,6 +2320,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_won" = "You won the auction with a bid of {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."; @@ -2274,6 +2350,23 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_action_gift_premium_about" = "Subscription for exclusive Telegram features."; "lng_action_gift_refunded" = "This gift was downgraded because a request to refund the payment related to this gift was made, and the money was returned."; "lng_action_gift_got_ton" = "Use TON to suggest posts to channels."; +"lng_action_gift_offer" = "{user} offered you {cost} for {name}."; +"lng_action_gift_offer_you" = "You offered {cost} for {name}."; +"lng_action_gift_offer_state_expires" = "This offer expires in {time}."; +"lng_action_gift_offer_time_large" = "{hours} h"; +"lng_action_gift_offer_time_medium" = "{hours} h {minutes} m"; +"lng_action_gift_offer_time_small" = "{minutes} m"; +"lng_action_gift_offer_state_accepted" = "This offer was accepted."; +"lng_action_gift_offer_state_rejected" = "This offer was rejected."; +"lng_action_gift_offer_state_expired" = "This offer has expired."; +"lng_action_gift_offer_sold" = "{user} sold {name} for {cost}."; +"lng_action_gift_offer_sold_you" = "You sold {name} for {cost}."; +"lng_action_gift_offer_decline" = "Reject"; +"lng_action_gift_offer_accept" = "Accept"; +"lng_action_gift_offer_expired" = "The offer from {user} to buy your {name} for {cost} has expired."; +"lng_action_gift_offer_expired_your" = "Your offer to buy {name} for {cost} has expired."; +"lng_action_gift_offer_declined" = "{user} rejected your offer to buy {name} for {cost}."; +"lng_action_gift_offer_declined_you" = "You rejected {user}'s offer to buy your {name} for {cost}."; "lng_action_suggested_photo_me" = "You suggested this photo for {user}'s Telegram profile."; "lng_action_suggested_photo" = "{user} suggests this photo for your Telegram profile."; "lng_action_suggested_photo_button" = "View Photo"; @@ -2401,6 +2494,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"; @@ -2578,6 +2673,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."; @@ -2696,8 +2798,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"; @@ -2742,6 +2848,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_premium_summary_about_filter_tags" = "Display folder names for each chat in the chat list."; "lng_premium_summary_subtitle_todo_lists" = "Checklists"; "lng_premium_summary_about_todo_lists" = "Plan, assign, and complete tasks - seamlessly and efficiently."; +"lng_premium_summary_subtitle_peer_colors" = "Name and Profile Colors"; +"lng_premium_summary_about_peer_colors" = "Choose a color and logo for your profile and replies to your messages."; +"lng_premium_summary_subtitle_gifts" = "Telegram Gifts"; +"lng_premium_summary_about_gifts" = "Gifts are collectible items you can trade or showcase on your profile."; "lng_premium_summary_bottom_subtitle" = "About Telegram Premium"; "lng_premium_summary_bottom_about" = "While the free version of Telegram already gives its users more than any other messaging application, **Telegram Premium** pushes its capabilities even further.\n\n**Telegram Premium** is a paid option, because most Premium Features require additional expenses from Telegram to third parties such as data center providers and server manufacturers. Contributions from **Telegram Premium** users allow us to cover such costs and also help Telegram stay free for everyone."; "lng_premium_summary_button" = "Subscribe for {cost} per month"; @@ -2892,6 +3002,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_credits_summary_history_entry_inner_in" = "In-App Purchase"; "lng_credits_summary_balance" = "Balance"; "lng_credits_commission" = "{amount} commission"; +"lng_credits_paid_messages_fee_live_reaction" = "Fee for Live Story Reaction"; "lng_credits_paid_messages_fee#one" = "Fee for {count} Message"; "lng_credits_paid_messages_fee#other" = "Fee for {count} Messages"; "lng_credits_paid_messages_fee_about" = "You receive {percent} of the price that you charge for each incoming message. {link}"; @@ -3023,11 +3134,13 @@ 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}."; "lng_credits_small_balance_for_messages" = "Buy **Stars** to send messages."; "lng_credits_small_balance_for_suggest" = "Buy **Stars** to suggest post to {channel}."; +"lng_credits_small_balance_for_offer" = "Buy **Stars** to offer for this gift."; "lng_credits_small_balance_for_search" = "Buy **Stars** to search through public posts."; "lng_credits_small_balance_fallback" = "Buy **Stars** to unlock content and services on Telegram."; "lng_credits_purchase_blocked" = "Sorry, you can't purchase this item with Telegram Stars."; @@ -3351,6 +3464,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_feature_custom_emoji_pack" = "Custom Emoji Pack"; "lng_feature_transcribe" = "Voice-to-Text Conversion"; "lng_feature_autotranslate" = "Autotranslation of Messages"; +"lng_feature_profile_color_channel#one" = "**{count}** Color for Channel Cover"; +"lng_feature_profile_color_channel#other" = "**{count}** Colors for Channel Cover"; +"lng_feature_profile_color_group#one" = "**{count}** Color for Group Cover"; +"lng_feature_profile_color_group#other" = "**{count}** Colors for Group Cover"; +"lng_feature_profile_icon_channel" = "Custom Logo for Channel Cover"; +"lng_feature_profile_icon_group" = "Custom Logo for Group Cover"; "lng_edit_topics_enable" = "Enable Topics"; "lng_edit_topics_about" = "The group chat will be divided into topics created by admins or users."; @@ -3616,11 +3735,18 @@ 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_auction_view" = "View"; +"lng_gift_stars_auction_soon" = "soon"; +"lng_gift_stars_auction_upgraded" = "upgraded"; "lng_gift_stars_your_left#one" = "{count} left"; "lng_gift_stars_your_left#other" = "{count} left"; "lng_gift_stars_your_finished" = "none left"; "lng_gift_stars_tabs_all" = "All Gifts"; "lng_gift_stars_tabs_my" = "My Gifts"; +"lng_gift_stars_tabs_my_empty" = "You don't have any gifts you can use as a profile cover."; +"lng_gift_stars_tabs_my_empty_next" = "Browse gifts available for purchase {emoji}"; "lng_gift_stars_tabs_collectibles" = "Collectibles"; "lng_gift_send_title" = "Send a Gift"; "lng_gift_send_message" = "Enter Message"; @@ -3782,6 +3908,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_upgrade_tradable_about" = "Sell or auction your gift on third-party NFT marketplaces."; "lng_gift_upgrade_tradable_about_user" = "{name} will be able to sell the gift on Telegram and NFT marketplaces."; "lng_gift_upgrade_tradable_about_channel" = "Admins of {name} will be able to sell the gift on Telegram and NFT marketplaces."; +"lng_gift_upgrade_wearable_title" = "Wearable"; +"lng_gift_upgrade_wearable_about" = "Display gifts on your page and set them as profile covers or statuses."; "lng_gift_upgrade_button" = "Upgrade for {price}"; "lng_gift_upgrade_decreases" = "Price decreases in {time}"; "lng_gift_upgrade_see_table" = "See how this price will decrease {arrow}"; @@ -3834,6 +3962,25 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_transfer_unlist" = "Unlist"; "lng_gift_transfer_locked_title" = "Action Locked"; "lng_gift_transfer_locked_text" = "Transfer this gift to your Telegram account on Fragment to unlock this action."; +"lng_gift_offer_button" = "Offer to Buy"; +"lng_gift_offer_title" = "Offer to Buy"; +"lng_gift_offer_stars_about" = "Choose how many Stars you'd like to offer for {name}."; +"lng_gift_offer_ton_about" = "Choose how many TON you'd like to offer for {name}."; +"lng_gift_offer_duration" = "Offer Duration"; +"lng_gift_offer_duration_about" = "Choose how long {user} can accept your offer. When the time expires, the amount will be refunded."; +"lng_gift_offer_cost_button" = "Offer {cost}"; +"lng_gift_offer_reject_title" = "Reject Offer"; +"lng_gift_offer_confirm_reject" = "Are you sure you want to reject the offer from {user}?"; +"lng_gift_offer_confirm_accept" = "Do you want to sell {name} to {user} for {cost}?"; +"lng_gift_offer_you_get" = "You will receive {cost} after fees."; +"lng_gift_offer_higher" = "The price you are offered is {percent} higher than the average price for {name}."; +"lng_gift_offer_lower" = "The price you are offered is {percent} lower than the average price for {name}."; +"lng_gift_offer_sell_for" = "Sell for {price}"; +"lng_gift_offer_confirm_title" = "Confirm Offer"; +"lng_gift_offer_confirm_text" = "Do you want to offer {cost} to {user} for {name}?"; +"lng_gift_offer_table_offer" = "Offer"; +"lng_gift_offer_table_fee" = "Fee"; +"lng_gift_offer_table_duration" = "Duration"; "lng_gift_sell_unlist_title" = "Unlist {name}"; "lng_gift_sell_unlist_sure" = "Are you sure you want to unlist your gift?"; "lng_gift_sell_title" = "Price in Stars"; @@ -3861,6 +4008,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_wear_badge_title" = "Radiant Badge"; "lng_gift_wear_badge_about" = "The glittering icon of this item will be displayed next to your name."; "lng_gift_wear_badge_about_channel" = "The glittering icon of this item will be displayed next to channel's name."; +"lng_gift_wear_design_title" = "Unique Profile Design"; +"lng_gift_wear_design_about" = "Your profile page will get the color and the symbol of this item."; +"lng_gift_wear_design_about_channel" = "Your channel page will get the color and the symbol of this item."; "lng_gift_wear_proof_title" = "Proof of Ownership"; "lng_gift_wear_proof_about" = "Clicking the icon of this item next to your name will show its info and owner."; "lng_gift_wear_proof_about_channel" = "Clicking the icon of this item next to channel's name will show its info and owner."; @@ -3917,6 +4067,138 @@ 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_top_short#one" = "{count} gift is dropped to the {bidders} by bid amount. {link}"; +"lng_auction_about_top_short#other" = "{count} gifts are dropped to the {bidders} by bid amount. {link}"; +"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_starts_label" = "Starts"; +"lng_auction_rounds_label" = "Rounds"; +"lng_auction_rounds_exact" = "Round {n}"; +"lng_auction_rounds_range" = "Rounds {n}-{last}"; +"lng_auction_rounds_seconds#one" = "{count} minute each"; +"lng_auction_rounds_seconds#other" = "{count} minutes each"; +"lng_auction_rounds_minutes#one" = "{count} minute each"; +"lng_auction_rounds_minutes#other" = "{count} minutes each"; +"lng_auction_rounds_hours#one" = "{count} hour each"; +"lng_auction_rounds_hours#other" = "{count} hours each"; +"lng_auction_rounds_extended" = "{duration} + {increase} for late bids in top {n}"; +"lng_auction_end_label" = "Ends"; +"lng_auction_round_label" = "Current Round"; +"lng_auction_round_value" = "{n} of {amount}"; +"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_join_starts_in" = "starts in {time}"; +"lng_auction_join_early_bid" = "Place an Early Bid"; +"lng_auction_menu_about" = "About"; +"lng_auction_menu_copy_link" = "Copy Link"; +"lng_auction_menu_share" = "Share"; +"lng_auction_bid_title" = "Place a Bid"; +"lng_auction_bid_title_early" = "Place an Early Bid"; +"lng_auction_bid_subtitle#one" = "Top {count} bidder will win"; +"lng_auction_bid_subtitle#other" = "Top {count} bidders will win"; +"lng_auction_bid_your" = "your bid"; +"lng_auction_bid_custom" = "click to bid more"; +"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_before_start" = "before start"; +"lng_auction_bid_your_title" = "Your bid will be"; +"lng_auction_bid_your_outbid" = "You've been outbid"; +"lng_auction_bid_your_winning" = "You're winning"; +"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_bid_custom_title" = "Custom Amount"; +"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_in_round" = "{name} in round {n}"; +"lng_auction_change_title" = "Change Recipient"; +"lng_auction_change_button" = "Change"; +"lng_auction_change_already" = "You've already placed a bid on this gift for {name}."; +"lng_auction_change_to" = "Do you want to raise your bid and change the recipient to {name}?"; +"lng_auction_change_already_me" = "You've already placed a bid on this gift for yourself."; +"lng_auction_change_to_me" = "Do you want to raise your bid and change the recipient to yourself?"; +"lng_auction_preview_name" = "Upcoming Auction"; +"lng_auction_preview_learn_gifts" = "Learn more about Telegram Gifts {arrow}"; +"lng_auction_preview_variants#one" = "View {emoji} {count} Variant {arrow}"; +"lng_auction_preview_variants#other" = "View {emoji} {count} Variants {arrow}"; +"lng_auction_preview_random" = "Random Traits"; +"lng_auction_preview_selected" = "Selected Traits"; +"lng_auction_preview_randomize" = "Randomize Traits"; +"lng_auction_preview_model" = "model"; +"lng_auction_preview_models#one" = "This collectible features **{count}** unique model."; +"lng_auction_preview_models#other" = "This collectible features **{count}** unique models."; +"lng_auction_preview_models_button" = "Models"; +"lng_auction_preview_backdrop" = "backdrop"; +"lng_auction_preview_backdrops#one" = "This collectible features **{count}** unique backdrop."; +"lng_auction_preview_backdrops#other" = "This collectible features **{count}** unique backdrops."; +"lng_auction_preview_backdrops_button" = "Backdrops"; +"lng_auction_preview_symbol" = "symbol"; +"lng_auction_preview_symbols#one" = "This collectible features **{count}** unique symbol."; +"lng_auction_preview_symbols#other" = "This collectible features **{count}** unique symbols."; +"lng_auction_preview_symbols_button" = "Symbols"; +"lng_auction_preview_wear" = "More about wearing gifts {arrow}"; +"lng_auction_preview_free_upgrade" = "You can upgrade your gift for free, sell it on the market, or set it as your profile cover."; + "lng_accounts_limit_title" = "Limit Reached"; "lng_accounts_limit1#one" = "You have reached the limit of **{count}** connected account."; "lng_accounts_limit1#other" = "You have reached the limit of **{count}** connected accounts."; @@ -4097,6 +4379,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..."; @@ -4104,6 +4397,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."; @@ -4508,6 +4811,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"; @@ -4542,6 +4848,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_message_tagged_with" = "Message tagged with {emoji}"; "lng_tagged_view_saved" = "View"; +"lng_add_channel_to_filter_selector" = "You can add a channel to your folder"; +"lng_add_group_to_filter_selector" = "You can add a group to your folder"; + "lng_context_animated_emoji" = "This message contains emoji from **{name} pack**."; "lng_context_animated_emoji_many#one" = "This message contains emoji from **{count} pack**."; "lng_context_animated_emoji_many#other" = "This message contains emoji from **{count} packs**."; @@ -4591,6 +4900,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_paid_react_send" = "Send {price}"; "lng_paid_react_agree" = "By sending stars, you agree to the {link}."; "lng_paid_react_agree_link" = "Terms of Service"; +"lng_paid_react_admin_cant" = "You can't send Stars to your own Live Story."; "lng_paid_react_toast#one" = "Star Sent!"; "lng_paid_react_toast#other" = "Stars Sent!"; "lng_paid_react_toast_anonymous#one" = "Star sent anonymously!"; @@ -4601,6 +4911,20 @@ 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_paid_admin_title" = "Receive Stars from Viewers"; +"lng_paid_admin_about" = "Viewers can send you Star Reactions."; + "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?"; @@ -6093,12 +6417,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_export_option_contacts_about" = "If you allow access, contacts are continuously synced with Telegram. You can adjust this in Settings > Privacy & Security on mobile devices."; "lng_export_option_stories" = "Story archive"; "lng_export_option_stories_about" = "All stories you posted from Telegram mobile apps."; +"lng_export_option_profile_music" = "Music on Profiles"; +"lng_export_option_profile_music_about" = "All tracks you saved to your playlist."; "lng_export_option_sessions" = "Active sessions"; "lng_export_option_sessions_about" = "We may store this to display your connected devices in Settings > Privacy & Security > Show all sessions."; "lng_export_header_other" = "Other"; "lng_export_option_other" = "Miscellaneous data"; "lng_export_option_other_about" = "Other types of data not mentioned above (beta)."; "lng_export_header_chats" = "Chat export settings"; +"lng_export_header_topic" = "Topic export settings"; "lng_export_option_personal_chats" = "Personal chats"; "lng_export_option_bot_chats" = "Bot chats"; "lng_export_option_private_groups" = "Private groups"; @@ -6593,6 +6920,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_stories_my_name" = "My Story"; "lng_stories_archive" = "Hide Stories"; "lng_stories_unarchive" = "Unhide Stories"; +"lng_stories_view_anonymously" = "View Anonymously"; "lng_stories_row_count#one" = "{count} Story"; "lng_stories_row_count#other" = "{count} Stories"; "lng_stories_views#one" = "{count} view"; @@ -6687,6 +7015,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_stealth_mode_next_about" = "Hide my views for the next 25 minutes."; "lng_stealth_mode_unlock" = "Unlock Stealth Mode"; "lng_stealth_mode_enable" = "Enable Stealth Mode"; +"lng_stealth_mode_enable_and_open" = "Enable and open the story"; "lng_stealth_mode_cooldown_in" = "Available in {left}"; "lng_stealth_mode_cooldown_tip" = "Please wait until **Stealth Mode** is ready to use again."; "lng_stealth_mode_enabled_tip_title" = "Stealth Mode On"; @@ -6696,6 +7025,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"; diff --git a/Telegram/Resources/qrc/telegram/animations.qrc b/Telegram/Resources/qrc/telegram/animations.qrc index 363c9e924a..34e0b4591e 100644 --- a/Telegram/Resources/qrc/telegram/animations.qrc +++ b/Telegram/Resources/qrc/telegram/animations.qrc @@ -45,6 +45,15 @@ ../../animations/photo_suggest_icon.tgs ../../animations/toast/saved_messages.tgs ../../animations/toast/tagged.tgs + ../../animations/my_gifts_empty.tgs + ../../animations/toast/chats_filter_in.tgs + ../../animations/rtmp.tgs + ../../animations/show_or_premium_lastseen.tgs + ../../animations/show_or_premium_readtime.tgs + ../../animations/passkeys.tgs + + ../../animations/profile/profile_muting.tgs + ../../animations/profile/profile_unmuting.tgs ../../animations/dice/dice_idle.tgs ../../animations/dice/dart_idle.tgs diff --git a/Telegram/Resources/qrc/telegram/export.qrc b/Telegram/Resources/qrc/telegram/export.qrc index 290d24a30b..e3656ae944 100644 --- a/Telegram/Resources/qrc/telegram/export.qrc +++ b/Telegram/Resources/qrc/telegram/export.qrc @@ -31,6 +31,8 @@ ../../export_html/images/section_contacts@2x.png ../../export_html/images/section_frequent.png ../../export_html/images/section_frequent@2x.png + ../../export_html/images/section_music.png + ../../export_html/images/section_music@2x.png ../../export_html/images/section_other.png ../../export_html/images/section_other@2x.png ../../export_html/images/section_photos.png diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml index a237a7a381..a2a0a88f9d 100644 --- a/Telegram/Resources/uwp/AppX/AppxManifest.xml +++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml @@ -10,7 +10,7 @@ + Version="6.3.6.0" /> Telegram Desktop Telegram Messenger LLP diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc index 377039706d..10970299d8 100644 --- a/Telegram/Resources/winrc/Telegram.rc +++ b/Telegram/Resources/winrc/Telegram.rc @@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico" // VS_VERSION_INFO VERSIONINFO - FILEVERSION 6,2,4,0 - PRODUCTVERSION 6,2,4,0 + FILEVERSION 6,3,6,0 + PRODUCTVERSION 6,3,6,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -62,10 +62,10 @@ BEGIN BEGIN VALUE "CompanyName", "Radolyn Labs" VALUE "FileDescription", "AyuGram Desktop" - VALUE "FileVersion", "6.2.4.0" + VALUE "FileVersion", "6.3.6.0" VALUE "LegalCopyright", "Copyright (C) 2014-2025" VALUE "ProductName", "AyuGram Desktop" - VALUE "ProductVersion", "6.2.4.0" + VALUE "ProductVersion", "6.3.6.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc index 6d075b5aed..2a5ec310d1 100644 --- a/Telegram/Resources/winrc/Updater.rc +++ b/Telegram/Resources/winrc/Updater.rc @@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US // VS_VERSION_INFO VERSIONINFO - FILEVERSION 6,2,4,0 - PRODUCTVERSION 6,2,4,0 + FILEVERSION 6,3,6,0 + PRODUCTVERSION 6,3,6,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -53,10 +53,10 @@ BEGIN BEGIN VALUE "CompanyName", "Radolyn Labs" VALUE "FileDescription", "AyuGram Desktop Updater" - VALUE "FileVersion", "6.2.4.0" + VALUE "FileVersion", "6.3.6.0" VALUE "LegalCopyright", "Copyright (C) 2014-2025" VALUE "ProductName", "AyuGram Desktop" - VALUE "ProductVersion", "6.2.4.0" + VALUE "ProductVersion", "6.3.6.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/SourceFiles/api/api_bot.cpp b/Telegram/SourceFiles/api/api_bot.cpp index 05c0d46928..5eeddb0a18 100644 --- a/Telegram/SourceFiles/api/api_bot.cpp +++ b/Telegram/SourceFiles/api/api_bot.cpp @@ -400,7 +400,7 @@ void ActivateBotCommand(ClickHandlerContext context, int row, int column) { } } const auto replyTo = FullReplyTo(); - const auto suggest = SuggestPostOptions(); + const auto suggest = SuggestOptions(); Window::PeerMenuCreatePoll( controller, item->history()->peer, diff --git a/Telegram/SourceFiles/api/api_common.cpp b/Telegram/SourceFiles/api/api_common.cpp index 55017ade84..9e86c94abf 100644 --- a/Telegram/SourceFiles/api/api_common.cpp +++ b/Telegram/SourceFiles/api/api_common.cpp @@ -14,7 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Api { -MTPSuggestedPost SuggestToMTP(SuggestPostOptions suggest) { +MTPSuggestedPost SuggestToMTP(SuggestOptions suggest) { using Flag = MTPDsuggestedPost::Flag; return suggest.exists ? MTP_suggestedPost( diff --git a/Telegram/SourceFiles/api/api_common.h b/Telegram/SourceFiles/api/api_common.h index bbbff2b0cc..44233354be 100644 --- a/Telegram/SourceFiles/api/api_common.h +++ b/Telegram/SourceFiles/api/api_common.h @@ -19,12 +19,13 @@ namespace Api { inline constexpr auto kScheduledUntilOnlineTimestamp = TimeId(0x7FFFFFFE); -[[nodiscard]] MTPSuggestedPost SuggestToMTP(SuggestPostOptions suggest); +[[nodiscard]] MTPSuggestedPost SuggestToMTP(SuggestOptions suggest); struct SendOptions { uint64 price = 0; PeerData *sendAs = nullptr; TimeId scheduled = 0; + TimeId scheduleRepeatPeriod = 0; BusinessShortcutId shortcutId = 0; EffectId effectId = 0; int starsApproved = 0; @@ -33,7 +34,7 @@ struct SendOptions { bool invertCaption = false; bool hideViaBot = false; crl::time ttlSeconds = 0; - SuggestPostOptions suggest; + SuggestOptions suggest; friend inline bool operator==( const SendOptions &, diff --git a/Telegram/SourceFiles/api/api_credits_history_entry.cpp b/Telegram/SourceFiles/api/api_credits_history_entry.cpp index dbf616471d..2aa3423e85 100644 --- a/Telegram/SourceFiles/api/api_credits_history_entry.cpp +++ b/Telegram/SourceFiles/api/api_credits_history_entry.cpp @@ -146,6 +146,8 @@ Data::CreditsHistoryEntry CreditsHistoryEntryFromTL( ? starrefAmount : CreditsAmount()), .paidMessagesCommission = paidMessagesCount ? starrefCommission : 0, + .limitedCount = parsedGift ? parsedGift->limitedCount : 0, + .limitedLeft = parsedGift ? parsedGift->limitedLeft : 0, .starsConverted = int(nonUniqueGift ? nonUniqueGift->vconvert_stars().v : 0), diff --git a/Telegram/SourceFiles/api/api_editing.cpp b/Telegram/SourceFiles/api/api_editing.cpp index e3ec52e172..c3afa0b728 100644 --- a/Telegram/SourceFiles/api/api_editing.cpp +++ b/Telegram/SourceFiles/api/api_editing.cpp @@ -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, diff --git a/Telegram/SourceFiles/api/api_global_privacy.cpp b/Telegram/SourceFiles/api/api_global_privacy.cpp index 0beb421833..3dbce0425a 100644 --- a/Telegram/SourceFiles/api/api_global_privacy.cpp +++ b/Telegram/SourceFiles/api/api_global_privacy.cpp @@ -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()); diff --git a/Telegram/SourceFiles/api/api_global_privacy.h b/Telegram/SourceFiles/api/api_global_privacy.h index 7346f2ff00..dcd86edddc 100644 --- a/Telegram/SourceFiles/api/api_global_privacy.h +++ b/Telegram/SourceFiles/api/api_global_privacy.h @@ -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; } diff --git a/Telegram/SourceFiles/api/api_peer_colors.cpp b/Telegram/SourceFiles/api/api_peer_colors.cpp index 5f3fe12d20..330863d90d 100644 --- a/Telegram/SourceFiles/api/api_peer_colors.cpp +++ b/Telegram/SourceFiles/api/api_peer_colors.cpp @@ -9,7 +9,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "data/data_peer.h" +#include "window/themes/window_theme.h" #include "ui/chat/chat_style.h" +#include "ui/color_int_conversion.h" namespace Api { namespace { @@ -20,8 +22,9 @@ constexpr auto kRequestEach = 3600 * crl::time(1000); PeerColors::PeerColors(not_null api) : _api(&api->instance()) -, _timer([=] { request(); }) { +, _timer([=] { request(); requestProfile(); }) { request(); + requestProfile(); _timer.callEach(kRequestEach); } @@ -45,6 +48,24 @@ void PeerColors::request() { }).send(); } +void PeerColors::requestProfile() { + if (_profileRequestId) { + return; + } + _profileRequestId = _api.request(MTPhelp_GetPeerProfileColors( + MTP_int(_profileHash) + )).done([=](const MTPhelp_PeerColors &result) { + _profileRequestId = 0; + result.match([&](const MTPDhelp_peerColors &data) { + _profileHash = data.vhash().v; + applyProfile(data); + }, [](const MTPDhelp_peerColorsNotModified &) { + }); + }).fail([=] { + _profileRequestId = 0; + }).send(); +} + std::vector PeerColors::suggested() const { return _suggested.current(); } @@ -76,21 +97,27 @@ const base::flat_map &PeerColors::requiredLevelsChannel() const { return _requiredLevelsChannel; } -int PeerColors::requiredGroupLevelFor(PeerId channel, uint8 index) const { +int PeerColors::requiredLevelFor( + PeerId channel, + uint8 index, + bool isMegagroup, + bool profile) const { if (Data::DecideColorIndex(channel) == index) { return 0; - } else if (const auto i = _requiredLevelsGroup.find(index) - ; i != end(_requiredLevelsGroup)) { - return i->second; } - return 1; -} - -int PeerColors::requiredChannelLevelFor(PeerId channel, uint8 index) const { - if (Data::DecideColorIndex(channel) == index) { - return 0; - } else if (const auto i = _requiredLevelsChannel.find(index) - ; i != end(_requiredLevelsChannel)) { + if (profile) { + const auto it = _profileColors.find(index); + if (it != end(_profileColors)) { + return isMegagroup + ? it->second.requiredLevelsGroup + : it->second.requiredLevelsChannel; + } + return 1; + } + const auto &levels = isMegagroup + ? _requiredLevelsGroup + : _requiredLevelsChannel; + if (const auto i = levels.find(index); i != end(levels)) { return i->second; } return 1; @@ -165,4 +192,87 @@ void PeerColors::apply(const MTPDhelp_peerColors &data) { _suggested = std::move(suggested); } +void PeerColors::applyProfile(const MTPDhelp_peerColors &data) { + const auto parseColors = [](const MTPhelp_PeerColorSet &set) { + const auto toUint = [](const MTPint &c) { + return (uint32(1) << 24) | uint32(c.v); + }; + return set.match([&](const MTPDhelp_peerColorSet &) { + LOG(("API Error: peerColorSet in profile colors result!")); + return Data::ColorProfileSet(); + }, [&](const MTPDhelp_peerColorProfileSet &data) { + auto set = Data::ColorProfileSet(); + set.palette.reserve(data.vpalette_colors().v.size()); + set.bg.reserve(data.vbg_colors().v.size()); + set.story.reserve(data.vstory_colors().v.size()); + for (const auto &c : data.vpalette_colors().v) { + set.palette.push_back(Ui::ColorFromSerialized(toUint(c))); + } + for (const auto &c : data.vbg_colors().v) { + set.bg.push_back(Ui::ColorFromSerialized(toUint(c))); + } + for (const auto &c : data.vstory_colors().v) { + set.story.push_back(Ui::ColorFromSerialized(toUint(c))); + } + return set; + }); + }; + + auto suggested = std::vector(); + const auto &list = data.vcolors().v; + suggested.reserve(list.size()); + for (const auto &color : list) { + const auto &data = color.data(); + const auto colorIndexBare = data.vcolor_id().v; + if (colorIndexBare < 0 || colorIndexBare >= Ui::kColorIndexCount) { + LOG(("API Error: Bad color index: %1").arg(colorIndexBare)); + continue; + } + const auto colorIndex = uint8(colorIndexBare); + auto result = ProfileColorOption(); + result.isHidden = data.is_hidden(); + if (const auto min = data.vgroup_min_level()) { + result.requiredLevelsGroup = min->v; + } + if (const auto min = data.vchannel_min_level()) { + result.requiredLevelsChannel = min->v; + } + if (const auto light = data.vcolors()) { + result.data.light = parseColors(*light); + } + if (const auto dark = data.vdark_colors()) { + result.data.dark = parseColors(*dark); + } + _profileColors[colorIndex] = std::move(result); + } +} + +std::optional PeerColors::colorProfileFor( + not_null peer) const { + if (const auto colorProfileIndex = peer->colorProfileIndex()) { + return colorProfileFor(*colorProfileIndex); + } + return std::nullopt; +} + +std::optional PeerColors::colorProfileFor( + uint8 index) const { + const auto i = _profileColors.find(index); + if (i != end(_profileColors)) { + return Window::Theme::IsNightMode() + ? i->second.data.dark + : i->second.data.light; + } + return std::nullopt; +} + +std::vector PeerColors::profileColorIndices() const { + auto result = std::vector(); + result.reserve(_profileColors.size()); + for (const auto &[index, option] : _profileColors) { + result.push_back(index); + } + return result; +} + } // namespace Api diff --git a/Telegram/SourceFiles/api/api_peer_colors.h b/Telegram/SourceFiles/api/api_peer_colors.h index f8d379020b..690073d856 100644 --- a/Telegram/SourceFiles/api/api_peer_colors.h +++ b/Telegram/SourceFiles/api/api_peer_colors.h @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "base/timer.h" +#include "data/data_peer_colors.h" #include "mtproto/sender.h" class ApiWrap; @@ -34,27 +35,45 @@ public: [[nodiscard]] auto requiredLevelsChannel() const -> const base::flat_map &; - [[nodiscard]] int requiredGroupLevelFor( - PeerId channel, - uint8 index) const; - [[nodiscard]] int requiredChannelLevelFor( + [[nodiscard]] int requiredLevelFor( PeerId channel, + uint8 index, + bool isMegagroup, + bool profile) const; + + [[nodiscard]] std::optional colorProfileFor( + not_null peer) const; + [[nodiscard]] std::optional colorProfileFor( uint8 index) const; + [[nodiscard]] std::vector profileColorIndices() const; + private: + struct ProfileColorOption { + Data::ColorProfileData data; + int requiredLevelsChannel = 0; + int requiredLevelsGroup = 0; + bool isHidden = false; + }; + void request(); + void requestProfile(); void apply(const MTPDhelp_peerColors &data); + void applyProfile(const MTPDhelp_peerColors &data); MTP::Sender _api; int32 _hash = 0; + int32 _profileHash = 0; mtpRequestId _requestId = 0; + mtpRequestId _profileRequestId = 0; base::Timer _timer; rpl::variable> _suggested; base::flat_map _requiredLevelsGroup; base::flat_map _requiredLevelsChannel; rpl::event_stream<> _colorIndicesChanged; std::unique_ptr _colorIndicesCurrent; + base::flat_map _profileColors; }; diff --git a/Telegram/SourceFiles/api/api_polls.cpp b/Telegram/SourceFiles/api/api_polls.cpp index b259be273a..06cc565aa4 100644 --- a/Telegram/SourceFiles/api/api_polls.cpp +++ b/Telegram/SourceFiles/api/api_polls.cpp @@ -66,6 +66,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; @@ -100,6 +103,7 @@ void Polls::create( MTPReplyMarkup(), MTPVector(), 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), @@ -202,6 +206,7 @@ void Polls::close(not_null item) { MTPReplyMarkup(), MTPVector(), MTP_int(0), // schedule_date + MTP_int(0), // schedule_repeat_period MTPint() // quick_reply_shortcut_id )).done([=](const MTPUpdates &result) { _pollCloseRequestIds.erase(itemId); diff --git a/Telegram/SourceFiles/api/api_premium.cpp b/Telegram/SourceFiles/api/api_premium.cpp index 562bdfa866..316681746a 100644 --- a/Telegram/SourceFiles/api/api_premium.cpp +++ b/Telegram/SourceFiles/api/api_premium.cpp @@ -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(), }; } @@ -848,8 +848,22 @@ std::optional FromTL( const auto releasedBy = releasedById ? session->data().peer(releasedById).get() : nullptr; + const auto background = [&] { + if (!data.vbackground()) { + return std::shared_ptr(); + } + const auto &fields = data.vbackground()->data(); + using namespace Ui; + return std::make_shared( + Data::StarGiftBackground{ + .center = ColorFromSerialized(fields.vcenter_color()), + .edge = ColorFromSerialized(fields.vedge_color()), + .text = ColorFromSerialized(fields.vtext_color()), + }); + }; return std::optional(Data::StarGift{ .id = uint64(data.vid().v), + .background = background(), .stars = int64(data.vstars().v), .starsConverted = int64(data.vconvert_stars().v), .starsToUpgrade = int64(data.vupgrade_stars().value_or_empty()), @@ -858,10 +872,14 @@ std::optional 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(), + .auctionStartDate = data.vauction_start_date().value_or_empty(), .limitedLeft = remaining.value_or_empty(), .limitedCount = total.value_or_empty(), .perUserTotal = data.vper_user_total().value_or_empty(), .perUserRemains = data.vper_user_remains().value_or_empty(), + .upgradeVariants = data.vupgrade_variants().value_or_empty(), .firstSaleDate = data.vfirst_sale_date().value_or_empty(), .lastSaleDate = data.vlast_sale_date().value_or_empty(), .lockedUntilDate = data.vlocked_until_date().value_or_empty(), @@ -928,6 +946,7 @@ std::optional FromTL( .themeUser = themeUser, .nanoTonForResale = FindTonForResale(data.vresell_amount()), .starsForResale = FindStarsForResale(data.vresell_amount()), + .starsMinOffer = data.voffer_min_stars().value_or(-1), .number = data.vnum().v, .onlyAcceptTon = data.is_resale_ton_only(), .canBeTheme = data.is_theme_available(), @@ -940,6 +959,8 @@ std::optional FromTL( data.vvalue_currency().value_or_empty()), .valuePrice = int64( data.vvalue_amount().value_or_empty()), + .valuePriceUsd = int64( + data.vvalue_usd_amount().value_or_empty()), }) : nullptr), .peerColor = colorCollectible, @@ -961,7 +982,7 @@ std::optional FromTL( unique->originalDetails = FromTL(session, data); }); } - return std::make_optional(result); + return std::make_optional(std::move(result)); }); } @@ -1007,6 +1028,7 @@ std::optional FromTL( ? peerFromMTP(*data.vfrom_id()) : PeerId()), .date = data.vdate().v, + .giftNum = data.vgift_num().value_or_empty(), .upgradeSeparate = data.is_upgrade_separate(), .upgradable = data.is_can_upgrade(), .anonymous = data.is_name_hidden(), diff --git a/Telegram/SourceFiles/api/api_premium.h b/Telegram/SourceFiles/api/api_premium.h index e3eb8f1e45..08b0f28b1b 100644 --- a/Telegram/SourceFiles/api/api_premium.h +++ b/Telegram/SourceFiles/api/api_premium.h @@ -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==( diff --git a/Telegram/SourceFiles/api/api_premium_option.cpp b/Telegram/SourceFiles/api/api_premium_option.cpp index f5f0af380d..5cc9c9a125 100644 --- a/Telegram/SourceFiles/api/api_premium_option.cpp +++ b/Telegram/SourceFiles/api/api_premium_option.cpp @@ -36,7 +36,9 @@ Data::PremiumSubscriptionOption CreateSubscriptionOption( .costNoDiscount = Ui::FillAmountAndCurrency( monthlyAmount * months, currency), - .costTotal = Ui::FillAmountAndCurrency(amount, currency), + .costPerYear = Ui::FillAmountAndCurrency( + amount / float64(months / 12.), + currency), .botUrl = botUrl, }; } diff --git a/Telegram/SourceFiles/api/api_sending.cpp b/Telegram/SourceFiles/api/api_sending.cpp index cbfa6a937d..a77cc1beb4 100644 --- a/Telegram/SourceFiles/api/api_sending.cpp +++ b/Telegram/SourceFiles/api/api_sending.cpp @@ -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(), 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(), 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), diff --git a/Telegram/SourceFiles/api/api_suggest_post.cpp b/Telegram/SourceFiles/api/api_suggest_post.cpp index 05d0304452..cb5aa52e3c 100644 --- a/Telegram/SourceFiles/api/api_suggest_post.cpp +++ b/Telegram/SourceFiles/api/api_suggest_post.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "base/unixtime.h" +#include "boxes/transfer_gift_box.h" #include "chat_helpers/message_field.h" #include "core/click_handler_types.h" #include "data/components/credits.h" @@ -44,7 +45,7 @@ void SendApproval( not_null item, TimeId scheduleDate = 0) { using Flag = MTPmessages_ToggleSuggestedPostApproval::Flag; - const auto suggestion = item->Get(); + const auto suggestion = item->Get(); if (!suggestion || suggestion->accepted || suggestion->rejected @@ -56,7 +57,7 @@ void SendApproval( const auto session = &show->session(); const auto finish = [=] { if (const auto item = session->data().message(id)) { - const auto suggestion = item->Get(); + const auto suggestion = item->Get(); if (suggestion) { suggestion->requestId = 0; } @@ -83,7 +84,7 @@ void ConfirmApproval( not_null item, TimeId scheduleDate = 0, Fn accepted = nullptr) { - const auto suggestion = item->Get(); + const auto suggestion = item->Get(); if (!suggestion || suggestion->accepted || suggestion->rejected @@ -244,7 +245,7 @@ void SendDecline( not_null item, const QString &comment) { using Flag = MTPmessages_ToggleSuggestedPostApproval::Flag; - const auto suggestion = item->Get(); + const auto suggestion = item->Get(); if (!suggestion || suggestion->accepted || suggestion->rejected @@ -256,7 +257,7 @@ void SendDecline( const auto session = &show->session(); const auto finish = [=] { if (const auto item = session->data().message(id)) { - const auto suggestion = item->Get(); + const auto suggestion = item->Get(); if (suggestion) { suggestion->requestId = 0; } @@ -365,10 +366,10 @@ void SendSuggest( std::shared_ptr show, not_null item, std::shared_ptr state, - Fn modify, + Fn modify, Fn done = nullptr, int starsApproved = 0) { - const auto suggestion = item->Get(); + const auto suggestion = item->Get(); const auto id = item->fullId(); const auto withPaymentApproved = [=](int stars) { if (const auto item = show->session().data().message(id)) { @@ -416,7 +417,7 @@ void SendSuggest( void SuggestApprovalDate( std::shared_ptr show, not_null item) { - const auto suggestion = item->Get(); + const auto suggestion = item->Get(); if (!suggestion) { return; } @@ -437,7 +438,7 @@ void SuggestApprovalDate( show, item, state, - [=](SuggestPostOptions &options) { options.date = result; }, + [=](SuggestOptions &options) { options.date = result; }, close); }; using namespace HistoryView; @@ -454,12 +455,12 @@ void SuggestApprovalDate( void SuggestOfferForMessage( std::shared_ptr show, not_null item, - SuggestPostOptions values, + SuggestOptions values, HistoryView::SuggestMode mode) { const auto id = item->fullId(); const auto state = std::make_shared(); const auto weak = std::make_shared>(); - const auto done = [=](SuggestPostOptions result) { + const auto done = [=](SuggestOptions result) { const auto item = show->session().data().message(id); if (!item) { return; @@ -473,7 +474,7 @@ void SuggestOfferForMessage( show, item, state, - [=](SuggestPostOptions &options) { options = result; }, + [=](SuggestOptions &options) { options = result; }, close); }; using namespace HistoryView; @@ -490,7 +491,7 @@ void SuggestOfferForMessage( void SuggestApprovalPrice( std::shared_ptr show, not_null item) { - const auto suggestion = item->Get(); + const auto suggestion = item->Get(); if (!suggestion) { return; } @@ -504,6 +505,20 @@ void SuggestApprovalPrice( }, SuggestMode::Change); } +void ConfirmGiftSaleAccept( + not_null window, + not_null item, + not_null suggestion) { + ShowGiftSaleAcceptBox(window, item, suggestion); +} + +void ConfirmGiftSaleDecline( + not_null window, + not_null item, + not_null suggestion) { + ShowGiftSaleRejectBox(window, item, suggestion); +} + } // namespace std::shared_ptr AcceptClickHandler( @@ -521,9 +536,11 @@ std::shared_ptr AcceptClickHandler( return; } const auto show = controller->uiShow(); - const auto suggestion = item->Get(); + const auto suggestion = item->Get(); if (!suggestion) { return; + } else if (suggestion->gift) { + ConfirmGiftSaleAccept(controller, item, suggestion); } else if (!suggestion->date) { RequestApprovalDate(show, item); } else { @@ -546,7 +563,12 @@ std::shared_ptr DeclineClickHandler( if (!item) { return; } - RequestDeclineComment(controller->uiShow(), item); + const auto suggestion = item->Get(); + if (suggestion && suggestion->gift) { + ConfirmGiftSaleDecline(controller, item, suggestion); + } else { + RequestDeclineComment(controller->uiShow(), item); + } }); } @@ -573,7 +595,7 @@ std::shared_ptr SuggestChangesClickHandler( if (!item) { return; } - const auto suggestion = item->Get(); + const auto suggestion = item->Get(); if (!suggestion) { return; } @@ -594,7 +616,7 @@ std::shared_ptr SuggestChangesClickHandler( .messageId = FullMsgId(history->peer->id, item->id), .monoforumPeerId = monoforumPeerId, }, - SuggestPostOptions{ + SuggestOptions{ .exists = uint32(1), .priceWhole = uint32(suggestion->price.whole()), .priceNano = uint32(suggestion->price.nano()), diff --git a/Telegram/SourceFiles/api/api_todo_lists.cpp b/Telegram/SourceFiles/api/api_todo_lists.cpp index 7471f55846..192df853bd 100644 --- a/Telegram/SourceFiles/api/api_todo_lists.cpp +++ b/Telegram/SourceFiles/api/api_todo_lists.cpp @@ -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(), 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), diff --git a/Telegram/SourceFiles/api/api_toggling_media.cpp b/Telegram/SourceFiles/api/api_toggling_media.cpp index 65d2ce317f..6c28df197d 100644 --- a/Telegram/SourceFiles/api/api_toggling_media.cpp +++ b/Telegram/SourceFiles/api/api_toggling_media.cpp @@ -24,7 +24,7 @@ void ToggleExistingMedia( Data::FileOrigin origin, ToggleRequestCallback toggleRequest, DoneCallback &&done) { - const auto api = &document->owner().session().api(); + const auto api = &document->session().api(); auto performRequest = [=](const auto &repeatRequest) -> void { const auto usedFileReference = document->fileReference(); diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index 3906d748fd..7e7025b2c5 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -23,12 +23,14 @@ 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" #include "data/notify/data_notify_settings.h" #include "data/stickers/data_stickers.h" #include "data/data_saved_messages.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_user.h" #include "data/data_chat.h" @@ -42,6 +44,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_histories.h" #include "data/data_folder.h" #include "data/data_forum.h" +#include "data/data_forum_topic.h" #include "data/data_send_action.h" #include "data/data_stories.h" #include "data/data_message_reactions.h" @@ -1243,7 +1246,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; @@ -1283,7 +1287,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; @@ -1707,13 +1712,21 @@ void Updates::feedUpdate(const MTPUpdate &update) { local->history()->peer, local->date()); } - local->setRealId(d.vid().v); + local->setRealId(newId); + if (const auto topic = local->topic()) { + topic->applyMaybeLast(local); + } + if (const auto sublist = local->savedSublist()) { + sublist->applyMaybeLast(local); + } } } } else { owner.histories().checkTopicCreated(id, newId); } session().data().unregisterMessageRandomId(randomId); + } else { + Core::App().calls().handleUpdate(&session(), update); } session().data().unregisterMessageSentData(randomId); } break; @@ -2151,7 +2164,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; @@ -2794,6 +2808,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; } } diff --git a/Telegram/SourceFiles/api/api_user_privacy.cpp b/Telegram/SourceFiles/api/api_user_privacy.cpp index e07858dda7..9b968392f6 100644 --- a/Telegram/SourceFiles/api/api_user_privacy.cpp +++ b/Telegram/SourceFiles/api/api_user_privacy.cpp @@ -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 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; } diff --git a/Telegram/SourceFiles/api/api_user_privacy.h b/Telegram/SourceFiles/api/api_user_privacy.h index 676e9be11d..42a5b1e946 100644 --- a/Telegram/SourceFiles/api/api_user_privacy.h +++ b/Telegram/SourceFiles/api/api_user_privacy.h @@ -33,6 +33,7 @@ public: Birthday, GiftsAutoSave, NoPaidMessages, + SavedMusic, }; enum class Option { Everyone, diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index fd9becfc29..10b077e301 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -805,8 +805,10 @@ QString ApiWrap::exportDirectStoryLink(not_null 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); @@ -1234,6 +1236,10 @@ void ApiWrap::gotUserFull( void ApiWrap::requestPeerSettings(not_null peer) { if (!_requestedPeerSettings.emplace(peer).second) { return; + } else if (peer->isMonoforum()) { + peer->setBarSettings(PeerBarSettings()); + _requestedPeerSettings.erase(peer); + return; } request(MTPmessages_GetPeerSettings( peer->input @@ -1245,6 +1251,7 @@ void ApiWrap::requestPeerSettings(not_null peer) { _requestedPeerSettings.erase(peer); }); }).fail([=] { + peer->setBarSettings(PeerBarSettings()); _requestedPeerSettings.erase(peer); }).send(); } @@ -1775,6 +1782,11 @@ void ApiWrap::joinChannel(not_null channel) { )).done([=](const MTPUpdates &result) { _channelAmInRequests.remove(channel); applyUpdates(result); + + session().data().addRecentJoinChat({ + .fromPeerId = channel->id, + .joinedPeerId = channel->id, + }); }).fail([=](const MTP::Error &error) { const auto &type = error.type(); @@ -3518,11 +3530,17 @@ 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; sendFlags |= SendFlag::f_quick_reply_shortcut; } + if (action.options.effectId) { + sendFlags |= SendFlag::f_effect; + } if (draft.options != Data::ForwardOptions::PreserveInfo) { sendFlags |= SendFlag::f_drop_author; } @@ -3585,8 +3603,10 @@ 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), + MTP_long(action.options.effectId), MTPint(), // video_timestamp MTP_long(starsPaid), Api::SuggestToMTP(action.options.suggest) @@ -4131,6 +4151,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; @@ -4159,6 +4183,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), @@ -4212,6 +4237,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), @@ -4232,6 +4258,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), @@ -4552,6 +4579,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)) @@ -4581,6 +4611,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), @@ -4639,6 +4670,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)) @@ -4667,6 +4701,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), @@ -4780,6 +4815,9 @@ void ApiWrap::sendAlbumIfReady(not_null 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 @@ -4800,6 +4838,7 @@ void ApiWrap::sendAlbumIfReady(not_null album) { Data::Histories::ReplyToPlaceholder(), MTP_vector(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), diff --git a/Telegram/SourceFiles/boxes/add_contact_box.cpp b/Telegram/SourceFiles/boxes/add_contact_box.cpp index 6e9bc23955..72e71e0ba7 100644 --- a/Telegram/SourceFiles/boxes/add_contact_box.cpp +++ b/Telegram/SourceFiles/boxes/add_contact_box.cpp @@ -111,6 +111,10 @@ void ChatCreateDone( show, chat, CollectForbiddenUsers(&chat->session(), result)); + chat->owner().addRecentJoinChat({ + .fromPeerId = chat->id, + .joinedPeerId = chat->id, + }); } }; if (!success) { @@ -454,10 +458,12 @@ void AddContactBox::save() { MTP_vector( 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(); diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style index 21e74b0fce..77a7a008ba 100644 --- a/Telegram/SourceFiles/boxes/boxes.style +++ b/Telegram/SourceFiles/boxes/boxes.style @@ -378,10 +378,8 @@ connectionPortInputField: InputField(defaultInputField) { width: 55px; } connectionUserInputField: InputField(defaultInputField) { - width: 95px; } connectionPasswordInputField: InputField(defaultInputField) { - width: 120px; } connectionIPv6Skip: 11px; @@ -537,7 +535,6 @@ changePhoneDescription: FlatLabel(defaultFlatLabel) { } changePhoneDescriptionPadding: margins(0px, 1px, 0px, 8px); changePhoneIconPadding: margins(0px, 39px, 0px, 5px); -changePhoneIconSize: 120px; changePhoneLabel: FlatLabel(defaultFlatLabel) { minWidth: 275px; textFg: windowSubTextFg; @@ -546,6 +543,8 @@ changePhoneError: FlatLabel(changePhoneLabel) { textFg: boxTextFgError; } +normalBoxLottieSize: size(120px, 120px); + adminLogFilterUserpicLeft: 15px; adminLogFilterLittleSkip: 16px; adminLogFilterCheckbox: Checkbox(defaultBoxCheckbox) { @@ -928,6 +927,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); @@ -997,9 +1004,8 @@ contactsWithStories: PeerList(peerListBox) { statusPosition: point(70px, 27px); checkbox: RoundImageCheckbox(defaultPeerListCheckbox) { - selectExtendTwice: 1px; imageRadius: 21px; - imageSmallRadius: 19px; + imageSmallRadius: 18px; check: RoundCheckbox(defaultPeerListCheck) { size: 0px; } diff --git a/Telegram/SourceFiles/boxes/connection_box.cpp b/Telegram/SourceFiles/boxes/connection_box.cpp index 898c049d66..9f2ea2ff41 100644 --- a/Telegram/SourceFiles/boxes/connection_box.cpp +++ b/Telegram/SourceFiles/boxes/connection_box.cpp @@ -115,6 +115,7 @@ void AddProxyFromClipboard( Success, Failed, Unsupported, + IncorrectSecret, Invalid, }; @@ -154,8 +155,11 @@ void AddProxyFromClipboard( qthelp::UrlParamNameTransform::ToLower); const auto proxy = ProxyDataFromFields(type, fields); if (!proxy) { - return (proxy.status() == ProxyData::Status::Unsupported) + const auto status = proxy.status(); + return (status == ProxyData::Status::Unsupported) ? Result::Unsupported + : (status == ProxyData::Status::IncorrectSecret) + ? Result::IncorrectSecret : Result::Invalid; } const auto contains = controller->contains(proxy); @@ -189,9 +193,11 @@ void AddProxyFromClipboard( tr::lng_proxy_add_from_clipboard_failed_toast(tr::now)); } else { show->showBox(Ui::MakeInformBox( - (success == Result::Unsupported - ? tr::lng_proxy_unsupported(tr::now) - : tr::lng_proxy_invalid(tr::now)))); + ((success == Result::IncorrectSecret) + ? tr::lng_proxy_incorrect_secret(tr::now, tr::rich) + : (success == Result::Unsupported) + ? tr::lng_proxy_unsupported(tr::now, tr::rich) + : tr::lng_proxy_invalid(tr::now, tr::rich)))); } } } @@ -1183,7 +1189,7 @@ void ProxyBox::setupSocketAddress(const ProxyData &data) { } void ProxyBox::setupCredentials(const ProxyData &data) { - _credentials = _content->add( + _credentials = _content->add( object_ptr>( _content, object_ptr(_content))); @@ -1316,10 +1322,13 @@ void ProxiesBoxController::ShowApplyConfirmation( const QMap &fields) { const auto proxy = ProxyDataFromFields(type, fields); if (!proxy) { + const auto status = proxy.status(); auto box = Ui::MakeInformBox( - (proxy.status() == ProxyData::Status::Unsupported - ? tr::lng_proxy_unsupported(tr::now) - : tr::lng_proxy_invalid(tr::now))); + ((status == ProxyData::Status::Unsupported) + ? tr::lng_proxy_unsupported(tr::now, tr::rich) + : (status == ProxyData::Status::IncorrectSecret) + ? tr::lng_proxy_incorrect_secret(tr::now, tr::rich) + : tr::lng_proxy_invalid(tr::now, tr::rich))); if (controller) { controller->uiShow()->showBox(std::move(box)); } else { diff --git a/Telegram/SourceFiles/boxes/edit_caption_box.cpp b/Telegram/SourceFiles/boxes/edit_caption_box.cpp index cc971734df..24c3ab7d3a 100644 --- a/Telegram/SourceFiles/boxes/edit_caption_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_caption_box.cpp @@ -232,7 +232,7 @@ EditCaptionBox::EditCaptionBox( not_null controller, not_null item, TextWithTags &&text, - SuggestPostOptions suggest, + SuggestOptions suggest, bool spoilered, bool invertCaption, Ui::PreparedList &&list, @@ -273,7 +273,7 @@ void EditCaptionBox::StartMediaReplace( not_null controller, FullMsgId itemId, TextWithTags text, - SuggestPostOptions suggest, + SuggestOptions suggest, bool spoilered, bool invertCaption, Fn saved) { @@ -304,7 +304,7 @@ void EditCaptionBox::StartMediaReplace( FullMsgId itemId, Ui::PreparedList &&list, TextWithTags text, - SuggestPostOptions suggest, + SuggestOptions suggest, bool spoilered, bool invertCaption, Fn saved) { @@ -353,7 +353,7 @@ void EditCaptionBox::StartPhotoEdit( std::shared_ptr media, FullMsgId itemId, TextWithTags text, - SuggestPostOptions suggest, + SuggestOptions suggest, bool spoilered, bool invertCaption, Fn saved) { diff --git a/Telegram/SourceFiles/boxes/edit_caption_box.h b/Telegram/SourceFiles/boxes/edit_caption_box.h index 0b0c5f7f96..569039ad05 100644 --- a/Telegram/SourceFiles/boxes/edit_caption_box.h +++ b/Telegram/SourceFiles/boxes/edit_caption_box.h @@ -39,7 +39,7 @@ public: not_null controller, not_null item, TextWithTags &&text, - SuggestPostOptions suggest, + SuggestOptions suggest, bool spoilered, bool invertCaption, Ui::PreparedList &&list, @@ -50,7 +50,7 @@ public: not_null controller, FullMsgId itemId, TextWithTags text, - SuggestPostOptions suggest, + SuggestOptions suggest, bool spoilered, bool invertCaption, Fn saved); @@ -59,7 +59,7 @@ public: FullMsgId itemId, Ui::PreparedList &&list, TextWithTags text, - SuggestPostOptions suggest, + SuggestOptions suggest, bool spoilered, bool invertCaption, Fn saved); @@ -68,7 +68,7 @@ public: std::shared_ptr media, FullMsgId itemId, TextWithTags text, - SuggestPostOptions suggest, + SuggestOptions suggest, bool spoilered, bool invertCaption, Fn saved); @@ -117,7 +117,7 @@ private: const not_null _controller; const not_null _historyItem; - const SuggestPostOptions _suggest; + const SuggestOptions _suggest; const bool _isAllowedEditMedia; const Ui::AlbumType _albumType; diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp index 7584c9f137..248c170af6 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp @@ -16,11 +16,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/premium_preview_box.h" #include "chat_helpers/emoji_suggestions_widget.h" #include "chat_helpers/message_field.h" +#include "chat_helpers/tabbed_panel.h" +#include "chat_helpers/tabbed_selector.h" #include "core/application.h" #include "core/core_settings.h" #include "core/ui_integration.h" +#include "data/stickers/data_custom_emoji.h" +#include "data/stickers/data_stickers.h" #include "data/data_channel.h" #include "data/data_chat_filters.h" +#include "data/data_document.h" #include "data/data_peer.h" #include "data/data_peer_values.h" // Data::AmPremiumValue. #include "data/data_premium_limits.h" @@ -32,6 +37,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "settings/settings_common.h" #include "ui/chat/chats_filter_tag.h" +#include "ui/controls/emoji_button_factory.h" +#include "ui/controls/emoji_button.h" #include "ui/effects/animation_value_f.h" #include "ui/effects/animations.h" #include "ui/effects/panel_animation.h" @@ -54,6 +61,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_layers.h" #include "styles/style_window.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/style_info_userpic_builder.h" namespace { @@ -357,6 +365,7 @@ void EditFilterBox( rpl::variable title; rpl::variable staticTitle; rpl::variable colorIndex; + base::unique_qptr emojiPanel; }; const auto owner = &window->session().data(); const auto state = box->lifetime().make_state(State{ @@ -423,7 +432,22 @@ void EditFilterBox( current.text, TextUtilities::ConvertEntitiesToTextTags(current.entities), }, Ui::InputField::HistoryAction::Clear); - name->setMaxLength(kMaxFilterTitleLength); + Ui::AddLengthLimitLabel( + name, + kMaxFilterTitleLength, + Ui::LengthLimitLabelOptions{ + .customThreshold = 0, + .customUpdatePosition = [=](QSize parent, QSize label) { + return QPoint( + parent.width() + - st::windowFilterNameCharsLimitRightPosition.x() + - label.width() / 2, + st::windowFilterNameCharsLimitRightPosition.y()); + }, + .customCharactersCount = [=] { + return Ui::ComputeFieldCharacterCount(name); + }, + }); const auto nameEditing = box->lifetime().make_state( NameEditing{ name }); @@ -468,6 +492,47 @@ void EditFilterBox( nameEditing->custom = true; }, box->lifetime()); + using Selector = ChatHelpers::TabbedSelector; + state->emojiPanel = base::make_unique_q( + box->getDelegate()->outerContainer(), + window, + object_ptr( + nullptr, + window->uiShow(), + Window::GifPauseReason::Layer, + Selector::Mode::EmojiOnly)); + state->emojiPanel->setDesiredHeightValues( + 1., + st::emojiPanMinHeight / 2, + st::emojiPanMinHeight); + state->emojiPanel->hide(); + state->emojiPanel->selector()->setCurrentPeer(window->session().user()); + state->emojiPanel->selector()->emojiChosen( + ) | rpl::start_with_next([=](ChatHelpers::EmojiChosen data) { + Ui::InsertEmojiAtCursor(name->textCursor(), data.emoji); + }, name->lifetime()); + state->emojiPanel->selector()->customEmojiChosen( + ) | rpl::start_with_next([=](ChatHelpers::FileChosen data) { + const auto info = data.document->sticker(); + if (info + && info->setType == Data::StickersType::Emoji + && !window->session().premium()) { + ShowPremiumPreviewBox( + window, + PremiumFeature::AnimatedEmoji); + } else { + Data::InsertCustomEmoji(name, data.document); + } + }, name->lifetime()); + + const auto emojiButton = Ui::AddEmojiToggleToField( + name, + box, + window, + state->emojiPanel.get(), + st::windowFilterNameEmojiPosition); + emojiButton->show(); + name->changes( ) | rpl::start_with_next([=] { if (!nameEditing->settingDefault) { @@ -585,18 +650,31 @@ void EditFilterBox( }), anim::type::instant); + const auto &padding = st::defaultSubsectionTitlePadding; const auto isPremium = session->premium(); - const auto title = Ui::AddSubsectionTitle( - colors, - tr::lng_filters_tag_color_subtitle()); - const auto preview = Ui::CreateChild(colors); - title->geometryValue( - ) | rpl::start_with_next([=](const QRect &r) { + const auto titleWrap = colors->add( + object_ptr( + colors, + rect::m::sum::v(padding) + + st::defaultSubsectionTitle.style.font->height)); + const auto title = Ui::CreateChild( + titleWrap, + tr::lng_filters_tag_color_subtitle(), + st::defaultSubsectionTitle); + title->move(rect::m::pos::tl(padding)); + const auto preview = Ui::CreateChild(titleWrap); + rpl::combine( + title->sizeValue(), + titleWrap->widthValue() + ) | rpl::start_with_next([=](const QSize &s, int w) { const auto h = st::normalFont->height; + const auto left = padding.left() + + s.width() + + st::settingsFilterTagPreviewSkip; preview->setGeometry( - rect::right(colors) - st::settingsFilterTagPreviewSkip, - r.y() + (r.height() - h) / 2 + st::lineWidth, - colors->width(), + left, + padding.top() + (s.height() - h) / 2, + w - left, h); }, preview->lifetime()); @@ -608,12 +686,16 @@ void EditFilterBox( }; const auto tag = preview->lifetime().make_state(); tag->context.textContext = Core::TextContext({ .session = session }); + const auto shift = st::settingsFilterTagPreviewSkip / 2; preview->paintRequest() | rpl::start_with_next([=] { auto p = QPainter(preview); p.setOpacity(tag->alpha); const auto size = tag->frame.size() / style::DevicePixelRatio(); const auto rect = QRect( - preview->width() - size.width() - st::boxRowPadding.right(), + preview->width() + - size.width() + - st::boxRowPadding.right() + - shift, (st::normalFont->height - size.height()) / 2, size.width(), size.height()); @@ -623,7 +705,7 @@ void EditFilterBox( p.setFont(st::normalFont); p.setPen(st::windowSubTextFg); p.drawText( - preview->rect() - st::boxRowPadding, + preview->rect().translated(-shift, 0) - st::boxRowPadding, tr::lng_filters_tag_color_no(tr::now), style::al_right); } @@ -741,7 +823,8 @@ void EditFilterBox( const auto staticTitle = !title.entities.isEmpty() && state->staticTitle.current(); const auto rules = data->current(); - if (title.empty()) { + if (Ui::ComputeFieldCharacterCount(name) > kMaxFilterTitleLength + || title.empty()) { name->showError(); box->scrollToY(0); return {}; diff --git a/Telegram/SourceFiles/boxes/gift_premium_box.cpp b/Telegram/SourceFiles/boxes/gift_premium_box.cpp index 0a1a8333e3..8ed8781e1a 100644 --- a/Telegram/SourceFiles/boxes/gift_premium_box.cpp +++ b/Telegram/SourceFiles/boxes/gift_premium_box.cpp @@ -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 parent; - Ui::ImportantTooltip *raw = nullptr; -}; - -void ShowInfoTooltip( - std::shared_ptr data, - not_null target, - rpl::producer text, - int duration) { - if (data->raw) { - data->raw->toggleAnimated(false); - } - const auto parent = data->parent; - const auto tooltip = Ui::CreateChild( - parent, - Ui::MakeNiceTooltipLabel( - parent, - std::move(text), - st::boxWideWidth, - st::defaultImportantTooltipLabel), - st::defaultImportantTooltip); - tooltip->toggleFast(false); - - base::install_event_filter(tooltip, qApp, [=](not_null 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 session, @@ -236,8 +168,10 @@ void ShowInfoTooltip( return result; } -[[nodiscard]] tr::phrase GiftDurationPhrase(int months) { - return (months < 12) +[[nodiscard]] tr::phrase 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 MakeValueWithSmallButton( - not_null table, - not_null value, - rpl::producer buttonText, - Fn 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(table); - const auto raw = result.data(); - - value->setParent(raw); - value->show(); - - const auto button = Ui::CreateChild( - 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 MakePeerTableValue( - not_null table, - std::shared_ptr show, - PeerId id, - rpl::producer button = nullptr, - Fn handler = nullptr) { - auto result = object_ptr(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(raw, peer, st); - const auto label = Ui::CreateChild( - 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 button) { handler(); }, - st::giveawayGiftCodeNamePosition.y()); -} - -[[nodiscard]] object_ptr MakePeerWithStatusValue( - not_null table, - std::shared_ptr show, - PeerId id, - Fn, EmojiStatusId)> pushStatusId) { - auto result = object_ptr(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 content; - }; - const auto peer = show->session().data().peer(id); - const auto state = peerLabel->lifetime().make_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( - 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 MakeHiddenPeerTableValue( - not_null table) { - auto result = object_ptr(table); - const auto raw = result.data(); - - const auto &st = st::giveawayGiftCodeUserpic; - raw->resize(raw->width(), st.photoSize); - - const auto userpic = Ui::CreateChild(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( - 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 table, - rpl::producer label, - object_ptr value, - style::margins valueMargins = st::giveawayGiftCodeValueMargin) { - table->addRow( - (label - ? object_ptr( - table, - std::move(label), - table->st().defaultLabel) - : object_ptr(nullptr)), - std::move(value), - st::giveawayGiftCodeLabelMargin, - valueMargins); -} - [[nodiscard]] object_ptr MakePriceWithChangePercentValue( not_null table, const std::shared_ptr &value) { @@ -509,35 +219,13 @@ void AddTableRow( return MakeValueWithSmallButton(table, label, std::move(text)); } -[[nodiscard]] object_ptr MakePriceValueWithTooltip( - not_null table, - std::shared_ptr data, - TextWithEntities price, - TextWithEntities tooltip) { - const auto label = Ui::CreateChild( - table, - rpl::single(price), - table->st().defaultValue); - label->setAttribute(Qt::WA_TransparentForMouseEvents); - - const auto handler = [=](not_null 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 MakeMinimumPriceValue( not_null table, - std::shared_ptr tooltip, + std::shared_ptr tooltip, const std::shared_ptr &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 MakeAveragePriceValue( not_null table, - std::shared_ptr tooltip, + std::shared_ptr tooltip, const std::shared_ptr &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 container, not_null table, not_null unique) { - const auto tooltip = std::make_shared(InfoTooltipData{ - .parent = container, - }); + const auto tooltip = std::make_shared( + TableRowTooltipData{ .parent = container }); const auto showTooltip = [=]( not_null widget, rpl::producer text) { - ShowInfoTooltip(tooltip, widget, std::move(text), kTooltipDuration); + ShowTableRowTooltip(tooltip, widget, std::move(text), kTooltipDuration); }; const auto showRarity = [=]( not_null widget, @@ -629,8 +316,17 @@ void AddUniqueGiftPropertyRows( const Data::CreditsHistoryEntry &entry, Fn convertToStars) { auto helper = Ui::Text::CustomEmojiHelper(); + const auto addUpgradeToValue = !entry.credits.ton() + && !entry.giftUpgradeGifted + && !entry.giftUpgradeSeparate + && entry.starsUpgradedBySender; + const auto amount = addUpgradeToValue + ? CreditsAmount( + entry.credits.whole() + entry.starsUpgradedBySender, + entry.credits.nano()) + : entry.credits; const auto price = helper.paletteDependent(Ui::Earn::IconCreditsEmoji( - )).append(' ').append(Lang::FormatCreditsAmountDecimal(entry.credits)); + )).append(' ').append(Lang::FormatCreditsAmountDecimal(amount)); auto label = object_ptr( table, rpl::single(price), @@ -712,37 +408,6 @@ void AddUniqueGiftPropertyRows( handler); } -not_null AddTableRow( - not_null table, - rpl::producer label, - rpl::producer value, - const Ui::Text::MarkedContext &context = {}) { - auto widget = object_ptr( - 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 table, - rpl::producer label, - std::shared_ptr show, - PeerId id) { - if (!id) { - return; - } - AddTableRow( - table, - std::move(label), - MakePeerTableValue(table, show, id), - st::giveawayGiftCodePeerMargin); -} - void AddTable( not_null container, std::shared_ptr show, @@ -778,7 +443,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 +528,22 @@ void ShowAlreadyPremiumToast( } // namespace -rpl::producer GiftDurationValue(int months) { - return GiftDurationPhrase(months)( +rpl::producer 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 +791,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 +912,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 +1220,12 @@ void AddStarGiftTable( const auto giftToChannel = entry.giftChannelSavedId && peerIsChannel(PeerId(entry.bareEntryOwnerId)); - const auto tooltip = std::make_shared(InfoTooltipData{ - .parent = container, - }); + const auto tooltip = std::make_shared( + TableRowTooltipData{ .parent = container }); const auto showTooltip = [=]( not_null widget, rpl::producer text) { - ShowInfoTooltip(tooltip, widget, std::move(text), kTooltipDuration); + ShowTableRowTooltip(tooltip, widget, std::move(text), kTooltipDuration); }; if (unique && entry.bareGiftResaleRecipientId) { @@ -1668,6 +1337,15 @@ void AddStarGiftTable( PeerId(entry.bareEntryOwnerId)), st::giveawayGiftCodePeerMargin); } + } else if (entry.auction && entry.bareGiftOwnerId) { + AddTableRow( + table, + tr::lng_credits_box_history_entry_peer(), + MakePeerTableValue( + table, + show, + PeerId(entry.bareGiftOwnerId)), + st::giveawayGiftCodePeerMargin); } else if (peerId && !giftToSelf) { const auto user = session->data().peer(peerId)->asUser(); const auto withSendButton = entry.in && user && !user->isBot(); @@ -1938,11 +1616,30 @@ void AddCreditsHistoryEntryTable( : entry.giftUpgraded ? tr::lng_credits_box_history_entry_gift_from() : tr::lng_credits_box_history_entry_peer(); + const auto targetId = actorId ? actorId : peerId; + const auto isPeerDefault = !entry.starrefCommission + && !entry.in + && !entry.giftResale + && !entry.giftUpgraded; + const auto user = isPeerDefault + ? session->data().peer(targetId)->asUser() + : nullptr; + const auto withSendButton = user + && !user->isInaccessible() + && !user->isBot(); + auto send = withSendButton ? tr::lng_gift_send_small() : nullptr; + auto handler = send + ? Fn([=] { + if (const auto window = show->resolveWindow()) { + Ui::ShowStarGiftBox(window, user); + } + }) + : nullptr; AddTableRow( table, std::move(text), - show, - actorId ? actorId : peerId); + MakePeerTableValue(table, show, targetId, send, handler), + st::giveawayGiftCodePeerMargin); } if (const auto msgId = MsgId(peerId ? entry.bareMsgId : 0)) { const auto peer = session->data().peer(peerId); @@ -2102,6 +1799,19 @@ void AddCreditsHistoryEntryTable( rpl::single( Ui::Text::Link(entry.successLink, entry.successLink))); } + if (entry.limitedCount > 0 && entry.limitedLeft >= 0) { + AddTableRow( + table, + tr::lng_gift_availability(), + tr::lng_gift_availability_left( + lt_count_decimal, + rpl::single(entry.limitedLeft) | tr::to_count(), + lt_amount, + rpl::single(TextWithEntities{ + Lang::FormatCountDecimal(entry.limitedCount) + }), + Ui::Text::WithEntities)); + } } void AddSubscriptionEntryTable( @@ -2313,9 +2023,8 @@ void AddUniqueGiftValueTable( MakePriceWithChangePercentValue(table, value)); } - const auto tooltip = std::make_shared(InfoTooltipData{ - .parent = container, - }); + const auto tooltip = std::make_shared( + TableRowTooltipData{ .parent = container }); if (value->minimumPrice) { AddTableRow( table, diff --git a/Telegram/SourceFiles/boxes/gift_premium_box.h b/Telegram/SourceFiles/boxes/gift_premium_box.h index 64c103a1fa..452599b753 100644 --- a/Telegram/SourceFiles/boxes/gift_premium_box.h +++ b/Telegram/SourceFiles/boxes/gift_premium_box.h @@ -44,8 +44,8 @@ namespace Window { class SessionNavigation; } // namespace Window -[[nodiscard]] rpl::producer GiftDurationValue(int months); -[[nodiscard]] QString GiftDuration(int months); +[[nodiscard]] rpl::producer GiftDurationValue(int days); +[[nodiscard]] QString GiftDuration(int days); void GiftCodeBox( not_null box, diff --git a/Telegram/SourceFiles/boxes/moderate_messages_box.cpp b/Telegram/SourceFiles/boxes/moderate_messages_box.cpp index 7ab0c2e396..ec249bf5ef 100644 --- a/Telegram/SourceFiles/boxes/moderate_messages_box.cpp +++ b/Telegram/SourceFiles/boxes/moderate_messages_box.cpp @@ -466,8 +466,7 @@ void CreateModerateMessagesBox( inner->add(object_ptr( inner, std::move(label), - st::defaultBoxDividerLabelPadding, - RectPart::Top | RectPart::Bottom)); + st::defaultBoxDividerLabelPadding)); using Flag = ChatRestriction; using Flags = ChatRestrictions; diff --git a/Telegram/SourceFiles/boxes/peer_list_box.cpp b/Telegram/SourceFiles/boxes/peer_list_box.cpp index fe03444089..eb61a75185 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_box.cpp @@ -993,10 +993,11 @@ void PeerListRow::setCheckedInternal(bool checked, anim::type animated) { } void PeerListRow::setCustomizedCheckSegments( - std::vector segments) { + std::vector segments, + bool liveBadge) { Expects(_checkbox != nullptr); - _checkbox->setCustomizedSegments(std::move(segments)); + _checkbox->setCustomizedSegments(std::move(segments), liveBadge); } void PeerListRow::finishCheckedAnimation() { diff --git a/Telegram/SourceFiles/boxes/peer_list_box.h b/Telegram/SourceFiles/boxes/peer_list_box.h index 0ae796e131..133019793f 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.h +++ b/Telegram/SourceFiles/boxes/peer_list_box.h @@ -229,7 +229,8 @@ public: setCheckedInternal(checked, animated); } void setCustomizedCheckSegments( - std::vector segments); + std::vector segments, + bool liveBadge); void setHidden(bool hidden) { _hidden = hidden; } diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp index 3b984c171a..e6d54fc826 100644 --- a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp @@ -132,27 +132,36 @@ QBrush PeerListStoriesGradient(const style::PeerList &st) { } std::vector 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(); 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 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 peer) { @@ -597,25 +606,28 @@ void PeerListStories::prepare(not_null 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 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); } } diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.h b/Telegram/SourceFiles/boxes/peer_list_controllers.h index 24887d3df2..fa02b17a58 100644 --- a/Telegram/SourceFiles/boxes/peer_list_controllers.h +++ b/Telegram/SourceFiles/boxes/peer_list_controllers.h @@ -42,9 +42,18 @@ class SessionController; [[nodiscard]] object_ptr PrepareContactsBox( not_null 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 PeerListStoriesSegments( - int count, - int unread, + PeerListStoriesCounts counts, const QBrush &unreadBrush); class PeerListRowWithLink : public PeerListRow { @@ -211,17 +220,13 @@ public: bool handleClick(not_null 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 row, - int count, - int unread, + Counts counts, bool force = false); const not_null _controller; diff --git a/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp b/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp index a5f2ec3c30..2ece4c18aa 100644 --- a/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp @@ -1070,6 +1070,10 @@ void AddParticipantsBoxController::Start( channel, params, ShowAtTheEndMsgId); + channel->owner().addRecentJoinChat({ + .fromPeerId = channel->id, + .joinedPeerId = channel->id, + }); } }, box->lifetime()); } diff --git a/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp b/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp index ca10840712..f8362cedb7 100644 --- a/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp @@ -97,6 +97,7 @@ using RightsMap = std::vector>>; { Flag::ManageCall, tr::lng_request_channel_manage_livestreams }, { Flag::ManageDirect, tr::lng_request_channel_manage_direct }, { Flag::AddAdmins, tr::lng_request_channel_add_admins }, + { Flag::BanUsers, tr::lng_request_group_ban_users }, }; } diff --git a/Telegram/SourceFiles/boxes/peers/edit_contact_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_contact_box.cpp index eae56c342c..a55c575100 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_contact_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_contact_box.cpp @@ -658,7 +658,7 @@ void Controller::setupPhotoButtons() { _window->session().api().peerPhoto().clearPersonal(_user); close(); }, - .confirmText = tr::lng_profile_photo_reset(tr::now), + .confirmText = tr::lng_profile_photo_reset_button(tr::now), })); }); diff --git a/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp index 84c47cd928..612d438f7e 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp @@ -246,7 +246,8 @@ ChatAdminRightsInfo EditAdminBox::defaultRights() const { | Flag::DeleteStories | Flag::InviteByLinkOrAdd | Flag::ManageCall - | Flag::ManageDirect) }; + | Flag::ManageDirect + | Flag::BanUsers) }; } void EditAdminBox::prepare() { diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp index 266c3b8148..9457279ddf 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/unixtime.h" #include "boxes/peers/replace_boost_box.h" #include "boxes/background_box.h" +#include "boxes/premium_preview_box.h" #include "boxes/star_gift_box.h" #include "boxes/stickers_box.h" #include "boxes/transfer_gift_box.h" @@ -37,6 +38,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/channel_statistics/boosts/info_boosts_widget.h" #include "info/peer_gifts/info_peer_gifts_common.h" #include "info/profile/info_profile_emoji_status_panel.h" +#include "info/profile/info_profile_top_bar.h" +#include "info/info_controller.h" // Key #include "info/info_memento.h" #include "iv/iv_data.h" #include "lang/lang_keys.h" @@ -53,6 +56,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/path_shift_gradient.h" #include "ui/effects/premium_graphics.h" #include "ui/layers/generic_box.h" +#include "ui/new_badges.h" +#include "ui/peer/color_sample.h" #include "ui/text/text_utilities.h" #include "ui/widgets/buttons.h" #include "ui/wrap/slide_wrap.h" @@ -77,40 +82,89 @@ using namespace Settings; constexpr auto kFakeChannelId = ChannelId(0xFFFFFFF000ULL); constexpr auto kFakeWebPageId = WebPageId(0xFFFFFFFF00000000ULL); -constexpr auto kSelectAnimationDuration = crl::time(150); constexpr auto kUnsetColorIndex = uint8(0xFF); -class ColorSample final : public Ui::AbstractButton { -public: - ColorSample( - not_null parent, - not_null session, - std::shared_ptr style, - rpl::producer colorIndex, - rpl::producer> collectible, - const QString &name); - ColorSample( - not_null parent, - std::shared_ptr style, - uint8 colorIndex, - bool selected); +base::unique_qptr CreateEmptyPlaceholder( + not_null parent, + int width, + const QMargins &padding, + Fn switchToNextTab) { + const auto container = Ui::CreateChild(parent); + auto result = base::unique_qptr{ container }; - [[nodiscard]] uint8 index() const; + auto icon = Settings::CreateLottieIcon( + container, + { + .name = u"my_gifts_empty"_q, + .sizeOverride = st::normalBoxLottieSize, + }, + st::settingsBlockedListIconPadding); + const auto iconWidget = icon.widget.data(); + iconWidget->show(); - void setSelected(bool selected); + const auto emptyLabel = Ui::CreateChild( + container, + tr::lng_gift_stars_tabs_my_empty(), + st::giftBoxGiftEmptyLabel); + emptyLabel->setTryMakeSimilarLines(true); + emptyLabel->resizeToWidth( + width - st::boxRowPadding.left() - st::boxRowPadding.right()); + emptyLabel->show(); -private: - void paintEvent(QPaintEvent *e) override; + const auto emptyNextLabel = switchToNextTab + ? Ui::CreateChild( + container, + tr::lng_gift_stars_tabs_my_empty_next( + lt_emoji, + rpl::single(Ui::Text::IconEmoji(&st::textMoreIconEmoji)), + tr::link), + st::giftBoxGiftEmptyLabel) + : nullptr; + if (emptyNextLabel) { + emptyNextLabel->resizeToWidth( + width - st::boxRowPadding.left() - st::boxRowPadding.right()); + emptyNextLabel->setClickHandlerFilter([=](auto...) { + switchToNextTab(); + return false; + }); + } - std::shared_ptr _style; - Ui::Text::String _name; - uint8 _index = 0; - std::shared_ptr _collectible; - Ui::Animations::Simple _selectAnimation; - bool _selected = false; - bool _simple = false; + icon.animate(anim::repeat::loop); -}; + const auto labelHeight = emptyLabel->height(); + const auto nextLabelHeight = emptyNextLabel + ? emptyNextLabel->height() + : 0; + const auto totalHeight = iconWidget->height() + + st::normalFont->height + labelHeight + nextLabelHeight + + (nextLabelHeight ? st::normalFont->height : 0) + + padding.top() + padding.bottom(); + container->resize(width, totalHeight); + + container->sizeValue( + ) | rpl::start_with_next([=](QSize size) { + const auto totalContentHeight = iconWidget->height() + + st::normalFont->height + emptyLabel->height() + + (emptyNextLabel + ? st::normalFont->height + emptyNextLabel->height() + : 0); + const auto iconY = (size.height() - totalContentHeight) / 2; + iconWidget->move( + (size.width() - iconWidget->width()) / 2, + iconY); + emptyLabel->move( + (size.width() - emptyLabel->width()) / 2, + iconY + iconWidget->height() + st::normalFont->height); + if (emptyNextLabel) { + emptyNextLabel->move( + (size.width() - emptyNextLabel->width()) / 2, + iconY + iconWidget->height() + st::normalFont->height + + emptyLabel->height() + st::normalFont->height); + } + }, container->lifetime()); + + return result; +} class PreviewDelegate final : public HistoryView::DefaultElementDelegate { public: @@ -185,153 +239,7 @@ private: }; -ColorSample::ColorSample( - not_null parent, - not_null session, - std::shared_ptr style, - rpl::producer colorIndex, - rpl::producer> collectible, - const QString &name) -: AbstractButton(parent) -, _style(style) { - rpl::combine( - std::move(colorIndex), - std::move(collectible) - ) | rpl::start_with_next([=]( - uint8 index, - std::shared_ptr collectible) { - _index = index; - _collectible = std::move(collectible); - if (const auto raw = _collectible.get()) { - _name.setMarkedText( - st::semiboldTextStyle, - Data::SingleCustomEmoji(raw->giftEmojiId), - kMarkupTextOptions, - Core::TextContext({ - .session = session, - .repaint = [=] { update(); }, - })); - } else { - _name.setText(st::semiboldTextStyle, name); - } - setNaturalWidth([&] { - if (_name.isEmpty() || _style->colorPatternIndex(_index)) { - return st::settingsColorSampleSize; - } - const auto padding = st::settingsColorSamplePadding; - return std::max( - padding.left() + _name.maxWidth() + padding.right(), - padding.top() + st::semiboldFont->height + padding.bottom()); - }()); - update(); - }, lifetime()); -} -ColorSample::ColorSample( - not_null parent, - std::shared_ptr style, - uint8 colorIndex, - bool selected) -: AbstractButton(parent) -, _style(style) -, _index(colorIndex) -, _selected(selected) -, _simple(true) { - setNaturalWidth(st::settingsColorSampleSize); -} - -void ColorSample::setSelected(bool selected) { - if (_selected == selected) { - return; - } - _selected = selected; - _selectAnimation.start( - [=] { update(); }, - _selected ? 0. : 1., - _selected ? 1. : 0., - kSelectAnimationDuration); -} - -void ColorSample::paintEvent(QPaintEvent *e) { - auto p = Painter(this); - auto hq = PainterHighQualityEnabler(p); - const auto colors = _style->coloredValues(false, _index); - if (!_simple && !colors.outlines[1].alpha()) { - const auto radius = height() / 2; - p.setPen(Qt::NoPen); - if (const auto raw = _collectible.get()) { - const auto withBg = [&](const QColor &color) { - return Ui::CountContrast(st::windowBg->c, color); - }; - const auto dark = (withBg({ 0, 0, 0 }) - < withBg({ 255, 255, 255 })); - const auto name = (dark && raw->darkAccentColor.alpha() > 0) - ? raw->darkAccentColor - : raw->accentColor; - auto bg = name; - bg.setAlpha(0.12 * 255); - p.setBrush(bg); - } else { - p.setBrush(colors.bg); - } - p.drawRoundedRect(rect(), radius, radius); - - const auto padding = st::settingsColorSamplePadding; - p.setPen(colors.name); - p.setBrush(Qt::NoBrush); - p.setFont(st::semiboldFont); - _name.drawLeftElided( - p, - padding.left(), - padding.top(), - width() - padding.left() - padding.right(), - width(), - 1, - style::al_top); - } else { - const auto size = float64(width()); - const auto half = size / 2.; - const auto full = QRectF(-half, -half, size, size); - p.translate(size / 2., size / 2.); - p.setPen(Qt::NoPen); - if (colors.outlines[1].alpha()) { - p.rotate(-45.); - p.setClipRect(-size, 0, 3 * size, size); - p.setBrush(colors.outlines[1]); - p.drawEllipse(full); - p.setClipRect(-size, -size, 3 * size, size); - } - p.setBrush(colors.outlines[0]); - p.drawEllipse(full); - p.setClipping(false); - if (colors.outlines[2].alpha()) { - const auto multiplier = size / st::settingsColorSampleSize; - const auto center = st::settingsColorSampleCenter * multiplier; - const auto radius = st::settingsColorSampleCenterRadius - * multiplier; - p.setBrush(colors.outlines[2]); - p.drawRoundedRect( - QRectF(-center / 2., -center / 2., center, center), - radius, - radius); - } - const auto selected = _selectAnimation.value(_selected ? 1. : 0.); - if (selected > 0) { - const auto line = st::settingsColorRadioStroke * 1.; - const auto thickness = selected * line; - auto pen = st::boxBg->p; - pen.setWidthF(thickness); - p.setBrush(Qt::NoBrush); - p.setPen(pen); - const auto skip = 1.5 * line; - p.drawEllipse(full.marginsRemoved({ skip, skip, skip, skip })); - } - } -} - -uint8 ColorSample::index() const { - return _index; -} PreviewWrap::PreviewWrap( not_null box, @@ -581,13 +489,19 @@ struct SetValues { EmojiStatusId statusId; TimeId statusUntil = 0; bool statusChanged = false; + bool forProfile = false; }; void Set( std::shared_ptr show, not_null peer, - SetValues values) { - const auto wasIndex = peer->colorIndex(); - const auto wasEmojiId = peer->backgroundEmojiId(); + SetValues values, + bool showToast = true) { + const auto wasIndex = values.forProfile + ? peer->colorProfileIndex().value_or(kUnsetColorIndex) + : peer->colorIndex(); + const auto wasEmojiId = values.forProfile + ? peer->profileBackgroundEmojiId() + : peer->backgroundEmojiId(); const auto &wasColorCollectible = peer->colorCollectible(); const auto setLocal = [=]( @@ -595,20 +509,32 @@ void Set( DocumentId emojiId, std::optional colorCollectible) { using UpdateFlag = Data::PeerUpdate::Flag; - if (index == kUnsetColorIndex) { - peer->clearColorIndex(); + if (values.forProfile) { + if (index == kUnsetColorIndex) { + peer->clearColorProfileIndex(); + } else { + peer->changeColorProfileIndex(index); + } + peer->changeProfileBackgroundEmojiId(emojiId); } else { - peer->changeColorIndex(index); + if (index == kUnsetColorIndex) { + peer->clearColorIndex(); + } else { + peer->changeColorIndex(index); + } + if (colorCollectible) { + peer->changeColorCollectible(*colorCollectible); + } else { + peer->clearColorCollectible(); + } + peer->changeBackgroundEmojiId(emojiId); } - if (colorCollectible) { - peer->changeColorCollectible(*colorCollectible); - } else { - peer->clearColorCollectible(); - } - peer->changeBackgroundEmojiId(emojiId); peer->session().changes().peerUpdated( peer, - UpdateFlag::Color | UpdateFlag::BackgroundEmoji); + (UpdateFlag::BackgroundEmoji + | (values.forProfile + ? UpdateFlag::ColorProfile + : UpdateFlag::Color))); }; setLocal( values.colorIndex, @@ -616,9 +542,15 @@ void Set( values.colorCollectible); const auto done = [=] { - show->showToast(peer->isSelf() - ? tr::lng_settings_color_changed(tr::now) - : tr::lng_settings_color_changed_channel(tr::now)); + if (showToast) { + show->showToast(peer->isSelf() + ? (values.forProfile + ? tr::lng_settings_color_changed_profile(tr::now) + : tr::lng_settings_color_changed(tr::now)) + : (values.forProfile + ? tr::lng_settings_color_changed_profile_channel(tr::now) + : tr::lng_settings_color_changed_channel(tr::now))); + } }; const auto fail = [=](const MTP::Error &error) { const auto type = error.type(); @@ -638,21 +570,38 @@ void Set( using Flag = MTPaccount_UpdateColor::Flag; using ColorFlag = MTPDpeerColor::Flag; send(MTPaccount_UpdateColor( - MTP_flags(Flag::f_color), - (values.colorCollectible + MTP_flags((values.forProfile ? Flag::f_for_profile : Flag(0)) + | (((!values.forProfile && values.colorCollectible) + || (values.colorIndex != kUnsetColorIndex)) + ? Flag::f_color + : Flag(0))), + ((!values.forProfile && values.colorCollectible) ? MTP_inputPeerColorCollectible( MTP_long(values.colorCollectible->collectibleId)) : MTP_peerColor( MTP_flags(ColorFlag() | ColorFlag::f_color - | ColorFlag::f_background_emoji_id), + | (values.backgroundEmojiId + ? ColorFlag::f_background_emoji_id + : 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; send(MTPchannels_UpdateColor( - MTP_flags(Flag::f_color | Flag::f_background_emoji_id), + MTP_flags((values.colorIndex != kUnsetColorIndex + ? Flag::f_color + : Flag(0)) + | Flag::f_background_emoji_id + | (values.forProfile ? Flag::f_for_profile : Flag(0))), channel->inputChannel, MTP_int(values.colorIndex), MTP_long(values.backgroundEmojiId))); @@ -669,23 +618,17 @@ void Set( } } -bool ShowPremiumToast( +bool ShowPremiumPreview( std::shared_ptr show, not_null peer) { if (!peer->isSelf() || show->session().premium()) { return false; } - Settings::ShowPremiumPromoToast( - show, - tr::lng_settings_color_subscribe( - tr::now, - lt_link, - Ui::Text::Link( - Ui::Text::Bold( - tr::lng_send_as_premium_required_link(tr::now))), - Ui::Text::WithEntities), - u"name_color"_q); - return true; + if (const auto controller = show->resolveWindow()) { + ShowPremiumPreviewBox(controller, PremiumFeature::PeerColors); + return true; + } + return false; } void Apply( @@ -693,27 +636,38 @@ void Apply( not_null peer, SetValues values, Fn close, - Fn cancel) { - if (peer->colorIndex() == values.colorIndex - && peer->backgroundEmojiId() == values.backgroundEmojiId - && (!peer->colorCollectible() == !values.colorCollectible) - && (!peer->colorCollectible() - || (*peer->colorCollectible() == *values.colorCollectible)) + Fn cancel, + bool showToast = true) { + const auto currentColorIndex = values.forProfile + ? peer->colorProfileIndex().value_or(kUnsetColorIndex) + : peer->colorIndex(); + const auto currentEmojiId = values.forProfile + ? peer->profileBackgroundEmojiId() + : peer->backgroundEmojiId(); + + const auto colorMatch = (currentColorIndex == values.colorIndex); + const auto emojiMatch = (currentEmojiId == values.backgroundEmojiId); + const auto collectibleMatch = values.forProfile + || ((!peer->colorCollectible() == !values.colorCollectible) + && (!peer->colorCollectible() + || (*peer->colorCollectible() == *values.colorCollectible))); + + if (colorMatch + && emojiMatch + && collectibleMatch && !values.statusChanged) { close(); } else if (peer->isSelf()) { - Set(show, peer, values); + Set(show, peer, values, showToast); close(); } else { CheckBoostLevel(show, peer, [=](int level) { const auto peerColors = &peer->session().api().peerColors(); - const auto colorRequired = peer->isMegagroup() - ? peerColors->requiredGroupLevelFor( - peer->id, - values.colorIndex) - : peerColors->requiredChannelLevelFor( - peer->id, - values.colorIndex); + const auto colorRequired = peerColors->requiredLevelFor( + peer->id, + values.colorIndex, + peer->isMegagroup(), + values.forProfile); const auto limits = Data::LevelLimits(&peer->session()); const auto iconRequired = values.backgroundEmojiId ? limits.channelBgIconLevelMin() @@ -728,7 +682,7 @@ void Apply( statusRequired, }); if (level >= required) { - Set(show, peer, values); + Set(show, peer, values, showToast); close(); return std::optional(); } @@ -748,111 +702,7 @@ void Apply( } } -class ColorSelector final : public Ui::RpWidget { -public: - ColorSelector( - not_null box, - std::shared_ptr style, - rpl::producer> indices, - rpl::producer index, - Fn callback); -private: - void fillFrom(std::vector indices); - - int resizeGetHeight(int newWidth) override; - - const std::shared_ptr _style; - std::vector> _samples; - const Fn _callback; - rpl::variable _index; - -}; - -ColorSelector::ColorSelector( - not_null box, - std::shared_ptr style, - rpl::producer> indices, - rpl::producer index, - Fn callback) -: RpWidget(box) -, _style(style) -, _callback(std::move(callback)) -, _index(std::move(index)) { - std::move( - indices - ) | rpl::start_with_next([=](std::vector indices) { - fillFrom(std::move(indices)); - }, lifetime()); -} - -void ColorSelector::fillFrom(std::vector indices) { - auto samples = std::vector>(); - const auto initial = _index.current(); - const auto add = [&](uint8 index) { - auto i = ranges::find(_samples, index, &ColorSample::index); - if (i != end(_samples)) { - samples.push_back(std::move(*i)); - _samples.erase(i); - } else { - samples.push_back(std::make_unique( - this, - _style, - index, - index == initial)); - samples.back()->show(); - samples.back()->setClickedCallback([=] { - _callback(index); - }); - } - }; - for (const auto index : indices) { - add(index); - } - if (initial != kUnsetColorIndex && !ranges::contains(indices, initial)) { - add(initial); - } - _samples = std::move(samples); - if (width() > 0) { - resizeToWidth(width()); - } - - _index.value( - ) | rpl::combine_previous( - ) | rpl::start_with_next([=](uint8 was, uint8 now) { - const auto i = ranges::find(_samples, was, &ColorSample::index); - if (i != end(_samples)) { - i->get()->setSelected(false); - } - const auto j = ranges::find(_samples, now, &ColorSample::index); - if (j != end(_samples)) { - j->get()->setSelected(true); - } - }, lifetime()); -} - -int ColorSelector::resizeGetHeight(int newWidth) { - if (newWidth <= 0) { - return 0; - } - const auto count = int(_samples.size()); - const auto columns = Ui::kSimpleColorIndexCount; - const auto skip = st::settingsColorRadioSkip; - const auto size = (newWidth - skip * (columns - 1)) / float64(columns); - const auto isize = int(base::SafeRound(size)); - auto top = 0; - auto left = 0.; - for (auto i = 0; i != count; ++i) { - _samples[i]->resize(isize, isize); - _samples[i]->move(int(base::SafeRound(left)), top); - left += size + skip; - if (!((i + 1) % columns)) { - top += isize + skip; - left = 0.; - } - } - return (top - skip) + ((count % columns) ? (isize + skip) : 0); -} [[nodiscard]] auto ButtonStyleWithAddedPadding( not_null parent, @@ -871,13 +721,18 @@ int ColorSelector::resizeGetHeight(int newWidth) { not_null peer, rpl::producer colorIndexValue, rpl::producer emojiIdValue, - Fn emojiIdChosen) { + Fn emojiIdChosen, + bool profileIndices) { const auto button = ButtonStyleWithRightEmoji( parent, tr::lng_settings_color_emoji_off(tr::now)); auto result = Settings::CreateButtonWithIcon( parent, - tr::lng_settings_color_emoji(), + !profileIndices + ? tr::lng_settings_color_emoji() + : peer->isChannel() + ? tr::lng_settings_color_profile_emoji_channel() + : tr::lng_settings_color_profile_emoji(), *button.st, { &st::menuBlueIconColorNames }); const auto raw = result.data(); @@ -906,7 +761,7 @@ int ColorSelector::resizeGetHeight(int newWidth) { }, right->lifetime()); const auto session = &show->session(); - const auto added = st::normalFont->spacew; + const auto added = st::lineWidth * 2; std::move(emojiIdValue) | rpl::start_with_next([=](DocumentId emojiId) { state->emojiId = emojiId; state->emoji = emojiId @@ -936,10 +791,19 @@ int ColorSelector::resizeGetHeight(int newWidth) { } auto p = QPainter(right); const auto height = right->height(); - if (state->emoji && state->index != kUnsetColorIndex) { - const auto colors = style->coloredValues(false, state->index); + if (state->emoji + && (state->index != kUnsetColorIndex || profileIndices)) { + const auto profileSet = profileIndices + ? peer->session().api().peerColors().colorProfileFor( + state->index) + : std::nullopt; + const auto textColor = profileSet && !profileSet->palette.empty() + ? profileSet->palette.front() + : profileIndices + ? style->windowActiveTextFg()->c + : style->coloredValues(false, state->index).name; state->emoji->paint(p, { - .textColor = colors.name, + .textColor = textColor, .position = QPoint(added, (height - button.emojiWidth) / 2), .internal = { .forceFirstFrame = true, @@ -957,6 +821,17 @@ int ColorSelector::resizeGetHeight(int newWidth) { raw->setClickedCallback([=] { const auto customTextColor = [=] { + if (state->index == kUnsetColorIndex) { + return style->windowActiveTextFg()->c; + } + if (profileIndices) { + const auto colorSet + = peer->session().api().peerColors().colorProfileFor( + state->index); + if (colorSet && !colorSet->palette.empty()) { + return colorSet->palette.front(); + } + } return style->coloredValues(false, state->index).name; }; const auto controller = show->resolveWindow(); @@ -972,8 +847,11 @@ int ColorSelector::resizeGetHeight(int newWidth) { }); if (const auto channel = peer->asChannel()) { + const auto limits = Data::LevelLimits(&channel->session()); AddLevelBadge( - Data::LevelLimits(&channel->session()).channelBgIconLevelMin(), + profileIndices + ? limits.channelProfileBgIconLevelMin() + : limits.channelBgIconLevelMin(), raw, right, channel, @@ -1217,10 +1095,11 @@ int ColorSelector::resizeGetHeight(int newWidth) { return result; } -void AddColorGiftTabs( +Fn AddColorGiftTabs( not_null container, not_null session, - Fn chosen) { + Fn chosen, + bool profile) { using namespace Info::PeerGifts; struct State { @@ -1235,7 +1114,7 @@ void AddColorGiftTabs( ) | rpl::start_with_next([=](const std::vector &list) { auto filtered = std::vector(); for (const auto &gift : list) { - if (gift.info.peerColorAvailable && gift.resale) { + if ((profile || gift.info.peerColorAvailable) && gift.resale) { filtered.push_back(gift.info); } } @@ -1282,6 +1161,16 @@ void AddColorGiftTabs( } container->resizeToWidth(container->width()); }, container->lifetime()); + + return [=]() { + const auto &list = state->list.current(); + if (!list.empty()) { + if (state->tabs) { + state->tabs->setActiveTab(QString::number(list.front().id)); + } + chosen(list.front().id); + } + }; } void AddGiftSelector( @@ -1289,7 +1178,10 @@ void AddGiftSelector( not_null session, rpl::producer showingGiftIdValue, Fn selected)> chosen, - rpl::producer> selected) { + rpl::producer selected, + bool profile, + rpl::producer selectedGiftId = rpl::single(uint64(0)), + Fn switchToNextTab = nullptr) { using namespace Info::PeerGifts; const auto raw = container->add( @@ -1309,8 +1201,10 @@ void AddGiftSelector( std::vector validated; std::vector> buttons; rpl::variable visibleRange; - rpl::variable> selected; + rpl::variable selected; + rpl::variable selectedGiftId; int perRow = 1; + base::unique_qptr emptyPlaceholder; Fn loadMore; Fn resize; @@ -1320,6 +1214,7 @@ void AddGiftSelector( state->delegate.emplace(session, GiftButtonMode::Full); state->showingGiftId = std::move(showingGiftIdValue); state->selected = std::move(selected); + state->selectedGiftId = std::move(selectedGiftId); const auto shadow = st::defaultDropdownMenu.wrap.shadow; const auto extend = shadow.extend; state->loadMore = [=] { @@ -1344,7 +1239,7 @@ void AddGiftSelector( auto &list = state->current->list; for (const auto &gift : slice.list) { - if (gift.unique && gift.unique->peerColor) { + if (gift.unique && (profile || gift.unique->peerColor)) { list.push_back({ .info = gift, .resale = true, @@ -1370,7 +1265,8 @@ void AddGiftSelector( auto &list = state->current->list; for (const auto &gift : slice.list) { - if (gift.info.unique && gift.info.unique->peerColor) { + if (gift.info.unique + && (profile || gift.info.unique->peerColor)) { list.push_back({ .info = gift.info }); } } @@ -1405,8 +1301,8 @@ 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 selectedId = current ? current->collectibleId : 0; + const auto selectedCollectibleId = state->selected.current(); + const auto selectedGiftId = state->selectedGiftId.current(); auto checkedFrom = 0; auto checkedTill = int(buttons.size()); const auto ensureButton = [&](int index) { @@ -1447,8 +1343,11 @@ void AddGiftSelector( chosen(unique); }); raw->setGeometry(QRect(QPoint(x, y), single), extend); + const auto isSelected = selectedCollectibleId + ? (gift.info.unique->id == selectedCollectibleId) + : (gift.info.unique->id == selectedGiftId); raw->toggleSelected( - gift.info.unique->id == selectedId, + isSelected, GiftSelectionMode::Inset, anim::type::instant); }; @@ -1479,31 +1378,49 @@ void AddGiftSelector( } } + const auto find = [=](uint64 id) -> GiftButton* { + if (!id) { + return nullptr; + } + const auto count = int(state->current->list.size()); + for (auto i = 0; i != count; ++i) { + const auto &gift = state->current->list[i]; + if (gift.info.unique->id == id) { + return state->buttons[i].get(); + } + } + return nullptr; + }; + state->selected.value( ) | rpl::combine_previous() | rpl::start_with_next([=]( - const std::optional &was, - const std::optional &now) { - const auto wasId = was ? was->collectibleId : 0; - const auto nowId = now ? now->collectibleId : 0; - const auto find = [&](uint64 id) -> GiftButton* { - if (!id) { - return nullptr; + uint64 wasCollectibleId, + uint64 nowCollectibleId) { + if (wasCollectibleId) { + if (const auto button = find(wasCollectibleId)) { + button->toggleSelected(false, GiftSelectionMode::Inset); } - for (auto i = 0, count = int(state->current->list.size()) - ; i != count - ; ++i) { - const auto &gift = state->current->list[i]; - if (gift.info.unique->id == id) { - return state->buttons[i].get(); - } - } - return nullptr; - }; - if (const auto button = find(wasId)) { - button->toggleSelected(false, GiftSelectionMode::Inset); } - if (const auto button = find(nowId)) { - button->toggleSelected(true, GiftSelectionMode::Inset); + if (nowCollectibleId) { + if (const auto button = find(nowCollectibleId)) { + button->toggleSelected(true, GiftSelectionMode::Inset); + } + } + }, raw->lifetime()); + + state->selectedGiftId.value( + ) | rpl::combine_previous() | rpl::start_with_next([=]( + uint64 wasGiftId, + uint64 nowGiftId) { + if (wasGiftId) { + if (const auto button = find(wasGiftId)) { + button->toggleSelected(false, GiftSelectionMode::Inset); + } + } + if (nowGiftId) { + if (const auto button = find(nowGiftId)) { + button->toggleSelected(true, GiftSelectionMode::Inset); + } } }, raw->lifetime()); @@ -1522,6 +1439,21 @@ void AddGiftSelector( const auto count = int(state->current->list.size()); state->validated.clear(); + if (count == 0 && state->showingGiftId.current() == 0) { + if (!state->emptyPlaceholder) { + state->emptyPlaceholder = CreateEmptyPlaceholder( + raw, + width, + padding, + switchToNextTab); + } + state->emptyPlaceholder->show(); + raw->resize(raw->width(), state->emptyPlaceholder->height()); + return; + } else if (state->emptyPlaceholder) { + state->emptyPlaceholder = nullptr; + } + const auto rows = (count + state->perRow - 1) / state->perRow; const auto height = padding.top() + (rows * state->delegate->buttonSize().height()) @@ -1536,6 +1468,9 @@ void AddGiftSelector( ) | rpl::start_with_next([=](uint64 showingId) { state->current = &state->lists[showingId]; state->buttons.clear(); + if (state->emptyPlaceholder) { + state->emptyPlaceholder = nullptr; + } state->delegate.emplace(session, showingId ? GiftButtonMode::Full : GiftButtonMode::Minimal); @@ -1547,6 +1482,209 @@ void AddGiftSelector( ) | rpl::start_with_next(state->rebuild, raw->lifetime()); } +Fn CreateTabsWidget( + not_null container, + const std::vector &tabs, + const std::vector> &callbacks) { + struct State { + int activeTab = 0; + Ui::Animations::Simple animation; + float64 animatedPosition = 0.; + std::vector tabWidths; + }; + const auto tabsContainer = container->add( + object_ptr(container), + st::boxRowPadding, + style::al_top); + const auto state = tabsContainer->lifetime().make_state(); + const auto height = st::semiboldFont->height * 1.5; + + auto totalWidth = 0; + state->tabWidths.reserve(tabs.size()); + for (const auto &text : tabs) { + const auto width = st::semiboldFont->width(text) + height * 2; + state->tabWidths.push_back(width); + totalWidth += width; + } + + tabsContainer->resize(totalWidth, height); + tabsContainer->setMaximumWidth(tabsContainer->width()); + + const auto switchTo = [=](int i) { + if (state->activeTab != i && i >= 0 && i < state->tabWidths.size()) { + auto targetPosition = 0.; + for (auto j = 0; j < i; ++j) { + targetPosition += state->tabWidths[j]; + } + state->animation.stop(); + state->animation.start( + [=](float64 v) { + state->animatedPosition = v; + tabsContainer->update(); + }, + state->animatedPosition, + targetPosition, + 400, + anim::easeOutQuint); + state->activeTab = i; + } + if (i < callbacks.size() && callbacks[i]) { + callbacks[i](); + } + }; + + auto left = 0; + for (auto i = 0; i < tabs.size(); ++i) { + const auto tabButton = Ui::CreateChild( + tabsContainer); + tabButton->setGeometry(left, 0, state->tabWidths[i], height); + tabButton->setClickedCallback([=] { switchTo(i); }); + left += state->tabWidths[i]; + } + + const auto penWidth = st::lineWidth * 2; + + tabsContainer->paintRequest() | rpl::start_with_next([=] { + auto p = QPainter(tabsContainer); + auto hq = PainterHighQualityEnabler(p); + const auto r = tabsContainer->rect(); + auto pen = QPen(st::giftBoxTabBgActive); + pen.setWidthF(penWidth); + p.setPen(pen); + const auto halfPen = penWidth / 2; + p.drawRoundedRect( + QRectF( + halfPen, + halfPen, + r.width() - penWidth, + r.height() - penWidth), + height / 2, + height / 2); + p.setFont(st::semiboldFont); + + const auto animatedLeft = state->animatedPosition; + const auto activeWidth = state->tabWidths[state->activeTab]; + p.setBrush(st::giftBoxTabBgActive); + p.setPen(Qt::NoPen); + p.drawRoundedRect( + QRect(animatedLeft, 0, activeWidth, height), + height / 2, + height / 2); + + auto left = 0; + for (auto i = 0; i < tabs.size(); ++i) { + auto textPen = QPen(state->activeTab == i + ? st::giftBoxTabFgActive + : st::giftBoxTabFg); + textPen.setWidthF(penWidth); + p.setPen(textPen); + p.drawText( + QRect(left, 0, state->tabWidths[i], height), + tabs[i], + style::al_center); + left += state->tabWidths[i]; + } + }, tabsContainer->lifetime()); + + state->animatedPosition = 0.; + + return switchTo; +} + +not_null CreateProfilePreview( + not_null box, + not_null container, + std::shared_ptr show, + not_null peer) { + const auto preview = container->add( + object_ptr( + container, + Info::Profile::TopBar::Descriptor { + .controller = show->resolveWindow(), + .key = Info::Key(peer), + .wrap = rpl::single(Info::Wrap::Side), + .source = Info::Profile::TopBar::Source::Preview, + .peer = peer, + .backToggles = rpl::single(false), + .showFinished = box->showFinishes(), + } + )); + preview->resize( + container->width(), + st::infoProfileTopBarNoActionsHeightMax); + preview->setAttribute(Qt::WA_TransparentForMouseEvents); + return preview; +} + +void ProcessButton(not_null button) { + button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); + // Raise to be above right emoji from buttons. + crl::on_main(button, [=] { button->raise(); }); +} + +void CreateBoostLevelContainer( + not_null container, + int levelHint, + rpl::producer> colorProducer, + Fn callback) { + const auto boostLevelContainer = container->add( + object_ptr(container)); + boostLevelContainer->resize( + 0, + st::infoProfileTopBarBoostFooter.style.font->height * 1.5); + + struct State { + base::unique_qptr label; + std::optional currentColor; + }; + const auto state = boostLevelContainer->lifetime().make_state(); + + boostLevelContainer->paintRequest() | rpl::start_with_next([=] { + auto p = QPainter(boostLevelContainer); + const auto bg = state->currentColor.value_or(st::boxDividerBg->c); + p.fillRect(boostLevelContainer->rect(), bg); + p.fillRect(boostLevelContainer->rect(), st::shadowFg); + }, boostLevelContainer->lifetime()); + + std::move(colorProducer) | rpl::start_with_next([=]( + std::optional color) { + const auto colorChanged = (state->currentColor != color) + || !state->label; + state->currentColor = color; + boostLevelContainer->update(); + + if (colorChanged) { + const auto &style = color + ? st::infoProfileTopBarBoostFooterColored + : st::infoProfileTopBarBoostFooter; + state->label = base::make_unique_q( + boostLevelContainer, + tr::lng_settings_color_group_boost_footer( + lt_count, + rpl::single(levelHint) | tr::to_count(), + lt_link, + tr::lng_settings_color_group_boost_footer_link( + ) | rpl::map([=](QString t) { + using namespace Ui::Text; + return Link(std::move(t), u"internal:"_q); + }), + Ui::Text::RichLangValue), + style); + state->label->show(); + boostLevelContainer->sizeValue( + ) | rpl::start_with_next([=](QSize s) { + state->label->moveToLeft( + (s.width() - state->label->width()) / 2, + (s.height() - state->label->height()) / 2); + }, state->label->lifetime()); + state->label->setClickHandlerFilter([=](auto...) { + callback(); + return false; + }); + } + }, boostLevelContainer->lifetime()); +} + } // namespace void AddLevelBadge( @@ -1582,23 +1720,16 @@ void AddLevelBadge( }, badge->lifetime()); } -void EditPeerColorBox( +void EditPeerColorSection( not_null box, + not_null container, + not_null button, std::shared_ptr show, not_null peer, std::shared_ptr style, std::shared_ptr theme) { + ProcessButton(button); const auto group = peer->isMegagroup(); - const auto container = box->verticalLayout(); - - box->setTitle(peer->isSelf() - ? tr::lng_settings_color_title() - : tr::lng_edit_channel_color()); - box->setWidth(st::boxWideWidth); - box->setStyle(st::giftBox); - box->addTopButton(st::boxTitleClose, [=] { - box->closeBox(); - }); struct State { rpl::variable index; @@ -1606,13 +1737,16 @@ void EditPeerColorBox( rpl::variable statusId; rpl::variable> collectible; rpl::variable showingGiftId; + rpl::variable profileIndex; + rpl::variable profileEmojiId; std::shared_ptr buyCollectible; + Info::Profile::TopBar *preview = nullptr; TimeId statusUntil = 0; bool statusChanged = false; bool changing = false; bool applying = false; }; - const auto state = box->lifetime().make_state(); + const auto state = button->lifetime().make_state(); state->index = peer->colorCollectible() ? kUnsetColorIndex : peer->colorIndex(); @@ -1621,32 +1755,162 @@ void EditPeerColorBox( state->collectible = peer->colorCollectible() ? *peer->colorCollectible() : std::optional(); - if (group) { - Settings::AddDividerTextWithLottie(box->verticalLayout(), { - .lottie = u"palette"_q, - .lottieSize = st::settingsCloudPasswordIconSize, - .lottieMargins = st::peerAppearanceIconPadding, - .showFinished = box->showFinishes(), - .about = tr::lng_boost_group_about(Ui::Text::WithEntities), - .aboutMargins = st::peerAppearanceCoverLabelMargin, + state->profileIndex = peer->colorProfileIndex().value_or( + kUnsetColorIndex); + state->profileEmojiId = peer->profileBackgroundEmojiId(); + + const auto appendProfileSettings = [=]( + not_null container, + not_null channel) { + state->preview = CreateProfilePreview(box, container, show, peer); + if (state->profileIndex.current() != kUnsetColorIndex) { + state->preview->setColorProfileIndex( + state->profileIndex.current()); + } + if (state->profileEmojiId.current()) { + state->preview->setPatternEmojiId( + state->profileEmojiId.current()); + } + state->statusId.value() | rpl::start_with_next([=](EmojiStatusId id) { + state->preview->setLocalEmojiStatusId(std::move(id)); + }, state->preview->lifetime()); + const auto peerColors = &peer->session().api().peerColors(); + const auto profileIndices = peerColors->profileColorIndices(); + + if (group) { + auto colorProducer = state->profileIndex.value( + ) | rpl::map([=, colors = &peer->session().api().peerColors()]( + uint8 index) { + const auto colorSet = colors->colorProfileFor(index); + return (colorSet && !colorSet->bg.empty()) + ? std::make_optional(colorSet->bg.front()) + : std::optional(); + }); + CreateBoostLevelContainer( + container, + channel->levelHint(), + std::move(colorProducer), + [=] { + if (const auto strong = show->resolveWindow()) { + strong->resolveBoostState(channel); + } + }); + } + + const auto profileMargin = st::settingsColorRadioMargin; + const auto profileSkip = st::settingsColorRadioSkip; + const auto selector = container->add( + object_ptr( + container, + profileIndices, + state->profileIndex.current(), + [=](uint8 index) { + state->profileIndex = index; + if (state->preview) { + state->preview->setColorProfileIndex( + index == kUnsetColorIndex + ? std::nullopt + : std::make_optional(index)); + } + }, + [=](uint8 index) { + return peerColors->colorProfileFor(index).value_or( + Data::ColorProfileSet{}); + }), + { profileMargin, profileSkip, profileMargin, profileSkip }); + + container->add(CreateEmojiIconButton( + container, + show, + style, + peer, + state->profileIndex.value(), + state->profileEmojiId.value(), + [=](DocumentId id) { + state->profileEmojiId = id; + if (state->preview) { + state->preview->setPatternEmojiId(id); + } + }, + true)); + + state->profileIndex.value( + ) | rpl::start_with_next([=](uint8 index) { + selector->updateSelection(index); + }, selector->lifetime()); + + Ui::AddSkip(container, st::settingsColorSampleSkip); + + const auto resetWrap = container->add( + object_ptr>( + container, + object_ptr(container))); + const auto resetInner = resetWrap->entity(); + const auto resetButton = resetInner->add( + object_ptr( + resetInner, + tr::lng_settings_color_reset(), + st::settingsButtonLightNoIcon)); + Ui::AddSkip(resetInner, st::settingsColorSampleSkip); + resetButton->setClickedCallback([=] { + state->profileIndex = kUnsetColorIndex; + state->profileEmojiId = 0; + if (state->preview) { + state->preview->setColorProfileIndex(std::nullopt); + state->preview->setPatternEmojiId(0); + } + resetWrap->toggle(false, anim::type::normal); }); + + resetWrap->toggleOn(state->profileIndex.value( + ) | rpl::map(rpl::mappers::_1 != kUnsetColorIndex)); + resetWrap->finishAnimating(); + + Ui::AddDividerText( + container, + group + ? tr::lng_settings_color_choose_group() + : tr::lng_settings_color_choose_channel()); + Ui::AddSkip(container, st::settingsColorSampleSkip); + + container->add(CreateEmojiStatusButton( + container, + show, + channel, + state->statusId.value(), + [=](EmojiStatusId id, TimeId until) { + state->statusId = id; + state->statusUntil = until; + state->statusChanged = true; + }, + group)); + Ui::AddSkip(container, st::settingsColorSampleSkip); + Ui::AddDividerText( + container, + (group + ? tr::lng_edit_channel_status_about_group() + : tr::lng_edit_channel_status_about()), + st::peerAppearanceDividerTextMargin); + }; + + if (group) { + appendProfileSettings(container, peer->asChannel()); } else { - box->addRow(object_ptr( + container->add(object_ptr( box, style, theme, peer, state->index.value(), state->emojiId.value(), - state->collectible.value() - ), style::margins()); + state->collectible.value())); auto indices = peer->session().api().peerColors().suggestedValue(); const auto margin = st::settingsColorRadioMargin; const auto skip = st::settingsColorRadioSkip; - box->addRow( - object_ptr( - box, + container->add( + object_ptr( + container, style, std::move(indices), state->index.value(), @@ -1681,7 +1945,8 @@ void EditPeerColorBox( peer, state->index.value(), state->emojiId.value(), - [=](DocumentId id) { state->emojiId = id; })); + [=](DocumentId id) { state->emojiId = id; }, + false)); Ui::AddSkip(iconInner, st::settingsColorSampleSkip); Ui::AddDividerText( @@ -1761,56 +2026,52 @@ void EditPeerColorBox( statuses->refreshChannelDefault(); statuses->refreshChannelColored(); - Ui::AddSkip(container, st::settingsColorSampleSkip); - container->add(CreateEmojiStatusButton( - container, - show, - channel, - state->statusId.value(), - [=](EmojiStatusId id, TimeId until) { - state->statusId = id; - state->statusUntil = until; - state->statusChanged = true; - }, - group)); - - Ui::AddSkip(container, st::settingsColorSampleSkip); - Ui::AddDividerText( - container, - (group - ? tr::lng_edit_channel_status_about_group() - : tr::lng_edit_channel_status_about()), - st::peerAppearanceDividerTextMargin); + if (!state->preview) { + Ui::AddSkip(container); + appendProfileSettings(container, channel); + } } else if (peer->isSelf()) { Ui::AddSkip(container, st::settingsColorSampleSkip); const auto session = &peer->session(); - AddColorGiftTabs(container, session, [=](uint64 giftId) { - state->showingGiftId = giftId; - }); + const auto switchToNextTab = AddColorGiftTabs( + container, + session, + [=](uint64 giftId) { state->showingGiftId = giftId; }, + false); auto showingGiftId = state->showingGiftId.value(); - AddGiftSelector(container, session, std::move(showingGiftId), [=]( - std::shared_ptr selected) { - state->index = selected->peerColor ? kUnsetColorIndex : 0; - state->emojiId = selected->peerColor - ? selected->peerColor->backgroundEmojiId - : 0; - state->buyCollectible = (selected->peerColor - && (selected->ownerId != session->userPeerId()) - && selected->starsForResale > 0) - ? selected - : nullptr; - state->collectible = selected->peerColor - ? *selected->peerColor - : std::optional(); - }, state->collectible.value()); + AddGiftSelector( + container, + session, + std::move(showingGiftId), + [=](std::shared_ptr selected) { + state->index = selected->peerColor ? kUnsetColorIndex : 0; + state->emojiId = selected->peerColor + ? selected->peerColor->backgroundEmojiId + : 0; + state->buyCollectible = (selected->peerColor + && (selected->ownerId != session->userPeerId()) + && selected->starsForResale > 0) + ? selected + : nullptr; + state->collectible = selected->peerColor + ? *selected->peerColor + : std::optional(); + }, + state->collectible.value() | rpl::map([]( + const std::optional &value) { + return value ? value->collectibleId : 0; + }), + false, + rpl::single(uint64(0)), + switchToNextTab); } - const auto button = box->addButton(tr::lng_settings_color_apply(), [=] { + button->setClickedCallback([=] { if (state->applying) { return; - } else if (ShowPremiumToast(show, peer)) { + } else if (ShowPremiumPreview(show, peer)) { return; } const auto values = SetValues{ @@ -1821,6 +2082,15 @@ void EditPeerColorBox( state->statusUntil, state->statusChanged, }; + const auto profileValues = SetValues{ + .colorIndex = state->profileIndex.current(), + .backgroundEmojiId = state->profileEmojiId.current(), + .colorCollectible = state->collectible.current(), + .statusId = {}, + .statusUntil = 0, + .statusChanged = false, + .forProfile = true, + }; if (const auto buy = state->buyCollectible) { const auto done = [=, weak = base::make_weak(box)](bool ok) { if (ok) { @@ -1835,6 +2105,20 @@ void EditPeerColorBox( return; } state->applying = true; + if (peer->isChannel()) { + // First request: regular color data (without toast) + Apply(show, peer, values, [=] { + // Second request: profile color data (with toast) + Apply(show, peer, profileValues, crl::guard(box, [=] { + box->closeBox(); + }), crl::guard(box, [=] { + state->applying = false; + }), true); + }, crl::guard(box, [=] { + state->applying = false; + }), false); + return; + } Apply(show, peer, values, crl::guard(box, [=] { box->closeBox(); }), crl::guard(box, [=] { @@ -1885,6 +2169,365 @@ void EditPeerColorBox( }, button->lifetime()); } +void EditPeerProfileColorSection( + not_null box, + not_null container, + not_null button, + std::shared_ptr show, + not_null peer, + std::shared_ptr style, + std::shared_ptr theme, + Fn aboutCallback) { + Expects(peer->isSelf()); + + ProcessButton(button); + + const auto preview = CreateProfilePreview(box, container, show, peer); + + const auto peerColors = &peer->session().api().peerColors(); + const auto indices = peerColors->profileColorIndices(); + + struct State { + rpl::variable index = kUnsetColorIndex; + rpl::variable patternEmojiId; + rpl::variable wearable; + rpl::variable showingGiftId; + rpl::variable selectedGiftId; + std::shared_ptr buyCollectible; + Ui::ColorSelector *selector = nullptr; + }; + const auto state = button->lifetime().make_state(); + state->patternEmojiId = peer->profileBackgroundEmojiId(); + state->wearable = peer->emojiStatusId(); + + const auto resetUnique = [=] { + preview->setLocalEmojiStatusId({}); + state->buyCollectible = nullptr; + 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(index == kUnsetColorIndex + ? std::nullopt + : std::make_optional(state->patternEmojiId.current())); + }; + setIndex(peer->colorProfileIndex().value_or(kUnsetColorIndex)); + + const auto margin = st::settingsColorRadioMargin; + const auto skip = st::settingsColorRadioSkip; + state->selector = container->add( + object_ptr( + box, + indices, + state->index.current(), + setIndex, + [=](uint8 index) { + return peerColors->colorProfileFor(index).value_or( + Data::ColorProfileSet{}); + }), + { margin, skip, margin, skip }); + + Ui::AddSkip(container, st::settingsColorSampleSkip); + container->add(CreateEmojiIconButton( + container, + show, + style, + peer, + state->index.value(), + state->patternEmojiId.value(), + [=](DocumentId id) { + state->patternEmojiId = id; + preview->setPatternEmojiId(id); + resetUnique(); + }, + true)); + + const auto resetWrap = container->add( + object_ptr>( + container, + object_ptr(container))); + const auto resetInner = resetWrap->entity(); + + Ui::AddSkip(resetInner, st::settingsColorSampleSkip); + const auto resetButton = resetInner->add( + object_ptr( + resetInner, + tr::lng_settings_color_reset(), + st::settingsButtonLightNoIcon)); + resetButton->setClickedCallback([=] { + state->index = kUnsetColorIndex; + state->patternEmojiId = 0; + preview->setColorProfileIndex(std::nullopt); + preview->setPatternEmojiId(0); + resetUnique(); + resetWrap->toggle(false, anim::type::normal); + }); + + resetWrap->toggleOn(state->index.value( + ) | rpl::map([](uint8 index) { return index != kUnsetColorIndex; })); + resetWrap->finishAnimating(); + + Ui::AddSkip(container, st::settingsColorSampleSkip); + const auto about = Ui::AddDividerText( + container, + tr::lng_settings_color_profile_about( + lt_link, + tr::lng_settings_color_profile_about_link( + lt_emoji, + rpl::single(Ui::Text::IconEmoji(&st::textMoreIconEmoji)), + Ui::Text::RichLangValue + ) | rpl::map([=](TextWithEntities t) { + return Ui::Text::Link(std::move(t), u"internal:"_q); + }), + Ui::Text::RichLangValue)); + Ui::AddSkip(container, st::settingsColorSampleSkip); + about->setClickHandlerFilter([=](auto...) { + aboutCallback(); + return false; + }); + + state->index.value( + ) | rpl::start_with_next([=](uint8 index) { + if (state->selector) { + state->selector->updateSelection(index); + } + }, button->lifetime()); + + if (peer->isSelf()) { + Ui::AddSkip(container, st::settingsColorSampleSkip); + + const auto session = &peer->session(); + const auto switchToNextTab = AddColorGiftTabs( + container, + session, + [=](uint64 giftId) { state->showingGiftId = giftId; }, + true); + + auto showingGiftId = state->showingGiftId.value(); + AddGiftSelector( + container, + session, + std::move(showingGiftId), + [=](std::shared_ptr selected) { + state->selectedGiftId = selected->id; + state->index = kUnsetColorIndex; + state->patternEmojiId = 0; + state->buyCollectible = (selected->peerColor + && (selected->ownerId != session->userPeerId()) + && selected->starsForResale > 0) + ? selected + : nullptr; + const auto statuses = &peer->owner().emojiStatuses(); + state->wearable = statuses->fromUniqueGift(*selected); + preview->setColorProfileIndex(std::nullopt); + preview->setPatternEmojiId(selected->pattern.document->id); + preview->setLocalEmojiStatusId(state->wearable.current()); + resetWrap->toggle(true, anim::type::normal); + }, + state->wearable.value() | rpl::map([=](const EmojiStatusId &value) { + return value.collectible ? value.collectible->id : 0; + }), + true, + state->selectedGiftId.value(), + switchToNextTab); + } + + struct ProfileState { + bool applying = false; + }; + const auto profileState = button->lifetime().make_state(); + + button->setClickedCallback([=] { + if (profileState->applying) { + return; + } 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 = std::nullopt, + .statusId = state->wearable.current(), + .statusUntil = 0, + .statusChanged = statusChanged, + .forProfile = true, + }; + if (const auto buy = state->buyCollectible) { + const auto done = [=, weak = base::make_weak(box)](bool ok) { + if (ok) { + if (const auto strong = weak.get()) { + strong->closeBox(); + } + Apply(show, peer, values, [] {}, [] {}); + } + }; + const auto to = peer->session().user(); + ShowBuyResaleGiftBox(show, buy, false, to, done); + return; + } + profileState->applying = true; + Apply(show, peer, values, crl::guard(box, [=] { + box->closeBox(); + }), crl::guard(box, [=] { + profileState->applying = false; + })); + }); + 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( + (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(); + if (!premium && peer->isSelf()) { + result.append(Ui::Text::IconEmoji(&st::giftBoxLock)); + } + result.append(text); + return result; + })); + } else if (buy->onlyAcceptTon) { + button->setText(rpl::single(QString())); + Ui::SetButtonTwoLabels( + button, + tr::lng_gift_buy_resale_button( + lt_cost, + rpl::single(Data::FormatGiftResaleTon(*buy)), + Ui::Text::WithEntities), + tr::lng_gift_buy_resale_equals( + lt_cost, + rpl::single(Ui::Text::IconEmoji( + &st::starIconEmojiSmall + ).append(Lang::FormatCountDecimal(buy->starsForResale))), + Ui::Text::WithEntities), + st::resaleButtonTitle, + st::resaleButtonSubtitle); + } else { + button->setText(tr::lng_gift_buy_resale_button( + lt_cost, + rpl::single(Ui::Text::IconEmoji(&st::starIconEmoji).append( + Lang::FormatCountDecimal(buy->starsForResale))), + Ui::Text::WithEntities)); + } + }, button->lifetime()); +} + +void EditPeerColorBox( + not_null box, + std::shared_ptr show, + not_null peer, + std::shared_ptr style, + std::shared_ptr theme) { + box->setTitle(peer->isSelf() + ? tr::lng_settings_color_title() + : tr::lng_edit_channel_color()); + box->setWidth(st::boxWideWidth); + box->setStyle(st::giftBox); + box->addTopButton(st::boxTitleClose, [=] { + box->closeBox(); + }); + if (peer->isChannel()) { + const auto button = box->addButton( + tr::lng_settings_color_apply(), + [] {}); + EditPeerColorSection(box, box->verticalLayout(), button, show, peer, style, theme); + return; + } + const auto buttonContainer = box->addButton( + rpl::single(QString()), + [] {}); + const auto content = box->verticalLayout(); + + const auto profileButton = Ui::CreateChild( + buttonContainer, + tr::lng_settings_color_apply(), + box->getDelegate()->style().button); + const auto nameButton = Ui::CreateChild( + buttonContainer, + tr::lng_settings_color_apply(), + box->getDelegate()->style().button); + rpl::combine( + buttonContainer->widthValue(), + profileButton->sizeValue(), + nameButton->sizeValue() + ) | rpl::start_with_next([=](int w, QSize, QSize) { + profileButton->resizeToWidth(w); + nameButton->resizeToWidth(w); + }, buttonContainer->lifetime()); + + auto nameOwned = object_ptr>( + content, + object_ptr(content)); + auto profileOwned = object_ptr>( + content, + object_ptr(content)); + const auto nameWrap = nameOwned.get(); + const auto profileWrap = profileOwned.get(); + const auto name = nameWrap->entity(); + const auto profile = profileWrap->entity(); + + const auto showName = [=] { + nameWrap->toggle(true, anim::type::instant); + profileWrap->toggle(false, anim::type::instant); + profileButton->hide(); + nameButton->show(); + }; + + const auto switchTab = CreateTabsWidget( + content, + { + tr::lng_settings_color_tab_profile(tr::now), + tr::lng_settings_color_tab_name(tr::now), + }, + { + [=] { + nameWrap->toggle(false, anim::type::instant); + profileWrap->toggle(true, anim::type::instant); + nameButton->hide(); + profileButton->show(); + }, + showName, + }); + + Ui::AddSkip(content); + nameWrap->toggle(false, anim::type::instant); + profileWrap->toggle(true, anim::type::instant); + nameButton->hide(); + content->add(std::move(profileOwned)); + content->add(std::move(nameOwned)); + + EditPeerProfileColorSection( + box, + profile, + profileButton, + show, + peer, + style, + theme, + [=] { switchTab(1); }); + + EditPeerColorSection(box, name, nameButton, show, peer, style, theme); +} + void SetupPeerColorSample( not_null button, not_null peer, @@ -1902,32 +2545,106 @@ void SetupPeerColorSample( ) | rpl::map([=] { return peer->colorCollectible(); }); + auto colorProfileIndexValue = peer->session().changes().peerFlagsValue( + peer, + Data::PeerUpdate::Flag::ColorProfile + ) | 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 sample = Ui::CreateChild( + const auto sampleSize = st::settingsColorSampleSize; + + const auto sample = Ui::CreateChild( button.get(), - &peer->session(), + [=] { return Core::TextContext({ .session = &peer->session() }); }, + [=](auto id) { return Data::SingleCustomEmoji(id); }, style, rpl::duplicate(colorIndexValue), rpl::duplicate(colorCollectibleValue), name); sample->show(); + struct ProfileSampleState { + Data::ColorProfileSet colorSet; + }; + const auto profileState + = button->lifetime().make_state(); + + const auto profileSample = Ui::CreateChild( + button.get(), + [=](uint8 index) { return profileState->colorSet; }, + 0, + false); + profileSample->hide(); + profileSample->resize(sampleSize, sampleSize); + + const auto emojiStatusWidget = Ui::CreateChild( + button.get()); + emojiStatusWidget->hide(); + emojiStatusWidget->resize(sampleSize, sampleSize); + button->lifetime().make_state>(); + + struct EmojiStatusState { + std::unique_ptr emoji; + }; + const auto emojiState = button->lifetime().make_state(); + rpl::combine( button->widthValue(), rpl::duplicate(label), - rpl::duplicate(colorIndexValue) + rpl::duplicate(colorIndexValue), + rpl::duplicate(colorProfileIndexValue), + rpl::duplicate(emojiStatusIdValue) ) | rpl::start_with_next([=]( int width, - const QString &button, - int colorIndex) { - const auto sampleSize = st::settingsColorSampleSize; + const QString &buttonText, + int colorIndex, + std::optional profileIndex, + EmojiStatusId emojiStatusId) { const auto available = width - st::settingsButton.padding.left() - (st::settingsColorButton.padding.right() - sampleSize) - - st::settingsButton.style.font->width(button) + - st::settingsButton.style.font->width(buttonText) - st::settingsButtonRightSkip; - if (style->colorPatternIndex(colorIndex)) { + + 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) { sample->resize(sampleSize, sampleSize); } else { const auto padding = st::settingsColorSamplePadding; @@ -1938,13 +2655,27 @@ void SetupPeerColorSample( sample->resize(std::min(wantedWidth, available), wantedHeight); } sample->update(); + sample->setCutoutPadding(hasProfile + ? st::settingsColorSampleCutout + : 0); + profileSample->update(); + emojiStatusWidget->update(); }, sample->lifetime()); rpl::combine( button->sizeValue(), sample->sizeValue(), - std::move(colorIndexValue) - ) | rpl::start_with_next([=](QSize outer, QSize inner, int colorIndex) { + rpl::duplicate(colorIndexValue), + rpl::duplicate(colorProfileIndexValue), + rpl::duplicate(emojiStatusIdValue) + ) | rpl::start_with_next([=]( + QSize outer, + QSize inner, + int colorIndex, + std::optional profileIndex, + EmojiStatusId emojiStatusId) { + const auto hasColor = (colorIndex != 0); + const auto right = st::settingsColorButton.padding.right() - st::settingsColorSampleSkip - st::settingsColorSampleSize @@ -1954,9 +2685,34 @@ void SetupPeerColorSample( sample->move( outer.width() - right - inner.width(), (outer.height() - inner.height()) / 2); + 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( @@ -1983,6 +2739,32 @@ void AddPeerColorButton( SetupPeerColorSample(button, peer, rpl::duplicate(label), style); } + { + const auto badge = Ui::NewBadge::CreateNewBadge( + button, + tr::lng_premium_summary_new_badge()).get(); + rpl::combine( + rpl::duplicate(label), + button->widthValue() + ) | rpl::start_with_next([=]( + const QString &text, + int width) { + const auto space = st.style.font->spacew; + const auto left = st.padding.left() + + st.style.font->width(text) + + space; + const auto available = width - left - st.padding.right(); + badge->setVisible(available >= badge->width()); + if (!badge->isHidden()) { + const auto top = st.padding.top() + + st.style.font->ascent + - st::settingsPremiumNewBadge.style.font->ascent + - st::settingsPremiumNewBadgePadding.top(); + badge->moveToLeft(left, top, width); + } + }, badge->lifetime()); + } + button->setClickedCallback([=] { show->show(Box(EditPeerColorBox, show, peer, style, theme)); }); diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp index 33b41cc31c..4fb3acfb3c 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp @@ -2289,6 +2289,9 @@ void Controller::saveUsernamesOrder() { continueSave(); }).send(); } else { + const auto weakContinue = crl::guard(this, [=] { + continueSave(); + }); const auto lifetime = std::make_shared(); const auto newUsernames = (*_savingData.usernamesOrder); _peer->session().api().usernames().reorder( @@ -2306,7 +2309,7 @@ void Controller::saveUsernamesOrder() { .editable = editable, }; }) | ranges::to_vector); - continueSave(); + weakContinue(); lifetime->destroy(); }, *lifetime); } diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp index 8ae139d446..d3e1d98e1c 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp @@ -174,6 +174,7 @@ constexpr auto kDefaultChargeStars = 10; { Flag::ManageCall, tr::lng_rights_channel_manage_calls(tr::now) }, { Flag::ManageDirect, tr::lng_rights_channel_manage_direct(tr::now) }, { Flag::AddAdmins, tr::lng_rights_add_admins(tr::now) }, + { Flag::BanUsers, tr::lng_rights_group_ban(tr::now) }, }; return { { std::nullopt, std::move(first) }, diff --git a/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp b/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp index 57f69b2de7..1990af38cd 100644 --- a/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp @@ -12,7 +12,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/event_filter.h" #include "base/unixtime.h" #include "boxes/peer_list_box.h" -#include "boxes/star_gift_box.h" #include "data/stickers/data_custom_emoji.h" #include "data/data_premium_limits.h" #include "data/data_channel.h" @@ -36,6 +35,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/empty_userpic.h" #include "ui/dynamic_image.h" #include "ui/painter.h" +#include "ui/top_background_gradient.h" #include "styles/style_boxes.h" #include "styles/style_credits.h" #include "styles/style_premium.h" @@ -433,6 +433,7 @@ Ui::BoostCounters ParseBoostCounters( Ui::BoostFeatures LookupBoostFeatures(not_null channel) { auto nameColorsByLevel = base::flat_map(); auto linkStylesByLevel = base::flat_map(); + auto profileColorsByLevel = base::flat_map(); const auto group = channel->isMegagroup(); const auto peerColors = &channel->session().api().peerColors(); const auto &list = group @@ -445,6 +446,29 @@ Ui::BoostFeatures LookupBoostFeatures(not_null channel) { } ++linkStylesByLevel[level]; } + { + const auto profileIndices = peerColors->profileColorIndices(); + auto lowestNonZeroLevel = std::numeric_limits::max(); + auto levels = std::vector(); + levels.reserve(profileIndices.size()); + + for (const auto index : profileIndices) { + const auto level = peerColors->requiredLevelFor( + channel->id, + index, + group, + true); + levels.push_back(level); + if (level) { + lowestNonZeroLevel = std::min(lowestNonZeroLevel, level); + } + } + + for (const auto level : levels) { + ++profileColorsByLevel[std::max(level, lowestNonZeroLevel)]; + } + } + const auto &themes = channel->owner().cloudThemes().chatThemes(); if (themes.empty()) { channel->owner().cloudThemes().refreshChatThemes(); @@ -453,7 +477,11 @@ Ui::BoostFeatures LookupBoostFeatures(not_null channel) { return Ui::BoostFeatures{ .nameColorsByLevel = std::move(nameColorsByLevel), .linkStylesByLevel = std::move(linkStylesByLevel), + .profileColorsByLevel = std::move(profileColorsByLevel), .linkLogoLevel = group ? 0 : levelLimits.channelBgIconLevelMin(), + .profileIconLevel = group + ? levelLimits.groupProfileBgIconLevelMin() + : levelLimits.channelProfileBgIconLevelMin(), .autotranslateLevel = group ? 0 : levelLimits.channelAutoTranslateLevelMin(), .transcribeLevel = group ? levelLimits.groupTranscribeLevelMin() : 0, .emojiPackLevel = group ? levelLimits.groupEmojiStickersLevelMin() : 0, @@ -549,6 +577,7 @@ object_ptr CreateUserpicsTransfer( rpl::producer>> from, not_null to, UserpicsTransferType type) { + using Type = UserpicsTransferType; struct State { std::vector> from; std::vector> buttons; @@ -648,27 +677,28 @@ object_ptr CreateUserpicsTransfer( button->render(&q, position, QRegion(), QWidget::DrawChildren); } state->painting = false; - const auto boosting = (type == UserpicsTransferType::BoostReplace); const auto last = state->buttons.back().get(); - const auto back = boosting ? last : right; - const auto add = st::boostReplaceIconAdd; - const auto &icon = boosting - ? st::boostReplaceIcon - : st::starrefJoinIcon; - const auto skip = boosting ? st::boostReplaceIconSkip : 0; - const auto w = icon.width() + 2 * skip; - const auto h = icon.height() + 2 * skip; - const auto x = back->x() + back->width() - w + add.x(); - const auto y = back->y() + back->height() - h + add.y(); - - auto brush = QLinearGradient(QPointF(x + w, y + h), QPointF(x, y)); - brush.setStops(Ui::Premium::ButtonGradientStops()); - q.setBrush(brush); - pen.setWidthF(stroke); - q.setPen(pen); - q.drawEllipse(x - half, y - half, w + stroke, h + stroke); - icon.paint(q, x + skip, y + skip, outerw); + if (type != Type::AuctionRecipient) { + const auto boosting = (type == Type::BoostReplace); + const auto back = boosting ? last : right; + const auto add = st::boostReplaceIconAdd; + const auto &icon = boosting + ? st::boostReplaceIcon + : st::starrefJoinIcon; + const auto skip = boosting ? st::boostReplaceIconSkip : 0; + const auto w = icon.width() + 2 * skip; + const auto h = icon.height() + 2 * skip; + const auto x = back->x() + back->width() - w + add.x(); + const auto y = back->y() + back->height() - h + add.y(); + auto brush = QLinearGradient(QPointF(x + w, y + h), QPointF(x, y)); + brush.setStops(Ui::Premium::ButtonGradientStops()); + q.setBrush(brush); + pen.setWidthF(stroke); + q.setPen(pen); + q.drawEllipse(x - half, y - half, w + stroke, h + stroke); + icon.paint(q, x + skip, y + skip, outerw); + } const auto size = st::boostReplaceArrow.size(); st::boostReplaceArrow.paint( q, @@ -677,7 +707,6 @@ object_ptr CreateUserpicsTransfer( + (st::boostReplaceUserpicsSkip - size.width()) / 2), (last->height() - size.height()) / 2, outerw); - q.end(); auto p = QPainter(overlay); @@ -895,9 +924,9 @@ public: auto p = QPainter(&_backgroundCache); p.setClipRect(inner); const auto skip = inner.width() / 3; - Ui::PaintPoints( + Ui::PaintBgPoints( p, - Ui::PatternPointsSmall(), + Ui::PatternBgPointsSmall(), _patternCache, _patternEmoji.get(), *_unique, diff --git a/Telegram/SourceFiles/boxes/peers/replace_boost_box.h b/Telegram/SourceFiles/boxes/peers/replace_boost_box.h index 5ab453f5bf..1ad668ac81 100644 --- a/Telegram/SourceFiles/boxes/peers/replace_boost_box.h +++ b/Telegram/SourceFiles/boxes/peers/replace_boost_box.h @@ -64,6 +64,7 @@ object_ptr ReassignBoostsBox( enum class UserpicsTransferType { BoostReplace, StarRefJoin, + AuctionRecipient, }; [[nodiscard]] object_ptr CreateUserpicsTransfer( not_null parent, diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.cpp b/Telegram/SourceFiles/boxes/premium_preview_box.cpp index 53fa77aee0..58688763b2 100644 --- a/Telegram/SourceFiles/boxes/premium_preview_box.cpp +++ b/Telegram/SourceFiles/boxes/premium_preview_box.cpp @@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_streaming.h" #include "data/data_peer_values.h" #include "data/data_premium_limits.h" +#include "info/profile/info_profile_icon.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "main/main_domain.h" // kMaxAccounts @@ -43,7 +44,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_session_controller.h" #include "api/api_premium.h" #include "apiwrap.h" +#include "styles/style_credits.h" // upgradeGiftSubtext +#include "styles/style_info.h" // infoStarsUnderstood #include "styles/style_layers.h" +#include "styles/style_menu_icons.h" #include "styles/style_premium.h" #include "styles/style_settings.h" @@ -135,6 +139,10 @@ void PreloadSticker(const std::shared_ptr &media) { return tr::lng_premium_summary_subtitle_effects(); case PremiumFeature::TodoLists: return tr::lng_premium_summary_subtitle_todo_lists(); + case PremiumFeature::PeerColors: + return tr::lng_premium_summary_subtitle_peer_colors(); + case PremiumFeature::Gifts: + return tr::lng_premium_summary_subtitle_gifts(); case PremiumFeature::BusinessLocation: return tr::lng_business_subtitle_location(); @@ -202,6 +210,10 @@ void PreloadSticker(const std::shared_ptr &media) { return tr::lng_premium_summary_about_effects(); case PremiumFeature::TodoLists: return tr::lng_premium_summary_about_todo_lists(); + case PremiumFeature::PeerColors: + return tr::lng_premium_summary_about_peer_colors(); + case PremiumFeature::Gifts: + return tr::lng_premium_summary_about_gifts(); case PremiumFeature::BusinessLocation: return tr::lng_business_about_location(); @@ -543,6 +555,8 @@ struct VideoPreviewDocument { case PremiumFeature::MessagePrivacy: return "message_privacy"; case PremiumFeature::Effects: return "effects"; case PremiumFeature::TodoLists: return "todo"; + case PremiumFeature::PeerColors: return "peer_colors"; + case PremiumFeature::Gifts: return "gifts"; case PremiumFeature::BusinessLocation: return "business_location"; case PremiumFeature::BusinessHours: return "business_hours"; @@ -889,6 +903,45 @@ struct VideoPreviewDocument { return result; } +void AddGiftsInfoRows(not_null container) { + const auto infoRow = [&]( + rpl::producer title, + rpl::producer text, + not_null icon) { + auto raw = container->add( + object_ptr(container)); + raw->add( + object_ptr( + raw, + std::move(title) | Ui::Text::ToBold(), + st::defaultFlatLabel), + st::settingsPremiumRowTitlePadding); + raw->add( + object_ptr( + raw, + std::move(text), + st::upgradeGiftSubtext), + st::settingsPremiumRowAboutPadding); + object_ptr( + raw, + *icon, + st::starrefInfoIconPosition); + }; + + infoRow( + tr::lng_gift_upgrade_unique_title(), + tr::lng_gift_upgrade_unique_about(), + &st::menuIconUnique); + infoRow( + tr::lng_gift_upgrade_tradable_title(), + tr::lng_gift_upgrade_tradable_about(), + &st::menuIconTradable); + infoRow( + tr::lng_gift_upgrade_wearable_title(), + tr::lng_gift_upgrade_wearable_about(), + &st::menuIconNftWear); +} + void PreviewBox( not_null box, std::shared_ptr show, @@ -951,22 +1004,32 @@ void PreviewBox( st::settingsPremiumTopBarClose); close->setClickedCallback([=] { box->closeBox(); }); - const auto left = Ui::CreateChild( + const auto gifts = (state->selected.current() == PremiumFeature::Gifts); + + const auto left = gifts ? nullptr : Ui::CreateChild( buttonsParent, st::settingsPremiumMoveLeft); - left->setClickedCallback([=] { move(-1); }); + if (left) { + left->setClickedCallback([=] { move(-1); }); + } - const auto right = Ui::CreateChild( + const auto right = gifts ? nullptr : Ui::CreateChild( buttonsParent, st::settingsPremiumMoveRight); - right->setClickedCallback([=] { move(1); }); + if (right) { + right->setClickedCallback([=] { move(1); }); + } buttonsParent->widthValue( ) | rpl::start_with_next([=](int width) { const auto outerHeight = st::premiumPreviewHeight; close->moveToRight(0, 0, width); - left->moveToLeft(0, (outerHeight - left->height()) / 2, width); - right->moveToRight(0, (outerHeight - right->height()) / 2, width); + if (left) { + left->moveToLeft(0, (outerHeight - left->height()) / 2, width); + } + if (right) { + right->moveToRight(0, (outerHeight - right->height()) / 2, width); + } }, close->lifetime()); state->preload = [=] { @@ -1094,16 +1157,30 @@ void PreviewBox( st::premiumPreviewAboutPadding, style::al_top )->setTryMakeSimilarLines(true); - box->addRow( - CreateSwitch(box->verticalLayout(), &state->selected, state->order), - st::premiumDotsMargin); + + if (gifts) { + box->setStyle(st::giftBox); + AddGiftsInfoRows(box->verticalLayout()); + } else { + box->addRow( + CreateSwitch(box->verticalLayout(), &state->selected, state->order), + st::premiumDotsMargin); + } const auto showFinished = [=] { state->showFinished = true; if (base::take(state->preloadScheduled)) { state->preload(); } }; - if ((descriptor.fromSettings && show->session().premium()) + if (gifts) { + box->setShowFinishedCallback(showFinished); + box->addButton( + rpl::single(QString()), + [=] { box->closeBox(); } + )->setText(rpl::single(Ui::Text::IconEmoji( + &st::infoStarsUnderstood + ).append(' ').append(tr::lng_auction_about_understood(tr::now)))); + } else if ((descriptor.fromSettings && show->session().premium()) || descriptor.hideSubscriptionButton) { box->setShowFinishedCallback(showFinished); box->addButton(tr::lng_close(), [=] { box->closeBox(); }); diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.h b/Telegram/SourceFiles/boxes/premium_preview_box.h index 9ac7d04ac2..0f64a0eaed 100644 --- a/Telegram/SourceFiles/boxes/premium_preview_box.h +++ b/Telegram/SourceFiles/boxes/premium_preview_box.h @@ -73,6 +73,8 @@ enum class PremiumFeature { Effects, FilterTags, TodoLists, + PeerColors, + Gifts, // Business features. BusinessLocation, diff --git a/Telegram/SourceFiles/boxes/report_messages_box.cpp b/Telegram/SourceFiles/boxes/report_messages_box.cpp index 25e548f1b2..8855cd581e 100644 --- a/Telegram/SourceFiles/boxes/report_messages_box.cpp +++ b/Telegram/SourceFiles/boxes/report_messages_box.cpp @@ -147,8 +147,7 @@ void ShowReportMessageBox( auto label = object_ptr( container, tr::lng_report_details_message_about(), - st::boxDividerLabel); - label->setTextColorOverride(st->dividerFg->c); + st->divider.label); using namespace Ui; const auto widget = container->add( object_ptr>( @@ -159,7 +158,7 @@ void ShowReportMessageBox( = CreateChild( widget, st::boxDividerHeight, - st->dividerBg, + st->divider.bar, RectPart::Top | RectPart::Bottom); background->lower(); widget->sizeValue( diff --git a/Telegram/SourceFiles/boxes/send_files_box.cpp b/Telegram/SourceFiles/boxes/send_files_box.cpp index 3aaca4e870..622334f8c4 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.cpp +++ b/Telegram/SourceFiles/boxes/send_files_box.cpp @@ -766,7 +766,9 @@ void SendFilesBox::refreshButtons() { _send, _show, _sendMenuDetails, - _sendMenuCallback); + _sendMenuCallback, + &_st.tabbed.menu, + &_st.tabbed.icons); } addButton(tr::lng_cancel(), [=] { closeBox(); }); _addFile = addLeftButton( diff --git a/Telegram/SourceFiles/boxes/send_gif_with_caption_box.cpp b/Telegram/SourceFiles/boxes/send_gif_with_caption_box.cpp index ce08b9ff69..4e83fd7e72 100644 --- a/Telegram/SourceFiles/boxes/send_gif_with_caption_box.cpp +++ b/Telegram/SourceFiles/boxes/send_gif_with_caption_box.cpp @@ -121,7 +121,7 @@ namespace { return true; }; if (!updateThumbnail()) { - document->owner().session().downloaderTaskFinished( + document->session().downloaderTaskFinished( ) | rpl::start_with_next([=] { if (updateThumbnail()) { state->loadingLifetime.destroy(); diff --git a/Telegram/SourceFiles/boxes/share_box.cpp b/Telegram/SourceFiles/boxes/share_box.cpp index 2d08549746..b5a2e07721 100644 --- a/Telegram/SourceFiles/boxes/share_box.cpp +++ b/Telegram/SourceFiles/boxes/share_box.cpp @@ -592,7 +592,7 @@ void ShareBox::showMenu(not_null parent) { uiShow()->showBox( HistoryView::PrepareScheduleBox( this, - nullptr, // ChatHelpers::Show for effect attachment. + _descriptor.session, sendMenuDetails(), [=](Api::SendOptions options) { submit(options); }, action.options, @@ -1713,6 +1713,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)) @@ -1734,14 +1737,14 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback( } return result; }; - auto &api = history->owner().session().api(); + auto &api = history->session().api(); auto &histories = history->owner().histories(); const auto donePhraseArgs = CreateForwardedMessagePhraseArgs( result, msgIds); const auto showRecentForwardsToSelf = result.size() == 1 && result.front()->peer()->isSelf() - && history->owner().session().premium(); + && history->session().premium(); const auto requestType = Data::Histories::RequestType::Send; @@ -1821,7 +1824,8 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback( : Flag(0)) | (starsPaid ? Flag::f_allow_paid_stars : Flag()) | (sublistPeer ? Flag::f_reply_to : Flag()) - | (options.suggest ? Flag::f_suggested_post : Flag()); + | (options.suggest ? Flag::f_suggested_post : Flag()) + | (options.effectId ? Flag::f_effect : Flag()); threadHistory->sendRequestId = api.request( MTPmessages_ForwardMessages( MTP_flags(sendFlags), @@ -1834,8 +1838,10 @@ 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_long(options.effectId), MTP_int(videoTimestamp.value_or(0)), MTP_long(starsPaid), Api::SuggestToMTP(options.suggest) diff --git a/Telegram/SourceFiles/boxes/star_gift_auction_box.cpp b/Telegram/SourceFiles/boxes/star_gift_auction_box.cpp new file mode 100644 index 0000000000..0b553a0b8c --- /dev/null +++ b/Telegram/SourceFiles/boxes/star_gift_auction_box.cpp @@ -0,0 +1,2114 @@ +/* +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_auction_box.h" + +#include "api/api_text_entities.h" +#include "base/random.h" +#include "base/timer_rpl.h" +#include "base/unixtime.h" +#include "boxes/peers/replace_boost_box.h" +#include "boxes/premium_preview_box.h" +#include "boxes/send_credits_box.h" // CreditsEmojiSmall +#include "boxes/share_box.h" +#include "boxes/star_gift_box.h" +#include "boxes/star_gift_preview_box.h" +#include "boxes/star_gift_resale_box.h" +#include "calls/group/calls_group_common.h" +#include "core/application.h" +#include "core/credits_amount.h" +#include "core/ui_integration.h" +#include "data/components/credits.h" +#include "data/components/gift_auctions.h" +#include "data/stickers/data_custom_emoji.h" +#include "data/data_document.h" +#include "data/data_message_reactions.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "history/view/controls/history_view_suggest_options.h" +#include "info/channel_statistics/earn/earn_icons.h" +#include "info/peer_gifts/info_peer_gifts_common.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 "payments/payments_checkout_process.h" +#include "settings/settings_credits_graphics.h" +#include "storage/storage_account.h" +#include "ui/boxes/confirm_box.h" +#include "ui/controls/button_labels.h" +#include "ui/controls/feature_list.h" +#include "ui/controls/table_rows.h" +#include "ui/controls/userpic_button.h" +#include "ui/effects/premium_bubble.h" +#include "ui/layers/generic_box.h" +#include "ui/text/custom_emoji_helper.h" +#include "ui/text/custom_emoji_text_badge.h" +#include "ui/text/format_values.h" +#include "ui/text/text_utilities.h" +#include "ui/toast/toast.h" +#include "ui/widgets/fields/number_input.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/popup_menu.h" +#include "ui/wrap/table_layout.h" +#include "ui/color_int_conversion.h" +#include "ui/dynamic_thumbnails.h" +#include "ui/vertical_list.h" +#include "window/window_session_controller.h" +#include "styles/style_calls.h" +#include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" +#include "styles/style_credits.h" +#include "styles/style_giveaway.h" +#include "styles/style_info.h" +#include "styles/style_layers.h" +#include "styles/style_menu_icons.h" +#include "styles/style_premium.h" +#include "styles/style_settings.h" + +#include +#include + +namespace Ui { +namespace { + +constexpr auto kAuctionAboutShownPref = "gift_auction_about_shown"_cs; +constexpr auto kBidPlacedToastDuration = 5 * crl::time(1000); +constexpr auto kSwitchPreviewCoverInterval = 3 * crl::time(1000); +constexpr auto kMaxShownBid = 30'000; +constexpr auto kShowTopPlaces = 3; + +enum class BidType { + Setting, + Winning, + Loosing, +}; +struct BidRowData { + UserData *user = nullptr; + int stars = 0; + int position = 0; + int winners = 0; + QString place; + BidType type = BidType::Setting; + + friend inline bool operator==( + const BidRowData &, + const BidRowData &) = default; +}; + +struct BidSliderValues { + int min = 0; + int explicitlyAllowed = 0; + int max = 0; + + friend inline bool operator==( + const BidSliderValues &, + const BidSliderValues &) = default; +}; + +[[nodiscard]] std::optional BidColorOverride(int position, int per) { + return (position <= per) + ? st::boxTextFgGood->c + : st::attentionButtonFg->c; + //switch (type) { + //case BidType::Setting: return {}; + //case BidType::Winning: return st::boxTextFgGood->c; + //case BidType::Loosing: return st::attentionButtonFg->c; + //} + //Unexpected("Type in BidType."); +} + +[[nodiscard]] rpl::producer MinutesLeftTillValue(TimeId endDate) { + return [=](auto consumer) { + auto lifetime = rpl::lifetime(); + + const auto now = base::unixtime::now(); + if (endDate <= now) { + consumer.put_next(0); + consumer.put_done(); + return lifetime; + } + + const auto timer = lifetime.make_state(); + const auto callback = [=] { + const auto now = base::unixtime::now(); + const auto left = (endDate > now) ? endDate - now : 0; + const auto minutes = (left + 59) / 60; + consumer.put_next_copy(minutes); + if (minutes) { + const auto next = left % 60; + const auto wait = next ? next : 60; + timer->callOnce(wait * crl::time(1000)); + } else { + consumer.put_done(); + } + }; + timer->setCallback(callback); + callback(); + + return lifetime; + }; +} + +[[nodiscard]] rpl::producer SecondsLeftTillValue(TimeId endDate) { + return [=](auto consumer) { + auto lifetime = rpl::lifetime(); + + const auto now = base::unixtime::now(); + const auto left = (endDate > now) ? endDate - now : 0; + if (!left) { + consumer.put_next(0); + consumer.put_done(); + return lifetime; + } + + const auto starts = crl::now(); + const auto ends = starts + left * crl::time(1000); + const auto timer = lifetime.make_state(); + const auto callback = [=] { + const auto now = crl::now(); + const auto left = (ends > now) ? ends - now : 0; + const auto seconds = (left + 999) / 1000; + consumer.put_next_copy(seconds); + if (seconds) { + const auto next = left % 1000; + const auto wait = next ? next : 1000; + timer->callOnce(wait); + } else { + consumer.put_done(); + } + }; + timer->setCallback(callback); + callback(); + + return lifetime; + }; +} + +[[nodiscard]] QString NiceCountdownText(int seconds) { + const auto minutes = seconds / 60; + const auto hours = minutes / 60; + return hours + ? u"%1:%2:%3"_q + .arg(hours) + .arg((minutes % 60), 2, 10, QChar('0')) + .arg((seconds % 60), 2, 10, QChar('0')) + : u"%1:%2"_q.arg(minutes).arg((seconds % 60), 2, 10, QChar('0')); +} + +[[nodiscard]] object_ptr MakeBidRow( + not_null parent, + std::shared_ptr show, + rpl::producer data) { + auto result = object_ptr(parent.get()); + const auto raw = result.data(); + + raw->setAttribute(Qt::WA_TransparentForMouseEvents); + + struct State { + std::unique_ptr place; + std::unique_ptr userpic; + std::unique_ptr name; + std::unique_ptr stars; + UserData *user = nullptr; + }; + const auto state = raw->lifetime().make_state(); + state->place = std::make_unique( + raw, + rpl::duplicate(data) | rpl::map(&BidRowData::place), + st::auctionBidPlace); + + auto name = rpl::duplicate(data) | rpl::map([](const BidRowData &bid) { + return bid.user ? bid.user->name() : QString(); + }); + state->name = std::make_unique( + raw, + std::move(name), + st::auctionBidName); + + auto helper = Text::CustomEmojiHelper(Core::TextContext({ + .session = &show->session(), + })); + const auto star = helper.paletteDependent(Ui::Earn::IconCreditsEmoji()); + auto stars = rpl::duplicate(data) | rpl::map([=](const BidRowData &bid) { + return tr::marked(star).append(' ').append( + Lang::FormatCountDecimal(bid.stars)); + }); + state->stars = std::make_unique( + raw, + std::move(stars), + st::auctionBidStars, + st::defaultPopupMenu, + helper.context()); + + const auto kHuge = u"99999"_q; + const auto userpicLeft = st::auctionBidPlace.style.font->width(kHuge); + + std::move(data) | rpl::start_with_next([=](BidRowData bid) { + state->place->setTextColorOverride( + BidColorOverride(bid.position, bid.winners)); + if (state->user != bid.user) { + state->user = bid.user; + if (state->user) { + if (auto was = state->userpic.release()) { + was->hide(); + crl::on_main(was, [=] { delete was; }); + } + state->userpic = std::make_unique( + raw, + state->user, + st::auctionBidUserpic); + state->userpic->show(); + state->userpic->moveToLeft(userpicLeft, 0); + raw->resize(raw->width(), state->userpic->height()); + } else { + raw->resize(raw->width(), 0); + } + } + }, raw->lifetime()); + + rpl::combine( + raw->widthValue(), + state->stars->widthValue() + ) | rpl::start_with_next([=](int outer, int stars) { + const auto userpicSize = st::auctionBidUserpic.size; + const auto top = (userpicSize.height() - st::normalFont->height) / 2; + state->place->moveToLeft(0, top, outer); + if (state->userpic) { + state->userpic->moveToLeft(userpicLeft, 0, outer); + } + state->stars->moveToRight(0, top, outer); + + const auto userpicRight = userpicLeft + userpicSize.width(); + const auto nameLeft = userpicRight + st::auctionBidSkip; + const auto nameRight = stars + st::auctionBidSkip; + state->name->resizeToWidth(outer - nameLeft - nameRight); + state->name->moveToLeft(nameLeft, top, outer); + }, raw->lifetime()); + + return result; +} + +Fn)> MakeAuctionFillMenuCallback( + std::shared_ptr show, + const Data::GiftAuctionState &state) { + const auto url = show->session().createInternalLinkFull( + u"auction/"_q + state.gift->auctionSlug); + const auto rounds = state.totalRounds; + const auto perRound = state.gift->auctionGiftsPerRound;; + return [=](not_null menu) { + menu->addAction(tr::lng_auction_menu_about(tr::now), [=] { + show->show(Box(AuctionAboutBox, rounds, perRound, nullptr)); + }, &st::menuIconInfo); + + menu->addAction(tr::lng_auction_menu_copy_link(tr::now), [=] { + QApplication::clipboard()->setText(url); + show->showToast(tr::lng_username_copied(tr::now)); + }, &st::menuIconLink); + + menu->addAction(tr::lng_auction_menu_share(tr::now), [=] { + FastShareLink(show, url); + }, &st::menuIconShare); + }; +} + +Fn MakeAuctionMenuCallback( + not_null parent, + std::shared_ptr show, + const Data::GiftAuctionState &state) { + const auto menu = std::make_shared>(); + return [=, fill = MakeAuctionFillMenuCallback(show, state)] { + *menu = base::make_unique_q( + parent, + st::popupMenuWithIcons); + fill(menu->get()); + (*menu)->popup(QCursor::pos()); + }; +} + +void PlaceAuctionBid( + std::shared_ptr show, + not_null to, + int64 amount, + const Data::GiftAuctionState &state, + std::unique_ptr details, + Fn done) { + auto paymentDone = [=]( + Payments::CheckoutResult result, + const MTPUpdates *updates) { + done(result); + }; + const auto passDetails = (details || !state.my.bid); + const auto hideName = passDetails && details && details->anonymous; + const auto text = details ? details->text : TextWithEntities(); + + using Flag = MTPDinputInvoiceStarGiftAuctionBid::Flag; + const auto invoice = MTP_inputInvoiceStarGiftAuctionBid( + MTP_flags((state.my.bid ? Flag::f_update_bid : Flag()) + | (passDetails ? Flag::f_peer : Flag()) + | (passDetails ? Flag::f_message : Flag()) + | (hideName ? Flag::f_hide_name : Flag())), + passDetails ? to->input : MTP_inputPeerEmpty(), + MTP_long(state.gift->id), + MTP_long(amount), + MTP_textWithEntities( + MTP_string(text.text), + Api::EntitiesToMTP(&to->session(), text.entities))); + RequestOurForm(show, invoice, [=]( + uint64 formId, + CreditsAmount price, + std::optional failure) { + if (failure) { + paymentDone(*failure, nullptr); + } else { + SubmitStarsForm( + show, + invoice, + formId, + price.whole(), + paymentDone); + } + }); +} + +object_ptr MakeAuctionInfoBlocks( + not_null box, + not_null session, + rpl::producer stateValue, + Fn setMinimal) { + auto helper = Text::CustomEmojiHelper(Core::TextContext({ + .session = session, + })); + const auto star = helper.paletteDependent(Ui::Earn::IconCreditsEmoji()); + + auto bidTitle = rpl::duplicate( + stateValue + ) | rpl::map([=](const Data::GiftAuctionState &state) { + const auto count = int(state.my.minBidAmount + ? state.my.minBidAmount + : state.minBidAmount); + const auto text = (count >= 10'000'000) + ? Lang::FormatCountToShort(count).string + : (count >= 1000'000) + ? Lang::FormatCountToShort(count, true).string + : Lang::FormatCountDecimal(count); + return tr::marked(star).append(' ').append(text); + }); + auto minimal = rpl::duplicate( + stateValue + ) | rpl::map([=](const Data::GiftAuctionState &state) { + return state.my.minBidAmount + ? state.my.minBidAmount + : state.minBidAmount; + }) | tr::to_count(); + auto untilTitle = rpl::duplicate( + stateValue + ) | rpl::map([=](const Data::GiftAuctionState &state) { + return SecondsLeftTillValue(state.startDate) | rpl::then( + SecondsLeftTillValue(state.nextRoundAt + ? state.nextRoundAt + : state.endDate)); + }) | rpl::flatten_latest( + ) | rpl::map(NiceCountdownText) | rpl::map(tr::marked); + auto untilSubtext = rpl::duplicate( + stateValue + ) | rpl::map([=](const Data::GiftAuctionState &state) { + auto preview = SecondsLeftTillValue( + state.startDate + ) | rpl::map(rpl::mappers::_1 > 0) | rpl::distinct_until_changed(); + return rpl::conditional( + std::move(preview), + tr::lng_auction_bid_before_start(), + tr::lng_auction_bid_until()); + }) | rpl::flatten_latest(); + auto leftTitle = rpl::duplicate( + stateValue + ) | rpl::map([=](const Data::GiftAuctionState &state) { + return Data::SingleCustomEmoji( + state.gift->document + ).append(' ').append(Lang::FormatCountDecimal(state.giftsLeft)); + }); + auto left = rpl::duplicate( + stateValue + ) | rpl::map([=](const Data::GiftAuctionState &state) { + return state.giftsLeft; + }) | tr::to_count(); + return MakeStarSelectInfoBlocks(box, { + { + .title = std::move(bidTitle), + .subtext = tr::lng_auction_bid_minimal( + lt_count, + std::move(minimal)), + .click = setMinimal, + }, + { + .title = std::move(untilTitle), + .subtext = std::move(untilSubtext), + }, + { + .title = std::move(leftTitle), + .subtext = tr::lng_auction_bid_left(lt_count, std::move(left)) + }, + }, helper.context()); +} + +void AddBidPlaces( + not_null box, + std::shared_ptr show, + rpl::producer value, + rpl::producer chosen) { + struct My { + BidType type; + int position = 0; + + inline bool operator==(const My &) const = default; + }; + struct State { + rpl::variable my; + rpl::variable started; + rpl::variable> top; + std::vector cache; + int winners = 0; + }; + const auto state = box->lifetime().make_state(); + + rpl::duplicate( + value + ) | rpl::start_with_next([=](const Data::GiftAuctionState &value) { + auto cache = std::vector(); + cache.reserve(value.topBidders.size()); + for (const auto &user : value.topBidders) { + cache.push_back(user->createUserpicView()); + } + state->winners = value.gift->auctionGiftsPerRound; + state->cache = std::move(cache); + state->started = SecondsLeftTillValue( + value.startDate + ) | rpl::map(!rpl::mappers::_1); + }, box->lifetime()); + + state->my = rpl::combine( + rpl::duplicate(value), + rpl::duplicate(chosen) + ) | rpl::map([=](const Data::GiftAuctionState &value, int chosen) { + const auto my = value.my.bid; + const auto &levels = value.bidLevels; + + auto top = std::vector(); + top.reserve(kShowTopPlaces); + const auto pushTop = [&](auto i) { + const auto index = int(i - begin(levels)); + if (top.size() >= kShowTopPlaces + || index >= value.topBidders.size()) { + return false; + } else if (!value.topBidders[index]->isSelf()) { + top.push_back({ value.topBidders[index], int(i->amount) }); + } + return true; + }; + + const auto setting = (chosen > my); + const auto finishWith = [&](int position) { + state->top = std::move(top); + const auto type = setting + ? BidType::Setting + : (position <= value.gift->auctionGiftsPerRound) + ? BidType::Winning + : BidType::Loosing; + return My{ type, position }; + }; + + for (auto i = begin(levels), e = end(levels); i != e; ++i) { + if (i->amount < chosen + || (!setting + && i->amount == chosen + && i->date >= value.my.date)) { + top.push_back({ show->session().user(), chosen }); + for (auto j = i; j != e; ++j) { + if (!pushTop(j)) { + break; + } + } + return finishWith(i->position); + } + pushTop(i); + } + top.push_back({ show->session().user(), chosen }); + return finishWith((levels.empty() ? 0 : levels.back().position) + 1); + }); + auto myLabelText = rpl::combine( + state->my.value(), + state->started.value() + ) | rpl::map([](My my, bool started) { + if (!started) { + return tr::lng_auction_bid_your_title(); + } + switch (my.type) { + case BidType::Setting: return tr::lng_auction_bid_your_title(); + case BidType::Winning: return tr::lng_auction_bid_your_winning(); + case BidType::Loosing: return tr::lng_auction_bid_your_outbid(); + } + Unexpected("Type in BidType."); + }) | rpl::flatten_latest(); + const auto myLabel = AddSubsectionTitle( + box->verticalLayout(), + std::move(myLabelText)); + state->my.value() | rpl::start_with_next([=](My my) { + myLabel->setTextColorOverride( + BidColorOverride(my.position, state->winners)); + }, myLabel->lifetime()); + + auto bid = rpl::combine( + state->my.value(), + rpl::duplicate(chosen) + ) | rpl::map([=, user = show->session().user()](My my, int stars) { + const auto position = my.position; + const auto winners = state->winners; + const auto place = QString::number(position); + return BidRowData{ user, stars, position, winners, place, my.type }; + }); + box->addRow(MakeBidRow(box, show, std::move(bid))); + + AddSubsectionTitle( + box->verticalLayout(), + tr::lng_auction_bid_winners_title(), + { 0, st::paidReactTitleSkip / 2, 0, 0 }); + for (auto i = 0; i != kShowTopPlaces; ++i) { + auto icon = QString::fromUtf8("\xf0\x9f\xa5\x87"); + icon.back().unicode() += i; + + auto bid = state->top.value( + ) | rpl::map([=](const std::vector &top) { + auto result = (i < top.size()) ? top[i] : BidRowData(); + result.place = icon; + return result; + }); + box->addRow(MakeBidRow(box, show, std::move(bid))); + } +} + +void EditCustomBid( + not_null box, + std::shared_ptr show, + Fn save, + rpl::producer minBid, + int current) { + box->setTitle(tr::lng_auction_bid_custom_title()); + + const auto container = box->verticalLayout(); + + box->addTopButton(st::boxTitleClose, [=] { box->closeBox(); }); + + const auto starsField = HistoryView::AddStarsInputField(container, { + .value = current, + }); + + const auto min = box->lifetime().make_state>( + std::move(minBid)); + + box->setFocusCallback([=] { + starsField->setFocusFast(); + }); + + const auto submit = [=] { + const auto value = starsField->getLastText().toLongLong(); + if (value <= min->current() || value > 1'000'000'000) { + starsField->showError(); + return; + } + save(value); + box->closeBox(); + }; + QObject::connect(starsField, &Ui::NumberInput::submitted, submit); + + box->addButton(tr::lng_settings_save(), submit); + box->addButton(tr::lng_cancel(), [=] { + box->closeBox(); + }); +} + +void AuctionBidBox(not_null box, AuctionBidBoxArgs &&args) { + using namespace Info::PeerGifts; + struct State { + State(rpl::producer value) + : value(std::move(value)) { + } + + rpl::variable value; + rpl::variable sliderValues; + rpl::variable chosen; + rpl::variable subtext; + rpl::variable started; + bool placing = false; + }; + const auto state = box->lifetime().make_state( + std::move(args.state)); + state->started = state->value.value( + ) | rpl::map([=](const Data::GiftAuctionState &value) { + return value.startDate; + }) | rpl::distinct_until_changed( + ) | rpl::map([=](TimeId startTime) { + return SecondsLeftTillValue( + startTime + ) | rpl::map([=](int seconds) { + return !seconds; + }); + }) | rpl::flatten_latest(); + state->sliderValues = state->value.value( + ) | rpl::map([=](const Data::GiftAuctionState &value) { + const auto mine = int(value.my.bid); + const auto min = std::max(1, int(value.my.minBidAmount + ? value.my.minBidAmount + : value.minBidAmount)); + const auto last = value.bidLevels.empty() + ? 0 + : value.bidLevels.front().amount; + auto max = std::max({ + min + 1, + kMaxShownBid, + int(base::SafeRound(mine * 1.2)), + }); + if (max < last * 1.05) { + max = int(base::SafeRound(last * 1.2)); + } + return BidSliderValues{ + .min = min, + .explicitlyAllowed = mine, + .max = max, + }; + }); + + const auto show = args.show; + const auto giftId = state->value.current().gift->id; + const auto &sliderValues = state->sliderValues.current(); + state->chosen = sliderValues.explicitlyAllowed + ? sliderValues.explicitlyAllowed + : sliderValues.min; + + state->subtext = rpl::combine( + state->value.value(), + state->chosen.value() + ) | rpl::map([=]( + const Data::GiftAuctionState &value, + int chosen) { + if (value.my.bid == chosen) { + return tr::lng_auction_bid_your(tr::now); + } else if (chosen == state->sliderValues.current().max) { + return tr::lng_auction_bid_custom(tr::now); + } else if (value.my.bid && chosen > value.my.bid) { + const auto delta = chosen - value.my.bid; + return '+' + Lang::FormatCountDecimal(delta); + } + return QString(); + }); + + args.peer->owner().giftAuctionGots( + ) | rpl::start_with_next([=](const Data::GiftAuctionGot &update) { + if (update.giftId == giftId) { + box->closeBox(); + + if (const auto window = show->resolveWindow()) { + window->showPeerHistory( + update.to, + Window::SectionShow::Way::ClearStack, + ShowAtTheEndMsgId); + } + } + }, box->lifetime()); + + const auto details = args.details + ? *args.details + : std::optional(); + const auto colorings = show->session().appConfig().groupCallColorings(); + + box->setWidth(st::boxWideWidth); + box->setStyle(st::paidReactBox); + box->setNoContentMargin(true); + + const auto content = box->verticalLayout(); + AddSkip(content, st::boxTitleClose.height + st::paidReactBubbleTop); + + const auto activeFgOverride = [=](int count) { + const auto coloring = Calls::Group::Ui::StarsColoringForCount( + colorings, + count); + return ColorFromSerialized(coloring.bgLight); + }; + const auto sliderWrap = content->add( + object_ptr(content)); + state->sliderValues.value( + ) | rpl::start_with_next([=](const BidSliderValues &values) { + const auto initial = !sliderWrap->count(); + if (!initial) { + while (sliderWrap->count()) { + delete sliderWrap->widgetAt(0); + } + while (!sliderWrap->children().isEmpty()) { + delete sliderWrap->children().front(); + } + } + + const auto setCustom = [=] { + auto min = state->value.value( + ) | rpl::map([=](const Data::GiftAuctionState &state) { + return std::max(1, int(state.my.minBidAmount + ? state.my.minBidAmount + : state.minBidAmount)); + }); + show->show(Box(EditCustomBid, show, crl::guard(box, [=](int v) { + state->chosen = v; + }), std::move(min), state->chosen.current())); + }; + + const auto bubble = AddStarSelectBubble( + sliderWrap, + initial ? BoxShowFinishes(box) : nullptr, + state->chosen.value(), + values.max, + activeFgOverride); + bubble->setAttribute(Qt::WA_TransparentForMouseEvents, false); + bubble->setClickedCallback(setCustom); + state->subtext.value() | rpl::start_with_next([=](QString &&text) { + bubble->setSubtext(std::move(text)); + }, bubble->lifetime()); + + PaidReactionSlider( + sliderWrap, + st::paidReactSlider, + values.min, + values.explicitlyAllowed, + state->chosen.value(), + values.max, + [=](int count) { state->chosen = count; }, + activeFgOverride); + + sliderWrap->resizeToWidth(st::boxWideWidth); + + const auto custom = CreateChild(sliderWrap); + state->chosen.changes() | rpl::start_with_next([=] { + custom->update(); + }, custom->lifetime()); + custom->show(); + custom->setClickedCallback(setCustom); + custom->resize(st::paidReactSlider.width, st::paidReactSlider.width); + custom->paintOn([=](QPainter &p) { + const auto rem = st::paidReactSlider.borderWidth * 2; + const auto inner = custom->width() - 2 * rem; + const auto sub = (inner - 1) / 2; + const auto stroke = inner - (2 * sub); + const auto color = activeFgOverride(state->chosen.current()); + p.fillRect(rem + sub, rem, stroke, sub, color); + p.fillRect(rem, rem + sub, inner, stroke, color); + p.fillRect(rem + sub, rem + inner - sub, stroke, sub, color); + }); + sliderWrap->sizeValue() | rpl::start_with_next([=](QSize size) { + custom->move( + size.width() - st::boxRowPadding.right() - custom->width(), + size.height() - custom->height()); + }, custom->lifetime()); + }, sliderWrap->lifetime()); + + box->addTopButton( + st::boxTitleClose, + [=] { box->closeBox(); }); + if (const auto now = state->value.current(); !now.finished()) { + box->addTopButton( + st::boxTitleMenu, + MakeAuctionMenuCallback(box, show, now)); + } + + const auto skip = st::paidReactTitleSkip; + box->addRow( + object_ptr( + box, + rpl::conditional( + state->started.value(), + tr::lng_auction_bid_title(), + tr::lng_auction_bid_title_early()), + st::boostCenteredTitle), + st::boxRowPadding + QMargins(0, skip / 2, 0, 0), + style::al_top); + + auto subtitle = tr::lng_auction_bid_subtitle( + lt_count, + state->value.value( + ) | rpl::map([=](const Data::GiftAuctionState &state) { + return state.gift->auctionGiftsPerRound * 1.; + })); + box->addRow( + object_ptr( + box, + std::move(subtitle), + st::auctionCenteredSubtitle), + style::al_top); + + const auto setMinimal = [=] { + const auto &now = state->value.current(); + state->chosen = int(now.my.minBidAmount + ? now.my.minBidAmount + : now.minBidAmount); + }; + box->addRow( + MakeAuctionInfoBlocks( + box, + &show->session(), + state->value.value(), + setMinimal), + st::boxRowPadding + QMargins(0, skip / 2, 0, skip)); + + AddBidPlaces(box, show, state->value.value(), state->chosen.value()); + + AddSkip(content); + AddSkip(content); + + const auto peer = args.peer; + const auto button = box->addButton(rpl::single(QString()), [=] { + const auto ¤t = state->value.current(); + const auto amount = state->chosen.current(); + if (amount <= current.my.bid) { + box->closeBox(); + return; + } else if (state->placing) { + return; + } + state->placing = true; + const auto was = (current.my.bid > 0); + const auto perRound = current.gift->auctionGiftsPerRound; + const auto done = [=](Payments::CheckoutResult result) { + state->placing = false; + if (result == Payments::CheckoutResult::Paid) { + show->showToast({ + .title = (was + ? tr::lng_auction_bid_increased_title + : tr::lng_auction_bid_placed_title)( + tr::now), + .text = tr::lng_auction_bid_done_text( + tr::now, + lt_count, + perRound, + tr::rich), + .st = &st::auctionBidToast, + .attach = RectPart::Top, + .duration = kBidPlacedToastDuration, + }); + } + }; + auto owned = details + ? std::make_unique(*details) + : nullptr; + PlaceAuctionBid(show, peer, amount, current, std::move(owned), done); + }); + + button->setText(rpl::combine( + state->value.value(), + state->chosen.value() + ) | rpl::map([=](const Data::GiftAuctionState &state, int count) { + return !state.my.bid + ? tr::lng_auction_bid_place( + lt_stars, + rpl::single(CreditsEmojiSmall().append( + Lang::FormatCountDecimal(count))), + tr::marked) + : (count <= state.my.bid) + ? tr::lng_box_ok(tr::marked) + : tr::lng_auction_bid_increase( + lt_stars, + rpl::single(CreditsEmojiSmall().append( + Lang::FormatCountDecimal(count - state.my.bid))), + tr::marked); + }) | rpl::flatten_latest()); + + show->session().credits().load(true); + AddStarSelectBalance( + box, + &show->session(), + show->session().credits().balanceValue()); +} + +[[nodiscard]] object_ptr MakeAveragePriceValue( + not_null table, + std::shared_ptr tooltip, + const QString &name, + int64 amount) { + auto helper = Text::CustomEmojiHelper(); + const auto price = helper.paletteDependent(Earn::IconCreditsEmoji( + )).append(' ').append( + Lang::FormatCreditsAmountDecimal(CreditsAmount{ amount })); + return MakeTableValueWithTooltip( + table, + std::move(tooltip), + price, + tr::lng_auction_average_tooltip( + tr::now, + lt_amount, + tr::bold( + Text::IconEmoji(&st::starIconEmojiInline).append( + Lang::FormatCountDecimal(amount))), + lt_gift, + tr::bold(name), + tr::marked), + helper.context()); +} + +[[nodiscard]] std::vector RandomIndicesSubset(int total, int subset) { + const auto take = std::min(total, subset); + if (!take) { + return {}; + } + auto result = std::vector(); + auto taken = base::flat_set(); + result.reserve(take); + taken.reserve(take); + for (auto i = 0; i < take; ++i) { + auto index = base::RandomIndex(total - i); + for (const auto already : taken) { + if (index >= already) { + ++index; + } else { + break; + } + } + taken.emplace(index); + result.push_back(index); + } + return result; +} + +[[nodiscard]] object_ptr AuctionInfoTable( + not_null parent, + not_null container, + rpl::producer value) { + auto result = object_ptr(parent.get(), st::defaultTable); + const auto raw = result.data(); + + struct State { + rpl::variable value; + rpl::variable finished; + }; + const auto state = raw->lifetime().make_state(); + state->value = std::move(value); + + const auto &now = state->value.current(); + const auto preview = (now.startDate > base::unixtime::now()); + const auto name = now.gift->resellTitle; + state->finished = now.finished() + ? (rpl::single(true) | rpl::type_erased()) + : (MinutesLeftTillValue(now.endDate) | rpl::map(!rpl::mappers::_1)); + + const auto date = [&](TimeId time) { + return rpl::single( + tr::marked(langDateTime(base::unixtime::parse(time)))); + }; + AddTableRow( + raw, + (preview + ? tr::lng_auction_starts_label() + : rpl::conditional( + state->finished.value(), + tr::lng_gift_link_label_first_sale(), + tr::lng_auction_start_label())), + date(now.startDate)); + AddTableRow( + raw, + rpl::conditional( + state->finished.value(), + tr::lng_gift_link_label_last_sale(), + tr::lng_auction_end_label()), + date(now.endDate)); + if (preview) { + AddTableRow( + raw, + tr::lng_gift_unique_availability_label(), + rpl::single(tr::marked( + Lang::FormatCountDecimal(now.gift->limitedCount)))); + AddTableRow( + raw, + tr::lng_auction_rounds_label(), + rpl::single(tr::marked( + Lang::FormatCountDecimal(now.totalRounds)))); + const auto formatDuration = [&](TimeId value, bool exact) { + return (!(value % 3600)) + ? (exact ? tr::lng_hours : tr::lng_auction_rounds_hours)( + tr::now, + lt_count, + value / 3600) + : (!(value % 60)) + ? (exact ? tr::lng_minutes : tr::lng_auction_rounds_minutes)( + tr::now, + lt_count, + value / 60) + : (exact ? tr::lng_seconds : tr::lng_auction_rounds_seconds)( + tr::now, + lt_count, + value); + }; + for (auto i = 0, n = int(now.roundParameters.size()); i != n; ++i) { + const auto &that = now.roundParameters[i]; + const auto next = (i + 1 < n) + ? now.roundParameters[i + 1] + : Data::GiftAuctionRound{ now.totalRounds + 1 }; + const auto exact = (next.number == that.number + 1); + const auto extended = that.extendTop && that.extendDuration; + const auto duration = formatDuration(that.duration, exact); + const auto value = extended + ? tr::lng_auction_rounds_extended( + tr::now, + lt_duration, + duration, + lt_increase, + formatDuration(that.extendDuration, true), + lt_n, + QString::number(that.extendTop)) + : duration; + AddTableRow( + raw, + (exact + ? tr::lng_auction_rounds_exact( + lt_n, + rpl::single(QString::number(that.number))) + : tr::lng_auction_rounds_range( + lt_n, + rpl::single(QString::number(that.number)), + lt_last, + rpl::single(QString::number(next.number - 1)))), + object_ptr( + raw, + value, + st::auctionInfoValueMultiline)); + } + } else { + auto roundText = state->value.value( + ) | rpl::map([](const Data::GiftAuctionState &state) { + const auto wrapped = [](int count) { + return rpl::single(tr::marked(Lang::FormatCountDecimal(count))); + }; + return tr::lng_auction_round_value( + lt_n, + wrapped(state.currentRound), + lt_amount, + wrapped(state.totalRounds), + tr::marked); + }) | rpl::flatten_latest(); + const auto round = AddTableRow( + raw, + tr::lng_auction_round_label(), + std::move(roundText)); + + auto availabilityText = state->value.value( + ) | rpl::map([](const Data::GiftAuctionState &state) { + const auto wrapped = [](int count) { + return rpl::single(tr::marked(Lang::FormatCountDecimal(count))); + }; + return tr::lng_auction_availability_value( + lt_n, + wrapped(state.giftsLeft), + lt_amount, + wrapped(state.gift->limitedCount), + tr::marked); + }) | rpl::flatten_latest(); + AddTableRow( + raw, + tr::lng_auction_availability_label(), + std::move(availabilityText)); + + const auto tooltip = std::make_shared( + TableRowTooltipData{ .parent = container }); + state->value.value( + ) | rpl::map([](const Data::GiftAuctionState &state) { + return state.averagePrice; + }) | rpl::filter( + rpl::mappers::_1 != 0 + ) | rpl::take( + 1 + ) | rpl::start_with_next([=](int64 price) { + delete round; + + raw->insertRow( + 2, + object_ptr( + raw, + tr::lng_auction_average_label(), + raw->st().defaultLabel), + MakeAveragePriceValue(raw, tooltip, name, price), + st::giveawayGiftCodeLabelMargin, + st::giveawayGiftCodeValueMargin); + raw->resizeToWidth(raw->widthNoMargins()); + }, raw->lifetime()); + } + return result; +} + +void AuctionGotGiftsBox( + not_null box, + std::shared_ptr show, + const Data::StarGift &gift, + std::vector list) { + Expects(!list.empty()); + + const auto count = int(list.size()); + box->setTitle( + tr::lng_auction_bought_title(lt_count, rpl::single(count * 1.))); + box->setWidth(st::boxWideWidth); + box->setMaxHeight(st::boxWideWidth * 2); + box->addButton(tr::lng_box_ok(), [=] { box->closeBox(); }); + box->addTopButton(st::boxTitleClose, [=] { box->closeBox(); }); + + auto helper = Text::CustomEmojiHelper(Core::TextContext({ + .session = &show->session(), + })); + const auto emoji = Data::SingleCustomEmoji(gift.document); + const auto container = box->verticalLayout(); + ranges::sort(list, ranges::less(), &Data::GiftAcquired::round); + for (const auto &entry : list) { + const auto table = container->add( + object_ptr( + container, + st::giveawayGiftCodeTable), + st::giveawayGiftCodeTableMargin); + const auto addFullWidth = [&](rpl::producer text) { + table->addRow( + object_ptr( + table, + std::move(text), + st::giveawayGiftMessage, + st::defaultPopupMenu, + helper.context()), + nullptr, + st::giveawayGiftCodeLabelMargin, + st::giveawayGiftCodeValueMargin); + }; + + // Title "Gift #number in round #n" + addFullWidth(tr::lng_auction_bought_in_round( + lt_name, + rpl::single(tr::marked( + emoji + ).append(' ').append( + Data::UniqueGiftName(gift.resellTitle, entry.number) + )), + lt_n, + rpl::single(tr::marked(QString::number(entry.round))), + tr::bold)); + + // Recipient + AddTableRow( + table, + tr::lng_credits_box_history_entry_peer(), + show, + entry.to->id); + + // Date + AddTableRow( + table, + tr::lng_auction_bought_date(), + rpl::single(tr::marked( + langDateTime(base::unixtime::parse(entry.date))))); + + // Accepted Bid + auto accepted = helper.paletteDependent( + Ui::Earn::IconCreditsEmoji() + ).append( + Lang::FormatCountDecimal(entry.bidAmount) + ).append(' ').append( + helper.paletteDependent( + Text::CustomEmojiTextBadge( + '#' + QString::number(entry.position), + st::defaultTableSmallButton))); + AddTableRow( + table, + tr::lng_auction_bought_bid(), + rpl::single(accepted), + helper.context()); + + // Message + if (!entry.message.empty()) { + addFullWidth(rpl::single(entry.message)); + } + } +} + +[[nodiscard]] rpl::producer MakePreviewAuctionStream( + const Data::StarGift &info, + rpl::producer attributes) { + Expects(attributes); + + const auto cover = [](Data::UniqueGift gift) { + return UniqueGiftCover{ std::move(gift) }; + }; + auto initial = Data::UniqueGift{ + .title = info.resellTitle, + .model = Data::UniqueGiftModel{ + .document = info.document, + }, + .pattern = Data::UniqueGiftPattern{ + .document = info.document, + }, + .backdrop = (info.background + ? info.background->backdrop() + : Data::UniqueGiftBackdrop()), + }; + return rpl::single(cover(initial)) | rpl::then(std::move( + attributes + ) | rpl::map([=](const Data::UniqueGiftAttributes &values) + -> rpl::producer { + if (values.backdrops.empty() + || values.models.empty() + || values.patterns.empty()) { + return rpl::never(); + } + return [=](auto consumer) { + auto lifetime = rpl::lifetime(); + + struct State { + Data::UniqueGiftAttributes data; + std::vector modelIndices; + std::vector patternIndices; + std::vector backdropIndices; + }; + const auto state = lifetime.make_state(State{ + .data = values, + }); + + const auto put = [=] { + const auto index = []( + std::vector &indices, + const auto &v) { + const auto fill = [&] { + if (!indices.empty()) { + return; + } + indices = ranges::views::ints( + 0 + ) | ranges::views::take( + v.size() + ) | ranges::to_vector; + ranges::shuffle(indices); + }; + fill(); + const auto result = indices.back(); + indices.pop_back(); + fill(); + if (indices.back() == result) { + std::swap(indices.front(), indices.back()); + } + return result; + }; + auto &models = state->data.models; + auto &patterns = state->data.patterns; + auto &backdrops = state->data.backdrops; + consumer.put_next(cover({ + .title = info.resellTitle, + .model = models[index(state->modelIndices, models)], + .pattern = patterns[index(state->patternIndices, patterns)], + .backdrop = backdrops[index(state->backdropIndices, backdrops)], + })); + }; + + put(); + base::timer_each( + kSwitchPreviewCoverInterval / 3 + ) | rpl::start_with_next(put, lifetime); + + return lifetime; + }; + }) | rpl::flatten_latest()); +} + +void AuctionInfoBox( + not_null box, + not_null window, + not_null peer, + rpl::producer value) { + using namespace Info::PeerGifts; + + struct State { + explicit State(not_null session) + : delegate(session, GiftButtonMode::Minimal) { + } + + Delegate delegate; + rpl::variable value; + rpl::variable minutesTillEnd; + rpl::variable secondsTillStart; + rpl::variable attributes; + + std::vector acquired; + bool acquiredRequested = false; + + base::unique_qptr menu; + + rpl::lifetime previewLifetime; + bool previewRequested = false; + }; + const auto show = window->uiShow(); + const auto state = box->lifetime().make_state(&show->session()); + state->value = std::move(value); + const auto &now = state->value.current(); + const auto auctions = &show->session().giftAuctions(); + const auto giftId = now.gift->id; + if (auto attributes = auctions->attributes(giftId)) { + state->attributes = std::move(*attributes); + } else { + auctions->requestAttributes(giftId, crl::guard(box, [=] { + state->attributes.force_assign(*auctions->attributes(giftId)); + })); + } + state->minutesTillEnd = MinutesLeftTillValue(now.endDate); + state->secondsTillStart = SecondsLeftTillValue(now.startDate); + const auto started = !state->secondsTillStart.current(); + + box->setStyle(st::giftBox); + box->setNoContentMargin(true); + + const auto container = box->verticalLayout(); + auto gift = MakePreviewAuctionStream( + *now.gift, + state->attributes.value()); + AddUniqueGiftCover(container, std::move(gift), { + .pretitle = started ? nullptr : tr::lng_auction_preview_name(), + .subtitle = tr::lng_auction_preview_learn_gifts( + lt_arrow, + rpl::single(Text::IconEmoji(&st::textMoreIconEmoji)), + tr::link), + .subtitleClick = [=] { + ShowPremiumPreviewBox(window, PremiumFeature::Gifts); + }, + .subtitleLinkColored = true, + }); + AddSkip(container, st::defaultVerticalListSkip * 2); + + AddUniqueCloseButton( + box, + {}, + now.finished() ? nullptr : MakeAuctionFillMenuCallback(show, now)); + + box->addRow( + AuctionInfoTable(box, box->verticalLayout(), state->value.value()), + st::boxRowPadding + st::auctionInfoTableMargin); + + if (const auto got = now.my.gotCount) { + box->addRow( + object_ptr( + box, + tr::lng_auction_bought( + lt_count_decimal, + rpl::single(1. * got), + lt_emoji, + rpl::single(Data::SingleCustomEmoji( + state->value.current().gift->document)), + lt_arrow, + rpl::single(Text::IconEmoji(&st::textMoreIconEmoji)), + tr::link), + st::uniqueGiftValueAvailableLink, + st::defaultPopupMenu, + Core::TextContext({ .session = &show->session() })), + st::boxRowPadding + st::uniqueGiftValueAvailableMargin, + style::al_top + )->setClickHandlerFilter([=](const auto &...) { + const auto &value = state->value.current(); + const auto &gift = *value.gift; + if (!value.my.gotCount) { + return false; + } else if (state->acquired.size() == value.my.gotCount) { + show->show(Box( + AuctionGotGiftsBox, + show, + gift, + state->acquired)); + } else if (!state->acquiredRequested) { + state->acquiredRequested = true; + auctions->requestAcquired( + value.gift->id, + crl::guard(box, [=]( + std::vector result) { + state->acquiredRequested = false; + state->acquired = std::move(result); + if (!state->acquired.empty()) { + show->show(Box( + AuctionGotGiftsBox, + show, + gift, + state->acquired)); + } + })); + } + return false; + }); + } else if (const auto variants = now.gift->upgradeVariants) { + using namespace Data; + state->attributes.value( + ) | rpl::filter([](const UniqueGiftAttributes &list) { + return !list.models.empty(); + }) | rpl::take( + 1 + ) | rpl::start_with_next([=](const UniqueGiftAttributes &list) { + auto emoji = tr::marked(); + const auto indices = RandomIndicesSubset(list.models.size(), 3); + for (const auto index : indices) { + emoji.append(Data::SingleCustomEmoji( + list.models[index].document)); + } + box->addRow( + object_ptr( + box, + tr::lng_auction_preview_variants( + lt_count_decimal, + rpl::single(1. * variants), + lt_emoji, + rpl::single(emoji), + lt_arrow, + rpl::single(Text::IconEmoji(&st::textMoreIconEmoji)), + tr::link), + st::uniqueGiftValueAvailableLink, + st::defaultPopupMenu, + Core::TextContext({ .session = &show->session() })), + st::boxRowPadding + st::uniqueGiftValueAvailableMargin, + style::al_top + )->setClickHandlerFilter([=](const auto &...) { + show->show(Box(StarGiftPreviewBox, window, *now.gift, list)); + return false; + }); + }, box->lifetime()); + } + const auto button = box->addButton(rpl::single(QString()), [=] { + if (state->value.current().finished() + || !state->minutesTillEnd.current()) { + box->closeBox(); + return; + } + const auto sendBox = show->show(Box( + SendGiftBox, + window, + peer, + nullptr, + GiftTypeStars{ .info = *state->value.current().gift }, + state->value.value())); + sendBox->boxClosing( + ) | rpl::start_with_next([=] { + box->closeBox(); + }, box->lifetime()); + }); + + SetAuctionButtonCountdownText( + button, + AuctionButtonCountdownType::Join, + state->value.value()); +} + +base::weak_qptr ChooseAndShowAuctionBox( + not_null window, + not_null peer, + std::shared_ptr> state, + Fn boxClosed) { + const auto local = &peer->session().local(); + const auto ¤t = state->current(); + const auto now = base::unixtime::now(); + const auto started = (current.startDate <= now); + const auto finished = current.finished() || (current.endDate <= now); + const auto showBidBox = current.my.bid + && !finished + && (!current.my.to || current.my.to == peer); + const auto showChangeRecipient = !showBidBox + && current.my.bid + && !finished; + const auto showInfoBox = !showBidBox + && !showChangeRecipient + && (!started + || finished + || local->readPref(kAuctionAboutShownPref)); + auto box = base::weak_qptr(); + if (showBidBox) { + box = window->show(MakeAuctionBidBox({ + .peer = peer, + .show = window->uiShow(), + .state = state->value(), + })); + } else if (showChangeRecipient) { + const auto change = [=](Fn close) { + const auto sendBox = window->show(Box( + SendGiftBox, + window, + peer, + nullptr, + Info::PeerGifts::GiftTypeStars{ + .info = *current.gift, + }, + state->value())); + sendBox->boxClosing( + ) | rpl::start_with_next(close, sendBox->lifetime()); + }; + const auto from = current.my.to; + const auto text = (from->isSelf() + ? tr::lng_auction_change_already_me(tr::now, tr::rich) + : tr::lng_auction_change_already( + tr::now, + lt_name, + tr::bold(from->name()), + tr::rich)).append(' ').append(peer->isSelf() + ? tr::lng_auction_change_to_me(tr::now, tr::rich) + : tr::lng_auction_change_to( + tr::now, + lt_name, + tr::bold(peer->name()), + tr::rich)); + box = window->show(Box([=](not_null box) { + box->addRow( + CreateUserpicsTransfer( + box, + rpl::single(std::vector{ not_null(from) }), + peer, + UserpicsTransferType::AuctionRecipient), + st::boxRowPadding + st::auctionChangeRecipientPadding + )->setAttribute(Qt::WA_TransparentForMouseEvents); + + ConfirmBox(box, { + .text = text, + .confirmed = change, + .confirmText = tr::lng_auction_change_button(), + .title = tr::lng_auction_change_title(), + }); + })); + } else if (showInfoBox) { + box = window->show(Box( + AuctionInfoBox, + window, + peer, + state->value())); + } else { + local->writePref(kAuctionAboutShownPref, true); + const auto understood = [=](Fn close) { + ChooseAndShowAuctionBox(window, peer, state, close); + }; + box = window->show(Box( + AuctionAboutBox, + current.totalRounds, + current.gift->auctionGiftsPerRound, + understood)); + } + if (const auto strong = box.get()) { + strong->boxClosing( + ) | rpl::start_with_next(boxClosed, strong->lifetime()); + } else { + boxClosed(); + } + return box; +} + +} // namespace + +rpl::lifetime ShowStarGiftAuction( + not_null controller, + PeerData *peer, + QString slug, + Fn finishRequesting, + Fn boxClosed) { + const auto weak = base::make_weak(controller); + const auto session = &controller->session(); + struct State { + rpl::variable value; + base::weak_qptr box; + }; + const auto state = std::make_shared(); + auto result = session->giftAuctions().state( + slug + ) | rpl::start_with_next([=](Data::GiftAuctionState &&value) { + if (const auto onstack = finishRequesting) { + onstack(); + } + const auto initial = !state->value.current().gift.has_value(); + const auto already = value.my.to; + state->value = std::move(value); + if (initial) { + if (const auto strong = weak.get()) { + const auto to = peer + ? peer + : already + ? already + : strong->session().user(); + state->box = ChooseAndShowAuctionBox( + strong, + to, + std::shared_ptr>( + state, + &state->value), + boxClosed); + } else { + boxClosed(); + } + } + }); + result.add([=] { + if (const auto strong = state->box.get()) { + strong->closeBox(); + } + }); + return result; +} + +object_ptr MakeAuctionBidBox(AuctionBidBoxArgs &&args) { + return Box(AuctionBidBox, std::move(args)); +} + +void SetAuctionButtonCountdownText( + not_null button, + AuctionButtonCountdownType type, + rpl::producer value) { + struct State { + rpl::variable value; + rpl::variable minutesTillEnd; + rpl::variable secondsTillStart; + }; + const auto state = button->lifetime().make_state(); + state->value = std::move(value); + + const auto &now = state->value.current(); + const auto preview = (now.startDate > base::unixtime::now()); + if (preview) { + state->secondsTillStart = SecondsLeftTillValue(now.startDate); + } else { + state->minutesTillEnd = MinutesLeftTillValue(now.endDate); + } + + auto buttonTitle = rpl::combine( + state->value.value(), + (preview + ? state->secondsTillStart.value() + : state->minutesTillEnd.value()) + ) | rpl::map([=](const Data::GiftAuctionState &state, int leftTill) { + return (state.finished() || (!preview && leftTill <= 0)) + ? tr::lng_box_ok(tr::marked) + : preview + ? tr::lng_auction_join_early_bid(tr::marked) + : (type != AuctionButtonCountdownType::Place) + ? tr::lng_auction_join_button(tr::marked) + : tr::lng_auction_join_bid(tr::marked); + }) | rpl::flatten_latest(); + + auto buttonSubtitle = rpl::combine( + state->value.value(), + (preview + ? state->secondsTillStart.value() + : state->minutesTillEnd.value()) + ) | rpl::map([=]( + const Data::GiftAuctionState &state, + int leftTill + ) -> rpl::producer { + if (state.finished() || leftTill <= 0) { + return rpl::single(TextWithEntities()); + } else if (preview) { + const auto hours = (leftTill / 3600); + const auto minutes = (leftTill % 3600) / 60; + const auto seconds = (leftTill % 60); + const auto time = hours + ? u"%1:%2:%3"_q + .arg(hours).arg(minutes, 2, 10, QChar('0')) + .arg(seconds, 2, 10, QChar('0')) + : u"%1:%2"_q.arg(minutes).arg(seconds, 2, 10, QChar('0')); + return tr::lng_auction_join_starts_in( + lt_time, + rpl::single(tr::marked(time)), + tr::marked); + } + const auto hours = (leftTill / 60); + const auto minutes = leftTill % 60; + + auto value = [](int count) { + return rpl::single(tr::marked(QString::number(count))); + }; + return tr::lng_auction_join_time_left( + lt_time, + (hours + ? tr::lng_auction_join_time_medium( + lt_hours, + value(hours), + lt_minutes, + value(minutes), + tr::marked) + : tr::lng_auction_join_time_small( + lt_minutes, + value(minutes), + tr::marked)), + tr::marked); + }) | rpl::flatten_latest(); + + SetButtonTwoLabels( + button, + std::move(buttonTitle), + std::move(buttonSubtitle), + st::resaleButtonTitle, + st::resaleButtonSubtitle); +} + +void AuctionAboutBox( + not_null box, + int rounds, + int giftsPerRound, + Fn close)> understood) { + box->setStyle(st::confcallJoinBox); + box->setWidth(st::boxWideWidth); + box->setNoContentMargin(true); + box->addTopButton(st::boxTitleClose, [=] { + box->closeBox(); + }); + box->addRow( + Calls::Group::MakeRoundActiveLogo( + box, + st::auctionAboutLogo, + st::auctionAboutLogoPadding), + st::boxRowPadding + st::confcallLinkHeaderIconPadding); + + box->addRow( + object_ptr( + box, + tr::lng_auction_about_title(), + st::boxTitle), + st::boxRowPadding, + style::al_top); + box->addRow( + object_ptr( + box, + tr::lng_auction_about_subtitle(tr::rich), + st::confcallLinkCenteredText), + st::boxRowPadding + st::auctionAboutTextPadding, + style::al_top + )->setTryMakeSimilarLines(true); + + const auto features = std::vector{ + { + st::menuIconAuctionDrop, + tr::lng_auction_about_top_title( + tr::now, + lt_count, + giftsPerRound), + tr::lng_auction_about_top_about( + tr::now, + lt_count, + giftsPerRound, + lt_rounds, + tr::lng_auction_about_top_rounds( + tr::now, + lt_count, + rounds, + tr::rich), + lt_bidders, + tr::lng_auction_about_top_bidders( + tr::now, + lt_count, + giftsPerRound, + tr::rich), + tr::rich), + }, + { + st::menuIconStarsCarryover, + tr::lng_auction_about_bid_title(tr::now), + tr::lng_auction_about_bid_about( + tr::now, + lt_count, + giftsPerRound, + tr::rich), + }, + { + st::menuIconStarsRefund, + tr::lng_auction_about_missed_title(tr::now), + tr::lng_auction_about_missed_about(tr::now, tr::rich), + }, + }; + for (const auto &feature : features) { + box->addRow(MakeFeatureListEntry(box, feature)); + } + + const auto close = Fn([weak = base::make_weak(box)] { + if (const auto strong = weak.get()) { + strong->closeBox(); + } + }); + box->addButton( + rpl::single(QString()), + understood ? [=] { understood(close); } : close + )->setText(rpl::single(Text::IconEmoji( + &st::infoStarsUnderstood + ).append(' ').append(tr::lng_auction_about_understood(tr::now)))); +} + +TextWithEntities ActiveAuctionsTitle(const Data::ActiveAuctions &auctions) { + const auto &list = auctions.list; + if (list.size() == 1) { + const auto auction = list.front(); + return Data::SingleCustomEmoji( + auction->gift->document + ).append(' ').append(tr::lng_auction_bar_active(tr::now)); + } + auto result = tr::marked(); + for (const auto auction : list | ranges::views::take(3)) { + result.append(Data::SingleCustomEmoji(auction->gift->document)); + } + return result.append(' ').append( + tr::lng_auction_bar_active_many(tr::now, lt_count, list.size())); +} + +ManyAuctionsState ActiveAuctionsState(const Data::ActiveAuctions &auctions) { + const auto &list = auctions.list; + const auto winning = [](not_null auction) { + const auto position = MyAuctionPosition(*auction); + return (position <= auction->gift->auctionGiftsPerRound) + ? position + : 0; + }; + if (list.size() == 1) { + const auto auction = list.front(); + const auto position = winning(auction); + auto text = position + ? tr::lng_auction_bar_winning( + tr::now, + lt_count, + position, + tr::marked) + : tr::lng_auction_bar_outbid(tr::now, tr::marked); + return { std::move(text), !position }; + } + auto outbid = 0; + for (const auto auction : list) { + if (!winning(auction)) { + ++outbid; + } + } + auto text = (outbid == list.size()) + ? tr::lng_auction_bar_outbid_all(tr::now, tr::marked) + : outbid + ? tr::lng_auction_bar_outbid_some( + tr::now, + lt_count, + outbid, + tr::marked) + : tr::lng_auction_bar_winning_all(tr::now, tr::marked); + return { std::move(text), outbid != 0 }; +} + +rpl::producer ActiveAuctionsButton( + const Data::ActiveAuctions &auctions) { + const auto &list = auctions.list; + const auto withIcon = [](const QString &text) { + using namespace Ui::Text; + return IconEmoji(&st::auctionBidEmoji).append(' ').append(text); + }; + if (list.size() == 1) { + const auto auction = auctions.list.front(); + const auto end = auction->nextRoundAt + ? auction->nextRoundAt + : auction->endDate; + return SecondsLeftTillValue(end) + | rpl::map(NiceCountdownText) + | rpl::map(withIcon); + } + return tr::lng_auction_bar_view() | rpl::map(withIcon); +} + +struct Single { + QString slug; + not_null document; + int round = 0; + int total = 0; + int bid = 0; + int position = 0; + int winning = 0; + TimeId ends = 0; +}; + +object_ptr MakeActiveAuctionRow( + not_null parent, + not_null window, + not_null document, + const QString &slug, + rpl::producer value) { + auto result = object_ptr(parent); + const auto raw = result.data(); + + raw->add(object_ptr(raw)); + + auto title = rpl::duplicate(value) | rpl::map([=](const Single &fields) { + return tr::lng_auction_bar_round( + tr::now, + lt_n, + QString::number(fields.round + 1), + lt_amount, + QString::number(fields.total)); + }); + raw->add( + object_ptr( + raw, + std::move(title), + st::auctionListTitle), + st::auctionListTitlePadding); + + const auto tag = Data::CustomEmojiSizeTag::Isolated; + const auto sticker = std::shared_ptr( + document->owner().customEmojiManager().create( + document, + [=] { raw->update(); }, + tag)); + + raw->paintRequest( + ) | rpl::start_with_next([=] { + auto q = QPainter(raw); + sticker->paint(q, { + .textColor = st::windowFg->c, + .now = crl::now(), + .position = QPoint(), + }); + }, raw->lifetime()); + + auto helper = Ui::Text::CustomEmojiHelper(); + const auto star = helper.paletteDependent(Ui::Earn::IconCreditsEmoji()); + auto text = rpl::duplicate(value) | rpl::map([=](const Single &fields) { + const auto stars = tr::marked(star).append(' ').append( + Lang::FormatCountDecimal(fields.bid)); + const auto outbid = (fields.position > fields.winning); + return outbid + ? tr::lng_auction_bar_bid_outbid( + tr::now, + lt_stars, + stars, + tr::rich) + : tr::lng_auction_bar_bid_ranked( + tr::now, + lt_stars, + stars, + lt_n, + tr::marked(QString::number(fields.position)), + tr::rich); + }); + const auto subtitle = raw->add( + object_ptr( + raw, + std::move(text), + st::auctionListText, + st::defaultPopupMenu, + helper.context()), + st::auctionListTextPadding); + rpl::duplicate(value) | rpl::start_with_next([=](const Single &fields) { + const auto outbid = (fields.position > fields.winning); + subtitle->setTextColorOverride(outbid + ? st::attentionButtonFg->c + : std::optional()); + }, subtitle->lifetime()); + + const auto button = raw->add( + object_ptr( + raw, + rpl::single(QString()), + st::auctionListRaise), + st::auctionListRaisePadding); + button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); + + auto secondsLeft = rpl::duplicate( + value + ) | rpl::map([=](const Single &fields) { + return SecondsLeftTillValue(fields.ends); + }) | rpl::flatten_latest(); + button->setText(rpl::combine( + std::move(secondsLeft), + tr::lng_auction_bar_raise_bid() + ) | rpl::map([=](int seconds, const QString &text) { + return Ui::Text::IconEmoji( + &st::auctionBidEmoji + ).append(' ').append(text).append(' ').append( + Ui::Text::Colorized(NiceCountdownText(seconds))); + })); + button->setClickedCallback([=] { + window->showStarGiftAuction(slug); + }); + button->setFullRadius(true); + raw->widthValue() | rpl::start_with_next([=](int width) { + button->setFullWidth(width); + }, button->lifetime()); + + return result; +} + +Fn ActiveAuctionsCallback( + not_null window, + const Data::ActiveAuctions &auctions) { + const auto &list = auctions.list; + const auto count = int(list.size()); + if (count == 1) { + const auto slug = list.front()->gift->auctionSlug; + return [=] { + window->showStarGiftAuction(slug); + }; + } + struct Auctions { + std::vector> list; + }; + const auto state = std::make_shared(); + const auto singleFrom = [](const Data::GiftAuctionState &state) { + return Single{ + .slug = state.gift->auctionSlug, + .document = state.gift->document, + .round = state.currentRound, + .total = state.totalRounds, + .bid = int(state.my.bid), + .position = MyAuctionPosition(state), + .winning = state.gift->auctionGiftsPerRound, + .ends = state.nextRoundAt ? state.nextRoundAt : state.endDate, + }; + }; + for (const auto auction : list) { + state->list.push_back(singleFrom(*auction)); + } + return [=] { + window->show(Box([=](not_null box) { + const auto rows = box->lifetime().make_state< + rpl::variable + >(count); + + box->setWidth(st::boxWideWidth); + box->setTitle(tr::lng_auction_bar_active_many( + lt_count, + rows->value() | tr::to_count())); + + const auto auctions = &window->session().giftAuctions(); + for (auto &entry : state->list) { + using Data::GiftAuctionState; + + const auto &now = entry.current(); + entry = auctions->state( + now.slug + ) | rpl::filter([=](const GiftAuctionState &state) { + return state.my.bid != 0; + }) | rpl::map(singleFrom); + + const auto skip = st::auctionListEntrySkip; + const auto row = box->addRow( + MakeActiveAuctionRow( + box, + window, + now.document, + now.slug, + entry.value()), + st::boxRowPadding + QMargins(0, skip, 0, skip)); + + auctions->state( + now.slug + ) | rpl::start_with_next([=](const GiftAuctionState &state) { + if (!state.my.bid) { + delete row; + if (const auto now = rows->current(); now > 1) { + *rows = (now - 1); + } else { + box->closeBox(); + } + } + }, row->lifetime()); + } + + box->addTopButton(st::boxTitleClose, [=] { box->closeBox(); }); + box->addButton(tr::lng_box_ok(), [=] { box->closeBox(); }); + })); + }; +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/boxes/star_gift_auction_box.h b/Telegram/SourceFiles/boxes/star_gift_auction_box.h new file mode 100644 index 0000000000..880abafb89 --- /dev/null +++ b/Telegram/SourceFiles/boxes/star_gift_auction_box.h @@ -0,0 +1,80 @@ +/* +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; +struct ActiveAuctions; +struct StarGift; +} // namespace Data + +namespace Info::PeerGifts { +struct GiftSendDetails; +} // namespace Info::PeerGifts + +namespace Window { +class SessionController; +} // namespace Window + +namespace Ui { + +class BoxContent; +class RoundButton; +class GenericBox; + +[[nodiscard]] rpl::lifetime ShowStarGiftAuction( + not_null controller, + PeerData *peer, + QString slug, + Fn finishRequesting, + Fn boxClosed); + +struct AuctionBidBoxArgs { + not_null peer; + std::shared_ptr show; + rpl::producer state; + std::unique_ptr details; +}; +[[nodiscard]] object_ptr MakeAuctionBidBox( + AuctionBidBoxArgs &&args); + +enum class AuctionButtonCountdownType { + Join, + Place, + Preview, +}; +void SetAuctionButtonCountdownText( + not_null button, + AuctionButtonCountdownType type, + rpl::producer value); + +void AuctionAboutBox( + not_null box, + int rounds, + int giftsPerRound, + Fn close)> understood); + +[[nodiscard]] TextWithEntities ActiveAuctionsTitle( + const Data::ActiveAuctions &auctions); +struct ManyAuctionsState { + TextWithEntities text; + bool someOutbid = false; +}; +[[nodiscard]] ManyAuctionsState ActiveAuctionsState( + const Data::ActiveAuctions &auctions); +[[nodiscard]] rpl::producer ActiveAuctionsButton( + const Data::ActiveAuctions &auctions); +[[nodiscard]] Fn ActiveAuctionsCallback( + not_null window, + const Data::ActiveAuctions &auctions); + +} // namespace Ui diff --git a/Telegram/SourceFiles/boxes/star_gift_box.cpp b/Telegram/SourceFiles/boxes/star_gift_box.cpp index b9e8d0d098..dab64bf0c8 100644 --- a/Telegram/SourceFiles/boxes/star_gift_box.cpp +++ b/Telegram/SourceFiles/boxes/star_gift_box.cpp @@ -24,6 +24,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/peer_list_controllers.h" #include "boxes/premium_preview_box.h" #include "boxes/send_credits_box.h" +#include "boxes/star_gift_auction_box.h" +#include "boxes/star_gift_resale_box.h" #include "boxes/transfer_gift_box.h" #include "chat_helpers/emoji_suggestions_widget.h" #include "chat_helpers/message_field.h" @@ -33,6 +35,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "chat_helpers/tabbed_selector.h" #include "core/application.h" #include "core/ui_integration.h" +#include "data/components/gift_auctions.h" #include "data/components/promo_suggestions.h" #include "data/data_birthday.h" #include "data/data_changes.h" @@ -91,6 +94,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/text/format_values.h" #include "ui/text/text_utilities.h" #include "ui/toast/toast.h" +#include "ui/top_background_gradient.h" #include "ui/ui_utility.h" #include "ui/vertical_list.h" #include "ui/widgets/fields/input_field.h" @@ -132,8 +136,8 @@ constexpr auto kSwitchUpgradeCoverInterval = 3 * crl::time(1000); constexpr auto kCrossfadeDuration = crl::time(400); constexpr auto kUpgradeDoneToastDuration = 4 * crl::time(1000); constexpr auto kGiftsPreloadTimeout = 3 * crl::time(1000); -constexpr auto kFiltersCount = 4; constexpr auto kResellPriceCacheLifetime = 60 * crl::time(1000); +constexpr auto kGradientButtonBgOpacity = 0.6; using namespace HistoryView; using namespace Info::PeerGifts; @@ -157,20 +161,6 @@ struct PremiumGiftsDescriptor { std::shared_ptr api; }; -struct GiftsDescriptor { - std::vector list; - std::shared_ptr api; -}; - -struct GiftDetails { - GiftDescriptor descriptor; - TextWithEntities text; - uint64 randomId = 0; - bool anonymous = false; - bool upgraded = false; - bool byStars = false; -}; - struct SessionResalePrices { explicit SessionResalePrices(not_null session) : api(std::make_unique(session->user())) { @@ -294,14 +284,14 @@ public: PreviewWrap( not_null parent, not_null recipient, - rpl::producer details); + rpl::producer details); ~PreviewWrap(); private: void paintEvent(QPaintEvent *e) override; void resizeTo(int width); - void prepare(rpl::producer details); + void prepare(rpl::producer details); const not_null _history; const not_null _recipient; @@ -440,7 +430,7 @@ auto GenerateGiftMedia( not_null parent, Element *replacing, not_null recipient, - const GiftDetails &data) + const GiftSendDetails &data) -> Fn, Fn)>)> { @@ -485,10 +475,10 @@ auto GenerateGiftMedia( tr::now, lt_count, gift.months, - Text::Bold); + tr::bold); }, [&](const GiftTypeStars &gift) { return recipient->isSelf() - ? tr::lng_action_gift_self_subtitle(tr::now, Text::Bold) + ? tr::lng_action_gift_self_subtitle(tr::now, tr::bold) : tr::lng_action_gift_got_subtitle( tr::now, lt_user, @@ -499,25 +489,23 @@ auto GenerateGiftMedia( recipient->session().user()))) .append(' ') .append(recipient->session().user()->shortName()), - Text::Bold); + tr::bold); }); auto textFallback = v::match(descriptor, [&](GiftTypePremium gift) { return tr::lng_action_gift_premium_about( tr::now, - Text::RichLangValue); + tr::rich); }, [&](const GiftTypeStars &gift) { return data.upgraded - ? tr::lng_action_gift_got_upgradable_text( - tr::now, - Text::RichLangValue) + ? tr::lng_action_gift_got_upgradable_text(tr::now, tr::rich) : (recipient->isSelf() && gift.info.starsToUpgrade) - ? tr::lng_action_gift_self_about_unique( - tr::now, - Text::RichLangValue) + ? tr::lng_action_gift_self_about_unique(tr::now, tr::rich) : (recipient->isBroadcast() && gift.info.starsToUpgrade) - ? tr::lng_action_gift_channel_about_unique( - tr::now, - Text::RichLangValue) + ? tr::lng_action_gift_channel_about_unique(tr::now, tr::rich) + : gift.info.auction() + ? (recipient->isBroadcast() + ? tr::lng_action_gift_got_gift_channel(tr::now, tr::rich) + : tr::lng_action_gift_got_gift_text(tr::now, tr::rich)) : (recipient->isSelf() ? tr::lng_action_gift_self_about : recipient->isBroadcast() @@ -526,7 +514,7 @@ auto GenerateGiftMedia( tr::now, lt_count, gift.info.starsConverted, - Text::RichLangValue); + tr::rich); }); auto description = data.text.empty() ? std::move(textFallback) @@ -571,65 +559,10 @@ auto GenerateGiftMedia( }; } -[[nodiscard]] QImage CreateGradient( - QSize size, - const Data::UniqueGift &gift) { - const auto ratio = style::DevicePixelRatio(); - auto result = QImage(size * ratio, QImage::Format_ARGB32_Premultiplied); - result.setDevicePixelRatio(ratio); - - auto p = QPainter(&result); - auto hq = PainterHighQualityEnabler(p); - auto gradient = QRadialGradient( - QRect(QPoint(), size).center(), - size.height() / 2); - gradient.setStops({ - { 0., gift.backdrop.centerColor }, - { 1., gift.backdrop.edgeColor }, - }); - p.setBrush(gradient); - p.setPen(Qt::NoPen); - p.drawRect(QRect(QPoint(), size)); - p.end(); - - const auto mask = Images::CornersMask(st::boxRadius); - return Images::Round(std::move(result), mask, RectPart::FullTop); -} - -void PrepareImage( - QImage &image, - not_null emoji, - const PatternPoint &point, - const Data::UniqueGift &gift) { - if (!image.isNull() || !emoji->ready()) { - return; - } - const auto ratio = style::DevicePixelRatio(); - const auto size = Emoji::GetSizeNormal() / ratio; - image = QImage( - 2 * QSize(size, size) * ratio, - QImage::Format_ARGB32_Premultiplied); - image.setDevicePixelRatio(ratio); - image.fill(Qt::transparent); - auto p = QPainter(&image); - auto hq = PainterHighQualityEnabler(p); - p.setOpacity(point.opacity); - if (point.scale < 1.) { - p.translate(size, size); - p.scale(point.scale, point.scale); - p.translate(-size, -size); - } - const auto shift = (2 * size - (Emoji::GetSizeLarge() / ratio)) / 2; - emoji->paint(p, { - .textColor = gift.backdrop.patternColor, - .position = QPoint(shift, shift), - }); -} - PreviewWrap::PreviewWrap( not_null parent, not_null recipient, - rpl::producer details) + rpl::producer details) : RpWidget(parent) , _history(recipient->owner().history(recipient->session().userPeerId())) , _recipient(recipient) @@ -661,7 +594,7 @@ PreviewWrap::PreviewWrap( void ShowSentToast( not_null window, const GiftDescriptor &descriptor, - const GiftDetails &details) { + const GiftSendDetails &details) { const auto &st = st::historyPremiumToast; const auto skip = st.padding.top(); const auto size = st.style.font->height * 2; @@ -744,8 +677,8 @@ PreviewWrap::~PreviewWrap() { _item = {}; } -void PreviewWrap::prepare(rpl::producer details) { - std::move(details) | rpl::start_with_next([=](GiftDetails details) { +void PreviewWrap::prepare(rpl::producer details) { + std::move(details) | rpl::start_with_next([=](GiftSendDetails details) { const auto &descriptor = details.descriptor; const auto cost = v::match(descriptor, [&](GiftTypePremium data) { const auto stars = (details.byStars && data.stars) @@ -949,423 +882,6 @@ void PreviewWrap::paintEvent(QPaintEvent *e) { return {}; } -[[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 filter; - object_ptr widget; -}; -[[nodiscard]] ResaleTabs MakeResaleTabs( - std::shared_ptr show, - not_null peer, - const ResaleGiftsDescriptor &info, - rpl::producer filter) { - auto widget = object_ptr((QWidget*)nullptr); - const auto raw = widget.data(); - - struct Button { - QRect geometry; - Text::String text; - }; - struct State { - rpl::variable filter; - rpl::variable fullWidth; - std::vector