diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 5ded5a0400b18..6ac646b3c6ede 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -94,6 +94,7 @@ jobs: -D CMAKE_CXX_FLAGS="-Werror" \ -D CMAKE_EXE_LINKER_FLAGS="-s" \ -D TDESKTOP_API_TEST=ON \ + -D DESKTOP_APP_DISABLE_AUTOUPDATE=OFF \ -D DESKTOP_APP_DISABLE_CRASH_REPORTS=OFF \ $DEFINE @@ -115,7 +116,7 @@ jobs: run: | cd $REPO_NAME/out/Debug mkdir artifact - mv Telegram artifact/ + mv {Telegram,Updater} artifact/ - uses: actions/upload-artifact@master if: env.UPLOAD_ARTIFACT == 'true' name: Upload artifact. diff --git a/.github/workflows/mac.yml b/.github/workflows/mac.yml index 96bfe6e1322f0..2a570aef7fa69 100644 --- a/.github/workflows/mac.yml +++ b/.github/workflows/mac.yml @@ -115,6 +115,7 @@ jobs: -D CMAKE_CXX_FLAGS="-Werror" \ -D CMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED=NO \ -D TDESKTOP_API_TEST=ON \ + -D DESKTOP_APP_DISABLE_AUTOUPDATE=OFF \ -D DESKTOP_APP_DISABLE_CRASH_REPORTS=OFF \ $DEFINE diff --git a/.github/workflows/win.yml b/.github/workflows/win.yml index 53b517939548b..b42906c7cbe50 100644 --- a/.github/workflows/win.yml +++ b/.github/workflows/win.yml @@ -169,6 +169,7 @@ jobs: %TDESKTOP_BUILD_GENERATOR% ^ %TDESKTOP_BUILD_ARCH% ^ %TDESKTOP_BUILD_API% ^ + -D DESKTOP_APP_DISABLE_AUTOUPDATE=OFF ^ -D DESKTOP_APP_DISABLE_CRASH_REPORTS=OFF ^ -D DESKTOP_APP_NO_PDB=ON ^ %TDESKTOP_BUILD_DEFINE% @@ -178,8 +179,10 @@ jobs: - name: Move artifact. if: (env.UPLOAD_ARTIFACT == 'true') || (github.ref == 'refs/heads/nightly') run: | + set OUT=%TBUILD%\%REPO_NAME%\out\Debug mkdir artifact - move %TBUILD%\%REPO_NAME%\out\Debug\Telegram.exe artifact/ + move %OUT%\Telegram.exe artifact/ + move %OUT%\Updater.exe artifact/ - uses: actions/upload-artifact@master name: Upload artifact. if: (env.UPLOAD_ARTIFACT == 'true') || (github.ref == 'refs/heads/nightly') diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index acf322fc23396..4f9db79398dba 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -550,6 +550,10 @@ PRIVATE data/data_replies_list.h data/data_reply_preview.cpp data/data_reply_preview.h + data/data_saved_messages.cpp + data/data_saved_messages.h + data/data_saved_sublist.cpp + data/data_saved_sublist.h data/data_search_controller.cpp data/data_search_controller.h data/data_send_action.cpp @@ -796,6 +800,8 @@ PRIVATE history/view/history_view_sponsored_click_handler.h history/view/history_view_sticker_toast.cpp history/view/history_view_sticker_toast.h + history/view/history_view_sublist_section.cpp + history/view/history_view_sublist_section.h history/view/history_view_transcribe_button.cpp history/view/history_view_transcribe_button.h history/view/history_view_translate_bar.cpp @@ -897,6 +903,8 @@ PRIVATE info/profile/info_profile_values.h info/profile/info_profile_widget.cpp info/profile/info_profile_widget.h + info/saved/info_saved_sublists_widget.cpp + info/saved/info_saved_sublists_widget.h info/settings/info_settings_widget.cpp info/settings/info_settings_widget.h info/similar_channels/info_similar_channels_widget.cpp diff --git a/Telegram/Resources/animations/voice_ttl_idle.tgs b/Telegram/Resources/animations/voice_ttl_idle.tgs new file mode 100644 index 0000000000000..01661129813aa Binary files /dev/null and b/Telegram/Resources/animations/voice_ttl_idle.tgs differ diff --git a/Telegram/Resources/animations/voice_ttl_start.tgs b/Telegram/Resources/animations/voice_ttl_start.tgs new file mode 100644 index 0000000000000..835556dd0b363 Binary files /dev/null and b/Telegram/Resources/animations/voice_ttl_start.tgs differ diff --git a/Telegram/Resources/art/winners.tgs b/Telegram/Resources/art/winners.tgs new file mode 100644 index 0000000000000..d1112ecbe732e Binary files /dev/null and b/Telegram/Resources/art/winners.tgs differ diff --git a/Telegram/Resources/icons/hidden_author_userpic.png b/Telegram/Resources/icons/hidden_author_userpic.png new file mode 100644 index 0000000000000..37acae5ab95ae Binary files /dev/null and b/Telegram/Resources/icons/hidden_author_userpic.png differ diff --git a/Telegram/Resources/icons/hidden_author_userpic@2x.png b/Telegram/Resources/icons/hidden_author_userpic@2x.png new file mode 100644 index 0000000000000..31c29045e918b Binary files /dev/null and b/Telegram/Resources/icons/hidden_author_userpic@2x.png differ diff --git a/Telegram/Resources/icons/hidden_author_userpic@3x.png b/Telegram/Resources/icons/hidden_author_userpic@3x.png new file mode 100644 index 0000000000000..b60d52b54c1c4 Binary files /dev/null and b/Telegram/Resources/icons/hidden_author_userpic@3x.png differ diff --git a/Telegram/Resources/icons/info/info_media_saved.png b/Telegram/Resources/icons/info/info_media_saved.png new file mode 100644 index 0000000000000..0b3ebc5dde0a7 Binary files /dev/null and b/Telegram/Resources/icons/info/info_media_saved.png differ diff --git a/Telegram/Resources/icons/info/info_media_saved@2x.png b/Telegram/Resources/icons/info/info_media_saved@2x.png new file mode 100644 index 0000000000000..aa91e6fb81eb6 Binary files /dev/null and b/Telegram/Resources/icons/info/info_media_saved@2x.png differ diff --git a/Telegram/Resources/icons/info/info_media_saved@3x.png b/Telegram/Resources/icons/info/info_media_saved@3x.png new file mode 100644 index 0000000000000..6c9814fe12eae Binary files /dev/null and b/Telegram/Resources/icons/info/info_media_saved@3x.png differ diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index c7160a2a39521..acebc453088b5 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -395,6 +395,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_dlg_new_bot_name" = "Bot name"; "lng_no_chats" = "Your chats will be here"; "lng_no_chats_filter" = "No chats currently belong to this folder."; +"lng_no_saved_sublists" = "You can save messages from other chats here."; "lng_contacts_loading" = "Loading..."; "lng_contacts_not_found" = "No contacts found"; "lng_topics_not_found" = "No topics found."; @@ -591,6 +592,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_settings_section_background" = "Chat background"; "lng_settings_bg_from_gallery" = "Choose from gallery"; "lng_settings_bg_from_file" = "Choose from file"; +"lng_settings_bg_remove" = "Remove wallpaper"; "lng_settings_bg_theme_edit" = "Edit theme"; "lng_settings_bg_theme_create" = "Create new theme"; "lng_settings_bg_cloud_themes" = "Custom themes"; @@ -813,6 +815,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_settings_manage_enabled_dictionary" = "Dictionary is enabled"; "lng_settings_manage_remove_dictionary" = "Remove Dictionary"; +"lng_settings_gift_premium" = "Premium Gifting"; +"lng_settings_gift_premium_users_confirm" = "Proceed"; +"lng_settings_gift_premium_users_error#one" = "You can select maximum {count} user."; +"lng_settings_gift_premium_users_error#other" = "You can select maximum {count} users."; + "lng_backgrounds_header" = "Choose Wallpaper"; "lng_theme_sure_keep" = "Keep this theme?"; "lng_theme_reverting#one" = "Reverting to the old theme in {count} second."; @@ -833,6 +840,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_background_blur" = "Blurred"; "lng_background_sure_delete" = "Are you sure you want to delete this background?"; "lng_background_other_info" = "{user} will be able to apply this wallpaper"; +"lng_background_other_channel" = "All subscribers will see this wallpaper"; "lng_background_apply1" = "Apply the wallpaper in this chat."; "lng_background_apply2" = "Enjoy the view."; "lng_background_apply_button" = "Apply For This Chat"; @@ -841,6 +849,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_background_reset_default" = "Reset"; "lng_background_apply_me" = "Apply for me"; "lng_background_apply_both" = "Apply for me and {user}"; +"lng_background_apply_channel" = "Apply For Channel"; "lng_download_path_ask" = "Ask download path for each file"; "lng_download_path" = "Download path"; @@ -1179,6 +1188,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_profile_common_groups#other" = "{count} groups in common"; "lng_profile_similar_channels#one" = "{count} similar channel"; "lng_profile_similar_channels#other" = "{count} similar channels"; +"lng_profile_saved_messages#one" = "{count} saved message"; +"lng_profile_saved_messages#other" = "{count} saved messages"; "lng_profile_participants_section" = "Members"; "lng_profile_subscribers_section" = "Subscribers"; "lng_profile_add_contact" = "Add Contact"; @@ -1682,7 +1693,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_action_giveaway_started" = "{from} just started a giveaway of Telegram Premium subscriptions to its followers."; "lng_action_giveaway_results#one" = "{count} winner of the giveaway was randomly selected by Telegram and received private messages with giftcodes."; "lng_action_giveaway_results#other" = "{count} winners of the giveaway were randomly selected by Telegram and received private messages with giftcodes."; -"lng_action_giveaway_results_some" = "Some winners of the giveaway was randomly selected by Telegram and received private messages with giftcodes."; +"lng_action_giveaway_results_some" = "Some winners of the giveaway were randomly selected by Telegram and received private messages with giftcodes."; "lng_action_giveaway_results_none" = "No winners of the giveaway could be selected."; "lng_similar_channels_title" = "Similar channels"; @@ -1704,6 +1715,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_ttl_video_received" = "{from} sent you a self-destructing video. Please view it on your mobile."; "lng_ttl_video_sent" = "You sent a self-destructing video."; "lng_ttl_video_expired" = "Video has expired"; +"lng_ttl_voice_sent" = "You sent a self-destructing voice messsage."; +"lng_ttl_voice_expired" = "Voice message expired"; +"lng_ttl_round_sent" = "You sent a self-destructing video message."; +"lng_ttl_round_expired" = "Round message expired"; "lng_profile_add_more_after_create" = "You will be able to add more members after you create the group."; "lng_profile_camera_title" = "Capture yourself"; @@ -1836,6 +1851,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_sponsored_message_title" = "Sponsored"; "lng_recommended_message_title" = "Recommended"; "lng_edited" = "edited"; +"lng_commented" = "commented"; "lng_edited_date" = "Edited: {date}"; "lng_sent_date" = "Sent: {date}"; "lng_views_tooltip#one" = "Views: {count}"; @@ -2055,6 +2071,22 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_premium_gift_per" = "{cost} / month"; "lng_premium_gift_terms" = "You can review the list of features and terms of use for Telegram Premium {link}."; "lng_premium_gift_terms_link" = "here"; +"lng_premium_gifts_about_user1" = "Give **{user}** access to exclusive features."; +"lng_premium_gifts_about_user2" = "Give **{user}** and **{second_user}** access to exclusive features."; +"lng_premium_gifts_about_user3" = "Give **{user}**, **{second_user}** and **{name}** access to exclusive features."; +"lng_premium_gifts_about_user_more#one" = "Give **{user}**, **{second_user}**, **{name}** and **{count}** more friend access to exclusive features."; +"lng_premium_gifts_about_user_more#other" = "Give **{user}**, **{second_user}**, **{name}** and **{count}** more friends access to exclusive features."; +"lng_premium_gifts_about_reward#one" = "You will receive {emoji}**{count}** boost."; +"lng_premium_gifts_about_reward#other" = "You will receive {emoji}**{count}** boosts."; +"lng_premium_gifts_about_paid_title" = "Gifts Sent!"; +"lng_premium_gifts_about_paid1" = "**{user}** has been notified about the gifts you purchased."; +"lng_premium_gifts_about_paid2" = "**{user}** and **{second_user}** have been notified about the gifts you purchased."; +"lng_premium_gifts_about_paid3" = "**{user}**, **{second_user}** and **{name}** have been notified about the gifts you purchased."; +"lng_premium_gifts_about_paid_more#one" = "**{user}**, **{second_user}**, **{name}** and **{count}** other have been notified about the gifts you purchased."; +"lng_premium_gifts_about_paid_more#other" = "**{user}**, **{second_user}**, **{name}** and **{count}** others have been notified about the gifts you purchased."; +"lng_premium_gifts_about_paid_below#one" = "They now have access to additional features."; +"lng_premium_gifts_about_paid_below#other" = "They now have access to additional features."; +"lng_premium_gifts_summary_subtitle" = "What's Included"; "lng_boost_channel_button" = "Boost Channel"; "lng_boost_again_button" = "Boost Again"; @@ -2111,6 +2143,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_boost_channel_needs_level_color#one" = "Your channel needs to reach **Level {count}** to change channel color."; "lng_boost_channel_needs_level_color#other" = "Your channel needs to reach **Level {count}** to change channel color."; +"lng_boost_channel_title_wallpaper" = "Enable wallpapers"; +"lng_boost_channel_needs_level_wallpaper#one" = "Your channel needs to reach **Level {count}** to change channel wallpaper."; +"lng_boost_channel_needs_level_wallpaper#other" = "Your channel needs to reach **Level {count}** to change channel wallpaper."; + +"lng_boost_channel_title_status" = "Enable emoji status"; +"lng_boost_channel_needs_level_status#one" = "Your channel needs to reach **Level {count}** to set emoji status."; +"lng_boost_channel_needs_level_status#other" = "Your channel needs to reach **Level {count}** to set emoji status."; + "lng_boost_channel_title_reactions" = "Custom reactions"; "lng_boost_channel_needs_level_reactions#one" = "Your channel needs to reach **Level {count}** to add **{same_count}** custom emoji as a reaction."; "lng_boost_channel_needs_level_reactions#other" = "Your channel needs to reach **Level {count}** to add **{same_count}** custom emoji as reactions."; @@ -2170,6 +2210,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_giveaway_maximum_users_error#other" = "You can select maximum {count} users."; "lng_giveaway_channels_confirm_title" = "Channel is Private"; "lng_giveaway_channels_confirm_about" = "Are you sure you want to add a private channel? Users won't be able to join it without an invite link."; +"lng_giveaway_additional_prizes" = "Additional prizes"; +"lng_giveaway_additional_about" = "Turn this on if you want to give the winners your own prizes in addition to Premium subscriptions."; +"lng_giveaway_additional_prizes_ph" = "Enter your prize"; +"lng_giveaway_prizes_just_premium#one" = "All prizes: **{count}** Telegram Premium subscription {duration}."; +"lng_giveaway_prizes_just_premium#other" = "All prizes: **{count}** Telegram Premium subscriptions {duration}."; +"lng_giveaway_prizes_additional#one" = "All prizes: **{count}** {prize} with Telegram Premium subscription {duration}."; +"lng_giveaway_prizes_additional#other" = "All prizes: **{count}** {prize} with Telegram Premium subscriptions {duration}."; +"lng_giveaway_show_winners" = "Show winners"; +"lng_giveaway_show_winners_about" = "Choose whether to make the list of winners public when the giveaway ends."; "lng_giveaway_created_title" = "Giveaway created"; "lng_giveaway_created_body" = "Check your channels' {link} to see how this giveaway boosted your channel."; @@ -2189,6 +2238,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_prizes_title#one" = "Giveaway Prize"; "lng_prizes_title#other" = "Giveaway Prizes"; +"lng_prizes_additional#one" = "**{count}** {prize}"; +"lng_prizes_additional#other" = "**{count}** {prize}"; +"lng_prizes_additional_with" = "with"; "lng_prizes_about#one" = "**{count}** Telegram Premium Subscription {duration}."; "lng_prizes_about#other" = "**{count}** Telegram Premium Subscriptions {duration}."; "lng_prizes_participants" = "Participants"; @@ -2207,6 +2259,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_prizes_end_text" = "This giveaway was sponsored by {admins}."; "lng_prizes_admins#one" = "the admins of {channel}, who aquired **{count} Telegram Premium** subscription {duration} for its followers"; "lng_prizes_admins#other" = "the admins of {channel}, who aquired **{count} Telegram Premium** subscriptions {duration} for its followers."; +"lng_prizes_additional_added#one" = "{channel} also included **{count} {prize}** in the prize. Admins of the channel are responsible for delivering this prize."; +"lng_prizes_additional_added#other" = "{channel} also included **{count} {prize}** in the prizes. Admins of the channel are responsible for delivering these prizes."; "lng_prizes_how_when_finish" = "On {date}, Telegram will automatically select {winners}."; "lng_prizes_end_when_finish" = "On {date}, Telegram automatically selected {winners}."; "lng_prizes_end_activated#one" = "**{count}** of the winners already used their gift link."; @@ -2232,6 +2286,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_prizes_cancelled" = "The channel cancelled the prizes by reversing the payment for them."; "lng_prizes_badge" = "x{amount}"; +"lng_prizes_results_title" = "Winners Selected!"; +"lng_prizes_results_about#one" = "**{count}** winner of the {link} was randomly selected by Telegram."; +"lng_prizes_results_about#other" = "**{count}** winners of the {link} were randomly selected by Telegram."; +"lng_prizes_results_link" = "Giveaway"; +"lng_prizes_results_winners" = "Winners"; +"lng_prizes_results_more#one" = "and {count} more!"; +"lng_prizes_results_more#other" = "and {count} more!"; +"lng_prizes_results_all" = "All winners received gift links in private messages."; +"lng_prizes_results_some" = "Some winners couldn't be selected."; + "lng_gift_link_title" = "Gift Link"; "lng_gift_link_about" = "This link allows you to activate\na **Telegram Premium** subscription."; "lng_gift_link_label_from" = "From"; @@ -2435,6 +2499,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_saved_short" = "Save"; "lng_saved_forward_here" = "Forward messages here for quick access"; "lng_saved_quote_here" = "Quote here to save"; +"lng_saved_open_chat" = "Open Chat"; +"lng_saved_open_channel" = "Open Channel"; +"lng_saved_open_group" = "Open Group"; +"lng_saved_about_hidden" = "Senders of this messages restricted to link their name when forwarding."; "lng_scheduled_messages" = "Scheduled Messages"; "lng_scheduled_messages_empty" = "No scheduled messages here yet..."; @@ -2461,6 +2529,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_comments_open_none" = "Leave a comment"; "lng_replies_view_original" = "View in chat"; "lng_replies_messages" = "Replies"; +"lng_hidden_author_messages" = "Author Hidden"; "lng_replies_discussion_started" = "Discussion started"; "lng_replies_no_comments" = "No comments here yet..."; @@ -2801,6 +2870,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_edit_sign_messages" = "Sign messages"; "lng_edit_group" = "Edit group"; "lng_edit_channel_color" = "Change name color"; +"lng_edit_channel_level_min" = "Level 1+"; +"lng_edit_channel_wallpaper" = "Channel wallpaper"; +"lng_edit_channel_wallpaper_about" = "Set a wallpaper that will be visible for everyone reading your channel."; +"lng_edit_channel_status" = "Channel emoji status"; +"lng_edit_channel_status_about" = "Choose a status that will be shown next to the channel's name."; "lng_edit_self_title" = "Edit your name"; "lng_confirm_contact_data" = "New Contact"; "lng_add_contact" = "Create"; @@ -3615,6 +3689,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_admin_log_set_background_emoji" = "{from} set channel background emoji to {emoji}"; "lng_admin_log_change_background_emoji" = "{from} changed channel background emoji from {previous} to {emoji}"; "lng_admin_log_removed_background_emoji" = "{from} removed channel background emoji {emoji}"; +"lng_admin_log_change_profile_color" = "{from} changed channel profile color from {previous} to {color}"; +"lng_admin_log_set_profile_background_emoji" = "{from} set channel profile background emoji to {emoji}"; +"lng_admin_log_change_profile_background_emoji" = "{from} changed channel profile background emoji from {previous} to {emoji}"; +"lng_admin_log_removed_profile_background_emoji" = "{from} removed channel profile background emoji {emoji}"; +"lng_admin_log_change_wallpaper" = "{from} changed channel wallpaper"; +"lng_admin_log_set_status" = "{from} set channel emoji status to {emoji}"; +"lng_admin_log_change_status" = "{from} changed channel emoji status from {previous} to {emoji}"; +"lng_admin_log_removed_status" = "{from} removed channel emoji status {emoji}"; +"lng_admin_log_set_status_until" = "{from} set channel emoji status to {emoji} until {date}"; +"lng_admin_log_change_status_until" = "{from} changed channel emoji status from {previous} to {emoji} until {date}"; "lng_admin_log_user_with_username" = "{name} ({mention})"; "lng_admin_log_messages_ttl_set" = "{from} enabled messages auto-delete after {duration}"; "lng_admin_log_messages_ttl_changed" = "{from} changed messages auto-delete period from {previous} to {duration}"; @@ -4207,6 +4291,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_request_peer_confirm_rights" = "This will also add {bot} to {chat} with the following rights: {rights}."; "lng_request_peer_confirm_send" = "Send"; "lng_request_user_title" = "Choose User"; +"lng_request_users_title" = "Choose Users"; "lng_request_user_premium_yes" = "The user should have a Premium subscription."; "lng_request_user_premium_no" = "The user shouldn't have a Premium subscription."; "lng_request_user_no" = "No such users"; diff --git a/Telegram/Resources/qrc/telegram/animations.qrc b/Telegram/Resources/qrc/telegram/animations.qrc index 705b508a246af..76ea1ef4a3c29 100644 --- a/Telegram/Resources/qrc/telegram/animations.qrc +++ b/Telegram/Resources/qrc/telegram/animations.qrc @@ -11,5 +11,7 @@ ../../animations/ttl.tgs ../../animations/discussion.tgs ../../animations/stats.tgs + ../../animations/voice_ttl_idle.tgs + ../../animations/voice_ttl_start.tgs diff --git a/Telegram/Resources/qrc/telegram/telegram.qrc b/Telegram/Resources/qrc/telegram/telegram.qrc index 0e43acb03d343..57afe388252d5 100644 --- a/Telegram/Resources/qrc/telegram/telegram.qrc +++ b/Telegram/Resources/qrc/telegram/telegram.qrc @@ -15,6 +15,7 @@ ../../art/slot_2_idle.tgs ../../art/slot_back.tgs ../../art/slot_pull.tgs + ../../art/winners.tgs ../../day-blue.tdesktop-theme ../../night.tdesktop-theme ../../night-green.tdesktop-theme diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml index f983e858561a2..d8a1b5191a47d 100644 --- a/Telegram/Resources/uwp/AppX/AppxManifest.xml +++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml @@ -10,7 +10,7 @@ + Version="4.14.0.0" /> Telegram Desktop Telegram Messenger LLP diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc index b2f5c49e7fedb..4f8a48a2472b7 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 4,12,2,0 - PRODUCTVERSION 4,12,2,0 + FILEVERSION 4,14,0,0 + PRODUCTVERSION 4,14,0,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -62,10 +62,10 @@ BEGIN BEGIN VALUE "CompanyName", "Telegram FZ-LLC" VALUE "FileDescription", "Telegram Desktop" - VALUE "FileVersion", "4.12.2.0" + VALUE "FileVersion", "4.14.0.0" VALUE "LegalCopyright", "Copyright (C) 2014-2023" VALUE "ProductName", "Telegram Desktop" - VALUE "ProductVersion", "4.12.2.0" + VALUE "ProductVersion", "4.14.0.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc index 95e556c9ab811..351bde377dcf4 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 4,12,2,0 - PRODUCTVERSION 4,12,2,0 + FILEVERSION 4,14,0,0 + PRODUCTVERSION 4,14,0,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -53,10 +53,10 @@ BEGIN BEGIN VALUE "CompanyName", "Telegram FZ-LLC" VALUE "FileDescription", "Telegram Desktop Updater" - VALUE "FileVersion", "4.12.2.0" + VALUE "FileVersion", "4.14.0.0" VALUE "LegalCopyright", "Copyright (C) 2014-2023" VALUE "ProductName", "Telegram Desktop" - VALUE "ProductVersion", "4.12.2.0" + VALUE "ProductVersion", "4.14.0.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/SourceFiles/_other/startup_task_win.cpp b/Telegram/SourceFiles/_other/startup_task_win.cpp index 8e780848d6650..a54b941ee7aec 100644 --- a/Telegram/SourceFiles/_other/startup_task_win.cpp +++ b/Telegram/SourceFiles/_other/startup_task_win.cpp @@ -6,6 +6,7 @@ For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include +#include #include #include diff --git a/Telegram/SourceFiles/_other/updater_win.cpp b/Telegram/SourceFiles/_other/updater_win.cpp index 1873815ce8a4f..0b9b247853279 100644 --- a/Telegram/SourceFiles/_other/updater_win.cpp +++ b/Telegram/SourceFiles/_other/updater_win.cpp @@ -537,11 +537,12 @@ HANDLE _generateDumpFileAtPath(const WCHAR *path) { GetLocalTime(&stLocalTime); - wsprintf(szFileName, L"%s%s-%s-%04d%02d%02d-%02d%02d%02d-%ld-%ld.dmp", - szPath, szExeName, updaterVersionStr, - stLocalTime.wYear, stLocalTime.wMonth, stLocalTime.wDay, - stLocalTime.wHour, stLocalTime.wMinute, stLocalTime.wSecond, - GetCurrentProcessId(), GetCurrentThreadId()); + wsprintf( + szFileName, L"%s%s-%s-%04d%02d%02d-%02d%02d%02d-%ld-%ld.dmp", + szPath, szExeName, updaterVersionStr, + stLocalTime.wYear, stLocalTime.wMonth, stLocalTime.wDay, + stLocalTime.wHour, stLocalTime.wMinute, stLocalTime.wSecond, + GetCurrentProcessId(), GetCurrentThreadId()); return CreateFile(szFileName, GENERIC_READ|GENERIC_WRITE, FILE_SHARE_WRITE|FILE_SHARE_READ, 0, CREATE_ALWAYS, 0, 0); } @@ -562,7 +563,7 @@ void _generateDump(EXCEPTION_POINTERS* pExceptionPointers) { DWORD len = GetModuleFileName(GetModuleHandle(0), szPath, maxFileLen); if (!len) return; - WCHAR *pathEnd = szPath + len; + WCHAR *pathEnd = szPath + len; if (!_wcsicmp(pathEnd - wcslen(_exeName), _exeName)) { wsprintf(pathEnd - wcslen(_exeName), L""); diff --git a/Telegram/SourceFiles/api/api_bot.cpp b/Telegram/SourceFiles/api/api_bot.cpp index 239d2d0ab89a4..d49f1bbf78472 100644 --- a/Telegram/SourceFiles/api/api_bot.cpp +++ b/Telegram/SourceFiles/api/api_bot.cpp @@ -415,12 +415,16 @@ void ActivateBotCommand(ClickHandlerContext context, int row, int column) { const auto peer = item->history()->peer; const auto itemId = item->id; const auto id = int32(button->buttonId); - const auto chosen = [=](not_null result) { + const auto chosen = [=](std::vector> result) { peer->session().api().request(MTPmessages_SendBotRequestedPeer( peer->input, MTP_int(itemId), MTP_int(id), - result->input + MTP_vector_from_range( + result + | ranges::views::transform([]( + not_null peer) { + return MTPInputPeer(peer->input); })) )).done([=](const MTPUpdates &result) { peer->session().api().applyUpdates(result); }).send(); diff --git a/Telegram/SourceFiles/api/api_messages_search.cpp b/Telegram/SourceFiles/api/api_messages_search.cpp index 09f9d6a2159ea..15119b9068ee1 100644 --- a/Telegram/SourceFiles/api/api_messages_search.cpp +++ b/Telegram/SourceFiles/api/api_messages_search.cpp @@ -90,6 +90,7 @@ void MessagesSearch::searchRequest() { (_from ? _from->input : MTP_inputPeerEmpty()), + MTPInputPeer(), // saved_peer_id MTPint(), // top_msg_id MTP_inputMessagesFilterEmpty(), MTP_int(0), // min_date diff --git a/Telegram/SourceFiles/api/api_peer_colors.cpp b/Telegram/SourceFiles/api/api_peer_colors.cpp index 85d60d814c8d7..50c2382fdcc64 100644 --- a/Telegram/SourceFiles/api/api_peer_colors.cpp +++ b/Telegram/SourceFiles/api/api_peer_colors.cpp @@ -8,6 +8,7 @@ For license and copyright information please follow this link: #include "api/api_peer_colors.h" #include "apiwrap.h" +#include "data/data_peer.h" #include "ui/chat/chat_style.h" namespace Api { @@ -62,6 +63,16 @@ auto PeerColors::indicesValue() const })); } +int PeerColors::requiredLevelFor(PeerId channel, uint8 index) const { + if (Data::DecideColorIndex(channel) == index) { + return 0; + } else if (const auto i = _requiredLevels.find(index) + ; i != end(_requiredLevels)) { + return i->second; + } + return 1; +} + void PeerColors::apply(const MTPDhelp_peerColors &data) { auto suggested = std::vector(); auto colors = std::make_shared< @@ -89,6 +100,7 @@ void PeerColors::apply(const MTPDhelp_peerColors &data) { }; const auto &list = data.vcolors().v; + _requiredLevels.clear(); suggested.reserve(list.size()); for (const auto &color : list) { const auto &data = color.data(); @@ -98,6 +110,9 @@ void PeerColors::apply(const MTPDhelp_peerColors &data) { continue; } const auto colorIndex = uint8(colorIndexBare); + if (const auto min = data.vchannel_min_level()) { + _requiredLevels[colorIndex] = min->v; + } if (!data.is_hidden()) { suggested.push_back(colorIndex); } diff --git a/Telegram/SourceFiles/api/api_peer_colors.h b/Telegram/SourceFiles/api/api_peer_colors.h index de06d42211d6f..0ad1a63c7ea8e 100644 --- a/Telegram/SourceFiles/api/api_peer_colors.h +++ b/Telegram/SourceFiles/api/api_peer_colors.h @@ -28,6 +28,10 @@ class PeerColors final { [[nodiscard]] auto indicesValue() const -> rpl::producer; + [[nodiscard]] int requiredLevelFor( + PeerId channel, + uint8 index) const; + private: void request(); void apply(const MTPDhelp_peerColors &data); @@ -38,6 +42,7 @@ class PeerColors final { mtpRequestId _requestId = 0; base::Timer _timer; rpl::variable> _suggested; + base::flat_map _requiredLevels; rpl::event_stream<> _colorIndicesChanged; std::unique_ptr _colorIndicesCurrent; diff --git a/Telegram/SourceFiles/api/api_peer_photo.cpp b/Telegram/SourceFiles/api/api_peer_photo.cpp index 25a7065873279..c70ef4491f82e 100644 --- a/Telegram/SourceFiles/api/api_peer_photo.cpp +++ b/Telegram/SourceFiles/api/api_peer_photo.cpp @@ -515,6 +515,7 @@ auto PeerPhoto::emojiList(EmojiListType type) -> EmojiListData & { case EmojiListType::Profile: return _profileEmojiList; case EmojiListType::Group: return _groupEmojiList; case EmojiListType::Background: return _backgroundEmojiList; + case EmojiListType::NoChannelStatus: return _noChannelStatusEmojiList; } Unexpected("Type in PeerPhoto::emojiList."); } @@ -551,6 +552,8 @@ void PeerPhoto::requestEmojiList(EmojiListType type) { ? send(MTPaccount_GetDefaultProfilePhotoEmojis()) : (type == EmojiListType::Group) ? send(MTPaccount_GetDefaultGroupPhotoEmojis()) + : (type == EmojiListType::NoChannelStatus) + ? send(MTPaccount_GetChannelRestrictedStatusEmojis()) : send(MTPaccount_GetDefaultBackgroundEmojis()); } diff --git a/Telegram/SourceFiles/api/api_peer_photo.h b/Telegram/SourceFiles/api/api_peer_photo.h index 1d4bd1071ee5e..71d340a031f6b 100644 --- a/Telegram/SourceFiles/api/api_peer_photo.h +++ b/Telegram/SourceFiles/api/api_peer_photo.h @@ -32,6 +32,7 @@ class PeerPhoto final { Profile, Group, Background, + NoChannelStatus, }; struct UserPhoto { @@ -112,6 +113,7 @@ class PeerPhoto final { EmojiListData _profileEmojiList; EmojiListData _groupEmojiList; EmojiListData _backgroundEmojiList; + EmojiListData _noChannelStatusEmojiList; }; diff --git a/Telegram/SourceFiles/api/api_premium.cpp b/Telegram/SourceFiles/api/api_premium.cpp index 58950f6f4b910..088c3cf876ac4 100644 --- a/Telegram/SourceFiles/api/api_premium.cpp +++ b/Telegram/SourceFiles/api/api_premium.cpp @@ -26,7 +26,7 @@ namespace { [[nodiscard]] GiftCode Parse(const MTPDpayments_checkedGiftCode &data) { return { - .from = peerFromMTP(data.vfrom_id()), + .from = data.vfrom_id() ? peerFromMTP(*data.vfrom_id()) : PeerId(), .to = data.vto_id() ? peerFromUser(*data.vto_id()) : PeerId(), .giveawayId = data.vgiveaway_msg_id().value_or_empty(), .date = data.vdate().v, @@ -342,15 +342,12 @@ PremiumGiftCodeOptions::PremiumGiftCodeOptions(not_null peer) rpl::producer PremiumGiftCodeOptions::request() { return [=](auto consumer) { auto lifetime = rpl::lifetime(); - const auto channel = _peer->asChannel(); - if (!channel) { - return lifetime; - } using TLOption = MTPPremiumGiftCodeOption; _api.request(MTPpayments_GetPremiumGiftCodeOptions( - MTP_flags( - MTPpayments_GetPremiumGiftCodeOptions::Flag::f_boost_peer), + MTP_flags(_peer->isChannel() + ? MTPpayments_GetPremiumGiftCodeOptions::Flag::f_boost_peer + : MTPpayments_GetPremiumGiftCodeOptions::Flag(0)), _peer->input )).done([=](const MTPVector &result) { auto tlMapOptions = base::flat_map>(); @@ -420,6 +417,8 @@ const std::vector &PremiumGiftCodeOptions::availablePresets() const { } [[nodiscard]] int PremiumGiftCodeOptions::monthsFromPreset(int monthsIndex) { + Expects(monthsIndex >= 0 && monthsIndex < _availablePresets.size()); + return _optionsForOnePerson.months[monthsIndex]; } diff --git a/Telegram/SourceFiles/api/api_statistics.cpp b/Telegram/SourceFiles/api/api_statistics.cpp index 6655c618ed56d..f24e156d0894b 100644 --- a/Telegram/SourceFiles/api/api_statistics.cpp +++ b/Telegram/SourceFiles/api/api_statistics.cpp @@ -10,6 +10,8 @@ For license and copyright information please follow this link: #include "apiwrap.h" #include "data/data_channel.h" #include "data/data_session.h" +#include "data/data_stories.h" +#include "data/data_story.h" #include "history/history.h" #include "main/main_session.h" #include "statistics/statistics_data_deserialize.h" @@ -359,161 +361,54 @@ PublicForwards::PublicForwards( void PublicForwards::request( const Data::PublicForwardsSlice::OffsetToken &token, Fn done) { - if (!_requestId) { - if (_fullId.messageId) { - requestMessage(token, std::move(done)); - } else if (_fullId.storyId) { - requestStory(token, std::move(done)); - } + if (_requestId) { + return; } -} - -void PublicForwards::requestMessage( - const Data::PublicForwardsSlice::OffsetToken &token, - Fn done) { - Expects(_fullId.messageId); - - const auto offsetPeer = channel()->owner().peer(token.fullId.peer); - const auto tlOffsetPeer = offsetPeer - ? offsetPeer->input - : MTP_inputPeerEmpty(); - constexpr auto kLimit = tl::make_int(100); - _requestId = makeRequest(MTPstats_GetMessagePublicForwards( - channel()->inputChannel, - MTP_int(_fullId.messageId.msg), - MTP_int(token.rate), - tlOffsetPeer, - MTP_int(token.fullId.msg), - kLimit - )).done([=, channel = channel()](const MTPmessages_Messages &result) { + const auto channel = StatisticsRequestSender::channel(); + const auto processResult = [=](const MTPstats_PublicForwards &tl) { using Messages = QVector; _requestId = 0; - auto nextToken = Data::PublicForwardsSlice::OffsetToken(); - const auto process = [&](const MTPVector &messages) { - auto result = Messages(); - for (const auto &message : messages.v) { - const auto msgId = IdFromMessage(message); - const auto peerId = PeerFromMessage(message); - const auto lastDate = DateFromMessage(message); - if (const auto peer = channel->owner().peerLoaded(peerId)) { - if (lastDate) { - channel->owner().addNewMessage( - message, - MessageFlags(), - NewMessageType::Existing); - nextToken.fullId = { peerId, msgId }; - result.push_back({ .messageId = nextToken.fullId }); - } - } - } - return result; - }; + const auto &data = tl.data(); + auto &owner = channel->owner(); - auto allLoaded = false; - auto fullCount = 0; - auto messages = result.match([&](const MTPDmessages_messages &data) { - channel->owner().processUsers(data.vusers()); - channel->owner().processChats(data.vchats()); - auto list = process(data.vmessages()); - allLoaded = true; - fullCount = list.size(); - return list; - }, [&](const MTPDmessages_messagesSlice &data) { - channel->owner().processUsers(data.vusers()); - channel->owner().processChats(data.vchats()); - auto list = process(data.vmessages()); - - if (const auto nextRate = data.vnext_rate()) { - const auto rateUpdated = (nextRate->v != token.rate); - if (rateUpdated) { - nextToken.rate = nextRate->v; - } else { - allLoaded = true; - } - } - fullCount = data.vcount().v; - return list; - }, [&](const MTPDmessages_channelMessages &data) { - channel->owner().processUsers(data.vusers()); - channel->owner().processChats(data.vchats()); - auto list = process(data.vmessages()); - allLoaded = true; - fullCount = data.vcount().v; - return list; - }, [&](const MTPDmessages_messagesNotModified &) { - allLoaded = true; - return Messages(); - }); + owner.processUsers(data.vusers()); + owner.processChats(data.vchats()); - _lastTotal = std::max(_lastTotal, fullCount); - done({ - .list = std::move(messages), - .total = _lastTotal, - .allLoaded = allLoaded, - .token = nextToken, - }); - }).fail([=] { - _requestId = 0; - }).send(); -} - -void PublicForwards::requestStory( - const Data::PublicForwardsSlice::OffsetToken &token, - Fn done) { - Expects(_fullId.storyId); - - constexpr auto kLimit = tl::make_int(100); - _requestId = makeRequest(MTPstats_GetStoryPublicForwards( - channel()->input, - MTP_int(_fullId.storyId.story), - MTP_string(token.storyOffset), - kLimit - )).done([=, channel = channel()]( - const MTPstats_PublicForwards &tlForwards) { - using Messages = QVector; - _requestId = 0; - - const auto &data = tlForwards.data(); + const auto nextToken = data.vnext_offset() + ? qs(*data.vnext_offset()) + : Data::PublicForwardsSlice::OffsetToken(); - channel->owner().processUsers(data.vusers()); - channel->owner().processChats(data.vchats()); - - const auto nextToken = Data::PublicForwardsSlice::OffsetToken({ - .storyOffset = data.vnext_offset().value_or_empty(), - }); - - const auto allLoaded = nextToken.storyOffset.isEmpty() - || (nextToken.storyOffset == token.storyOffset); const auto fullCount = data.vcount().v; - auto recentList = Messages(); + auto recentList = Messages(data.vforwards().v.size()); for (const auto &tlForward : data.vforwards().v) { tlForward.match([&](const MTPDpublicForwardMessage &data) { const auto &message = data.vmessage(); const auto msgId = IdFromMessage(message); const auto peerId = PeerFromMessage(message); const auto lastDate = DateFromMessage(message); - if (const auto peer = channel->owner().peerLoaded(peerId)) { + if (const auto peer = owner.peerLoaded(peerId)) { if (!lastDate) { return; } - channel->owner().addNewMessage( + owner.addNewMessage( message, MessageFlags(), NewMessageType::Existing); recentList.push_back({ .messageId = { peerId, msgId } }); } }, [&](const MTPDpublicForwardStory &data) { - data.vstory().match([&](const MTPDstoryItem &d) { - recentList.push_back({ - .storyId = { peerFromMTP(data.vpeer()), d.vid().v } - }); - }, [](const auto &) { - }); + const auto story = owner.stories().applySingle( + peerFromMTP(data.vpeer()), + data.vstory()); + if (story) { + recentList.push_back({ .storyId = story->fullId() }); + } }); } + const auto allLoaded = nextToken.isEmpty() || (nextToken == token); _lastTotal = std::max(_lastTotal, fullCount); done({ .list = std::move(recentList), @@ -521,9 +416,24 @@ void PublicForwards::requestStory( .allLoaded = allLoaded, .token = nextToken, }); - }).fail([=] { - _requestId = 0; - }).send(); + }; + + constexpr auto kLimit = tl::make_int(100); + if (_fullId.messageId) { + _requestId = makeRequest(MTPstats_GetMessagePublicForwards( + channel->inputChannel, + MTP_int(_fullId.messageId.msg), + MTP_string(token), + kLimit + )).done(processResult).fail([=] { _requestId = 0; }).send(); + } else if (_fullId.storyId) { + _requestId = makeRequest(MTPstats_GetStoryPublicForwards( + channel->input, + MTP_int(_fullId.storyId.story), + MTP_string(token), + kLimit + )).done(processResult).fail([=] { _requestId = 0; }).send(); + } } MessageStatistics::MessageStatistics( @@ -702,6 +612,7 @@ rpl::producer Boosts::request() { _peer->input )).done([=](const MTPpremium_BoostsStatus &result) { const auto &data = result.data(); + channel->updateLevelHint(data.vlevel().v); const auto hasPremium = !!data.vpremium_audience(); const auto premiumMemberCount = hasPremium ? std::max(0, int(data.vpremium_audience()->data().vpart().v)) diff --git a/Telegram/SourceFiles/api/api_statistics.h b/Telegram/SourceFiles/api/api_statistics.h index d1b5b6624e44e..f5360adb21600 100644 --- a/Telegram/SourceFiles/api/api_statistics.h +++ b/Telegram/SourceFiles/api/api_statistics.h @@ -77,13 +77,6 @@ class PublicForwards final : public StatisticsRequestSender { Fn done); private: - void requestMessage( - const Data::PublicForwardsSlice::OffsetToken &token, - Fn done); - void requestStory( - const Data::PublicForwardsSlice::OffsetToken &token, - Fn done); - const Data::RecentPostId _fullId; mtpRequestId _requestId = 0; int _lastTotal = 0; diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index 5f903a67d9d4b..c9714d67ec637 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -22,6 +22,7 @@ For license and copyright information please follow this link: #include "mtproto/mtproto_dc_options.h" #include "data/notify/data_notify_settings.h" #include "data/stickers/data_stickers.h" +#include "data/data_saved_messages.h" #include "data/data_session.h" #include "data/data_user.h" #include "data/data_chat.h" @@ -44,6 +45,7 @@ For license and copyright information please follow this link: #include "lang/lang_cloud_manager.h" #include "history/history.h" #include "history/history_item.h" +#include "history/history_item_helpers.h" #include "history/history_unread_things.h" #include "core/application.h" #include "storage/storage_account.h" @@ -1111,6 +1113,7 @@ void Updates::applyUpdatesNoPtsCheck(const MTPUpdates &updates) { ? peerToMTP(_session->userPeerId()) : MTP_peerUser(d.vuser_id())), MTP_peerUser(d.vuser_id()), + MTPPeer(), // saved_peer_id d.vfwd_from() ? *d.vfwd_from() : MTPMessageFwdHeader(), MTP_long(d.vvia_bot_id().value_or_empty()), d.vreply_to() ? *d.vreply_to() : MTPMessageReplyHeader(), @@ -1142,6 +1145,7 @@ void Updates::applyUpdatesNoPtsCheck(const MTPUpdates &updates) { d.vid(), MTP_peerUser(d.vfrom_id()), MTP_peerChat(d.vchat_id()), + MTPPeer(), // saved_peer_id d.vfwd_from() ? *d.vfwd_from() : MTPMessageFwdHeader(), MTP_long(d.vvia_bot_id().value_or_empty()), d.vreply_to() ? *d.vreply_to() : MTPMessageReplyHeader(), @@ -1202,11 +1206,12 @@ void Updates::applyUpdateNoPtsCheck(const MTPUpdate &update) { item->markMediaAndMentionRead(); _session->data().requestItemRepaint(item); - if (item->out() - && item->history()->peer->isUser() - && !requestingDifference()) { - item->history()->peer->asUser()->madeAction( - base::unixtime::now()); + if (item->out()) { + const auto user = item->history()->peer->asUser(); + if (user && !requestingDifference()) { + user->madeAction(base::unixtime::now()); + } + ClearMediaAsExpired(item); } } } else { @@ -2204,6 +2209,16 @@ void Updates::feedUpdate(const MTPUpdate &update) { } } break; + case mtpc_updatePinnedSavedDialogs: { + session().data().savedMessages().apply( + update.c_updatePinnedSavedDialogs()); + } break; + + case mtpc_updateSavedDialogPinned: { + session().data().savedMessages().apply( + update.c_updateSavedDialogPinned()); + } break; + case mtpc_updateChannel: { auto &d = update.c_updateChannel(); if (const auto channel = session().data().channelLoaded(d.vchannel_id())) { diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 3b12875988a1a..a247ad1964c6d 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -39,6 +39,7 @@ For license and copyright information please follow this link: #include "data/data_folder.h" #include "data/data_forum_topic.h" #include "data/data_forum.h" +#include "data/data_saved_sublist.h" #include "data/data_search_controller.h" #include "data/data_scheduled_messages.h" #include "data/data_session.h" @@ -221,11 +222,11 @@ void ApiWrap::setupSupportMode() { void ApiWrap::requestChangelog( const QString &sinceVersion, Fn callback) { - request(MTPhelp_GetAppChangelog( - MTP_string(sinceVersion) - )).done( - callback - ).send(); + //request(MTPhelp_GetAppChangelog( + // MTP_string(sinceVersion) + //)).done( + // callback + //).send(); } void ApiWrap::refreshTopPromotion() { @@ -440,6 +441,26 @@ void ApiWrap::savePinnedOrder(not_null forum) { }).send(); } +void ApiWrap::savePinnedOrder(not_null saved) { + const auto &order = _session->data().pinnedChatsOrder(saved); + const auto input = [](Dialogs::Key key) { + if (const auto sublist = key.sublist()) { + return MTP_inputDialogPeer(sublist->peer()->input); + } + Unexpected("Key type in pinnedDialogsOrder()."); + }; + auto peers = QVector(); + peers.reserve(order.size()); + ranges::transform( + order, + ranges::back_inserter(peers), + input); + request(MTPmessages_ReorderPinnedSavedDialogs( + MTP_flags(MTPmessages_ReorderPinnedSavedDialogs::Flag::f_force), + MTP_vector(peers) + )).send(); +} + void ApiWrap::toggleHistoryArchived( not_null history, bool archived, diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h index 70ed1affa9ecb..7e44d460c3641 100644 --- a/Telegram/SourceFiles/apiwrap.h +++ b/Telegram/SourceFiles/apiwrap.h @@ -34,6 +34,7 @@ class Forum; class ForumTopic; class Thread; class Story; +class SavedMessages; } // namespace Data namespace InlineBots { @@ -152,6 +153,7 @@ class ApiWrap final : public MTP::Sender { void savePinnedOrder(Data::Folder *folder); void savePinnedOrder(not_null forum); + void savePinnedOrder(not_null saved); void toggleHistoryArchived( not_null history, bool archived, diff --git a/Telegram/SourceFiles/boxes/background_box.cpp b/Telegram/SourceFiles/boxes/background_box.cpp index 0ae3023e0a99b..877cf556e6562 100644 --- a/Telegram/SourceFiles/boxes/background_box.cpp +++ b/Telegram/SourceFiles/boxes/background_box.cpp @@ -129,6 +129,8 @@ class BackgroundBox::Inner final : public Ui::RpWidget { int row) const; void validatePaperThumbnail(const Paper &paper) const; + [[nodiscard]] bool forChannel() const; + const not_null _session; PeerData * const _forPeer = nullptr; @@ -176,6 +178,23 @@ void BackgroundBox::prepare() { st::infoIconMediaPhoto, st::infoSharedMediaButtonIconPosition); + if (forChannel() && _forPeer->wallPaper()) { + const auto remove = container->add(object_ptr( + container, + tr::lng_settings_bg_remove(), + st::infoBlockButton)); + object_ptr( + remove, + st::infoIconDeleteRed, + st::infoSharedMediaButtonIconPosition); + + remove->setClickedCallback([=] { + if (const auto resolved = _inner->resolveResetCustomPaper()) { + chosen(*resolved); + } + }); + } + button->setClickedCallback([=] { chooseFromFile(); }); @@ -290,6 +309,23 @@ void BackgroundBox::chosen(const Data::WallPaper &paper) { closeBox(); } return; + } else if (forChannel()) { + if (_forPeer->wallPaper() && _forPeer->wallPaper()->equals(paper)) { + closeBox(); + return; + } + const auto &themes = _forPeer->owner().cloudThemes(); + for (const auto &theme : themes.chatThemes()) { + for (const auto &[type, themed] : theme.settings) { + if (themed.paper && themed.paper->equals(paper)) { + _controller->show(Box( + _controller, + Data::WallPaper::FromEmojiId(theme.emoticon), + BackgroundPreviewArgs{ _forPeer })); + return; + } + } + } } _controller->show(Box( _controller, @@ -316,6 +352,10 @@ void BackgroundBox::resetForPeer() { } } +bool BackgroundBox::forChannel() const { + return _forPeer && _forPeer->isChannel(); +} + void BackgroundBox::removePaper(const Data::WallPaper &paper) { const auto session = &_controller->session(); const auto remove = [=, weak = Ui::MakeWeak(this)](Fn &&close) { @@ -345,9 +385,16 @@ BackgroundBox::Inner::Inner( , _session(session) , _forPeer(forPeer) , _api(&_session->mtp()) -, _check(std::make_unique(st::overviewCheck, [=] { update(); })) { +, _check( + std::make_unique( + st::overviewCheck, + [=] { update(); })) { _check->setChecked(true, anim::type::instant); - resize(st::boxWideWidth, 2 * (st::backgroundSize.height() + st::backgroundPadding) + st::backgroundPadding); + resize( + st::boxWideWidth, + (2 * (st::backgroundSize.height() + st::backgroundPadding) + + st::backgroundPadding)); + Window::Theme::IsNightModeValue( ) | rpl::start_with_next([=] { updatePapers(); @@ -364,21 +411,31 @@ BackgroundBox::Inner::Inner( _check->invalidateCache(); }, lifetime()); - using Update = Window::Theme::BackgroundUpdate; - Window::Theme::Background()->updates( - ) | rpl::start_with_next([=](const Update &update) { - if (update.type == Update::Type::New) { - sortPapers(); - requestPapers(); - this->update(); - } - }, lifetime()); - + if (forChannel()) { + _session->data().cloudThemes().chatThemesUpdated( + ) | rpl::start_with_next([=] { + updatePapers(); + }, lifetime()); + } else { + using Update = Window::Theme::BackgroundUpdate; + Window::Theme::Background()->updates( + ) | rpl::start_with_next([=](const Update &update) { + if (update.type == Update::Type::New) { + sortPapers(); + requestPapers(); + this->update(); + } + }, lifetime()); + } setMouseTracking(true); } void BackgroundBox::Inner::requestPapers() { + if (forChannel()) { + _session->data().cloudThemes().refreshChatThemes(); + return; + } _api.request(MTPaccount_GetWallPapers( MTP_long(_session->data().wallpapersHash()) )).done([=](const MTPaccount_WallPapers &result) { @@ -395,7 +452,7 @@ auto BackgroundBox::Inner::resolveResetCustomPaper() const } const auto nonCustom = Window::Theme::Background()->paper(); const auto themeEmoji = _forPeer->themeEmoji(); - if (themeEmoji.isEmpty()) { + if (forChannel() || themeEmoji.isEmpty()) { return nonCustom; } const auto &themes = _forPeer->owner().cloudThemes(); @@ -443,6 +500,8 @@ void BackgroundBox::Inner::pushCustomPapers() { } void BackgroundBox::Inner::sortPapers() { + Expects(!forChannel()); + const auto currentCustom = _forPeer ? _forPeer->wallPaper() : nullptr; _currentId = currentCustom ? currentCustom->id() @@ -472,23 +531,60 @@ void BackgroundBox::Inner::sortPapers() { } void BackgroundBox::Inner::updatePapers() { - if (_session->data().wallpapers().empty()) { - return; + if (forChannel()) { + if (_session->data().cloudThemes().chatThemes().empty()) { + return; + } + } else { + if (_session->data().wallpapers().empty()) { + return; + } } _over = _overDown = Selection(); - _papers = _session->data().wallpapers( - ) | ranges::views::filter([&](const Data::WallPaper &paper) { - return (!paper.isPattern() || !paper.backgroundColors().empty()) - && (!_forPeer - || (!Data::IsDefaultWallPaper(paper) - && (Data::IsCloudWallPaper(paper) - || Data::IsCustomWallPaper(paper)))); - }) | ranges::views::transform([](const Data::WallPaper &paper) { - return Paper{ paper }; - }) | ranges::to_vector; - pushCustomPapers(); - sortPapers(); + const auto was = base::take(_papers); + if (forChannel()) { + const auto now = _forPeer->wallPaper(); + const auto &list = _session->data().cloudThemes().chatThemes(); + if (list.empty()) { + return; + } + using Type = Data::CloudThemeType; + const auto type = Window::Theme::IsNightMode() + ? Type::Dark + : Type::Light; + _papers.reserve(list.size() + 1); + const auto nowEmojiId = now ? now->emojiId() : QString(); + if (!now || !now->emojiId().isEmpty()) { + _papers.push_back({ Window::Theme::Background()->paper() }); + _currentId = _papers.back().data.id(); + } else { + _papers.push_back({ *now }); + _currentId = now->id(); + } + for (const auto &theme : list) { + const auto i = theme.settings.find(type); + if (i != end(theme.settings) && i->second.paper) { + _papers.push_back({ *i->second.paper }); + if (nowEmojiId == theme.emoticon) { + _currentId = _papers.back().data.id(); + } + } + } + } else { + _papers = _session->data().wallpapers( + ) | ranges::views::filter([&](const Data::WallPaper &paper) { + return (!paper.isPattern() || !paper.backgroundColors().empty()) + && (!_forPeer + || (!Data::IsDefaultWallPaper(paper) + && (Data::IsCloudWallPaper(paper) + || Data::IsCustomWallPaper(paper)))); + }) | ranges::views::transform([](const Data::WallPaper &paper) { + return Paper{ paper }; + }) | ranges::to_vector; + pushCustomPapers(); + sortPapers(); + } resizeToContentAndPreload(); } @@ -587,6 +683,10 @@ void BackgroundBox::Inner::validatePaperThumbnail( paper.thumbnail.setDevicePixelRatio(cRetinaFactor()); } +bool BackgroundBox::Inner::forChannel() const { + return _forPeer && _forPeer->isChannel(); +} + void BackgroundBox::Inner::paintPaper( QPainter &p, const Paper &paper, @@ -604,7 +704,8 @@ void BackgroundBox::Inner::paintPaper( const auto checkLeft = x + st::backgroundSize.width() - st::overviewCheckSkip - st::overviewCheck.size; const auto checkTop = y + st::backgroundSize.height() - st::overviewCheckSkip - st::overviewCheck.size; _check->paint(p, checkLeft, checkTop, width()); - } else if (Data::IsCloudWallPaper(paper.data) + } else if (!forChannel() + && Data::IsCloudWallPaper(paper.data) && !Data::IsDefaultWallPaper(paper.data) && !Data::IsLegacy2DefaultWallPaper(paper.data) && !Data::IsLegacy3DefaultWallPaper(paper.data) @@ -642,7 +743,8 @@ void BackgroundBox::Inner::mouseMoveEvent(QMouseEvent *e) { - st::stickerPanDeleteIconBg.width(); const auto deleteBottom = row * (height + skip) + skip + st::stickerPanDeleteIconBg.height(); - const auto inDelete = (x >= deleteLeft) + const auto inDelete = !forChannel() + && (x >= deleteLeft) && (y < deleteBottom) && Data::IsCloudWallPaper(data) && !Data::IsDefaultWallPaper(data) diff --git a/Telegram/SourceFiles/boxes/background_box.h b/Telegram/SourceFiles/boxes/background_box.h index 3fe16d60217d2..8aa63ff9164ab 100644 --- a/Telegram/SourceFiles/boxes/background_box.h +++ b/Telegram/SourceFiles/boxes/background_box.h @@ -38,6 +38,7 @@ class BackgroundBox : public Ui::BoxContent { const Data::WallPaper &paper) const; void removePaper(const Data::WallPaper &paper); void resetForPeer(); + [[nodiscard]] bool forChannel() const; void chooseFromFile(); diff --git a/Telegram/SourceFiles/boxes/background_preview_box.cpp b/Telegram/SourceFiles/boxes/background_preview_box.cpp index c54f581ced90d..db72d5690fb87 100644 --- a/Telegram/SourceFiles/boxes/background_preview_box.cpp +++ b/Telegram/SourceFiles/boxes/background_preview_box.cpp @@ -8,11 +8,13 @@ For license and copyright information please follow this link: #include "boxes/background_preview_box.h" #include "base/unixtime.h" +#include "boxes/peers/edit_peer_color_box.h" #include "boxes/premium_preview_box.h" #include "lang/lang_keys.h" #include "mainwidget.h" #include "window/themes/window_theme.h" #include "ui/boxes/confirm_box.h" +#include "ui/boxes/boost_box.h" #include "ui/controls/chat_service_checkbox.h" #include "ui/chat/chat_theme.h" #include "ui/chat/chat_style.h" @@ -29,6 +31,8 @@ For license and copyright information please follow this link: #include "history/history_item.h" #include "history/history_item_helpers.h" #include "history/view/history_view_message.h" +#include "main/main_account.h" +#include "main/main_app_config.h" #include "main/main_session.h" #include "apiwrap.h" #include "data/data_session.h" @@ -53,7 +57,6 @@ For license and copyright information please follow this link: namespace { constexpr auto kMaxWallPaperSlugLength = 255; -constexpr auto kDefaultDimming = 50; [[nodiscard]] bool IsValidWallPaperSlug(const QString &slug) { if (slug.isEmpty() || slug.size() > kMaxWallPaperSlugLength) { @@ -159,6 +162,25 @@ constexpr auto kDefaultDimming = 50; return result; } +[[nodiscard]] Data::WallPaper Resolve( + not_null session, + const Data::WallPaper &paper, + bool dark) { + if (paper.emojiId().isEmpty()) { + return paper; + } + const auto &themes = session->data().cloudThemes(); + if (const auto theme = themes.themeForEmoji(paper.emojiId())) { + using Type = Data::CloudThemeType; + const auto type = dark ? Type::Dark : Type::Light; + const auto i = theme->settings.find(type); + if (i != end(theme->settings) && i->second.paper) { + return *i->second.paper; + } + } + return paper; +} + } // namespace struct BackgroundPreviewBox::OverridenStyle { @@ -196,15 +218,17 @@ BackgroundPreviewBox::BackgroundPreviewBox( ? tr::lng_background_apply2(tr::now) : tr::lng_background_text2(tr::now)), true)) -, _paper(paper) +, _paperEmojiId(paper.emojiId()) +, _paper( + Resolve(&controller->session(), paper, Window::Theme::IsNightMode())) , _media(_paper.document() ? _paper.document()->createMediaView() : nullptr) , _radial([=](crl::time now) { radialAnimationCallback(now); }) , _appNightMode(Window::Theme::IsNightModeValue()) , _boxDarkMode(_appNightMode.current()) -, _dimmingIntensity(std::clamp(paper.patternIntensity(), 0, 100)) +, _dimmingIntensity(std::clamp(_paper.patternIntensity(), 0, 100)) , _dimmed(_forPeer - && (paper.document() || paper.localThumbnail()) - && !paper.isPattern()) { + && (_paper.document() || _paper.localThumbnail()) + && !_paper.isPattern()) { if (_media) { _media->thumbnailWanted(_paper.fileOrigin()); } @@ -244,7 +268,36 @@ BackgroundPreviewBox::BackgroundPreviewBox( BackgroundPreviewBox::~BackgroundPreviewBox() = default; +void BackgroundPreviewBox::recreate(bool dark) { + _paper = Resolve( + &_controller->session(), + Data::WallPaper::FromEmojiId(_paperEmojiId), + dark); + _media = _paper.document() + ? _paper.document()->createMediaView() + : nullptr; + if (_media) { + _media->thumbnailWanted(_paper.fileOrigin()); + } + _full = QImage(); + _generated = _scaled = _blurred = _fadeOutThumbnail = QPixmap(); + _generating = {}; + generateBackground(); + _paper.loadDocument(); + if (const auto document = _paper.document()) { + if (document->loading()) { + _radial.start(_media->progress()); + } + } + checkLoadedDocument(); + updateServiceBg(_paper.backgroundColors()); + update(); +} + void BackgroundPreviewBox::applyDarkMode(bool dark) { + if (!_paperEmojiId.isEmpty()) { + recreate(dark); + } const auto equals = (dark == Window::Theme::IsNightMode()); const auto &palette = (dark ? _darkPalette : _lightPalette); if (!equals && !palette) { @@ -410,6 +463,10 @@ auto BackgroundPreviewBox::prepareOverridenStyle(bool dark) return result; } +bool BackgroundPreviewBox::forChannel() const { + return _forPeer && _forPeer->isChannel(); +} + void BackgroundPreviewBox::generateBackground() { if (_paper.backgroundColors().empty()) { return; @@ -435,7 +492,9 @@ void BackgroundPreviewBox::resetTitle() { void BackgroundPreviewBox::rebuildButtons(bool dark) { clearButtons(); - addButton(_forPeer + addButton(forChannel() + ? tr::lng_background_apply_channel() + : _forPeer ? tr::lng_background_apply_button() : tr::lng_settings_apply(), [=] { apply(); }); addButton(tr::lng_cancel(), [=] { closeBox(); }); @@ -624,6 +683,36 @@ void BackgroundPreviewBox::setExistingForPeer( _controller->finishChatThemeEdit(_forPeer); } +void BackgroundPreviewBox::checkLevelForChannel() { + Expects(forChannel()); + + const auto show = _controller->uiShow(); + _forPeerLevelCheck = true; + const auto weak = Ui::MakeWeak(this); + CheckBoostLevel(show, _forPeer, [=](int level) { + if (!weak) { + return std::optional(); + } + const auto appConfig = &_forPeer->session().account().appConfig(); + const auto defaultRequired = appConfig->get( + "channel_wallpaper_level_min", + 9); + const auto customRequired = appConfig->get( + "channel_custom_wallpaper_level_min", + 10); + const auto required = _paperEmojiId.isEmpty() + ? customRequired + : defaultRequired; + if (level >= required) { + applyForPeer(false); + return std::optional(); + } + return std::make_optional(Ui::AskBoostReason{ + Ui::AskBoostWallpaper{ required } + }); + }, [=] { _forPeerLevelCheck = false; }); +} + void BackgroundPreviewBox::applyForPeer() { Expects(_forPeer != nullptr); @@ -636,105 +725,110 @@ void BackgroundPreviewBox::applyForPeer() { } } - if (!_fromMessageId && _forPeer->session().premiumPossible()) { - if (_forBothOverlay) { - return; - } - const auto size = this->size() * style::DevicePixelRatio(); - const auto bg = Images::DitherImage( - Images::BlurLargeImage( - Ui::GrabWidgetToImage(this).scaled( - size / style::ConvertScale(4), - Qt::IgnoreAspectRatio, - Qt::SmoothTransformation), - 24).scaled( - size, - Qt::IgnoreAspectRatio, - Qt::SmoothTransformation)); - - _forBothOverlay = std::make_unique>( - this, - object_ptr(this)); - const auto overlay = _forBothOverlay->entity(); - - sizeValue() | rpl::start_with_next([=](QSize size) { - _forBothOverlay->setGeometry({ QPoint(), size }); - overlay->setGeometry({ QPoint(), size }); - }, _forBothOverlay->lifetime()); - - overlay->paintRequest( - ) | rpl::start_with_next([=](QRect clip) { - auto p = QPainter(overlay); - p.drawImage(0, 0, bg); - p.fillRect(clip, QColor(0, 0, 0, 64)); - }, overlay->lifetime()); - - using namespace Ui; - const auto forMe = CreateChild( - overlay, - tr::lng_background_apply_me(), - st::backgroundConfirm); - forMe->setClickedCallback([=] { - applyForPeer(false); - }); - using namespace rpl::mappers; - const auto forBoth = ::Settings::CreateLockedButton( - overlay, - tr::lng_background_apply_both( - lt_user, - rpl::single(_forPeer->shortName())), - st::backgroundConfirm, - Data::AmPremiumValue(&_forPeer->session()) | rpl::map(!_1)); - forBoth->setClickedCallback([=] { - if (_forPeer->session().premium()) { - applyForPeer(true); - } else { - ShowPremiumPreviewBox( - _controller->uiShow(), - PremiumPreview::Wallpapers); - } - }); - const auto cancel = CreateChild( - overlay, - tr::lng_cancel(), - st::backgroundConfirmCancel); - cancel->setClickedCallback([=] { - const auto raw = _forBothOverlay.release(); - raw->shownValue() | rpl::filter( - !rpl::mappers::_1 - ) | rpl::take(1) | rpl::start_with_next(crl::guard(raw, [=] { - delete raw; - }), raw->lifetime()); - raw->toggle(false, anim::type::normal); - }); - forMe->setTextTransform(RoundButton::TextTransform::NoTransform); - forBoth->setTextTransform(RoundButton::TextTransform::NoTransform); - cancel->setTextTransform(RoundButton::TextTransform::NoTransform); - - overlay->sizeValue( - ) | rpl::start_with_next([=](QSize size) { - const auto padding = st::backgroundConfirmPadding; - const auto width = size.width() - - padding.left() - - padding.right(); - const auto height = cancel->height(); - auto top = size.height() - padding.bottom() - height; - cancel->setGeometry(padding.left(), top, width, height); - top -= height + padding.top(); - forBoth->setGeometry(padding.left(), top, width, height); - top -= height + padding.top(); - forMe->setGeometry(padding.left(), top, width, height); - }, _forBothOverlay->lifetime()); - - _forBothOverlay->hide(anim::type::instant); - _forBothOverlay->show(anim::type::normal); - } else { + if (forChannel()) { + checkLevelForChannel(); + return; + } else if (_fromMessageId || !_forPeer->session().premiumPossible()) { applyForPeer(false); + return; + } else if (_forBothOverlay) { + return; } + const auto size = this->size() * style::DevicePixelRatio(); + const auto bg = Images::DitherImage( + Images::BlurLargeImage( + Ui::GrabWidgetToImage(this).scaled( + size / style::ConvertScale(4), + Qt::IgnoreAspectRatio, + Qt::SmoothTransformation), + 24).scaled( + size, + Qt::IgnoreAspectRatio, + Qt::SmoothTransformation)); + + _forBothOverlay = std::make_unique>( + this, + object_ptr(this)); + const auto overlay = _forBothOverlay->entity(); + + sizeValue() | rpl::start_with_next([=](QSize size) { + _forBothOverlay->setGeometry({ QPoint(), size }); + overlay->setGeometry({ QPoint(), size }); + }, _forBothOverlay->lifetime()); + + overlay->paintRequest( + ) | rpl::start_with_next([=](QRect clip) { + auto p = QPainter(overlay); + p.drawImage(0, 0, bg); + p.fillRect(clip, QColor(0, 0, 0, 64)); + }, overlay->lifetime()); + + using namespace Ui; + const auto forMe = CreateChild( + overlay, + tr::lng_background_apply_me(), + st::backgroundConfirm); + forMe->setClickedCallback([=] { + applyForPeer(false); + }); + using namespace rpl::mappers; + const auto forBoth = ::Settings::CreateLockedButton( + overlay, + tr::lng_background_apply_both( + lt_user, + rpl::single(_forPeer->shortName())), + st::backgroundConfirm, + Data::AmPremiumValue(&_forPeer->session()) | rpl::map(!_1)); + forBoth->setClickedCallback([=] { + if (_forPeer->session().premium()) { + applyForPeer(true); + } else { + ShowPremiumPreviewBox( + _controller->uiShow(), + PremiumPreview::Wallpapers); + } + }); + const auto cancel = CreateChild( + overlay, + tr::lng_cancel(), + st::backgroundConfirmCancel); + cancel->setClickedCallback([=] { + const auto raw = _forBothOverlay.release(); + raw->shownValue() | rpl::filter( + !rpl::mappers::_1 + ) | rpl::take(1) | rpl::start_with_next(crl::guard(raw, [=] { + delete raw; + }), raw->lifetime()); + raw->toggle(false, anim::type::normal); + }); + forMe->setTextTransform(RoundButton::TextTransform::NoTransform); + forBoth->setTextTransform(RoundButton::TextTransform::NoTransform); + cancel->setTextTransform(RoundButton::TextTransform::NoTransform); + + overlay->sizeValue( + ) | rpl::start_with_next([=](QSize size) { + const auto padding = st::backgroundConfirmPadding; + const auto width = size.width() + - padding.left() + - padding.right(); + const auto height = cancel->height(); + auto top = size.height() - padding.bottom() - height; + cancel->setGeometry(padding.left(), top, width, height); + top -= height + padding.top(); + forBoth->setGeometry(padding.left(), top, width, height); + top -= height + padding.top(); + forMe->setGeometry(padding.left(), top, width, height); + }, _forBothOverlay->lifetime()); + + _forBothOverlay->hide(anim::type::instant); + _forBothOverlay->show(anim::type::normal); } void BackgroundPreviewBox::applyForPeer(bool both) { - if (Data::IsCustomWallPaper(_paper)) { + using namespace Data; + if (forChannel() && !_paperEmojiId.isEmpty()) { + setExistingForPeer(WallPaper::FromEmojiId(_paperEmojiId), both); + } else if (IsCustomWallPaper(_paper)) { uploadForPeer(both); } else { setExistingForPeer(_paper, both); @@ -855,7 +949,7 @@ int BackgroundPreviewBox::textsTop() const { - st::historyPaddingBottom - (_service ? _service->height() : 0) - _text1->height() - - _text2->height(); + - (forChannel() ? _text2->height() : 0); } QRect BackgroundPreviewBox::radialRect() const { @@ -885,10 +979,11 @@ void BackgroundPreviewBox::paintTexts(Painter &p, crl::time ms) { context.outbg = _text1->hasOutLayout(); _text1->draw(p, context); p.translate(0, height1); - - context.outbg = _text2->hasOutLayout(); - _text2->draw(p, context); - p.translate(0, height2); + if (!forChannel()) { + context.outbg = _text2->hasOutLayout(); + _text2->draw(p, context); + p.translate(0, height2); + } } void BackgroundPreviewBox::radialAnimationCallback(crl::time now) { @@ -988,7 +1083,9 @@ void BackgroundPreviewBox::updateServiceBg(const std::vector &bg) { _service = GenerateServiceItem( delegate(), _serviceHistory, - ((_forPeer && !_fromMessageId) + (forChannel() + ? tr::lng_background_other_channel(tr::now) + : (_forPeer && !_fromMessageId) ? tr::lng_background_other_info( tr::now, lt_user, diff --git a/Telegram/SourceFiles/boxes/background_preview_box.h b/Telegram/SourceFiles/boxes/background_preview_box.h index 8c1bd43462e0f..3eb06d1a502f5 100644 --- a/Telegram/SourceFiles/boxes/background_preview_box.h +++ b/Telegram/SourceFiles/boxes/background_preview_box.h @@ -94,18 +94,24 @@ class BackgroundPreviewBox void applyDarkMode(bool dark); [[nodiscard]] OverridenStyle prepareOverridenStyle(bool dark); + [[nodiscard]] bool forChannel() const; + void checkLevelForChannel(); + + void recreate(bool dark); void resetTitle(); void rebuildButtons(bool dark); void createDimmingSlider(bool dark); const not_null _controller; PeerData * const _forPeer = nullptr; + bool _forPeerLevelCheck = false; FullMsgId _fromMessageId; std::unique_ptr _chatStyle; const not_null _serviceHistory; AdminLog::OwnedItem _service; AdminLog::OwnedItem _text1; AdminLog::OwnedItem _text2; + QString _paperEmojiId; Data::WallPaper _paper; std::shared_ptr _media; QImage _full; diff --git a/Telegram/SourceFiles/boxes/connection_box.cpp b/Telegram/SourceFiles/boxes/connection_box.cpp index 144582a533fce..3bdaa932d9685 100644 --- a/Telegram/SourceFiles/boxes/connection_box.cpp +++ b/Telegram/SourceFiles/boxes/connection_box.cpp @@ -113,7 +113,8 @@ Base64UrlInput::Base64UrlInput( rpl::producer placeholder, const QString &val) : MaskedInputField(parent, st, std::move(placeholder), val) { - if (!QRegularExpression("^[a-zA-Z0-9_\\-]+$").match(val).hasMatch()) { + static const auto RegExp = QRegularExpression("^[a-zA-Z0-9_\\-]+$"); + if (!RegExp.match(val).hasMatch()) { setText(QString()); } } @@ -831,8 +832,9 @@ void ProxyBox::prepare() { connect(_host.data(), &HostInput::changed, [=] { Ui::PostponeCall(_host, [=] { const auto host = _host->getLastText().trimmed(); - static const auto mask = u"^\\d+\\.\\d+\\.\\d+\\.\\d+:(\\d*)$"_q; - const auto match = QRegularExpression(mask).match(host); + static const auto mask = QRegularExpression( + u"^\\d+\\.\\d+\\.\\d+\\.\\d+:(\\d*)$"_q); + const auto match = mask.match(host); if (_host->cursorPosition() == host.size() && match.hasMatch()) { const auto port = match.captured(1); @@ -1107,6 +1109,10 @@ void ProxiesBoxController::ShowApplyConfirmation( proxy.password = fields.value(u"secret"_q); } if (proxy) { + static const auto UrlStartRegExp = QRegularExpression( + "^https://", + QRegularExpression::CaseInsensitiveOption); + static const auto UrlEndRegExp = QRegularExpression("/$"); const auto displayed = "https://" + server + "/"; const auto parsed = QUrl::fromUserInput(displayed); const auto displayUrl = !UrlClickHandler::IsSuspicious(displayed) @@ -1117,11 +1123,9 @@ void ProxiesBoxController::ShowApplyConfirmation( const auto displayServer = QString( displayUrl ).replace( - QRegularExpression( - "^https://", - QRegularExpression::CaseInsensitiveOption), + UrlStartRegExp, QString() - ).replace(QRegularExpression("/$"), QString()); + ).replace(UrlEndRegExp, QString()); const auto text = tr::lng_sure_enable_socks( tr::now, lt_server, diff --git a/Telegram/SourceFiles/boxes/gift_premium_box.cpp b/Telegram/SourceFiles/boxes/gift_premium_box.cpp index 1244964d2e8c3..63603f6d21b2b 100644 --- a/Telegram/SourceFiles/boxes/gift_premium_box.cpp +++ b/Telegram/SourceFiles/boxes/gift_premium_box.cpp @@ -12,18 +12,26 @@ For license and copyright information please follow this link: #include "apiwrap.h" #include "base/unixtime.h" #include "base/weak_ptr.h" +#include "boxes/peer_list_controllers.h" // ContactsBoxController. #include "boxes/peers/prepare_short_info_box.h" +#include "boxes/peers/replace_boost_box.h" // BoostsForGift. +#include "boxes/premium_preview_box.h" // ShowPremiumPreviewBox. +#include "core/ui_integration.h" // Core::MarkedTextContext. #include "data/data_boosts.h" #include "data/data_changes.h" #include "data/data_channel.h" -#include "data/data_media_types.h" // Data::Giveaway +#include "data/data_media_types.h" // Data::GiveawayStart. #include "data/data_peer_values.h" // Data::PeerPremiumValue. #include "data/data_session.h" #include "data/data_subscription_option.h" #include "data/data_user.h" +#include "data/stickers/data_custom_emoji.h" +#include "info/boosts/giveaway/boost_badge.h" // InfiniteRadialAnimationWidget. #include "lang/lang_keys.h" #include "main/main_session.h" #include "mainwidget.h" +#include "payments/payments_checkout_process.h" +#include "payments/payments_form.h" #include "settings/settings_premium.h" #include "ui/basic_click_handlers.h" // UrlClickHandler::Open. #include "ui/boxes/boost_box.h" // StartFireworks. @@ -33,11 +41,14 @@ For license and copyright information please follow this link: #include "ui/effects/premium_top_bar.h" #include "ui/effects/spoiler_mess.h" #include "ui/layers/generic_box.h" +#include "ui/painter.h" #include "ui/rect.h" +#include "ui/vertical_list.h" #include "ui/text/text_utilities.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/gradient_round_button.h" #include "ui/wrap/padding_wrap.h" +#include "ui/wrap/slide_wrap.h" #include "ui/wrap/table_layout.h" #include "window/window_peer_menu.h" // ShowChooseRecipientBox. #include "window/window_session_controller.h" @@ -51,7 +62,7 @@ For license and copyright information please follow this link: namespace { -constexpr auto kDiscountDivider = 5.; +constexpr auto kUserpicsMax = size_t(3); using GiftOption = Data::SubscriptionOption; using GiftOptions = Data::SubscriptionOptions; @@ -72,6 +83,137 @@ GiftOptions GiftOptionFromTL(const MTPDuserFull &data) { return result; } +using TagUser1 = lngtag_user; +using TagUser2 = lngtag_second_user; +using TagUser3 = lngtag_name; +[[nodiscard]] rpl::producer ComplexAboutLabel( + const std::vector> &users, + tr::phrase phrase1, + tr::phrase phrase2, + tr::phrase phrase3, + tr::phrase phraseMore) { + Expects(!users.empty()); + + const auto count = users.size(); + const auto nameValue = [&](not_null user) { + return user->session().changes().peerFlagsValue( + user, + Data::PeerUpdate::Flag::Name + ) | rpl::map([=] { return TextWithEntities{ user->firstName }; }); + }; + if (count == 1) { + return phrase1( + lt_user, + nameValue(users.front()), + Ui::Text::RichLangValue); + } else if (count == 2) { + return phrase2( + lt_user, + nameValue(users.front()), + lt_second_user, + nameValue(users[1]), + Ui::Text::RichLangValue); + } else if (count == 3) { + return phrase3( + lt_user, + nameValue(users.front()), + lt_second_user, + nameValue(users[1]), + lt_name, + nameValue(users[2]), + Ui::Text::RichLangValue); + } else { + return phraseMore( + lt_count, + rpl::single(count - kUserpicsMax) | tr::to_count(), + lt_user, + nameValue(users.front()), + lt_second_user, + nameValue(users[1]), + lt_name, + nameValue(users[2]), + Ui::Text::RichLangValue); + } +} + +[[nodiscard]] not_null CircleBadge( + not_null parent, + const QString &text) { + const auto widget = Ui::CreateChild(parent.get()); + + const auto full = Rect(st::premiumGiftsUserpicBadgeSize); + const auto inner = full - Margins(st::premiumGiftsUserpicBadgeInner); + auto gradient = QLinearGradient( + QPointF(0, full.height()), + QPointF(full.width(), 0)); + gradient.setStops(Ui::Premium::GiftGradientStops()); + + widget->paintRequest( + ) | rpl::start_with_next([=] { + auto p = QPainter(widget); + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + p.setBrush(st::boxBg); + p.drawEllipse(full); + p.setPen(Qt::NoPen); + p.setBrush(gradient); + p.drawEllipse(inner); + p.setFont(st::premiumGiftsUserpicBadgeFont); + p.setPen(st::premiumButtonFg); + p.drawText(full, text, style::al_center); + }, widget->lifetime()); + widget->resize(full.size()); + return widget; +} + +[[nodiscard]] not_null UserpicsContainer( + not_null parent, + std::vector> users) { + Expects(!users.empty()); + + if (users.size() == 1) { + const auto userpic = Ui::CreateChild( + parent.get(), + users.front(), + st::defaultUserpicButton); + userpic->setAttribute(Qt::WA_TransparentForMouseEvents); + return userpic; + } + + const auto &singleSize = st::defaultUserpicButton.size; + + const auto container = Ui::CreateChild(parent.get()); + const auto single = singleSize.width(); + const auto shift = single - st::boostReplaceUserpicsShift; + const auto maxWidth = users.size() * (single - shift) + shift; + container->resize(maxWidth, singleSize.height()); + container->setAttribute(Qt::WA_TransparentForMouseEvents); + + const auto diff = (single - st::premiumGiftsUserpicButton.size.width()) + / 2; + for (auto i = 0; i < users.size(); i++) { + const auto bg = Ui::CreateChild(container); + bg->resize(singleSize); + bg->paintRequest( + ) | rpl::start_with_next([=] { + auto p = QPainter(bg); + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + p.setBrush(st::boxBg); + p.drawEllipse(bg->rect()); + }, bg->lifetime()); + bg->moveToLeft(std::max(0, i * (single - shift)), 0); + + const auto userpic = Ui::CreateChild( + bg, + users[i], + st::premiumGiftsUserpicButton); + userpic->moveToLeft(diff, diff); + } + + return container; +} + void GiftBox( not_null box, not_null controller, @@ -95,12 +237,11 @@ void GiftBox( + st::defaultUserpicButton.size.height())); using ColoredMiniStars = Ui::Premium::ColoredMiniStars; - const auto stars = box->lifetime().make_state(top, true); - - const auto userpic = Ui::CreateChild( + const auto stars = box->lifetime().make_state( top, - user, - st::defaultUserpicButton); + true); + + const auto userpic = UserpicsContainer(top, { user }); userpic->setAttribute(Qt::WA_TransparentForMouseEvents); top->widthValue( ) | rpl::start_with_next([=](int width) { @@ -211,7 +352,7 @@ void GiftBox( auto raw = Settings::CreateSubscribeButton({ controller, box, - [] { return QString("gift"); }, + [] { return u"gift"_q; }, state->buttonText.events(), Ui::Premium::GiftGradientStops(), [=] { @@ -222,10 +363,8 @@ void GiftBox( }, }); auto button = object_ptr::fromRaw(raw); - button->resizeToWidth(boxWidth - - stButton.buttonPadding.left() - - stButton.buttonPadding.right()); - box->setShowFinishedCallback([raw = button.data()]{ + button->resizeToWidth(boxWidth - rect::m::sum::h(stButton.buttonPadding)); + box->setShowFinishedCallback([raw = button.data()] { raw->startGlareAnimation(); }); box->addButton(std::move(button)); @@ -239,6 +378,302 @@ void GiftBox( }, box->lifetime()); } +void GiftsBox( + not_null box, + not_null controller, + std::vector> users, + not_null api) { + Expects(!users.empty()); + + const auto boxWidth = st::boxWideWidth; + box->setWidth(boxWidth); + box->setNoContentMargin(true); + const auto buttonsParent = box->verticalLayout().get(); + const auto session = &users.front()->session(); + + struct State { + rpl::event_stream buttonText; + rpl::variable confirmButtonBusy = false; + rpl::variable isPaymentComplete = false; + }; + const auto state = box->lifetime().make_state(); + + const auto userpicPadding = st::premiumGiftUserpicPadding; + const auto top = box->addRow(object_ptr( + buttonsParent, + userpicPadding.top() + + userpicPadding.bottom() + + st::defaultUserpicButton.size.height())); + + using ColoredMiniStars = Ui::Premium::ColoredMiniStars; + const auto stars = box->lifetime().make_state( + top, + true); + + const auto maxWithUserpic = std::min(users.size(), kUserpicsMax); + const auto userpics = UserpicsContainer( + top, + { users.begin(), users.begin() + maxWithUserpic }); + top->widthValue( + ) | rpl::start_with_next([=](int width) { + userpics->moveToLeft( + (width - userpics->width()) / 2, + userpicPadding.top()); + + const auto center = top->rect().center(); + const auto size = QSize( + userpics->width() * Ui::Premium::MiniStars::kSizeFactor, + userpics->height()); + const auto ministarsRect = QRect( + QPoint(center.x() - size.width(), center.y() - size.height()), + QPoint(center.x() + size.width(), center.y() + size.height())); + stars->setPosition(ministarsRect.topLeft()); + stars->setSize(ministarsRect.size()); + }, userpics->lifetime()); + if (const auto rest = users.size() - maxWithUserpic; rest > 0) { + const auto badge = CircleBadge( + userpics, + QChar('+') + QString::number(rest)); + badge->moveToRight(0, userpics->height() - badge->height()); + } + + top->paintRequest( + ) | rpl::start_with_next([=](const QRect &r) { + auto p = QPainter(top); + + p.fillRect(r, Qt::transparent); + stars->paint(p); + }, top->lifetime()); + + const auto close = Ui::CreateChild( + buttonsParent, + st::infoTopBarClose); + close->setClickedCallback([=] { box->closeBox(); }); + + buttonsParent->widthValue( + ) | rpl::start_with_next([=](int width) { + close->moveToRight(0, 0, width); + }, close->lifetime()); + + // Header. + const auto &padding = st::premiumGiftAboutPadding; + const auto available = boxWidth - padding.left() - padding.right(); + const auto &stTitle = st::premiumPreviewAboutTitle; + auto titleLabel = object_ptr( + box, + rpl::conditional( + state->isPaymentComplete.value(), + tr::lng_premium_gifts_about_paid_title(), + tr::lng_premium_gift_title()), + stTitle); + titleLabel->resizeToWidth(available); + box->addRow( + object_ptr>( + box, + std::move(titleLabel)), + st::premiumGiftTitlePadding); + + // About. + { + const auto emoji = Ui::Text::SingleCustomEmoji( + session->data().customEmojiManager().registerInternalEmoji( + st::premiumGiftsBoostIcon, + QMargins(0, st::premiumGiftsUserpicBadgeInner, 0, 0), + false)); + auto text = rpl::conditional( + state->isPaymentComplete.value(), + ComplexAboutLabel( + users, + tr::lng_premium_gifts_about_paid1, + tr::lng_premium_gifts_about_paid2, + tr::lng_premium_gifts_about_paid3, + tr::lng_premium_gifts_about_paid_more + ) | rpl::map([count = users.size()](TextWithEntities text) { + text.append('\n'); + text.append('\n'); + text.append(tr::lng_premium_gifts_about_paid_below( + tr::now, + lt_count, + float64(count), + Ui::Text::RichLangValue)); + return text; + }), + ComplexAboutLabel( + users, + tr::lng_premium_gifts_about_user1, + tr::lng_premium_gifts_about_user2, + tr::lng_premium_gifts_about_user3, + tr::lng_premium_gifts_about_user_more + ) | rpl::map([=, count = users.size()](TextWithEntities text) { + text.append('\n'); + text.append('\n'); + text.append(tr::lng_premium_gifts_about_reward( + tr::now, + lt_count, + count * BoostsForGift(session), + lt_emoji, + emoji, + Ui::Text::RichLangValue)); + return text; + }) + ); + const auto label = box->addRow( + object_ptr>( + box, + object_ptr(box, st::premiumPreviewAbout)), + padding)->entity(); + std::move( + text + ) | rpl::start_with_next([=](const TextWithEntities &t) { + using namespace Core; + label->setMarkedText(t, MarkedTextContext{ .session = session }); + }, label->lifetime()); + label->setTextColorOverride(stTitle.textFg->c); + label->resizeToWidth(available); + } + + // List. + const auto optionsContainer = buttonsParent->add( + object_ptr>( + buttonsParent, + object_ptr(buttonsParent))); + const auto options = api->options(users.size()); + const auto group = std::make_shared(); + const auto groupValueChangedCallback = [=](int value) { + Expects(value < options.size() && value >= 0); + auto text = tr::lng_premium_gift_button( + tr::now, + lt_cost, + options[value].costTotal); + state->buttonText.fire(std::move(text)); + }; + group->setChangedCallback(groupValueChangedCallback); + Ui::Premium::AddGiftOptions( + optionsContainer->entity(), + group, + options, + st::premiumGiftOption); + optionsContainer->toggleOn( + state->isPaymentComplete.value() | rpl::map(!rpl::mappers::_1), + anim::type::instant); + + // Summary. + { + { + // Will be hidden after payment. + const auto content = optionsContainer->entity(); + Ui::AddSkip(content); + Ui::AddDivider(content); + Ui::AddSkip(content); + Ui::AddSubsectionTitle( + content, + tr::lng_premium_gifts_summary_subtitle()); + } + const auto content = box->addRow( + object_ptr(box), + {}); + auto buttonCallback = [=](PremiumPreview section) { + stars->setPaused(true); + const auto previewBoxShown = [=]( + not_null previewBox) { + previewBox->boxClosing( + ) | rpl::start_with_next(crl::guard(box, [=] { + stars->setPaused(false); + }), previewBox->lifetime()); + }; + + ShowPremiumPreviewBox( + controller->uiShow(), + section, + previewBoxShown, + true); + }; + Settings::AddSummaryPremium( + content, + controller, + u"gift"_q, + std::move(buttonCallback)); + } + + // Footer. + { + box->addRow( + object_ptr( + box, + object_ptr( + box, + session->api().premium().statusTextValue(), // TODO. + st::premiumGiftTerms), + st::defaultBoxDividerLabelPadding), + {}); + } + + // Button. + const auto &stButton = st::premiumGiftBox; + box->setStyle(stButton); + auto raw = Settings::CreateSubscribeButton({ + controller, + box, + [] { return u"gift"_q; }, + rpl::combine( + state->buttonText.events(), + state->confirmButtonBusy.value(), + state->isPaymentComplete.value() + ) | rpl::map([](const QString &text, bool busy, bool paid) { + return busy + ? QString() + : paid + ? tr::lng_close(tr::now) + : text; + }), + Ui::Premium::GiftGradientStops(), + }); + raw->setClickedCallback([=] { + if (state->confirmButtonBusy.current()) { + return; + } + if (state->isPaymentComplete.current()) { + return box->closeBox(); + } + auto invoice = api->invoice( + users.size(), + api->monthsFromPreset(group->value())); + invoice.purpose = Payments::InvoicePremiumGiftCodeUsers{ users }; + + state->confirmButtonBusy = true; + const auto show = box->uiShow(); + const auto weak = Ui::MakeWeak(box.get()); + const auto done = [=](Payments::CheckoutResult result) { + if (const auto strong = weak.data()) { + strong->window()->setFocus(); + state->confirmButtonBusy = false; + if (result == Payments::CheckoutResult::Paid) { + state->isPaymentComplete = true; + Ui::StartFireworks(box->parentWidget()); + } + } + }; + + Payments::CheckoutProcess::Start(std::move(invoice), done); + }); + { + using namespace Info::Statistics; + const auto loadingAnimation = InfiniteRadialAnimationWidget( + raw, + raw->height() / 2); + AddChildToWidgetCenter(raw, loadingAnimation); + loadingAnimation->showOn(state->confirmButtonBusy.value()); + } + auto button = object_ptr::fromRaw(raw); + button->resizeToWidth(boxWidth - rect::m::sum::h(stButton.buttonPadding)); + box->setShowFinishedCallback([raw = button.data()] { + raw->startGlareAnimation(); + }); + box->addButton(std::move(button)); + + groupValueChangedCallback(0); +} + [[nodiscard]] Data::GiftCodeLink MakeGiftCodeLink( not_null session, const QString &slug) { @@ -370,18 +805,20 @@ void AddTable( container, st::giveawayGiftCodeTable), st::giveawayGiftCodeTableMargin); - AddTableRow( - table, - tr::lng_gift_link_label_from(), - controller, - current.from); - if (current.to) { + if (current.from) { + AddTableRow( + table, + tr::lng_gift_link_label_from(), + controller, + current.from); + } + if (current.from && current.to) { AddTableRow( table, tr::lng_gift_link_label_to(), controller, current.to); - } else { + } else if (current.from) { AddTableRow( table, tr::lng_gift_link_label_to(), @@ -394,7 +831,7 @@ void AddTable( lt_duration, GiftDurationValue(current.months) | Ui::Text::ToWithEntities(), Ui::Text::WithEntities)); - if (!skipReason) { + if (!skipReason && current.from) { const auto reason = AddTableRow( table, tr::lng_gift_link_label_reason(), @@ -439,6 +876,112 @@ void GiftPremiumValidator::cancel() { _requestId = 0; } +void GiftPremiumValidator::showChoosePeerBox() { + if (_manyGiftsLifetime) { + return; + } + using namespace Api; + const auto api = _manyGiftsLifetime.make_state( + _controller->session().user()); + const auto show = _controller->uiShow(); + api->request( + ) | rpl::start_with_error_done([=](const QString &error) { + show->showToast(error); + }, [=] { + const auto maxAmount = *ranges::max_element(api->availablePresets()); + + class Controller final : public ContactsBoxController { + public: + Controller( + not_null session, + Fn checkErrorCallback) + : ContactsBoxController(session) + , _checkErrorCallback(std::move(checkErrorCallback)) { + } + + protected: + std::unique_ptr createRow( + not_null user) override { + return !user->isSelf() + ? ContactsBoxController::createRow(user) + : nullptr; + } + + void rowClicked(not_null row) override { + const auto checked = !row->checked(); + if (checked + && _checkErrorCallback + && _checkErrorCallback( + delegate()->peerListSelectedRowsCount())) { + return; + } + delegate()->peerListSetRowChecked(row, checked); + } + + private: + const Fn _checkErrorCallback; + + }; + auto initBox = [=](not_null peersBox) { + const auto ignoreClose = peersBox->lifetime().make_state(0); + + auto process = [=] { + const auto selected = peersBox->collectSelectedRows(); + const auto users = ranges::views::all( + selected + ) | ranges::views::transform([](not_null p) { + return p->asUser(); + }) | ranges::views::filter([](UserData *u) -> bool { + return u; + }) | ranges::to>>(); + if (!users.empty()) { + const auto giftBox = show->show( + Box(GiftsBox, _controller, users, api)); + giftBox->boxClosing( + ) | rpl::start_with_next([=] { + _manyGiftsLifetime.destroy(); + }, giftBox->lifetime()); + } + (*ignoreClose) = true; + peersBox->closeBox(); + }; + + peersBox->setTitle(tr::lng_premium_gift_title()); + peersBox->addButton( + tr::lng_settings_gift_premium_users_confirm(), + std::move(process)); + peersBox->addButton(tr::lng_cancel(), [=] { + peersBox->closeBox(); + }); + peersBox->boxClosing( + ) | rpl::start_with_next([=] { + if (!(*ignoreClose)) { + _manyGiftsLifetime.destroy(); + } + }, peersBox->lifetime()); + }; + + auto listController = std::make_unique( + &_controller->session(), + [=](int count) { + if (count <= maxAmount) { + return false; + } + show->showToast(tr::lng_settings_gift_premium_users_error( + tr::now, + lt_count, + maxAmount)); + return true; + }); + show->showBox( + Box( + std::move(listController), + std::move(initBox)), + Ui::LayerOption::KeepOther); + + }, _manyGiftsLifetime); +} + void GiftPremiumValidator::showBox(not_null user) { if (_requestId) { return; @@ -723,10 +1266,22 @@ void GiftCodePendingBox( void ResolveGiftCode( not_null controller, - const QString &slug) { + const QString &slug, + PeerId fromId, + PeerId toId) { const auto done = [=](Api::GiftCode code) { + const auto session = &controller->session(); + const auto selfId = session->userPeerId(); if (!code) { controller->showToast(tr::lng_gift_link_expired(tr::now)); + } else if (!code.from && fromId == selfId) { + code.from = fromId; + code.to = toId; + const auto self = (fromId == selfId); + const auto peer = session->data().peer(self ? toId : fromId); + const auto months = code.months; + const auto parent = controller->parentController(); + Settings::ShowGiftPremium(parent, peer, months, self); } else { controller->uiShow()->showBox(Box(GiftCodeBox, controller, slug)); } @@ -739,8 +1294,11 @@ void ResolveGiftCode( void GiveawayInfoBox( not_null box, not_null controller, - Data::Giveaway giveaway, + std::optional start, + std::optional results, Api::GiveawayInfo info) { + Expects(start || results); + using State = Api::GiveawayState; const auto finished = (info.state == State::Finished) || (info.state == State::Refunded); @@ -749,10 +1307,31 @@ void GiveawayInfoBox( ? tr::lng_prizes_end_title : tr::lng_prizes_how_title)()); - const auto first = !giveaway.channels.empty() - ? giveaway.channels.front()->name() + const auto first = results + ? results->channel->name() + : !start->channels.empty() + ? start->channels.front()->name() : u"channel"_q; - auto text = (finished + auto text = TextWithEntities(); + + if (!info.giftCode.isEmpty()) { + text.append("\n\n"); + text.append(Ui::Text::Bold(tr::lng_prizes_you_won( + tr::now, + lt_cup, + QString::fromUtf8("\xf0\x9f\x8f\x86")))); + text.append("\n\n"); + } else if (info.state == State::Finished) { + text.append("\n\n"); + text.append(Ui::Text::Bold(tr::lng_prizes_you_didnt(tr::now))); + text.append("\n\n"); + } + + const auto quantity = start + ? start->quantity + : (results->winnersCount + results->unclaimedCount); + const auto months = start ? start->months : results->months; + text.append((finished ? tr::lng_prizes_end_text : tr::lng_prizes_how_text)( tr::now, @@ -760,18 +1339,21 @@ void GiveawayInfoBox( tr::lng_prizes_admins( tr::now, lt_count, - giveaway.quantity, + quantity, lt_channel, Ui::Text::Bold(first), lt_duration, - TextWithEntities{ GiftDuration(giveaway.months) }, + TextWithEntities{ GiftDuration(months) }, Ui::Text::RichLangValue), - Ui::Text::RichLangValue); - const auto many = (giveaway.channels.size() > 1); + Ui::Text::RichLangValue)); + const auto many = start + ? (start->channels.size() > 1) + : (results->additionalPeersCount > 0); const auto count = info.winnersCount ? info.winnersCount - : giveaway.quantity; - auto winners = giveaway.all + : quantity; + const auto all = start ? start->all : results->all; + auto winners = all ? (many ? tr::lng_prizes_winners_all_of_many : tr::lng_prizes_winners_all_of_one)( @@ -793,13 +1375,30 @@ void GiveawayInfoBox( Ui::Text::Bold( langDateTime(base::unixtime::parse(info.startDate))), Ui::Text::RichLangValue); + const auto additionalPrize = results + ? results->additionalPrize + : start->additionalPrize; + if (!additionalPrize.isEmpty()) { + text.append("\n\n").append(tr::lng_prizes_additional_added( + tr::now, + lt_count, + count, + lt_channel, + Ui::Text::Bold(first), + lt_prize, + TextWithEntities{ additionalPrize }, + Ui::Text::RichLangValue)); + } + const auto untilDate = start + ? start->untilDate + : results->untilDate; text.append("\n\n").append((finished ? tr::lng_prizes_end_when_finish : tr::lng_prizes_how_when_finish)( tr::now, lt_date, Ui::Text::Bold(langDayOfMonthFull( - base::unixtime::parse(giveaway.untilDate).date())), + base::unixtime::parse(untilDate).date())), lt_winners, winners, Ui::Text::RichLangValue)); @@ -810,17 +1409,9 @@ void GiveawayInfoBox( info.activatedCount, Ui::Text::RichLangValue)); } - if (!info.giftCode.isEmpty()) { - text.append("\n\n"); - text.append(tr::lng_prizes_you_won( - tr::now, - lt_cup, - QString::fromUtf8("\xf0\x9f\x8f\x86"))); - } else if (info.state == State::Finished) { - text.append("\n\n"); - text.append(tr::lng_prizes_you_didnt(tr::now)); - } else if (info.state == State::Preparing) { - + if (!info.giftCode.isEmpty() + || info.state == State::Finished + || info.state == State::Preparing) { } else if (info.state != State::Refunded) { if (info.adminChannelId) { const auto channel = controller->session().data().channel( @@ -858,7 +1449,7 @@ void GiveawayInfoBox( Ui::Text::Bold(first), lt_date, Ui::Text::Bold(langDayOfMonthFull( - base::unixtime::parse(giveaway.untilDate).date())), + base::unixtime::parse(untilDate).date())), Ui::Text::RichLangValue)); } } @@ -902,14 +1493,15 @@ void ResolveGiveawayInfo( not_null controller, not_null peer, MsgId messageId, - Data::Giveaway giveaway) { + std::optional start, + std::optional results) { const auto show = [=](Api::GiveawayInfo info) { if (!info) { controller->showToast( tr::lng_confirm_phone_link_invalid(tr::now)); } else { controller->uiShow()->showBox( - Box(GiveawayInfoBox, controller, giveaway, info)); + Box(GiveawayInfoBox, controller, start, results, info)); } }; controller->session().api().premium().resolveGiveawayInfo( diff --git a/Telegram/SourceFiles/boxes/gift_premium_box.h b/Telegram/SourceFiles/boxes/gift_premium_box.h index 1891ce5ff43fa..5a0bfb65967a6 100644 --- a/Telegram/SourceFiles/boxes/gift_premium_box.h +++ b/Telegram/SourceFiles/boxes/gift_premium_box.h @@ -16,7 +16,8 @@ struct GiftCode; } // namespace Api namespace Data { -struct Giveaway; +struct GiveawayStart; +struct GiveawayResults; } // namespace Data namespace Ui { @@ -33,6 +34,7 @@ class GiftPremiumValidator final { GiftPremiumValidator(not_null controller); void showBox(not_null user); + void showChoosePeerBox(); void cancel(); private: @@ -41,6 +43,8 @@ class GiftPremiumValidator final { mtpRequestId _requestId = 0; + rpl::lifetime _manyGiftsLifetime; + }; [[nodiscard]] rpl::producer GiftDurationValue(int months); @@ -56,10 +60,13 @@ void GiftCodePendingBox( const Api::GiftCode &data); void ResolveGiftCode( not_null controller, - const QString &slug); + const QString &slug, + PeerId fromId = 0, + PeerId toId = 0); void ResolveGiveawayInfo( not_null controller, not_null peer, MsgId messageId, - Data::Giveaway giveaway); + std::optional start, + std::optional results); diff --git a/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp b/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp index 6ccc564fc5e0d..7d4d744530a35 100644 --- a/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp @@ -40,11 +40,14 @@ class ChoosePeerBoxController final not_null navigation, not_null bot, RequestPeerQuery query, - Fn)> callback); + Fn>)> callback); Main::Session &session() const override; void rowClicked(not_null row) override; + [[nodiscard]] rpl::producer selectedCountValue() const; + void submit(); + QString savedMessagesChatStatus() const override { return tr::lng_saved_forward_here(tr::now); } @@ -60,7 +63,9 @@ class ChoosePeerBoxController final not_null _bot; RequestPeerQuery _query; base::flat_set> _commonGroups; - Fn)> _callback; + base::flat_set> _selected; + rpl::variable _selectedCount; + Fn>)> _callback; }; @@ -253,10 +258,10 @@ object_ptr CreatePeerByQueryBox( not_null navigation, not_null bot, RequestPeerQuery query, - Fn)> done) { + Fn>)> done) { const auto weak = std::make_shared>(); auto callback = [=](not_null peer) { - done(peer); + done({ peer }); if (const auto strong = weak->data()) { strong->closeBox(); } @@ -332,7 +337,7 @@ ChoosePeerBoxController::ChoosePeerBoxController( not_null navigation, not_null bot, RequestPeerQuery query, - Fn)> callback) + Fn>)> callback) : ChatsListBoxController(&navigation->session()) , _navigation(navigation) , _bot(bot) @@ -415,6 +420,8 @@ void ChoosePeerBoxController::prepareViewHook() { switch (_query.type) { case Type::User: return (_query.userIsBot == Restriction::Yes) ? tr::lng_request_bot_title() + : (_query.maxQuantity > 1) + ? tr::lng_request_users_title() : tr::lng_request_user_title(); case Type::Group: return tr::lng_request_group_title(); case Type::Broadcast: return tr::lng_request_channel_title(); @@ -425,10 +432,24 @@ void ChoosePeerBoxController::prepareViewHook() { } void ChoosePeerBoxController::rowClicked(not_null row) { + const auto limit = _query.maxQuantity; + const auto multiselect = (limit > 1); const auto peer = row->peer(); + if (multiselect) { + if (_selected.contains(peer) || _selected.size() < limit) { + delegate()->peerListSetRowChecked(row, !row->checked()); + if (row->checked()) { + _selected.emplace(peer); + } else { + _selected.remove(peer); + } + _selectedCount = int(_selected.size()); + } + return; + } const auto done = [callback = _callback, peer] { const auto onstack = callback; - onstack(peer); + onstack({ peer }); }; if (const auto user = peer->asUser()) { done(); @@ -438,6 +459,15 @@ void ChoosePeerBoxController::rowClicked(not_null row) { } } +rpl::producer ChoosePeerBoxController::selectedCountValue() const { + return _selectedCount.value(); +} + +void ChoosePeerBoxController::submit() { + const auto onstack = _callback; + onstack(ranges::to_vector(_selected)); +} + auto ChoosePeerBoxController::createRow(not_null history) -> std::unique_ptr { return FilterPeerByQuery(history->peer, _query, _commonGroups) @@ -474,7 +504,7 @@ void ShowChoosePeerBox( not_null navigation, not_null bot, RequestPeerQuery query, - Fn)> chosen) { + Fn>)> chosen) { const auto needCommonGroups = query.isBotParticipant && (query.type == RequestPeerQuery::Type::Group) && !query.myRights; @@ -488,22 +518,39 @@ void ShowChoosePeerBox( return; } const auto weak = std::make_shared>(); - auto initBox = [=](not_null box) { - box->addButton(tr::lng_cancel(), [box] { - box->closeBox(); - }); - }; - auto callback = [=, done = std::move(chosen)](not_null peer) { - done(peer); + auto callback = [=, done = std::move(chosen)]( + std::vector> peers) { + done(std::move(peers)); if (const auto strong = weak->data()) { strong->closeBox(); } }; + const auto limit = query.maxQuantity; + auto controller = std::make_unique( + navigation, + bot, + query, + std::move(callback)); + auto initBox = [=, ptr = controller.get()](not_null box) { + ptr->selectedCountValue() | rpl::start_with_next([=](int count) { + box->clearButtons(); + if (limit > 1) { + box->setAdditionalTitle(rpl::single(u"%1 / %2"_q.arg(count).arg(limit))); + } + if (count > 0) { + box->addButton(tr::lng_intro_submit(), [=] { + ptr->submit(); + if (*weak) { + (*weak)->closeBox(); + } + }); + } + box->addButton(tr::lng_cancel(), [box] { + box->closeBox(); + }); + }, box->lifetime()); + }; *weak = navigation->parentController()->show(Box( - std::make_unique( - navigation, - bot, - query, - std::move(callback)), + std::move(controller), std::move(initBox))); } diff --git a/Telegram/SourceFiles/boxes/peers/choose_peer_box.h b/Telegram/SourceFiles/boxes/peers/choose_peer_box.h index 2c9ab7a1d1f6d..982ec4784c9a2 100644 --- a/Telegram/SourceFiles/boxes/peers/choose_peer_box.h +++ b/Telegram/SourceFiles/boxes/peers/choose_peer_box.h @@ -21,4 +21,4 @@ void ShowChoosePeerBox( not_null navigation, not_null bot, RequestPeerQuery query, - Fn)> chosen); + Fn>)> chosen); diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp index fd3458c1daadf..38f064cb2d03e 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp @@ -9,12 +9,15 @@ For license and copyright information please follow this link: #include "apiwrap.h" #include "api/api_peer_colors.h" +#include "api/api_peer_photo.h" #include "base/unixtime.h" #include "boxes/peers/replace_boost_box.h" +#include "boxes/background_box.h" #include "chat_helpers/compose/compose_show.h" #include "data/data_changes.h" #include "data/data_channel.h" #include "data/stickers/data_custom_emoji.h" +#include "data/data_emoji_statuses.h" #include "data/data_peer.h" #include "data/data_session.h" #include "data/data_web_page.h" @@ -428,11 +431,17 @@ HistoryView::Context PreviewDelegate::elementContext() { return HistoryView::Context::AdminLog; } +struct SetValues { + uint8 colorIndex = 0; + DocumentId backgroundEmojiId = 0; + DocumentId statusId = 0; + TimeId statusUntil = 0; + bool statusChanged = false; +}; void Set( std::shared_ptr show, not_null peer, - uint8 colorIndex, - DocumentId backgroundEmojiId) { + SetValues values) { const auto wasIndex = peer->colorIndex(); const auto wasEmojiId = peer->backgroundEmojiId(); @@ -444,7 +453,7 @@ void Set( peer, UpdateFlag::Color | UpdateFlag::BackgroundEmoji); }; - setLocal(colorIndex, backgroundEmojiId); + setLocal(values.colorIndex, values.backgroundEmojiId); const auto done = [=] { show->showToast(peer->isSelf() @@ -452,8 +461,11 @@ void Set( : tr::lng_settings_color_changed_channel(tr::now)); }; const auto fail = [=](const MTP::Error &error) { - setLocal(wasIndex, wasEmojiId); - show->showToast(error.type()); + const auto type = error.type(); + if (type != u"CHAT_NOT_MODIFIED"_q) { + setLocal(wasIndex, wasEmojiId); + show->showToast(type); + } }; const auto send = [&](auto &&request) { peer->session().api().request( @@ -464,15 +476,23 @@ void Set( using Flag = MTPaccount_UpdateColor::Flag; send(MTPaccount_UpdateColor( MTP_flags(Flag::f_color | Flag::f_background_emoji_id), - MTP_int(colorIndex), - MTP_long(backgroundEmojiId))); + MTP_int(values.colorIndex), + MTP_long(values.backgroundEmojiId))); } else if (const auto channel = peer->asChannel()) { using Flag = MTPchannels_UpdateColor::Flag; send(MTPchannels_UpdateColor( - MTP_flags(Flag::f_background_emoji_id), + MTP_flags(Flag::f_color | Flag::f_background_emoji_id), channel->inputChannel, - MTP_int(colorIndex), - MTP_long(backgroundEmojiId))); + MTP_int(values.colorIndex), + MTP_long(values.backgroundEmojiId))); + + if (values.statusChanged + && (values.statusId || peer->emojiStatusId())) { + peer->owner().emojiStatuses().set( + channel, + values.statusId, + values.statusUntil); + } } else { Unexpected("Invalid peer type in Set(colorIndex)."); } @@ -481,13 +501,13 @@ void Set( void Apply( std::shared_ptr show, not_null peer, - uint8 colorIndex, - DocumentId backgroundEmojiId, + SetValues values, Fn close, Fn cancel) { const auto session = &peer->session(); - if (peer->colorIndex() == colorIndex - && peer->backgroundEmojiId() == backgroundEmojiId) { + if (peer->colorIndex() == values.colorIndex + && peer->backgroundEmojiId() == values.backgroundEmojiId + && !values.statusChanged) { close(); } else if (peer->isSelf() && !session->premium()) { Settings::ShowPremiumPromoToast( @@ -502,39 +522,45 @@ void Apply( u"name_color"_q); cancel(); } else if (peer->isSelf()) { - Set(show, peer, colorIndex, backgroundEmojiId); + Set(show, peer, values); close(); } else { - session->api().request(MTPpremium_GetBoostsStatus( - peer->input - )).done([=](const MTPpremium_BoostsStatus &result) { - const auto &data = result.data(); - const auto required = session->account().appConfig().get( - "channel_color_level_min", - 5); - if (data.vlevel().v >= required) { - Set(show, peer, colorIndex, backgroundEmojiId); + CheckBoostLevel(show, peer, [=](int level) { + const auto peerColors = &peer->session().api().peerColors(); + const auto colorRequired = peerColors->requiredLevelFor( + peer->id, + values.colorIndex); + const auto iconRequired = values.backgroundEmojiId + ? session->account().appConfig().get( + "channel_bg_icon_level_min", + 5) + : 0; + const auto statusRequired = (values.statusChanged + && values.statusId) + ? session->account().appConfig().get( + "channel_emoji_status_level_min", + 8) + : 0; + const auto required = std::max({ + colorRequired, + iconRequired, + statusRequired, + }); + if (level >= required) { + Set(show, peer, values); close(); - return; + return std::optional(); } - const auto openStatistics = [=] { - if (const auto controller = show->resolveWindow( - ChatHelpers::WindowUsage::PremiumPromo)) { - controller->showSection(Info::Boosts::Make(peer)); + const auto reason = [&]() -> Ui::AskBoostReason { + if (level < statusRequired) { + return { Ui::AskBoostEmojiStatus{ statusRequired } }; + } else if (level < iconRequired) { + return { Ui::AskBoostChannelColor{ iconRequired } }; } - }; - auto counters = ParseBoostCounters(result); - counters.mine = 0; // Don't show current level as just-reached. - show->show(Box(Ui::AskBoostBox, Ui::AskBoostBoxData{ - .link = qs(data.vboost_url()), - .boost = counters, - .reason = { Ui::AskBoostChannelColor{ required } }, - }, openStatistics, nullptr)); - cancel(); - }).fail([=](const MTP::Error &error) { - show->showToast(error.type()); - cancel(); - }).send(); + return { Ui::AskBoostChannelColor{ colorRequired } }; + }(); + return std::make_optional(reason); + }, cancel); } } @@ -672,15 +698,18 @@ int ColorSelector::resizeGetHeight(int newWidth) { const auto right = Ui::CreateChild(raw); right->show(); + using namespace Info::Profile; struct State { - Info::Profile::EmojiStatusPanel panel; + EmojiStatusPanel panel; std::unique_ptr emoji; DocumentId emojiId = 0; uint8 index = 0; }; const auto state = right->lifetime().make_state(); - state->panel.backgroundEmojiChosen( - ) | rpl::start_with_next(emojiIdChosen, raw->lifetime()); + state->panel.someCustomChosen( + ) | rpl::start_with_next([=](EmojiStatusPanel::CustomChosen chosen) { + emojiIdChosen(chosen.id); + }, raw->lifetime()); std::move(colorIndexValue) | rpl::start_with_next([=](uint8 index) { state->index = index; @@ -748,7 +777,7 @@ int ColorSelector::resizeGetHeight(int newWidth) { state->panel.show({ .controller = controller, .button = right, - .currentBackgroundEmojiId = state->emojiId, + .ensureAddedEmojiId = state->emojiId, .customTextColor = customTextColor, .backgroundEmojiMode = true, }); @@ -758,6 +787,108 @@ int ColorSelector::resizeGetHeight(int newWidth) { return result; } +[[nodiscard]] object_ptr CreateEmojiStatusButton( + not_null parent, + std::shared_ptr show, + rpl::producer statusIdValue, + Fn statusIdChosen) { + const auto &basicSt = st::settingsButtonNoIcon; + const auto ratio = style::DevicePixelRatio(); + const auto added = st::normalFont->spacew; + const auto emojiSize = Data::FrameSizeFromTag({}) / ratio; + const auto noneWidth = added + + st::normalFont->width(tr::lng_settings_color_emoji_off(tr::now)); + const auto emojiWidth = added + emojiSize; + const auto rightPadding = std::max(noneWidth, emojiWidth) + + basicSt.padding.right(); + const auto st = parent->lifetime().make_state( + basicSt); + st->padding.setRight(rightPadding); + auto result = object_ptr( + parent, + tr::lng_edit_channel_status(), + *st); + const auto raw = result.data(); + + const auto right = Ui::CreateChild(raw); + right->show(); + + using namespace Info::Profile; + struct State { + EmojiStatusPanel panel; + std::unique_ptr emoji; + DocumentId statusId = 0; + }; + const auto state = right->lifetime().make_state(); + state->panel.someCustomChosen( + ) | rpl::start_with_next([=](EmojiStatusPanel::CustomChosen chosen) { + statusIdChosen(chosen.id, chosen.until); + }, raw->lifetime()); + + const auto session = &show->session(); + std::move(statusIdValue) | rpl::start_with_next([=](DocumentId id) { + state->statusId = id; + state->emoji = id + ? session->data().customEmojiManager().create( + id, + [=] { right->update(); }) + : nullptr; + right->resize( + (id ? emojiWidth : noneWidth) + added, + right->height()); + right->update(); + }, right->lifetime()); + + rpl::combine( + raw->sizeValue(), + right->widthValue() + ) | rpl::start_with_next([=](QSize outer, int width) { + right->resize(width, outer.height()); + const auto skip = st::settingsButton.padding.right(); + right->moveToRight(skip - added, 0, outer.width()); + }, right->lifetime()); + + right->paintRequest( + ) | rpl::start_with_next([=] { + if (state->panel.paintBadgeFrame(right)) { + return; + } + auto p = QPainter(right); + const auto height = right->height(); + if (state->emoji) { + state->emoji->paint(p, { + .textColor = anim::color( + st::stickerPanPremium1, + st::stickerPanPremium2, + 0.5), + .position = QPoint(added, (height - emojiSize) / 2), + }); + } else { + const auto &font = st::normalFont; + p.setFont(font); + p.setPen(st::windowActiveTextFg); + p.drawText( + QPoint(added, (height - font->height) / 2 + font->ascent), + tr::lng_settings_color_emoji_off(tr::now)); + } + }, right->lifetime()); + + raw->setClickedCallback([=] { + const auto controller = show->resolveWindow( + ChatHelpers::WindowUsage::PremiumPromo); + if (controller) { + state->panel.show({ + .controller = controller, + .button = right, + .ensureAddedEmojiId = state->statusId, + .channelStatusMode = true, + }); + } + }); + + return result; +} + } // namespace void EditPeerColorBox( @@ -772,12 +903,16 @@ void EditPeerColorBox( struct State { rpl::variable index; rpl::variable emojiId; + rpl::variable statusId; + TimeId statusUntil = 0; + bool statusChanged = false; bool changing = false; bool applying = false; }; const auto state = box->lifetime().make_state(); state->index = peer->colorIndex(); state->emojiId = peer->backgroundEmojiId(); + state->statusId = peer->emojiStatusId(); box->addRow(object_ptr( box, @@ -820,14 +955,61 @@ void EditPeerColorBox( ? tr::lng_settings_color_emoji_about() : tr::lng_settings_color_emoji_about_channel()); + if (const auto channel = peer->asChannel()) { + Ui::AddSkip(container, st::settingsColorSampleSkip); + container->add(object_ptr( + container, + tr::lng_edit_channel_wallpaper(), + st::settingsButtonNoIcon) + )->setClickedCallback([=] { + const auto usage = ChatHelpers::WindowUsage::PremiumPromo; + if (const auto strong = show->resolveWindow(usage)) { + show->show(Box(strong, channel)); + } + }); + + Ui::AddSkip(container, st::settingsColorSampleSkip); + Ui::AddDividerText( + container, + tr::lng_edit_channel_wallpaper_about()); + + // Preload exceptions list. + const auto peerPhoto = &channel->session().api().peerPhoto(); + [[maybe_unused]] auto list = peerPhoto->emojiListValue( + Api::PeerPhoto::EmojiListType::NoChannelStatus + ); + + const auto statuses = &channel->owner().emojiStatuses(); + statuses->refreshChannelDefault(); + statuses->refreshChannelColored(); + + Ui::AddSkip(container, st::settingsColorSampleSkip); + container->add(CreateEmojiStatusButton( + container, + show, + state->statusId.value(), + [=](DocumentId id, TimeId until) { + state->statusId = id; + state->statusUntil = until; + state->statusChanged = true; + })); + + Ui::AddSkip(container, st::settingsColorSampleSkip); + Ui::AddDividerText(container, tr::lng_edit_channel_status_about()); + } + box->addButton(tr::lng_settings_apply(), [=] { if (state->applying) { return; } state->applying = true; - const auto index = state->index.current(); - const auto emojiId = state->emojiId.current(); - Apply(show, peer, index, emojiId, crl::guard(box, [=] { + Apply(show, peer, { + state->index.current(), + state->emojiId.current(), + state->statusId.current(), + state->statusUntil, + state->statusChanged, + }, crl::guard(box, [=] { box->closeBox(); }), crl::guard(box, [=] { state->applying = false; @@ -842,11 +1024,12 @@ void AddPeerColorButton( not_null container, std::shared_ptr show, not_null peer) { + auto label = peer->isSelf() + ? tr::lng_settings_theme_name_color() + : tr::lng_edit_channel_color(); const auto button = AddButtonWithIcon( container, - (peer->isSelf() - ? tr::lng_settings_theme_name_color() - : tr::lng_edit_channel_color()), + rpl::duplicate(label), st::settingsColorButton, { &st::menuIconChangeColors }); @@ -873,7 +1056,7 @@ void AddPeerColorButton( rpl::combine( button->widthValue(), - tr::lng_settings_theme_name_color(), + rpl::duplicate(label), rpl::duplicate(colorIndexValue) ) | rpl::start_with_next([=]( int width, @@ -920,3 +1103,39 @@ void AddPeerColorButton( show->show(Box(EditPeerColorBox, show, peer, style, theme)); }); } + +void CheckBoostLevel( + std::shared_ptr show, + not_null peer, + Fn(int level)> askMore, + Fn cancel) { + peer->session().api().request(MTPpremium_GetBoostsStatus( + peer->input + )).done([=](const MTPpremium_BoostsStatus &result) { + const auto &data = result.data(); + if (const auto channel = peer->asChannel()) { + channel->updateLevelHint(data.vlevel().v); + } + const auto reason = askMore(data.vlevel().v); + if (!reason) { + return; + } + const auto openStatistics = [=] { + if (const auto controller = show->resolveWindow( + ChatHelpers::WindowUsage::PremiumPromo)) { + controller->showSection(Info::Boosts::Make(peer)); + } + }; + auto counters = ParseBoostCounters(result); + counters.mine = 0; // Don't show current level as just-reached. + show->show(Box(Ui::AskBoostBox, Ui::AskBoostBoxData{ + .link = qs(data.vboost_url()), + .boost = counters, + .reason = *reason, + }, openStatistics, nullptr)); + cancel(); + }).fail([=](const MTP::Error &error) { + show->showToast(error.type()); + cancel(); + }).send(); +} diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.h b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.h index 68aecaab25684..6b72844494822 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.h +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.h @@ -16,6 +16,7 @@ class GenericBox; class ChatStyle; class ChatTheme; class VerticalLayout; +struct AskBoostReason; } // namespace Ui void EditPeerColorBox( @@ -29,3 +30,9 @@ void AddPeerColorButton( not_null container, std::shared_ptr show, not_null peer); + +void CheckBoostLevel( + std::shared_ptr show, + not_null peer, + Fn(int level)> askMore, + Fn cancel); diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp index 7b1010272e6ad..fba0c73034105 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp @@ -1294,6 +1294,9 @@ void Controller::editReactions() { _peer->input )).done([=](const MTPpremium_BoostsStatus &result) { _controls.levelRequested = false; + if (const auto channel = _peer->asChannel()) { + channel->updateLevelHint(result.data().vlevel().v); + } const auto link = qs(result.data().vboost_url()); const auto weak = base::make_weak(_navigation->parentController()); auto counters = ParseBoostCounters(result); diff --git a/Telegram/SourceFiles/boxes/premium_limits_box.cpp b/Telegram/SourceFiles/boxes/premium_limits_box.cpp index 64452a906e599..60eb5ecde3686 100644 --- a/Telegram/SourceFiles/boxes/premium_limits_box.cpp +++ b/Telegram/SourceFiles/boxes/premium_limits_box.cpp @@ -24,6 +24,7 @@ For license and copyright information please follow this link: #include "data/data_user.h" #include "data/data_channel.h" #include "data/data_forum.h" +#include "data/data_saved_messages.h" #include "data/data_session.h" #include "data/data_folder.h" #include "data/data_premium_limits.h" @@ -882,6 +883,18 @@ void PinsLimitBox( limits.dialogsPinnedPremium(), PinsCount(session->data().chatsList())); } +void SublistsPinsLimitBox( + not_null box, + not_null session) { + const auto limits = Data::PremiumLimits(session); + SimplePinsLimitBox( + box, + session, + "saved_dialog_pinned", + limits.savedSublistsPinnedDefault(), + limits.savedSublistsPinnedPremium(), + PinsCount(session->data().savedMessages().chatsList())); +} void ForumPinsLimitBox( not_null box, diff --git a/Telegram/SourceFiles/boxes/premium_limits_box.h b/Telegram/SourceFiles/boxes/premium_limits_box.h index 5a4fa8a0a88b1..8640dc235bf8b 100644 --- a/Telegram/SourceFiles/boxes/premium_limits_box.h +++ b/Telegram/SourceFiles/boxes/premium_limits_box.h @@ -60,6 +60,9 @@ void PinsLimitBox( void ForumPinsLimitBox( not_null box, not_null forum); +void SublistsPinsLimitBox( + not_null box, + not_null session); void CaptionLimitBox( not_null box, not_null session, diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.cpp b/Telegram/SourceFiles/boxes/premium_preview_box.cpp index 6cdb9a10aa671..06835d60372d8 100644 --- a/Telegram/SourceFiles/boxes/premium_preview_box.cpp +++ b/Telegram/SourceFiles/boxes/premium_preview_box.cpp @@ -51,8 +51,6 @@ For license and copyright information please follow this link: namespace { constexpr auto kPremiumShift = 21. / 240; -constexpr auto kReactionsPerRow = 5; -constexpr auto kDisabledOpacity = 0.5; constexpr auto kToggleStickerTimeout = 2 * crl::time(1000); constexpr auto kStarOpacityOff = 0.1; constexpr auto kStarOpacityOn = 1.; @@ -66,6 +64,7 @@ struct Descriptor { bool fromSettings = false; Fn hiddenCallback; Fn)> shownCallback; + bool hideSubscriptionButton = false; }; bool operator==(const Descriptor &a, const Descriptor &b) { @@ -1025,7 +1024,8 @@ void PreviewBox( state->preload(); } }; - if (descriptor.fromSettings && show->session().premium()) { + if ((descriptor.fromSettings && show->session().premium()) + || descriptor.hideSubscriptionButton) { box->setShowFinishedCallback(showFinished); box->addButton(tr::lng_close(), [=] { box->closeBox(); }); } else { @@ -1151,7 +1151,7 @@ void DecorateListPromoBox( box->boxClosing() | rpl::start_with_next(hidden, box->lifetime()); } - if (session->premium()) { + if (session->premium() || descriptor.hideSubscriptionButton) { box->addButton(tr::lng_close(), [=] { box->closeBox(); }); @@ -1286,10 +1286,12 @@ void ShowPremiumPreviewBox( void ShowPremiumPreviewBox( std::shared_ptr show, PremiumPreview section, - Fn)> shown) { + Fn)> shown, + bool hideSubscriptionButton) { Show(std::move(show), Descriptor{ .section = section, .shownCallback = std::move(shown), + .hideSubscriptionButton = hideSubscriptionButton, }); } diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.h b/Telegram/SourceFiles/boxes/premium_preview_box.h index 35c05571778b2..60c5699ca1ac0 100644 --- a/Telegram/SourceFiles/boxes/premium_preview_box.h +++ b/Telegram/SourceFiles/boxes/premium_preview_box.h @@ -73,7 +73,8 @@ void ShowPremiumPreviewBox( void ShowPremiumPreviewBox( std::shared_ptr show, PremiumPreview section, - Fn)> shown = nullptr); + Fn)> shown = nullptr, + bool hideSubscriptionButton = false); void ShowPremiumPreviewToBuy( not_null controller, diff --git a/Telegram/SourceFiles/boxes/sessions_box.cpp b/Telegram/SourceFiles/boxes/sessions_box.cpp index ec428042af606..4328fc4431d4f 100644 --- a/Telegram/SourceFiles/boxes/sessions_box.cpp +++ b/Telegram/SourceFiles/boxes/sessions_box.cpp @@ -211,7 +211,7 @@ void RenameBox(not_null box) { return Type::Other; } else if (const auto browser = detectBrowser()) { return *browser; - } else if (device.contains("iphone")) { + } else if (device.contains("iphone")) { return Type::iPhone; } else if (device.contains("ipad")) { return Type::iPad; @@ -221,9 +221,9 @@ void RenameBox(not_null box) { return *desktop; } else if (platform.contains("android") || system.contains("android")) { return Type::Android; - } else if (platform.contains("ios") || system.contains("ios")) { + } else if (platform.contains("ios") || system.contains("ios")) { return Type::iPhone; - } + } return Type::Other; } diff --git a/Telegram/SourceFiles/boxes/share_box.cpp b/Telegram/SourceFiles/boxes/share_box.cpp index 1ffcc1ddfdb89..d1b3dbb3775d2 100644 --- a/Telegram/SourceFiles/boxes/share_box.cpp +++ b/Telegram/SourceFiles/boxes/share_box.cpp @@ -848,7 +848,7 @@ void ShareBox::Inner::loadProfilePhotos(int yFrom) { if (!_chatsIndexed->empty()) { const auto index = yFrom / _rowHeight; auto i = _chatsIndexed->begin() - + std::min(index, _chatsIndexed->size());; + + std::min(index, _chatsIndexed->size()); for (auto end = _chatsIndexed->cend(); i != end; ++i) { if (((*i)->index() * _rowHeight) >= yTo) { break; diff --git a/Telegram/SourceFiles/boxes/translate_box.cpp b/Telegram/SourceFiles/boxes/translate_box.cpp index e0a760350d413..39380bd3ed500 100644 --- a/Telegram/SourceFiles/boxes/translate_box.cpp +++ b/Telegram/SourceFiles/boxes/translate_box.cpp @@ -305,7 +305,7 @@ bool SkipTranslate(TextWithEntities textWithEntities) { const auto skip = Core::App().settings().skipTranslationLanguages(); return result.known() && ranges::contains(skip, result); #else - return false; + return false; #endif } diff --git a/Telegram/SourceFiles/calls/calls_box_controller.cpp b/Telegram/SourceFiles/calls/calls_box_controller.cpp index 66a4e01f5b4a2..9eb53580f6235 100644 --- a/Telegram/SourceFiles/calls/calls_box_controller.cpp +++ b/Telegram/SourceFiles/calls/calls_box_controller.cpp @@ -520,6 +520,7 @@ void BoxController::loadMoreRows() { MTP_inputPeerEmpty(), MTP_string(), // q MTP_inputPeerEmpty(), + MTPInputPeer(), // saved_peer_id MTPint(), // top_msg_id MTP_inputMessagesFilterPhoneCalls(MTP_flags(0)), MTP_int(0), // min_date diff --git a/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.cpp b/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.cpp index 26a9cbed0df16..946c6dca8e9d3 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.cpp @@ -321,9 +321,6 @@ void Viewport::RendererGL::init( _frameBuffer->bind(); _frameBuffer->allocate(kValues * sizeof(GLfloat)); _downscaleProgram.yuv420.emplace(); - const auto downscaleVertexSource = VertexShader({ - VertexPassTextureCoord(), - }); _downscaleVertexShader = LinkProgram( &*_downscaleProgram.yuv420, VertexShader({ diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp index 9ae411d24b03e..2d27f1db47474 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp @@ -7,6 +7,8 @@ For license and copyright information please follow this link: */ #include "chat_helpers/emoji_list_widget.h" +#include "api/api_peer_photo.h" +#include "apiwrap.h" #include "base/unixtime.h" #include "ui/boxes/confirm_box.h" #include "ui/controls/tabbed_search.h" @@ -477,10 +479,23 @@ EmojiListWidget::EmojiListWidget( setAttribute(Qt::WA_OpaquePaintEvent); } - if (_mode != Mode::RecentReactions && _mode != Mode::BackgroundEmoji) { + if (_mode != Mode::RecentReactions + && _mode != Mode::BackgroundEmoji + && _mode != Mode::ChannelStatus) { setupSearch(); } + if (_mode == Mode::ChannelStatus) { + session().api().peerPhoto().emojiListValue( + Api::PeerPhoto::EmojiListType::NoChannelStatus + ) | rpl::start_with_next([=](const std::vector &list) { + _restrictedCustomList = { begin(list), end(list) }; + if (!_custom.empty()) { + refreshCustom(); + } + }, lifetime()); + } + _customSingleSize = Data::FrameSizeFromTag( Data::CustomEmojiManager::SizeTag::Large ) / style::DevicePixelRatio(); @@ -1034,7 +1049,9 @@ void EmojiListWidget::fillRecentFrom(const std::vector &list) { if (!id && _mode == Mode::EmojiStatus) { const auto star = QString::fromUtf8("\xe2\xad\x90\xef\xb8\x8f"); _recent.push_back({ .id = { Ui::Emoji::Find(star) } }); - } else if (!id && _mode == Mode::BackgroundEmoji) { + } else if (!id + && (_mode == Mode::BackgroundEmoji + || _mode == Mode::ChannelStatus)) { const auto fakeId = DocumentId(5246772116543512028ULL); const auto no = QString::fromUtf8("\xe2\x9b\x94\xef\xb8\x8f"); _recent.push_back({ @@ -1070,7 +1087,7 @@ base::unique_qptr EmojiListWidget::fillContextMenu( : st::defaultPopupMenu)); if (_mode == Mode::Full) { fillRecentMenu(menu, section, index); - } else if (_mode == Mode::EmojiStatus) { + } else if (_mode == Mode::EmojiStatus || _mode == Mode::ChannelStatus) { fillEmojiStatusMenu(menu, section, index); } if (menu->empty()) { @@ -1205,7 +1222,7 @@ void EmojiListWidget::validateEmojiPaintContext( auto value = Ui::Text::CustomEmojiPaintContext{ .textColor = (_customTextColor ? _customTextColor() - : (_mode == Mode::EmojiStatus) + : (_mode == Mode::EmojiStatus || _mode == Mode::ChannelStatus) ? anim::color( st::stickerPanPremium1, st::stickerPanPremium2, @@ -1402,6 +1419,10 @@ void EmojiListWidget::drawRecent( _emojiPaintContext->position = position + _innerPosition + _customPosition; + if (_mode == Mode::ChannelStatus) { + _emojiPaintContext->internal.forceFirstFrame + = (recent.id == _recent.front().id); + } custom->paint(p, *_emojiPaintContext); } else if (const auto emoji = std::get_if(&recent.id.data)) { if (_mode == Mode::EmojiStatus) { @@ -1642,6 +1663,7 @@ void EmojiListWidget::mouseReleaseEvent(QMouseEvent *e) { Settings::ShowPremium(resolved, u"infinite_reactions"_q); break; case Mode::EmojiStatus: + case Mode::ChannelStatus: Settings::ShowPremium(resolved, u"emoji_status"_q); break; case Mode::TopicIcon: @@ -2018,8 +2040,9 @@ void EmojiListWidget::refreshCustom() { auto it = sets.find(setId); if (it == sets.cend() || it->second->stickers.isEmpty() - || (_mode == Mode::BackgroundEmoji - && !it->second->textColor())) { + || (_mode == Mode::BackgroundEmoji && !it->second->textColor()) + || (_mode == Mode::ChannelStatus + && !it->second->channelStatus())) { return; } const auto canRemove = !!(it->second->flags @@ -2070,7 +2093,9 @@ void EmojiListWidget::refreshCustom() { auto set = std::vector(); set.reserve(list.size()); for (const auto document : list) { - if (const auto sticker = document->sticker()) { + if (_restrictedCustomList.contains(document->id)) { + continue; + } else if (const auto sticker = document->sticker()) { set.push_back({ .custom = resolveCustomEmoji(document, setId), .document = document, diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h index 5735d7a55d381..c2a8ef3ed0287 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h @@ -71,6 +71,7 @@ enum class EmojiListMode { Full, TopicIcon, EmojiStatus, + ChannelStatus, FullReactions, RecentReactions, UserpicBuilder, @@ -384,6 +385,7 @@ class EmojiListWidget final bool _grabbingChosen = false; QVector _emoji[kEmojiSectionCount]; std::vector _custom; + base::flat_set _restrictedCustomList; base::flat_map _customEmoji; base::flat_map< DocumentId, diff --git a/Telegram/SourceFiles/chat_helpers/stickers_dice_pack.cpp b/Telegram/SourceFiles/chat_helpers/stickers_dice_pack.cpp index dcebbfdc42e62..69aaddee1ed9f 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_dice_pack.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_dice_pack.cpp @@ -26,6 +26,7 @@ const QString DicePacks::kDartString = QString::fromUtf8("\xF0\x9F\x8E\xAF"); const QString DicePacks::kSlotString = QString::fromUtf8("\xF0\x9F\x8E\xB0"); const QString DicePacks::kFballString = QString::fromUtf8("\xE2\x9A\xBD"); const QString DicePacks::kBballString = QString::fromUtf8("\xF0\x9F\x8F\x80"); +const QString DicePacks::kPartyPopper = QString::fromUtf8("\xf0\x9f\x8e\x89"); DicePack::DicePack(not_null session, const QString &emoji) : _session(session) @@ -35,7 +36,7 @@ DicePack::DicePack(not_null session, const QString &emoji) DicePack::~DicePack() = default; DocumentData *DicePack::lookup(int value) { - if (!_requestId) { + if (!_requestId && _emoji != DicePacks::kPartyPopper) { load(); } tryGenerateLocalZero(); @@ -117,6 +118,8 @@ void DicePack::tryGenerateLocalZero() { generateLocal(8, u"slot_0_idle"_q); generateLocal(14, u"slot_1_idle"_q); generateLocal(20, u"slot_2_idle"_q); + } else if (_emoji == DicePacks::kPartyPopper) { + generateLocal(0, u"winners"_q); } } diff --git a/Telegram/SourceFiles/chat_helpers/stickers_dice_pack.h b/Telegram/SourceFiles/chat_helpers/stickers_dice_pack.h index 53564363a9425..3a1c49f46ad57 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_dice_pack.h +++ b/Telegram/SourceFiles/chat_helpers/stickers_dice_pack.h @@ -44,6 +44,7 @@ class DicePacks final { static const QString kSlotString; static const QString kFballString; static const QString kBballString; + static const QString kPartyPopper; [[nodiscard]] static bool IsSlot(const QString &emoji) { return (emoji == kSlotString); diff --git a/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp b/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp index 9d6f669cddde6..6fc2c555ad8ee 100644 --- a/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp +++ b/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp @@ -332,6 +332,7 @@ TabbedSelector::TabbedSelector( : TabbedSelector(parent, { .show = std::move(show), .st = ((mode == Mode::EmojiStatus + || mode == Mode::ChannelStatus || mode == Mode::BackgroundEmoji || mode == Mode::FullReactions) ? st::statusEmojiPan @@ -521,6 +522,8 @@ TabbedSelector::Tab TabbedSelector::createTab(SelectorTab type, int index) { .show = _show, .mode = (_mode == Mode::EmojiStatus ? EmojiMode::EmojiStatus + : _mode == Mode::ChannelStatus + ? EmojiMode::ChannelStatus : _mode == Mode::BackgroundEmoji ? EmojiMode::BackgroundEmoji : _mode == Mode::FullReactions diff --git a/Telegram/SourceFiles/chat_helpers/tabbed_selector.h b/Telegram/SourceFiles/chat_helpers/tabbed_selector.h index c70118df2e7f4..3b2f45d5dc792 100644 --- a/Telegram/SourceFiles/chat_helpers/tabbed_selector.h +++ b/Telegram/SourceFiles/chat_helpers/tabbed_selector.h @@ -81,6 +81,7 @@ enum class TabbedSelectorMode { EmojiOnly, MediaEditor, EmojiStatus, + ChannelStatus, BackgroundEmoji, FullReactions, RecentReactions, diff --git a/Telegram/SourceFiles/core/application.cpp b/Telegram/SourceFiles/core/application.cpp index a25bf54c7a932..3992469730c5a 100644 --- a/Telegram/SourceFiles/core/application.cpp +++ b/Telegram/SourceFiles/core/application.cpp @@ -428,11 +428,12 @@ void Application::showOpenGLCrashNotification() { Local::writeSettings(); Restart(); }; - const auto keepDisabled = [=] { + const auto keepDisabled = [=](Fn close) { Ui::GL::ForceDisable(true); Ui::GL::CrashCheckFinish(); settings().setDisableOpenGL(true); Local::writeSettings(); + close(); }; _lastActivePrimaryWindow->show(Ui::MakeConfirmBox({ .text = "" @@ -682,7 +683,8 @@ bool Application::eventFilter(QObject *object, QEvent *e) { } break; case QEvent::ThemeChange: { - if (Platform::IsLinux() && object == QGuiApplication::allWindows().first()) { + if (Platform::IsLinux() + && object == QGuiApplication::allWindows().constFirst()) { Core::App().refreshApplicationIcon(); Core::App().tray().updateIconCounters(); } diff --git a/Telegram/SourceFiles/core/launcher.cpp b/Telegram/SourceFiles/core/launcher.cpp index b171b070e10fb..2765fe3ed9394 100644 --- a/Telegram/SourceFiles/core/launcher.cpp +++ b/Telegram/SourceFiles/core/launcher.cpp @@ -499,7 +499,7 @@ uint64 Launcher::installationTag() const { } void Launcher::processArguments() { - enum class KeyFormat { + enum class KeyFormat { NoValues, OneValue, AllLeftValues, @@ -542,9 +542,13 @@ void Launcher::processArguments() { } } + static const auto RegExp = QRegularExpression("[^a-z0-9\\-_]"); gDebugMode = parseResult.contains("-debug"); - gKeyFile = parseResult.value("-key", {}).join(QString()).toLower(); - gKeyFile = gKeyFile.replace(QRegularExpression("[^a-z0-9\\-_]"), {}); + gKeyFile = parseResult + .value("-key", {}) + .join(QString()) + .toLower() + .replace(RegExp, {}); gLaunchMode = parseResult.contains("-autostart") ? LaunchModeAutoStart : parseResult.contains("-fixprevious") ? LaunchModeFixPrevious : parseResult.contains("-cleanup") ? LaunchModeCleanup diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp index 2d5f882832313..9bfcedd3dd4d9 100644 --- a/Telegram/SourceFiles/core/local_url_handlers.cpp +++ b/Telegram/SourceFiles/core/local_url_handlers.cpp @@ -344,9 +344,19 @@ bool ResolveUsernameOrPhone( qthelp::UrlParamNameTransform::ToLower); const auto domainParam = params.value(u"domain"_q); const auto appnameParam = params.value(u"appname"_q); + const auto myContext = context.value(); if (domainParam == u"giftcode"_q && !appnameParam.isEmpty()) { - ResolveGiftCode(controller, appnameParam); + const auto itemId = myContext.itemId; + const auto item = controller->session().data().message(itemId); + const auto fromId = item ? item->from()->id : PeerId(); + const auto selfId = controller->session().userPeerId(); + const auto toId = !item + ? PeerId() + : (fromId == selfId) + ? item->history()->peer->id + : selfId; + ResolveGiftCode(controller, appnameParam, fromId, toId); return true; } @@ -413,7 +423,6 @@ bool ResolveUsernameOrPhone( startToken = params.value(u"startapp"_q); } } - const auto myContext = context.value(); controller->window().activate(); controller->showPeerByLink(Window::PeerByLinkInfo{ .usernameOrId = domain, @@ -732,7 +741,8 @@ void ExportTestChatTheme( MTP_int(color(bg.size() > 2 ? bg[2] : Qt::black)), MTP_int(color(bg.size() > 3 ? bg[3] : Qt::black)), MTP_int(fields.paper->patternIntensity()), - MTP_int(0))); + MTP_int(0), // rotation + MTPstring())); // emoticon }; const auto light = inputSettings(Data::CloudThemeType::Light); if (!light) { @@ -1082,7 +1092,7 @@ QString TryConvertUrlToLocal(QString url) { const auto base = u"tg://privatepost?channel="_q + channel; auto added = QString(); if (const auto threadPostMatch = regex_match(u"^/(\\d+)/(\\d+)(/?\\?|/?$)"_q, privateMatch->captured(2))) { - added = u"&topic=%1&post=%2"_q.arg(threadPostMatch->captured(1)).arg(threadPostMatch->captured(2)); + added = u"&topic=%1&post=%2"_q.arg(threadPostMatch->captured(1), threadPostMatch->captured(2)); } else if (const auto postMatch = regex_match(u"^/(\\d+)(/?\\?|/?$)"_q, privateMatch->captured(2))) { added = u"&post="_q + postMatch->captured(1); } @@ -1112,7 +1122,7 @@ QString TryConvertUrlToLocal(QString url) { const auto base = u"tg://resolve?domain="_q + url_encode(usernameMatch->captured(1)); auto added = QString(); if (const auto threadPostMatch = regex_match(u"^/(\\d+)/(\\d+)(/?\\?|/?$)"_q, usernameMatch->captured(2))) { - added = u"&topic=%1&post=%2"_q.arg(threadPostMatch->captured(1)).arg(threadPostMatch->captured(2)); + added = u"&topic=%1&post=%2"_q.arg(threadPostMatch->captured(1), threadPostMatch->captured(2)); } else if (const auto postMatch = regex_match(u"^/(\\d+)(/?\\?|/?$)"_q, usernameMatch->captured(2))) { added = u"&post="_q + postMatch->captured(1); } else if (const auto storyMatch = regex_match(u"^/s/(\\d+)(/?\\?|/?$)"_q, usernameMatch->captured(2))) { diff --git a/Telegram/SourceFiles/core/sandbox.cpp b/Telegram/SourceFiles/core/sandbox.cpp index 0b92376cd6b34..6feacdb5bd968 100644 --- a/Telegram/SourceFiles/core/sandbox.cpp +++ b/Telegram/SourceFiles/core/sandbox.cpp @@ -21,7 +21,6 @@ For license and copyright information please follow this link: #include "core/local_url_handlers.h" #include "core/update_checker.h" #include "core/deadlock_detector.h" -#include "base/options.h" #include "base/timer.h" #include "base/concurrent_timer.h" #include "base/invoke_queued.h" @@ -38,24 +37,6 @@ For license and copyright information please follow this link: namespace Core { namespace { -base::options::toggle OptionForceWaylandFractionalScaling({ - .id = kOptionForceWaylandFractionalScaling, - .name = "Enable xdg-output fractional scaling", - .description = "Enable xdg-output based fractional scaling on Wayland. " - "This works without fractional-scale-v1 and without " - "precise High DPI scaling. " - "Requires Qt with Desktop App Toolkit patches.", - .defaultValue = true, - .scope = [] { -#ifdef DESKTOP_APP_QT_PATCHED - return Platform::IsWayland(); -#else // DESKTOP_APP_QT_PATCHED - return false; -#endif // !DESKTOP_APP_QT_PATCHED - }, - .restartRequired = true, -}); - QChar _toHex(ushort v) { v = v & 0x000F; return QChar::fromLatin1((v >= 10) ? ('a' + (v - 10)) : ('0' + v)); @@ -96,17 +77,12 @@ QString _escapeFrom7bit(const QString &str) { } // namespace -const char kOptionForceWaylandFractionalScaling[] = "force-wayland-fractional-scaling"; - bool Sandbox::QuitOnStartRequested = false; Sandbox::Sandbox(int &argc, char **argv) : QApplication(argc, argv) , _mainThreadId(QThread::currentThreadId()) { setQuitOnLastWindowClosed(false); - if (OptionForceWaylandFractionalScaling.value()) { - setProperty("_q_force_wayland_fractional_scale", true); - } } int Sandbox::start() { @@ -240,7 +216,7 @@ void Sandbox::setupScreenScale() { const auto logEnv = [](const char *name) { const auto value = qEnvironmentVariable(name); if (!value.isEmpty()) { - LOG(("%1: %2").arg(name).arg(value)); + LOG(("%1: %2").arg(name, value)); } }; logEnv("QT_DEVICE_PIXEL_RATIO"); diff --git a/Telegram/SourceFiles/core/sandbox.h b/Telegram/SourceFiles/core/sandbox.h index 39d16296c78bb..4c15c2828bfe1 100644 --- a/Telegram/SourceFiles/core/sandbox.h +++ b/Telegram/SourceFiles/core/sandbox.h @@ -19,8 +19,6 @@ class QLockFile; namespace Core { -extern const char kOptionForceWaylandFractionalScaling[]; - class UpdateChecker; class Application; diff --git a/Telegram/SourceFiles/core/update_checker.cpp b/Telegram/SourceFiles/core/update_checker.cpp index 43cbbdaeb1bf7..fe95f60dbc8f1 100644 --- a/Telegram/SourceFiles/core/update_checker.cpp +++ b/Telegram/SourceFiles/core/update_checker.cpp @@ -241,7 +241,7 @@ QString FindUpdateFile() { } const auto list = updates.entryInfoList(QDir::Files); for (const auto &info : list) { - if (QRegularExpression( + static const auto RegExp = QRegularExpression( "^(" "tupdate|" "tx64upd|" @@ -250,7 +250,8 @@ QString FindUpdateFile() { "tlinuxupd|" ")\\d+(_[a-z\\d]+)?$", QRegularExpression::CaseInsensitiveOption - ).match(info.fileName()).hasMatch()) { + ); + if (RegExp.match(info.fileName()).hasMatch()) { return info.absoluteFilePath(); } } diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h index a5ae5ed6cbacd..c6c44f6662ce1 100644 --- a/Telegram/SourceFiles/core/version.h +++ b/Telegram/SourceFiles/core/version.h @@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D1ED}"_cs; constexpr auto AppNameOld = "Telegram Win (Unofficial)"_cs; constexpr auto AppName = "Telegram Desktop"_cs; constexpr auto AppFile = "Telegram"_cs; -constexpr auto AppVersion = 4012002; -constexpr auto AppVersionStr = "4.12.2"; +constexpr auto AppVersion = 4014000; +constexpr auto AppVersionStr = "4.14"; constexpr auto AppBetaVersion = false; constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION; diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp index 6f25e02c8a764..fecbe62877599 100644 --- a/Telegram/SourceFiles/data/data_channel.cpp +++ b/Telegram/SourceFiles/data/data_channel.cpp @@ -19,6 +19,7 @@ For license and copyright information please follow this link: #include "data/data_histories.h" #include "data/data_group_call.h" #include "data/data_message_reactions.h" +#include "data/data_wall_paper.h" #include "data/notify/data_notify_settings.h" #include "main/main_session.h" #include "main/session/send_as_peers.h" @@ -948,6 +949,14 @@ void ChannelData::processTopics(const MTPVector &topics) { } } +int ChannelData::levelHint() const { + return _levelHint; +} + +void ChannelData::updateLevelHint(int levelHint) { + _levelHint = levelHint; +} + namespace Data { void ApplyMigration( @@ -1142,6 +1151,13 @@ void ApplyChannelUpdate( session->sendAsPeers().setChosen(channel, PeerId()); } + if (const auto paper = update.vwallpaper()) { + channel->setWallPaper( + Data::WallPaper::Create(&channel->session(), *paper)); + } else { + channel->setWallPaper({}); + } + // For clearUpTill() call. channel->owner().sendHistoryChangeNotifications(); } diff --git a/Telegram/SourceFiles/data/data_channel.h b/Telegram/SourceFiles/data/data_channel.h index 0d1faf26940f5..d55e8a4e6f396 100644 --- a/Telegram/SourceFiles/data/data_channel.h +++ b/Telegram/SourceFiles/data/data_channel.h @@ -463,6 +463,9 @@ class ChannelData final : public PeerData { void processTopics(const MTPVector &topics); + [[nodiscard]] int levelHint() const; + void updateLevelHint(int levelHint); + // Still public data members. uint64 access = 0; @@ -497,6 +500,7 @@ class ChannelData final : public PeerData { int _restrictedCount = 0; int _kickedCount = 0; int _pendingRequestsCount = 0; + int _levelHint = 0; std::vector _recentRequesters; MsgId _availableMinId = 0; diff --git a/Telegram/SourceFiles/data/data_download_manager.cpp b/Telegram/SourceFiles/data/data_download_manager.cpp index 1d7ea12af01b2..cd736e9e7fb72 100644 --- a/Telegram/SourceFiles/data/data_download_manager.cpp +++ b/Telegram/SourceFiles/data/data_download_manager.cpp @@ -889,7 +889,6 @@ not_null DownloadManager::generateItem( const auto replyTo = FullReplyTo(); const auto viaBotId = UserId(); const auto date = base::unixtime::now(); - const auto postAuthor = QString(); const auto caption = TextWithEntities(); const auto make = [&](const auto media) { return history->makeMessage( diff --git a/Telegram/SourceFiles/data/data_emoji_statuses.cpp b/Telegram/SourceFiles/data/data_emoji_statuses.cpp index a394edc0cc53b..dd1ad583bc06d 100644 --- a/Telegram/SourceFiles/data/data_emoji_statuses.cpp +++ b/Telegram/SourceFiles/data/data_emoji_statuses.cpp @@ -8,6 +8,7 @@ For license and copyright information please follow this link: #include "data/data_emoji_statuses.h" #include "main/main_session.h" +#include "data/data_channel.h" #include "data/data_user.h" #include "data/data_session.h" #include "data/data_document.h" @@ -53,6 +54,7 @@ EmojiStatuses::EmojiStatuses(not_null owner) kRefreshDefaultListEach ) | rpl::start_with_next([=] { refreshDefault(); + refreshChannelDefault(); }, _lifetime); } @@ -74,6 +76,14 @@ void EmojiStatuses::refreshColored() { requestColored(); } +void EmojiStatuses::refreshChannelDefault() { + requestChannelDefault(); +} + +void EmojiStatuses::refreshChannelColored() { + requestChannelColored(); +} + void EmojiStatuses::refreshRecentDelayed() { if (_recentRequestId || _recentRequestScheduled) { return; @@ -91,6 +101,8 @@ const std::vector &EmojiStatuses::list(Type type) const { case Type::Recent: return _recent; case Type::Default: return _default; case Type::Colored: return _colored; + case Type::ChannelDefault: return _channelDefault; + case Type::ChannelColored: return _channelColored; } Unexpected("Type in EmojiStatuses::list."); } @@ -103,20 +115,24 @@ rpl::producer<> EmojiStatuses::defaultUpdates() const { return _defaultUpdated.events(); } +rpl::producer<> EmojiStatuses::channelDefaultUpdates() const { + return _channelDefaultUpdated.events(); +} + void EmojiStatuses::registerAutomaticClear( - not_null user, + not_null peer, TimeId until) { if (!until) { - _clearing.remove(user); + _clearing.remove(peer); if (_clearing.empty()) { _clearingTimer.cancel(); } - } else if (auto &already = _clearing[user]; already != until) { + } else if (auto &already = _clearing[peer]; already != until) { already = until; const auto i = ranges::min_element(_clearing, {}, [](auto &&pair) { return pair.second; }); - if (i->first == user) { + if (i->first == peer) { const auto now = base::unixtime::now(); if (now < until) { processClearingIn(until - now); @@ -266,7 +282,7 @@ void EmojiStatuses::requestDefault() { } auto &api = _owner->session().api(); _defaultRequestId = api.request(MTPaccount_GetDefaultEmojiStatuses( - MTP_long(_recentHash) + MTP_long(_defaultHash) )).done([=](const MTPaccount_EmojiStatuses &result) { _defaultRequestId = 0; result.match([&](const MTPDaccount_emojiStatuses &data) { @@ -299,6 +315,45 @@ void EmojiStatuses::requestColored() { }).send(); } +void EmojiStatuses::requestChannelDefault() { + if (_channelDefaultRequestId) { + return; + } + auto &api = _owner->session().api(); + _channelDefaultRequestId = api.request(MTPaccount_GetDefaultEmojiStatuses( + MTP_long(_channelDefaultHash) + )).done([=](const MTPaccount_EmojiStatuses &result) { + _channelDefaultRequestId = 0; + result.match([&](const MTPDaccount_emojiStatuses &data) { + updateChannelDefault(data); + }, [&](const MTPDaccount_emojiStatusesNotModified &) { + }); + }).fail([=] { + _channelDefaultRequestId = 0; + _channelDefaultHash = 0; + }).send(); +} + +void EmojiStatuses::requestChannelColored() { + if (_channelColoredRequestId) { + return; + } + auto &api = _owner->session().api(); + _channelColoredRequestId = api.request(MTPmessages_GetStickerSet( + MTP_inputStickerSetEmojiChannelDefaultStatuses(), + MTP_int(0) // hash + )).done([=](const MTPmessages_StickerSet &result) { + _channelColoredRequestId = 0; + result.match([&](const MTPDmessages_stickerSet &data) { + updateChannelColored(data); + }, [](const MTPDmessages_stickerSetNotModified &) { + LOG(("API Error: Unexpected messages.stickerSetNotModified.")); + }); + }).fail([=] { + _channelColoredRequestId = 0; + }).send(); +} + void EmojiStatuses::updateRecent(const MTPDaccount_emojiStatuses &data) { _recentHash = data.vhash().v; _recent = ListFromMTP(data); @@ -321,27 +376,57 @@ void EmojiStatuses::updateColored(const MTPDmessages_stickerSet &data) { _coloredUpdated.fire({}); } +void EmojiStatuses::updateChannelDefault( + const MTPDaccount_emojiStatuses &data) { + _channelDefaultHash = data.vhash().v; + _channelDefault = ListFromMTP(data); + _channelDefaultUpdated.fire({}); +} + +void EmojiStatuses::updateChannelColored( + const MTPDmessages_stickerSet &data) { + const auto &list = data.vdocuments().v; + _channelColored.clear(); + _channelColored.reserve(list.size()); + for (const auto &sticker : data.vdocuments().v) { + _channelColored.push_back(_owner->processDocument(sticker)->id); + } + _channelColoredUpdated.fire({}); +} + void EmojiStatuses::set(DocumentId id, TimeId until) { + set(_owner->session().user(), id, until); +} + +void EmojiStatuses::set( + not_null peer, + DocumentId id, + TimeId until) { auto &api = _owner->session().api(); - if (_sentRequestId) { - api.request(base::take(_sentRequestId)).cancel(); + auto &requestId = _sentRequests[peer]; + if (requestId) { + api.request(base::take(requestId)).cancel(); } - _owner->session().user()->setEmojiStatus(id, until); - _sentRequestId = api.request(MTPaccount_UpdateEmojiStatus( - !id + peer->setEmojiStatus(id, until); + const auto send = [&](auto &&request) { + requestId = api.request( + std::move(request) + ).done([=] { + _sentRequests.remove(peer); + }).fail([=] { + _sentRequests.remove(peer); + }).send(); + }; + const auto status = !id ? MTP_emojiStatusEmpty() : !until ? MTP_emojiStatus(MTP_long(id)) - : MTP_emojiStatusUntil(MTP_long(id), MTP_int(until)) - )).done([=] { - _sentRequestId = 0; - }).fail([=] { - _sentRequestId = 0; - }).send(); -} - -bool EmojiStatuses::setting() const { - return _sentRequestId != 0;; + : MTP_emojiStatusUntil(MTP_long(id), MTP_int(until)); + if (peer->isSelf()) { + send(MTPaccount_UpdateEmojiStatus(status)); + } else if (const auto channel = peer->asChannel()) { + send(MTPchannels_UpdateEmojiStatus(channel->inputChannel, status)); + } } EmojiStatusData ParseEmojiStatus(const MTPEmojiStatus &status) { diff --git a/Telegram/SourceFiles/data/data_emoji_statuses.h b/Telegram/SourceFiles/data/data_emoji_statuses.h index 29bd8491c4085..5501eb8bc0437 100644 --- a/Telegram/SourceFiles/data/data_emoji_statuses.h +++ b/Telegram/SourceFiles/data/data_emoji_statuses.h @@ -36,22 +36,26 @@ class EmojiStatuses final { void refreshRecentDelayed(); void refreshDefault(); void refreshColored(); + void refreshChannelDefault(); + void refreshChannelColored(); enum class Type { Recent, Default, Colored, + ChannelDefault, + ChannelColored, }; [[nodiscard]] const std::vector &list(Type type) const; [[nodiscard]] rpl::producer<> recentUpdates() const; [[nodiscard]] rpl::producer<> defaultUpdates() const; - [[nodiscard]] rpl::producer<> coloredUpdates() const; + [[nodiscard]] rpl::producer<> channelDefaultUpdates() const; void set(DocumentId id, TimeId until = 0); - [[nodiscard]] bool setting() const; + void set(not_null peer, DocumentId id, TimeId until = 0); - void registerAutomaticClear(not_null user, TimeId until); + void registerAutomaticClear(not_null peer, TimeId until); using Groups = std::vector; [[nodiscard]] rpl::producer emojiGroupsValue() const; @@ -71,10 +75,14 @@ class EmojiStatuses final { void requestRecent(); void requestDefault(); void requestColored(); + void requestChannelDefault(); + void requestChannelColored(); void updateRecent(const MTPDaccount_emojiStatuses &data); void updateDefault(const MTPDaccount_emojiStatuses &data); void updateColored(const MTPDmessages_stickerSet &data); + void updateChannelDefault(const MTPDaccount_emojiStatuses &data); + void updateChannelColored(const MTPDmessages_stickerSet &data); void processClearingIn(TimeId wait); void processClearing(); @@ -87,9 +95,13 @@ class EmojiStatuses final { std::vector _recent; std::vector _default; std::vector _colored; + std::vector _channelDefault; + std::vector _channelColored; rpl::event_stream<> _recentUpdated; rpl::event_stream<> _defaultUpdated; rpl::event_stream<> _coloredUpdated; + rpl::event_stream<> _channelDefaultUpdated; + rpl::event_stream<> _channelColoredUpdated; mtpRequestId _recentRequestId = 0; bool _recentRequestScheduled = false; @@ -100,9 +112,14 @@ class EmojiStatuses final { mtpRequestId _coloredRequestId = 0; - mtpRequestId _sentRequestId = 0; + mtpRequestId _channelDefaultRequestId = 0; + uint64 _channelDefaultHash = 0; - base::flat_map, TimeId> _clearing; + mtpRequestId _channelColoredRequestId = 0; + + base::flat_map, mtpRequestId> _sentRequests; + + base::flat_map, TimeId> _clearing; base::Timer _clearingTimer; GroupsType _emojiGroups; diff --git a/Telegram/SourceFiles/data/data_folder.cpp b/Telegram/SourceFiles/data/data_folder.cpp index bbd4ce9872d94..76f599943ed25 100644 --- a/Telegram/SourceFiles/data/data_folder.cpp +++ b/Telegram/SourceFiles/data/data_folder.cpp @@ -343,12 +343,6 @@ int Folder::storiesUnreadCount() const { return _storiesUnreadCount; } -void Folder::requestChatListMessage() { - if (!chatListMessageKnown()) { - owner().histories().requestDialogEntry(this); - } -} - TimeId Folder::adjustedChatListTimeId() const { return chatListTimeId(); } diff --git a/Telegram/SourceFiles/data/data_folder.h b/Telegram/SourceFiles/data/data_folder.h index 7008adac0b852..7f6347e935a9d 100644 --- a/Telegram/SourceFiles/data/data_folder.h +++ b/Telegram/SourceFiles/data/data_folder.h @@ -49,9 +49,9 @@ class Folder final : public Dialogs::Entry { Dialogs::BadgesState chatListBadgesState() const override; HistoryItem *chatListMessage() const override; bool chatListMessageKnown() const override; - void requestChatListMessage() override; const QString &chatListName() const override; const QString &chatListNameSortKey() const override; + int chatListNameVersion() const override; const base::flat_set &chatListNameWords() const override; const base::flat_set &chatListFirstLetters() const override; @@ -82,8 +82,6 @@ class Folder final : public Dialogs::Entry { private: void indexNameParts(); - int chatListNameVersion() const override; - void reorderLastHistories(); void paintUserpic( diff --git a/Telegram/SourceFiles/data/data_forum_icons.cpp b/Telegram/SourceFiles/data/data_forum_icons.cpp index 1469cf559605d..c47c8e669cb6c 100644 --- a/Telegram/SourceFiles/data/data_forum_icons.cpp +++ b/Telegram/SourceFiles/data/data_forum_icons.cpp @@ -15,13 +15,6 @@ For license and copyright information please follow this link: #include "apiwrap.h" namespace Data { -namespace { - -constexpr auto kRefreshDefaultListEach = 60 * 60 * crl::time(1000); -constexpr auto kRecentRequestTimeout = 10 * crl::time(1000); -constexpr auto kMaxTimeout = 6 * 60 * 60 * crl::time(1000); - -} // namespace ForumIcons::ForumIcons(not_null owner) : _owner(owner) diff --git a/Telegram/SourceFiles/data/data_forum_topic.h b/Telegram/SourceFiles/data/data_forum_topic.h index 66430a0a37ca5..275f38fcbf8fa 100644 --- a/Telegram/SourceFiles/data/data_forum_topic.h +++ b/Telegram/SourceFiles/data/data_forum_topic.h @@ -98,6 +98,7 @@ class ForumTopic final : public Thread { void setRealRootId(MsgId realId); void readTillEnd(); + void requestChatListMessage(); void applyTopic(const MTPDforumTopic &data); @@ -109,9 +110,9 @@ class ForumTopic final : public Thread { Dialogs::BadgesState chatListBadgesState() const override; HistoryItem *chatListMessage() const override; bool chatListMessageKnown() const override; - void requestChatListMessage() override; const QString &chatListName() const override; const QString &chatListNameSortKey() const override; + int chatListNameVersion() const override; const base::flat_set &chatListNameWords() const override; const base::flat_set &chatListFirstLetters() const override; @@ -187,8 +188,6 @@ class ForumTopic final : public Thread { void allowChatListMessageResolve(); void resolveChatListMessageGroup(); - int chatListNameVersion() const override; - void subscribeToUnreadChanges(); [[nodiscard]] Dialogs::UnreadState unreadStateFor( int count, diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp index 3965657504db9..e9c4dd66e89a9 100644 --- a/Telegram/SourceFiles/data/data_media_types.cpp +++ b/Telegram/SourceFiles/data/data_media_types.cpp @@ -61,6 +61,7 @@ For license and copyright information please follow this link: #include "data/data_file_origin.h" #include "data/data_stories.h" #include "data/data_story.h" +#include "data/data_user.h" #include "main/main_session.h" #include "main/main_session_settings.h" #include "core/application.h" @@ -363,10 +364,10 @@ Call ComputeCallData(const MTPDmessageActionPhoneCall &call) { return result; } -Giveaway ComputeGiveawayData( +GiveawayStart ComputeGiveawayStartData( not_null item, const MTPDmessageMediaGiveaway &data) { - auto result = Giveaway{ + auto result = GiveawayStart{ .untilDate = data.vuntil_date().v, .quantity = data.vquantity().v, .months = data.vmonths().v, @@ -383,6 +384,35 @@ Giveaway ComputeGiveawayData( result.countries.push_back(qs(country)); } } + if (const auto additional = data.vprize_description()) { + result.additionalPrize = qs(*additional); + } + return result; +} + +GiveawayResults ComputeGiveawayResultsData( + not_null item, + const MTPDmessageMediaGiveawayResults &data) { + const auto additional = data.vadditional_peers_count(); + auto result = GiveawayResults{ + .channel = item->history()->owner().channel(data.vchannel_id()), + .untilDate = data.vuntil_date().v, + .launchId = data.vlaunch_msg_id().v, + .additionalPeersCount = additional.value_or_empty(), + .winnersCount = data.vwinners_count().v, + .unclaimedCount = data.vunclaimed_count().v, + .months = data.vmonths().v, + .refunded = data.is_refunded(), + .all = !data.is_only_new_subscribers(), + }; + result.winners.reserve(data.vwinners().v.size()); + const auto owner = &item->history()->owner(); + for (const auto &id : data.vwinners().v) { + result.winners.push_back(owner->user(UserId(id))); + } + if (const auto additional = data.vprize_description()) { + result.additionalPrize = qs(*additional); + } return result; } @@ -453,7 +483,11 @@ bool Media::storyMention() const { return false; } -const Giveaway *Media::giveaway() const { +const GiveawayStart *Media::giveawayStart() const { + return nullptr; +} + +const GiveawayResults *Media::giveawayResults() const { return nullptr; } @@ -521,6 +555,10 @@ bool Media::hasSpoiler() const { return false; } +crl::time Media::ttlSeconds() const { + return 0; +} + bool Media::consumeMessageText(const TextWithEntities &text) { return false; } @@ -815,12 +853,14 @@ MediaFile::MediaFile( not_null parent, not_null document, bool skipPremiumEffect, - bool spoiler) + bool spoiler, + crl::time ttlSeconds) : Media(parent) , _document(document) , _emoji(document->sticker() ? document->sticker()->alt : QString()) , _skipPremiumEffect(skipPremiumEffect) -, _spoiler(spoiler) { +, _spoiler(spoiler) +, _ttlSeconds(ttlSeconds) { parent->history()->owner().registerDocumentItem(_document, parent); if (!_emoji.isEmpty()) { @@ -848,7 +888,8 @@ std::unique_ptr MediaFile::clone(not_null parent) { parent, _document, !_document->session().premium(), - _spoiler); + _spoiler, + _ttlSeconds); } DocumentData *MediaFile::document() const { @@ -1095,6 +1136,14 @@ bool MediaFile::hasSpoiler() const { return _spoiler; } +crl::time MediaFile::ttlSeconds() const { + return _ttlSeconds; +} + +bool MediaFile::allowsForward() const { + return !ttlSeconds(); +} + bool MediaFile::updateInlineResultMedia(const MTPMessageMedia &media) { if (media.type() != mtpc_messageMediaDocument) { return false; @@ -1506,15 +1555,57 @@ bool MediaWebPage::replyPreviewLoaded() const { } ItemPreview MediaWebPage::toPreview(ToPreviewOptions options) const { - auto text = options.ignoreMessageText - ? TextWithEntities() - : options.translated - ? parent()->translatedText() - : parent()->originalText(); - if (text.empty()) { - text = Ui::Text::Colorized(_page->url); + const auto caption = [&] { + const auto text = options.ignoreMessageText + ? TextWithEntities() + : options.translated + ? parent()->translatedText() + : parent()->originalText(); + return text.empty() ? Ui::Text::Colorized(_page->url) : text; + }(); + const auto pageTypeWithPreview = _page->type == WebPageType::Photo + || _page->type == WebPageType::Video + || _page->type == WebPageType::Document; + if (pageTypeWithPreview || !_page->collage.items.empty()) { + if (auto found = FindCachedPreview(options.existing, _page, false)) { + return { .text = caption, .images = { std::move(found) } }; + } + auto context = std::any(); + auto images = std::vector(); + auto prepared = ItemPreviewImage(); + const auto radius = ImageRoundRadius::Small; + if (const auto photo = MediaWebPage::photo()) { + const auto media = photo->createMediaView(); + prepared = PreparePhotoPreview(parent(), media, radius, false); + if (prepared || !prepared.cacheKey) { + images.push_back(std::move(prepared)); + if (!prepared.cacheKey) { + context = media; + } + } + } else { + const auto document = MediaWebPage::document(); + if (document + && document->hasThumbnail() + && (document->isGifv() || document->isVideoFile())) { + const auto media = document->createMediaView(); + prepared = PrepareFilePreview(parent(), media, radius, false); + if (prepared || !prepared.cacheKey) { + images.push_back(std::move(prepared)); + if (!prepared.cacheKey) { + context = media; + } + } + } + } + return { + .text = caption, + .images = std::move(images), + .loadingContext = std::move(context), + }; + } else { + return { .text = caption }; } - return { .text = text }; } TextWithEntities MediaWebPage::notificationText() const { @@ -2196,51 +2287,103 @@ std::unique_ptr MediaStory::createView( } } -MediaGiveaway::MediaGiveaway( +MediaGiveawayStart::MediaGiveawayStart( not_null parent, - const Giveaway &data) + const GiveawayStart &data) : Media(parent) -, _giveaway(data) { +, _data(data) { parent->history()->session().giftBoxStickersPacks().load(); } -std::unique_ptr MediaGiveaway::clone(not_null parent) { - return std::make_unique(parent, _giveaway); +std::unique_ptr MediaGiveawayStart::clone( + not_null parent) { + return std::make_unique(parent, _data); } -const Giveaway *MediaGiveaway::giveaway() const { - return &_giveaway; +const GiveawayStart *MediaGiveawayStart::giveawayStart() const { + return &_data; } -TextWithEntities MediaGiveaway::notificationText() const { +TextWithEntities MediaGiveawayStart::notificationText() const { return { - .text = tr::lng_prizes_title(tr::now, lt_count, _giveaway.quantity), + .text = tr::lng_prizes_title(tr::now, lt_count, _data.quantity), }; } -QString MediaGiveaway::pinnedTextSubstring() const { +QString MediaGiveawayStart::pinnedTextSubstring() const { return QString::fromUtf8("\xC2\xAB") + notificationText().text + QString::fromUtf8("\xC2\xBB"); } -TextForMimeData MediaGiveaway::clipboardText() const { +TextForMimeData MediaGiveawayStart::clipboardText() const { return TextForMimeData(); } -bool MediaGiveaway::updateInlineResultMedia(const MTPMessageMedia &media) { +bool MediaGiveawayStart::updateInlineResultMedia(const MTPMessageMedia &media) { return true; } -bool MediaGiveaway::updateSentMedia(const MTPMessageMedia &media) { +bool MediaGiveawayStart::updateSentMedia(const MTPMessageMedia &media) { return true; } -std::unique_ptr MediaGiveaway::createView( +std::unique_ptr MediaGiveawayStart::createView( not_null message, not_null realParent, HistoryView::Element *replacing) { - return std::make_unique(message, &_giveaway); + return std::make_unique( + message, + HistoryView::GenerateGiveawayStart(message, &_data)); +} + +MediaGiveawayResults::MediaGiveawayResults( + not_null parent, + const GiveawayResults &data) +: Media(parent) +, _data(data) { +} + +std::unique_ptr MediaGiveawayResults::clone( + not_null parent) { + return std::make_unique(parent, _data); +} + +const GiveawayResults *MediaGiveawayResults::giveawayResults() const { + return &_data; +} + +TextWithEntities MediaGiveawayResults::notificationText() const { + return { + .text = tr::lng_prizes_results_title(tr::now), + }; +} + +QString MediaGiveawayResults::pinnedTextSubstring() const { + return QString::fromUtf8("\xC2\xAB") + + notificationText().text + + QString::fromUtf8("\xC2\xBB"); +} + +TextForMimeData MediaGiveawayResults::clipboardText() const { + return TextForMimeData(); +} + +bool MediaGiveawayResults::updateInlineResultMedia(const MTPMessageMedia &media) { + return true; +} + +bool MediaGiveawayResults::updateSentMedia(const MTPMessageMedia &media) { + return true; +} + +std::unique_ptr MediaGiveawayResults::createView( + not_null message, + not_null realParent, + HistoryView::Element *replacing) { + return std::make_unique( + message, + HistoryView::GenerateGiveawayResults(message, &_data)); } } // namespace Data diff --git a/Telegram/SourceFiles/data/data_media_types.h b/Telegram/SourceFiles/data/data_media_types.h index 10c9389636e43..b2323af8a505b 100644 --- a/Telegram/SourceFiles/data/data_media_types.h +++ b/Telegram/SourceFiles/data/data_media_types.h @@ -90,15 +90,30 @@ struct Invoice { bool isTest = false; }; -struct Giveaway { +struct GiveawayStart { std::vector> channels; std::vector countries; + QString additionalPrize; TimeId untilDate = 0; int quantity = 0; int months = 0; bool all = false; }; +struct GiveawayResults { + not_null channel; + std::vector> winners; + QString additionalPrize; + TimeId untilDate = 0; + MsgId launchId = 0; + int additionalPeersCount = 0; + int winnersCount = 0; + int unclaimedCount = 0; + int months = 0; + bool refunded = false; + bool all = false; +}; + struct GiftCode { QString slug; ChannelData *channel = nullptr; @@ -135,7 +150,8 @@ class Media { virtual FullStoryId storyId() const; virtual bool storyExpired(bool revalidate = false); virtual bool storyMention() const; - virtual const Giveaway *giveaway() const; + virtual const GiveawayStart *giveawayStart() const; + virtual const GiveawayResults *giveawayResults() const; virtual bool uploading() const; virtual Storage::SharedMediaTypesMask sharedMediaTypes() const; @@ -158,6 +174,7 @@ class Media { virtual bool dropForwardedInfo() const; virtual bool forceForwardedInfo() const; [[nodiscard]] virtual bool hasSpoiler() const; + [[nodiscard]] virtual crl::time ttlSeconds() const; [[nodiscard]] virtual bool consumeMessageText( const TextWithEntities &text); @@ -240,7 +257,8 @@ class MediaFile final : public Media { not_null parent, not_null document, bool skipPremiumEffect, - bool spoiler); + bool spoiler, + crl::time ttlSeconds); ~MediaFile(); std::unique_ptr clone(not_null parent) override; @@ -262,6 +280,8 @@ class MediaFile final : public Media { bool forwardedBecomesUnread() const override; bool dropForwardedInfo() const override; bool hasSpoiler() const override; + crl::time ttlSeconds() const override; + bool allowsForward() const override; bool updateInlineResultMedia(const MTPMessageMedia &media) override; bool updateSentMedia(const MTPMessageMedia &media) override; @@ -276,6 +296,9 @@ class MediaFile final : public Media { bool _skipPremiumEffect = false; bool _spoiler = false; + // Video (unsupported) / Voice / Round. + crl::time _ttlSeconds = 0; + }; class MediaContact final : public Media { @@ -634,15 +657,15 @@ class MediaStory final : public Media, public base::has_weak_ptr { }; -class MediaGiveaway final : public Media { +class MediaGiveawayStart final : public Media { public: - MediaGiveaway( + MediaGiveawayStart( not_null parent, - const Giveaway &data); + const GiveawayStart &data); std::unique_ptr clone(not_null parent) override; - const Giveaway *giveaway() const override; + const GiveawayStart *giveawayStart() const override; TextWithEntities notificationText() const override; QString pinnedTextSubstring() const override; @@ -656,7 +679,33 @@ class MediaGiveaway final : public Media { HistoryView::Element *replacing = nullptr) override; private: - Giveaway _giveaway; + GiveawayStart _data; + +}; + +class MediaGiveawayResults final : public Media { +public: + MediaGiveawayResults( + not_null parent, + const GiveawayResults &data); + + std::unique_ptr clone(not_null parent) override; + + const GiveawayResults *giveawayResults() const override; + + TextWithEntities notificationText() const override; + QString pinnedTextSubstring() const override; + TextForMimeData clipboardText() const override; + + bool updateInlineResultMedia(const MTPMessageMedia &media) override; + bool updateSentMedia(const MTPMessageMedia &media) override; + std::unique_ptr createView( + not_null message, + not_null realParent, + HistoryView::Element *replacing = nullptr) override; + +private: + GiveawayResults _data; }; @@ -670,8 +719,12 @@ class MediaGiveaway final : public Media { [[nodiscard]] Call ComputeCallData(const MTPDmessageActionPhoneCall &call); -[[nodiscard]] Giveaway ComputeGiveawayData( +[[nodiscard]] GiveawayStart ComputeGiveawayStartData( not_null item, const MTPDmessageMediaGiveaway &data); +[[nodiscard]] GiveawayResults ComputeGiveawayResultsData( + not_null item, + const MTPDmessageMediaGiveawayResults &data); + } // namespace Data diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp index 0f34729164576..a6f0339242aa9 100644 --- a/Telegram/SourceFiles/data/data_peer.cpp +++ b/Telegram/SourceFiles/data/data_peer.cpp @@ -12,11 +12,13 @@ For license and copyright information please follow this link: #include "data/data_chat_participant_status.h" #include "data/data_channel.h" #include "data/data_changes.h" +#include "data/data_emoji_statuses.h" #include "data/data_message_reaction_id.h" #include "data/data_photo.h" #include "data/data_folder.h" #include "data/data_forum.h" #include "data/data_forum_topic.h" +#include "data/data_saved_messages.h" #include "data/data_session.h" #include "data/data_file_origin.h" #include "data/data_histories.h" @@ -926,6 +928,24 @@ bool PeerData::changeBackgroundEmojiId(DocumentId id) { _backgroundEmojiId = id; return true; } + +void PeerData::setEmojiStatus(const MTPEmojiStatus &status) { + const auto parsed = Data::ParseEmojiStatus(status); + setEmojiStatus(parsed.id, parsed.until); +} + +void PeerData::setEmojiStatus(DocumentId emojiStatusId, TimeId until) { + if (_emojiStatusId != emojiStatusId) { + _emojiStatusId = emojiStatusId; + session().changes().peerUpdated(this, UpdateFlag::EmojiStatus); + } + owner().emojiStatuses().registerAutomaticClear(this, until); +} + +DocumentId PeerData::emojiStatusId() const { + return _emojiStatusId; +} + bool PeerData::isSelf() const { if (const auto user = asUser()) { return (user->flags() & UserDataFlag::Self); @@ -1010,6 +1030,10 @@ bool PeerData::sharedMediaInfo() const { return isSelf() || isRepliesChat(); } +bool PeerData::savedSublistsInfo() const { + return isSelf() && owner().savedMessages().supported(); +} + bool PeerData::hasStoriesHidden() const { if (const auto user = asUser()) { return user->hasStoriesHidden(); diff --git a/Telegram/SourceFiles/data/data_peer.h b/Telegram/SourceFiles/data/data_peer.h index 1ff2676d727dd..0252063f4b20e 100644 --- a/Telegram/SourceFiles/data/data_peer.h +++ b/Telegram/SourceFiles/data/data_peer.h @@ -165,6 +165,7 @@ class PeerData { virtual ~PeerData(); static constexpr auto kServiceNotificationsId = peerFromUser(777000); + static constexpr auto kSavedHiddenAuthorId = peerFromUser(2666000); [[nodiscard]] Data::Session &owner() const; [[nodiscard]] Main::Session &session() const; @@ -178,6 +179,10 @@ class PeerData { [[nodiscard]] DocumentId backgroundEmojiId() const; bool changeBackgroundEmojiId(DocumentId id); + void setEmojiStatus(const MTPEmojiStatus &status); + void setEmojiStatus(DocumentId emojiStatusId, TimeId until = 0); + [[nodiscard]] DocumentId emojiStatusId() const; + [[nodiscard]] bool isUser() const { return peerIsUser(id); } @@ -198,6 +203,7 @@ class PeerData { [[nodiscard]] bool isGigagroup() const; [[nodiscard]] bool isRepliesChat() const; [[nodiscard]] bool sharedMediaInfo() const; + [[nodiscard]] bool savedSublistsInfo() const; [[nodiscard]] bool hasStoriesHidden() const; void setStoriesHidden(bool hidden); @@ -208,6 +214,9 @@ class PeerData { [[nodiscard]] bool isServiceUser() const { return isUser() && !(id.value % 1000); } + [[nodiscard]] bool isSavedHiddenAuthor() const { + return (id == kSavedHiddenAuthorId); + } [[nodiscard]] Data::Forum *forum() const; [[nodiscard]] Data::ForumTopic *forumTopicFor(MsgId rootId) const; @@ -466,6 +475,7 @@ class PeerData { base::flat_set _nameWords; // for filtering base::flat_set _nameFirstLetters; + DocumentId _emojiStatusId = 0; uint64 _backgroundEmojiId = 0; crl::time _lastFullUpdate = 0; diff --git a/Telegram/SourceFiles/data/data_photo_media.cpp b/Telegram/SourceFiles/data/data_photo_media.cpp index 0b66e192a796a..af01588f455a8 100644 --- a/Telegram/SourceFiles/data/data_photo_media.cpp +++ b/Telegram/SourceFiles/data/data_photo_media.cpp @@ -226,7 +226,6 @@ bool PhotoMedia::setToClipboard() { if (fallback.isNull()) { return false; } - const auto bytes = imageBytes(large); auto mime = std::make_unique(); mime->setImageData(std::move(fallback)); if (auto bytes = imageBytes(large); !bytes.isEmpty()) { diff --git a/Telegram/SourceFiles/data/data_photo_media.h b/Telegram/SourceFiles/data/data_photo_media.h index a5ef88b22e6d1..0ac1ddd821094 100644 --- a/Telegram/SourceFiles/data/data_photo_media.h +++ b/Telegram/SourceFiles/data/data_photo_media.h @@ -64,7 +64,7 @@ class PhotoMedia final { // In case this is a problem the ~Gif code should be rewritten. const not_null _owner; mutable std::unique_ptr _inlineThumbnail; - std::array _images; + std::array _images; QByteArray _videoBytesSmall; QByteArray _videoBytesLarge; diff --git a/Telegram/SourceFiles/data/data_premium_limits.cpp b/Telegram/SourceFiles/data/data_premium_limits.cpp index 07a5124a67953..443474040c5b2 100644 --- a/Telegram/SourceFiles/data/data_premium_limits.cpp +++ b/Telegram/SourceFiles/data/data_premium_limits.cpp @@ -141,6 +141,18 @@ int PremiumLimits::topicsPinnedCurrent() const { return appConfigLimit("topics_pinned_limit", 5); } +int PremiumLimits::savedSublistsPinnedDefault() const { + return appConfigLimit("saved_dialogs_pinned_limit_default", 5); +} +int PremiumLimits::savedSublistsPinnedPremium() const { + return appConfigLimit("saved_dialogs_pinned_limit_premium", 100); +} +int PremiumLimits::savedSublistsPinnedCurrent() const { + return isPremium() + ? savedSublistsPinnedPremium() + : savedSublistsPinnedDefault(); +} + int PremiumLimits::channelsPublicDefault() const { return appConfigLimit("channels_public_limit_default", 10); } diff --git a/Telegram/SourceFiles/data/data_premium_limits.h b/Telegram/SourceFiles/data/data_premium_limits.h index f9bd416d24890..bc3c86d9ff15a 100644 --- a/Telegram/SourceFiles/data/data_premium_limits.h +++ b/Telegram/SourceFiles/data/data_premium_limits.h @@ -59,6 +59,10 @@ class PremiumLimits final { [[nodiscard]] int topicsPinnedCurrent() const; + [[nodiscard]] int savedSublistsPinnedDefault() const; + [[nodiscard]] int savedSublistsPinnedPremium() const; + [[nodiscard]] int savedSublistsPinnedCurrent() const; + [[nodiscard]] int channelsPublicDefault() const; [[nodiscard]] int channelsPublicPremium() const; [[nodiscard]] int channelsPublicCurrent() const; diff --git a/Telegram/SourceFiles/data/data_saved_messages.cpp b/Telegram/SourceFiles/data/data_saved_messages.cpp new file mode 100644 index 0000000000000..9fd28f7354c02 --- /dev/null +++ b/Telegram/SourceFiles/data/data_saved_messages.cpp @@ -0,0 +1,276 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "data/data_saved_messages.h" + +#include "apiwrap.h" +#include "data/data_peer.h" +#include "data/data_saved_sublist.h" +#include "data/data_session.h" +#include "history/history.h" +#include "history/history_item.h" +#include "main/main_session.h" + +namespace Data { +namespace { + +constexpr auto kPerPage = 50; +constexpr auto kFirstPerPage = 10; + +} // namespace + +SavedMessages::SavedMessages(not_null owner) +: _owner(owner) +, _chatsList( + &owner->session(), + FilterId(), + owner->maxPinnedChatsLimitValue(this)) { +} + +SavedMessages::~SavedMessages() = default; + +bool SavedMessages::supported() const { + return !_unsupported; +} + +Session &SavedMessages::owner() const { + return *_owner; +} + +Main::Session &SavedMessages::session() const { + return _owner->session(); +} + +not_null SavedMessages::chatsList() { + return &_chatsList; +} + +not_null SavedMessages::sublist(not_null peer) { + const auto i = _sublists.find(peer); + if (i != end(_sublists)) { + return i->second.get(); + } + return _sublists.emplace( + peer, + std::make_unique(peer)).first->second.get(); +} + +void SavedMessages::loadMore() { + if (_loadMoreRequestId || _chatsList.loaded()) { + return; + } else if (!_pinnedLoaded) { + loadPinned(); + } + _loadMoreRequestId = _owner->session().api().request( + MTPmessages_GetSavedDialogs( + MTP_flags(MTPmessages_GetSavedDialogs::Flag::f_exclude_pinned), + MTP_int(_offsetDate), + MTP_int(_offsetId), + _offsetPeer ? _offsetPeer->input : MTP_inputPeerEmpty(), + MTP_int(kPerPage), + MTP_long(0)) // hash + ).done([=](const MTPmessages_SavedDialogs &result) { + apply(result, false); + }).fail([=](const MTP::Error &error) { + if (error.type() == u"SAVED_DIALOGS_UNSUPPORTED"_q) { + _unsupported = true; + } + _chatsList.setLoaded(); + _loadMoreRequestId = 0; + }).send(); +} + +void SavedMessages::loadPinned() { + if (_pinnedRequestId) { + return; + } + _pinnedRequestId = _owner->session().api().request( + MTPmessages_GetPinnedSavedDialogs() + ).done([=](const MTPmessages_SavedDialogs &result) { + apply(result, true); + }).fail([=](const MTP::Error &error) { + if (error.type() == u"SAVED_DIALOGS_UNSUPPORTED"_q) { + _unsupported = true; + } else { + _pinnedLoaded = true; + } + _pinnedRequestId = 0; + }).send(); +} + +void SavedMessages::loadMore(not_null sublist) { + if (_loadMoreRequests.contains(sublist) || sublist->isFullLoaded()) { + return; + } + const auto &list = sublist->messages(); + const auto offsetId = list.empty() ? MsgId(0) : list.back()->id; + const auto offsetDate = list.empty() ? MsgId(0) : list.back()->date(); + const auto limit = offsetId ? kPerPage : kFirstPerPage; + const auto requestId = _owner->session().api().request( + MTPmessages_GetSavedHistory( + sublist->peer()->input, + MTP_int(offsetId), + MTP_int(offsetDate), + MTP_int(0), // add_offset + MTP_int(limit), + MTP_int(0), // max_id + MTP_int(0), // min_id + MTP_long(0)) // hash + ).done([=](const MTPmessages_Messages &result) { + auto count = 0; + auto list = (const QVector*)nullptr; + result.match([](const MTPDmessages_channelMessages &) { + LOG(("API Error: messages.channelMessages in sublist.")); + }, [](const MTPDmessages_messagesNotModified &) { + LOG(("API Error: messages.messagesNotModified in sublist.")); + }, [&](const auto &data) { + owner().processUsers(data.vusers()); + owner().processChats(data.vchats()); + list = &data.vmessages().v; + if constexpr (MTPDmessages_messages::Is()) { + count = int(list->size()); + } else { + count = data.vcount().v; + } + }); + + _loadMoreRequests.remove(sublist); + if (!list) { + sublist->setFullLoaded(); + return; + } + auto items = std::vector>(); + items.reserve(list->size()); + for (const auto &message : *list) { + const auto item = owner().addNewMessage( + message, + {}, + NewMessageType::Existing); + if (item) { + items.push_back(item); + } + } + sublist->append(std::move(items), count); + if (result.type() == mtpc_messages_messages) { + sublist->setFullLoaded(); + } + }).fail([=](const MTP::Error &error) { + if (error.type() == u"SAVED_DIALOGS_UNSUPPORTED"_q) { + _unsupported = true; + } + sublist->setFullLoaded(); + _loadMoreRequests.remove(sublist); + }).send(); + _loadMoreRequests[sublist] = requestId; +} + +void SavedMessages::apply( + const MTPmessages_SavedDialogs &result, + bool pinned) { + auto list = (const QVector*)nullptr; + result.match([](const MTPDmessages_savedDialogsNotModified &) { + LOG(("API Error: messages.savedDialogsNotModified.")); + }, [&](const auto &data) { + _owner->processUsers(data.vusers()); + _owner->processChats(data.vchats()); + _owner->processMessages( + data.vmessages(), + NewMessageType::Existing); + list = &data.vdialogs().v; + }); + if (pinned) { + _pinnedRequestId = 0; + _pinnedLoaded = true; + } else { + _loadMoreRequestId = 0; + } + if (!list) { + if (!pinned) { + _chatsList.setLoaded(); + } + return; + } + auto lastValid = false; + auto offsetDate = TimeId(); + auto offsetId = MsgId(); + auto offsetPeer = (PeerData*)nullptr; + const auto selfId = _owner->session().userPeerId(); + for (const auto &dialog : *list) { + const auto &data = dialog.data(); + const auto peer = _owner->peer(peerFromMTP(data.vpeer())); + const auto topId = MsgId(data.vtop_message().v); + if (const auto item = _owner->message(selfId, topId)) { + offsetPeer = peer; + offsetDate = item->date(); + offsetId = topId; + lastValid = true; + const auto entry = sublist(peer); + const auto entryPinned = pinned || data.is_pinned(); + entry->applyMaybeLast(item); + _owner->setPinnedFromEntryList(entry, entryPinned); + } else { + lastValid = false; + } + } + if (pinned) { + } else if (!lastValid) { + LOG(("API Error: Unknown message in the end of a slice.")); + _chatsList.setLoaded(); + } else if (result.type() == mtpc_messages_savedDialogs) { + _chatsList.setLoaded(); + } else if (offsetDate < _offsetDate + || (offsetDate == _offsetDate && offsetId == _offsetId && offsetPeer == _offsetPeer)) { + LOG(("API Error: Bad order in messages.savedDialogs.")); + _chatsList.setLoaded(); + } else { + _offsetDate = offsetDate; + _offsetId = offsetId; + _offsetPeer = offsetPeer; + } +} + +void SavedMessages::apply(const MTPDupdatePinnedSavedDialogs &update) { + const auto list = update.vorder(); + if (!list) { + loadPinned(); + return; + } + const auto &order = list->v; + const auto notLoaded = [&](const MTPDialogPeer &peer) { + return peer.match([&](const MTPDdialogPeer &data) { + const auto peer = _owner->peer(peerFromMTP(data.vpeer())); + return !_sublists.contains(peer); + }, [&](const MTPDdialogPeerFolder &data) { + LOG(("API Error: " + "updatePinnedSavedDialogs has folders.")); + return false; + }); + }; + if (!ranges::none_of(order, notLoaded)) { + loadPinned(); + } else { + _chatsList.pinned()->applyList(_owner, order); + _owner->notifyPinnedDialogsOrderUpdated(); + } +} + +void SavedMessages::apply(const MTPDupdateSavedDialogPinned &update) { + update.vpeer().match([&](const MTPDdialogPeer &data) { + const auto peer = _owner->peer(peerFromMTP(data.vpeer())); + const auto i = _sublists.find(peer); + if (i != end(_sublists)) { + const auto entry = i->second.get(); + _owner->setChatPinned(entry, FilterId(), update.is_pinned()); + } else { + loadPinned(); + } + }, [&](const MTPDdialogPeerFolder &data) { + DEBUG_LOG(("API Error: Folder in updateSavedDialogPinned.")); + }); +} + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_saved_messages.h b/Telegram/SourceFiles/data/data_saved_messages.h new file mode 100644 index 0000000000000..3fbc5a772f55c --- /dev/null +++ b/Telegram/SourceFiles/data/data_saved_messages.h @@ -0,0 +1,64 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "dialogs/dialogs_main_list.h" + +namespace Main { +class Session; +} // namespace Main + +namespace Data { + +class Session; +class SavedSublist; + +class SavedMessages final { +public: + explicit SavedMessages(not_null owner); + ~SavedMessages(); + + [[nodiscard]] bool supported() const; + + [[nodiscard]] Session &owner() const; + [[nodiscard]] Main::Session &session() const; + + [[nodiscard]] not_null chatsList(); + [[nodiscard]] not_null sublist(not_null peer); + + void loadMore(); + void loadMore(not_null sublist); + + void apply(const MTPDupdatePinnedSavedDialogs &update); + void apply(const MTPDupdateSavedDialogPinned &update); + +private: + void loadPinned(); + void apply(const MTPmessages_SavedDialogs &result, bool pinned); + + const not_null _owner; + + Dialogs::MainList _chatsList; + base::flat_map< + not_null, + std::unique_ptr> _sublists; + + base::flat_map, mtpRequestId> _loadMoreRequests; + mtpRequestId _loadMoreRequestId = 0; + mtpRequestId _pinnedRequestId = 0; + + TimeId _offsetDate = 0; + MsgId _offsetId = 0; + PeerData *_offsetPeer = nullptr; + + bool _pinnedLoaded = false; + bool _unsupported = false; + +}; + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_saved_sublist.cpp b/Telegram/SourceFiles/data/data_saved_sublist.cpp new file mode 100644 index 0000000000000..2d129a5f67e02 --- /dev/null +++ b/Telegram/SourceFiles/data/data_saved_sublist.cpp @@ -0,0 +1,256 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "data/data_saved_sublist.h" + +#include "data/data_histories.h" +#include "data/data_peer.h" +#include "data/data_saved_messages.h" +#include "data/data_session.h" +#include "history/view/history_view_item_preview.h" +#include "history/history.h" +#include "history/history_item.h" + +namespace Data { + +SavedSublist::SavedSublist(not_null peer) +: Entry(&peer->owner(), Dialogs::Entry::Type::SavedSublist) +, _history(peer->owner().history(peer)) { +} + +SavedSublist::~SavedSublist() = default; + +not_null SavedSublist::history() const { + return _history; +} + +not_null SavedSublist::peer() const { + return _history->peer; +} + +bool SavedSublist::isHiddenAuthor() const { + return peer()->isSavedHiddenAuthor(); +} + +bool SavedSublist::isFullLoaded() const { + return (_flags & Flag::FullLoaded) != 0; +} + +auto SavedSublist::messages() const +-> const std::vector> & { + return _items; +} + +void SavedSublist::applyMaybeLast(not_null item, bool added) { + const auto before = []( + not_null a, + not_null b) { + return IsServerMsgId(a->id) + ? (IsServerMsgId(b->id) ? (a->id < b->id) : true) + : (IsServerMsgId(b->id) ? false : (a->id < b->id)); + }; + + if (_items.empty()) { + _items.push_back(item); + } else if (_items.front() == item) { + return; + } else if (!isFullLoaded() + && _items.size() == 1 + && before(_items.front(), item)) { + _items[0] = item; + } else if (before(_items.back(), item)) { + for (auto i = begin(_items); i != end(_items); ++i) { + if (item == *i) { + return; + } else if (before(*i, item)) { + _items.insert(i, item); + break; + } + } + } + if (added && _fullCount) { + ++*_fullCount; + } + if (_items.front() == item) { + setChatListTimeId(item->date()); + resolveChatListMessageGroup(); + } + _changed.fire({}); +} + +void SavedSublist::removeOne(not_null item) { + if (_items.empty()) { + return; + } + const auto last = (_items.front() == item); + const auto from = ranges::remove(_items, item); + const auto removed = end(_items) - from; + if (removed) { + _items.erase(from, end(_items)); + } + if (_fullCount) { + --*_fullCount; + } + if (last) { + if (_items.empty()) { + if (isFullLoaded()) { + updateChatListExistence(); + } else { + updateChatListEntry(); + crl::on_main(this, [=] { + owner().savedMessages().loadMore(this); + }); + } + } else { + setChatListTimeId(_items.front()->date()); + } + } + if (removed || _fullCount) { + _changed.fire({}); + } +} + +rpl::producer<> SavedSublist::changes() const { + return _changed.events(); +} + +std::optional SavedSublist::fullCount() const { + return isFullLoaded() ? int(_items.size()) : _fullCount; +} + +rpl::producer SavedSublist::fullCountValue() const { + return _changed.events_starting_with({}) | rpl::map([=] { + return fullCount(); + }) | rpl::filter_optional(); +} + +void SavedSublist::append( + std::vector> &&items, + int fullCount) { + _fullCount = fullCount; + if (items.empty()) { + setFullLoaded(); + } else if (_items.empty()) { + _items = std::move(items); + setChatListTimeId(_items.front()->date()); + _changed.fire({}); + } else if (_items.back()->id > items.front()->id) { + _items.insert(end(_items), begin(items), end(items)); + _changed.fire({}); + } else { + _items.insert(end(_items), begin(items), end(items)); + ranges::stable_sort( + _items, + ranges::greater(), + &HistoryItem::id); + ranges::unique(_items, ranges::greater(), &HistoryItem::id); + _changed.fire({}); + } +} + +void SavedSublist::setFullLoaded(bool loaded) { + if (loaded != isFullLoaded()) { + if (loaded) { + _flags |= Flag::FullLoaded; + if (_items.empty()) { + updateChatListExistence(); + } + } else { + _flags &= ~Flag::FullLoaded; + } + _changed.fire({}); + } +} + +int SavedSublist::fixedOnTopIndex() const { + return 0; +} + +bool SavedSublist::shouldBeInChatList() const { + return isPinnedDialog(FilterId()) || !_items.empty(); +} + +Dialogs::UnreadState SavedSublist::chatListUnreadState() const { + return {}; +} + +Dialogs::BadgesState SavedSublist::chatListBadgesState() const { + return {}; +} + +HistoryItem *SavedSublist::chatListMessage() const { + return _items.empty() ? nullptr : _items.front().get(); +} + +bool SavedSublist::chatListMessageKnown() const { + return true; +} + +const QString &SavedSublist::chatListName() const { + return _history->chatListName(); +} + +const base::flat_set &SavedSublist::chatListNameWords() const { + return _history->chatListNameWords(); +} + +const base::flat_set &SavedSublist::chatListFirstLetters() const { + return _history->chatListFirstLetters(); +} + +const QString &SavedSublist::chatListNameSortKey() const { + return _history->chatListNameSortKey(); +} + +int SavedSublist::chatListNameVersion() const { + return _history->chatListNameVersion(); +} + +void SavedSublist::paintUserpic( + Painter &p, + Ui::PeerUserpicView &view, + const Dialogs::Ui::PaintContext &context) const { + _history->paintUserpic(p, view, context); +} + +void SavedSublist::chatListPreloadData() { + peer()->loadUserpic(); + allowChatListMessageResolve(); +} + +void SavedSublist::allowChatListMessageResolve() { + if (_flags & Flag::ResolveChatListMessage) { + return; + } + _flags |= Flag::ResolveChatListMessage; + resolveChatListMessageGroup(); +} + +bool SavedSublist::hasOrphanMediaGroupPart() const { + if (isFullLoaded() || _items.size() != 1) { + return false; + } + return (_items.front()->groupId() != MessageGroupId()); +} + +void SavedSublist::resolveChatListMessageGroup() { + const auto item = chatListMessage(); + if (!(_flags & Flag::ResolveChatListMessage) + || !item + || !hasOrphanMediaGroupPart()) { + return; + } + // If we set a single album part, request the full album. + const auto withImages = !item->toPreview({ + .hideSender = true, + .hideCaption = true }).images.empty(); + if (withImages) { + owner().histories().requestGroupAround(item); + } +} + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_saved_sublist.h b/Telegram/SourceFiles/data/data_saved_sublist.h new file mode 100644 index 0000000000000..15c3428fff42d --- /dev/null +++ b/Telegram/SourceFiles/data/data_saved_sublist.h @@ -0,0 +1,85 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "dialogs/ui/dialogs_message_view.h" +#include "dialogs/dialogs_entry.h" + +class PeerData; +class History; + +namespace Data { + +class Session; + +class SavedSublist final : public Dialogs::Entry { +public: + explicit SavedSublist(not_null peer); + ~SavedSublist(); + + [[nodiscard]] not_null history() const; + [[nodiscard]] not_null peer() const; + [[nodiscard]] bool isHiddenAuthor() const; + [[nodiscard]] bool isFullLoaded() const; + + [[nodiscard]] auto messages() const + -> const std::vector> &; + void applyMaybeLast(not_null item, bool added = false); + void removeOne(not_null item); + void append(std::vector> &&items, int fullCount); + void setFullLoaded(bool loaded = true); + + [[nodiscard]] rpl::producer<> changes() const; + [[nodiscard]] std::optional fullCount() const; + [[nodiscard]] rpl::producer fullCountValue() const; + + [[nodiscard]] Dialogs::Ui::MessageView &lastItemDialogsView() { + return _lastItemDialogsView; + } + + int fixedOnTopIndex() const override; + bool shouldBeInChatList() const override; + Dialogs::UnreadState chatListUnreadState() const override; + Dialogs::BadgesState chatListBadgesState() const override; + HistoryItem *chatListMessage() const override; + bool chatListMessageKnown() const override; + const QString &chatListName() const override; + const QString &chatListNameSortKey() const override; + int chatListNameVersion() const override; + const base::flat_set &chatListNameWords() const override; + const base::flat_set &chatListFirstLetters() const override; + + void chatListPreloadData() override; + void paintUserpic( + Painter &p, + Ui::PeerUserpicView &view, + const Dialogs::Ui::PaintContext &context) const override; + +private: + enum class Flag : uchar { + ResolveChatListMessage = (1 << 0), + FullLoaded = (1 << 1), + }; + friend inline constexpr bool is_flag_type(Flag) { return true; } + using Flags = base::flags; + + bool hasOrphanMediaGroupPart() const; + void allowChatListMessageResolve(); + void resolveChatListMessageGroup(); + + const not_null _history; + + std::vector> _items; + std::optional _fullCount; + rpl::event_stream<> _changed; + Dialogs::Ui::MessageView _lastItemDialogsView; + Flags _flags; + +}; + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_scheduled_messages.cpp b/Telegram/SourceFiles/data/data_scheduled_messages.cpp index 7f5821b7c7649..f122f05b90500 100644 --- a/Telegram/SourceFiles/data/data_scheduled_messages.cpp +++ b/Telegram/SourceFiles/data/data_scheduled_messages.cpp @@ -68,6 +68,7 @@ constexpr auto kRequestTimeLimit = 60 * crl::time(1000); data.vid(), data.vfrom_id() ? *data.vfrom_id() : MTPPeer(), data.vpeer_id(), + data.vsaved_peer_id() ? *data.vsaved_peer_id() : MTPPeer(), data.vfwd_from() ? *data.vfwd_from() : MTPMessageFwdHeader(), MTP_long(data.vvia_bot_id().value_or_empty()), data.vreply_to() ? *data.vreply_to() : MTPMessageReplyHeader(), @@ -216,6 +217,7 @@ void ScheduledMessages::sendNowSimpleMessage( update.vid(), peerToMTP(local->from()->id), peerToMTP(history->peer->id), + MTPPeer(), // saved_peer_id MTPMessageFwdHeader(), MTPlong(), // via_bot_id replyHeader, diff --git a/Telegram/SourceFiles/data/data_search_controller.cpp b/Telegram/SourceFiles/data/data_search_controller.cpp index 08fae4983a480..688ce4ae983e1 100644 --- a/Telegram/SourceFiles/data/data_search_controller.cpp +++ b/Telegram/SourceFiles/data/data_search_controller.cpp @@ -97,6 +97,7 @@ std::optional PrepareSearchRequest( peer->input, MTP_string(query), MTP_inputPeerEmpty(), + MTPInputPeer(), // saved_peer_id MTP_int(topicRootId), filter, MTP_int(0), // min_date diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index a6fdfc1ee72e0..6001a46c2f3ff 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -60,6 +60,8 @@ For license and copyright information please follow this link: #include "data/data_emoji_statuses.h" #include "data/data_forum_icons.h" #include "data/data_cloud_themes.h" +#include "data/data_saved_messages.h" +#include "data/data_saved_sublist.h" #include "data/data_stories.h" #include "data/data_streaming.h" #include "data/data_media_rotation.h" @@ -261,7 +263,8 @@ Session::Session(not_null session) , _forumIcons(std::make_unique(this)) , _notifySettings(std::make_unique(this)) , _customEmojiManager(std::make_unique(this)) -, _stories(std::make_unique(this)) { +, _stories(std::make_unique(this)) +, _savedMessages(std::make_unique(this)) { _cache->open(_session->local().cacheKey()); _bigFileCache->open(_session->local().cacheBigFileKey()); @@ -845,6 +848,7 @@ not_null Session::processChat(const MTPChat &data) { const auto wasCallNotEmpty = Data::ChannelHasActiveCall(channel); + channel->updateLevelHint(data.vlevel().value_or_empty()); if (const auto count = data.vparticipants_count()) { channel->setMembersCount(count->v); } @@ -854,6 +858,11 @@ not_null Session::processChat(const MTPChat &data) { channel->setDefaultRestrictions(ChatRestrictions()); } + if (const auto &status = data.vemoji_status()) { + channel->setEmojiStatus(*status); + } else { + channel->setEmojiStatus(0); + } if (minimal) { if (channel->input.type() == mtpc_inputPeerEmpty || channel->inputChannel.type() == mtpc_inputChannelEmpty) { @@ -1706,6 +1715,11 @@ void Session::requestItemRepaint(not_null item) { topic->updateChatListEntry(); } } + if (const auto sublist = item->savedSublist()) { + if (sublist->lastItemDialogsView().dependsOn(item)) { + sublist->updateChatListEntry(); + } + } } rpl::producer> Session::itemRepaintRequest() const { @@ -2093,13 +2107,17 @@ void Session::applyDialog( setPinnedFromEntryList(folder, data.is_pinned()); } -bool Session::pinnedCanPin(not_null thread) const { - if (const auto topic = thread->asTopic()) { +bool Session::pinnedCanPin(not_null entry) const { + if (const auto sublist = entry->asSublist()) { + const auto saved = &savedMessages(); + return pinnedChatsOrder(saved).size() < pinnedChatsLimit(saved); + } else if (const auto topic = entry->asTopic()) { const auto forum = topic->forum(); return pinnedChatsOrder(forum).size() < pinnedChatsLimit(forum); + } else { + const auto folder = entry->folder(); + return pinnedChatsOrder(folder).size() < pinnedChatsLimit(folder); } - const auto folder = thread->folder(); - return pinnedChatsOrder(folder).size() < pinnedChatsLimit(folder); } bool Session::pinnedCanPin( @@ -2131,6 +2149,11 @@ int Session::pinnedChatsLimit(not_null forum) const { return limits.topicsPinnedCurrent(); } +int Session::pinnedChatsLimit(not_null saved) const { + const auto limits = Data::PremiumLimits(_session); + return limits.savedSublistsPinnedCurrent(); +} + rpl::producer Session::maxPinnedChatsLimitValue( Data::Folder *folder) const { // Premium limit from appconfig. @@ -2171,6 +2194,20 @@ rpl::producer Session::maxPinnedChatsLimitValue( }); } +rpl::producer Session::maxPinnedChatsLimitValue( + not_null saved) const { + // Premium limit from appconfig. + // We always use premium limit in the MainList limit producer, + // because it slices the list to that limit. We don't want to slice + // premium-ly added chats from the pinned list because of sync issues. + return rpl::single(rpl::empty_value()) | rpl::then( + _session->account().appConfig().refreshed() + ) | rpl::map([=] { + const auto limits = Data::PremiumLimits(_session); + return limits.savedSublistsPinnedPremium(); + }); +} + const std::vector &Session::pinnedChatsOrder( Data::Folder *folder) const { return chatsList(folder)->pinned()->order(); @@ -2186,6 +2223,11 @@ const std::vector &Session::pinnedChatsOrder( return forum->topicsList()->pinned()->order(); } +const std::vector &Session::pinnedChatsOrder( + not_null saved) const { + return saved->chatsList()->pinned()->order(); +} + void Session::clearPinnedChats(Data::Folder *folder) { chatsList(folder)->pinned()->clear(); } @@ -2202,7 +2244,7 @@ void Session::reorderTwoPinnedChats( ? topic->forum()->topicsList() : filterId ? chatsFilters().chatsList(filterId) - : chatsList(key1.entry()->folder()); + : chatsListFor(key1.entry()); list->pinned()->reorder(key1, key2); notifyPinnedDialogsOrderUpdated(); } @@ -3416,7 +3458,7 @@ void Session::webpageApplyFields( data.vid().v, }; if (const auto embed = data.vstory()) { - story = stories().applyFromWebpage( + story = stories().applySingle( peerFromMTP(data.vpeer()), *embed); } else if (const auto maybe = stories().lookup(storyId)) { @@ -4192,6 +4234,8 @@ not_null Session::chatsListFor( const auto topic = entry->asTopic(); return topic ? topic->forum()->topicsList() + : entry->asSublist() + ? _savedMessages->chatsList() : chatsList(entry->folder()); } @@ -4388,6 +4432,7 @@ void Session::insertCheckedServiceNotification( MTP_int(0), // Not used (would've been trimmed to 32 bits). peerToMTP(PeerData::kServiceNotificationsId), peerToMTP(PeerData::kServiceNotificationsId), + MTPPeer(), // saved_peer_id MTPMessageFwdHeader(), MTPlong(), // via_bot_id MTPMessageReplyHeader(), diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index ea93cc178e9c0..244fa1524d689 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -61,6 +61,7 @@ class GroupCall; class NotifySettings; class CustomEmojiManager; class Stories; +class SavedMessages; struct RepliesReadTillUpdate { FullMsgId id; @@ -137,6 +138,9 @@ class Session final { [[nodiscard]] Stories &stories() const { return *_stories; } + [[nodiscard]] SavedMessages &savedMessages() const { + return *_savedMessages; + } [[nodiscard]] MsgId nextNonHistoryEntryId() { return ++_nonHistoryEntryId; @@ -345,25 +349,31 @@ class Session final { const QVector &dialogs, std::optional count = std::nullopt); - [[nodiscard]] bool pinnedCanPin(not_null thread) const; + [[nodiscard]] bool pinnedCanPin(not_null entry) const; [[nodiscard]] bool pinnedCanPin( FilterId filterId, not_null history) const; [[nodiscard]] int pinnedChatsLimit(Folder *folder) const; [[nodiscard]] int pinnedChatsLimit(FilterId filterId) const; [[nodiscard]] int pinnedChatsLimit(not_null forum) const; + [[nodiscard]] int pinnedChatsLimit( + not_null saved) const; [[nodiscard]] rpl::producer maxPinnedChatsLimitValue( Folder *folder) const; [[nodiscard]] rpl::producer maxPinnedChatsLimitValue( FilterId filterId) const; [[nodiscard]] rpl::producer maxPinnedChatsLimitValue( not_null forum) const; + [[nodiscard]] rpl::producer maxPinnedChatsLimitValue( + not_null saved) const; [[nodiscard]] const std::vector &pinnedChatsOrder( Folder *folder) const; [[nodiscard]] const std::vector &pinnedChatsOrder( not_null forum) const; [[nodiscard]] const std::vector &pinnedChatsOrder( FilterId filterId) const; + [[nodiscard]] const std::vector &pinnedChatsOrder( + not_null saved) const; void setChatPinned(Dialogs::Key key, FilterId filterId, bool pinned); void setPinnedFromEntryList(Dialogs::Key key, bool pinned); void clearPinnedChats(Folder *folder); @@ -1041,6 +1051,7 @@ class Session final { const std::unique_ptr _notifySettings; const std::unique_ptr _customEmojiManager; const std::unique_ptr _stories; + const std::unique_ptr _savedMessages; MsgId _nonHistoryEntryId = ServerMaxMsgId.bare + ScheduledMsgIdsRange; diff --git a/Telegram/SourceFiles/data/data_statistics.h b/Telegram/SourceFiles/data/data_statistics.h index 4c0f327b62103..9d2b2c74ab0d3 100644 --- a/Telegram/SourceFiles/data/data_statistics.h +++ b/Telegram/SourceFiles/data/data_statistics.h @@ -148,11 +148,7 @@ struct RecentPostId final { }; struct PublicForwardsSlice final { - struct OffsetToken final { - int rate = 0; - FullMsgId fullId; - QString storyOffset; - }; + using OffsetToken = QString; QVector list; int total = 0; bool allLoaded = false; diff --git a/Telegram/SourceFiles/data/data_statistics_chart.cpp b/Telegram/SourceFiles/data/data_statistics_chart.cpp index 62f0354dd47f0..5faf7bd203a02 100644 --- a/Telegram/SourceFiles/data/data_statistics_chart.cpp +++ b/Telegram/SourceFiles/data/data_statistics_chart.cpp @@ -8,6 +8,8 @@ For license and copyright information please follow this link: #include "data/data_statistics_chart.h" #include "statistics/statistics_format_values.h" +#include "styles/style_basic.h" +#include "styles/style_statistics.h" #include #include @@ -46,6 +48,11 @@ void StatisticalChart::measure() { const auto dateCount = int((end - start) / timeStep) + 10; daysLookup.reserve(dateCount); constexpr auto kOneDay = 3600 * 24 * 1000; + + // View data. + auto maxWidth = 0; + const auto &defaultFont = st::statisticsDetailsBottomCaptionStyle.font; + for (auto i = 0; i < dateCount; i++) { const auto r = (start + (i * timeStep)) / 1000; if (timeStep == 1) { @@ -59,7 +66,9 @@ void StatisticalChart::measure() { } else { daysLookup.push_back(Statistic::LangDayMonth(r)); } + maxWidth = std::max(maxWidth, defaultFont->width(daysLookup.back())); } + dayStringMaxWidth = maxWidth; oneDayPercentage = timeStep / float64(end - start); } diff --git a/Telegram/SourceFiles/data/data_statistics_chart.h b/Telegram/SourceFiles/data/data_statistics_chart.h index 9fd644cd71b9b..d7448e273683d 100644 --- a/Telegram/SourceFiles/data/data_statistics_chart.h +++ b/Telegram/SourceFiles/data/data_statistics_chart.h @@ -66,6 +66,9 @@ struct StatisticalChart { bool hasPercentages = false; bool weekFormat = false; + // View data. + int dayStringMaxWidth = 0; + }; struct StatisticalGraph final { diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp index b37afa6d5ea9b..abf9227fd74db 100644 --- a/Telegram/SourceFiles/data/data_stories.cpp +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -220,7 +220,7 @@ void Stories::apply(not_null peer, const MTPPeerStories *data) { } } -Story *Stories::applyFromWebpage(PeerId peerId, const MTPstoryItem &story) { +Story *Stories::applySingle(PeerId peerId, const MTPstoryItem &story) { const auto idDates = parseAndApply( _owner->peer(peerId), story, @@ -1290,7 +1290,6 @@ void Stories::sendIncrementViewsRequests() { if (_incrementViewsPending.empty()) { return; } - auto ids = QVector(); struct Prepared { PeerId peer = 0; QVector ids; @@ -1353,6 +1352,96 @@ void Stories::loadViewsSlice( } } +void Stories::loadReactionsSlice( + not_null peer, + StoryId id, + QString offset, + Fn done) { + Expects(peer->isChannel()); + + if (_reactionsStoryPeer == peer + && _reactionsStoryId == id + && _reactionsOffset == offset) { + if (_reactionsRequestId) { + _reactionsDone = std::move(done); + } + return; + } + _reactionsStoryPeer = peer; + _reactionsStoryId = id; + _reactionsOffset = offset; + _reactionsDone = std::move(done); + + using Flag = MTPstories_GetStoryReactionsList::Flag; + const auto api = &_owner->session().api(); + _owner->session().api().request(_reactionsRequestId).cancel(); + _reactionsRequestId = api->request(MTPstories_GetStoryReactionsList( + MTP_flags(offset.isEmpty() ? Flag() : Flag::f_offset), + _reactionsStoryPeer->input, + MTP_int(_reactionsStoryId), + MTPReaction(), + MTP_string(_reactionsOffset), + MTP_int(kViewsPerPage) + )).done([=](const MTPstories_StoryReactionsList &result) { + _reactionsRequestId = 0; + + const auto &data = result.data(); + auto slice = StoryViews{ + .nextOffset = data.vnext_offset().value_or_empty(), + .reactions = data.vcount().v, + .total = data.vcount().v, + }; + _owner->processUsers(data.vusers()); + _owner->processChats(data.vchats()); + slice.list.reserve(data.vreactions().v.size()); + for (const auto &reaction : data.vreactions().v) { + reaction.match([&](const MTPDstoryReaction &data) { + slice.list.push_back({ + .peer = _owner->peer(peerFromMTP(data.vpeer_id())), + .reaction = ReactionFromMTP(data.vreaction()), + .date = data.vdate().v, + }); + }, [&](const MTPDstoryReactionPublicRepost &data) { + const auto story = applySingle( + peerFromMTP(data.vpeer_id()), + data.vstory()); + if (story) { + slice.list.push_back({ + .peer = story->peer(), + .repostId = story->id(), + }); + } + }, [&](const MTPDstoryReactionPublicForward &data) { + const auto item = _owner->addNewMessage( + data.vmessage(), + {}, + NewMessageType::Existing); + if (item) { + slice.list.push_back({ + .peer = item->history()->peer, + .forwardId = item->id, + }); + } + }); + } + const auto fullId = FullStoryId{ + .peer = _reactionsStoryPeer->id, + .story = _reactionsStoryId, + }; + if (const auto story = lookup(fullId)) { + (*story)->applyChannelReactionsSlice(_reactionsOffset, slice); + } + if (const auto done = base::take(_reactionsDone)) { + done(std::move(slice)); + } + }).fail([=] { + _reactionsRequestId = 0; + if (const auto done = base::take(_reactionsDone)) { + done({}); + } + }).send(); +} + void Stories::sendViewsSliceRequest() { Expects(_viewsStoryPeer != nullptr); Expects(_viewsStoryPeer->isSelf()); @@ -1374,17 +1463,43 @@ void Stories::sendViewsSliceRequest() { auto slice = StoryViews{ .nextOffset = data.vnext_offset().value_or_empty(), .reactions = data.vreactions_count().v, + .forwards = data.vforwards_count().v, + .views = data.vviews_count().v, .total = data.vcount().v, }; _owner->processUsers(data.vusers()); + _owner->processChats(data.vchats()); slice.list.reserve(data.vviews().v.size()); for (const auto &view : data.vviews().v) { - slice.list.push_back({ - .peer = _owner->peer(peerFromUser(view.data().vuser_id())), - .reaction = (view.data().vreaction() - ? ReactionFromMTP(*view.data().vreaction()) - : Data::ReactionId()), - .date = view.data().vdate().v, + view.match([&](const MTPDstoryView &data) { + slice.list.push_back({ + .peer = _owner->peer(peerFromUser(data.vuser_id())), + .reaction = (data.vreaction() + ? ReactionFromMTP(*data.vreaction()) + : Data::ReactionId()), + .date = data.vdate().v, + }); + }, [&](const MTPDstoryViewPublicRepost &data) { + const auto story = applySingle( + peerFromMTP(data.vpeer_id()), + data.vstory()); + if (story) { + slice.list.push_back({ + .peer = story->peer(), + .repostId = story->id(), + }); + } + }, [&](const MTPDstoryViewPublicForward &data) { + const auto item = _owner->addNewMessage( + data.vmessage(), + {}, + NewMessageType::Existing); + if (item) { + slice.list.push_back({ + .peer = item->history()->peer, + .forwardId = item->id, + }); + } }); } const auto fullId = FullStoryId{ diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h index 16cc27d38f971..1cc3bb30d2251 100644 --- a/Telegram/SourceFiles/data/data_stories.h +++ b/Telegram/SourceFiles/data/data_stories.h @@ -149,7 +149,7 @@ class Stories final : public base::has_weak_ptr { void apply(const MTPDupdateReadStories &data); void apply(const MTPStoriesStealthMode &stealthMode); void apply(not_null peer, const MTPPeerStories *data); - Story *applyFromWebpage(PeerId peerId, const MTPstoryItem &story); + Story *applySingle(PeerId peerId, const MTPstoryItem &story); void loadAround(FullStoryId id, StoriesContext context); const StoriesSource *source(PeerId id) const; @@ -182,6 +182,11 @@ class Stories final : public base::has_weak_ptr { StoryId id, QString offset, Fn done); + void loadReactionsSlice( + not_null peer, + StoryId id, + QString offset, + Fn done); [[nodiscard]] bool hasArchive(not_null peer) const; @@ -379,6 +384,12 @@ class Stories final : public base::has_weak_ptr { Fn _viewsDone; mtpRequestId _viewsRequestId = 0; + PeerData *_reactionsStoryPeer = nullptr; + StoryId _reactionsStoryId = 0; + QString _reactionsOffset; + Fn _reactionsDone; + mtpRequestId _reactionsRequestId = 0; + base::flat_set _preloaded; std::vector _toPreloadSources[kStorySourcesListCount]; std::vector _toPreloadViewer; diff --git a/Telegram/SourceFiles/data/data_story.cpp b/Telegram/SourceFiles/data/data_story.cpp index 79b8f7fba87cc..7631892a3e571 100644 --- a/Telegram/SourceFiles/data/data_story.cpp +++ b/Telegram/SourceFiles/data/data_story.cpp @@ -81,8 +81,11 @@ using UpdateFlag = StoryUpdate::Flag; }, [](const MTPDgeoPointEmpty &) { }); }, [&](const MTPDmediaAreaSuggestedReaction &data) { + }, [&](const MTPDmediaAreaChannelPost &data) { + }, [&](const MTPDinputMediaAreaChannelPost &data) { + LOG(("API Error: Unexpected inputMediaAreaChannelPost from API.")); }, [&](const MTPDinputMediaAreaVenue &data) { - LOG(("API Error: Unexpected inputMediaAreaVenue in API data.")); + LOG(("API Error: Unexpected inputMediaAreaVenue from API.")); }); return result; } @@ -99,8 +102,32 @@ using UpdateFlag = StoryUpdate::Flag; .flipped = data.is_flipped(), .dark = data.is_dark(), }); + }, [&](const MTPDmediaAreaChannelPost &data) { + }, [&](const MTPDinputMediaAreaChannelPost &data) { + LOG(("API Error: Unexpected inputMediaAreaChannelPost from API.")); }, [&](const MTPDinputMediaAreaVenue &data) { - LOG(("API Error: Unexpected inputMediaAreaVenue in API data.")); + LOG(("API Error: Unexpected inputMediaAreaVenue from API.")); + }); + return result; +} + +[[nodiscard]] auto ParseChannelPost(const MTPMediaArea &area) +-> std::optional { + auto result = std::optional(); + area.match([&](const MTPDmediaAreaVenue &data) { + }, [&](const MTPDmediaAreaGeoPoint &data) { + }, [&](const MTPDmediaAreaSuggestedReaction &data) { + }, [&](const MTPDmediaAreaChannelPost &data) { + result.emplace(ChannelPost{ + .area = ParseArea(data.vcoordinates()), + .itemId = FullMsgId( + peerFromChannel(data.vchannel_id()), + data.vmsg_id().v), + }); + }, [&](const MTPDinputMediaAreaChannelPost &data) { + LOG(("API Error: Unexpected inputMediaAreaChannelPost from API.")); + }, [&](const MTPDinputMediaAreaVenue &data) { + LOG(("API Error: Unexpected inputMediaAreaVenue from API.")); }); return result; } @@ -130,6 +157,13 @@ using UpdateFlag = StoryUpdate::Flag; return {}; } +[[nodiscard]] bool RepostModified(const MTPDstoryItem &data) { + if (const auto forwarded = data.vfwd_from()) { + return forwarded->data().is_modified(); + } + return false; +} + } // namespace class StoryPreload::LoadTask final : private Storage::DownloadMtprotoTask { @@ -151,7 +185,7 @@ class StoryPreload::LoadTask final : private Storage::DownloadMtprotoTask { Fn _done; base::flat_set _requestedOffsets; int64 _full = 0; - int _nextRequestOffset = 0; + int _nextRequestOffset = 0; bool _finished = false; bool _failed = false; @@ -245,7 +279,8 @@ Story::Story( , _repostSourceName(RepostSourceName(data)) , _repostSourceId(RepostSourceId(data)) , _date(data.vdate().v) -, _expires(data.vexpire_date().v) { +, _expires(data.vexpire_date().v) +, _repostModified(RepostModified(data)) { applyFields(std::move(media), data, now, true); } @@ -478,10 +513,22 @@ const StoryViews &Story::viewsList() const { return _views; } -int Story::views() const { +const StoryViews &Story::channelReactionsList() const { + return _channelReactions; +} + +int Story::interactions() const { return _views.total; } +int Story::views() const { + return _views.views; +} + +int Story::forwards() const { + return _views.forwards; +} + int Story::reactions() const { return _views.reactions; } @@ -490,12 +537,19 @@ void Story::applyViewsSlice( const QString &offset, const StoryViews &slice) { const auto changed = (_views.reactions != slice.reactions) + || (_views.views != slice.views) + || (_views.forwards != slice.forwards) || (_views.total != slice.total); _views.reactions = slice.reactions; + _views.forwards = slice.forwards; + _views.views = slice.views; _views.total = slice.total; _views.known = true; if (offset.isEmpty()) { _views = slice; + if (!_channelReactions.total) { + _channelReactions.total = _views.reactions + _views.forwards; + } } else if (_views.nextOffset == offset) { _views.list.insert( end(_views.list), @@ -509,6 +563,15 @@ void Story::applyViewsSlice( _views.list, Data::ReactionId(), &StoryView::reaction); + _views.forwards = _views.total + - ranges::count( + _views.list, + 0, + [](const StoryView &view) { + return view.repostId + ? view.repostId + : view.forwardId.bare; + }); } } const auto known = int(_views.list.size()); @@ -535,6 +598,33 @@ void Story::applyViewsSlice( } } +void Story::applyChannelReactionsSlice( + const QString &offset, + const StoryViews &slice) { + const auto changed = (_channelReactions.reactions != slice.reactions) + || (_channelReactions.total != slice.total); + _channelReactions.reactions = slice.reactions; + _channelReactions.total = slice.total; + _channelReactions.known = true; + if (offset.isEmpty()) { + _channelReactions = slice; + } else if (_channelReactions.nextOffset == offset) { + _channelReactions.list.insert( + end(_channelReactions.list), + begin(slice.list), + end(slice.list)); + _channelReactions.nextOffset = slice.nextOffset; + if (_channelReactions.nextOffset.isEmpty()) { + _channelReactions.total = int(_channelReactions.list.size()); + } + } + if (changed) { + _peer->session().changes().storyUpdated( + this, + UpdateFlag::ViewsChanged); + } +} + const std::vector &Story::locations() const { return _locations; } @@ -543,6 +633,10 @@ const std::vector &Story::suggestedReactions() const { return _suggestedReactions; } +const std::vector &Story::channelPosts() const { + return _channelPosts; +} + void Story::applyChanges( StoryMedia media, const MTPDstoryItem &data, @@ -555,6 +649,7 @@ Story::ViewsCounts Story::parseViewsCounts( const Data::ReactionId &mine) { auto result = ViewsCounts{ .views = data.vviews_count().v, + .forwards = data.vforwards_count().value_or_empty(), .reactions = data.vreactions_count().value_or_empty(), }; if (const auto list = data.vrecent_viewers()) { @@ -633,6 +728,7 @@ void Story::applyFields( viewsKnown = true; } else { counts.views = _views.total; + counts.forwards = _views.forwards; counts.reactions = _views.reactions; counts.viewers = _recentViewers; for (const auto &suggested : _suggestedReactions) { @@ -643,9 +739,8 @@ void Story::applyFields( } auto locations = std::vector(); auto suggestedReactions = std::vector(); + auto channelPosts = std::vector(); if (const auto areas = data.vmedia_areas()) { - locations.reserve(areas->v.size()); - suggestedReactions.reserve(areas->v.size()); for (const auto &area : areas->v) { if (const auto location = ParseLocation(area)) { locations.push_back(*location); @@ -656,6 +751,8 @@ void Story::applyFields( reaction->count = i->second; } suggestedReactions.push_back(*reaction); + } else if (auto post = ParseChannelPost(area)) { + channelPosts.push_back(*post); } } } @@ -667,6 +764,7 @@ void Story::applyFields( const auto locationsChanged = (_locations != locations); const auto suggestedReactionsChanged = (_suggestedReactions != suggestedReactions); + const auto channelPostsChanged = (_channelPosts != channelPosts); const auto reactionChanged = (_sentReactionId != reaction); _out = out; @@ -689,6 +787,9 @@ void Story::applyFields( if (suggestedReactionsChanged) { _suggestedReactions = std::move(suggestedReactions); } + if (channelPostsChanged) { + _channelPosts = std::move(channelPosts); + } if (reactionChanged) { _sentReactionId = reaction; } @@ -697,7 +798,8 @@ void Story::applyFields( const auto changed = editedChanged || captionChanged || mediaChanged - || locationsChanged; + || locationsChanged + || channelPostsChanged; const auto reactionsChanged = reactionChanged || suggestedReactionsChanged; if (!initial && (changed || reactionsChanged)) { @@ -717,17 +819,27 @@ void Story::applyFields( } void Story::updateViewsCounts(ViewsCounts &&counts, bool known, bool initial) { - const auto viewsChanged = (_views.total != counts.views) + const auto total = _views.total + ? _views.total + : (counts.views + counts.forwards); + const auto viewsChanged = (_views.total != total) + || (_views.forwards != counts.forwards) || (_views.reactions != counts.reactions) || (_recentViewers != counts.viewers); if (_views.reactions != counts.reactions - || _views.total != counts.views + || _views.forwards != counts.forwards + || _views.total != total || _views.known != known) { _views = StoryViews{ .reactions = counts.reactions, - .total = counts.views, + .forwards = counts.forwards, + .views = counts.views, + .total = total, .known = known, }; + if (!_channelReactions.total) { + _channelReactions.total = _views.reactions + _views.forwards; + } } if (viewsChanged) { _recentViewers = std::move(counts.viewers); @@ -762,6 +874,10 @@ bool Story::repost() const { return _repostSourcePeer || !_repostSourceName.isEmpty(); } +bool Story::repostModified() const { + return _repostModified; +} + PeerData *Story::repostSourcePeer() const { return _repostSourcePeer; } diff --git a/Telegram/SourceFiles/data/data_story.h b/Telegram/SourceFiles/data/data_story.h index b97e123798cb0..4fb4796b225d6 100644 --- a/Telegram/SourceFiles/data/data_story.h +++ b/Telegram/SourceFiles/data/data_story.h @@ -61,6 +61,8 @@ struct StoryMedia { struct StoryView { not_null peer; Data::ReactionId reaction; + StoryId repostId = 0; + MsgId forwardId = 0; TimeId date = 0; friend inline bool operator==(StoryView, StoryView) = default; @@ -70,6 +72,8 @@ struct StoryViews { std::vector list; QString nextOffset; int reactions = 0; + int forwards = 0; + int views = 0; int total = 0; bool known = false; }; @@ -109,6 +113,15 @@ struct SuggestedReaction { const SuggestedReaction &) = default; }; +struct ChannelPost { + StoryArea area; + FullMsgId itemId; + + friend inline bool operator==( + const ChannelPost &, + const ChannelPost &) = default; +}; + class Story final { public: Story( @@ -166,13 +179,21 @@ class Story final { [[nodiscard]] auto recentViewers() const -> const std::vector> &; [[nodiscard]] const StoryViews &viewsList() const; + [[nodiscard]] const StoryViews &channelReactionsList() const; + [[nodiscard]] int interactions() const; [[nodiscard]] int views() const; + [[nodiscard]] int forwards() const; [[nodiscard]] int reactions() const; void applyViewsSlice(const QString &offset, const StoryViews &slice); + void applyChannelReactionsSlice( + const QString &offset, + const StoryViews &slice); [[nodiscard]] const std::vector &locations() const; [[nodiscard]] auto suggestedReactions() const -> const std::vector &; + [[nodiscard]] auto channelPosts() const + -> const std::vector &; void applyChanges( StoryMedia media, @@ -182,6 +203,7 @@ class Story final { [[nodiscard]] TimeId lastUpdateTime() const; [[nodiscard]] bool repost() const; + [[nodiscard]] bool repostModified() const; [[nodiscard]] PeerData *repostSourcePeer() const; [[nodiscard]] QString repostSourceName() const; [[nodiscard]] StoryId repostSourceId() const; @@ -189,6 +211,7 @@ class Story final { private: struct ViewsCounts { int views = 0; + int forwards = 0; int reactions = 0; base::flat_map reactionsCounts; std::vector> viewers; @@ -217,7 +240,9 @@ class Story final { std::vector> _recentViewers; std::vector _locations; std::vector _suggestedReactions; + std::vector _channelPosts; StoryViews _views; + StoryViews _channelReactions; const TimeId _date = 0; const TimeId _expires = 0; TimeId _lastUpdateTime = 0; @@ -227,6 +252,7 @@ class Story final { bool _privacyCloseFriends : 1 = false; bool _privacyContacts : 1 = false; bool _privacySelectedContacts : 1 = false; + const bool _repostModified : 1 = false; bool _noForwards : 1 = false; bool _edited : 1 = false; diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp index 33dd42ca5064f..3f83a9e945303 100644 --- a/Telegram/SourceFiles/data/data_user.cpp +++ b/Telegram/SourceFiles/data/data_user.cpp @@ -15,7 +15,6 @@ For license and copyright information please follow this link: #include "data/data_peer_bot_command.h" #include "data/data_photo.h" #include "data/data_stories.h" -#include "data/data_emoji_statuses.h" #include "data/data_wall_paper.h" #include "data/notify/data_notify_settings.h" #include "history/history.h" @@ -73,23 +72,6 @@ void UserData::setPhoto(const MTPUserProfilePhoto &photo) { }); } -void UserData::setEmojiStatus(const MTPEmojiStatus &status) { - const auto parsed = Data::ParseEmojiStatus(status); - setEmojiStatus(parsed.id, parsed.until); -} - -void UserData::setEmojiStatus(DocumentId emojiStatusId, TimeId until) { - if (_emojiStatusId != emojiStatusId) { - _emojiStatusId = emojiStatusId; - session().changes().peerUpdated(this, UpdateFlag::EmojiStatus); - } - owner().emojiStatuses().registerAutomaticClear(this, until); -} - -DocumentId UserData::emojiStatusId() const { - return _emojiStatusId; -} - auto UserData::unavailableReasons() const -> const std::vector & { return _unavailableReasons; @@ -386,7 +368,7 @@ QString UserData::username() const { } QString UserData::editableUsername() const { - return _username.editableUsername();; + return _username.editableUsername(); } const std::vector &UserData::usernames() const { diff --git a/Telegram/SourceFiles/data/data_user.h b/Telegram/SourceFiles/data/data_user.h index 96811051ee65d..328661260549f 100644 --- a/Telegram/SourceFiles/data/data_user.h +++ b/Telegram/SourceFiles/data/data_user.h @@ -76,7 +76,6 @@ class UserData final : public PeerData { UserData(not_null owner, PeerId id); void setPhoto(const MTPUserProfilePhoto &photo); - void setEmojiStatus(const MTPEmojiStatus &status); void setName( const QString &newFirstName, @@ -85,9 +84,6 @@ class UserData final : public PeerData { const QString &newUsername); void setUsernames(const Data::Usernames &newUsernames); - void setEmojiStatus(DocumentId emojiStatusId, TimeId until = 0); - [[nodiscard]] DocumentId emojiStatusId() const; - void setUsername(const QString &username); void setPhone(const QString &newPhone); void setBotInfoVersion(int version); @@ -199,8 +195,6 @@ class UserData final : public PeerData { static constexpr auto kInaccessibleAccessHashOld = 0xFFFFFFFFFFFFFFFFULL; - DocumentId _emojiStatusId = 0; - }; namespace Data { diff --git a/Telegram/SourceFiles/data/data_wall_paper.cpp b/Telegram/SourceFiles/data/data_wall_paper.cpp index f8532a15860c8..1bfd653359919 100644 --- a/Telegram/SourceFiles/data/data_wall_paper.cpp +++ b/Telegram/SourceFiles/data/data_wall_paper.cpp @@ -198,9 +198,14 @@ WallPaperId WallPaper::id() const { return _id; } +QString WallPaper::emojiId() const { + return _emojiId; +} + bool WallPaper::equals(const WallPaper &paper) const { return (_flags == paper._flags) && (_slug == paper._slug) + && (_emojiId == paper._emojiId) && (_backgroundColors == paper._backgroundColors) && (_rotation == paper._rotation) && (_intensity == paper._intensity) @@ -362,6 +367,7 @@ MTPWallPaperSettings WallPaper::mtpSettings() const { MTP_flags((_blurred ? Flag::f_blur : Flag(0)) | Flag::f_intensity | Flag::f_rotation + | (_emojiId.isEmpty() ? Flag() : Flag::f_emoticon) | flagForIndex(0) | flagForIndex(1) | flagForIndex(2) @@ -371,7 +377,8 @@ MTPWallPaperSettings WallPaper::mtpSettings() const { serializeForIndex(2), serializeForIndex(3), MTP_int(_intensity), - MTP_int(_rotation)); + MTP_int(_rotation), + MTP_string(_emojiId)); } WallPaper WallPaper::withUrlParams( @@ -519,6 +526,7 @@ std::optional WallPaper::Create(const MTPDwallPaperNoFile &data) { if (const auto rotation = data.vrotation()) { result._rotation = rotation->v; } + result._emojiId = qs(data.vemoticon().value_or_empty()); }); } return result; @@ -690,6 +698,12 @@ std::optional WallPaper::FromColorsSlug(const QString &slug) { return result; } +WallPaper WallPaper::FromEmojiId(const QString &emojiId) { + auto result = WallPaper(0); + result._emojiId = emojiId; + return result; +} + WallPaper WallPaper::ConstructDefault() { auto result = WallPaper( kDefaultBackground diff --git a/Telegram/SourceFiles/data/data_wall_paper.h b/Telegram/SourceFiles/data/data_wall_paper.h index fb98eb9de7a71..e7bfd2d511d48 100644 --- a/Telegram/SourceFiles/data/data_wall_paper.h +++ b/Telegram/SourceFiles/data/data_wall_paper.h @@ -45,6 +45,7 @@ class WallPaper { [[nodiscard]] bool equals(const WallPaper &paper) const; [[nodiscard]] WallPaperId id() const; + [[nodiscard]] QString emojiId() const; [[nodiscard]] bool isNull() const; [[nodiscard]] QString key() const; [[nodiscard]] const std::vector backgroundColors() const; @@ -102,6 +103,7 @@ class WallPaper { qint32 legacyId); [[nodiscard]] static std::optional FromColorsSlug( const QString &slug); + [[nodiscard]] static WallPaper FromEmojiId(const QString &emojiId); [[nodiscard]] static WallPaper ConstructDefault(); private: @@ -114,6 +116,7 @@ class WallPaper { UserId _ownerId = 0; WallPaperFlags _flags; QString _slug; + QString _emojiId; std::vector _backgroundColors; int _rotation = 0; diff --git a/Telegram/SourceFiles/data/data_web_page.cpp b/Telegram/SourceFiles/data/data_web_page.cpp index 7aaeaf5d79933..6174c138bd3a0 100644 --- a/Telegram/SourceFiles/data/data_web_page.cpp +++ b/Telegram/SourceFiles/data/data_web_page.cpp @@ -141,7 +141,7 @@ WebPageType ParseWebPageType( const QString &type, const QString &embedUrl, bool hasIV) { - if (type == u"video"_q || !embedUrl.isEmpty()) { + if (type == u"video"_q || type == u"gif"_q || !embedUrl.isEmpty()) { return WebPageType::Video; } else if (type == u"photo"_q) { return WebPageType::Photo; diff --git a/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp b/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp index 123621b37d4c2..1f80c53f636a2 100644 --- a/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp +++ b/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp @@ -349,7 +349,6 @@ void CustomEmojiLoader::check() { const auto tag = _tag; const auto sizeOverride = int(_sizeOverride); const auto size = FrameSizeFromTag(_tag, _sizeOverride); - auto bytes = Lottie::ReadContent(data, filepath); auto loader = [=] { return std::make_unique( document, diff --git a/Telegram/SourceFiles/data/stickers/data_stickers_set.cpp b/Telegram/SourceFiles/data/stickers/data_stickers_set.cpp index 347a3f13ca26d..3639e436a3248 100644 --- a/Telegram/SourceFiles/data/stickers/data_stickers_set.cpp +++ b/Telegram/SourceFiles/data/stickers/data_stickers_set.cpp @@ -54,7 +54,8 @@ StickersSetFlags ParseStickersSetFlags(const MTPDstickerSet &data) { | (data.is_emojis() ? Flag::Emoji : Flag()) | (data.vinstalled_date() ? Flag::Installed : Flag()) | (data.is_videos() ? Flag::Webm : Flag()) - | (data.is_text_color() ? Flag::TextColor : Flag()); + | (data.is_text_color() ? Flag::TextColor : Flag()) + | (data.is_channel_emoji_status() ? Flag::ChannelStatus : Flag()); } StickersSet::StickersSet( @@ -113,6 +114,10 @@ bool StickersSet::textColor() const { return flags & StickersSetFlag::TextColor; } +bool StickersSet::channelStatus() const { + return flags & StickersSetFlag::ChannelStatus; +} + void StickersSet::setThumbnail(const ImageWithLocation &data) { Data::UpdateCloudFile( _thumbnail, diff --git a/Telegram/SourceFiles/data/stickers/data_stickers_set.h b/Telegram/SourceFiles/data/stickers/data_stickers_set.h index bf9a6e4177f23..e9124471c2d60 100644 --- a/Telegram/SourceFiles/data/stickers/data_stickers_set.h +++ b/Telegram/SourceFiles/data/stickers/data_stickers_set.h @@ -58,6 +58,7 @@ enum class StickersSetFlag { Webm = (1 << 8), Emoji = (1 << 9), TextColor = (1 << 10), + ChannelStatus = (1 << 11), }; inline constexpr bool is_flag_type(StickersSetFlag) { return true; }; using StickersSetFlags = base::flags; @@ -86,6 +87,7 @@ class StickersSet final { [[nodiscard]] StickerSetIdentifier identifier() const; [[nodiscard]] StickersType type() const; [[nodiscard]] bool textColor() const; + [[nodiscard]] bool channelStatus() const; void setThumbnail(const ImageWithLocation &data); diff --git a/Telegram/SourceFiles/dialogs/dialogs.style b/Telegram/SourceFiles/dialogs/dialogs.style index 5743e74d67d03..4d379e4fb8f03 100644 --- a/Telegram/SourceFiles/dialogs/dialogs.style +++ b/Telegram/SourceFiles/dialogs/dialogs.style @@ -347,6 +347,7 @@ dialogsForumIcon: ThreeStateIcon { dialogsArchiveUserpic: icon {{ "archive_userpic", historyPeerUserpicFg }}; dialogsRepliesUserpic: icon {{ "replies_userpic", historyPeerUserpicFg }}; dialogsInaccessibleUserpic: icon {{ "dialogs/inaccessible_userpic", historyPeerUserpicFg }}; +dialogsHiddenAuthorUserpic: icon {{ "hidden_author_userpic", historyPeerUserpicFg }}; dialogsSendStateSkip: 20px; dialogsSendingIcon: ThreeStateIcon { @@ -396,7 +397,7 @@ dialogsPremiumIcon: ThreeStateIcon { historySendingIcon: icon {{ "dialogs/dialogs_sending", historySendingOutIconFg, point(5px, 5px) }}; historySendingInvertedIcon: icon {{ "dialogs/dialogs_sending", historySendingInvertedIconFg, point(5px, 5px) }}; historyViewsSendingIcon: icon {{ "dialogs/dialogs_sending", historySendingInIconFg, point(3px, 0px) }}; -historyViewsSendingInvertedIcon: icon {{ "dialogs/dialogs_sending", historySendingInvertedIconFg, point(3px, 0px) }}; +historyViewsSendingInvertedIcon: icon {{ "dialogs/dialogs_sending", historySendingInvertedIconFg, point(3px, 0px) }}; dialogsUpdateButton: FlatButton { color: activeButtonFg; diff --git a/Telegram/SourceFiles/dialogs/dialogs_entry.cpp b/Telegram/SourceFiles/dialogs/dialogs_entry.cpp index f2d5ebcd433c4..26f6e1fcba2f3 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_entry.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_entry.cpp @@ -14,6 +14,7 @@ For license and copyright information please follow this link: #include "data/data_folder.h" #include "data/data_forum_topic.h" #include "data/data_chat_filters.h" +#include "data/data_saved_sublist.h" #include "core/application.h" #include "core/core_settings.h" #include "mainwidget.h" @@ -83,6 +84,8 @@ Entry::Entry(not_null owner, Type type) ? (Flag::IsThread | Flag::IsHistory) : (type == Type::ForumTopic) ? Flag::IsThread + : (type == Type::SavedSublist) + ? Flag::IsSavedSublist : Flag(0)) { } @@ -109,7 +112,7 @@ Data::Forum *Entry::asForum() { } Data::Folder *Entry::asFolder() { - return (_flags & Flag::IsThread) + return (_flags & (Flag::IsThread | Flag::IsSavedSublist)) ? nullptr : static_cast(this); } @@ -126,6 +129,12 @@ Data::ForumTopic *Entry::asTopic() { : nullptr; } +Data::SavedSublist *Entry::asSublist() { + return (_flags & Flag::IsSavedSublist) + ? static_cast(this) + : nullptr; +} + const History *Entry::asHistory() const { return const_cast(this)->asHistory(); } @@ -146,6 +155,10 @@ const Data::ForumTopic *Entry::asTopic() const { return const_cast(this)->asTopic(); } +const Data::SavedSublist *Entry::asSublist() const { + return const_cast(this)->asSublist(); +} + void Entry::pinnedIndexChanged(FilterId filterId, int was, int now) { if (!filterId && session().supportMode()) { // Force reorder in support mode. diff --git a/Telegram/SourceFiles/dialogs/dialogs_entry.h b/Telegram/SourceFiles/dialogs/dialogs_entry.h index 3ad4281f3000a..56e84f48c77cb 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_entry.h +++ b/Telegram/SourceFiles/dialogs/dialogs_entry.h @@ -25,6 +25,7 @@ class Session; class Forum; class Folder; class ForumTopic; +class SavedSublist; } // namespace Data namespace Ui { @@ -151,6 +152,7 @@ class Entry : public base::has_weak_ptr { History, Folder, ForumTopic, + SavedSublist, }; Entry(not_null owner, Type type); virtual ~Entry(); @@ -163,12 +165,14 @@ class Entry : public base::has_weak_ptr { Data::Folder *asFolder(); Data::Thread *asThread(); Data::ForumTopic *asTopic(); + Data::SavedSublist *asSublist(); const History *asHistory() const; const Data::Forum *asForum() const; const Data::Folder *asFolder() const; const Data::Thread *asThread() const; const Data::ForumTopic *asTopic() const; + const Data::SavedSublist *asSublist() const; PositionChange adjustByPosInChatList( FilterId filterId, @@ -206,27 +210,29 @@ class Entry : public base::has_weak_ptr { void setChatListTimeId(TimeId date); virtual void updateChatListExistence(); bool needUpdateInChatList() const; - virtual TimeId adjustedChatListTimeId() const; + [[nodiscard]] virtual TimeId adjustedChatListTimeId() const; - virtual int fixedOnTopIndex() const = 0; + [[nodiscard]] virtual int fixedOnTopIndex() const = 0; static constexpr auto kArchiveFixOnTopIndex = 1; static constexpr auto kTopPromotionFixOnTopIndex = 2; - virtual bool shouldBeInChatList() const = 0; - virtual UnreadState chatListUnreadState() const = 0; - virtual BadgesState chatListBadgesState() const = 0; - virtual HistoryItem *chatListMessage() const = 0; - virtual bool chatListMessageKnown() const = 0; - virtual void requestChatListMessage() = 0; - virtual const QString &chatListName() const = 0; - virtual const QString &chatListNameSortKey() const = 0; - virtual const base::flat_set &chatListNameWords() const = 0; - virtual const base::flat_set &chatListFirstLetters() const = 0; - - virtual bool folderKnown() const { + [[nodiscard]] virtual bool shouldBeInChatList() const = 0; + [[nodiscard]] virtual UnreadState chatListUnreadState() const = 0; + [[nodiscard]] virtual BadgesState chatListBadgesState() const = 0; + [[nodiscard]] virtual HistoryItem *chatListMessage() const = 0; + [[nodiscard]] virtual bool chatListMessageKnown() const = 0; + [[nodiscard]] virtual const QString &chatListName() const = 0; + [[nodiscard]] virtual const QString &chatListNameSortKey() const = 0; + [[nodiscard]] virtual int chatListNameVersion() const = 0; + [[nodiscard]] virtual auto chatListNameWords() const + -> const base::flat_set & = 0; + [[nodiscard]] virtual auto chatListFirstLetters() const + -> const base::flat_set & = 0; + + [[nodiscard]] virtual bool folderKnown() const { return true; } - virtual Data::Folder *folder() const { + [[nodiscard]] virtual Data::Folder *folder() const { return nullptr; } @@ -255,8 +261,9 @@ class Entry : public base::has_weak_ptr { enum class Flag : uchar { IsThread = (1 << 0), IsHistory = (1 << 1), - UpdatePostponed = (1 << 2), - InUnreadChangeBlock = (1 << 3), + IsSavedSublist = (1 << 2), + UpdatePostponed = (1 << 3), + InUnreadChangeBlock = (1 << 4), }; friend inline constexpr bool is_flag_type(Flag) { return true; } using Flags = base::flags; @@ -265,8 +272,6 @@ class Entry : public base::has_weak_ptr { void pinnedIndexChanged(FilterId filterId, int was, int now); [[nodiscard]] uint64 computeSortPosition(FilterId filterId) const; - [[nodiscard]] virtual int chatListNameVersion() const = 0; - void setChatListExistence(bool exists); not_null mainChatListLink(FilterId filterId) const; Row *maybeMainChatListLink(FilterId filterId) const; diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index 5a9e98a690a9d..c1a630b784222 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -40,6 +40,7 @@ For license and copyright information please follow this link: #include "data/data_chat_filters.h" #include "data/data_cloud_file.h" #include "data/data_changes.h" +#include "data/data_saved_messages.h" #include "data/data_stories.h" #include "data/stickers/data_stickers.h" #include "data/data_send_action.h" @@ -219,7 +220,9 @@ InnerWidget::InnerWidget( session().data().chatsListChanges(), session().data().chatsListLoadedEvents() ) | rpl::filter([=](Data::Folder *folder) { - return !_openedForum && (folder == _openedFolder); + return !_savedSublists + && !_openedForum + && (folder == _openedFolder); }) | rpl::start_with_next([=] { refresh(); }, lifetime()); @@ -499,6 +502,8 @@ int InnerWidget::searchInChatSkip() const { } void InnerWidget::changeOpenedFolder(Data::Folder *folder) { + Expects(!folder || !_savedSublists); + if (_openedFolder == folder) { return; } @@ -513,6 +518,8 @@ void InnerWidget::changeOpenedFolder(Data::Folder *folder) { } void InnerWidget::changeOpenedForum(Data::Forum *forum) { + Expects(!forum || !_savedSublists); + if (_openedForum == forum) { return; } @@ -553,12 +560,39 @@ void InnerWidget::changeOpenedForum(Data::Forum *forum) { } } +void InnerWidget::showSavedSublists() { + Expects(!_geometryInited); + Expects(!_savedSublists); + + _savedSublists = true; + + stopReorderPinned(); + clearSelection(); + + _filterId = 0; + _openedForum = nullptr; + _st = &st::defaultDialogRow; + refreshShownList(); + + _openedForumLifetime.destroy(); + + //session().data().savedMessages().chatsListChanges( + //) | rpl::start_with_next([=] { + // refresh(); + //}, lifetime()); + + refreshWithCollapsedRows(true); + if (_loadMoreCallback) { + _loadMoreCallback(); + } +} + void InnerWidget::paintEvent(QPaintEvent *e) { Painter p(this); p.setInactive( _controller->isGifPausedAtLeastFor(Window::GifPauseReason::Any)); - if (_controller->contentOverlapped(this, e)) { + if (!_savedSublists && _controller->contentOverlapped(this, e)) { return; } const auto activeEntry = _controller->activeChatEntryCurrent(); @@ -1416,11 +1450,14 @@ void InnerWidget::mousePressEvent(QMouseEvent *e) { } } const std::vector &InnerWidget::pinnedChatsOrder() const { - return _openedForum - ? session().data().pinnedChatsOrder(_openedForum) + const auto owner = &session().data(); + return _savedSublists + ? owner->pinnedChatsOrder(&owner->savedMessages()) + : _openedForum + ? owner->pinnedChatsOrder(_openedForum) : _filterId - ? session().data().pinnedChatsOrder(_filterId) - : session().data().pinnedChatsOrder(_openedFolder); + ? owner->pinnedChatsOrder(_filterId) + : owner->pinnedChatsOrder(_openedFolder); } void InnerWidget::checkReorderPinnedStart(QPoint localPosition) { @@ -1473,7 +1510,9 @@ void InnerWidget::savePinnedOrder() { return; // Something has changed in the set of pinned chats. } } - if (_openedForum) { + if (_savedSublists) { + session().api().savePinnedOrder(&session().data().savedMessages()); + } else if (_openedForum) { session().api().savePinnedOrder(_openedForum); } else if (_filterId) { Api::SaveNewFilterPinned(&session(), _filterId); @@ -1577,7 +1616,7 @@ bool InnerWidget::updateReorderPinned(QPoint localPosition) { const auto delta = [&] { if (localPosition.y() < _visibleTop) { return localPosition.y() - _visibleTop; - } else if ((_openedFolder || _openedForum || _filterId) + } else if ((_savedSublists || _openedFolder || _openedForum || _filterId) && localPosition.y() > _visibleBottom) { return localPosition.y() - _visibleBottom; } @@ -1832,6 +1871,8 @@ void InnerWidget::handleChatListEntryRefreshes() { return false; } else if (const auto topic = event.key.topic()) { return (topic->forum() == _openedForum); + } else if (event.key.sublist()) { + return _savedSublists; } else { return !_openedForum; } @@ -1848,6 +1889,8 @@ void InnerWidget::handleChatListEntryRefreshes() { && (_state == WidgetState::Default) && (key.topic() ? (key.topic()->forum() == _openedForum) + : key.sublist() + ? _savedSublists : (entry->folder() == _openedFolder))) { _dialogMoved.fire({ from, to }); } @@ -2051,7 +2094,11 @@ void InnerWidget::enterEventHook(QEnterEvent *e) { Row *InnerWidget::shownRowByKey(Key key) { const auto entry = key.entry(); - if (_openedForum) { + if (_savedSublists) { + if (!entry->asSublist()) { + return nullptr; + } + } else if (_openedForum) { const auto topic = entry->asTopic(); if (!topic || topic->forum() != _openedForum) { return nullptr; @@ -2114,7 +2161,9 @@ void InnerWidget::updateSelectedRow(Key key) { } void InnerWidget::refreshShownList() { - const auto list = _openedForum + const auto list = _savedSublists + ? session().data().savedMessages().chatsList()->indexed() + : _openedForum ? _openedForum->topicsList()->indexed() : _filterId ? session().data().chatsFilters().chatsList(_filterId)->indexed() @@ -2294,15 +2343,19 @@ void InnerWidget::applyFilterUpdate(QString newFilter, bool force) { } }; if (!_searchInChat && !_searchFromPeer && !words.isEmpty()) { - if (_openedForum) { + if (_savedSublists) { + const auto owner = &session().data(); + append(owner->savedMessages().chatsList()->indexed()); + } else if (_openedForum) { append(_openedForum->topicsList()->indexed()); } else { - append(session().data().chatsList()->indexed()); + const auto owner = &session().data(); + append(owner->chatsList()->indexed()); const auto id = Data::Folder::kId; - if (const auto add = session().data().folderLoaded(id)) { + if (const auto add = owner->folderLoaded(id)) { append(add->chatsList()->indexed()); } - append(session().data().contactsNoChatsList()); + append(owner->contactsNoChatsList()); } } refresh(true); @@ -2759,6 +2812,10 @@ void InnerWidget::refreshEmptyLabel() { const auto data = &session().data(); const auto state = !_shownList->empty() ? EmptyState::None + : _savedSublists + ? (data->savedMessages().chatsList()->loaded() + ? EmptyState::EmptySavedSublists + : EmptyState::Loading) : _openedForum ? (_openedForum->topicsList()->loaded() ? EmptyState::EmptyForum @@ -2783,6 +2840,8 @@ void InnerWidget::refreshEmptyLabel() { ? tr::lng_no_chats_filter() : (state == EmptyState::EmptyForum) ? tr::lng_forum_no_topics() + : (state == EmptyState::EmptySavedSublists) + ? tr::lng_no_saved_sublists() : tr::lng_contacts_loading(); auto link = (state == EmptyState::NoContacts) ? tr::lng_add_contact_button() diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h index 608513c853c66..55d257bb56704 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h @@ -107,6 +107,7 @@ class InnerWidget final : public Ui::RpWidget { void changeOpenedFolder(Data::Folder *folder); void changeOpenedForum(Data::Forum *forum); + void showSavedSublists(); void selectSkip(int32 direction); void selectSkipPage(int32 pixels, int32 direction); @@ -198,6 +199,7 @@ class InnerWidget final : public Ui::RpWidget { NoContacts, EmptyFolder, EmptyForum, + EmptySavedSublists, }; struct PinnedRow { @@ -503,6 +505,8 @@ class InnerWidget final : public Ui::RpWidget { float64 _narrowRatio = 0.; bool _geometryInited = false; + bool _savedSublists = false; + base::unique_qptr _menu; }; diff --git a/Telegram/SourceFiles/dialogs/dialogs_key.cpp b/Telegram/SourceFiles/dialogs/dialogs_key.cpp index fe96a5a7dc46f..dfa728141be16 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_key.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_key.cpp @@ -9,6 +9,7 @@ For license and copyright information please follow this link: #include "data/data_folder.h" #include "data/data_forum_topic.h" +#include "data/data_saved_sublist.h" #include "history/history.h" namespace Dialogs { @@ -25,6 +26,9 @@ Key::Key(Data::Thread *thread) : _value(thread) { Key::Key(Data::ForumTopic *topic) : _value(topic) { } +Key::Key(Data::SavedSublist *sublist) : _value(sublist) { +} + Key::Key(not_null history) : _value(history) { } @@ -37,6 +41,9 @@ Key::Key(not_null folder) : _value(folder) { Key::Key(not_null topic) : _value(topic) { } +Key::Key(not_null sublist) : _value(sublist) { +} + not_null Key::entry() const { Expects(_value != nullptr); @@ -59,6 +66,10 @@ Data::Thread *Key::thread() const { return _value ? _value->asThread() : nullptr; } +Data::SavedSublist *Key::sublist() const { + return _value ? _value->asSublist() : nullptr; +} + History *Key::owningHistory() const { if (const auto thread = this->thread()) { return thread->owningHistory(); diff --git a/Telegram/SourceFiles/dialogs/dialogs_key.h b/Telegram/SourceFiles/dialogs/dialogs_key.h index 34fd9aa296a63..2396b216f5e5b 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_key.h +++ b/Telegram/SourceFiles/dialogs/dialogs_key.h @@ -14,6 +14,7 @@ namespace Data { class Thread; class Folder; class ForumTopic; +class SavedSublist; } // namespace Data namespace Dialogs { @@ -29,12 +30,14 @@ class Key { Key(Data::Folder *folder); Key(Data::Thread *thread); Key(Data::ForumTopic *topic); + Key(Data::SavedSublist *sublist); Key(not_null entry) : _value(entry) { } Key(not_null history); Key(not_null thread); Key(not_null folder); Key(not_null topic); + Key(not_null sublist); explicit operator bool() const { return (_value != nullptr); @@ -46,6 +49,7 @@ class Key { [[nodiscard]] Data::Thread *thread() const; [[nodiscard]] History *owningHistory() const; [[nodiscard]] PeerData *peer() const; + [[nodiscard]] Data::SavedSublist *sublist() const; friend inline constexpr auto operator<=>(Key, Key) noexcept = default; @@ -102,6 +106,7 @@ struct EntryState { Scheduled, Pinned, Replies, + SavedSublist, ContextMenu, }; diff --git a/Telegram/SourceFiles/dialogs/dialogs_row.cpp b/Telegram/SourceFiles/dialogs/dialogs_row.cpp index aaa03035feaa2..54247193cdee6 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_row.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_row.cpp @@ -261,8 +261,10 @@ void Row::recountHeight(float64 narrowRatio) { : st::defaultDialogRow.height; } else if (_id.folder()) { _height = st::defaultDialogRow.height; - } else { + } else if (_id.topic()) { _height = st::forumTopicRow.height; + } else { + _height = st::defaultDialogRow.height; } } diff --git a/Telegram/SourceFiles/dialogs/dialogs_row.h b/Telegram/SourceFiles/dialogs/dialogs_row.h index e814e5ff16801..738c9faa48524 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_row.h +++ b/Telegram/SourceFiles/dialogs/dialogs_row.h @@ -131,6 +131,9 @@ class Row final : public BasicRow { [[nodiscard]] Data::Thread *thread() const { return _id.thread(); } + [[nodiscard]] Data::SavedSublist *sublist() const { + return _id.sublist(); + } [[nodiscard]] not_null entry() const { return _id.entry(); } diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index e8a04121d4e3a..4aa36ec385315 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -1763,6 +1763,7 @@ bool Widget::searchMessages(bool searchCache) { (_searchQueryFrom ? _searchQueryFrom->input : MTP_inputPeerEmpty()), + MTPInputPeer(), // saved_peer_id MTP_int(topic ? topic->rootId() : 0), MTP_inputMessagesFilterEmpty(), MTP_int(0), // min_date @@ -2004,6 +2005,7 @@ void Widget::searchMore() { (_searchQueryFrom ? _searchQueryFrom->input : MTP_inputPeerEmpty()), + MTPInputPeer(), // saved_peer_id MTP_int(topic ? topic->rootId() : 0), MTP_inputMessagesFilterEmpty(), MTP_int(0), // min_date @@ -2076,6 +2078,7 @@ void Widget::searchMore() { (_searchQueryFrom ? _searchQueryFrom->input : MTP_inputPeerEmpty()), + MTPInputPeer(), // saved_peer_id MTPint(), // top_msg_id MTP_inputMessagesFilterEmpty(), MTP_int(0), // min_date @@ -2692,7 +2695,7 @@ void Widget::filterCursorMoved() { } void Widget::completeHashtag(QString tag) { - const auto t = _filter->getLastText();; + const auto t = _filter->getLastText(); auto cur = _filter->textCursor().position(); auto hashtag = QString(); for (int start = cur; start > 0;) { @@ -2968,7 +2971,7 @@ RowDescriptor Widget::resolveChatNext(RowDescriptor from) const { } RowDescriptor Widget::resolveChatPrevious(RowDescriptor from) const { - return _inner->resolveChatPrevious(from); + return _inner->resolveChatPrevious(from); } void Widget::keyPressEvent(QKeyEvent *e) { diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp index 5299b648c9693..e4647733854d9 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp @@ -9,6 +9,7 @@ For license and copyright information please follow this link: #include "data/data_drafts.h" #include "data/data_forum_topic.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "dialogs/dialogs_list.h" #include "dialogs/dialogs_three_state_icon.h" @@ -269,6 +270,7 @@ enum class Flag { RepliesMessages = 0x10, AllowUserOnline = 0x20, TopicJumpRipple = 0x40, + HiddenAuthor = 0x80, }; inline constexpr bool is_flag_type(Flag) { return true; } @@ -311,6 +313,7 @@ void PaintRow( const auto history = entry->asHistory(); const auto thread = entry->asThread(); + const auto sublist = entry->asSublist(); if (flags & Flag::SavedMessages) { EmptyUserpic::PaintSavedMessages( @@ -326,6 +329,13 @@ void PaintRow( context.st->padding.top(), context.width, context.st->photoSize); + } else if (flags & Flag::HiddenAuthor) { + EmptyUserpic::PaintHiddenAuthor( + p, + context.st->padding.left(), + context.st->padding.top(), + context.width, + context.st->photoSize); } else if (!from && hiddenSenderInfo) { hiddenSenderInfo->emptyUserpic.paintCircle( p, @@ -547,7 +557,7 @@ void PaintRow( // Empty history } } else if (!item->isEmpty()) { - if (thread && !promoted) { + if ((thread || sublist) && !promoted) { PaintRowDate(p, date, rectForName, context); } @@ -606,10 +616,15 @@ void PaintRow( } p.setFont(st::semiboldFont); - if (flags & (Flag::SavedMessages | Flag::RepliesMessages)) { + if (flags + & (Flag::SavedMessages + | Flag::RepliesMessages + | Flag::HiddenAuthor)) { auto text = (flags & Flag::SavedMessages) ? tr::lng_saved_messages(tr::now) - : tr::lng_replies_messages(tr::now); + : (flags & Flag::RepliesMessages) + ? tr::lng_replies_messages(tr::now) + : tr::lng_hidden_author_messages(tr::now); const auto textWidth = st::semiboldFont->width(text); if (textWidth > rectForName.width()) { text = st::semiboldFont->elided(text, rectForName.width()); @@ -621,7 +636,7 @@ void PaintRow( : st::dialogsNameFg); p.drawTextLeft(rectForName.left(), rectForName.top(), context.width, text); } else if (from) { - if (history && !context.search) { + if ((history || sublist) && !context.search) { const auto badgeWidth = fromBadge.drawGetWidth( p, rectForName, @@ -732,6 +747,7 @@ void RowPainter::Paint( const auto entry = row->entry(); const auto history = row->history(); const auto thread = row->thread(); + const auto sublist = row->sublist(); const auto peer = history ? history->peer.get() : nullptr; const auto badgesState = entry->chatListBadgesState(); entry->chatListPreloadData(); // Allow chat list message resolve. @@ -771,11 +787,18 @@ void RowPainter::Paint( ? (history->peer->migrateTo() ? history->peer->migrateTo() : history->peer.get()) + : sublist + ? sublist->peer().get() : nullptr; const auto allowUserOnline = true;// !context.narrow || badgesState.empty(); const auto flags = (allowUserOnline ? Flag::AllowUserOnline : Flag(0)) - | (peer && peer->isSelf() ? Flag::SavedMessages : Flag(0)) - | (peer && peer->isRepliesChat() ? Flag::RepliesMessages : Flag(0)) + | ((peer && peer->isSelf()) ? Flag::SavedMessages : Flag(0)) + | ((from && from->isRepliesChat()) + ? Flag::RepliesMessages + : Flag(0)) + | ((sublist && from->isSavedHiddenAuthor()) + ? Flag::HiddenAuthor + : Flag(0)) | (row->topicJumpRipple() ? Flag::TopicJumpRipple : Flag(0)); const auto paintItemCallback = [&](int nameleft, int namewidth) { const auto texttop = context.st->textTop; @@ -810,6 +833,8 @@ void RowPainter::Paint( ? nullptr : thread ? &thread->lastItemDialogsView() + : sublist + ? &sublist->lastItemDialogsView() : nullptr; if (view) { const auto forum = context.st->topicsHeight @@ -872,7 +897,9 @@ void RowPainter::Paint( if (const auto peer = searchChat.peer()) { if (const auto forwarded = item->Get()) { if (peer->isSelf() || forwarded->imported) { - return forwarded->hiddenSenderInfo.get(); + return forwarded->savedFromHiddenSenderInfo.get() + ? forwarded->savedFromHiddenSenderInfo.get() + : forwarded->originalHiddenSenderInfo.get(); } } } diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp index cda8da94d47a7..e6a93af3af2d2 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp @@ -36,7 +36,6 @@ constexpr auto kCollapseAfterRatio = 0.68; constexpr auto kFrictionRatio = 0.15; constexpr auto kExpandCatchUpDuration = crl::time(200); constexpr auto kMaxTooltipNames = 3; -constexpr auto kStoriesTooltipHideBgOpacity = 0.2; [[nodiscard]] int AvailableNameWidth(const style::DialogsStoriesList &st) { const auto &full = st.full; diff --git a/Telegram/SourceFiles/export/data/export_data_types.cpp b/Telegram/SourceFiles/export/data/export_data_types.cpp index f02454abd4c19..7c9910e1e52ba 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.cpp +++ b/Telegram/SourceFiles/export/data/export_data_types.cpp @@ -52,6 +52,112 @@ QString PrepareStoryFileName( + extension; } +std::vector> ButtonRowsFromTL( + const MTPDreplyInlineMarkup &data) { + const auto &list = data.vrows().v; + + if (list.isEmpty()) { + return {}; + } + using Type = HistoryMessageMarkupButton::Type; + auto rows = std::vector>(); + rows.reserve(list.size()); + + for (const auto &tlRow : list) { + auto row = std::vector(); + row.reserve(tlRow.data().vbuttons().v.size()); + for (const auto &button : tlRow.data().vbuttons().v) { + button.match([&](const MTPDkeyboardButton &data) { + row.push_back({ Type::Default, qs(data.vtext()) }); + }, [&](const MTPDkeyboardButtonCallback &data) { + row.push_back({ + (data.is_requires_password() + ? Type::CallbackWithPassword + : Type::Callback), + qs(data.vtext()), + qba(data.vdata()) + }); + }, [&](const MTPDkeyboardButtonRequestGeoLocation &data) { + row.push_back({ Type::RequestLocation, qs(data.vtext()) }); + }, [&](const MTPDkeyboardButtonRequestPhone &data) { + row.push_back({ Type::RequestPhone, qs(data.vtext()) }); + }, [&](const MTPDkeyboardButtonRequestPeer &data) { + row.push_back({ + Type::RequestPeer, + qs(data.vtext()), + QByteArray("unsupported"), + QString(), + int64(data.vbutton_id().v), + }); + }, [&](const MTPDkeyboardButtonUrl &data) { + row.push_back({ + Type::Url, + qs(data.vtext()), + qba(data.vurl()) + }); + }, [&](const MTPDkeyboardButtonSwitchInline &data) { + const auto type = data.is_same_peer() + ? Type::SwitchInlineSame + : Type::SwitchInline; + row.push_back({ type, qs(data.vtext()), qba(data.vquery()) }); + }, [&](const MTPDkeyboardButtonGame &data) { + row.push_back({ Type::Game, qs(data.vtext()) }); + }, [&](const MTPDkeyboardButtonBuy &data) { + row.push_back({ Type::Buy, qs(data.vtext()) }); + }, [&](const MTPDkeyboardButtonUrlAuth &data) { + row.push_back({ + Type::Auth, + qs(data.vtext()), + qba(data.vurl()), + qs(data.vfwd_text().value_or_empty()), + data.vbutton_id().v, + }); + }, [&](const MTPDkeyboardButtonRequestPoll &data) { + const auto quiz = [&] { + if (!data.vquiz()) { + return QByteArray(); + } + return data.vquiz()->match([&](const MTPDboolTrue&) { + return QByteArray(1, 1); + }, [&](const MTPDboolFalse&) { + return QByteArray(1, 0); + }); + }(); + row.push_back({ + Type::RequestPoll, + qs(data.vtext()), + quiz + }); + }, [&](const MTPDkeyboardButtonUserProfile &data) { + row.push_back({ + Type::UserProfile, + qs(data.vtext()), + QByteArray::number(data.vuser_id().v) + }); + }, [&](const MTPDinputKeyboardButtonUrlAuth &data) { + }, [&](const MTPDinputKeyboardButtonUserProfile &data) { + }, [&](const MTPDkeyboardButtonWebView &data) { + row.push_back({ + Type::WebView, + qs(data.vtext()), + data.vurl().v + }); + }, [&](const MTPDkeyboardButtonSimpleWebView &data) { + row.push_back({ + Type::SimpleWebView, + qs(data.vtext()), + data.vurl().v + }); + }); + } + if (!row.empty()) { + rows.push_back(std::move(row)); + } + } + + return rows; +} + } // namespace uint8 PeerColorIndex(BareId bareId) { @@ -575,8 +681,8 @@ Poll ParsePoll(const MTPDmessageMediaPoll &data) { return result; } -Giveaway ParseGiveaway(const MTPDmessageMediaGiveaway &data) { - auto result = Giveaway{ +GiveawayStart ParseGiveaway(const MTPDmessageMediaGiveaway &data) { + auto result = GiveawayStart{ .untilDate = data.vuntil_date().v, .quantity = data.vquantity().v, .months = data.vmonths().v, @@ -1090,6 +1196,8 @@ Media ParseMedia( // #TODO export stories }, [&](const MTPDmessageMediaGiveaway &data) { result.content = ParseGiveaway(data); + }, [&](const MTPDmessageMediaGiveawayResults &data) { + // #TODO export giveaway }, [](const MTPDmessageMediaEmpty &data) {}); return result; } @@ -1327,7 +1435,9 @@ ServiceAction ParseServiceAction( result.content = content; }, [&](const MTPDmessageActionRequestedPeer &data) { auto content = ActionRequestedPeer(); - content.peerId = ParsePeerId(data.vpeer()); + for (const auto &peer : data.vpeers().v) { + content.peers.push_back(ParsePeerId(peer)); + } content.buttonId = data.vbutton_id().v; result.content = content; }, [&](const MTPDmessageActionGiftCode &data) { @@ -1496,6 +1606,14 @@ Message ParseMessage( } context.botId = 0; } + if (const auto replyMarkup = data.vreply_markup()) { + replyMarkup->match([](const MTPDreplyKeyboardMarkup &) { + }, [&](const MTPDreplyInlineMarkup &data) { + result.inlineButtonRows = ButtonRowsFromTL(data); + }, [](const MTPDreplyKeyboardHide &) { + }, [](const MTPDreplyKeyboardForceReply &) { + }); + } result.text = ParseText( data.vmessage(), data.ventities().value_or_empty()); diff --git a/Telegram/SourceFiles/export/data/export_data_types.h b/Telegram/SourceFiles/export/data/export_data_types.h index 59ffa1c395705..20bd152064a03 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.h +++ b/Telegram/SourceFiles/export/data/export_data_types.h @@ -197,7 +197,7 @@ struct Poll { bool closed = false; }; -struct Giveaway { +struct GiveawayStart { std::vector channels; TimeId untilDate = 0; int quantity = 0; @@ -336,7 +336,7 @@ struct Media { Game, Invoice, Poll, - Giveaway, + GiveawayStart, UnsupportedMedia> content; TimeId ttl = 0; @@ -547,7 +547,7 @@ struct ActionGiftCode { }; struct ActionRequestedPeer { - PeerId peerId = 0; + std::vector peers; int buttonId = 0; }; @@ -665,6 +665,33 @@ inline bool operator>=(MessageId a, MessageId b) { return !(a < b); } +struct HistoryMessageMarkupButton { + enum class Type { + Default, + Url, + Callback, + CallbackWithPassword, + RequestPhone, + RequestLocation, + RequestPoll, + RequestPeer, + SwitchInline, + SwitchInlineSame, + Game, + Buy, + Auth, + UserProfile, + WebView, + SimpleWebView, + }; + + Type type; + QString text; + QByteArray data; + QString forwardText; + int64 buttonId = 0; +}; + struct Message { int32 id = 0; TimeId date = 0; @@ -686,6 +713,7 @@ struct Message { Media media; ServiceAction action; bool out = false; + std::vector> inlineButtonRows; File &file(); const File &file() const; diff --git a/Telegram/SourceFiles/export/export_api_wrap.cpp b/Telegram/SourceFiles/export/export_api_wrap.cpp index 5ed8addd9d07d..61f29b16c2dce 100644 --- a/Telegram/SourceFiles/export/export_api_wrap.cpp +++ b/Telegram/SourceFiles/export/export_api_wrap.cpp @@ -1624,6 +1624,7 @@ void ApiWrap::requestChatMessages( realPeerInput, MTP_string(), // query MTP_inputPeerSelf(), + MTPInputPeer(), // saved_peer_id MTPint(), // top_msg_id MTP_inputMessagesFilterEmpty(), MTP_int(0), // min_date diff --git a/Telegram/SourceFiles/export/output/export_output_html.cpp b/Telegram/SourceFiles/export/output/export_output_html.cpp index 3a19a8b4a8581..3f0b03eaf72af 100644 --- a/Telegram/SourceFiles/export/output/export_output_html.cpp +++ b/Telegram/SourceFiles/export/output/export_output_html.cpp @@ -613,7 +613,7 @@ class HtmlWriter::Wrap { [[nodiscard]] QByteArray pushPoll(const Data::Poll &data); [[nodiscard]] QByteArray pushGiveaway( const PeersMap &peers, - const Data::Giveaway &data); + const Data::GiveawayStart &data); File _file; QByteArray _composedStart; @@ -1112,7 +1112,7 @@ auto HtmlWriter::Wrap::pushMessage( if (data.recurringUsed) { return "You were charged " + amount + " via recurring payment"; } - auto result = "You have successfully transferred " + auto result = "You have successfully transferred " + amount + " for " + wrapReplyToLink("this invoice"); @@ -1501,7 +1501,7 @@ QByteArray HtmlWriter::Wrap::pushMedia( return pushPhotoMedia(*photo, basePath); } else if (const auto poll = std::get_if(&content)) { return pushPoll(*poll); - } else if (const auto giveaway = std::get_if(&content)) { + } else if (const auto giveaway = std::get_if(&content)) { return pushGiveaway(peers, *giveaway); } Assert(v::is_null(content)); @@ -1826,7 +1826,7 @@ QByteArray HtmlWriter::Wrap::pushPoll(const Data::Poll &data) { QByteArray HtmlWriter::Wrap::pushGiveaway( const PeersMap &peers, - const Data::Giveaway &data) { + const Data::GiveawayStart &data) { auto result = pushDiv("media_wrap clearfix"); result.append(pushDiv("media_giveaway")); @@ -2028,7 +2028,7 @@ MediaData HtmlWriter::Wrap::prepareMediaData( result.description = data.description; result.status = Data::FormatMoneyAmount(data.amount, data.currency); }, [](const Poll &data) { - }, [](const Giveaway &data) { + }, [](const GiveawayStart &data) { }, [](const UnsupportedMedia &data) { Unexpected("Unsupported message."); }, [](v::null_t) {}); diff --git a/Telegram/SourceFiles/export/output/export_output_json.cpp b/Telegram/SourceFiles/export/output/export_output_json.cpp index 53e8a89d6ad58..dc23fd38f1842 100644 --- a/Telegram/SourceFiles/export/output/export_output_json.cpp +++ b/Telegram/SourceFiles/export/output/export_output_json.cpp @@ -446,7 +446,6 @@ QByteArray SerializeMessage( pushAction("send_payment"); push("amount", data.amount); push("currency", data.currency); - const auto amount = FormatMoneyAmount(data.amount, data.currency); pushReplyToMsgId("invoice_message_id"); if (data.recurringUsed) { push("recurring", "used"); @@ -592,7 +591,11 @@ QByteArray SerializeMessage( pushActor(); pushAction("requested_peer"); push("button_id", data.buttonId); - push("peer_id", data.peerId.value); + auto values = std::vector(); + for (const auto &one : data.peers) { + values.push_back(Data::NumberToString(one.value)); + } + push("peers", SerializeArray(context, values)); }, [&](const ActionGiftCode &data) { pushAction("gift_code_prize"); push("gift_code", data.code); @@ -753,7 +756,7 @@ QByteArray SerializeMessage( { "total_voters", NumberToString(data.totalVotes) }, { "answers", serialized } })); - }, [&](const Giveaway &data) { + }, [&](const GiveawayStart &data) { context.nesting.push_back(Context::kObject); const auto channels = ranges::views::all( data.channels @@ -776,6 +779,76 @@ QByteArray SerializeMessage( pushBare("text", SerializeText(context, message.text)); pushBare("text_entities", SerializeText(context, message.text, true)); + if (!message.inlineButtonRows.empty()) { + const auto typeString = []( + const HistoryMessageMarkupButton &entry) -> QByteArray { + using Type = HistoryMessageMarkupButton::Type; + switch (entry.type) { + case Type::Default: return "default"; + case Type::Url: return "url"; + case Type::Callback: return "callback"; + case Type::CallbackWithPassword: return "callback_with_password"; + case Type::RequestPhone: return "request_phone"; + case Type::RequestLocation: return "request_location"; + case Type::RequestPoll: return "request_poll"; + case Type::RequestPeer: return "request_peer"; + case Type::SwitchInline: return "switch_inline"; + case Type::SwitchInlineSame: return "switch_inline_same"; + case Type::Game: return "game"; + case Type::Buy: return "buy"; + case Type::Auth: return "auth"; + case Type::UserProfile: return "user_profile"; + case Type::WebView: return "web_view"; + case Type::SimpleWebView: return "simple_web_view"; + } + Unexpected("Type in HistoryMessageMarkupButton::Type."); + }; + const auto serializeRow = [&]( + const std::vector &row) { + context.nesting.push_back(Context::kArray); + const auto buttons = ranges::views::all( + row + ) | ranges::views::transform([&]( + const HistoryMessageMarkupButton &entry) { + auto pairs = std::vector>(); + pairs.push_back({ + "type", + SerializeString(typeString(entry)), + }); + if (!entry.text.isEmpty()) { + pairs.push_back({ + "text", + SerializeString(entry.text.toUtf8()), + }); + } + if (!entry.data.isEmpty()) { + pairs.push_back({ "data", SerializeString(entry.data) }); + } + if (!entry.forwardText.isEmpty()) { + pairs.push_back({ + "forward_text", + SerializeString(entry.forwardText.toUtf8()), + }); + } + if (entry.buttonId) { + pairs.push_back({ + "button_id", + NumberToString(entry.buttonId), + }); + } + return SerializeObject(context, pairs); + }) | ranges::to_vector; + context.nesting.pop_back(); + return SerializeArray(context, buttons); + }; + context.nesting.push_back(Context::kArray); + const auto rows = ranges::views::all( + message.inlineButtonRows + ) | ranges::views::transform(serializeRow) | ranges::to_vector; + context.nesting.pop_back(); + pushBare("inline_bot_buttons", SerializeArray(context, rows)); + } + return serialized(); } @@ -1078,7 +1151,7 @@ Result JsonWriter::writeFrequentContacts(const Data::ContactsList &data) { { "id", Data::NumberToString(Data::PeerToBareId(top.peer.id())) }, { "category", SerializeString(category) }, { "type", SerializeString(type) }, - { "name", StringAllowNull(top.peer.name()) }, + { "name", StringAllowNull(top.peer.name()) }, { "rating", Data::NumberToString(top.rating) }, })); } diff --git a/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp b/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp index b59bf86bad34a..378d9b4d1f7b6 100644 --- a/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp +++ b/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp @@ -134,8 +134,9 @@ void PremultiplyLine(uchar *dst, const uchar *src, int intsCount) { } DEBUG_LOG(("Video Info: " "Trying \"%1\" hardware acceleration for \"%2\" decoder." - ).arg(av_hwdevice_get_type_name(type) - ).arg(context->codec->name)); + ).arg( + av_hwdevice_get_type_name(type), + context->codec->name)); if (parent->hw_device_ctx) { av_buffer_unref(&parent->hw_device_ctx); } diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp index 89a2e6b496ef2..fb6f950e0a32c 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp @@ -442,7 +442,6 @@ void InnerWidget::applyFilter(FilterValue &&value) { } void InnerWidget::applySearch(const QString &query) { - auto clearQuery = query.trimmed(); if (_searchQuery != query) { _searchQuery = query; clearAndRequestLog(); @@ -1243,7 +1242,7 @@ void InnerWidget::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { showContextInFolder(lnkDocument); }, &st::menuIconShowInFolder); } - _menu->addAction(lnkIsVideo ? tr::lng_context_save_video(tr::now) : (lnkIsVoice ? tr::lng_context_save_audio(tr::now) : (lnkIsAudio ? tr::lng_context_save_audio_file(tr::now) : tr::lng_context_save_file(tr::now))), base::fn_delayed(st::defaultDropdownMenu.menu.ripple.hideDuration, this, [this, lnkDocument] { + _menu->addAction(lnkIsVideo ? tr::lng_context_save_video(tr::now) : (lnkIsVoice ? tr::lng_context_save_audio(tr::now) : (lnkIsAudio ? tr::lng_context_save_audio_file(tr::now) : tr::lng_context_save_file(tr::now))), base::fn_delayed(st::defaultDropdownMenu.menu.ripple.hideDuration, this, [this, lnkDocument] { saveDocumentToFile(lnkDocument); }), &st::menuIconDownload); if (lnkDocument->hasAttachedStickers()) { diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp index df8819d10e646..f9ece93b06556 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp @@ -117,6 +117,7 @@ MTPMessage PrepareLogMessage(const MTPMessage &message, TimeId newDate) { data.vid(), data.vfrom_id() ? *data.vfrom_id() : MTPPeer(), data.vpeer_id(), + MTPPeer(), // saved_peer_id data.vfwd_from() ? *data.vfwd_from() : MTPMessageFwdHeader(), MTP_long(data.vvia_bot_id().value_or_empty()), MTPMessageReplyHeader(), @@ -767,8 +768,10 @@ void GenerateItems( using LogDeleteTopic = MTPDchannelAdminLogEventActionDeleteTopic; using LogPinTopic = MTPDchannelAdminLogEventActionPinTopic; using LogToggleAntiSpam = MTPDchannelAdminLogEventActionToggleAntiSpam; - using LogChangeColor = MTPDchannelAdminLogEventActionChangeColor; - using LogChangeBackgroundEmoji = MTPDchannelAdminLogEventActionChangeBackgroundEmoji; + using LogChangePeerColor = MTPDchannelAdminLogEventActionChangePeerColor; + using LogChangeProfilePeerColor = MTPDchannelAdminLogEventActionChangeProfilePeerColor; + using LogChangeWallpaper = MTPDchannelAdminLogEventActionChangeWallpaper; + using LogChangeEmojiStatus = MTPDchannelAdminLogEventActionChangeEmojiStatus; const auto session = &history->session(); const auto id = event.vid().v; @@ -1817,51 +1820,170 @@ void GenerateItems( addSimpleServiceMessage(text); }; - const auto createChangeColor = [&](const LogChangeColor &data) { - const auto text = tr::lng_admin_log_change_color( + const auto createColorChange = [&]( + const MTPPeerColor &was, + const MTPPeerColor &now, + const auto &colorPhrase, + const auto &setEmoji, + const auto &removeEmoji, + const auto &changeEmoji) { + const auto prevColor = was.data().vcolor(); + const auto nextColor = now.data().vcolor(); + if (prevColor != nextColor) { + const auto wrap = [&](tl::conditional value) { + return value + ? value->v + : Data::DecideColorIndex(history->peer->id); + }; + const auto text = colorPhrase( + tr::now, + lt_from, + fromLinkText, + lt_previous, + { '#' + QString::number(wrap(prevColor) + 1) }, + lt_color, + { '#' + QString::number(wrap(nextColor) + 1) }, + Ui::Text::WithEntities); + addSimpleServiceMessage(text); + } + const auto prevEmoji = was.data().vbackground_emoji_id().value_or_empty(); + const auto nextEmoji = now.data().vbackground_emoji_id().value_or_empty(); + if (prevEmoji != nextEmoji) { + const auto text = !prevEmoji + ? setEmoji( + tr::now, + lt_from, + fromLinkText, + lt_emoji, + Ui::Text::SingleCustomEmoji( + Data::SerializeCustomEmojiId(nextEmoji)), + Ui::Text::WithEntities) + : !nextEmoji + ? removeEmoji( + tr::now, + lt_from, + fromLinkText, + lt_emoji, + Ui::Text::SingleCustomEmoji( + Data::SerializeCustomEmojiId(prevEmoji)), + Ui::Text::WithEntities) + : changeEmoji( + tr::now, + lt_from, + fromLinkText, + lt_previous, + Ui::Text::SingleCustomEmoji( + Data::SerializeCustomEmojiId(prevEmoji)), + lt_emoji, + Ui::Text::SingleCustomEmoji( + Data::SerializeCustomEmojiId(nextEmoji)), + Ui::Text::WithEntities); + addSimpleServiceMessage(text); + } + }; + + const auto createChangePeerColor = [&](const LogChangePeerColor &data) { + createColorChange( + data.vprev_value(), + data.vnew_value(), + tr::lng_admin_log_change_color, + tr::lng_admin_log_set_background_emoji, + tr::lng_admin_log_removed_background_emoji, + tr::lng_admin_log_change_background_emoji); + }; + + const auto createChangeProfilePeerColor = [&](const LogChangeProfilePeerColor &data) { + createColorChange( + data.vprev_value(), + data.vnew_value(), + tr::lng_admin_log_change_profile_color, + tr::lng_admin_log_set_profile_background_emoji, + tr::lng_admin_log_removed_profile_background_emoji, + tr::lng_admin_log_change_profile_background_emoji); + }; + + const auto createChangeWallpaper = [&](const LogChangeWallpaper &data) { + addSimpleServiceMessage(tr::lng_admin_log_change_wallpaper( tr::now, lt_from, fromLinkText, - lt_previous, - { '#' + QString::number(data.vprev_value().v + 1) }, - lt_color, - { '#' + QString::number(data.vnew_value().v + 1) }, - Ui::Text::WithEntities); - addSimpleServiceMessage(text); + Ui::Text::WithEntities)); }; - const auto createChangeBackgroundEmoji = [&](const LogChangeBackgroundEmoji &data) { - const auto was = data.vprev_value().v; - const auto now = data.vnew_value().v; - const auto text = !was - ? tr::lng_admin_log_set_background_emoji( - tr::now, - lt_from, - fromLinkText, - lt_emoji, - Ui::Text::SingleCustomEmoji( - Data::SerializeCustomEmojiId(now)), - Ui::Text::WithEntities) - : !now - ? tr::lng_admin_log_removed_background_emoji( + const auto createChangeEmojiStatus = [&](const LogChangeEmojiStatus &data) { + const auto parse = [](const MTPEmojiStatus &status) { + return status.match([]( + const MTPDemojiStatus &data) { + return data.vdocument_id().v; + }, [](const MTPDemojiStatusEmpty &) { + return DocumentId(); + }, [](const MTPDemojiStatusUntil &data) { + return data.vdocument_id().v; + }); + }; + const auto prevEmoji = parse(data.vprev_value()); + const auto nextEmoji = parse(data.vnew_value()); + const auto nextUntil = data.vnew_value().match([]( + const MTPDemojiStatusUntil &data) { + return data.vuntil().v; + }, [](const auto &) { return TimeId(); }); + + const auto text = !prevEmoji + ? (nextUntil + ? tr::lng_admin_log_set_status_until( + tr::now, + lt_from, + fromLinkText, + lt_emoji, + Ui::Text::SingleCustomEmoji( + Data::SerializeCustomEmojiId(nextEmoji)), + lt_date, + TextWithEntities{ + langDateTime(base::unixtime::parse(nextUntil)) }, + Ui::Text::WithEntities) + : tr::lng_admin_log_set_status( + tr::now, + lt_from, + fromLinkText, + lt_emoji, + Ui::Text::SingleCustomEmoji( + Data::SerializeCustomEmojiId(nextEmoji)), + Ui::Text::WithEntities)) + : !nextEmoji + ? tr::lng_admin_log_removed_status( tr::now, lt_from, fromLinkText, lt_emoji, Ui::Text::SingleCustomEmoji( - Data::SerializeCustomEmojiId(was)), + Data::SerializeCustomEmojiId(prevEmoji)), Ui::Text::WithEntities) - : tr::lng_admin_log_change_background_emoji( - tr::now, - lt_from, - fromLinkText, - lt_previous, - Ui::Text::SingleCustomEmoji( - Data::SerializeCustomEmojiId(was)), - lt_emoji, - Ui::Text::SingleCustomEmoji( - Data::SerializeCustomEmojiId(now)), - Ui::Text::WithEntities); + : (nextUntil + ? tr::lng_admin_log_change_status_until( + tr::now, + lt_from, + fromLinkText, + lt_previous, + Ui::Text::SingleCustomEmoji( + Data::SerializeCustomEmojiId(prevEmoji)), + lt_emoji, + Ui::Text::SingleCustomEmoji( + Data::SerializeCustomEmojiId(nextEmoji)), + lt_date, + TextWithEntities{ + langDateTime(base::unixtime::parse(nextUntil)) }, + Ui::Text::WithEntities) + : tr::lng_admin_log_change_status( + tr::now, + lt_from, + fromLinkText, + lt_previous, + Ui::Text::SingleCustomEmoji( + Data::SerializeCustomEmojiId(prevEmoji)), + lt_emoji, + Ui::Text::SingleCustomEmoji( + Data::SerializeCustomEmojiId(nextEmoji)), + Ui::Text::WithEntities)); addSimpleServiceMessage(text); }; @@ -1909,8 +2031,10 @@ void GenerateItems( createDeleteTopic, createPinTopic, createToggleAntiSpam, - createChangeColor, - createChangeBackgroundEmoji); + createChangePeerColor, + createChangeProfilePeerColor, + createChangeWallpaper, + createChangeEmojiStatus); } } // namespace AdminLog diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index 13cdf046cb71d..bdc0d659c33fb 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -21,6 +21,7 @@ For license and copyright information please follow this link: #include "data/notify/data_notify_settings.h" #include "data/stickers/data_stickers.h" #include "data/data_drafts.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_media_types.h" #include "data/data_channel_admins.h" @@ -609,6 +610,11 @@ not_null History::addNewItem( addNewToBack(item, unread); checkForLoadedAtTop(item); } + + if (const auto sublist = item->savedSublist()) { + sublist->applyMaybeLast(item, unread); + } + return item; } @@ -1427,6 +1433,12 @@ void History::addCreatedOlderSlice( if (loadedAtBottom()) { // Add photos to overview and authors to lastAuthors. addItemsToLists(items); + + for (const auto &item : items) { + if (const auto sublist = item->savedSublist()) { + sublist->applyMaybeLast(item); + } + } } addToSharedMedia(items); } diff --git a/Telegram/SourceFiles/history/history.h b/Telegram/SourceFiles/history/history.h index 9095496840774..adeb63fc26656 100644 --- a/Telegram/SourceFiles/history/history.h +++ b/Telegram/SourceFiles/history/history.h @@ -365,6 +365,7 @@ class History final : public Data::Thread { void takeLocalDraft(not_null from); void applyCloudDraft(MsgId topicRootId); void draftSavedToCloud(MsgId topicRootId); + void requestChatListMessage(); [[nodiscard]] const Data::ForwardDraft &forwardDraft( MsgId topicRootId) const; @@ -383,9 +384,9 @@ class History final : public Data::Thread { Dialogs::BadgesState chatListBadgesState() const override; HistoryItem *chatListMessage() const override; bool chatListMessageKnown() const override; - void requestChatListMessage() override; const QString &chatListName() const override; const QString &chatListNameSortKey() const override; + int chatListNameVersion() const override; const base::flat_set &chatListNameWords() const override; const base::flat_set &chatListFirstLetters() const override; void chatListPreloadData() override; @@ -589,8 +590,6 @@ class History final : public Data::Thread { [[nodiscard]] Dialogs::UnreadState computeUnreadState() const; void setFolderPointer(Data::Folder *folder); - int chatListNameVersion() const override; - void hasUnreadMentionChanged(bool has) override; void hasUnreadReactionChanged(bool has) override; diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 11abf78a8963e..da1d308750965 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -437,43 +437,6 @@ HistoryInner::HistoryInner( _migrated->translateTo(_history->translatedTo()); } -#if 0 - if (const auto channel = _history->peer->asBroadcast()) { - if (channel->amCreator()) { - const auto weak = base::make_weak(_controller); - channel->session().api().request(MTPpayments_GetPremiumGiftCodeOptions( - MTP_flags(MTPpayments_GetPremiumGiftCodeOptions::Flag::f_boost_peer), - channel->input - )).done(crl::guard(weak, [=](const MTPVector &result) { - if (result.v.isEmpty()) { - return; - } - const auto &data = result.v.front().data(); - const auto randomId = base::RandomValue(); - Payments::CheckoutProcess::Start( - Payments::InvoicePremiumGiftCode{ - .purpose = Payments::InvoicePremiumGiftCodeGiveaway{ - .boostPeer = channel, - //.additionalChannels = , - .untilDate = (base::unixtime::now() + 300), - .onlyNewSubscribers = true, - }, - .randomId = randomId, - .currency = qs(data.vcurrency()), - .amount = data.vamount().v, - .storeProduct = qs(data.vstore_product().value_or_empty()), - .storeQuantity = data.vstore_quantity().value_or_empty(), - .users = data.vusers().v, - .months = data.vmonths().v, - }, - crl::guard(weak, [=](auto) { weak->window().activate(); })); - })).fail(crl::guard(weak, [=](const MTP::Error &error) { - weak.get()->showToast(error.type()); - })).send(); - } - } -#endif - Window::ChatThemeValueFromPeer( controller, _peer @@ -1255,7 +1218,7 @@ void HistoryInner::paintEvent(QPaintEvent *e) { width(), st::msgPhotoSize, context.paused); - } else if (const auto info = item->hiddenSenderInfo()) { + } else if (const auto info = item->displayHiddenSenderInfo()) { if (info->customUserpic.empty()) { info->emptyUserpic.paintCircle( p, @@ -2563,6 +2526,9 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { ? link->copyToClipboardContextItemText() : QString(); + if (item && item->isSponsored()) { + FillSponsoredMessagesMenu(controller, item->fullId(), _menu); + } if (isUponSelected > 0) { addReplyAction(item); const auto selectedText = getSelectedText(); @@ -2625,10 +2591,6 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { }, &st::menuIconCopy); } } - if (item->isSponsored()) { - const auto itemId = item->fullId(); - FillSponsoredMessagesMenu(controller, itemId, _menu); - } if (!item->isService() && view && actionText.isEmpty()) { if (!hasCopyRestriction(item) && (view->hasVisibleText() || mediaHasTextForCopy)) { diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 59e2ecc563976..e758858cf0add 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -41,6 +41,8 @@ For license and copyright information please follow this link: #include "api/api_updates.h" #include "data/notify/data_notify_settings.h" #include "data/data_bot_app.h" +#include "data/data_saved_messages.h" +#include "data/data_saved_sublist.h" #include "data/data_scheduled_messages.h" #include "data/data_changes.h" #include "data/data_session.h" @@ -145,10 +147,16 @@ struct HistoryItem::CreateConfig { QString originalSenderName; QString originalPostAuthor; + PeerId savedSublistPeer = 0; + QString forwardPsaType; PeerId savedFromPeer = 0; MsgId savedFromMsgId = 0; + PeerId savedFromSenderId = 0; + QString savedFromSenderName; + bool savedFromOutgoing = false; + TimeId editDate = 0; HistoryMessageMarkupData markup; HistoryMessageRepliesData replies; @@ -175,6 +183,13 @@ void HistoryItem::FillForwardedInfo( config.savedFromPeer = peerFromMTP(*savedFromPeer); config.savedFromMsgId = savedFromMsgId->v; } + config.savedFromSenderId = data.vsaved_from_id() + ? peerFromMTP(*data.vsaved_from_id()) + : PeerId(); + config.savedFromSenderName = qs( + data.vsaved_from_name().value_or_empty()); + config.savedFromOutgoing = data.is_saved_out(); + config.imported = data.is_imported(); } @@ -238,7 +253,7 @@ std::unique_ptr HistoryItem::CreateMedia( }); }, [&](const MTPDmessageMediaDocument &media) -> Result { const auto document = media.vdocument(); - if (media.vttl_seconds()) { + if (media.vttl_seconds() && media.is_video()) { LOG(("App Error: " "Unexpected MTPMessageMediaDocument " "with ttl_seconds in CreateMedia.")); @@ -254,7 +269,8 @@ std::unique_ptr HistoryItem::CreateMedia( item, item->history()->owner().processDocument(document), media.is_nopremium(), - media.is_spoiler()); + media.is_spoiler(), + media.vttl_seconds().value_or_empty()); }, [](const MTPDdocumentEmpty &) -> Result { return nullptr; }); @@ -311,9 +327,13 @@ std::unique_ptr HistoryItem::CreateMedia( media.vid().v, }, media.is_via_mention()); }, [&](const MTPDmessageMediaGiveaway &media) -> Result { - return std::make_unique( + return std::make_unique( + item, + Data::ComputeGiveawayStartData(item, media)); + }, [&](const MTPDmessageMediaGiveawayResults &media) -> Result { + return std::make_unique( item, - Data::ComputeGiveawayData(item, media)); + Data::ComputeGiveawayResultsData(item, media)); }, [](const MTPDmessageMediaEmpty &) -> Result { return nullptr; }, [](const MTPDmessageMediaUnsupported &) -> Result { @@ -346,7 +366,8 @@ HistoryItem::HistoryItem( setServiceText({ tr::lng_message_empty(tr::now, Ui::Text::WithEntities) }); - } else if (checked == MediaCheckResult::HasTimeToLive) { + } else if ((checked == MediaCheckResult::HasUnsupportedTimeToLive) + || (checked == MediaCheckResult::HasExpiredMediaTimeToLive)) { createServiceFromMtp(data); applyTTL(data); } else if (checked == MediaCheckResult::HasStoryMention) { @@ -466,7 +487,7 @@ HistoryItem::HistoryItem( } if (!dropForwardInfo) { config.originalDate = original->originalDate(); - if (const auto info = original->hiddenSenderInfo()) { + if (const auto info = original->originalHiddenSenderInfo()) { config.originalSenderName = info->name; } else if (const auto originalSender = original->originalSender()) { config.originalSenderId = originalSender->id; @@ -490,6 +511,11 @@ HistoryItem::HistoryItem( config.savedFromPeer = original->history()->peer->id; config.savedFromMsgId = original->id; //} + + config.savedFromOutgoing = original->out(); + config.savedFromSenderId = original->Get() + ? original->author()->id + : PeerId(); } if (flags & MessageFlag::HasPostAuthor) { config.postAuthor = postAuthor; @@ -609,7 +635,8 @@ HistoryItem::HistoryItem( this, document, skipPremiumEffect, - spoiler); + spoiler, + /*ttlSeconds = */0); setText(caption); } @@ -765,6 +792,9 @@ HistoryItem::~HistoryItem() { if (const auto reply = Get()) { reply->clearData(this); } + if (const auto saved = Get()) { + saved->sublist->removeOne(this); + } clearDependencyMessage(); applyTTL(0); } @@ -788,6 +818,8 @@ HistoryServiceDependentData *HistoryItem::GetServiceDependentData() { return info; } else if (const auto same = Get()) { return same; + } else if (const auto results = Get()) { + return results; } return nullptr; } @@ -1177,10 +1209,10 @@ PeerData *HistoryItem::computeDisplayFrom() const { if (const auto sender = discussionPostOriginalSender()) { return sender; } else if (const auto forwarded = Get()) { - if (_history->peer->isSelf() - || _history->peer->isRepliesChat() - || forwarded->imported) { - return forwarded->originalSender; + if (showForwardsFromSender(forwarded)) { + return forwarded->forwardOfForward() + ? forwarded->savedFromSender + : forwarded->originalSender; } } return author().get(); @@ -1197,10 +1229,10 @@ PeerData *HistoryItem::displayFrom() const { uint8 HistoryItem::colorIndex() const { if (const auto from = displayFrom()) { return from->colorIndex(); - } else if (const auto info = hiddenSenderInfo()) { + } else if (const auto info = displayHiddenSenderInfo()) { return info->colorIndex; } - Unexpected("No displayFrom and no hiddenSenderInfo."); + Unexpected("No displayFrom and no displayHiddenSenderInfo."); } std::unique_ptr HistoryItem::createView( @@ -1223,6 +1255,9 @@ void HistoryItem::invalidateChatListEntry() { if (const auto topic = this->topic()) { topic->lastItemDialogsView().itemInvalidated(this); } + if (const auto sublist = savedSublist()) { + sublist->lastItemDialogsView().itemInvalidated(this); + } } void HistoryItem::customEmojiRepaint() { @@ -1643,7 +1678,8 @@ void HistoryItem::setStoryFields(not_null story) { this, document, /*skipPremiumEffect=*/false, - spoiler); + spoiler, + /*ttlSeconds = */0); } setText(story->caption()); } @@ -2473,16 +2509,32 @@ PeerData *HistoryItem::originalSender() const { return forwarded->originalSender; } const auto peer = _history->peer; - return (peer->isChannel() && !peer->isMegagroup()) ? peer : from(); + return peer->isBroadcast() ? peer : from(); +} + +const HiddenSenderInfo *HistoryItem::originalHiddenSenderInfo() const { + if (const auto forwarded = Get()) { + return forwarded->originalHiddenSenderInfo.get(); + } + return nullptr; } -const HiddenSenderInfo *HistoryItem::hiddenSenderInfo() const { +const HiddenSenderInfo *HistoryItem::displayHiddenSenderInfo() const { if (const auto forwarded = Get()) { - return forwarded->hiddenSenderInfo.get(); + return forwarded->savedFromHiddenSenderInfo + ? forwarded->savedFromHiddenSenderInfo.get() + : forwarded->originalHiddenSenderInfo.get(); } return nullptr; } +bool HistoryItem::showForwardsFromSender( + not_null forwarded) const { + const auto peer = history()->peer; + return !forwarded->story + && (peer->isSelf() || peer->isRepliesChat() || forwarded->imported); +} + not_null HistoryItem::fromOriginal() const { if (const auto forwarded = Get()) { if (forwarded->originalSender) { @@ -3021,6 +3073,34 @@ bool HistoryItem::isEmpty() const { && !Has(); } +Data::SavedSublist *HistoryItem::savedSublist() const { + if (const auto saved = Get()) { + return saved->sublist; + } + return nullptr; +} + +PeerData *HistoryItem::savedSublistPeer() const { + if (const auto sublist = savedSublist()) { + return sublist->peer(); + } + return nullptr; +} + +PeerData *HistoryItem::savedFromSender() const { + if (const auto forwarded = Get()) { + return forwarded->savedFromSender; + } + return nullptr; +} + +const HiddenSenderInfo *HistoryItem::savedFromHiddenSenderInfo() const { + if (const auto forwarded = Get()) { + return forwarded->savedFromHiddenSenderInfo.get(); + } + return nullptr; +} + TextWithEntities HistoryItem::notificationText( NotificationTextOptions options) const { auto result = [&] { @@ -3073,16 +3153,25 @@ ItemPreview HistoryItem::toPreview(ToPreviewOptions options) const { ? tr::lng_from_you(tr::now) : sender->shortName(); }; - result.icon = (Get() != nullptr) + const auto forwarded = Get(); + const auto forwardFromSender = forwarded + && showForwardsFromSender(forwarded); + result.icon = (forwarded + && (!forwardFromSender || forwarded->forwardOfForward())) ? ItemPreview::Icon::ForwardedMessage : replyToStory().valid() ? ItemPreview::Icon::ReplyToStory : ItemPreview::Icon::None; const auto fromForwarded = [&]() -> std::optional { - if (const auto forwarded = Get()) { - return forwarded->originalSender - ? fromSender(forwarded->originalSender) - : forwarded->hiddenSenderInfo->name; + if (forwarded) { + const auto sender = forwarded->forwardOfForward() + ? forwarded->savedFromSender + : forwarded->originalSender; + return sender + ? fromSender(sender) + : forwarded->savedFromHiddenSenderInfo + ? forwarded->savedFromHiddenSenderInfo->name + : forwarded->originalHiddenSenderInfo->name; } return {}; }; @@ -3172,9 +3261,28 @@ void HistoryItem::createComponents(CreateConfig &&config) { } else if (config.inlineMarkup) { mask |= HistoryMessageReplyMarkup::Bit(); } + if (_history->peer->isSelf()) { + mask |= HistoryMessageSaved::Bit(); + } UpdateComponents(mask); + if (const auto saved = Get()) { + if (!config.savedSublistPeer) { + if (config.savedFromPeer) { + config.savedSublistPeer = config.savedFromPeer; + } else if (config.originalSenderId) { + config.savedSublistPeer = config.originalSenderId; + } else if (!config.originalSenderName.isEmpty()) { + config.savedSublistPeer = PeerData::kSavedHiddenAuthorId; + } else { + config.savedSublistPeer = _history->session().userPeerId(); + } + } + const auto peer = _history->owner().peer(config.savedSublistPeer); + saved->sublist = _history->owner().savedMessages().sublist(peer); + } + if (const auto reply = Get()) { reply->set(std::move(config.reply)); if (!reply->updateData(this)) { @@ -3260,9 +3368,10 @@ void HistoryItem::setupForwardedComponent(const CreateConfig &config) { ? _history->owner().peer(originalSender).get() : nullptr; if (!forwarded->originalSender) { - forwarded->hiddenSenderInfo = std::make_unique( - config.originalSenderName, - config.imported); + forwarded->originalHiddenSenderInfo + = std::make_unique( + config.originalSenderName, + config.imported); } forwarded->originalId = config.originalId; forwarded->originalPostAuthor = config.originalPostAuthor; @@ -3270,6 +3379,14 @@ void HistoryItem::setupForwardedComponent(const CreateConfig &config) { forwarded->savedFromPeer = _history->owner().peerLoaded( config.savedFromPeer); forwarded->savedFromMsgId = config.savedFromMsgId; + forwarded->savedFromSender = _history->owner().peerLoaded( + config.savedFromSenderId); + forwarded->savedFromOutgoing = config.savedFromOutgoing; + if (!forwarded->savedFromSender + && !config.savedFromSenderName.isEmpty()) { + forwarded->savedFromHiddenSenderInfo + = std::make_unique(config.savedFromSenderName, false); + } forwarded->imported = config.imported; } @@ -3468,6 +3585,9 @@ void HistoryItem::applyTTL(const MTPDmessageService &data) { void HistoryItem::createComponents(const MTPDmessage &data) { auto config = CreateConfig(); + config.savedSublistPeer = data.vsaved_peer_id() + ? peerFromMTP(*data.vsaved_peer_id()) + : PeerId(); if (const auto forwarded = data.vfwd_from()) { forwarded->match([&](const MTPDmessageFwdHeader &data) { FillForwardedInfo(config, data); @@ -3553,25 +3673,43 @@ void HistoryItem::createServiceFromMtp(const MTPDmessage &message) { const auto ttl = data.vttl_seconds(); Assert(ttl != nullptr); - setSelfDestruct(HistoryServiceSelfDestruct::Type::Video, *ttl); - if (out()) { - setServiceText({ - tr::lng_ttl_video_sent(tr::now, Ui::Text::WithEntities) - }); - } else { - auto result = PreparedServiceText(); - result.links.push_back(fromLink()); - result.text = tr::lng_ttl_video_received( - tr::now, - lt_from, - fromLinkText(), // Link 1. - Ui::Text::WithEntities); - setServiceText(std::move(result)); + if (data.is_video()) { + setSelfDestruct( + HistoryServiceSelfDestruct::Type::Video, + *ttl); + if (out()) { + setServiceText({ + tr::lng_ttl_video_sent( + tr::now, + Ui::Text::WithEntities) + }); + } else { + auto result = PreparedServiceText(); + result.links.push_back(fromLink()); + result.text = tr::lng_ttl_video_received( + tr::now, + lt_from, + fromLinkText(), // Link 1. + Ui::Text::WithEntities); + setServiceText(std::move(result)); + } + } else if (out()) { + auto text = (data.is_voice() + ? tr::lng_ttl_voice_sent + : data.is_round() + ? tr::lng_ttl_round_sent + : tr::lng_message_empty)(tr::now, Ui::Text::WithEntities); + setServiceText({ std::move(text) }); } } else { - setServiceText({ - tr::lng_ttl_video_expired(tr::now, Ui::Text::WithEntities) - }); + auto text = (data.is_video() + ? tr::lng_ttl_video_expired + : data.is_voice() + ? tr::lng_ttl_voice_expired + : data.is_round() + ? tr::lng_ttl_round_expired + : tr::lng_message_empty)(tr::now, Ui::Text::WithEntities); + setServiceText({ std::move(text) }); } }, [&](const MTPDmessageMediaStory &data) { setServiceText(prepareStoryMentionText()); @@ -3705,6 +3843,8 @@ void HistoryItem::createServiceFromMtp(const MTPDmessageService &message) { } else { RemoveComponents(HistoryServiceSameBackground::Bit()); } + } else if (type == mtpc_messageActionGiveawayResults) { + UpdateComponents(HistoryServiceGiveawayResults::Bit()); } if (const auto replyTo = message.vreply_to()) { replyTo->match([&](const MTPDmessageReplyHeader &data) { @@ -3919,7 +4059,7 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { lt_from, fromLinkText(), // Link 1. lt_user, - Ui::Text::Link(user->name(), 2), // Link 2. + Ui::Text::Link(user->name(), 2), // Link 2. Ui::Text::WithEntities); } return result; @@ -4468,18 +4608,45 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { auto prepareRequestedPeer = [&]( const MTPDmessageActionRequestedPeer &action) { - const auto peerId = peerFromMTP(action.vpeer()); - const auto peer = history()->owner().peer(peerId); auto result = PreparedServiceText{}; + result.links.push_back(fromLink()); + + const auto &list = action.vpeers().v; + for (auto i = 0, count = int(list.size()); i != count; ++i) { + const auto id = peerFromMTP(list[i]); + + auto user = _history->owner().peer(id); + result.links.push_back(user->createOpenLink()); + + auto linkText = Ui::Text::Link(user->name(), 2 + i); + if (i == 0) { + result.text = linkText; + } else if (i + 1 == count) { + result.text = tr::lng_action_add_users_and_last( + tr::now, + lt_accumulated, + result.text, + lt_user, + linkText, + Ui::Text::WithEntities); + } else { + result.text = tr::lng_action_add_users_and_one( + tr::now, + lt_accumulated, + result.text, + lt_user, + linkText, + Ui::Text::WithEntities); + } + } + result.text = tr::lng_action_shared_chat_with_bot( tr::now, lt_chat, - Ui::Text::Link(peer->name(), 1), + result.text, lt_bot, Ui::Text::Link(history()->peer->name(), 2), Ui::Text::WithEntities); - result.links.push_back(peer->createOpenLink()); - result.links.push_back(history()->peer->createOpenLink()); return result; }; @@ -4522,19 +4689,35 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { auto prepareGiftCode = [&](const MTPDmessageActionGiftCode &action) { auto result = PreparedServiceText(); _history->session().giftBoxStickersPacks().load(); - result.text = { - (action.is_unclaimed() - ? tr::lng_prize_unclaimed_about - : action.is_via_giveaway() - ? tr::lng_prize_about - : tr::lng_prize_gift_about)( + if (const auto boosted = action.vboost_peer()) { + result.text = { + (action.is_unclaimed() + ? tr::lng_prize_unclaimed_about + : action.is_via_giveaway() + ? tr::lng_prize_about + : tr::lng_prize_gift_about)( + tr::now, + lt_channel, + _from->owner().peer( + peerFromMTP(*action.vboost_peer()))->name()), + }; + } else { + const auto isSelf = (_from->id == _from->session().userPeerId()); + const auto peer = isSelf ? _history->peer : _from; + result.links.push_back(peer->createOpenLink()); + result.text = (isSelf + ? tr::lng_action_gift_received_me + : tr::lng_action_gift_received)( tr::now, - lt_channel, - (action.vboost_peer() - ? _from->owner().peer( - peerFromMTP(*action.vboost_peer()))->name() - : "a channel")), - }; + lt_user, + Ui::Text::Link(peer->name(), 1), // Link 1. + lt_cost, + { Ui::FillAmountAndCurrency( + action.vamount().value_or_empty(), + qs(action.vcurrency().value_or_empty())) }, + Ui::Text::WithEntities); + + } return result; }; @@ -4689,7 +4872,7 @@ void HistoryItem::applyAction(const MTPMessageAction &action) { _from, Data::GiftCode{ .slug = qs(data.vslug()), - .channel = (peerIsChannel(boostedId) + .channel = (boostedId ? history()->owner().channel(boostedId).get() : nullptr), .months = data.vmonths().v, diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index c14c65654cdb1..de206baacbfc7 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -20,6 +20,7 @@ struct HistoryMessageViews; struct HistoryMessageMarkupData; struct HistoryMessageReplyMarkup; struct HistoryMessageTranslation; +struct HistoryMessageForwarded; struct HistoryServiceDependentData; enum class HistorySelfDestructType; struct PreparedServiceText; @@ -56,6 +57,7 @@ class ForumTopic; class Thread; struct SponsoredFrom; class Story; +class SavedSublist; } // namespace Data namespace Main { @@ -480,11 +482,21 @@ class HistoryItem final : public RuntimeComposer { [[nodiscard]] TimeId originalDate() const; [[nodiscard]] PeerData *originalSender() const; - [[nodiscard]] const HiddenSenderInfo *hiddenSenderInfo() const; + [[nodiscard]] const HiddenSenderInfo *originalHiddenSenderInfo() const; [[nodiscard]] not_null fromOriginal() const; [[nodiscard]] QString originalPostAuthor() const; [[nodiscard]] MsgId originalId() const; + [[nodiscard]] Data::SavedSublist *savedSublist() const; + [[nodiscard]] PeerData *savedSublistPeer() const; + [[nodiscard]] PeerData *savedFromSender() const; + [[nodiscard]] const HiddenSenderInfo *savedFromHiddenSenderInfo() const; + + [[nodiscard]] const HiddenSenderInfo *displayHiddenSenderInfo() const; + + [[nodiscard]] bool showForwardsFromSender( + not_null forwarded) const; + [[nodiscard]] bool isEmpty() const; [[nodiscard]] MessageGroupId groupId() const; diff --git a/Telegram/SourceFiles/history/history_item_components.cpp b/Telegram/SourceFiles/history/history_item_components.cpp index 2fee9928f2df3..956f6e644379e 100644 --- a/Telegram/SourceFiles/history/history_item_components.cpp +++ b/Telegram/SourceFiles/history/history_item_components.cpp @@ -190,7 +190,7 @@ void HistoryMessageForwarded::create(const HistoryMessageVia *via) const { const auto name = TextWithEntities{ .text = (originalSender ? originalSender->name() - : hiddenSenderInfo->name) + : originalHiddenSenderInfo->name) }; if (!originalPostAuthor.isEmpty()) { phrase = tr::lng_forwarded_signed( @@ -216,7 +216,7 @@ void HistoryMessageForwarded::create(const HistoryMessageVia *via) const { lt_channel, Ui::Text::Link(phrase.text, 1), // Link 1. lt_inline_bot, - Ui::Text::Link('@' + via->bot->username(), 2), // Link 2. + Ui::Text::Link('@' + via->bot->username(), 2), // Link 2. Ui::Text::WithEntities); } else { phrase = tr::lng_forwarded_via( @@ -224,7 +224,7 @@ void HistoryMessageForwarded::create(const HistoryMessageVia *via) const { lt_user, Ui::Text::Link(phrase.text, 1), // Link 1. lt_inline_bot, - Ui::Text::Link('@' + via->bot->username(), 2), // Link 2. + Ui::Text::Link('@' + via->bot->username(), 2), // Link 2. Ui::Text::WithEntities); } } else { diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h index 0cce4e499d12f..b4a6981bb69d9 100644 --- a/Telegram/SourceFiles/history/history_item_components.h +++ b/Telegram/SourceFiles/history/history_item_components.h @@ -126,9 +126,13 @@ class HiddenSenderInfo { struct HistoryMessageForwarded : public RuntimeComponent { void create(const HistoryMessageVia *via) const; + [[nodiscard]] bool forwardOfForward() const { + return savedFromSender || savedFromHiddenSenderInfo; + } + TimeId originalDate = 0; PeerData *originalSender = nullptr; - std::unique_ptr hiddenSenderInfo; + std::unique_ptr originalHiddenSenderInfo; QString originalPostAuthor; QString psaType; MsgId originalId = 0; @@ -136,10 +140,19 @@ struct HistoryMessageForwarded : public RuntimeComponent savedFromHiddenSenderInfo; + + bool savedFromOutgoing = false; bool imported = false; bool story = false; }; +struct HistoryMessageSaved : public RuntimeComponent { + Data::SavedSublist *sublist = nullptr; +}; + class ReplyToMessagePointer final { public: ReplyToMessagePointer(HistoryItem *item = nullptr) : _data(item) { @@ -599,6 +612,11 @@ struct HistoryServiceSameBackground , public HistoryServiceDependentData { }; +struct HistoryServiceGiveawayResults +: public RuntimeComponent +, public HistoryServiceDependentData { +}; + enum class HistorySelfDestructType { Photo, Video, diff --git a/Telegram/SourceFiles/history/history_item_helpers.cpp b/Telegram/SourceFiles/history/history_item_helpers.cpp index d3bdddacac61c..4a29e4e93dee5 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.cpp +++ b/Telegram/SourceFiles/history/history_item_helpers.cpp @@ -14,6 +14,7 @@ For license and copyright information please follow this link: #include "data/data_channel.h" #include "data/data_chat.h" #include "data/data_changes.h" +#include "data/data_document.h" #include "data/data_group_call.h" #include "data/data_forum.h" #include "data/data_forum_topic.h" @@ -451,7 +452,7 @@ MediaCheckResult CheckMessageMedia(const MTPMessageMedia &media) { }, [](const MTPDmessageMediaPhoto &data) { const auto photo = data.vphoto(); if (data.vttl_seconds()) { - return Result::HasTimeToLive; + return Result::HasUnsupportedTimeToLive; } else if (!photo) { return Result::Empty; } @@ -463,7 +464,11 @@ MediaCheckResult CheckMessageMedia(const MTPMessageMedia &media) { }, [](const MTPDmessageMediaDocument &data) { const auto document = data.vdocument(); if (data.vttl_seconds()) { - return Result::HasTimeToLive; + if (data.is_video()) { + return Result::HasUnsupportedTimeToLive; + } else if (!document) { + return Result::HasExpiredMediaTimeToLive; + } } else if (!document) { return Result::Empty; } @@ -498,6 +503,8 @@ MediaCheckResult CheckMessageMedia(const MTPMessageMedia &media) { : Result::Good; }, [](const MTPDmessageMediaGiveaway &) { return Result::Good; + }, [](const MTPDmessageMediaGiveawayResults &) { + return Result::Good; }, [](const MTPDmessageMediaUnsupported &) { return Result::Unsupported; }); @@ -777,3 +784,31 @@ void ShowTrialTranscribesToast(int left, TimeId until) { .filter = filter, }); } + +void ClearMediaAsExpired(not_null item) { + if (const auto media = item->media()) { + if (!media->ttlSeconds()) { + return; + } + if (const auto document = media->document()) { + item->applyEditionToHistoryCleared(); + auto text = (document->isVideoFile() + ? tr::lng_ttl_video_expired + : document->isVoiceMessage() + ? tr::lng_ttl_voice_expired + : document->isVideoMessage() + ? tr::lng_ttl_round_expired + : tr::lng_message_empty)(tr::now, Ui::Text::WithEntities); + item->updateServiceText(PreparedServiceText{ std::move(text) }); + } else if (const auto photo = media->photo()) { + item->applyEditionToHistoryCleared(); + item->updateServiceText(PreparedServiceText{ + tr::lng_ttl_photo_expired(tr::now, Ui::Text::WithEntities) + }); + } + } +} + +[[nodiscard]] bool IsVoiceOncePlayable(not_null item) { + return !item->out() && item->media()->ttlSeconds(); +} diff --git a/Telegram/SourceFiles/history/history_item_helpers.h b/Telegram/SourceFiles/history/history_item_helpers.h index 661efe6520499..f96d9c0922348 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.h +++ b/Telegram/SourceFiles/history/history_item_helpers.h @@ -43,7 +43,8 @@ enum class MediaCheckResult { Good, Unsupported, Empty, - HasTimeToLive, + HasExpiredMediaTimeToLive, + HasUnsupportedTimeToLive, HasStoryMention, }; [[nodiscard]] MediaCheckResult CheckMessageMedia( @@ -155,3 +156,6 @@ ClickHandlerPtr JumpToStoryClickHandler( CallId callId); void ShowTrialTranscribesToast(int left, TimeId until); + +void ClearMediaAsExpired(not_null item); +[[nodiscard]] bool IsVoiceOncePlayable(not_null item); diff --git a/Telegram/SourceFiles/history/history_item_reply_markup.cpp b/Telegram/SourceFiles/history/history_item_reply_markup.cpp index c15381e92a186..05af2717918b2 100644 --- a/Telegram/SourceFiles/history/history_item_reply_markup.cpp +++ b/Telegram/SourceFiles/history/history_item_reply_markup.cpp @@ -37,10 +37,11 @@ namespace { } [[nodiscard]] RequestPeerQuery RequestPeerQueryFromTL( - const MTPRequestPeerType &query) { + const MTPDkeyboardButtonRequestPeer &query) { using Type = RequestPeerQuery::Type; using Restriction = RequestPeerQuery::Restriction; auto result = RequestPeerQuery(); + result.maxQuantity = query.vmax_quantity().v; const auto restriction = [](const MTPBool *value) { return !value ? Restriction::Any @@ -51,7 +52,7 @@ namespace { const auto rights = [](const MTPChatAdminRights *value) { return value ? ChatAdminRightsInfo(*value).flags : ChatAdminRights(); }; - query.match([&](const MTPDrequestPeerTypeUser &data) { + query.vpeer_type().match([&](const MTPDrequestPeerTypeUser &data) { result.type = Type::User; result.userIsBot = restriction(data.vbot()); result.userIsPremium = restriction(data.vpremium()); @@ -134,7 +135,7 @@ void HistoryMessageMarkupData::fillRows( }, [&](const MTPDkeyboardButtonRequestPhone &data) { row.emplace_back(Type::RequestPhone, qs(data.vtext())); }, [&](const MTPDkeyboardButtonRequestPeer &data) { - const auto query = RequestPeerQueryFromTL(data.vpeer_type()); + const auto query = RequestPeerQueryFromTL(data); row.emplace_back( Type::RequestPeer, qs(data.vtext()), diff --git a/Telegram/SourceFiles/history/history_item_reply_markup.h b/Telegram/SourceFiles/history/history_item_reply_markup.h index 82843211383a9..5ed8dc4a374cb 100644 --- a/Telegram/SourceFiles/history/history_item_reply_markup.h +++ b/Telegram/SourceFiles/history/history_item_reply_markup.h @@ -45,6 +45,8 @@ struct RequestPeerQuery { Yes, No, }; + + int maxQuantity = 0; Type type = Type::User; Restriction userIsBot = Restriction::Any; Restriction userIsPremium = Restriction::Any; diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 4cf3f03c81cc0..914af7961ad99 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -4561,7 +4561,7 @@ bool HistoryWidget::eventFilter(QObject *obj, QEvent *e) { if (k->key() == Qt::Key_Up) { #ifdef Q_OS_MAC // Cmd + Up is used instead of Home. - if (!_field->textCursor().atStart()) { + if (HasSendText(_field)) { return false; } #endif @@ -4569,7 +4569,7 @@ bool HistoryWidget::eventFilter(QObject *obj, QEvent *e) { } else if (k->key() == Qt::Key_Down) { #ifdef Q_OS_MAC // Cmd + Down is used instead of End. - if (!_field->textCursor().atEnd()) { + if (HasSendText(_field)) { return false; } #endif diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_search.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_search.cpp index 0786b07452036..d1dbbd0e358a2 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_search.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_search.cpp @@ -281,7 +281,7 @@ class TopBar final : public Ui::RpWidget { base::unique_qptr _cancel; base::unique_qptr _select; - rpl::variable _from = nullptr;; + rpl::variable _from = nullptr; base::Timer _searchTimer; diff --git a/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp b/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp index 9c7ce70c5ab97..4eef1697084fe 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp @@ -376,7 +376,7 @@ void PreviewWrap::paintEvent(QPaintEvent *e) { userpicTop, width(), st::msgPhotoSize); - } else if (const auto info = item->hiddenSenderInfo()) { + } else if (const auto info = item->originalHiddenSenderInfo()) { if (info->customUserpic.empty()) { info->emptyUserpic.paintCircle( p, diff --git a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp index ff3347cf523b9..868e55383667f 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp @@ -127,7 +127,7 @@ void ForwardPanel::checkTexts() { for (const auto item : _data.items) { if (const auto from = item->originalSender()) { version += from->nameVersion(); - } else if (const auto info = item->hiddenSenderInfo()) { + } else if (const auto info = item->originalHiddenSenderInfo()) { ++version; } else { Unexpected("Corrupt forwarded information in message."); @@ -168,7 +168,7 @@ void ForwardPanel::updateTexts() { names.push_back(from->shortName()); fullname = from->name(); } - } else if (const auto info = item->hiddenSenderInfo()) { + } else if (const auto info = item->originalHiddenSenderInfo()) { if (!insertedNames.contains(info->name)) { insertedNames.emplace(info->name); names.push_back(info->firstName); diff --git a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp index cba6d2e7cfbc4..3f507ca61bc0b 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp @@ -82,8 +82,7 @@ enum class FilterType { const auto durationString = Ui::FormatDurationText(duration / kPrecision); const auto decimalPart = duration % kPrecision; return QString("%1%2%3") - .arg(durationString) - .arg(QLocale().decimalPoint()) + .arg(durationString, QLocale().decimalPoint()) .arg(decimalPart); } diff --git a/Telegram/SourceFiles/history/view/history_view_contact_status.cpp b/Telegram/SourceFiles/history/view/history_view_contact_status.cpp index dffcdde9541aa..5767831e297af 100644 --- a/Telegram/SourceFiles/history/view/history_view_contact_status.cpp +++ b/Telegram/SourceFiles/history/view/history_view_contact_status.cpp @@ -98,16 +98,15 @@ namespace { [[nodiscard]] rpl::producer PeerCustomStatus( not_null peer) { - const auto user = peer->asUser(); - if (!user) { + if (peer->isChat()) { return rpl::single(TextWithEntities()); } - const auto owner = &user->owner(); - return user->session().changes().peerFlagsValue( - user, + const auto owner = &peer->owner(); + return peer->session().changes().peerFlagsValue( + peer, Data::PeerUpdate::Flag::EmojiStatus ) | rpl::map([=] { - const auto id = user->emojiStatusId(); + const auto id = peer->emojiStatusId(); return id ? ResolveIsCustom(owner, id) : rpl::single(TextWithEntities()); @@ -715,8 +714,9 @@ void ContactStatus::setupShareHandler(not_null user) { void ContactStatus::setupUnarchiveHandler(not_null peer) { _inner->unarchiveClicks( - ) | rpl::start_with_next([=] { - Window::ToggleHistoryArchived(peer->owner().history(peer), false); + ) | rpl::start_with_next([=, show = _controller->uiShow()] { + using namespace Window; + ToggleHistoryArchived(show, peer->owner().history(peer), false); peer->owner().notifySettings().resetToDefault(peer); if (const auto settings = peer->settings()) { const auto flags = PeerSetting::AutoArchived diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index cc680ab1d5ed8..7d58593d015da 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -109,7 +109,7 @@ void SavePhotoToFile(not_null photo) { return; } - const auto image = media->image(Data::PhotoSize::Large)->original(); + const auto image = media->image(Data::PhotoSize::Large)->original(); // clazy:exclude=unused-non-trivial-variable FileDialog::GetWritePath( Core::App().getFileDialogParent(), tr::lng_save_photo(tr::now), diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index 5de4648e9f636..c0c1c7995c658 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -64,16 +64,20 @@ Element *MousedElement/* = nullptr*/; HistoryMessageForwarded *prevForwarded, not_null item, HistoryMessageForwarded *forwarded) { - const auto sender = previous->originalSender(); + const auto sender = previous->displayFrom(); if ((prevForwarded != nullptr) != (forwarded != nullptr)) { return false; - } else if (sender != item->originalSender()) { + } else if (sender != item->displayFrom()) { return false; } else if (!prevForwarded || sender) { return true; } - const auto previousInfo = prevForwarded->hiddenSenderInfo.get(); - const auto itemInfo = forwarded->hiddenSenderInfo.get(); + const auto previousInfo = prevForwarded->savedFromHiddenSenderInfo + ? prevForwarded->savedFromHiddenSenderInfo.get() + : prevForwarded->originalHiddenSenderInfo.get(); + const auto itemInfo = forwarded->savedFromHiddenSenderInfo + ? forwarded->savedFromHiddenSenderInfo.get() + : forwarded->originalHiddenSenderInfo.get(); Assert(previousInfo != nullptr); Assert(itemInfo != nullptr); return (*previousInfo == *itemInfo); @@ -806,7 +810,7 @@ auto Element::contextDependentServiceText() -> TextWithLinks { return {}; } const auto from = item->from(); - const auto topicUrl = u"internal:url:https://t.me/c/%1/%2"_q + const auto topicUrl = u"internal:url:https://t.me/c/%1/%2"_q .arg(peerToChannel(peerId).bare) .arg(topicRootId.bare); const auto fromLink = [&](int index) { @@ -1389,6 +1393,14 @@ bool Element::allowTextSelectionByHandler( return false; } +bool Element::usesBubblePattern(const PaintContext &context) const { + return (context.selection != FullSelection) + && hasOutLayout() + && context.bubblesPattern + && !context.viewport.isEmpty() + && !context.bubblesPattern->pixmap.size().isEmpty(); +} + bool Element::hasVisibleText() const { return false; } diff --git a/Telegram/SourceFiles/history/view/history_view_element.h b/Telegram/SourceFiles/history/view/history_view_element.h index a619f15de62d9..f9c3bab2f71fc 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.h +++ b/Telegram/SourceFiles/history/view/history_view_element.h @@ -56,7 +56,8 @@ enum class Context : char { Replies, Pinned, AdminLog, - ContactPreview + ContactPreview, + SavedSublist, }; enum class OnlyEmojiAndSpaces : char { @@ -465,6 +466,8 @@ class Element [[nodiscard]] virtual bool allowTextSelectionByHandler( const ClickHandlerPtr &handler) const; + [[nodiscard]] bool usesBubblePattern(const PaintContext &context) const; + struct VerticalRepaintRange { int top = 0; int height = 0; diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index 08332097f3537..50822615c728f 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -2181,7 +2181,7 @@ void ListWidget::paintEvent(QPaintEvent *e) { userpicTop, view->width(), st::msgPhotoSize); - } else if (const auto info = item->hiddenSenderInfo()) { + } else if (const auto info = item->displayHiddenSenderInfo()) { if (info->customUserpic.empty()) { info->emptyUserpic.paintCircle( p, @@ -3665,7 +3665,7 @@ void ListWidget::performDrag() { _reactionsManager->updateButton({}); _controller->widget()->launchDrag( std::move(mimeData), - crl::guard(this, [=] { mouseActionUpdate(QCursor::pos()); }));; + crl::guard(this, [=] { mouseActionUpdate(QCursor::pos()); })); } } diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 238efa500b24b..36bcb31d6d91f 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -182,7 +182,7 @@ void KeyboardStyle::paintButtonBg( const auto &small = sti->msgServiceBgCornersSmall; const auto &large = sti->msgServiceBgCornersLarge; auto corners = Ui::CornersPixmaps(); - int radiuses[4]; + int radiuses[4]; for (auto i = 0; i != 4; ++i) { const auto isLarge = (rounding[i] == Corner::Large); corners.p[i] = (isLarge ? large : small).p[i]; @@ -403,6 +403,11 @@ Message::Message( , _bottomInfo( &data->history()->owner().reactions(), BottomInfoDataFromMessage(this)) { + if (const auto media = data->media()) { + if (media->giveawayResults()) { + _hideReply = 1; + } + } initLogEntryOriginal(); initPsa(); refreshReactions(); @@ -733,7 +738,7 @@ QSize Message::performCountOptimalSize() { validateFromNameText(from); const auto &name = from ? _fromName - : item->hiddenSenderInfo()->nameText(); + : item->displayHiddenSenderInfo()->nameText(); auto namew = st::msgPadding.left() + name.maxWidth() + (_fromNameStatus @@ -1384,7 +1389,7 @@ void Message::paintFromName( const auto stm = context.messageStyle(); const auto from = item->displayFrom(); - const auto info = from ? nullptr : item->hiddenSenderInfo(); + const auto info = from ? nullptr : item->displayHiddenSenderInfo(); Assert(from || info); const auto nameFg = !context.outbg ? FromNameFg(context, colorIndex()) @@ -1405,13 +1410,12 @@ void Message::paintFromName( const auto y = trect.top(); auto color = nameFg; color.setAlpha(115); - const auto user = from->asUser(); - const auto id = user ? user->emojiStatusId() : 0; + const auto id = from ? from->emojiStatusId() : 0; if (_fromNameStatus->id != id) { const auto that = const_cast(this); _fromNameStatus->custom = id ? std::make_unique( - user->owner().customEmojiManager().create( + history()->owner().customEmojiManager().create( id, [=] { that->customEmojiRepaint(); }), kPlayStatusLimit) @@ -1970,13 +1974,6 @@ void Message::unloadHeavyPart() { } } -bool Message::showForwardsFromSender( - not_null forwarded) const { - const auto peer = data()->history()->peer; - return !forwarded->story - && (peer->isSelf() || peer->isRepliesChat() || forwarded->imported); -} - bool Message::hasFromPhoto() const { if (isHidden()) { return false; @@ -1986,7 +1983,8 @@ bool Message::hasFromPhoto() const { return true; case Context::History: case Context::Pinned: - case Context::Replies: { + case Context::Replies: + case Context::SavedSublist: { const auto item = data(); if (item->isPost()) { return false; @@ -2324,7 +2322,7 @@ bool Message::getStateFromName( if (from) { validateFromNameText(from); return &_fromName; - } else if (const auto info = item->hiddenSenderInfo()) { + } else if (const auto info = item->displayHiddenSenderInfo()) { return &info->nameText(); } else { Unexpected("Corrupt forwarded information in message."); @@ -3012,7 +3010,10 @@ void Message::validateFromNameText(PeerData *from) const { from->name(), Ui::NameTextOptions()); } - if (from->isPremium()) { + if (from->isPremium() + || (from->isChannel() + && from->emojiStatusId() + && from != history()->peer)) { if (!_fromNameStatus) { _fromNameStatus = std::make_unique(); const auto size = st::emojiSize; @@ -3157,10 +3158,17 @@ bool Message::hasFromName() const { return true; case Context::History: case Context::Pinned: - case Context::Replies: { + case Context::Replies: + case Context::SavedSublist: { const auto item = data(); const auto peer = item->history()->peer; if (hasOutLayout() && !item->from()->isChannel()) { + if (peer->isSelf()) { + if (const auto forwarded = item->Get()) { + return forwarded->savedFromSender + && forwarded->savedFromSender->isChannel(); + } + } return false; } else if (!peer->isUser()) { if (const auto media = this->media()) { @@ -3172,7 +3180,7 @@ bool Message::hasFromName() const { if (forwarded->imported && peer.get() == forwarded->originalSender) { return false; - } else if (showForwardsFromSender(forwarded)) { + } else if (item->showForwardsFromSender(forwarded)) { return true; } } @@ -3196,8 +3204,9 @@ bool Message::displayForwardedFrom() const { if (const auto forwarded = item->Get()) { if (forwarded->story) { return true; - } else if (showForwardsFromSender(forwarded)) { - return false; + } else if (item->showForwardsFromSender(forwarded)) { + return forwarded->savedFromSender + && (forwarded->savedFromSender != forwarded->originalSender); } if (const auto sender = item->discussionPostOriginalSender()) { if (sender == forwarded->originalSender) { @@ -3213,12 +3222,21 @@ bool Message::displayForwardedFrom() const { bool Message::hasOutLayout() const { const auto item = data(); if (item->history()->peer->isSelf()) { - return !item->Has(); + if (const auto forwarded = item->Get()) { + return (context() == Context::SavedSublist) + && (!forwarded->forwardOfForward() + ? (forwarded->originalSender + && forwarded->originalSender->isSelf()) + : ((forwarded->savedFromSender + && forwarded->savedFromSender->isSelf()) + || forwarded->savedFromOutgoing)); + } + return true; } else if (const auto forwarded = item->Get()) { if (!forwarded->imported || !forwarded->originalSender || !forwarded->originalSender->isSelf()) { - if (showForwardsFromSender(forwarded)) { + if (item->showForwardsFromSender(forwarded)) { return false; } } @@ -3320,6 +3338,7 @@ bool Message::displayFastReply() const { bool Message::displayRightActionComments() const { return !isPinnedContext() + && (context() != Context::SavedSublist) && data()->repliesAreComments() && media() && media()->isDisplayed() @@ -3354,11 +3373,10 @@ bool Message::displayFastShare() const { return !peer->isMegagroup(); } else if (const auto user = peer->asUser()) { if (const auto forwarded = item->Get()) { - return !showForwardsFromSender(forwarded) - && !item->out() + return !item->out() && forwarded->originalSender - && forwarded->originalSender->isChannel() - && !forwarded->originalSender->isMegagroup(); + && forwarded->originalSender->isBroadcast() + && !item->showForwardsFromSender(forwarded); } else if (user->isBot() && !item->out()) { if (const auto media = this->media()) { return media->allowsFastShare(); @@ -3377,7 +3395,7 @@ bool Message::displayGoToOriginal() const { return forwarded->savedFromPeer && forwarded->savedFromMsgId && (!item->externalReply() || !hasBubble()) - && !(context() == Context::Replies); + && (context() != Context::Replies); } return false; } @@ -3446,7 +3464,9 @@ void Message::drawRightAction( } else { const auto &icon = data()->isSponsored() ? st->historyFastCloseIcon() - : (displayFastShare() && !isPinnedContext()) + : (displayFastShare() + && !isPinnedContext() + && this->context() != Context::SavedSublist) ? st->historyFastShareIcon() : st->historyGoToOriginalIcon(); icon.paintInCenter(p, { left, top, size->width(), size->height() }); @@ -3478,7 +3498,8 @@ ClickHandlerPtr Message::prepareRightActionLink() const { return HideSponsoredClickHandler(); } else if (isPinnedContext()) { return JumpToMessageClickHandler(data()); - } else if (displayRightActionComments()) { + } else if ((context() != Context::SavedSublist) + && displayRightActionComments()) { return createGoToCommentsLink(); } const auto sessionId = data()->history()->session().uniqueId(); @@ -3650,7 +3671,7 @@ void Message::fromNameUpdated(int width) const { const auto nameText = [&]() -> const Ui::Text::String * { if (from) { return &_fromName; - } else if (const auto info = item->hiddenSenderInfo()) { + } else if (const auto info= item->originalHiddenSenderInfo()) { return &info->nameText(); } else { Unexpected("Corrupted forwarded information in message."); diff --git a/Telegram/SourceFiles/history/view/history_view_message.h b/Telegram/SourceFiles/history/view/history_view_message.h index 4b75f21fa6114..e849d5f63e8e4 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.h +++ b/Telegram/SourceFiles/history/view/history_view_message.h @@ -173,8 +173,6 @@ class Message final : public Element { void initPsa(); void fromNameUpdated(int width) const; - [[nodiscard]] bool showForwardsFromSender( - not_null forwarded) const; [[nodiscard]] TextSelection skipTextSelection( TextSelection selection) const; [[nodiscard]] TextSelection unskipTextSelection( diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.h b/Telegram/SourceFiles/history/view/history_view_replies_section.h index 68f95177bfb9f..a4bfdabb65384 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.h +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.h @@ -374,7 +374,6 @@ class RepliesWidget final }; - class RepliesMemento final : public Window::SectionMemento { public: RepliesMemento( diff --git a/Telegram/SourceFiles/history/view/history_view_reply.cpp b/Telegram/SourceFiles/history/view/history_view_reply.cpp index b0975a387bbaa..b2d89f2fd0fe6 100644 --- a/Telegram/SourceFiles/history/view/history_view_reply.cpp +++ b/Telegram/SourceFiles/history/view/history_view_reply.cpp @@ -208,7 +208,7 @@ void Reply::update( ? _externalSender : nullptr; _hiddenSenderColorIndexPlusOne = (!_colorPeer && message) - ? (message->hiddenSenderInfo()->colorIndex + 1) + ? (message->originalHiddenSenderInfo()->colorIndex + 1) : 0; const auto hasPreview = (story && story->hasReplyPreview()) @@ -376,8 +376,8 @@ QString Reply::senderName( const auto forwarded = data->resolvedMessage->Get(); if (forwarded) { - Assert(forwarded->hiddenSenderInfo != nullptr); - return forwarded->hiddenSenderInfo->name; + Assert(forwarded->originalHiddenSenderInfo != nullptr); + return forwarded->originalHiddenSenderInfo->name; } } return QString(); diff --git a/Telegram/SourceFiles/history/view/history_view_service_message.cpp b/Telegram/SourceFiles/history/view/history_view_service_message.cpp index b9b27a1685c3b..319a2980a96fe 100644 --- a/Telegram/SourceFiles/history/view/history_view_service_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_service_message.cpp @@ -673,6 +673,8 @@ TextState Service::textState(QPoint point, StateRequest request) const { } } else if (const auto same = item->Get()) { result.link = same->lnk; + } else if (const auto results = item->Get()) { + result.link = results->lnk; } else if (media && data()->showSimilarChannels()) { result = media->textState(mediaPoint, request); } diff --git a/Telegram/SourceFiles/history/view/history_view_sublist_section.cpp b/Telegram/SourceFiles/history/view/history_view_sublist_section.cpp new file mode 100644 index 0000000000000..5664029bbc4b5 --- /dev/null +++ b/Telegram/SourceFiles/history/view/history_view_sublist_section.cpp @@ -0,0 +1,657 @@ +/* +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 "history/view/history_view_sublist_section.h" + +#include "main/main_session.h" +#include "data/data_saved_messages.h" +#include "data/data_saved_sublist.h" +#include "data/data_session.h" +#include "data/data_peer_values.h" +#include "data/data_user.h" +#include "history/view/history_view_top_bar_widget.h" +#include "history/view/history_view_translate_bar.h" +#include "history/view/history_view_list_widget.h" +#include "history/history.h" +#include "history/history_item.h" +#include "lang/lang_keys.h" +#include "ui/chat/chat_style.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/scroll_area.h" +#include "ui/widgets/shadow.h" +#include "window/window_session_controller.h" +#include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" +#include "styles/style_window.h" + +namespace HistoryView { +namespace { + +} // namespace + +SublistMemento::SublistMemento(not_null sublist) +: _sublist(sublist) { + const auto selfId = sublist->session().userPeerId(); + _list.setAroundPosition({ + .fullId = FullMsgId(selfId, ShowAtUnreadMsgId), + .date = TimeId(0), + }); +} + +object_ptr SublistMemento::createWidget( + QWidget *parent, + not_null controller, + Window::Column column, + const QRect &geometry) { + if (column == Window::Column::Third) { + return nullptr; + } + auto result = object_ptr( + parent, + controller, + _sublist); + result->setInternalState(geometry, this); + return result; +} + +SublistWidget::SublistWidget( + QWidget *parent, + not_null controller, + not_null sublist) +: Window::SectionWidget(parent, controller, sublist->peer()) +, _sublist(sublist) +, _history(sublist->owner().history(sublist->session().user())) +, _topBar(this, controller) +, _topBarShadow(this) +, _translateBar(std::make_unique(this, controller, _history)) +, _scroll(std::make_unique( + this, + controller->chatStyle()->value(lifetime(), st::historyScroll), + false)) +, _cornerButtons( + _scroll.get(), + controller->chatStyle(), + static_cast(this)) { + controller->chatStyle()->paletteChanged( + ) | rpl::start_with_next([=] { + _scroll->updateBars(); + }, _scroll->lifetime()); + + setupOpenChatButton(); + setupAboutHiddenAuthor(); + + Window::ChatThemeValueFromPeer( + controller, + sublist->peer() + ) | rpl::start_with_next([=](std::shared_ptr &&theme) { + _theme = std::move(theme); + controller->setChatStyleTheme(_theme); + }, lifetime()); + + _topBar->setActiveChat( + TopBarWidget::ActiveChat{ + .key = sublist, + .section = Dialogs::EntryState::Section::SavedSublist, + }, + nullptr); + + _topBar->move(0, 0); + _topBar->resizeToWidth(width()); + _topBar->show(); + + _topBar->deleteSelectionRequest( + ) | rpl::start_with_next([=] { + confirmDeleteSelected(); + }, _topBar->lifetime()); + _topBar->forwardSelectionRequest( + ) | rpl::start_with_next([=] { + confirmForwardSelected(); + }, _topBar->lifetime()); + _topBar->clearSelectionRequest( + ) | rpl::start_with_next([=] { + clearSelected(); + }, _topBar->lifetime()); + + _translateBar->raise(); + _topBarShadow->raise(); + controller->adaptive().value( + ) | rpl::start_with_next([=] { + updateAdaptiveLayout(); + }, lifetime()); + + _inner = _scroll->setOwnedWidget(object_ptr( + this, + controller, + static_cast(this))); + _scroll->move(0, _topBar->height()); + _scroll->show(); + _scroll->scrolls( + ) | rpl::start_with_next([=] { + onScroll(); + }, lifetime()); + + setupTranslateBar(); +} + +SublistWidget::~SublistWidget() = default; + +void SublistWidget::setupOpenChatButton() { + if (_sublist->peer()->isSavedHiddenAuthor()) { + return; + } + _openChatButton = std::make_unique( + this, + (_sublist->peer()->isBroadcast() + ? tr::lng_saved_open_channel(tr::now) + : _sublist->peer()->isUser() + ? tr::lng_saved_open_chat(tr::now) + : tr::lng_saved_open_group(tr::now)), + st::historyComposeButton); + + _openChatButton->setClickedCallback([=] { + controller()->showPeerHistory( + _sublist->peer(), + Window::SectionShow::Way::Forward); + }); +} + +void SublistWidget::setupAboutHiddenAuthor() { + if (!_sublist->peer()->isSavedHiddenAuthor()) { + return; + } + _aboutHiddenAuthor = std::make_unique(this); + _aboutHiddenAuthor->paintRequest() | rpl::start_with_next([=] { + auto p = QPainter(_aboutHiddenAuthor.get()); + auto rect = _aboutHiddenAuthor->rect(); + + p.fillRect(rect, st::historyReplyBg); + + p.setFont(st::normalFont); + p.setPen(st::windowSubTextFg); + p.drawText( + rect.marginsRemoved( + QMargins(st::historySendPadding, 0, st::historySendPadding, 0)), + tr::lng_saved_about_hidden(tr::now), + style::al_center); + }, _aboutHiddenAuthor->lifetime()); +} + +void SublistWidget::setupTranslateBar() { + controller()->adaptive().oneColumnValue( + ) | rpl::start_with_next([=, raw = _translateBar.get()](bool one) { + raw->setShadowGeometryPostprocess([=](QRect geometry) { + if (!one) { + geometry.setLeft(geometry.left() + st::lineWidth); + } + return geometry; + }); + }, _translateBar->lifetime()); + + _translateBarHeight = 0; + _translateBar->heightValue( + ) | rpl::start_with_next([=](int height) { + if (const auto delta = height - _translateBarHeight) { + _translateBarHeight = height; + setGeometryWithTopMoved(geometry(), delta); + } + }, _translateBar->lifetime()); + + _translateBar->finishAnimating(); +} + +void SublistWidget::cornerButtonsShowAtPosition( + Data::MessagePosition position) { + showAtPosition(position); +} + +Data::Thread *SublistWidget::cornerButtonsThread() { + return nullptr; +} + +FullMsgId SublistWidget::cornerButtonsCurrentId() { + return {}; +} + +bool SublistWidget::cornerButtonsIgnoreVisibility() { + return animatingShow(); +} + +std::optional SublistWidget::cornerButtonsDownShown() { + const auto top = _scroll->scrollTop() + st::historyToDownShownAfter; + if (top < _scroll->scrollTopMax() || _cornerButtons.replyReturn()) { + return true; + } else if (_inner->loadedAtBottomKnown()) { + return !_inner->loadedAtBottom(); + } + return std::nullopt; +} + +bool SublistWidget::cornerButtonsUnreadMayBeShown() { + return _inner->loadedAtBottomKnown(); +} + +bool SublistWidget::cornerButtonsHas(CornerButtonType type) { + return (type == CornerButtonType::Down); +} + +void SublistWidget::showAtPosition( + Data::MessagePosition position, + FullMsgId originId) { + _inner->showAtPosition( + position, + {}, + _cornerButtons.doneJumpFrom(position.fullId, originId)); +} + +void SublistWidget::updateAdaptiveLayout() { + _topBarShadow->moveToLeft( + controller()->adaptive().isOneColumn() ? 0 : st::lineWidth, + _topBar->height()); +} + +not_null SublistWidget::sublist() const { + return _sublist; +} + +Dialogs::RowDescriptor SublistWidget::activeChat() const { + return { + _history, + FullMsgId(_history->peer->id, ShowAtUnreadMsgId) + }; +} + +QPixmap SublistWidget::grabForShowAnimation(const Window::SectionSlideParams ¶ms) { + _topBar->updateControlsVisibility(); + if (params.withTopBarShadow) _topBarShadow->hide(); + auto result = Ui::GrabWidget(this); + if (params.withTopBarShadow) _topBarShadow->show(); + _translateBar->hide(); + return result; +} + +void SublistWidget::checkActivation() { + _inner->checkActivation(); +} + +void SublistWidget::doSetInnerFocus() { + _inner->setFocus(); +} + +bool SublistWidget::showInternal( + not_null memento, + const Window::SectionShow ¶ms) { + if (auto logMemento = dynamic_cast(memento.get())) { + if (logMemento->getSublist() == sublist()) { + restoreState(logMemento); + return true; + } + } + return false; +} + +void SublistWidget::setInternalState( + const QRect &geometry, + not_null memento) { + setGeometry(geometry); + Ui::SendPendingMoveResizeEvents(this); + restoreState(memento); +} + +std::shared_ptr SublistWidget::createMemento() { + auto result = std::make_shared(sublist()); + saveState(result.get()); + return result; +} + +bool SublistWidget::showMessage( + PeerId peerId, + const Window::SectionShow ¶ms, + MsgId messageId) { + return false; // We want 'Go to original' to work. +} + +void SublistWidget::saveState(not_null memento) { + _inner->saveState(memento->list()); +} + +void SublistWidget::restoreState(not_null memento) { + _inner->restoreState(memento->list()); +} + +void SublistWidget::resizeEvent(QResizeEvent *e) { + if (!width() || !height()) { + return; + } + recountChatWidth(); + updateControlsGeometry(); +} + +void SublistWidget::recountChatWidth() { + auto layout = (width() < st::adaptiveChatWideWidth) + ? Window::Adaptive::ChatLayout::Normal + : Window::Adaptive::ChatLayout::Wide; + controller()->adaptive().setChatLayout(layout); +} + +void SublistWidget::updateControlsGeometry() { + const auto contentWidth = width(); + + const auto newScrollTop = _scroll->isHidden() + ? std::nullopt + : base::make_optional(_scroll->scrollTop() + topDelta()); + _topBar->resizeToWidth(contentWidth); + _topBarShadow->resize(contentWidth, st::lineWidth); + + auto bottom = height(); + if (_openChatButton) { + _openChatButton->resizeToWidth(width()); + bottom -= _openChatButton->height(); + _openChatButton->move(0, bottom); + } + if (_aboutHiddenAuthor) { + _aboutHiddenAuthor->resize(width(), st::historyUnblock.height); + bottom -= _aboutHiddenAuthor->height(); + _aboutHiddenAuthor->move(0, bottom); + } + const auto controlsHeight = 0; + auto top = _topBar->height(); + _translateBar->move(0, top); + _translateBar->resizeToWidth(contentWidth); + top += _translateBarHeight; + const auto scrollHeight = bottom - top - controlsHeight; + const auto scrollSize = QSize(contentWidth, scrollHeight); + if (_scroll->size() != scrollSize) { + _skipScrollEvent = true; + _scroll->resize(scrollSize); + _inner->resizeToWidth(scrollSize.width(), _scroll->height()); + _skipScrollEvent = false; + } + _scroll->move(0, top); + if (!_scroll->isHidden()) { + if (newScrollTop) { + _scroll->scrollToY(*newScrollTop); + } + updateInnerVisibleArea(); + } + + _cornerButtons.updatePositions(); +} + +void SublistWidget::paintEvent(QPaintEvent *e) { + if (animatingShow()) { + SectionWidget::paintEvent(e); + return; + } else if (controller()->contentOverlapped(this, e)) { + return; + } + + const auto aboveHeight = _topBar->height(); + const auto bg = e->rect().intersected( + QRect(0, aboveHeight, width(), height() - aboveHeight)); + SectionWidget::PaintBackground(controller(), _theme.get(), this, bg); +} + +void SublistWidget::onScroll() { + if (_skipScrollEvent) { + return; + } + updateInnerVisibleArea(); +} + +void SublistWidget::updateInnerVisibleArea() { + const auto scrollTop = _scroll->scrollTop(); + _inner->setVisibleTopBottom(scrollTop, scrollTop + _scroll->height()); + _cornerButtons.updateJumpDownVisibility(); + _cornerButtons.updateUnreadThingsVisibility(); +} + +void SublistWidget::showAnimatedHook( + const Window::SectionSlideParams ¶ms) { + _topBar->setAnimatingMode(true); + if (params.withTopBarShadow) { + _topBarShadow->show(); + } +} + +void SublistWidget::showFinishedHook() { + _topBar->setAnimatingMode(false); + _inner->showFinished(); + _translateBar->show(); +} + +bool SublistWidget::floatPlayerHandleWheelEvent(QEvent *e) { + return _scroll->viewportEvent(e); +} + +QRect SublistWidget::floatPlayerAvailableRect() { + return mapToGlobal(_scroll->geometry()); +} + +Context SublistWidget::listContext() { + return Context::SavedSublist; +} + +bool SublistWidget::listScrollTo(int top, bool syntetic) { + top = std::clamp(top, 0, _scroll->scrollTopMax()); + if (_scroll->scrollTop() == top) { + updateInnerVisibleArea(); + return false; + } + _scroll->scrollToY(top); + return true; +} + +void SublistWidget::listCancelRequest() { + if (_inner && !_inner->getSelectedIds().empty()) { + clearSelected(); + return; + } + controller()->showBackFromStack(); +} + +void SublistWidget::listDeleteRequest() { + confirmDeleteSelected(); +} + +void SublistWidget::listTryProcessKeyInput(not_null e) { +} + +rpl::producer SublistWidget::listSource( + Data::MessagePosition aroundId, + int limitBefore, + int limitAfter) { + const auto messageId = aroundId.fullId.msg + ? aroundId.fullId.msg + : (ServerMaxMsgId - 1); + return [=](auto consumer) { + const auto pushSlice = [=] { + auto result = Data::MessagesSlice(); + result.fullCount = _sublist->fullCount(); + _topBar->setCustomTitle(result.fullCount + ? tr::lng_forum_messages( + tr::now, + lt_count_decimal, + *result.fullCount) + : tr::lng_contacts_loading(tr::now)); + const auto &messages = _sublist->messages(); + const auto i = ranges::lower_bound( + messages, + messageId, + ranges::greater(), + [](not_null item) { return item->id; }); + const auto before = int(end(messages) - i); + const auto useBefore = std::min(before, limitBefore); + const auto after = int(i - begin(messages)); + const auto useAfter = std::min(after, limitAfter); + const auto from = i - useAfter; + const auto till = i + useBefore; + auto nearestDistance = std::numeric_limits::max(); + result.ids.reserve(useAfter + useBefore); + for (auto j = till; j != from;) { + const auto item = *--j; + result.ids.push_back(item->fullId()); + const auto distance = std::abs((messageId - item->id).bare); + if (nearestDistance > distance) { + nearestDistance = distance; + result.nearestToAround = result.ids.back(); + } + } + result.skippedAfter = after - useAfter; + result.skippedBefore = result.fullCount + ? (*result.fullCount - after - useBefore) + : std::optional(); + if (!result.fullCount || useBefore < limitBefore) { + _sublist->owner().savedMessages().loadMore(_sublist); + } + consumer.put_next(std::move(result)); + }; + auto lifetime = rpl::lifetime(); + _sublist->changes() | rpl::start_with_next(pushSlice, lifetime); + pushSlice(); + return lifetime; + }; +} + +bool SublistWidget::listAllowsMultiSelect() { + return true; +} + +bool SublistWidget::listIsItemGoodForSelection( + not_null item) { + return item->isRegular() && !item->isService(); +} + +bool SublistWidget::listIsLessInOrder( + not_null first, + not_null second) { + return first->id < second->id; +} + +void SublistWidget::listSelectionChanged(SelectedItems &&items) { + HistoryView::TopBarWidget::SelectedState state; + state.count = items.size(); + for (const auto &item : items) { + if (item.canDelete) { + ++state.canDeleteCount; + } + if (item.canForward) { + ++state.canForwardCount; + } + } + _topBar->showSelected(state); +} + +void SublistWidget::listMarkReadTill(not_null item) { +} + +void SublistWidget::listMarkContentsRead( + const base::flat_set> &items) { +} + +MessagesBarData SublistWidget::listMessagesBar( + const std::vector> &elements) { + return {}; +} + +void SublistWidget::listContentRefreshed() { +} + +void SublistWidget::listUpdateDateLink( + ClickHandlerPtr &link, + not_null view) { +} + +bool SublistWidget::listElementHideReply(not_null view) { + return false; +} + +bool SublistWidget::listElementShownUnread(not_null view) { + return view->data()->unread(view->data()->history()); +} + +bool SublistWidget::listIsGoodForAroundPosition( + not_null view) { + return view->data()->isRegular(); +} + +void SublistWidget::listSendBotCommand( + const QString &command, + const FullMsgId &context) { +} + +void SublistWidget::listHandleViaClick(not_null bot) { +} + +not_null SublistWidget::listChatTheme() { + return _theme.get(); +} + +CopyRestrictionType SublistWidget::listCopyRestrictionType( + HistoryItem *item) { + return CopyRestrictionTypeFor(_history->peer, item); +} + +CopyRestrictionType SublistWidget::listCopyMediaRestrictionType( + not_null item) { + return CopyMediaRestrictionTypeFor(_history->peer, item); +} + +CopyRestrictionType SublistWidget::listSelectRestrictionType() { + return SelectRestrictionTypeFor(_history->peer); +} + +auto SublistWidget::listAllowedReactionsValue() +-> rpl::producer { + return Data::PeerAllowedReactionsValue(_history->peer); +} + +void SublistWidget::listShowPremiumToast(not_null document) { +} + +void SublistWidget::listOpenPhoto( + not_null photo, + FullMsgId context) { + controller()->openPhoto(photo, { context }); +} + +void SublistWidget::listOpenDocument( + not_null document, + FullMsgId context, + bool showInMediaView) { + controller()->openDocument(document, showInMediaView, { context }); +} + +void SublistWidget::listPaintEmpty( + Painter &p, + const Ui::ChatPaintContext &context) { +} + +QString SublistWidget::listElementAuthorRank(not_null view) { + return {}; +} + +History *SublistWidget::listTranslateHistory() { + return _history; +} + +void SublistWidget::listAddTranslatedItems( + not_null tracker) { +} + +void SublistWidget::confirmDeleteSelected() { + ConfirmDeleteSelectedItems(_inner); +} + +void SublistWidget::confirmForwardSelected() { + ConfirmForwardSelectedItems(_inner); +} + +void SublistWidget::clearSelected() { + _inner->cancelSelection(); +} + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_sublist_section.h b/Telegram/SourceFiles/history/view/history_view_sublist_section.h new file mode 100644 index 0000000000000..008f521c5f079 --- /dev/null +++ b/Telegram/SourceFiles/history/view/history_view_sublist_section.h @@ -0,0 +1,217 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "window/section_widget.h" +#include "window/section_memento.h" +#include "history/view/history_view_list_widget.h" +#include "history/view/history_view_corner_buttons.h" +#include "data/data_messages.h" +#include "base/weak_ptr.h" +#include "base/timer.h" + +class History; + +namespace Ui { +class ScrollArea; +class PlainShadow; +class FlatButton; +} // namespace Ui + +namespace Profile { +class BackButton; +} // namespace Profile + +namespace HistoryView { + +class Element; +class TopBarWidget; +class SublistMemento; +class TranslateBar; + +class SublistWidget final + : public Window::SectionWidget + , private ListDelegate + , private CornerButtonsDelegate { +public: + SublistWidget( + QWidget *parent, + not_null controller, + not_null sublist); + ~SublistWidget(); + + [[nodiscard]] not_null sublist() const; + Dialogs::RowDescriptor activeChat() const override; + + bool hasTopBarShadow() const override { + return true; + } + + QPixmap grabForShowAnimation( + const Window::SectionSlideParams ¶ms) override; + + bool showInternal( + not_null memento, + const Window::SectionShow ¶ms) override; + std::shared_ptr createMemento() override; + bool showMessage( + PeerId peerId, + const Window::SectionShow ¶ms, + MsgId messageId) override; + + void setInternalState( + const QRect &geometry, + not_null memento); + + Window::SectionActionResult sendBotCommand( + Bot::SendCommandRequest request) override { + return Window::SectionActionResult::Fallback; + } + + // Float player interface. + bool floatPlayerHandleWheelEvent(QEvent *e) override; + QRect floatPlayerAvailableRect() override; + + // ListDelegate interface. + Context listContext() override; + bool listScrollTo(int top, bool syntetic = true) override; + void listCancelRequest() override; + void listDeleteRequest() override; + void listTryProcessKeyInput(not_null e) override; + rpl::producer listSource( + Data::MessagePosition aroundId, + int limitBefore, + int limitAfter) override; + bool listAllowsMultiSelect() override; + bool listIsItemGoodForSelection(not_null item) override; + bool listIsLessInOrder( + not_null first, + not_null second) override; + void listSelectionChanged(SelectedItems &&items) override; + void listMarkReadTill(not_null item) override; + void listMarkContentsRead( + const base::flat_set> &items) override; + MessagesBarData listMessagesBar( + const std::vector> &elements) override; + void listContentRefreshed() override; + void listUpdateDateLink( + ClickHandlerPtr &link, + not_null view) override; + bool listElementHideReply(not_null view) override; + bool listElementShownUnread(not_null view) override; + bool listIsGoodForAroundPosition(not_null view) override; + void listSendBotCommand( + const QString &command, + const FullMsgId &context) override; + void listHandleViaClick(not_null bot) override; + not_null listChatTheme() override; + CopyRestrictionType listCopyRestrictionType(HistoryItem *item) override; + CopyRestrictionType listCopyMediaRestrictionType( + not_null item) override; + CopyRestrictionType listSelectRestrictionType() override; + auto listAllowedReactionsValue() + -> rpl::producer override; + void listShowPremiumToast(not_null document) override; + void listOpenPhoto( + not_null photo, + FullMsgId context) override; + void listOpenDocument( + not_null document, + FullMsgId context, + bool showInMediaView) override; + void listPaintEmpty( + Painter &p, + const Ui::ChatPaintContext &context) override; + QString listElementAuthorRank(not_null view) override; + History *listTranslateHistory() override; + void listAddTranslatedItems( + not_null tracker) override; + + // CornerButtonsDelegate delegate. + void cornerButtonsShowAtPosition( + Data::MessagePosition position) override; + Data::Thread *cornerButtonsThread() override; + FullMsgId cornerButtonsCurrentId() override; + bool cornerButtonsIgnoreVisibility() override; + std::optional cornerButtonsDownShown() override; + bool cornerButtonsUnreadMayBeShown() override; + bool cornerButtonsHas(CornerButtonType type) override; + +private: + void resizeEvent(QResizeEvent *e) override; + void paintEvent(QPaintEvent *e) override; + + void showAnimatedHook( + const Window::SectionSlideParams ¶ms) override; + void showFinishedHook() override; + void doSetInnerFocus() override; + void checkActivation() override; + + void onScroll(); + void updateInnerVisibleArea(); + void updateControlsGeometry(); + void updateAdaptiveLayout(); + void saveState(not_null memento); + void restoreState(not_null memento); + void showAtPosition( + Data::MessagePosition position, + FullMsgId originId = {}); + + void setupOpenChatButton(); + void setupAboutHiddenAuthor(); + void setupTranslateBar(); + + void confirmDeleteSelected(); + void confirmForwardSelected(); + void clearSelected(); + void recountChatWidth(); + + const not_null _sublist; + const not_null _history; + std::shared_ptr _theme; + QPointer _inner; + object_ptr _topBar; + object_ptr _topBarShadow; + + std::unique_ptr _translateBar; + int _translateBarHeight = 0; + + bool _skipScrollEvent = false; + std::unique_ptr _scroll; + std::unique_ptr _openChatButton; + std::unique_ptr _aboutHiddenAuthor; + + CornerButtons _cornerButtons; + +}; + +class SublistMemento : public Window::SectionMemento { +public: + explicit SublistMemento(not_null sublist); + + object_ptr createWidget( + QWidget *parent, + not_null controller, + Window::Column column, + const QRect &geometry) override; + + [[nodiscard]] not_null getSublist() const { + return _sublist; + } + + [[nodiscard]] not_null list() { + return &_list; + } + +private: + const not_null _sublist; + ListMemento _list; + +}; + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp index c026ab9c76724..67a3b6c57a302 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp @@ -44,6 +44,7 @@ For license and copyright information please follow this link: #include "data/data_peer_values.h" #include "data/data_group_call.h" // GroupCall::input. #include "data/data_folder.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_stories.h" #include "data/data_channel.h" @@ -467,9 +468,17 @@ void TopBarWidget::paintTopBar(Painter &p) { } const auto now = crl::now(); - const auto history = _activeChat.key.owningHistory(); + const auto peer = _activeChat.key.owningHistory() + ? _activeChat.key.owningHistory()->peer.get() + : nullptr; const auto folder = _activeChat.key.folder(); + const auto sublist = _activeChat.key.sublist(); const auto topic = _activeChat.key.topic(); + const auto history = _activeChat.key.history(); + const auto namePeer = history + ? history->peer.get() + : sublist ? sublist->peer().get() + : nullptr; if (topic && _activeChat.section == Section::Replies) { p.setPen(st::dialogsNameFg); topic->chatListNameText().drawElided( @@ -492,22 +501,22 @@ void TopBarWidget::paintTopBar(Painter &p) { p.drawTextLeft(nameleft, statustop, width(), _customTitleText); } } else if (folder - || history->peer->sharedMediaInfo() + || (peer && peer->sharedMediaInfo()) || (_activeChat.section == Section::Scheduled) || (_activeChat.section == Section::Pinned)) { auto text = (_activeChat.section == Section::Scheduled) - ? ((history && history->peer->isSelf()) + ? ((peer && peer->isSelf()) ? tr::lng_reminder_messages(tr::now) : tr::lng_scheduled_messages(tr::now)) : (_activeChat.section == Section::Pinned) ? _customTitleText : folder ? folder->chatListName() - : history->peer->isSelf() + : peer->isSelf() ? tr::lng_saved_messages(tr::now) - : history->peer->isRepliesChat() + : peer->isRepliesChat() ? tr::lng_replies_messages(tr::now) - : history->peer->name(); + : peer->name(); const auto textWidth = st::historySavedFont->width(text); if (availableWidth < textWidth) { text = st::historySavedFont->elided(text, availableWidth); @@ -538,16 +547,14 @@ void TopBarWidget::paintTopBar(Painter &p) { width(), st::historyStatusFgTyping, now)) { - p.setPen(st::historyStatusFg); - p.drawTextLeft(nameleft, statustop, width(), _customTitleText); + paintStatus(p, nameleft, statustop, availableWidth, width()); } - } else if (const auto history = _activeChat.key.history()) { - const auto peer = history->peer; - if (_titleNameVersion < peer->nameVersion()) { - _titleNameVersion = peer->nameVersion(); + } else if (namePeer) { + if (_titleNameVersion < namePeer->nameVersion()) { + _titleNameVersion = namePeer->nameVersion(); _title.setText( st::msgNameStyle, - peer->topBarNameText(), + namePeer->topBarNameText(), Ui::NameTextOptions()); } const auto badgeWidth = _titleBadge.drawGetWidth( @@ -560,7 +567,7 @@ void TopBarWidget::paintTopBar(Painter &p) { _title.maxWidth(), width(), { - .peer = peer, + .peer = namePeer, .verified = &st::dialogsVerifiedIcon, .premium = &st::dialogsPremiumIcon.icon, .scam = &st::attentionButtonFg, @@ -602,6 +609,9 @@ bool TopBarWidget::paintSendAction( int outerWidth, style::color fg, crl::time now) { + if (!_sendAction) { + return false; + } const auto seen = _emojiInteractionSeen.get(); if (!seen || seen->till <= now) { return _sendAction->paint(p, x, y, availableWidth, outerWidth, fg, now); @@ -652,10 +662,22 @@ void TopBarWidget::paintStatus( int top, int availableWidth, int outerWidth) { - p.setPen(_titlePeerTextOnline - ? st::historyStatusFgActive - : st::historyStatusFg); - _titlePeerText.drawLeftElided(p, left, top, availableWidth, outerWidth); + using Section = Dialogs::EntryState::Section; + const auto section = _activeChat.section; + if (section == Section::Replies || section == Section::SavedSublist) { + p.setPen(st::historyStatusFg); + p.drawTextLeft(left, top, outerWidth, _customTitleText); + } else { + p.setPen(_titlePeerTextOnline + ? st::historyStatusFgActive + : st::historyStatusFg); + _titlePeerText.drawLeftElided( + p, + left, + top, + availableWidth, + outerWidth); + } } QRect TopBarWidget::getMembersShowAreaGeometry() const { @@ -689,11 +711,15 @@ void TopBarWidget::infoClicked() { return; } else if (const auto topic = key.topic()) { _controller->showSection(std::make_shared(topic)); - } else if (key.peer()->isSelf()) { + } else if (const auto sublist = key.sublist()) { _controller->showSection(std::make_shared( - key.peer(), + _controller->session().user(), Info::Section(Storage::SharedMediaType::Photo))); - } else if (key.peer()->isRepliesChat()) { + } else if (key.peer()->savedSublistsInfo()) { + _controller->showSection(std::make_shared( + key.peer(), + Info::Section::Type::SavedSublists)); + } else if (key.peer()->sharedMediaInfo()) { _controller->showSection(std::make_shared( key.peer(), Info::Section(Storage::SharedMediaType::Photo))); diff --git a/Telegram/SourceFiles/history/view/history_view_view_button.cpp b/Telegram/SourceFiles/history/view/history_view_view_button.cpp index 0780e4ddf4797..52e9847ae959d 100644 --- a/Telegram/SourceFiles/history/view/history_view_view_button.cpp +++ b/Telegram/SourceFiles/history/view/history_view_view_button.cpp @@ -23,14 +23,21 @@ namespace { [[nodiscard]] ClickHandlerPtr MakeMediaButtonClickHandler( not_null media) { - const auto giveaway = media->giveaway(); - Assert(giveaway != nullptr); + const auto start = media->giveawayStart(); + const auto results = media->giveawayResults(); + Assert(start || results); + const auto peer = media->parent()->history()->peer; const auto messageId = media->parent()->id; if (media->parent()->isSending() || media->parent()->hasFailed()) { return nullptr; } - const auto info = *giveaway; + const auto maybeStart = start + ? *start + : std::optional(); + const auto maybeResults = results + ? *results + : std::optional(); return std::make_shared([=]( ClickContext context) { const auto my = context.other.value(); @@ -38,13 +45,18 @@ namespace { if (!controller) { return; } - ResolveGiveawayInfo(controller, peer, messageId, info); + ResolveGiveawayInfo( + controller, + peer, + messageId, + maybeStart, + maybeResults); }); } [[nodiscard]] QString MakeMediaButtonText(not_null media) { - const auto giveaway = media->giveaway(); - Assert(giveaway != nullptr); + Expects(media->giveawayStart() || media->giveawayResults()); + return Ui::Text::Upper(tr::lng_prizes_how_works(tr::now)); } @@ -72,7 +84,7 @@ struct ViewButton::Inner { }; bool ViewButton::MediaHasViewButton(not_null media) { - return (media->giveaway() != nullptr); + return media->giveawayStart() || media->giveawayResults(); } ViewButton::Inner::Inner( diff --git a/Telegram/SourceFiles/history/view/media/history_view_document.cpp b/Telegram/SourceFiles/history/view/media/history_view_document.cpp index 04df9f2aab003..07e82409ea8e8 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_document.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_document.cpp @@ -9,12 +9,14 @@ For license and copyright information please follow this link: #include "base/random.h" #include "lang/lang_keys.h" +#include "lottie/lottie_icon.h" #include "storage/localstorage.h" #include "main/main_session.h" #include "media/player/media_player_float.h" // Media::Player::RoundPainter. #include "media/audio/media_audio.h" #include "media/player/media_player_instance.h" #include "history/history_item_components.h" +#include "history/history_item_helpers.h" // ClearMediaAsExpired. #include "history/history.h" #include "core/click_handler_types.h" // kDocumentFilenameTooltipProperty. #include "history/view/history_view_element.h" @@ -30,6 +32,7 @@ For license and copyright information please follow this link: #include "ui/cached_round_corners.h" #include "ui/painter.h" #include "ui/power_saving.h" +#include "ui/rect.h" #include "ui/ui_utility.h" #include "data/data_session.h" #include "data/data_document.h" @@ -41,12 +44,102 @@ For license and copyright information please follow this link: #include "api/api_transcribes.h" #include "apiwrap.h" #include "styles/style_chat.h" +#include "styles/style_dialogs.h" namespace HistoryView { namespace { constexpr auto kAudioVoiceMsgUpdateView = crl::time(100); +void DrawCornerBadgeTTL( + QPainter &p, + const style::color &color, + const QRect &circleRect) { + p.save(); + const auto ratio = style::DevicePixelRatio(); + const auto partRect = QRect( + circleRect.left() + circleRect.width() - st::dialogsTTLBadgeSize * 0.85, + circleRect.top() + circleRect.height() - st::dialogsTTLBadgeSize * 0.85, + st::dialogsTTLBadgeSize, + st::dialogsTTLBadgeSize); + + auto hq = PainterHighQualityEnabler(p); + p.setBrush(color); + p.drawEllipse(partRect); + + const auto innerRect = partRect - st::dialogsTTLBadgeInnerMargins; + const auto ttlText = u"1"_q; + + p.setFont(st::dialogsScamFont); + p.setPen(st::premiumButtonFg); + p.drawText(innerRect, ttlText, style::al_center); + + constexpr auto kPenWidth = 1.5; + + const auto penWidth = style::ConvertScaleExact(kPenWidth); + auto pen = QPen(st::premiumButtonFg); + pen.setJoinStyle(Qt::RoundJoin); + pen.setCapStyle(Qt::RoundCap); + pen.setWidthF(penWidth); + + p.setPen(pen); + p.setBrush(Qt::NoBrush); + p.drawArc(innerRect, arc::kQuarterLength, arc::kHalfLength); + + p.setClipRect(innerRect + - QMargins(innerRect.width() / 2, 0, -penWidth, -penWidth)); + pen.setStyle(Qt::DotLine); + p.setPen(pen); + p.drawEllipse(innerRect); + p.restore(); +} + +[[nodiscard]] HistoryView::TtlPaintCallback CreateTtlPaintCallback( + std::shared_ptr lifetime, + Fn update) { + struct State final { + std::unique_ptr start; + std::unique_ptr idle; + }; + const auto iconSize = Size(std::min( + st::historyFileInPause.width(), + st::historyFileInPause.height())); + const auto state = lifetime->make_state(); + state->start = Lottie::MakeIcon({ + .name = u"voice_ttl_start"_q, + .color = &st::historyFileInIconFg, + .sizeOverride = iconSize, + }); + + const auto animateSingle = [=]( + not_null icon, + Fn next) { + auto callback = [=] { + update(); + if (icon->frameIndex() == icon->framesCount()) { + next(); + } + }; + icon->animate(std::move(callback), 0, icon->framesCount()); + }; + const auto animate = [=](auto reanimate) -> void { + animateSingle(state->idle.get(), [=] { reanimate(reanimate); }); + }; + animateSingle( + state->start.get(), + [=] { + state->idle = Lottie::MakeIcon({ + .name = u"voice_ttl_idle"_q, + .color = &st::historyFileInIconFg, + .sizeOverride = iconSize, + }); + animate(animate); + }); + return [=](QPainter &p, QRect r, QColor c) { + (state->idle ? state->idle : state->start)->paintInCenter(p, r, c); + }; +} + [[nodiscard]] QString CleanTagSymbols(const QString &value) { auto result = QString(); const auto begin = value.begin(), end = value.end(); @@ -195,7 +288,8 @@ Document::Document( not_null document) : File(parent, realParent) , _data(document) { - if (_data->isVideoMessage()) { + const auto isRound = _data->isVideoMessage(); + if (isRound) { const auto &entry = _data->session().api().transcribes().entry( realParent); _transcribedRound = entry.shown; @@ -209,7 +303,44 @@ Document::Document( _tooltipFilename.setTooltipText(named->name); } - setDocumentLinks(_data, realParent); + if ((_data->isVoiceMessage() || isRound) + && IsVoiceOncePlayable(_parent->data())) { + _parent->data()->removeFromSharedMediaIndex(); + setDocumentLinks(_data, realParent, [=] { + _openl = nullptr; + + auto lifetime = std::make_shared(); + rpl::merge( + ::Media::Player::instance()->updatedNotifier( + ) | rpl::filter([=](::Media::Player::TrackState state) { + using State = ::Media::Player::State; + const auto badState = state.state == State::Stopped + || state.state == State::StoppedAtEnd + || state.state == State::StoppedAtError + || state.state == State::StoppedAtStart; + return (state.id.contextId() != _realParent->fullId()) + && !badState; + }) | rpl::to_empty, + ::Media::Player::instance()->tracksFinished( + ) | rpl::filter([=](AudioMsgId::Type type) { + return (type == AudioMsgId::Type::Voice); + }) | rpl::to_empty + ) | rpl::start_with_next([=]() mutable { + _drawTtl = nullptr; + const auto item = _parent->data(); + if (lifetime) { + base::take(lifetime)->destroy(); + } + // Destroys this. + ClearMediaAsExpired(item); + }, *lifetime); + _drawTtl = CreateTtlPaintCallback(lifetime, [=] { repaint(); }); + + return false; + }); + } else { + setDocumentLinks(_data, realParent); + } setStatusSize(Ui::FileStatusSizeReady); @@ -273,9 +404,9 @@ void Document::createComponents(bool caption) { _realParent->fullId()); } if (const auto voice = Get()) { - voice->seekl = std::make_shared( - _data, - [](FullMsgId) {}); + voice->seekl = !IsVoiceOncePlayable(_parent->data()) + ? std::make_shared(_data, [](FullMsgId) {}) + : nullptr; if (_transcribedRound) { voice->round = std::make_unique<::Media::Player::RoundPainter>( _realParent); @@ -306,7 +437,9 @@ QSize Document::countOptimalSize() { const auto voice = Get(); if (voice) { const auto session = &_realParent->history()->session(); - if (!session->premium() && !session->api().transcribes().trialsSupport()) { + if (_parent->data()->media()->ttlSeconds() + || (!session->premium() + && !session->api().transcribes().trialsSupport())) { voice->transcribe = nullptr; voice->transcribeText = {}; } else { @@ -586,6 +719,10 @@ void Document::draw( PainterHighQualityEnabler hq(p); p.setBrush(stm->msgFileBg); p.drawEllipse(inner); + + if (_parent->data()->media()->ttlSeconds()) { + DrawCornerBadgeTTL(p, stm->msgFileBg, inner); + } } } @@ -622,7 +759,9 @@ void Document::draw( : nullptr; const auto paintContent = [&](QPainter &q) { - if (previous && radialOpacity > 0. && radialOpacity < 1.) { + if (_drawTtl) { + _drawTtl(q, inner, context.st->historyFileInIconFg()->c); + } else if (previous && radialOpacity > 0. && radialOpacity < 1.) { PaintInterpolatedIcon(q, icon, *previous, radialOpacity, inner); } else { icon.paintInCenter(q, inner); diff --git a/Telegram/SourceFiles/history/view/media/history_view_document.h b/Telegram/SourceFiles/history/view/media/history_view_document.h index 126a7c777883c..5e57308ed5729 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_document.h +++ b/Telegram/SourceFiles/history/view/media/history_view_document.h @@ -25,6 +25,8 @@ class String; namespace HistoryView { +using TtlPaintCallback = Fn; + class Document final : public File , public RuntimeComposer { @@ -178,6 +180,8 @@ class Document final mutable TooltipFilename _tooltipFilename; + TtlPaintCallback _drawTtl; + bool _transcribedRound = false; }; diff --git a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp index 134d09fb50c34..e06ecb909018d 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp @@ -23,6 +23,7 @@ For license and copyright information please follow this link: #include "ui/boxes/confirm_box.h" #include "ui/painter.h" #include "history/history_item_components.h" +#include "history/history_item_helpers.h" #include "history/history_item.h" #include "history/history.h" #include "history/view/history_view_element.h" @@ -942,13 +943,13 @@ void Gif::drawCornerStatus( const auto statusX = position.x() + st::msgDateImgDelta + padding.x(); const auto statusY = position.y() + st::msgDateImgDelta + padding.y(); const auto around = style::rtlrect(statusX - padding.x(), statusY - padding.y(), statusW, statusH, width()); - const auto statusTextTop = statusY + (cornerDownload ? (((statusH - 2 * st::normalFont->height) / 3) - padding.y()) : 0); + const auto statusTextTop = statusY + (cornerDownload ? (((statusH - 2 * st::normalFont->height) / 3) - padding.y()) : 0); Ui::FillRoundRect(p, around, sti->msgDateImgBg, sti->msgDateImgBgCorners); p.setFont(st::normalFont); p.setPen(st->msgDateImgFg()); p.drawTextLeft(statusX + addLeft, statusTextTop, width(), text, statusW - 2 * padding.x()); if (cornerDownload) { - const auto downloadTextTop = statusY + st::normalFont->height + (2 * (statusH - 2 * st::normalFont->height) / 3) - padding.y(); + const auto downloadTextTop = statusY + st::normalFont->height + (2 * (statusH - 2 * st::normalFont->height) / 3) - padding.y(); p.drawTextLeft(statusX + addLeft, downloadTextTop, width(), _downloadSize, statusW - 2 * padding.x()); const auto inner = QRect(statusX + padding.y() - padding.x(), statusY, st::historyVideoDownloadSize, st::historyVideoDownloadSize); const auto &icon = _data->loading() @@ -1974,6 +1975,7 @@ bool Gif::needCornerStatusDisplay() const { void Gif::ensureTranscribeButton() const { if (_data->isVideoMessage() + && !IsVoiceOncePlayable(_parent->data()) && (_data->session().premium() || _data->session().api().transcribes().trialsSupport())) { if (!_transcribe) { diff --git a/Telegram/SourceFiles/history/view/media/history_view_giveaway.cpp b/Telegram/SourceFiles/history/view/media/history_view_giveaway.cpp index 81dbf0684f99b..6205011a48db1 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_giveaway.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_giveaway.cpp @@ -10,6 +10,7 @@ For license and copyright information please follow this link: #include "base/unixtime.h" #include "boxes/gift_premium_box.h" #include "chat_helpers/stickers_gift_box_pack.h" +#include "chat_helpers/stickers_dice_pack.h" #include "countries/countries_instance.h" #include "data/data_channel.h" #include "data/data_document.h" @@ -19,6 +20,7 @@ For license and copyright information please follow this link: #include "history/history.h" #include "history/history_item.h" #include "history/history_item_components.h" +#include "history/history_item_helpers.h" #include "history/view/history_view_element.h" #include "history/view/history_view_cursor_state.h" #include "lang/lang_keys.h" @@ -35,6 +37,8 @@ For license and copyright information please follow this link: namespace HistoryView { namespace { +constexpr auto kAdditionalPrizesWithLineOpacity = 0.6; + [[nodiscard]] QSize CountOptimalTextSize( const Ui::Text::String &text, int minWidth, @@ -50,217 +54,264 @@ namespace { } // namespace -Giveaway::Giveaway( +TextState MediaInBubble::Part::textState( + QPoint point, + StateRequest request, + int outerWidth) const { + return {}; +} + +void MediaInBubble::Part::clickHandlerPressedChanged( + const ClickHandlerPtr &p, + bool pressed) { +} + +bool MediaInBubble::Part::hasHeavyPart() { + return false; +} + +void MediaInBubble::Part::unloadHeavyPart() { +} + +MediaInBubble::MediaInBubble( not_null parent, - not_null giveaway) -: Media(parent) -, _prizesTitle(st::msgMinWidth) -, _prizes(st::msgMinWidth) -, _participantsTitle(st::msgMinWidth) -, _participants(st::msgMinWidth) -, _countries(st::msgMinWidth) -, _winnersTitle(st::msgMinWidth) -, _winners(st::msgMinWidth) { - fillFromData(giveaway); -} - -Giveaway::~Giveaway() { + Fn)>)> generate) +: Media(parent) { + generate([&](std::unique_ptr part) { + _entries.push_back({ + .object = std::move(part), + }); + }); +} + +MediaInBubble::~MediaInBubble() { if (hasHeavyPart()) { unloadHeavyPart(); _parent->checkHeavyPart(); } } -void Giveaway::fillFromData(not_null giveaway) { - _months = giveaway->months; - _quantity = giveaway->quantity; - - _prizesTitle.setText( - st::semiboldTextStyle, - tr::lng_prizes_title(tr::now, lt_count, _quantity), - kDefaultTextOptions); - - _prizes.setMarkedText( - st::defaultTextStyle, - tr::lng_prizes_about( - tr::now, - lt_count, - _quantity, - lt_duration, - Ui::Text::Bold(GiftDuration(_months)), - Ui::Text::RichLangValue), - kDefaultTextOptions); - _participantsTitle.setText( - st::semiboldTextStyle, - tr::lng_prizes_participants(tr::now), - kDefaultTextOptions); - - for (const auto &channel : giveaway->channels) { - _channels.push_back({ - .name = Ui::Text::String( - st::semiboldTextStyle, - channel->name(), - kDefaultTextOptions, - st::msgMinWidth), - .thumbnail = Dialogs::Stories::MakeUserpicThumbnail(channel), - .link = channel->openLink(), - .colorIndex = channel->colorIndex(), - }); +QSize MediaInBubble::countOptimalSize() { + const auto maxWidth = st::chatGiveawayWidth; + + auto top = 0; + for (auto &entry : _entries) { + const auto raw = entry.object.get(); + raw->initDimensions(); + top += raw->resizeGetHeight(maxWidth); } - const auto channels = int(_channels.size()); - - const auto &instance = Countries::Instance(); ; - auto countries = QStringList(); - for (const auto &country : giveaway->countries) { - const auto name = instance.countryNameByISO2(country); - const auto flag = instance.flagEmojiByISO2(country); - countries.push_back(flag + QChar(0xA0) + name); + return { maxWidth, top }; +} + +QSize MediaInBubble::countCurrentSize(int newWidth) { + return { maxWidth(), minHeight()}; +} + +void MediaInBubble::draw(Painter &p, const PaintContext &context) const { + const auto outer = width(); + if (outer < st::msgPadding.left() + st::msgPadding.right() + 1) { + return; } - if (const auto count = countries.size()) { - auto united = countries.front(); - for (auto i = 1; i != count; ++i) { - united = ((i + 1 == count) - ? tr::lng_prizes_countries_and_last - : tr::lng_prizes_countries_and_one)( - tr::now, - lt_countries, - united, - lt_country, - countries[i]); - } - _countries.setText( - st::defaultTextStyle, - tr::lng_prizes_countries(tr::now, lt_countries, united), - kDefaultTextOptions); - } else { - _countries.clear(); + auto translated = 0; + for (const auto &entry : _entries) { + const auto raw = entry.object.get(); + const auto height = raw->height(); + raw->draw(p, context, outer); + translated += height; + p.translate(0, height); } + p.translate(0, -translated); +} - _participants.setText( - st::defaultTextStyle, - (giveaway->all - ? tr::lng_prizes_participants_all - : tr::lng_prizes_participants_new)(tr::now, lt_count, channels), - kDefaultTextOptions); - _winnersTitle.setText( - st::semiboldTextStyle, - tr::lng_prizes_date(tr::now), - kDefaultTextOptions); - _winners.setText( - st::defaultTextStyle, - langDateTime(base::unixtime::parse(giveaway->untilDate)), - kDefaultTextOptions); +TextState MediaInBubble::textState( + QPoint point, + StateRequest request) const { + auto result = TextState(_parent); - ensureStickerCreated(); + const auto outer = width(); + if (outer < st::msgPadding.left() + st::msgPadding.right() + 1) { + return result; + } + + for (const auto &entry : _entries) { + const auto raw = entry.object.get(); + const auto height = raw->height(); + if (point.y() >= 0 && point.y() < height) { + const auto part = raw->textState(point, request, outer); + result.link = part.link; + return result; + } + point.setY(point.y() - height); + } + return result; } -QSize Giveaway::countOptimalSize() { - const auto maxWidth = st::chatGiveawayWidth; - const auto padding = inBubblePadding(); - const auto available = maxWidth - padding.left() - padding.right(); - - _stickerTop = st::chatGiveawayStickerTop; - _prizesTitleTop = _stickerTop - + st::msgServiceGiftBoxStickerSize.height() - + st::chatGiveawayPrizesTop; - _prizesTop = _prizesTitleTop - + _prizesTitle.countHeight(available) - + st::chatGiveawayPrizesSkip; - const auto prizesSize = CountOptimalTextSize( - _prizes, - st::msgMinWidth, - available); - _prizesWidth = prizesSize.width(); - _participantsTitleTop = _prizesTop - + prizesSize.height() - + st::chatGiveawayParticipantsTop; - _participantsTop = _participantsTitleTop - + _participantsTitle.countHeight(available) - + st::chatGiveawayParticipantsSkip; - const auto participantsSize = CountOptimalTextSize( - _participants, - st::msgMinWidth, - available); - _participantsWidth = participantsSize.width(); - const auto channelsTop = _participantsTop - + participantsSize.height() - + st::chatGiveawayChannelTop; - const auto channelsBottom = layoutChannels( - padding.left(), - channelsTop, - available); - _countriesTop = channelsBottom; - if (_countries.isEmpty()) { - _winnersTitleTop = _countriesTop + st::chatGiveawayDateTop; - } else { - const auto countriesSize = CountOptimalTextSize( - _countries, - st::msgMinWidth, - available); - _countriesWidth = countriesSize.width(); - _winnersTitleTop = _countriesTop - + _countries.countHeight(available) - + st::chatGiveawayCountriesSkip; +void MediaInBubble::clickHandlerActiveChanged( + const ClickHandlerPtr &p, + bool active) { +} + +void MediaInBubble::clickHandlerPressedChanged( + const ClickHandlerPtr &p, + bool pressed) { + for (const auto &entry : _entries) { + entry.object->clickHandlerPressedChanged(p, pressed); } - _winnersTop = _winnersTitleTop - + _winnersTitle.countHeight(available) - + st::chatGiveawayDateSkip; - const auto height = _winnersTop - + _winners.countHeight(available) - + st::chatGiveawayBottomSkip; - return { maxWidth, height }; -} - -int Giveaway::layoutChannels(int x, int y, int available) { - const auto size = st::chatGiveawayChannelSize; - const auto skip = st::chatGiveawayChannelSkip; - const auto padding = st::chatGiveawayChannelPadding; - auto left = available; - const auto shiftRow = [&](int i, int top, int shift) { - for (auto j = i; j != 0; --j) { - auto &geometry = _channels[j - 1].geometry; - if (geometry.top() != top) { - break; - } - geometry.moveLeft(geometry.x() + shift); - } - }; - const auto count = int(_channels.size()); - for (auto i = 0; i != count; ++i) { - const auto desired = size - + padding.left() - + _channels[i].name.maxWidth() - + padding.right(); - const auto width = std::min(desired, available); - if (left < width) { - shiftRow(i, y, (left + skip) / 2); - left = available; - y += size + skip; +} + +bool MediaInBubble::hideFromName() const { + return !parent()->data()->Has(); +} + +bool MediaInBubble::hasHeavyPart() const { + for (const auto &entry : _entries) { + if (entry.object->hasHeavyPart()) { + return true; } - _channels[i].geometry = { x + available - left, y, width, size }; - left -= width + skip; } - shiftRow(count, y, (left + skip) / 2); - return y + size + skip; + return false; } -QSize Giveaway::countCurrentSize(int newWidth) { - return { maxWidth(), minHeight()}; +void MediaInBubble::unloadHeavyPart() { + for (const auto &entry : _entries) { + entry.object->unloadHeavyPart(); + } +} + +QMargins MediaInBubble::inBubblePadding() const { + auto lshift = st::msgPadding.left(); + auto rshift = st::msgPadding.right(); + auto bshift = isBubbleBottom() ? st::msgPadding.top() : st::mediaInBubbleSkip; + auto tshift = isBubbleTop() ? st::msgPadding.bottom() : st::mediaInBubbleSkip; + return QMargins(lshift, tshift, rshift, bshift); +} + +TextMediaInBubblePart::TextMediaInBubblePart( + TextWithEntities text, + QMargins margins, + const base::flat_map &links) +: _text(st::msgMinWidth) +, _margins(margins) { + _text.setMarkedText(st::defaultTextStyle, text); + for (const auto &[index, link] : links) { + _text.setLink(index, link); + } +} + +void TextMediaInBubblePart::draw( + Painter &p, + const PaintContext &context, + int outerWidth) const { + p.setPen(context.messageStyle()->historyTextFg); + _text.draw(p, { + .position = { (outerWidth - width()) / 2, _margins.top() }, + .outerWidth = outerWidth, + .availableWidth = width(), + .align = style::al_top, + .palette = &context.messageStyle()->textPalette, + .now = context.now, + }); +} + +TextState TextMediaInBubblePart::textState( + QPoint point, + StateRequest request, + int outerWidth) const { + point -= QPoint{ (outerWidth - width()) / 2, _margins.top() }; + auto result = TextState(); + auto forText = request.forText(); + forText.align = style::al_top; + result.link = _text.getState(point, width(), forText).link; + return result; +} + +QSize TextMediaInBubblePart::countOptimalSize() { + return { + _margins.left() + _text.maxWidth() + _margins.right(), + _margins.top() + _text.minHeight() + _margins.bottom(), + }; } -void Giveaway::draw(Painter &p, const PaintContext &context) const { - if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) return; +QSize TextMediaInBubblePart::countCurrentSize(int newWidth) { + auto skip = _margins.left() + _margins.right(); + const auto size = CountOptimalTextSize( + _text, + st::msgMinWidth, + newWidth - skip); + return { + size.width() + skip, + _margins.top() + size.height() + _margins.bottom(), + }; +} +TextDelimeterPart::TextDelimeterPart( + const QString &text, + QMargins margins) +: _margins(margins) { + _text.setText(st::defaultTextStyle, text); +} + +void TextDelimeterPart::draw( + Painter &p, + const PaintContext &context, + int outerWidth) const { const auto stm = context.messageStyle(); + const auto available = outerWidth - _margins.left() - _margins.right(); + p.setPen(stm->msgDateFg); + _text.draw(p, { + .position = { _margins.left(), _margins.top() }, + .outerWidth = outerWidth, + .availableWidth = available, + .align = style::al_top, + .palette = &stm->textPalette, + .now = context.now, + .elisionLines = 1, + }); + const auto skip = st::chatGiveawayPrizesWithSkip; + const auto inner = available - 2 * skip; + const auto sub = _text.maxWidth(); + if (inner > sub + 1) { + const auto fill = (inner - sub) / 2; + const auto stroke = st::lineWidth; + const auto top = _margins.top() + + st::chatGiveawayPrizesWithLineTop; + p.setOpacity(kAdditionalPrizesWithLineOpacity); + p.fillRect(_margins.left(), top, fill, stroke, stm->msgDateFg); + const auto start = outerWidth - _margins.right() - fill; + p.fillRect(start, top, fill, stroke, stm->msgDateFg); + p.setOpacity(1.); + } +} - auto padding = inBubblePadding(); +QSize TextDelimeterPart::countOptimalSize() { + return { + _margins.left() + _text.maxWidth() + _margins.right(), + _margins.top() + st::normalFont->height + _margins.bottom(), + }; +} - const auto outer = width(); - const auto paintw = outer - padding.left() - padding.right(); +QSize TextDelimeterPart::countCurrentSize(int newWidth) { + return { newWidth, minHeight() }; +} + +StickerWithBadgePart::StickerWithBadgePart( + not_null parent, + Fn lookup, + QString badge) +: _parent(parent) +, _lookup(std::move(lookup)) +, _badgeText(badge) { + ensureCreated(); +} + +void StickerWithBadgePart::draw( + Painter &p, + const PaintContext &context, + int outerWidth) const { const auto stickerSize = st::msgServiceGiftBoxStickerSize; const auto sticker = QRect( - (outer - stickerSize.width()) / 2, - _stickerTop, + (outerWidth - stickerSize.width()) / 2, + st::chatGiveawayStickerTop + _skipTop, stickerSize.width(), stickerSize.height()); @@ -268,35 +319,48 @@ void Giveaway::draw(Painter &p, const PaintContext &context) const { _sticker->draw(p, context, sticker); paintBadge(p, context); } else { - ensureStickerCreated(); + ensureCreated(); } - const auto paintText = [&]( - const Ui::Text::String &text, - int top, - int width) { - p.setPen(stm->historyTextFg); - text.draw(p, { - .position = { padding.left() + (paintw - width) / 2, top}, - .outerWidth = outer, - .availableWidth = width, - .align = style::al_top, - .palette = &stm->textPalette, - .now = context.now, - }); - }; - paintText(_prizesTitle, _prizesTitleTop, paintw); - paintText(_prizes, _prizesTop, _prizesWidth); - paintText(_participantsTitle, _participantsTitleTop, paintw); - paintText(_participants, _participantsTop, _participantsWidth); - if (!_countries.isEmpty()) { - paintText(_countries, _countriesTop, _countriesWidth); +} + +bool StickerWithBadgePart::hasHeavyPart() { + return _sticker && _sticker->hasHeavyPart(); +} + +void StickerWithBadgePart::unloadHeavyPart() { + if (_sticker) { + _sticker->unloadHeavyPart(); } - paintText(_winnersTitle, _winnersTitleTop, paintw); - paintText(_winners, _winnersTop, paintw); - paintChannels(p, context); } -void Giveaway::paintBadge(Painter &p, const PaintContext &context) const { +QSize StickerWithBadgePart::countOptimalSize() { + const auto size = st::msgServiceGiftBoxStickerSize; + return { size.width(), st::chatGiveawayStickerTop + size.height() }; +} + +QSize StickerWithBadgePart::countCurrentSize(int newWidth) { + return { newWidth, minHeight() }; +} + +void StickerWithBadgePart::ensureCreated() const { + if (_sticker) { + return; + } else if (const auto data = _lookup()) { + const auto document = data.sticker; + if (const auto sticker = document->sticker()) { + const auto skipPremiumEffect = false; + _skipTop = data.skipTop; + _sticker.emplace(_parent, document, skipPremiumEffect, _parent); + _sticker->setDiceIndex(sticker->alt, 1); + _sticker->setGiftBoxSticker(data.isGiftBoxSticker); + _sticker->initSize(); + } + } +} + +void StickerWithBadgePart::paintBadge( + Painter &p, + const PaintContext &context) const { validateBadge(context); const auto badge = _badge.size() / _badge.devicePixelRatio(); @@ -318,7 +382,7 @@ void Giveaway::paintBadge(Painter &p, const PaintContext &context) const { p.drawRoundedRect(inner, radius, radius); } - if (!usesBubblePattern(context)) { + if (!_parent->usesBubblePattern(context)) { paintContent(p); } else { Ui::PaintPatternBubblePart( @@ -331,47 +395,108 @@ void Giveaway::paintBadge(Painter &p, const PaintContext &context) const { } } -void Giveaway::paintChannels( - Painter &p, +void StickerWithBadgePart::validateBadge( const PaintContext &context) const { - if (_channels.empty()) { + const auto stm = context.messageStyle(); + const auto &badgeFg = stm->historyFileRadialFg->c; + const auto &badgeBorder = stm->msgBg->c; + if (!_badge.isNull() + && _badgeFg == badgeFg + && _badgeBorder == badgeBorder) { return; } + const auto &font = st::chatGiveawayBadgeFont; + _badgeFg = badgeFg; + _badgeBorder = badgeBorder; + const auto width = font->width(_badgeText); + const auto inner = QRect(0, 0, width, font->height); + const auto rect = inner.marginsAdded(st::chatGiveawayBadgePadding); + const auto size = rect.size(); + const auto ratio = style::DevicePixelRatio(); + _badge = QImage(size * ratio, QImage::Format_ARGB32_Premultiplied); + _badge.setDevicePixelRatio(ratio); + _badge.fill(Qt::transparent); + + auto p = QPainter(&_badge); + auto hq = PainterHighQualityEnabler(p); + p.setPen(QPen(_badgeBorder, st::chatGiveawayBadgeStroke * 1.)); + p.setBrush(Qt::NoBrush); + const auto half = st::chatGiveawayBadgeStroke / 2.; + const auto smaller = QRectF( + rect.translated(-rect.topLeft()) + ).marginsRemoved({ half, half, half, half }); + const auto radius = smaller.height() / 2.; + p.drawRoundedRect(smaller, radius, radius); + p.setPen(_badgeFg); + p.setFont(font); + p.drawText( + st::chatGiveawayBadgePadding.left(), + st::chatGiveawayBadgePadding.top() + font->ascent, + _badgeText); +} + +PeerBubbleListPart::PeerBubbleListPart( + not_null parent, + const std::vector> &list) +: _parent(parent) { + for (const auto &peer : list) { + _peers.push_back({ + .name = Ui::Text::String( + st::semiboldTextStyle, + peer->name(), + kDefaultTextOptions, + st::msgMinWidth), + .thumbnail = Dialogs::Stories::MakeUserpicThumbnail(peer), + .link = peer->openLink(), + .colorIndex = peer->colorIndex(), + }); + } +} + +PeerBubbleListPart::~PeerBubbleListPart() = default; - const auto size = _channels[0].geometry.height(); +void PeerBubbleListPart::draw( + Painter &p, + const PaintContext &context, + int outerWidth) const { + if (_peers.empty()) { + return; + } + + const auto size = _peers[0].geometry.height(); const auto st = context.st; const auto stm = context.messageStyle(); const auto selected = context.selected(); - const auto padding = st::chatGiveawayChannelPadding; - for (const auto &channel : _channels) { - const auto &thumbnail = channel.thumbnail; - const auto &geometry = channel.geometry; - if (!_subscribedToThumbnails) { - thumbnail->subscribeToUpdates([=] { repaint(); }); + const auto padding = st::chatGiveawayPeerPadding; + for (const auto &peer : _peers) { + const auto &thumbnail = peer.thumbnail; + const auto &geometry = peer.geometry; + if (!_subscribed) { + thumbnail->subscribeToUpdates([=] { _parent->repaint(); }); } - const auto colorIndex = channel.colorIndex; + const auto colorIndex = peer.colorIndex; const auto cache = context.outbg ? stm->replyCache[st->colorPatternIndex(colorIndex)].get() : st->coloredReplyCache(selected, colorIndex).get(); - if (channel.corners[0].isNull() || channel.bg != cache->bg) { - channel.bg = cache->bg; - channel.corners = Images::CornersMask(size / 2); - for (auto &image : channel.corners) { + if (peer.corners[0].isNull() || peer.bg != cache->bg) { + peer.bg = cache->bg; + peer.corners = Images::CornersMask(size / 2); + for (auto &image : peer.corners) { style::colorizeImage(image, cache->bg, &image); } } p.setPen(cache->icon); - Ui::DrawRoundedRect(p, geometry, channel.bg, channel.corners); - if (channel.ripple) { - channel.ripple->paint( + Ui::DrawRoundedRect(p, geometry, peer.bg, peer.corners); + if (peer.ripple) { + peer.ripple->paint( p, geometry.x(), geometry.y(), width(), &cache->bg); - if (channel.ripple->empty()) { - channel.ripple = nullptr; + if (peer.ripple->empty()) { + peer.ripple = nullptr; } } @@ -379,7 +504,7 @@ void Giveaway::paintChannels( const auto left = size + padding.left(); const auto top = padding.top(); const auto available = geometry.width() - left - padding.right(); - channel.name.draw(p, { + peer.name.draw(p, { .position = { geometry.left() + left, geometry.top() + top }, .outerWidth = width(), .availableWidth = available, @@ -390,138 +515,311 @@ void Giveaway::paintChannels( .elisionBreakEverywhere = true, }); } - _subscribedToThumbnails = 1; + _subscribed = true; } -void Giveaway::ensureStickerCreated() const { - if (_sticker) { - return; - } - const auto &session = _parent->history()->session(); - auto &packs = session.giftBoxStickersPacks(); - if (const auto document = packs.lookup(_months)) { - if (const auto sticker = document->sticker()) { - const auto skipPremiumEffect = false; - _sticker.emplace(_parent, document, skipPremiumEffect, _parent); - _sticker->setDiceIndex(sticker->alt, 1); - _sticker->setGiftBoxSticker(true); - _sticker->initSize(); +int PeerBubbleListPart::layout(int x, int y, int available) { + const auto size = st::chatGiveawayPeerSize; + const auto skip = st::chatGiveawayPeerSkip; + const auto padding = st::chatGiveawayPeerPadding; + auto left = available; + const auto shiftRow = [&](int i, int top, int shift) { + for (auto j = i; j != 0; --j) { + auto &geometry = _peers[j - 1].geometry; + if (geometry.top() != top) { + break; + } + geometry.moveLeft(geometry.x() + shift); } + }; + const auto count = int(_peers.size()); + for (auto i = 0; i != count; ++i) { + const auto desired = size + + padding.left() + + _peers[i].name.maxWidth() + + padding.right(); + const auto width = std::min(desired, available); + if (left < width) { + shiftRow(i, y, (left + skip) / 2); + left = available; + y += size + skip; + } + _peers[i].geometry = { x + available - left, y, width, size }; + left -= width + skip; } + shiftRow(count, y, (left + skip) / 2); + return y + size + skip; } -void Giveaway::validateBadge(const PaintContext &context) const { - const auto stm = context.messageStyle(); - const auto &badgeFg = stm->historyFileRadialFg->c; - const auto &badgeBorder = stm->msgBg->c; - if (!_badge.isNull() - && _badgeFg == badgeFg - && _badgeBorder == badgeBorder) { - return; - } - const auto &font = st::chatGiveawayBadgeFont; - _badgeFg = badgeFg; - _badgeBorder = badgeBorder; - const auto text = tr::lng_prizes_badge( - tr::now, - lt_amount, - QString::number(_quantity)); - const auto width = font->width(text); - const auto inner = QRect(0, 0, width, font->height); - const auto rect = inner.marginsAdded(st::chatGiveawayBadgePadding); - const auto size = rect.size(); - const auto ratio = style::DevicePixelRatio(); - _badge = QImage(size * ratio, QImage::Format_ARGB32_Premultiplied); - _badge.setDevicePixelRatio(ratio); - _badge.fill(Qt::transparent); - - auto p = QPainter(&_badge); - auto hq = PainterHighQualityEnabler(p); - p.setPen(QPen(_badgeBorder, st::chatGiveawayBadgeStroke * 1.)); - p.setBrush(Qt::NoBrush); - const auto half = st::chatGiveawayBadgeStroke / 2.; - const auto smaller = QRectF( - rect.translated(-rect.topLeft()) - ).marginsRemoved({ half, half, half, half }); - const auto radius = smaller.height() / 2.; - p.drawRoundedRect(smaller, radius, radius); - p.setPen(_badgeFg); - p.setFont(font); - p.drawText( - st::chatGiveawayBadgePadding.left(), - st::chatGiveawayBadgePadding.top() + font->ascent, - text); -} - -TextState Giveaway::textState(QPoint point, StateRequest request) const { +TextState PeerBubbleListPart::textState( + QPoint point, + StateRequest request, + int outerWidth) const { auto result = TextState(_parent); - - if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) { - return result; - } - - for (const auto &channel : _channels) { - if (channel.geometry.contains(point)) { - result.link = channel.link; + for (const auto &peer : _peers) { + if (peer.geometry.contains(point)) { + result.link = peer.link; _lastPoint = point; - return result; + break; } } return result; } -void Giveaway::clickHandlerActiveChanged( - const ClickHandlerPtr &p, - bool active) { -} - -void Giveaway::clickHandlerPressedChanged( +void PeerBubbleListPart::clickHandlerPressedChanged( const ClickHandlerPtr &p, bool pressed) { - for (auto &channel : _channels) { - if (channel.link != p) { + for (auto &peer : _peers) { + if (peer.link != p) { continue; } if (pressed) { - if (!channel.ripple) { - channel.ripple = std::make_unique( - st::defaultRippleAnimation, - Ui::RippleAnimation::RoundRectMask( - channel.geometry.size(), - channel.geometry.height() / 2), - [=] { repaint(); }); + if (!peer.ripple) { + peer.ripple = std::make_unique( + st::defaultRippleAnimation, + Ui::RippleAnimation::RoundRectMask( + peer.geometry.size(), + peer.geometry.height() / 2), + [=] { _parent->repaint(); }); } - channel.ripple->add(_lastPoint - channel.geometry.topLeft()); - } else if (channel.ripple) { - channel.ripple->lastStop(); + peer.ripple->add(_lastPoint - peer.geometry.topLeft()); + } else if (peer.ripple) { + peer.ripple->lastStop(); } break; } } -bool Giveaway::hideFromName() const { - return !parent()->data()->Has(); +bool PeerBubbleListPart::hasHeavyPart() { + return _subscribed; } -bool Giveaway::hasHeavyPart() const { - return _subscribedToThumbnails; +void PeerBubbleListPart::unloadHeavyPart() { + if (_subscribed) { + _subscribed = false; + for (const auto &peer : _peers) { + peer.thumbnail->subscribeToUpdates(nullptr); + } + } } -void Giveaway::unloadHeavyPart() { - if (_subscribedToThumbnails) { - _subscribedToThumbnails = 0; - for (const auto &channel : _channels) { - channel.thumbnail->subscribeToUpdates(nullptr); - } +QSize PeerBubbleListPart::countOptimalSize() { + if (_peers.empty()) { + return {}; } + const auto size = st::chatGiveawayPeerSize; + const auto skip = st::chatGiveawayPeerSkip; + const auto padding = st::chatGiveawayPeerPadding; + auto left = st::msgPadding.left(); + for (const auto &peer : _peers) { + const auto desired = size + + padding.left() + + peer.name.maxWidth() + + padding.right(); + left += desired + skip; + } + return { left - skip + st::msgPadding.right(), size }; } -QMargins Giveaway::inBubblePadding() const { - auto lshift = st::msgPadding.left(); - auto rshift = st::msgPadding.right(); - auto bshift = isBubbleBottom() ? st::msgPadding.top() : st::mediaInBubbleSkip; - auto tshift = isBubbleTop() ? st::msgPadding.bottom() : st::mediaInBubbleSkip; - return QMargins(lshift, tshift, rshift, bshift); +QSize PeerBubbleListPart::countCurrentSize(int newWidth) { + if (_peers.empty()) { + return {}; + } + const auto padding = st::msgPadding; + const auto available = newWidth - padding.left() - padding.right(); + const auto channelsBottom = layout( + padding.left(), + 0, + available); + return { newWidth, channelsBottom }; +} + +auto GenerateGiveawayStart( + not_null parent, + not_null data) +-> Fn)>)> { + return [=](Fn)> push) { + const auto months = data->months; + const auto quantity = data->quantity; + + using Data = StickerWithBadgePart::Data; + const auto sticker = [=] { + const auto &session = parent->history()->session(); + auto &packs = session.giftBoxStickersPacks(); + return Data{ packs.lookup(months), 0, true }; + }; + push(std::make_unique( + parent, + sticker, + tr::lng_prizes_badge( + tr::now, + lt_amount, + QString::number(quantity)))); + + auto pushText = [&]( + TextWithEntities text, + QMargins margins = {}, + const base::flat_map &links = {}) { + push(std::make_unique( + std::move(text), + margins, + links)); + }; + pushText( + Ui::Text::Bold( + tr::lng_prizes_title(tr::now, lt_count, quantity)), + st::chatGiveawayPrizesTitleMargin); + + if (!data->additionalPrize.isEmpty()) { + pushText( + tr::lng_prizes_additional( + tr::now, + lt_count, + quantity, + lt_prize, + TextWithEntities{ data->additionalPrize }, + Ui::Text::RichLangValue), + st::chatGiveawayPrizesMargin); + push(std::make_unique( + tr::lng_prizes_additional_with(tr::now), + st::chatGiveawayPrizesWithPadding)); + } + + pushText( + tr::lng_prizes_about( + tr::now, + lt_count, + quantity, + lt_duration, + Ui::Text::Bold(GiftDuration(months)), + Ui::Text::RichLangValue), + st::chatGiveawayPrizesMargin); + pushText( + Ui::Text::Bold(tr::lng_prizes_participants(tr::now)), + st::chatGiveawayPrizesTitleMargin); + + pushText({ (data->all + ? tr::lng_prizes_participants_all + : tr::lng_prizes_participants_new)( + tr::now, + lt_count, + data->channels.size()), + }, st::chatGiveawayParticipantsMargin); + + auto list = ranges::views::all( + data->channels + ) | ranges::views::transform([](not_null channel) { + return not_null(channel); + }) | ranges::to_vector; + push(std::make_unique( + parent, + std::move(list))); + + const auto &instance = Countries::Instance(); + auto countries = QStringList(); + for (const auto &country : data->countries) { + const auto name = instance.countryNameByISO2(country); + const auto flag = instance.flagEmojiByISO2(country); + countries.push_back(flag + QChar(0xA0) + name); + } + if (const auto count = countries.size()) { + auto united = countries.front(); + for (auto i = 1; i != count; ++i) { + united = ((i + 1 == count) + ? tr::lng_prizes_countries_and_last + : tr::lng_prizes_countries_and_one)( + tr::now, + lt_countries, + united, + lt_country, + countries[i]); + } + pushText({ + tr::lng_prizes_countries(tr::now, lt_countries, united), + }, st::chatGiveawayPrizesMargin); + } + + pushText( + Ui::Text::Bold(tr::lng_prizes_date(tr::now)), + (countries.empty() + ? st::chatGiveawayNoCountriesTitleMargin + : st::chatGiveawayPrizesMargin)); + pushText({ + langDateTime(base::unixtime::parse(data->untilDate)), + }, st::chatGiveawayEndDateMargin); + }; +} + +auto GenerateGiveawayResults( + not_null parent, + not_null data) +-> Fn)>)> { + return [=](Fn)> push) { + const auto quantity = data->winnersCount; + + using Data = StickerWithBadgePart::Data; + const auto sticker = [=] { + const auto &session = parent->history()->session(); + auto &packs = session.diceStickersPacks(); + const auto &emoji = Stickers::DicePacks::kPartyPopper; + const auto skip = st::chatGiveawayWinnersTopSkip; + return Data{ packs.lookup(emoji, 0), skip }; + }; + push(std::make_unique( + parent, + sticker, + tr::lng_prizes_badge( + tr::now, + lt_amount, + QString::number(quantity)))); + + auto pushText = [&]( + TextWithEntities text, + QMargins margins = {}, + const base::flat_map &links = {}) { + push(std::make_unique( + std::move(text), + margins, + links)); + }; + pushText( + Ui::Text::Bold( + tr::lng_prizes_results_title(tr::now)), + st::chatGiveawayPrizesTitleMargin); + const auto showGiveawayHandler = JumpToMessageClickHandler( + data->channel, + data->launchId, + parent->data()->fullId()); + pushText( + tr::lng_prizes_results_about( + tr::now, + lt_count, + quantity, + lt_link, + Ui::Text::Link(tr::lng_prizes_results_link(tr::now)), + Ui::Text::RichLangValue), + st::chatGiveawayPrizesMargin, + { { 1, showGiveawayHandler } }); + pushText( + Ui::Text::Bold(tr::lng_prizes_results_winners(tr::now)), + st::chatGiveawayPrizesTitleMargin); + + push(std::make_unique( + parent, + data->winners)); + if (data->winnersCount > data->winners.size()) { + pushText( + Ui::Text::Bold(tr::lng_prizes_results_more( + tr::now, + lt_count, + data->winnersCount - data->winners.size())), + st::chatGiveawayNoCountriesTitleMargin); + } + pushText({ data->unclaimedCount + ? tr::lng_prizes_results_some(tr::now) + : tr::lng_prizes_results_all(tr::now) + }, st::chatGiveawayEndDateMargin); + }; } } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/media/history_view_giveaway.h b/Telegram/SourceFiles/history/view/media/history_view_giveaway.h index 250e214c299ab..534886fc7710d 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_giveaway.h +++ b/Telegram/SourceFiles/history/view/media/history_view_giveaway.h @@ -11,7 +11,8 @@ For license and copyright information please follow this link: #include "history/view/media/history_view_sticker.h" namespace Data { -struct Giveaway; +struct GiveawayStart; +struct GiveawayResults; } // namespace Data namespace Dialogs::Stories { @@ -24,12 +25,31 @@ class RippleAnimation; namespace HistoryView { -class Giveaway final : public Media { +class MediaInBubble final : public Media { public: - Giveaway( + class Part : public Object { + public: + virtual ~Part() = default; + + virtual void draw( + Painter &p, + const PaintContext &context, + int outerWidth) const = 0; + [[nodiscard]] virtual TextState textState( + QPoint point, + StateRequest request, + int outerWidth) const; + virtual void clickHandlerPressedChanged( + const ClickHandlerPtr &p, + bool pressed); + [[nodiscard]] virtual bool hasHeavyPart(); + virtual void unloadHeavyPart(); + }; + + MediaInBubble( not_null parent, - not_null giveaway); - ~Giveaway(); + Fn)>)> generate); + ~MediaInBubble(); void draw(Painter &p, const PaintContext &context) const override; TextState textState(QPoint point, StateRequest request) const override; @@ -49,7 +69,7 @@ class Giveaway final : public Media { } bool toggleSelectionByHandlerClick( - const ClickHandlerPtr &p) const override { + const ClickHandlerPtr &p) const override { return true; } bool dragItemByHandler(const ClickHandlerPtr &p) const override { @@ -62,62 +82,159 @@ class Giveaway final : public Media { bool hasHeavyPart() const override; private: - using Thumbnail = Dialogs::Stories::Thumbnail; - struct Channel { - Ui::Text::String name; - std::shared_ptr thumbnail; - QRect geometry; - ClickHandlerPtr link; - mutable std::unique_ptr ripple; - mutable std::array corners; - mutable QColor bg; - uint8 colorIndex = 0; + struct Entry { + std::unique_ptr object; }; - void paintBadge(Painter &p, const PaintContext &context) const; - void paintChannels(Painter &p, const PaintContext &context) const; - int layoutChannels(int x, int y, int available); QSize countOptimalSize() override; QSize countCurrentSize(int newWidth) override; - void fillFromData(not_null giveaway); - void ensureStickerCreated() const; - void validateBadge(const PaintContext &context) const; - [[nodiscard]] QMargins inBubblePadding() const; - mutable std::optional _sticker; + std::vector _entries; + +}; + +class TextMediaInBubblePart final : public MediaInBubble::Part { +public: + TextMediaInBubblePart( + TextWithEntities text, + QMargins margins, + const base::flat_map &links = {}); + + void draw( + Painter &p, + const PaintContext &context, + int outerWidth) const override; + TextState textState( + QPoint point, + StateRequest request, + int outerWidth) const override; - Ui::Text::String _prizesTitle; - Ui::Text::String _prizes; - Ui::Text::String _participantsTitle; - Ui::Text::String _participants; - std::vector _channels; - Ui::Text::String _countries; - Ui::Text::String _winnersTitle; - Ui::Text::String _winners; + QSize countOptimalSize() override; + QSize countCurrentSize(int newWidth) override; + +private: + Ui::Text::String _text; + QMargins _margins; + +}; + +class TextDelimeterPart final : public MediaInBubble::Part { +public: + TextDelimeterPart(const QString &text, QMargins margins); + void draw( + Painter &p, + const PaintContext &context, + int outerWidth) const override; + + QSize countOptimalSize() override; + QSize countCurrentSize(int newWidth) override; + +private: + Ui::Text::String _text; + QMargins _margins; + +}; + +class StickerWithBadgePart final : public MediaInBubble::Part { +public: + struct Data { + DocumentData *sticker = nullptr; + int skipTop = 0; + bool isGiftBoxSticker = false; + + explicit operator bool() const { + return sticker != nullptr; + } + }; + StickerWithBadgePart( + not_null parent, + Fn lookup, + QString badge); + + void draw( + Painter &p, + const PaintContext &context, + int outerWidth) const override; + bool hasHeavyPart() override; + void unloadHeavyPart() override; + + QSize countOptimalSize() override; + QSize countCurrentSize(int newWidth) override; + +private: + void ensureCreated() const; + void validateBadge(const PaintContext &context) const; + void paintBadge(Painter &p, const PaintContext &context) const; + + const not_null _parent; + Fn _lookup; + QString _badgeText; + mutable int _skipTop = 0; + mutable std::optional _sticker; mutable QColor _badgeFg; mutable QColor _badgeBorder; mutable QImage _badge; mutable QImage _badgeCache; +}; + +class PeerBubbleListPart final : public MediaInBubble::Part { +public: + PeerBubbleListPart( + not_null parent, + const std::vector> &list); + ~PeerBubbleListPart(); + + void draw( + Painter &p, + const PaintContext &context, + int outerWidth) const override; + TextState textState( + QPoint point, + StateRequest request, + int outerWidth) const override; + void clickHandlerPressedChanged( + const ClickHandlerPtr &p, + bool pressed) override; + bool hasHeavyPart() override; + void unloadHeavyPart() override; + + QSize countOptimalSize() override; + QSize countCurrentSize(int newWidth) override; + +private: + int layout(int x, int y, int available); + + using Thumbnail = Dialogs::Stories::Thumbnail; + struct Peer { + Ui::Text::String name; + std::shared_ptr thumbnail; + QRect geometry; + ClickHandlerPtr link; + mutable std::unique_ptr ripple; + mutable std::array corners; + mutable QColor bg; + uint8 colorIndex = 0; + }; + + const not_null _parent; + std::vector _peers; mutable QPoint _lastPoint; - int _months = 0; - int _quantity = 0; - int _stickerTop = 0; - int _prizesTitleTop = 0; - int _prizesTop = 0; - int _prizesWidth = 0; - int _participantsTitleTop = 0; - int _participantsTop = 0; - int _participantsWidth = 0; - int _countriesTop = 0; - int _countriesWidth = 0; - int _winnersTitleTop = 0; - int _winnersTop = 0; - mutable uint8 _subscribedToThumbnails : 1 = 0; + mutable bool _subscribed = false; }; +[[nodiscard]] auto GenerateGiveawayStart( + not_null parent, + not_null data) +-> Fn)>)>; + +[[nodiscard]] auto GenerateGiveawayResults( + not_null parent, + not_null data) +-> Fn)>)>; + } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/media/history_view_media.cpp b/Telegram/SourceFiles/history/view/media/history_view_media.cpp index 048c43218ecda..f64cb808b1999 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media.cpp @@ -341,11 +341,7 @@ auto Media::getBubbleSelectionIntervals( } bool Media::usesBubblePattern(const PaintContext &context) const { - return (context.selection != FullSelection) - && _parent->hasOutLayout() - && context.bubblesPattern - && !context.viewport.isEmpty() - && !context.bubblesPattern->pixmap.size().isEmpty(); + return _parent->usesBubblePattern(context); } PointState Media::pointState(QPoint point) const { diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_grouped.cpp b/Telegram/SourceFiles/history/view/media/history_view_media_grouped.cpp index 23342263136b3..1ac40603c0c22 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_grouped.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media_grouped.cpp @@ -459,7 +459,7 @@ PointState GroupedMedia::pointState(QPoint point) const { return PointState::Outside; } const auto groupPadding = groupedPadding(); - point -= QPoint(0, groupPadding.top()); + point -= QPoint(0, groupPadding.top()); for (const auto &part : _parts) { if (part.geometry.contains(point)) { return PointState::GroupPart; diff --git a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp index 84b88da8a3112..b5729c6fdcfdb 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp @@ -662,10 +662,10 @@ QRect Photo::enlargeRect() const { const auto enlargeInner = st::historyPageEnlargeSize; const auto enlargeOuter = 2 * skip + enlargeInner; return { - width() - enlargeOuter + skip, - skip, - enlargeInner, - enlargeInner, + width() - enlargeOuter + skip, + skip, + enlargeInner, + enlargeInner, }; } diff --git a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp index a1b60cb672962..7701f6db864b9 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp @@ -43,7 +43,7 @@ QSize PremiumGift::size() { } QString PremiumGift::title() { - return _data.slug.isEmpty() + return gift() ? tr::lng_premium_summary_title(tr::now) : _data.unclaimed ? tr::lng_prize_unclaimed_title(tr::now) @@ -51,7 +51,7 @@ QString PremiumGift::title() { } TextWithEntities PremiumGift::subtitle() { - if (_data.slug.isEmpty()) { + if (gift()) { return { GiftDuration(_data.months) }; } const auto name = _data.channel ? _data.channel->name() : "channel"; @@ -78,7 +78,7 @@ TextWithEntities PremiumGift::subtitle() { } rpl::producer PremiumGift::button() { - return _data.slug.isEmpty() + return (gift() && (outgoingGift() || !_data.unclaimed)) ? tr::lng_sticker_premium_view() : tr::lng_prize_open(); } @@ -90,14 +90,16 @@ ClickHandlerPtr PremiumGift::createViewLink() { return std::make_shared([=](ClickContext context) { const auto my = context.other.value(); if (const auto controller = my.sessionWindow.get()) { + const auto selfId = controller->session().userPeerId(); + const auto self = (from->id == selfId); if (data.slug.isEmpty()) { - const auto selfId = controller->session().userPeerId(); - const auto self = (from->id == selfId); const auto peer = self ? to : from; const auto months = data.months; Settings::ShowGiftPremium(controller, peer, months, self); } else { - ResolveGiftCode(controller, data.slug); + const auto fromId = from->id; + const auto toId = self ? to->id : selfId; + ResolveGiftCode(controller, data.slug, fromId, toId); } } }); @@ -119,7 +121,7 @@ void PremiumGift::draw( } bool PremiumGift::hideServiceText() { - return !_data.slug.isEmpty(); + return !gift(); } void PremiumGift::stickerClearLoopPlayed() { @@ -146,6 +148,18 @@ void PremiumGift::unloadHeavyPart() { } } +bool PremiumGift::incomingGift() const { + return gift() && !_parent->data()->out(); +} + +bool PremiumGift::outgoingGift() const { + return gift() && _parent->data()->out(); +} + +bool PremiumGift::gift() const { + return _data.slug.isEmpty() || !_data.channel; +} + void PremiumGift::ensureStickerCreated() const { if (_sticker) { return; diff --git a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h index b83c4e16be94a..af5724dfacf46 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h +++ b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h @@ -46,6 +46,9 @@ class PremiumGift final : public ServiceBoxContent { void unloadHeavyPart() override; private: + [[nodiscard]] bool incomingGift() const; + [[nodiscard]] bool outgoingGift() const; + [[nodiscard]] bool gift() const; void ensureStickerCreated() const; const not_null _parent; diff --git a/Telegram/SourceFiles/history/view/media/history_view_theme_document.cpp b/Telegram/SourceFiles/history/view/media/history_view_theme_document.cpp index d268d4b3f9128..96bd885f34519 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_theme_document.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_theme_document.cpp @@ -33,6 +33,7 @@ For license and copyright information please follow this link: #include "ui/cached_round_corners.h" #include "ui/painter.h" #include "ui/ui_utility.h" +#include "window/section_widget.h" #include "window/window_session_controller.h" #include "window/themes/window_theme.h" #include "styles/style_chat.h" @@ -471,13 +472,28 @@ ThemeDocumentBox::ThemeDocumentBox( not_null parent, const Data::WallPaper &paper) : _parent(parent) -, _preview( - parent, +, _emojiId(paper.emojiId()) { + Window::WallPaperResolved( + &_parent->history()->owner(), + &paper + ) | rpl::start_with_next([=](const Data::WallPaper *paper) { + _parent->repaint(); + if (!paper) { + _preview.reset(); + } else { + createPreview(*paper); + } + }, _lifetime); +} + +void ThemeDocumentBox::createPreview(const Data::WallPaper &paper) { + _preview.emplace( + _parent, paper.document(), paper, - st::msgServicePhotoWidth) { - _preview.initDimensions(); - _preview.resizeGetHeight(_preview.maxWidth()); + st::msgServicePhotoWidth); + _preview->initDimensions(); + _preview->resizeGetHeight(_preview->maxWidth()); } ThemeDocumentBox::~ThemeDocumentBox() = default; @@ -487,7 +503,9 @@ int ThemeDocumentBox::top() { } QSize ThemeDocumentBox::size() { - return { _preview.maxWidth(), _preview.minHeight() }; + return _preview + ? QSize(_preview->maxWidth(), _preview->minHeight()) + : QSize(st::msgServicePhotoWidth, st::msgServicePhotoWidth); } QString ThemeDocumentBox::title() { @@ -509,8 +527,11 @@ rpl::producer ThemeDocumentBox::button() { } ClickHandlerPtr ThemeDocumentBox::createViewLink() { - const auto out = _parent->data()->out(); const auto to = _parent->history()->peer; + if (to->isChannel()) { + return nullptr; + } + const auto out = _parent->data()->out(); const auto media = _parent->data()->media(); const auto weak = base::make_weak(_parent); const auto paper = media ? media->paper() : nullptr; @@ -557,9 +578,11 @@ void ThemeDocumentBox::draw( Painter &p, const PaintContext &context, const QRect &geometry) { - p.translate(geometry.topLeft()); - _preview.draw(p, context); - p.translate(-geometry.topLeft()); + if (_preview) { + p.translate(geometry.topLeft()); + _preview->draw(p, context); + p.translate(-geometry.topLeft()); + } } void ThemeDocumentBox::stickerClearLoopPlayed() { @@ -572,11 +595,13 @@ std::unique_ptr ThemeDocumentBox::stickerTakePlayer( } bool ThemeDocumentBox::hasHeavyPart() { - return _preview.hasHeavyPart(); + return _preview && _preview->hasHeavyPart(); } void ThemeDocumentBox::unloadHeavyPart() { - _preview.unloadHeavyPart(); + if (_preview) { + _preview->unloadHeavyPart(); + } } } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/media/history_view_theme_document.h b/Telegram/SourceFiles/history/view/media/history_view_theme_document.h index a9e2e6eccdd36..3e1d34ecfc94f 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_theme_document.h +++ b/Telegram/SourceFiles/history/view/media/history_view_theme_document.h @@ -119,8 +119,12 @@ class ThemeDocumentBox final : public ServiceBoxContent { void unloadHeavyPart() override; private: + void createPreview(const Data::WallPaper &paper); + const not_null _parent; - ThemeDocument _preview; + QString _emojiId; + std::optional _preview; + rpl::lifetime _lifetime; }; diff --git a/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp b/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp index ecd2884dff922..7e75ddb91f796 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp @@ -9,34 +9,26 @@ For license and copyright information please follow this link: #include "core/click_handler_types.h" #include "core/ui_integration.h" -#include "lang/lang_keys.h" -#include "history/history_item_components.h" -#include "history/history_item.h" +#include "data/data_file_click_handler.h" +#include "data/data_photo_media.h" +#include "data/data_session.h" +#include "data/data_sponsored_messages.h" +#include "data/data_web_page.h" #include "history/history.h" +#include "history/history_item_components.h" #include "history/view/history_view_cursor_state.h" -#include "history/view/history_view_element.h" +#include "history/view/history_view_reply.h" #include "history/view/history_view_sponsored_click_handler.h" #include "history/view/media/history_view_media_common.h" -#include "history/view/media/history_view_theme_document.h" -#include "ui/image/image.h" -#include "ui/text/text_options.h" -#include "ui/text/text_utilities.h" -#include "ui/text/format_values.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" #include "ui/chat/chat_style.h" -#include "ui/cached_round_corners.h" -#include "ui/effects/ripple_animation.h" #include "ui/painter.h" +#include "ui/rect.h" #include "ui/power_saving.h" -#include "main/main_session.h" -#include "data/data_session.h" -#include "data/data_wall_paper.h" -#include "data/data_media_types.h" -#include "data/data_web_page.h" -#include "data/data_photo.h" -#include "data/data_photo_media.h" -#include "data/data_file_click_handler.h" -#include "data/data_file_origin.h" -#include "data/data_sponsored_messages.h" +#include "ui/text/format_values.h" +#include "ui/text/text_options.h" +#include "ui/text/text_utilities.h" #include "styles/style_chat.h" namespace HistoryView { @@ -44,34 +36,37 @@ namespace { constexpr auto kMaxOriginalEntryLines = 8192; -int articleThumbWidth(not_null thumb, int height) { +[[nodiscard]] int ArticleThumbWidth(not_null thumb, int height) { const auto size = thumb->location(Data::PhotoSize::Thumbnail); return size.height() - ? qMax(qMin(height * size.width() / size.height(), height), 1) + ? std::max(std::min(height * size.width() / size.height(), height), 1) : 1; } -int articleThumbHeight(not_null thumb, int width) { +[[nodiscard]] int ArticleThumbHeight( + not_null thumb, + int width) { const auto size = thumb->size(Data::PhotoSize::Thumbnail); return size.width() ? std::max(size.height() * width / size.width(), 1) : 1; } -std::vector> PrepareCollageMedia( +[[nodiscard]] std::vector> PrepareCollageMedia( not_null parent, const WebPageCollage &data) { auto result = std::vector>(); result.reserve(data.items.size()); + const auto spoiler = false; for (const auto &item : data.items) { - const auto spoiler = false; if (const auto document = std::get_if(&item)) { const auto skipPremiumEffect = false; result.push_back(std::make_unique( parent, *document, skipPremiumEffect, - spoiler)); + spoiler, + /*ttlSeconds = */0)); } else if (const auto photo = std::get_if(&item)) { result.push_back(std::make_unique( parent, @@ -205,14 +200,12 @@ QSize WebPage::countOptimalSize() { _openl = nullptr; _attach = nullptr; _collage = PrepareCollageMedia(_parent->data(), _data->collage); - const auto min = st::msgMinWidth - - _st.padding.left() - - _st.padding.right(); + const auto min = st::msgMinWidth - rect::m::sum::h(_st.padding); _siteName = Ui::Text::String(min); _title = Ui::Text::String(min); _description = Ui::Text::String(min); } - auto lineHeight = UnitedLineHeight(); + const auto lineHeight = UnitedLineHeight(); if (!_openl && (!_data->url.isEmpty() || _sponsoredData)) { const auto previewOfHiddenUrl = [&] { @@ -272,7 +265,7 @@ QSize WebPage::countOptimalSize() { } // init layout - auto title = TextUtilities::SingleLine(_data->title.isEmpty() + const auto title = TextUtilities::SingleLine(_data->title.isEmpty() ? _data->author : _data->title); using Flag = MediaWebPageFlag; @@ -296,13 +289,13 @@ QSize WebPage::countOptimalSize() { // init strings if (_description.isEmpty() && !_data->description.text.isEmpty()) { - auto text = _data->description; + const auto &text = _data->description; if (isLogEntryOriginal()) { - // Fix layout for small bubbles (narrow media caption edit log entries). + // Fix layout for small bubbles + // (narrow media caption edit log entries). _description = Ui::Text::String(st::minPhotoSize - - padding.left() - - padding.right()); + - rect::m::sum::h(padding)); } using MarkedTextContext = Core::MarkedTextContext; auto context = MarkedTextContext{ @@ -329,11 +322,10 @@ QSize WebPage::countOptimalSize() { Ui::WebpageTextTitleOptions()); } if (_title.isEmpty() && !title.isEmpty()) { - auto titleWithEntities = Ui::Text::Link(title, _data->url); if (!_siteNameLines && !_data->url.isEmpty()) { _title.setMarkedText( st::webPageTitleStyle, - std::move(titleWithEntities), + Ui::Text::Link(title, _data->url), Ui::WebpageTextTitleOptions()); } else { @@ -345,19 +337,27 @@ QSize WebPage::countOptimalSize() { } // init dimensions - auto skipBlockWidth = _parent->skipBlockWidth(); + const auto skipBlockWidth = _parent->skipBlockWidth(); auto maxWidth = skipBlockWidth; auto minHeight = 0; - auto siteNameHeight = _siteName.isEmpty() ? 0 : lineHeight; - auto titleMinHeight = _title.isEmpty() ? 0 : lineHeight; - auto descMaxLines = isLogEntryOriginal() ? kMaxOriginalEntryLines : (3 + (siteNameHeight ? 0 : 1) + (titleMinHeight ? 0 : 1)); - auto descriptionMinHeight = _description.isEmpty() ? 0 : qMin(_description.minHeight(), descMaxLines * lineHeight); - auto articleMinHeight = siteNameHeight + titleMinHeight + descriptionMinHeight; - auto articlePhotoMaxWidth = 0; - if (_asArticle) { - articlePhotoMaxWidth = st::webPagePhotoDelta + qMax(articleThumbWidth(_data->photo, articleMinHeight), lineHeight); - } + const auto siteNameHeight = _siteName.isEmpty() ? 0 : lineHeight; + const auto titleMinHeight = _title.isEmpty() ? 0 : lineHeight; + const auto descMaxLines = isLogEntryOriginal() + ? kMaxOriginalEntryLines + : (3 + (siteNameHeight ? 0 : 1) + (titleMinHeight ? 0 : 1)); + const auto descriptionMinHeight = _description.isEmpty() + ? 0 + : std::min(_description.minHeight(), descMaxLines * lineHeight); + const auto articleMinHeight = siteNameHeight + + titleMinHeight + + descriptionMinHeight; + const auto articlePhotoMaxWidth = _asArticle + ? st::webPagePhotoDelta + + std::max( + ArticleThumbWidth(_data->photo, articleMinHeight), + lineHeight) + : 0; if (!_siteName.isEmpty()) { accumulate_max(maxWidth, _siteName.maxWidth() + articlePhotoMaxWidth); @@ -368,32 +368,38 @@ QSize WebPage::countOptimalSize() { minHeight += titleMinHeight; } if (!_description.isEmpty()) { - accumulate_max(maxWidth, _description.maxWidth() + articlePhotoMaxWidth); + accumulate_max( + maxWidth, + _description.maxWidth() + articlePhotoMaxWidth); minHeight += descriptionMinHeight; } if (_attach) { - auto attachAtTop = _siteName.isEmpty() && _title.isEmpty() && _description.isEmpty(); - if (!attachAtTop) minHeight += st::mediaInBubbleSkip; + const auto attachAtTop = _siteName.isEmpty() + && _title.isEmpty() + && _description.isEmpty(); + if (!attachAtTop) { + minHeight += st::mediaInBubbleSkip; + } _attach->initDimensions(); - auto bubble = _attach->bubbleMargins(); - auto maxMediaWidth = _attach->maxWidth() - bubble.left() - bubble.right(); + const auto bubble = _attach->bubbleMargins(); + auto maxMediaWidth = _attach->maxWidth() - rect::m::sum::h(bubble); if (isBubbleBottom() && _attach->customInfoLayout()) { maxMediaWidth += skipBlockWidth; } accumulate_max(maxWidth, maxMediaWidth); - minHeight += _attach->minHeight() - bubble.top() - bubble.bottom(); + minHeight += _attach->minHeight() - rect::m::sum::v(bubble); } if (_data->type == WebPageType::Video && _data->duration) { _duration = Ui::FormatDurationText(_data->duration); _durationWidth = st::msgDateFont->width(_duration); } if (_openButtonWidth) { - const auto &margins = st::historyPageButtonPadding; - maxWidth += margins.left() + _openButtonWidth + margins.right(); + maxWidth += rect::m::sum::h(st::historyPageButtonPadding) + + _openButtonWidth; } - maxWidth += padding.left() + padding.right(); - minHeight += padding.top() + padding.bottom(); + maxWidth += rect::m::sum::h(padding); + minHeight += rect::m::sum::v(padding); if (_asArticle) { minHeight = resizeGetHeight(maxWidth); @@ -406,43 +412,51 @@ QSize WebPage::countCurrentSize(int newWidth) { return { newWidth, minHeight() }; } - auto padding = inBubblePadding() + innerMargin(); - auto innerWidth = newWidth - padding.left() - padding.right(); + const auto padding = inBubblePadding() + innerMargin(); + const auto innerWidth = newWidth - rect::m::sum::h(padding); auto newHeight = 0; - auto lineHeight = UnitedLineHeight(); - auto linesMax = (_sponsoredData || isLogEntryOriginal()) + const auto lineHeight = UnitedLineHeight(); + const auto linesMax = (_sponsoredData || isLogEntryOriginal()) ? kMaxOriginalEntryLines : 5; - auto siteNameHeight = _siteNameLines ? lineHeight : 0; + const auto siteNameHeight = _siteNameLines ? lineHeight : 0; + const auto twoTitleLines = 2 * st::webPageTitleFont->height; + const auto descriptionLineHeight = st::webPageDescriptionFont->height; const auto asSponsored = (!!_sponsoredData); if (asArticle() || asSponsored) { + const auto sponsoredUserpic = (asSponsored && _sponsoredData->peer); constexpr auto kSponsoredUserpicLines = 2; - _pixh = (asSponsored ? kSponsoredUserpicLines : linesMax) * lineHeight; + _pixh = lineHeight + * (asSponsored ? kSponsoredUserpicLines : linesMax); do { - _pixw = asSponsored ? _pixh : articleThumbWidth(_data->photo, _pixh); - auto wleft = innerWidth - st::webPagePhotoDelta - qMax(_pixw, lineHeight); + _pixw = asSponsored + ? _pixh + : ArticleThumbWidth(_data->photo, _pixh); + const auto wleft = innerWidth + - st::webPagePhotoDelta + - std::max(_pixw, lineHeight); newHeight = siteNameHeight; if (_title.isEmpty()) { _titleLines = 0; } else { - if (_title.countHeight(wleft) < 2 * st::webPageTitleFont->height) { - _titleLines = 1; - } else { - _titleLines = 2; - } + _titleLines = (_title.countHeight(wleft) < twoTitleLines) + ? 1 + : 2; newHeight += _titleLines * lineHeight; } - auto descriptionHeight = _description.countHeight(wleft); - if (descriptionHeight < (linesMax - _siteNameLines - _titleLines) * st::webPageDescriptionFont->height) { + const auto descriptionHeight = _description.countHeight( + sponsoredUserpic ? innerWidth : wleft); + const auto restLines = (linesMax - _siteNameLines - _titleLines); + if (descriptionHeight < restLines * descriptionLineHeight) { // We have height for all the lines. _descriptionLines = -1; newHeight += descriptionHeight; } else { - _descriptionLines = (linesMax - _siteNameLines - _titleLines); + _descriptionLines = restLines; newHeight += _descriptionLines * lineHeight; } @@ -458,55 +472,55 @@ QSize WebPage::countCurrentSize(int newWidth) { if (_title.isEmpty()) { _titleLines = 0; } else { - if (_title.countHeight(innerWidth) < 2 * st::webPageTitleFont->height) { - _titleLines = 1; - } else { - _titleLines = 2; - } + _titleLines = (_title.countHeight(innerWidth) < twoTitleLines) + ? 1 + : 2; newHeight += _titleLines * lineHeight; } if (_description.isEmpty()) { _descriptionLines = 0; } else { - auto descriptionHeight = _description.countHeight(innerWidth); - if (descriptionHeight < (linesMax - _siteNameLines - _titleLines) * st::webPageDescriptionFont->height) { + const auto restLines = (linesMax - _siteNameLines - _titleLines); + const auto descriptionHeight = _description.countHeight( + innerWidth); + if (descriptionHeight < restLines * descriptionLineHeight) { // We have height for all the lines. _descriptionLines = -1; newHeight += descriptionHeight; } else { - _descriptionLines = (linesMax - _siteNameLines - _titleLines); + _descriptionLines = restLines; newHeight += _descriptionLines * lineHeight; } } if (_attach) { - auto attachAtTop = !_siteNameLines && !_titleLines && !_descriptionLines; - if (!attachAtTop) newHeight += st::mediaInBubbleSkip; - - auto bubble = _attach->bubbleMargins(); + const auto attachAtTop = !_siteNameLines + && !_titleLines + && !_descriptionLines; + if (!attachAtTop) { + newHeight += st::mediaInBubbleSkip; + } - _attach->resizeGetHeight(innerWidth + bubble.left() + bubble.right()); - newHeight += _attach->height() - bubble.top() - bubble.bottom(); + const auto bubble = _attach->bubbleMargins(); + _attach->resizeGetHeight(innerWidth + rect::m::sum::h(bubble)); + newHeight += _attach->height() - rect::m::sum::v(bubble); } } - newHeight += padding.top() + padding.bottom(); + newHeight += rect::m::sum::v(padding); return { newWidth, newHeight }; } -TextSelection WebPage::toTitleSelection( - TextSelection selection) const { +TextSelection WebPage::toTitleSelection(TextSelection selection) const { return UnshiftItemSelection(selection, _siteName); } -TextSelection WebPage::fromTitleSelection( - TextSelection selection) const { +TextSelection WebPage::fromTitleSelection(TextSelection selection) const { return ShiftItemSelection(selection, _siteName); } -TextSelection WebPage::toDescriptionSelection( - TextSelection selection) const { +TextSelection WebPage::toDescriptionSelection(TextSelection selection) const { return UnshiftItemSelection(toTitleSelection(selection), _title); } @@ -551,7 +565,7 @@ void WebPage::unloadHeavyPart() { } void WebPage::draw(Painter &p, const PaintContext &context) const { - if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) { + if (width() < rect::m::sum::h(st::msgPadding) + 1) { return; } const auto st = context.st; @@ -559,20 +573,48 @@ void WebPage::draw(Painter &p, const PaintContext &context) const { const auto stm = context.messageStyle(); const auto bubble = _attach ? _attach->bubbleMargins() : QMargins(); - const auto full = QRect(0, 0, width(), height()); - auto outer = full.marginsRemoved(inBubblePadding()); - auto inner = outer.marginsRemoved(innerMargin()); + const auto full = Rect(currentSize()); + const auto outer = full - inBubblePadding(); + const auto inner = outer - innerMargin(); + const auto attachAdditionalInfoText = _attach + ? _attach->additionalInfoString() + : QString(); auto tshift = inner.top(); auto paintw = inner.width(); - auto attachAdditionalInfoText = _attach ? _attach->additionalInfoString() : QString(); const auto selected = context.selected(); - const auto colorIndex = parent()->colorIndex(); + const auto view = parent(); + const auto colorIndex = view->colorIndex(); const auto cache = context.outbg ? stm->replyCache[st->colorPatternIndex(colorIndex)].get() : st->coloredReplyCache(selected, colorIndex).get(); + const auto from = view->data()->displayFrom(); + const auto backgroundEmojiId = from + ? from->backgroundEmojiId() + : DocumentId(); + const auto backgroundEmoji = backgroundEmojiId + ? st->backgroundEmojiData(backgroundEmojiId).get() + : nullptr; + const auto backgroundEmojiCache = backgroundEmoji + ? &backgroundEmoji->caches[Ui::BackgroundEmojiData::CacheIndex( + selected, + context.outbg, + true, + colorIndex + 1)] + : nullptr; Ui::Text::ValidateQuotePaintCache(*cache, _st); Ui::Text::FillQuotePaint(p, outer, *cache, _st); + if (backgroundEmoji) { + ValidateBackgroundEmoji( + backgroundEmojiId, + backgroundEmoji, + backgroundEmojiCache, + cache, + view); + if (!backgroundEmojiCache->frames[0].isNull()) { + FillBackgroundEmoji(p, outer, false, *backgroundEmojiCache); + } + } if (_ripple) { _ripple->paint(p, outer.x(), outer.y(), width(), &cache->bg); @@ -581,32 +623,36 @@ void WebPage::draw(Painter &p, const PaintContext &context) const { } } + const auto asSponsored = (!!_sponsoredData); + auto lineHeight = UnitedLineHeight(); if (asArticle()) { ensurePhotoMediaCreated(); - QPixmap pix; - auto pw = qMax(_pixw, lineHeight); - auto ph = _pixh; - auto pixw = _pixw, pixh = articleThumbHeight(_photoMedia.get(), _pixw); + auto pix = QPixmap(); + const auto pw = qMax(_pixw, lineHeight); + const auto ph = _pixh; + auto pixw = _pixw; + auto pixh = ArticleThumbHeight(_photoMedia.get(), _pixw); const auto maxsize = _photoMedia->size(Data::PhotoSize::Thumbnail); const auto maxw = style::ConvertScale(maxsize.width()); const auto maxh = style::ConvertScale(maxsize.height()); if (pixw * ph != pixh * pw) { - float64 coef = (pixw * ph > pixh * pw) ? qMin(ph / float64(pixh), maxh / float64(pixh)) : qMin(pw / float64(pixw), maxw / float64(pixw)); - pixh = qRound(pixh * coef); - pixw = qRound(pixw * coef); + const auto coef = (pixw * ph > pixh * pw) + ? std::min(ph / float64(pixh), maxh / float64(pixh)) + : std::min(pw / float64(pixw), maxw / float64(pixw)); + pixh = std::round(pixh * coef); + pixw = std::round(pixw * coef); } const auto size = QSize(pixw, pixh); const auto args = Images::PrepareArgs{ .options = Images::Option::RoundSmall, .outer = { pw, ph }, }; - if (const auto thumbnail = _photoMedia->image( - Data::PhotoSize::Thumbnail)) { + using namespace Data; + if (const auto thumbnail = _photoMedia->image(PhotoSize::Thumbnail)) { pix = thumbnail->pixSingle(size, args); - } else if (const auto small = _photoMedia->image( - Data::PhotoSize::Small)) { + } else if (const auto small = _photoMedia->image(PhotoSize::Small)) { pix = small->pixSingle(size, args.blurred()); } else if (const auto blurred = _photoMedia->thumbnailInline()) { pix = blurred->pixSingle(size, args.blurred()); @@ -616,12 +662,21 @@ void WebPage::draw(Painter &p, const PaintContext &context) const { const auto st = context.st; Ui::FillRoundRect( p, - style::rtlrect(inner.left() + paintw - pw, tshift, pw, _pixh, width()), + style::rtlrect( + inner.left() + paintw - pw, + tshift, + pw, + _pixh, + width()), st->msgSelectOverlay(), st->msgSelectOverlayCorners(Ui::CachedCornerRadius::Small)); } - paintw -= pw + st::webPagePhotoDelta; - } else if (_sponsoredData && _sponsoredData->peer) { + if (!asSponsored) { + // Ignore photo width in sponsored messages, + // as its width only affects the title. + paintw -= pw + st::webPagePhotoDelta; + } + } else if (asSponsored && _sponsoredData->peer) { const auto size = _pixh; const auto sizeHq = size * style::DevicePixelRatio(); const auto userpicPos = QPoint(inner.left() + paintw - size, tshift); @@ -643,29 +698,53 @@ void WebPage::draw(Painter &p, const PaintContext &context) const { ? stm->semiboldPalette : st->coloredTextPalette(selected, colorIndex)); - auto endskip = 0; - if (_siteName.hasSkipBlock()) { - endskip = _parent->skipBlockWidth(); - } - _siteName.drawLeftElided(p, inner.left(), tshift, paintw, width(), _siteNameLines, style::al_left, 0, -1, endskip, false, context.selection); + const auto endskip = _siteName.hasSkipBlock() + ? _parent->skipBlockWidth() + : 0; + _siteName.drawLeftElided( + p, + inner.left(), + tshift, + paintw, + width(), + _siteNameLines, + style::al_left, + 0, + -1, + endskip, + false, + context.selection); tshift += lineHeight; p.setTextPalette(stm->textPalette); } p.setPen(stm->historyTextFg); if (_titleLines) { - auto endskip = 0; - if (_title.hasSkipBlock()) { - endskip = _parent->skipBlockWidth(); - } - _title.drawLeftElided(p, inner.left(), tshift, paintw, width(), _titleLines, style::al_left, 0, -1, endskip, false, toTitleSelection(context.selection)); + const auto endskip = _title.hasSkipBlock() + ? _parent->skipBlockWidth() + : 0; + const auto titleWidth = asSponsored + ? (paintw - _pixh - st::webPagePhotoDelta) + : paintw; + _title.drawLeftElided( + p, + inner.left(), + tshift, + titleWidth, + width(), + _titleLines, + style::al_left, + 0, + -1, + endskip, + false, + toTitleSelection(context.selection)); tshift += _titleLines * lineHeight; } if (_descriptionLines) { - auto endskip = 0; - if (_description.hasSkipBlock()) { - endskip = _parent->skipBlockWidth(); - } + const auto endskip = _description.hasSkipBlock() + ? _parent->skipBlockWidth() + : 0; _parent->prepareCustomEmojiPaint(p, context, _description); _description.draw(p, { .position = { inner.left(), tshift }, @@ -686,12 +765,17 @@ void WebPage::draw(Painter &p, const PaintContext &context) const { : _description.countHeight(paintw); } if (_attach) { - auto attachAtTop = !_siteNameLines && !_titleLines && !_descriptionLines; - if (!attachAtTop) tshift += st::mediaInBubbleSkip; + const auto attachAtTop = !_siteNameLines + && !_titleLines + && !_descriptionLines; + if (!attachAtTop) { + tshift += st::mediaInBubbleSkip; + } - auto attachLeft = inner.left() - bubble.left(); - auto attachTop = tshift - bubble.top(); - if (rtl()) attachLeft = width() - attachLeft - _attach->width(); + const auto attachLeft = rtl() + ? (width() - (inner.left() - bubble.left()) - _attach->width()) + : (inner.left() - bubble.left()); + const auto attachTop = tshift - bubble.top(); p.translate(attachLeft, attachTop); @@ -701,8 +785,8 @@ void WebPage::draw(Painter &p, const PaintContext &context) const { ).withSelection(context.selected() ? FullSelection : TextSelection())); - auto pixwidth = _attach->width(); - auto pixheight = _attach->height(); + const auto pixwidth = _attach->width(); + const auto pixheight = _attach->height(); if (_data->type == WebPageType::Video && _collage.empty() @@ -710,22 +794,47 @@ void WebPage::draw(Painter &p, const PaintContext &context) const { && !_data->document) { if (_attach->isReadyForOpen()) { if (_data->siteName == u"YouTube"_q) { - st->youtubeIcon().paint(p, (pixwidth - st::youtubeIcon.width()) / 2, (pixheight - st::youtubeIcon.height()) / 2, width()); + st->youtubeIcon().paint( + p, + (pixwidth - st::youtubeIcon.width()) / 2, + (pixheight - st::youtubeIcon.height()) / 2, + width()); } else { - st->videoIcon().paint(p, (pixwidth - st::videoIcon.width()) / 2, (pixheight - st::videoIcon.height()) / 2, width()); + st->videoIcon().paint( + p, + (pixwidth - st::videoIcon.width()) / 2, + (pixheight - st::videoIcon.height()) / 2, + width()); } } if (_durationWidth) { - auto dateX = pixwidth - _durationWidth - st::msgDateImgDelta - 2 * st::msgDateImgPadding.x(); - auto dateY = pixheight - st::msgDateFont->height - 2 * st::msgDateImgPadding.y() - st::msgDateImgDelta; - auto dateW = pixwidth - dateX - st::msgDateImgDelta; - auto dateH = pixheight - dateY - st::msgDateImgDelta; - - Ui::FillRoundRect(p, dateX, dateY, dateW, dateH, sti->msgDateImgBg, sti->msgDateImgBgCorners); + const auto dateX = pixwidth + - _durationWidth + - st::msgDateImgDelta + - 2 * st::msgDateImgPadding.x(); + const auto dateY = pixheight + - st::msgDateFont->height + - 2 * st::msgDateImgPadding.y() + - st::msgDateImgDelta; + const auto dateW = pixwidth - dateX - st::msgDateImgDelta; + const auto dateH = pixheight - dateY - st::msgDateImgDelta; + + Ui::FillRoundRect( + p, + dateX, + dateY, + dateW, + dateH, + sti->msgDateImgBg, + sti->msgDateImgBgCorners); p.setFont(st::msgDateFont); p.setPen(st->msgDateImgFg()); - p.drawTextLeft(dateX + st::msgDateImgPadding.x(), dateY + st::msgDateImgPadding.y(), pixwidth, _duration); + p.drawTextLeft( + dateX + st::msgDateImgPadding.x(), + dateY + st::msgDateImgPadding.y(), + pixwidth, + _duration); } } @@ -734,7 +843,11 @@ void WebPage::draw(Painter &p, const PaintContext &context) const { if (!attachAdditionalInfoText.isEmpty()) { p.setFont(st::msgDateFont); p.setPen(stm->msgDateFg); - p.drawTextLeft(st::msgPadding.left(), outer.y() + outer.height() + st::mediaInBubbleSkip, width(), attachAdditionalInfoText); + p.drawTextLeft( + st::msgPadding.left(), + outer.y() + outer.height() + st::mediaInBubbleSkip, + width(), + attachAdditionalInfoText); } } @@ -761,74 +874,91 @@ bool WebPage::asArticle() const { TextState WebPage::textState(QPoint point, StateRequest request) const { auto result = TextState(_parent); - if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) { + if (width() < rect::m::sum::h(st::msgPadding) + 1) { return result; } const auto bubble = _attach ? _attach->bubbleMargins() : QMargins(); - const auto full = QRect(0, 0, width(), height()); - auto outer = full.marginsRemoved(inBubblePadding()); + const auto full = Rect(currentSize()); + auto outer = full - inBubblePadding(); if (_sponsoredData) { outer.translate(0, st::msgDateFont->height); } - auto inner = outer.marginsRemoved(innerMargin()); + const auto inner = outer - innerMargin(); auto tshift = inner.top(); auto paintw = inner.width(); - auto lineHeight = UnitedLineHeight(); + const auto lineHeight = UnitedLineHeight(); auto inThumb = false; if (asArticle()) { - auto pw = qMax(_pixw, lineHeight); - if (style::rtlrect(inner.left() + paintw - pw, tshift, pw, _pixh, width()).contains(point)) { - inThumb = true; - } + const auto pw = std::max(_pixw, lineHeight); + inThumb = style::rtlrect( + inner.left() + paintw - pw, + tshift, + pw, + _pixh, + width()).contains(point); paintw -= pw + st::webPagePhotoDelta; } - int symbolAdd = 0; + auto symbolAdd = int(0); if (_siteNameLines) { if (point.y() >= tshift && point.y() < tshift + lineHeight) { - Ui::Text::StateRequestElided siteNameRequest = request.forText(); + auto siteNameRequest = Ui::Text::StateRequestElided( + request.forText()); siteNameRequest.lines = _siteNameLines; - result = TextState(_parent, _siteName.getStateElidedLeft( - point - QPoint(inner.left(), tshift), - paintw, - width(), - siteNameRequest)); + result = TextState( + _parent, + _siteName.getStateElidedLeft( + point - QPoint(inner.left(), tshift), + paintw, + width(), + siteNameRequest)); } else if (point.y() >= tshift + lineHeight) { symbolAdd += _siteName.length(); } tshift += lineHeight; } if (_titleLines) { - if (point.y() >= tshift && point.y() < tshift + _titleLines * lineHeight) { - Ui::Text::StateRequestElided titleRequest = request.forText(); + if (point.y() >= tshift + && point.y() < tshift + _titleLines * lineHeight) { + auto titleRequest = Ui::Text::StateRequestElided( + request.forText()); titleRequest.lines = _titleLines; - result = TextState(_parent, _title.getStateElidedLeft( - point - QPoint(inner.left(), tshift), - paintw, - width(), - titleRequest)); + result = TextState( + _parent, + _title.getStateElidedLeft( + point - QPoint(inner.left(), tshift), + paintw, + width(), + titleRequest)); } else if (point.y() >= tshift + _titleLines * lineHeight) { symbolAdd += _title.length(); } tshift += _titleLines * lineHeight; } if (_descriptionLines) { - auto descriptionHeight = (_descriptionLines > 0) ? _descriptionLines * lineHeight : _description.countHeight(paintw); + const auto descriptionHeight = (_descriptionLines > 0) + ? _descriptionLines * lineHeight + : _description.countHeight(paintw); if (point.y() >= tshift && point.y() < tshift + descriptionHeight) { if (_descriptionLines > 0) { - Ui::Text::StateRequestElided descriptionRequest = request.forText(); + auto descriptionRequest = Ui::Text::StateRequestElided( + request.forText()); descriptionRequest.lines = _descriptionLines; - result = TextState(_parent, _description.getStateElidedLeft( - point - QPoint(inner.left(), tshift), - paintw, - width(), - descriptionRequest)); + result = TextState( + _parent, + _description.getStateElidedLeft( + point - QPoint(inner.left(), tshift), + paintw, + width(), + descriptionRequest)); } else { - result = TextState(_parent, _description.getStateLeft( - point - QPoint(inner.left(), tshift), - paintw, - width(), - request.forText())); + result = TextState( + _parent, + _description.getStateLeft( + point - QPoint(inner.left(), tshift), + paintw, + width(), + request.forText())); } } else if (point.y() >= tshift + descriptionHeight) { symbolAdd += _description.length(); @@ -838,14 +968,26 @@ TextState WebPage::textState(QPoint point, StateRequest request) const { if (inThumb) { result.link = _openl; } else if (_attach) { - auto attachAtTop = !_siteNameLines && !_titleLines && !_descriptionLines; - if (!attachAtTop) tshift += st::mediaInBubbleSkip; - - if (QRect(inner.left(), tshift, paintw, inner.top() + inner.height() - tshift).contains(point)) { - auto attachLeft = inner.left() - bubble.left(); - auto attachTop = tshift - bubble.top(); - if (rtl()) attachLeft = width() - attachLeft - _attach->width(); - result = _attach->textState(point - QPoint(attachLeft, attachTop), request); + const auto attachAtTop = !_siteNameLines + && !_titleLines + && !_descriptionLines; + if (!attachAtTop) { + tshift += st::mediaInBubbleSkip; + } + + const auto rect = QRect( + inner.left(), + tshift, + paintw, + inner.top() + inner.height() - tshift); + if (rect.contains(point)) { + const auto attachLeft = rtl() + ? width() - (inner.left() - bubble.left()) - _attach->width() + : (inner.left() - bubble.left()); + const auto attachTop = tshift - bubble.top(); + result = _attach->textState( + point - QPoint(attachLeft, attachTop), + request); if (result.cursor == CursorState::Enlarge) { result.cursor = CursorState::None; } else { @@ -877,27 +1019,37 @@ ClickHandlerPtr WebPage::replaceAttachLink( return _openl; } -TextSelection WebPage::adjustSelection(TextSelection selection, TextSelectType type) const { - if ((!_titleLines && !_descriptionLines) || selection.to <= _siteName.length()) { +TextSelection WebPage::adjustSelection( + TextSelection selection, + TextSelectType type) const { + if ((!_titleLines && !_descriptionLines) + || selection.to <= _siteName.length()) { return _siteName.adjustSelection(selection, type); } - auto titlesLength = _siteName.length() + _title.length(); - auto titleSelection = _title.adjustSelection(toTitleSelection(selection), type); - if ((!_siteNameLines && !_descriptionLines) || (selection.from >= _siteName.length() && selection.to <= titlesLength)) { + const auto titlesLength = _siteName.length() + _title.length(); + const auto titleSelection = _title.adjustSelection( + toTitleSelection(selection), + type); + if ((!_siteNameLines && !_descriptionLines) + || (selection.from >= _siteName.length() + && selection.to <= titlesLength)) { return fromTitleSelection(titleSelection); } - auto descriptionSelection = _description.adjustSelection(toDescriptionSelection(selection), type); + const auto descriptionSelection = _description.adjustSelection( + toDescriptionSelection(selection), + type); if ((!_siteNameLines && !_titleLines) || selection.from >= titlesLength) { return fromDescriptionSelection(descriptionSelection); } - auto siteNameSelection = _siteName.adjustSelection(selection, type); - if (!_descriptionLines || selection.to <= titlesLength) { - return { siteNameSelection.from, fromTitleSelection(titleSelection).to }; - } - return { siteNameSelection.from, fromDescriptionSelection(descriptionSelection).to }; + return { + _siteName.adjustSelection(selection, type).from, + (!_descriptionLines || selection.to <= titlesLength) + ? fromTitleSelection(titleSelection).to + : fromDescriptionSelection(descriptionSelection).to, + }; } uint16 WebPage::fullSelectionLength() const { @@ -918,8 +1070,8 @@ void WebPage::clickHandlerPressedChanged( if (p == _openl) { if (pressed) { if (!_ripple) { - const auto full = QRect(0, 0, width(), height()); - const auto outer = full.marginsRemoved(inBubblePadding()); + const auto full = Rect(currentSize()); + const auto outer = full - inBubblePadding(); const auto owner = &parent()->history()->owner(); _ripple = std::make_unique( st::defaultRippleAnimation, @@ -955,23 +1107,20 @@ void WebPage::playAnimation(bool autoplay) { } bool WebPage::isDisplayed() const { - const auto item = _parent->data(); return !_data->pendingTill && !_data->failed - && !item->Has(); + && !_parent->data()->Has(); } QString WebPage::additionalInfoString() const { return _attach ? _attach->additionalInfoString() : QString(); } -bool WebPage::toggleSelectionByHandlerClick( - const ClickHandlerPtr &p) const { +bool WebPage::toggleSelectionByHandlerClick(const ClickHandlerPtr &p) const { return _attach && _attach->toggleSelectionByHandlerClick(p); } -bool WebPage::allowTextSelectionByHandler( - const ClickHandlerPtr &p) const { +bool WebPage::allowTextSelectionByHandler(const ClickHandlerPtr &p) const { return (p == _openl); } @@ -981,8 +1130,7 @@ bool WebPage::dragItemByHandler(const ClickHandlerPtr &p) const { TextForMimeData WebPage::selectedText(TextSelection selection) const { auto siteNameResult = _siteName.toTextForMimeData(selection); - auto titleResult = _title.toTextForMimeData( - toTitleSelection(selection)); + auto titleResult = _title.toTextForMimeData(toTitleSelection(selection)); auto descriptionResult = _description.toTextForMimeData( toDescriptionSelection(selection)); if (titleResult.empty() && descriptionResult.empty()) { @@ -994,12 +1142,18 @@ TextForMimeData WebPage::selectedText(TextSelection selection) const { } else if (siteNameResult.empty()) { return titleResult.append('\n').append(std::move(descriptionResult)); } else if (titleResult.empty()) { - return siteNameResult.append('\n').append(std::move(descriptionResult)); + return siteNameResult + .append('\n') + .append(std::move(descriptionResult)); } else if (descriptionResult.empty()) { return siteNameResult.append('\n').append(std::move(titleResult)); } - return siteNameResult.append('\n').append(std::move(titleResult)).append('\n').append(std::move(descriptionResult)); + return siteNameResult + .append('\n') + .append(std::move(titleResult)) + .append('\n') + .append(std::move(descriptionResult)); } QMargins WebPage::inBubblePadding() const { diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_button.cpp b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_button.cpp index 345512ea18fc7..91ad934843217 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_button.cpp +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_button.cpp @@ -28,7 +28,6 @@ For license and copyright information please follow this link: namespace HistoryView::Reactions { namespace { -constexpr auto kDivider = 4; constexpr auto kToggleDuration = crl::time(120); constexpr auto kActivateDuration = crl::time(150); constexpr auto kExpandDuration = crl::time(300); diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp index 3d0225317a584..c60a49fa51498 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp @@ -806,7 +806,7 @@ void Selector::expand() { createList(); cacheExpandIcon(); - [[maybe_unused]] const auto grabbed = Ui::GrabWidget(_scroll); + [[maybe_unused]] const auto grabbed = Ui::GrabWidget(_scroll); // clazy:exclude=unused-non-trivial-variable _list->prepareExpanding(); setSelected(-1); diff --git a/Telegram/SourceFiles/info/boosts/create_giveaway_box.cpp b/Telegram/SourceFiles/info/boosts/create_giveaway_box.cpp index b34175a136996..3b06d0adb0956 100644 --- a/Telegram/SourceFiles/info/boosts/create_giveaway_box.cpp +++ b/Telegram/SourceFiles/info/boosts/create_giveaway_box.cpp @@ -37,6 +37,7 @@ For license and copyright information please follow this link: #include "ui/toast/toast.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/continuous_sliders.h" +#include "ui/widgets/fields/input_field.h" #include "ui/widgets/labels.h" #include "ui/wrap/slide_wrap.h" #include "styles/style_giveaway.h" @@ -48,6 +49,7 @@ For license and copyright information please follow this link: namespace { constexpr auto kDoneTooltipDuration = 5 * crl::time(1000); +constexpr auto kAdditionalPrizeLengthMax = 128; [[nodiscard]] QDateTime ThreeDaysAfterToday() { auto dateNow = QDateTime::currentDateTime(); @@ -257,6 +259,10 @@ void CreateGiveawayBox( rpl::variable dateValue; rpl::variable> countriesValue; + rpl::variable additionalPrize; + rpl::variable chosenMonths; + rpl::variable showWinners; + rpl::variable confirmButtonBusy = true; }; const auto state = box->lifetime().make_state(peer); @@ -669,6 +675,182 @@ void CreateGiveawayBox( c->add(std::move(terms)); }; + const auto durationGroup = std::make_shared(0); + durationGroup->setChangedCallback([=](int value) { + state->chosenMonths = state->apiOptions.monthsFromPreset(value); + }); + const auto listOptionsRandom = randomWrap->entity()->add( + object_ptr(box)); + const auto listOptionsSpecific = contentWrap->entity()->add( + object_ptr(box)); + const auto rebuildListOptions = [=](GiveawayType type, int usersCount) { + if (prepaid) { + return; + } + while (listOptionsRandom->count()) { + delete listOptionsRandom->widgetAt(0); + } + while (listOptionsSpecific->count()) { + delete listOptionsSpecific->widgetAt(0); + } + const auto listOptions = (type == GiveawayType::SpecificUsers) + ? listOptionsSpecific + : listOptionsRandom; + Ui::AddSubsectionTitle( + listOptions, + tr::lng_giveaway_duration_title( + lt_count, + rpl::single(usersCount) | tr::to_count()), + st::giveawayGiftCodeChannelsSubsectionPadding); + Ui::Premium::AddGiftOptions( + listOptions, + durationGroup, + state->apiOptions.options(usersCount), + st::giveawayGiftCodeGiftOption, + true); + + Ui::AddSkip(listOptions); + + auto termsContainer = object_ptr(listOptions); + addTerms(termsContainer.data()); + listOptions->add(object_ptr( + listOptions, + std::move(termsContainer), + st::defaultBoxDividerLabelPadding)); + + Ui::AddSkip(listOptions); + + box->verticalLayout()->resizeToWidth(box->width()); + }; + if (!prepaid) { + rpl::combine( + state->sliderValue.value(), + state->typeValue.value() + ) | rpl::start_with_next([=](int users, GiveawayType type) { + typeGroup->setValue(type); + rebuildListOptions(type, (type == GiveawayType::SpecificUsers) + ? state->selectedToAward.size() + : users); + }, box->lifetime()); + } else { + typeGroup->setValue(GiveawayType::Random); + } + + { + const auto additionalWrap = randomWrap->entity()->add( + object_ptr(randomWrap)); + const auto additionalToggle = additionalWrap->add( + object_ptr( + additionalWrap, + tr::lng_giveaway_additional_prizes(), + st::defaultSettingsButton)); + const auto additionalInner = additionalWrap->add( + object_ptr>( + additionalWrap, + object_ptr( + additionalWrap, + st::giveawayGiftCodeAdditionalField, + Ui::InputField::Mode::SingleLine, + tr::lng_giveaway_additional_prizes_ph()), + st::giveawayGiftCodeAdditionalPaddingMin)); + const auto additionalPadded = additionalInner->wrapped(); + const auto additional = additionalInner->entity(); + additionalInner->hide(anim::type::instant); + additional->setMaxLength(kAdditionalPrizeLengthMax); + const auto fillAdditionalPrizeValue = [=] { + state->additionalPrize = additional->getLastText().trimmed(); + }; + additionalToggle->toggleOn(rpl::single(false))->toggledChanges( + ) | rpl::start_with_next([=](bool toggled) { + if (!toggled && Ui::InFocusChain(additional)) { + additionalWrap->setFocus(); + state->additionalPrize = QString(); + } + additionalInner->toggle(toggled, anim::type::normal); + if (toggled) { + additional->setFocusFast(); + fillAdditionalPrizeValue(); + } + }, additionalInner->lifetime()); + additionalInner->finishAnimating(); + + additional->changes() | rpl::filter([=] { + return additionalInner->toggled(); + }) | rpl::start_with_next( + fillAdditionalPrizeValue, + additional->lifetime()); + + Ui::AddSkip(additionalWrap); + + auto monthsValue = prepaid + ? (rpl::single(prepaid->months) | rpl::type_erased()) + : state->chosenMonths.value(); + const auto usersCountByType = [=](GiveawayType type) { + if (type != GiveawayType::SpecificUsers) { + return state->sliderValue.value() | rpl::type_erased(); + } + return state->toAwardAmountChanged.events_starting_with_copy( + rpl::empty + ) | rpl::map([=] { + return int(state->selectedToAward.size()); + }) | rpl::type_erased(); + }; + auto usersCountValue = prepaid + ? (rpl::single(prepaid->quantity) | rpl::type_erased()) + : state->typeValue.value( + ) | rpl::map(usersCountByType) | rpl::flatten_latest(); + + const auto additionalLabel = Ui::CreateChild( + additionalInner, + rpl::duplicate(usersCountValue) | rpl::map([](int count) { + return QString::number(count); + }), + st::giveawayGiftCodeAdditionalLabel); + additionalLabel->widthValue() | rpl::start_with_next([=](int width) { + const auto min = st::giveawayGiftCodeAdditionalPaddingMin; + const auto skip = st::giveawayGiftCodeAdditionalLabelSkip; + const auto added = std::max(width + skip - min.left(), 0); + const auto &field = st::giveawayGiftCodeAdditionalField; + const auto top = field.textMargins.top(); + additionalLabel->moveToLeft(min.right(), min.top() + top); + additionalPadded->setPadding(min + QMargins(added, 0, 0, 0)); + }, additionalLabel->lifetime()); + + auto additionalAbout = rpl::combine( + state->additionalPrize.value(), + std::move(monthsValue), + std::move(usersCountValue) + ) | rpl::map([=](QString prize, int months, int users) { + const auto duration = ((months >= 12) + ? tr::lng_premium_gift_duration_years + : tr::lng_premium_gift_duration_months)( + tr::now, + lt_count, + (months >= 12) ? (months / 12) : months); + if (prize.isEmpty()) { + return tr::lng_giveaway_prizes_just_premium( + tr::now, + lt_count, + users, + lt_duration, + TextWithEntities{ duration }, + Ui::Text::RichLangValue); + } + return tr::lng_giveaway_prizes_additional( + tr::now, + lt_count, + users, + lt_prize, + TextWithEntities{ prize }, + lt_duration, + TextWithEntities{ duration }, + Ui::Text::RichLangValue); + }); + + Ui::AddDividerText(additionalWrap, std::move(additionalAbout)); + Ui::AddSkip(additionalWrap); + } + { const auto dateContainer = randomWrap->entity()->add( object_ptr(randomWrap)); @@ -699,7 +881,7 @@ void CreateGiveawayBox( .time = state->dateValue.current(), .max = [=] { return QDateTime::currentSecsSinceEpoch() - + state->apiOptions.giveawayPeriodMax();; + + state->apiOptions.giveawayPeriodMax(); }, }); })); @@ -721,6 +903,7 @@ void CreateGiveawayBox( dateContainer, std::move(terms), st::defaultBoxDividerLabelPadding)); + Ui::AddSkip(dateContainer); } else { Ui::AddDividerText( dateContainer, @@ -731,53 +914,24 @@ void CreateGiveawayBox( } } - const auto durationGroup = std::make_shared(0); - const auto listOptions = contentWrap->entity()->add( - object_ptr(box)); - const auto rebuildListOptions = [=](int amountUsers) { - if (prepaid) { - return; - } - while (listOptions->count()) { - delete listOptions->widgetAt(0); - } - Ui::AddSubsectionTitle( - listOptions, - tr::lng_giveaway_duration_title( - lt_count, - rpl::single(amountUsers) | tr::to_count()), - st::giveawayGiftCodeChannelsSubsectionPadding); - Ui::Premium::AddGiftOptions( - listOptions, - durationGroup, - state->apiOptions.options(amountUsers), - st::giveawayGiftCodeGiftOption, - true); - - Ui::AddSkip(listOptions); - - auto termsContainer = object_ptr(listOptions); - addTerms(termsContainer.data()); - listOptions->add(object_ptr( - listOptions, - std::move(termsContainer), - st::defaultBoxDividerLabelPadding)); + { + const auto winnersWrap = randomWrap->entity()->add( + object_ptr(randomWrap)); + const auto winnersToggle = winnersWrap->add( + object_ptr( + winnersWrap, + tr::lng_giveaway_show_winners(), + st::defaultSettingsButton)); + state->showWinners = winnersToggle->toggleOn( + rpl::single(false) + )->toggledValue(); + Ui::AddSkip(winnersWrap); - box->verticalLayout()->resizeToWidth(box->width()); - }; - if (!prepaid) { - rpl::combine( - state->sliderValue.value(), - state->typeValue.value() - ) | rpl::start_with_next([=](int users, GiveawayType type) { - typeGroup->setValue(type); - rebuildListOptions((type == GiveawayType::SpecificUsers) - ? state->selectedToAward.size() - : users); - }, box->lifetime()); - } else { - typeGroup->setValue(GiveawayType::Random); + Ui::AddDividerText( + winnersWrap, + tr::lng_giveaway_show_winners_about()); } + { using namespace Info::Statistics; const auto &stButton = st::startGiveawayBox; @@ -804,13 +958,7 @@ void CreateGiveawayBox( const auto loadingAnimation = InfiniteRadialAnimationWidget( button, st::giveawayGiftCodeStartButton.height / 2); - button->sizeValue( - ) | rpl::start_with_next([=](const QSize &s) { - const auto size = loadingAnimation->size(); - loadingAnimation->moveToLeft( - (s.width() - size.width()) / 2, - (s.height() - size.height()) / 2); - }, loadingAnimation->lifetime()); + AddChildToWidgetCenter(button.data(), loadingAnimation); loadingAnimation->showOn(state->confirmButtonBusy.value()); } @@ -862,9 +1010,11 @@ void CreateGiveawayBox( return not_null{ p->asChannel() }; }) | ranges::to_vector, .countries = state->countriesValue.current(), + .additionalPrize = state->additionalPrize.current(), .untilDate = state->dateValue.current(), .onlyNewSubscribers = (membersGroup->value() == GiveawayType::OnlyNewMembers), + .showWinners = state->showWinners.current(), }; } state->confirmButtonBusy = true; @@ -960,7 +1110,10 @@ void CreateGiveawayBox( loading->toggle(false, anim::type::instant); state->confirmButtonBusy = false; fillSliderContainer(); - rebuildListOptions(1); + if (!prepaid) { + state->chosenMonths = state->apiOptions.monthsFromPreset(0); + } + rebuildListOptions(state->typeValue.current(), 1); contentWrap->toggle(true, anim::type::instant); contentWrap->resizeToWidth(box->width()); }; diff --git a/Telegram/SourceFiles/info/boosts/giveaway/boost_badge.cpp b/Telegram/SourceFiles/info/boosts/giveaway/boost_badge.cpp index 13e8fb1923be5..f6cc1f548feab 100644 --- a/Telegram/SourceFiles/info/boosts/giveaway/boost_badge.cpp +++ b/Telegram/SourceFiles/info/boosts/giveaway/boost_badge.cpp @@ -52,6 +52,18 @@ not_null InfiniteRadialAnimationWidget( return Ui::CreateChild(parent.get(), size); } +void AddChildToWidgetCenter( + not_null parent, + not_null child) { + parent->sizeValue( + ) | rpl::start_with_next([=](const QSize &s) { + const auto size = child->size(); + child->moveToLeft( + (s.width() - size.width()) / 2, + (s.height() - size.height()) / 2); + }, child->lifetime()); +} + QImage CreateBadge( const style::TextStyle &textStyle, const QString &text, diff --git a/Telegram/SourceFiles/info/boosts/giveaway/boost_badge.h b/Telegram/SourceFiles/info/boosts/giveaway/boost_badge.h index fe2427e9c773e..221f6017f1fb8 100644 --- a/Telegram/SourceFiles/info/boosts/giveaway/boost_badge.h +++ b/Telegram/SourceFiles/info/boosts/giveaway/boost_badge.h @@ -32,6 +32,10 @@ namespace Info::Statistics { not_null parent, int size); +void AddChildToWidgetCenter( + not_null parent, + not_null child); + void AddLabelWithBadgeToButton( not_null parent, rpl::producer text, diff --git a/Telegram/SourceFiles/info/boosts/giveaway/giveaway.style b/Telegram/SourceFiles/info/boosts/giveaway/giveaway.style index 4f04fdabb393d..2368a616feaa5 100644 --- a/Telegram/SourceFiles/info/boosts/giveaway/giveaway.style +++ b/Telegram/SourceFiles/info/boosts/giveaway/giveaway.style @@ -121,6 +121,13 @@ giveawayGiftCodeTypeDividerPadding: margins(0px, 7px, 0px, 5px); giveawayGiftCodeSliderPadding: margins(0px, 24px, 0px, 10px); giveawayGiftCodeSliderFloatSkip: 6px; giveawayGiftCodeChannelsSubsectionPadding: margins(0px, -1px, 0px, -4px); +giveawayGiftCodeAdditionalPaddingMin: margins(50px, 4px, 22px, 0px); +giveawayGiftCodeAdditionalField: InputField(defaultMultiSelectSearchField) { +} +giveawayGiftCodeAdditionalLabel: FlatLabel(defaultFlatLabel) { + style: semiboldTextStyle; +} +giveawayGiftCodeAdditionalLabelSkip: 12px; giveawayGiftCodeChannelsPeerList: PeerList(boostsListBox) { padding: margins(0px, 7px, 0px, 0px); diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style index 2be1c96501568..c709a701414b4 100644 --- a/Telegram/SourceFiles/info/info.style +++ b/Telegram/SourceFiles/info/info.style @@ -33,6 +33,10 @@ InfoTopBar { back: IconButton; title: FlatLabel; titlePosition: point; + titleWithSubtitle: FlatLabel; + titleWithSubtitlePosition: point; + subtitle: FlatLabel; + subtitlePosition: point; bg: color; mediaCancel: IconButton; mediaActionsSkip: pixels; @@ -186,6 +190,14 @@ infoTopBar: InfoTopBar { back: infoTopBarBack; title: infoTopBarTitle; titlePosition: point(24px, 17px); + titleWithSubtitle: FlatLabel(infoTopBarTitle) { + style: semiboldTextStyle; + } + titleWithSubtitlePosition: point(16px, 8px); + subtitle: FlatLabel(defaultFlatLabel) { + textFg: windowSubTextFg; + } + subtitlePosition: point(16px, 28px); bg: windowBg; mediaCancel: infoTopBarMediaCancel; mediaActionsSkip: 4px; @@ -261,6 +273,8 @@ infoLayerTopBar: InfoTopBar(infoTopBar) { back: infoLayerTopBarBack; title: boxTitle; titlePosition: point(24px, 17px); + titleWithSubtitlePosition: point(16px, 9px); + subtitlePosition: point(16px, 30px); bg: boxBg; mediaCancel: infoLayerTopBarMediaCancel; mediaActionsSkip: 6px; @@ -398,12 +412,14 @@ infoIconMediaGroup: icon {{ "info/info_common_groups", infoIconFg }}; infoIconMediaChannel: icon {{ "menu/channel", infoIconFg, point(4px, 4px) }}; infoIconMediaVoice: icon {{ "info/info_media_voice", infoIconFg }}; infoIconMediaStories: icon {{ "info/info_media_stories", infoIconFg }}; +infoIconMediaSaved: icon {{ "info/info_media_saved", infoIconFg }}; infoIconMediaStoriesArchive: icon {{ "info/info_stories_archive", infoIconFg }}; infoIconMediaStoriesRecent: icon {{ "info/info_stories_recent", infoIconFg }}; infoIconShare: icon {{ "info/info_share", infoIconFg }}; infoIconEdit: icon {{ "info/info_edit", infoIconFg }}; infoIconDelete: icon {{ "info/info_delete", infoIconFg }}; +infoIconDeleteRed: icon {{ "info/info_delete", attentionButtonFg }}; infoIconReport: icon {{ "info/info_report", attentionButtonFg }}; infoIconLeave: icon {{ "info/info_leave", infoIconFg }}; infoIconBlock: icon {{ "info/info_block", attentionButtonFg }}; diff --git a/Telegram/SourceFiles/info/info_content_widget.h b/Telegram/SourceFiles/info/info_content_widget.h index 5d53efee53a2d..a7782d4f9ae24 100644 --- a/Telegram/SourceFiles/info/info_content_widget.h +++ b/Telegram/SourceFiles/info/info_content_widget.h @@ -96,6 +96,9 @@ class ContentWidget : public Ui::RpWidget { } [[nodiscard]] virtual rpl::producer title() = 0; + [[nodiscard]] virtual rpl::producer subtitle() { + return nullptr; + } [[nodiscard]] virtual auto titleStories() -> rpl::producer; diff --git a/Telegram/SourceFiles/info/info_controller.h b/Telegram/SourceFiles/info/info_controller.h index 046635020ed2e..bb5eca6bf9f85 100644 --- a/Telegram/SourceFiles/info/info_controller.h +++ b/Telegram/SourceFiles/info/info_controller.h @@ -126,6 +126,7 @@ class Section final { Media, CommonGroups, SimilarChannels, + SavedSublists, Members, Settings, Downloads, diff --git a/Telegram/SourceFiles/info/info_memento.cpp b/Telegram/SourceFiles/info/info_memento.cpp index b2f78b029a007..ec4eb246e7a49 100644 --- a/Telegram/SourceFiles/info/info_memento.cpp +++ b/Telegram/SourceFiles/info/info_memento.cpp @@ -11,6 +11,7 @@ For license and copyright information please follow this link: #include "info/media/info_media_widget.h" #include "info/members/info_members_widget.h" #include "info/common_groups/info_common_groups_widget.h" +#include "info/saved/info_saved_sublists_widget.h" #include "info/settings/info_settings_widget.h" #include "info/similar_channels/info_similar_channels_widget.h" #include "info/polls/info_polls_results_widget.h" @@ -111,7 +112,9 @@ std::vector> Memento::DefaultStack( } Section Memento::DefaultSection(not_null peer) { - if (peer->sharedMediaInfo()) { + if (peer->savedSublistsInfo()) { + return Section(Section::Type::SavedSublists); + } else if (peer->sharedMediaInfo()) { return Section(Section::MediaType::Photo); } return Section(Section::Type::Profile); @@ -145,6 +148,8 @@ std::shared_ptr Memento::DefaultContent( case Section::Type::SimilarChannels: return std::make_shared( peer->asChannel()); + case Section::Type::SavedSublists: + return std::make_shared(&peer->session()); case Section::Type::Members: return std::make_shared( peer, diff --git a/Telegram/SourceFiles/info/info_top_bar.cpp b/Telegram/SourceFiles/info/info_top_bar.cpp index 04985c6179ce5..6c91d38ca2930 100644 --- a/Telegram/SourceFiles/info/info_top_bar.cpp +++ b/Telegram/SourceFiles/info/info_top_bar.cpp @@ -82,13 +82,36 @@ void TopBar::registerToggleControlCallback( }); } -void TopBar::setTitle(rpl::producer &&title) { +void TopBar::setTitle(TitleDescriptor descriptor) { if (_title) { delete _title; } + if (_subtitle) { + delete _subtitle; + } + const auto withSubtitle = !!descriptor.subtitle; + if (withSubtitle) { + _subtitle = Ui::CreateChild>( + this, + object_ptr( + this, + std::move(descriptor.subtitle), + _st.subtitle), + st::infoTopBarScale); + _subtitle->setDuration(st::infoTopBarDuration); + _subtitle->toggle( + !selectionMode() && !storiesTitle(), + anim::type::instant); + registerToggleControlCallback(_subtitle.data(), [=] { + return !selectionMode() && !storiesTitle() && !searchMode(); + }); + } _title = Ui::CreateChild>( this, - object_ptr(this, std::move(title), _st.title), + object_ptr( + this, + std::move(descriptor.title), + withSubtitle ? _st.titleWithSubtitle : _st.title), st::infoTopBarScale); _title->setDuration(st::infoTopBarDuration); _title->toggle( @@ -100,6 +123,9 @@ void TopBar::setTitle(rpl::producer &&title) { if (_back) { _title->setAttribute(Qt::WA_TransparentForMouseEvents); + if (_subtitle) { + _subtitle->setAttribute(Qt::WA_TransparentForMouseEvents); + } } updateControlsGeometry(width()); } @@ -124,6 +150,9 @@ void TopBar::enableBackButton() { if (_title) { _title->setAttribute(Qt::WA_TransparentForMouseEvents); } + if (_subtitle) { + _subtitle->setAttribute(Qt::WA_TransparentForMouseEvents); + } if (_storiesWrap) { _storiesWrap->raise(); } @@ -339,10 +368,21 @@ void TopBar::updateDefaultControlsGeometry(int newWidth) { newWidth); } if (_title) { - _title->moveToLeft( - _back ? _st.back.width : _st.titlePosition.x(), - _st.titlePosition.y(), - newWidth); + const auto x = _back + ? _st.back.width + : _subtitle + ? _st.titleWithSubtitlePosition.x() + : _st.titlePosition.x(); + const auto y = _subtitle + ? _st.titleWithSubtitlePosition.y() + : _st.titlePosition.y(); + _title->moveToLeft(x, y, newWidth); + if (_subtitle) { + _subtitle->moveToLeft( + _back ? _st.back.width : _st.subtitlePosition.x(), + _st.subtitlePosition.y(), + newWidth); + } } } diff --git a/Telegram/SourceFiles/info/info_top_bar.h b/Telegram/SourceFiles/info/info_top_bar.h index 4961669549eb0..9e6ad80a8f70d 100644 --- a/Telegram/SourceFiles/info/info_top_bar.h +++ b/Telegram/SourceFiles/info/info_top_bar.h @@ -41,6 +41,11 @@ namespace Info { class Key; class Section; +struct TitleDescriptor { + rpl::producer title; + rpl::producer subtitle; +}; + class TopBar : public Ui::RpWidget { public: TopBar( @@ -56,7 +61,7 @@ class TopBar : public Ui::RpWidget { return _storyClicks.events(); } - void setTitle(rpl::producer &&title); + void setTitle(TitleDescriptor descriptor); void setStories(rpl::producer content); void setStoriesArchive(bool archive); void enableBackButton(); @@ -155,6 +160,7 @@ class TopBar : public Ui::RpWidget { QPointer> _back; std::vector> _buttons; QPointer> _title; + QPointer> _subtitle; bool _searchModeEnabled = false; bool _searchModeAvailable = false; diff --git a/Telegram/SourceFiles/info/info_wrap_widget.cpp b/Telegram/SourceFiles/info/info_wrap_widget.cpp index 02d1f75e160d9..32bd69c7a5db1 100644 --- a/Telegram/SourceFiles/info/info_wrap_widget.cpp +++ b/Telegram/SourceFiles/info/info_wrap_widget.cpp @@ -188,23 +188,32 @@ void WrapWidget::injectActivePeerProfile(not_null peer) { ? _historyStack.front().section->section().type() : _controller->section().type(); const auto firstSectionMediaType = [&] { - if (firstSectionType == Section::Type::Profile) { + if (firstSectionType == Section::Type::Profile + || firstSectionType == Section::Type::SavedSublists) { return Section::MediaType::kCount; } return hasStackHistory() ? _historyStack.front().section->section().mediaType() : _controller->section().mediaType(); }(); - const auto expectedType = peer->sharedMediaInfo() + const auto savedSublistsInfo = peer->savedSublistsInfo(); + const auto sharedMediaInfo = peer->sharedMediaInfo(); + const auto expectedType = savedSublistsInfo + ? Section::Type::SavedSublists + : sharedMediaInfo ? Section::Type::Media : Section::Type::Profile; - const auto expectedMediaType = peer->sharedMediaInfo() + const auto expectedMediaType = savedSublistsInfo + ? Section::MediaType::kCount + : sharedMediaInfo ? Section::MediaType::Photo : Section::MediaType::kCount; if (firstSectionType != expectedType || firstSectionMediaType != expectedMediaType || firstPeer != peer) { - auto section = peer->sharedMediaInfo() + auto section = savedSublistsInfo + ? Section(Section::Type::SavedSublists) + : sharedMediaInfo ? Section(Section::MediaType::Photo) : Section(Section::Type::Profile); injectActiveProfileMemento(std::move( @@ -545,6 +554,8 @@ void WrapWidget::removeFromStack(const std::vector
§ions) { const auto &s = item.section->section(); if (s.type() != section.type()) { return false; + } else if (s.type() == Section::Type::SavedSublists) { + return true; } else if (s.type() == Section::Type::Media) { return (s.mediaType() == section.mediaType()); } else if (s.type() == Section::Type::Settings) { @@ -586,7 +597,10 @@ void WrapWidget::finishShowContent() { updateContentGeometry(); _content->setIsStackBottom(!hasStackHistory()); if (_topBar) { - _topBar->setTitle(_content->title()); + _topBar->setTitle({ + .title = _content->title(), + .subtitle = _content->subtitle(), + }); _topBar->setStories(_content->titleStories()); _topBar->setStoriesArchive( _controller->key().storiesTab() == Stories::Tab::Archive); diff --git a/Telegram/SourceFiles/info/media/info_media_buttons.h b/Telegram/SourceFiles/info/media/info_media_buttons.h index 527f7a4db466d..1331ae07d614c 100644 --- a/Telegram/SourceFiles/info/media/info_media_buttons.h +++ b/Telegram/SourceFiles/info/media/info_media_buttons.h @@ -10,8 +10,11 @@ For license and copyright information please follow this link: #include #include #include "lang/lang_keys.h" +#include "data/data_saved_messages.h" +#include "data/data_session.h" #include "data/data_stories_ids.h" #include "storage/storage_shared_media.h" +#include "history/view/history_view_sublist_section.h" #include "info/info_memento.h" #include "info/info_controller.h" #include "info/profile/info_profile_values.h" @@ -126,7 +129,7 @@ inline auto AddCommonGroupsButton( Section::Type::CommonGroups)); }); return result; -}; +} inline auto AddSimilarChannelsButton( Ui::VerticalLayout *parent, @@ -150,7 +153,7 @@ inline auto AddSimilarChannelsButton( Section::Type::SimilarChannels)); }); return result; -}; +} inline auto AddStoriesButton( Ui::VerticalLayout *parent, @@ -178,6 +181,26 @@ inline auto AddStoriesButton( navigation->showSection(Info::Stories::Make(peer)); }); return result; -}; +} + +inline auto AddSavedSublistButton( + Ui::VerticalLayout *parent, + not_null navigation, + not_null peer, + Ui::MultiSlideTracker &tracker) { + auto result = AddCountedButton( + parent, + Profile::SavedSublistCountValue(peer), + [](int count) { + return tr::lng_profile_saved_messages(tr::now, lt_count, count); + }, + tracker)->entity(); + result->addClickHandler([=] { + navigation->showSection( + std::make_shared( + peer->owner().savedMessages().sublist(peer))); + }); + return result; +} } // namespace Info::Media diff --git a/Telegram/SourceFiles/info/media/info_media_inner_widget.cpp b/Telegram/SourceFiles/info/media/info_media_inner_widget.cpp index dce97363cb146..ee9eb4dfd2223 100644 --- a/Telegram/SourceFiles/info/media/info_media_inner_widget.cpp +++ b/Telegram/SourceFiles/info/media/info_media_inner_widget.cpp @@ -43,7 +43,7 @@ InnerWidget::InnerWidget( // Allows showing additional shared media links and tabs. // Used for shared media in Saved Messages. void InnerWidget::setupOtherTypes() { - if (_controller->key().peer()->isSelf() && _isStackBottom) { + if (_controller->key().peer()->sharedMediaInfo() && _isStackBottom) { createOtherTypes(); } else { _otherTypes.destroy(); diff --git a/Telegram/SourceFiles/info/profile/info_profile_badge.cpp b/Telegram/SourceFiles/info/profile/info_profile_badge.cpp index 1cbcef23ee89e..214c6e1cc8609 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_badge.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_badge.cpp @@ -9,6 +9,7 @@ For license and copyright information please follow this link: #include "data/data_peer.h" #include "data/data_session.h" +#include "data/data_user.h" #include "data/stickers/data_custom_emoji.h" #include "info/profile/info_profile_values.h" #include "info/profile/info_profile_emoji_status_panel.h" @@ -24,10 +25,16 @@ namespace { [[nodiscard]] rpl::producer ContentForPeer( not_null peer) { + const auto statusOnlyForPremium = peer->isUser(); return rpl::combine( BadgeValue(peer), EmojiStatusIdValue(peer) ) | rpl::map([=](BadgeType badge, DocumentId emojiStatusId) { + if (statusOnlyForPremium && badge != BadgeType::Premium) { + emojiStatusId = 0; + } else if (emojiStatusId && badge == BadgeType::None) { + badge = BadgeType::Premium; + } return Badge::Content{ badge, emojiStatusId }; }); } @@ -91,9 +98,6 @@ void Badge::setContent(Content content) { if (!(_allowed & content.badge)) { content.badge = BadgeType::None; } - if (content.badge != BadgeType::Premium) { - content.emojiStatusId = 0; - } if (_content == content) { return; } diff --git a/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.cpp b/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.cpp index db8714c7051a9..8881dbc99c6aa 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.cpp @@ -72,6 +72,7 @@ void EmojiStatusPanel::show( .controller = controller, .button = button, .animationSizeTag = animationSizeTag, + .ensureAddedEmojiId = controller->session().user()->emojiStatusId(), }); } @@ -99,28 +100,41 @@ void EmojiStatusPanel::show(Descriptor &&descriptor) { } _panelButton = button; _animationSizeTag = descriptor.animationSizeTag; - auto list = std::vector(); + const auto feed = [=, now = descriptor.ensureAddedEmojiId]( + std::vector list) { + list.insert(begin(list), 0); + if (now && !ranges::contains(list, now)) { + list.push_back(now); + } + _panel->selector()->provideRecentEmoji(list); + }; if (descriptor.backgroundEmojiMode) { controller->session().api().peerPhoto().emojiListValue( Api::PeerPhoto::EmojiListType::Background ) | rpl::start_with_next([=](std::vector &&list) { - list.insert(begin(list), 0); - if (const auto now = descriptor.currentBackgroundEmojiId) { - if (!ranges::contains(list, now)) { - list.push_back(now); - } - } - _panel->selector()->provideRecentEmoji(list); + feed(std::move(list)); }, _panel->lifetime()); + } else if (descriptor.channelStatusMode) { + const auto &statuses = controller->session().data().emojiStatuses(); + const auto &other = statuses.list(Data::EmojiStatuses::Type::ChannelDefault); + auto list = statuses.list(Data::EmojiStatuses::Type::ChannelColored); + if (list.size() > kLimitFirstRow - 1) { + list.erase(begin(list) + kLimitFirstRow - 1, end(list)); + } + list.reserve(list.size() + other.size() + 1); + for (const auto &id : other) { + if (!ranges::contains(list, id)) { + list.push_back(id); + } + } + feed(std::move(list)); } else { - const auto self = controller->session().user(); const auto &statuses = controller->session().data().emojiStatuses(); const auto &recent = statuses.list(Data::EmojiStatuses::Type::Recent); const auto &other = statuses.list(Data::EmojiStatuses::Type::Default); auto list = statuses.list(Data::EmojiStatuses::Type::Colored); - list.insert(begin(list), 0); - if (list.size() > kLimitFirstRow) { - list.erase(begin(list) + kLimitFirstRow, end(list)); + if (list.size() > kLimitFirstRow - 1) { + list.erase(begin(list) + kLimitFirstRow - 1, end(list)); } list.reserve(list.size() + recent.size() + other.size() + 1); for (const auto &id : ranges::views::concat(recent, other)) { @@ -128,15 +142,12 @@ void EmojiStatusPanel::show(Descriptor &&descriptor) { list.push_back(id); } } - if (!ranges::contains(list, self->emojiStatusId())) { - list.push_back(self->emojiStatusId()); - } - _panel->selector()->provideRecentEmoji(list); + feed(std::move(list)); } const auto parent = _panel->parentWidget(); const auto global = button->mapToGlobal(QPoint()); const auto local = parent->mapFromGlobal(global); - if (descriptor.backgroundEmojiMode) { + if (descriptor.backgroundEmojiMode || descriptor.channelStatusMode) { _panel->moveBottomRight( local.y() + (st::normalFont->height / 2), local.x() + button->width() * 3); @@ -175,18 +186,22 @@ void EmojiStatusPanel::create(const Descriptor &descriptor) { nullptr, Descriptor{ .show = controller->uiShow(), - .st = (descriptor.backgroundEmojiMode + .st = ((descriptor.backgroundEmojiMode + || descriptor.channelStatusMode) ? st::backgroundEmojiPan : st::statusEmojiPan), .level = Window::GifPauseReason::Layer, .mode = (descriptor.backgroundEmojiMode ? Mode::BackgroundEmoji + : descriptor.channelStatusMode + ? Mode::ChannelStatus : Mode::EmojiStatus), .customTextColor = descriptor.customTextColor, })); _customTextColor = descriptor.customTextColor; _backgroundEmojiMode = descriptor.backgroundEmojiMode; - _panel->setDropDown(!_backgroundEmojiMode); + _channelStatusMode = descriptor.channelStatusMode; + _panel->setDropDown(!_backgroundEmojiMode && !_channelStatusMode); _panel->setDesiredHeightValues( 1., st::emojiPanMinHeight / 2, @@ -218,14 +233,14 @@ void EmojiStatusPanel::create(const Descriptor &descriptor) { return Chosen{ .animation = data.messageSendingFrom }; }); - if (descriptor.backgroundEmojiMode) { + if (descriptor.backgroundEmojiMode || descriptor.channelStatusMode) { rpl::merge( std::move(statusChosen), std::move(emojiChosen) ) | rpl::start_with_next([=](const Chosen &chosen) { const auto owner = &controller->session().data(); startAnimation(owner, body, chosen.id, chosen.animation); - _backgroundEmojiChosen.fire_copy(chosen.id); + _someCustomChosen.fire({ chosen.id, chosen.until }); _panel->hideAnimated(); }, _panel->lifetime()); } else { diff --git a/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.h b/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.h index 246b171fe040a..a373c904c9457 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.h +++ b/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.h @@ -51,15 +51,20 @@ class EmojiStatusPanel final { not_null controller; not_null button; Data::CustomEmojiSizeTag animationSizeTag = {}; - DocumentId currentBackgroundEmojiId = 0; + DocumentId ensureAddedEmojiId = 0; Fn customTextColor; bool backgroundEmojiMode = false; + bool channelStatusMode = false; }; void show(Descriptor &&descriptor); void repaint(); - [[nodiscard]] rpl::producer backgroundEmojiChosen() const { - return _backgroundEmojiChosen.events(); + struct CustomChosen { + DocumentId id = 0; + TimeId until = 0; + }; + [[nodiscard]] rpl::producer someCustomChosen() const { + return _someCustomChosen.events(); } bool paintBadgeFrame(not_null widget); @@ -81,9 +86,10 @@ class EmojiStatusPanel final { Fn _chooseFilter; QPointer _panelButton; std::unique_ptr _animation; - rpl::event_stream _backgroundEmojiChosen; + rpl::event_stream _someCustomChosen; Data::CustomEmojiSizeTag _animationSizeTag = {}; bool _backgroundEmojiMode = false; + bool _channelStatusMode = false; }; diff --git a/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp b/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp index e6056857f8df5..4cca8c165fedd 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp @@ -215,8 +215,22 @@ object_ptr InnerWidget::setupSharedMedia( icon, st::infoSharedMediaButtonIconPosition); }; + auto addSavedSublistButton = [&]( + not_null peer, + const style::icon &icon) { + auto result = Media::AddSavedSublistButton( + content, + _controller, + peer, + tracker); + object_ptr( + result, + icon, + st::infoSharedMediaButtonIconPosition); + }; addStoriesButton(_peer, st::infoIconMediaStories); + addSavedSublistButton(_peer, st::infoIconMediaSaved); addMediaButton(MediaType::Photo, st::infoIconMediaPhoto); addMediaButton(MediaType::Video, st::infoIconMediaVideo); addMediaButton(MediaType::File, st::infoIconMediaFile); diff --git a/Telegram/SourceFiles/info/profile/info_profile_values.cpp b/Telegram/SourceFiles/info/profile/info_profile_values.cpp index e5dc793793d95..6c7b4ac2b0c09 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_values.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_values.cpp @@ -20,6 +20,8 @@ For license and copyright information please follow this link: #include "lang/lang_keys.h" #include "data/notify/data_notify_settings.h" #include "data/data_peer_values.h" +#include "data/data_saved_messages.h" +#include "data/data_saved_sublist.h" #include "data/data_shared_media.h" #include "data/data_message_reactions.h" #include "data/data_folder.h" @@ -206,7 +208,7 @@ TextWithEntities AboutWithEntities( const auto used = (!user || isPremium || value.size() <= limit) ? value : value.mid(0, limit) + "..."; - auto result = TextWithEntities{ value }; + auto result = TextWithEntities{ used }; TextUtilities::ParseEntities(result, flags); if (stripExternal) { StripExternalLinks(result); @@ -536,6 +538,17 @@ rpl::producer SimilarChannelsCountValue( }); } +rpl::producer SavedSublistCountValue( + not_null peer) { + const auto saved = &peer->owner().savedMessages(); + const auto sublist = saved->sublist(peer); + if (!sublist->fullCount()) { + saved->loadMore(sublist); + return rpl::single(0) | rpl::then(sublist->fullCountValue()); + } + return sublist->fullCountValue(); +} + rpl::producer CanAddMemberValue(not_null peer) { if (const auto chat = peer->asChat()) { return peer->session().changes().peerFlagsValue( @@ -609,15 +622,14 @@ rpl::producer BadgeValue(not_null peer) { } rpl::producer EmojiStatusIdValue(not_null peer) { - if (const auto user = peer->asUser()) { - return user->session().changes().peerFlagsValue( - peer, - Data::PeerUpdate::Flag::EmojiStatus - ) | rpl::map([=] { return user->emojiStatusId(); }); + if (peer->isChat()) { + return rpl::single(DocumentId(0)); } - return rpl::single(DocumentId(0)); + return peer->session().changes().peerFlagsValue( + peer, + Data::PeerUpdate::Flag::EmojiStatus + ) | rpl::map([=] { return peer->emojiStatusId(); }); } - } // namespace Profile } // namespace Info diff --git a/Telegram/SourceFiles/info/profile/info_profile_values.h b/Telegram/SourceFiles/info/profile/info_profile_values.h index f669a453585e8..a74f22a42519a 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_values.h +++ b/Telegram/SourceFiles/info/profile/info_profile_values.h @@ -104,6 +104,8 @@ rpl::producer> MigratedOrMeValue( not_null user); [[nodiscard]] rpl::producer SimilarChannelsCountValue( not_null channel); +[[nodiscard]] rpl::producer SavedSublistCountValue( + not_null peer); [[nodiscard]] rpl::producer CanAddMemberValue( not_null peer); [[nodiscard]] rpl::producer FullReactionsCountValue( diff --git a/Telegram/SourceFiles/info/saved/info_saved_sublists_widget.cpp b/Telegram/SourceFiles/info/saved/info_saved_sublists_widget.cpp new file mode 100644 index 0000000000000..db39909636b75 --- /dev/null +++ b/Telegram/SourceFiles/info/saved/info_saved_sublists_widget.cpp @@ -0,0 +1,183 @@ +/* +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 "info/saved/info_saved_sublists_widget.h" +// +#include "data/data_saved_messages.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "dialogs/dialogs_inner_widget.h" +#include "history/view/history_view_sublist_section.h" +#include "info/media/info_media_buttons.h" +#include "info/profile/info_profile_icon.h" +#include "info/info_controller.h" +#include "info/info_memento.h" +#include "main/main_session.h" +#include "lang/lang_keys.h" +#include "ui/widgets/box_content_divider.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/wrap/vertical_layout.h" +#include "styles/style_info.h" + +namespace Info::Saved { + +SublistsMemento::SublistsMemento(not_null session) +: ContentMemento(session->user(), nullptr, PeerId()) { +} + +Section SublistsMemento::section() const { + return Section(Section::Type::SavedSublists); +} + +object_ptr SublistsMemento::createWidget( + QWidget *parent, + not_null controller, + const QRect &geometry) { + auto result = object_ptr(parent, controller); + result->setInternalState(geometry, this); + return result; +} + +SublistsMemento::~SublistsMemento() = default; + +SublistsWidget::SublistsWidget( + QWidget *parent, + not_null controller) +: ContentWidget(parent, controller) +, _layout(setInnerWidget(object_ptr(this))) { + setupOtherTypes(); + + _list = _layout->add(object_ptr( + this, + controller->parentController(), + rpl::single(Dialogs::InnerWidget::ChildListShown()))); + _list->showSavedSublists(); + _list->setNarrowRatio(0.); + + _list->chosenRow() | rpl::start_with_next([=](Dialogs::ChosenRow row) { + if (const auto sublist = row.key.sublist()) { + controller->showSection( + std::make_shared(sublist), + Window::SectionShow::Way::Forward); + } + }, _list->lifetime()); + + const auto saved = &controller->session().data().savedMessages(); + _list->heightValue() | rpl::start_with_next([=] { + if (!saved->supported()) { + crl::on_main(controller, [=] { + controller->showSection( + Memento::Default(controller->session().user()), + Window::SectionShow::Way::Backward); + }); + } + }, lifetime()); + + _list->setLoadMoreCallback([=] { + saved->loadMore(); + }); +} + +void SublistsWidget::setupOtherTypes() { + auto wrap = _layout->add( + object_ptr>( + _layout, + object_ptr(_layout))); + auto content = wrap->entity(); + content->add(object_ptr( + content, + st::infoProfileSkip)); + + using Type = Media::Type; + auto tracker = Ui::MultiSlideTracker(); + const auto peer = controller()->session().user(); + const auto addMediaButton = [&]( + Type buttonType, + const style::icon &icon) { + auto result = Media::AddButton( + content, + controller(), + peer, + MsgId(), // topicRootId + nullptr, // migrated + buttonType, + tracker); + object_ptr( + result, + icon, + st::infoSharedMediaButtonIconPosition)->show(); + }; + + addMediaButton(Type::Photo, st::infoIconMediaPhoto); + addMediaButton(Type::Video, st::infoIconMediaVideo); + addMediaButton(Type::File, st::infoIconMediaFile); + addMediaButton(Type::MusicFile, st::infoIconMediaAudio); + addMediaButton(Type::Link, st::infoIconMediaLink); + addMediaButton(Type::RoundVoiceFile, st::infoIconMediaVoice); + addMediaButton(Type::GIF, st::infoIconMediaGif); + + content->add(object_ptr( + content, + st::infoProfileSkip)); + wrap->toggleOn(tracker.atLeastOneShownValue()); + wrap->finishAnimating(); + + _layout->add(object_ptr(_layout)); + _layout->add(object_ptr( + content, + st::infoProfileSkip)); +} + +rpl::producer SublistsWidget::title() { + return tr::lng_saved_messages(); +} + +rpl::producer SublistsWidget::subtitle() { + const auto saved = &controller()->session().data().savedMessages(); + return saved->chatsList()->fullSize().value( + ) | rpl::map([=](int value) { + return (value || saved->chatsList()->loaded()) + ? tr::lng_filters_chats_count(tr::now, lt_count, value) + : tr::lng_contacts_loading(tr::now); + }); +} + +bool SublistsWidget::showInternal(not_null memento) { + if (!controller()->validateMementoPeer(memento)) { + return false; + } + if (auto my = dynamic_cast(memento.get())) { + restoreState(my); + return true; + } + return false; +} + +void SublistsWidget::setInternalState( + const QRect &geometry, + not_null memento) { + setGeometry(geometry); + Ui::SendPendingMoveResizeEvents(this); + restoreState(memento); +} + +std::shared_ptr SublistsWidget::doCreateMemento() { + auto result = std::make_shared( + &controller()->session()); + saveState(result.get()); + return result; +} + +void SublistsWidget::saveState(not_null memento) { + memento->setScrollTop(scrollTopSave()); +} + +void SublistsWidget::restoreState(not_null memento) { + scrollTopRestore(memento->scrollTop()); +} + +} // namespace Info::Saved diff --git a/Telegram/SourceFiles/info/saved/info_saved_sublists_widget.h b/Telegram/SourceFiles/info/saved/info_saved_sublists_widget.h new file mode 100644 index 0000000000000..64ada22abd308 --- /dev/null +++ b/Telegram/SourceFiles/info/saved/info_saved_sublists_widget.h @@ -0,0 +1,72 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "info/info_content_widget.h" + +namespace Dialogs { +class InnerWidget; +} // namespace Dialogs + +namespace Main { +class Session; +} // namespace Main + +namespace Ui { +class VerticalLayout; +} // namespace Ui + +namespace Info::Saved { + +class SublistsMemento final : public ContentMemento { +public: + explicit SublistsMemento(not_null session); + + object_ptr createWidget( + QWidget *parent, + not_null controller, + const QRect &geometry) override; + + Section section() const override; + + ~SublistsMemento(); + +private: + +}; + +class SublistsWidget final : public ContentWidget { +public: + SublistsWidget( + QWidget *parent, + not_null controller); + + bool showInternal( + not_null memento) override; + + void setInternalState( + const QRect &geometry, + not_null memento); + + rpl::producer title() override; + rpl::producer subtitle() override; + +private: + void saveState(not_null memento); + void restoreState(not_null memento); + + std::shared_ptr doCreateMemento() override; + + void setupOtherTypes(); + + const not_null _layout; + Dialogs::InnerWidget *_list = nullptr; + +}; + +} // namespace Info::Saved diff --git a/Telegram/SourceFiles/info/similar_channels/info_similar_channels_widget.cpp b/Telegram/SourceFiles/info/similar_channels/info_similar_channels_widget.cpp index 886a7869ed589..cfc911aaf9c11 100644 --- a/Telegram/SourceFiles/info/similar_channels/info_similar_channels_widget.cpp +++ b/Telegram/SourceFiles/info/similar_channels/info_similar_channels_widget.cpp @@ -513,4 +513,3 @@ void Widget::restoreState(not_null memento) { } } // namespace Info::SimilarChannels - diff --git a/Telegram/SourceFiles/info/similar_channels/info_similar_channels_widget.h b/Telegram/SourceFiles/info/similar_channels/info_similar_channels_widget.h index 7c32a855cad11..74f15e64fbc71 100644 --- a/Telegram/SourceFiles/info/similar_channels/info_similar_channels_widget.h +++ b/Telegram/SourceFiles/info/similar_channels/info_similar_channels_widget.h @@ -68,4 +68,3 @@ class Widget final : public ContentWidget { }; } // namespace Info::SimilarChannels - diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_inner_widget.cpp b/Telegram/SourceFiles/info/statistics/info_statistics_inner_widget.cpp index ca77f7ad523c9..4e50b2bf66d5a 100644 --- a/Telegram/SourceFiles/info/statistics/info_statistics_inner_widget.cpp +++ b/Telegram/SourceFiles/info/statistics/info_statistics_inner_widget.cpp @@ -728,7 +728,15 @@ void InnerWidget::fill() { AddPublicForwards( _state.publicForwardsFirstSlice, inner, - [=](FullMsgId id) { _showRequests.fire({ .history = id }); }, + [=](RecentPostId id) { + _showRequests.fire({ + .info = (!id.messageId && !id.storyId) + ? id.messageId.peer + : PeerId(0), + .history = id.messageId, + .story = id.storyId, + }); + }, descriptor.peer, RecentPostId{ .messageId = _contextId, .storyId = _storyId }); } diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_inner_widget.h b/Telegram/SourceFiles/info/statistics/info_statistics_inner_widget.h index ed4e93a9f4523..ada52f65c95e2 100644 --- a/Telegram/SourceFiles/info/statistics/info_statistics_inner_widget.h +++ b/Telegram/SourceFiles/info/statistics/info_statistics_inner_widget.h @@ -33,6 +33,7 @@ class InnerWidget final : public Ui::VerticalLayout { FullMsgId history; FullMsgId messageStatistic; FullStoryId storyStatistic; + FullStoryId story; }; InnerWidget( diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp b/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp index cdaffc1833c5d..1a975b78813b7 100644 --- a/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp +++ b/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp @@ -9,22 +9,25 @@ For license and copyright information please follow this link: #include "api/api_statistics.h" #include "boxes/peer_list_controllers.h" -#include "data/data_boosts.h" #include "data/data_channel.h" #include "data/data_session.h" +#include "data/data_stories.h" #include "data/data_user.h" #include "history/history_item.h" #include "info/boosts/giveaway/boost_badge.h" #include "lang/lang_keys.h" #include "main/main_session.h" +#include "ui/effects/outline_segments.h" // Ui::UnreadStoryOutlineGradient. #include "ui/effects/toggle_arrow.h" -#include "ui/empty_userpic.h" #include "ui/painter.h" #include "ui/rect.h" #include "ui/vertical_list.h" #include "ui/widgets/buttons.h" +#include "ui/widgets/popup_menu.h" #include "ui/wrap/slide_wrap.h" #include "ui/wrap/vertical_layout.h" +#include "styles/style_dialogs.h" // dialogsStoriesFull. +#include "styles/style_menu_icons.h" #include "styles/style_settings.h" #include "styles/style_statistics.h" #include "styles/style_window.h" @@ -93,7 +96,7 @@ void AddSubtitle( struct PublicForwardsDescriptor final { Data::PublicForwardsSlice firstSlice; - Fn showPeerHistory; + Fn requestShow; not_null peer; Data::RecentPostId contextId; }; @@ -110,24 +113,62 @@ struct BoostsDescriptor final { not_null peer; }; -class PeerListRowWithMsgId : public PeerListRow { +class PeerListRowWithFullId : public PeerListRow { public: - using PeerListRow::PeerListRow; + PeerListRowWithFullId( + not_null peer, + Data::RecentPostId contextId); + + [[nodiscard]] PaintRoundImageCallback generatePaintUserpicCallback( + bool) override; - void setMsgId(MsgId msgId); - [[nodiscard]] MsgId msgId() const; + [[nodiscard]] Data::RecentPostId contextId() const; private: - MsgId _msgId; + const Data::RecentPostId _contextId; }; -void PeerListRowWithMsgId::setMsgId(MsgId msgId) { - _msgId = msgId; +PeerListRowWithFullId::PeerListRowWithFullId( + not_null peer, + Data::RecentPostId contextId) +: PeerListRow(peer) +, _contextId(contextId) { } -MsgId PeerListRowWithMsgId::msgId() const { - return _msgId; +PaintRoundImageCallback PeerListRowWithFullId::generatePaintUserpicCallback( + bool forceRound) { + if (!_contextId.storyId) { + return PeerListRow::generatePaintUserpicCallback(forceRound); + } + const auto peer = PeerListRow::peer(); + auto userpic = PeerListRow::ensureUserpicView(); + + const auto line = st::dialogsStoriesFull.lineTwice; + const auto penWidth = line / 2.; + const auto offset = 1.5 * penWidth * 2; + return [=](Painter &p, int x, int y, int outerWidth, int size) mutable { + const auto rect = QRect(QPoint(x, y), Size(size)); + peer->paintUserpicLeft( + p, + userpic, + x + offset, + y + offset, + outerWidth, + size - offset * 2); + auto hq = PainterHighQualityEnabler(p); + auto gradient = Ui::UnreadStoryOutlineGradient(); + gradient.setStart(rect.topRight()); + gradient.setFinalStop(rect.bottomLeft()); + + p.setPen(QPen(gradient, penWidth)); + p.setBrush(Qt::NoBrush); + p.drawEllipse(rect - Margins(penWidth)); + }; +} + +Data::RecentPostId PeerListRowWithFullId::contextId() const { + return _contextId; } class MembersController final : public PeerListController { @@ -235,13 +276,16 @@ class PublicForwardsController final : public PeerListController { void prepare() override; void rowClicked(not_null row) override; void loadMoreRows() override; + base::unique_qptr rowContextMenu( + QWidget *parent, + not_null row) override; private: - void appendRow(not_null peer, MsgId msgId); + void appendRow(not_null peer, Data::RecentPostId contextId); void applySlice(const Data::PublicForwardsSlice &slice); const not_null _session; - Fn _showPeerHistory; + Fn _requestShow; Api::PublicForwards _api; Data::PublicForwardsSlice _firstSlice; @@ -253,7 +297,7 @@ class PublicForwardsController final : public PeerListController { PublicForwardsController::PublicForwardsController(PublicForwardsDescriptor d) : _session(&d.peer->session()) -, _showPeerHistory(std::move(d.showPeerHistory)) +, _requestShow(std::move(d.requestShow)) , _api(d.peer->asChannel(), d.contextId) , _firstSlice(std::move(d.firstSlice)) { } @@ -282,10 +326,13 @@ void PublicForwardsController::applySlice( _apiToken = slice.token; for (const auto &item : slice.list) { - // TODO support stories. - if (const auto fullId = item.messageId) { - if (const auto peer = session().data().peerLoaded(fullId.peer)) { - appendRow(peer, fullId.msg); + if (const auto &full = item.messageId) { + if (const auto peer = session().data().peerLoaded(full.peer)) { + appendRow(peer, item); + } + } else if (const auto &full = item.storyId) { + if (const auto story = session().data().stories().lookup(full)) { + appendRow((*story)->peer(), item); } } } @@ -293,25 +340,55 @@ void PublicForwardsController::applySlice( } void PublicForwardsController::rowClicked(not_null row) { - const auto rowWithMsgId = static_cast(row.get()); - crl::on_main([=, msgId = rowWithMsgId->msgId(), peer = row->peer()] { - _showPeerHistory({ peer->id, msgId }); - }); + const auto rowWithId = static_cast(row.get()); + crl::on_main([=, id = rowWithId->contextId()] { _requestShow(id); }); +} + +base::unique_qptr PublicForwardsController::rowContextMenu( + QWidget *parent, + not_null row) { + auto menu = base::make_unique_q( + parent, + st::popupMenuWithIcons); + const auto peer = row->peer(); + const auto text = (peer->isChat() || peer->isMegagroup()) + ? tr::lng_context_view_group(tr::now) + : peer->isUser() + ? tr::lng_context_view_profile(tr::now) + : peer->isChannel() + ? tr::lng_context_view_channel(tr::now) + : QString(); + if (text.isEmpty()) { + return nullptr; + } + menu->addAction(text, crl::guard(parent, [=, peerId = peer->id] { + _requestShow({ .messageId = { peerId, MsgId() } }); + }), peer->isUser() ? &st::menuIconProfile : &st::menuIconInfo); + return menu; } void PublicForwardsController::appendRow( not_null peer, - MsgId msgId) { + Data::RecentPostId contextId) { if (delegate()->peerListFindRow(peer->id.value)) { return; } - auto row = std::make_unique(peer); - row->setMsgId(msgId); + auto row = std::make_unique(peer, contextId); - const auto members = peer->asChannel()->membersCount(); - const auto message = peer->owner().message({ peer->id, msgId }); - const auto views = message ? message->viewsCount() : 0; + const auto members = peer->isChannel() + ? peer->asChannel()->membersCount() + : 0; + const auto views = [&] { + if (contextId.messageId) { + const auto message = peer->owner().message(contextId.messageId); + return message ? message->viewsCount() : 0; + } else if (const auto &id = contextId.storyId) { + const auto story = peer->owner().stories().lookup(id); + return story ? (*story)->views() : 0; + } + return 0; + }(); const auto membersText = !members ? QString() @@ -321,7 +398,9 @@ void PublicForwardsController::appendRow( const auto viewsText = views ? tr::lng_stats_recent_messages_views({}, lt_count_decimal, views) : QString(); - const auto resultText = (membersText.isEmpty() || viewsText.isEmpty()) + const auto resultText = (membersText.isEmpty() && viewsText.isEmpty()) + ? tr::lng_stories_no_views(tr::now) + : (membersText.isEmpty() || viewsText.isEmpty()) ? membersText + viewsText : QString("%1, %2").arg(membersText, viewsText); row->setCustomStatus(resultText); @@ -618,7 +697,7 @@ rpl::producer BoostsController::totalBoostsValue() const { void AddPublicForwards( const Data::PublicForwardsSlice &firstSlice, not_null container, - Fn showPeerHistory, + Fn requestShow, not_null peer, Data::RecentPostId contextId) { if (!peer->isChannel()) { @@ -633,7 +712,7 @@ void AddPublicForwards( }; auto d = PublicForwardsDescriptor{ firstSlice, - std::move(showPeerHistory), + std::move(requestShow), peer, contextId, }; diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.h b/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.h index 0696062c7714d..2a2dfe93a0d2a 100644 --- a/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.h +++ b/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.h @@ -26,7 +26,7 @@ namespace Info::Statistics { void AddPublicForwards( const Data::PublicForwardsSlice &firstSlice, not_null container, - Fn showPeerHistory, + Fn requestShow, not_null peer, Data::RecentPostId contextId); diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_widget.cpp b/Telegram/SourceFiles/info/statistics/info_statistics_widget.cpp index 3544b2ae0d01d..6c978b2600d9b 100644 --- a/Telegram/SourceFiles/info/statistics/info_statistics_widget.cpp +++ b/Telegram/SourceFiles/info/statistics/info_statistics_widget.cpp @@ -7,10 +7,13 @@ For license and copyright information please follow this link: */ #include "info/statistics/info_statistics_widget.h" -#include "info/statistics/info_statistics_inner_widget.h" +#include "data/data_session.h" +#include "data/data_stories.h" #include "info/info_controller.h" #include "info/info_memento.h" +#include "info/statistics/info_statistics_inner_widget.h" #include "lang/lang_keys.h" +#include "main/main_session.h" namespace Info::Statistics { @@ -78,6 +81,13 @@ Widget::Widget( controller->statisticsPeer(), request.messageStatistic, request.storyStatistic)); + } else if (const auto &s = request.story) { + if (const auto peer = controller->session().data().peer(s.peer)) { + controller->parentController()->openPeerStory( + peer, + s.story, + { Data::StoriesContextSingle() }); + } } }, _inner->lifetime()); _inner->scrollToRequests( diff --git a/Telegram/SourceFiles/info/stories/info_stories_provider.cpp b/Telegram/SourceFiles/info/stories/info_stories_provider.cpp index 7fdd26a63c679..ba82c297f8102 100644 --- a/Telegram/SourceFiles/info/stories/info_stories_provider.cpp +++ b/Telegram/SourceFiles/info/stories/info_stories_provider.cpp @@ -60,7 +60,7 @@ Provider::Provider(not_null controller) _peer->session().changes().storyUpdates( Data::StoryUpdate::Flag::Destroyed ) | rpl::filter([=](const Data::StoryUpdate &update) { - return update.story->peer() == _peer; + return update.story->peer() == _peer; }) | rpl::start_with_next([=](const Data::StoryUpdate &update) { storyRemoved(update.story); }, _lifetime); diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp index 9e671a890b5ee..1a2703675c923 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp @@ -1458,6 +1458,10 @@ void AttachWebView::show( : attached->inMainMenu ? Button::RemoveFromMainMenu : Button::RemoveFromMenu); + if (attached != end(_attachBots) + && (attached->inAttachMenu || attached->inMainMenu)) { + allowClipboardRead = true; + } _lastShownUrl = url; _lastShownQueryId = queryId; diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.cpp b/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.cpp index d1d1cafade10e..2f5242e85a93f 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.cpp +++ b/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.cpp @@ -213,7 +213,7 @@ QString ItemBase::getResultThumbLetter() const { domain = parts.at(2); } - parts = domain.split('@').back().split('.'); + parts = domain.split('@').constLast().split('.'); if (parts.size() > 1) { return parts.at(parts.size() - 2).at(0).toUpper(); } diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_send_data.cpp b/Telegram/SourceFiles/inline_bots/inline_bot_send_data.cpp index a2f342d28311e..a8135979bd9d7 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_send_data.cpp +++ b/Telegram/SourceFiles/inline_bots/inline_bot_send_data.cpp @@ -84,7 +84,6 @@ SendDataCommon::SentMessageFields SendGeo::getSentMessageFields() const { } SendDataCommon::SentMessageFields SendVenue::getSentMessageFields() const { - const auto venueType = QString(); return { .media = MTP_messageMediaVenue( _location.toMTP(), MTP_string(_title), diff --git a/Telegram/SourceFiles/intro/intro_phone.cpp b/Telegram/SourceFiles/intro/intro_phone.cpp index bf8a355bcd602..7ac4209562d4f 100644 --- a/Telegram/SourceFiles/intro/intro_phone.cpp +++ b/Telegram/SourceFiles/intro/intro_phone.cpp @@ -169,7 +169,8 @@ void PhoneWidget::submit() { // Check if such account is authorized already. const auto digitsOnly = [](QString value) { - return value.replace(QRegularExpression("[^0-9]"), QString()); + static const auto RegExp = QRegularExpression("[^0-9]"); + return value.replace(RegExp, QString()); }; const auto phoneDigits = digitsOnly(phone); for (const auto &[index, existing] : Core::App().domain().accounts()) { diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index 77ca1bfe497fb..202ac2589cff3 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -752,13 +752,20 @@ void MainWidget::searchMessages(const QString &query, Dialogs::Key inChat) { void MainWidget::handleAudioUpdate(const Media::Player::TrackState &state) { using State = Media::Player::State; const auto document = state.id.audio(); + const auto item = session().data().message(state.id.contextId()); if (!Media::Player::IsStoppedOrStopping(state.state)) { - createPlayer(); + const auto ttlSeconds = item + && !item->out() + && item->media() + && item->media()->ttlSeconds(); + if (!ttlSeconds) { + createPlayer(); + } } else if (state.state == State::StoppedAtStart) { Media::Player::instance()->stopAndClose(); } - if (const auto item = session().data().message(state.id.contextId())) { + if (item) { session().data().requestItemRepaint(item); } if (document) { diff --git a/Telegram/SourceFiles/media/audio/media_audio.cpp b/Telegram/SourceFiles/media/audio/media_audio.cpp index 9c7a53b3fd5ff..c882856f6d2e1 100644 --- a/Telegram/SourceFiles/media/audio/media_audio.cpp +++ b/Telegram/SourceFiles/media/audio/media_audio.cpp @@ -36,7 +36,6 @@ namespace { constexpr auto kSuppressRatioAll = 0.2; constexpr auto kSuppressRatioSong = 0.05; constexpr auto kWaveformCounterBufferSize = 256 * 1024; -constexpr auto kEffectDestructionDelay = crl::time(1000); QMutex AudioMutex; ALCdevice *AudioDevice = nullptr; @@ -182,7 +181,7 @@ void ClosePlaybackDevice(not_null instance) { } // namespace - // Thread: Main. +// Thread: Main. void Start(not_null instance) { Assert(AudioDevice == nullptr); @@ -538,16 +537,16 @@ Mixer::Mixer(not_null instance) }); }, _lifetime); - connect(this, SIGNAL(loaderOnStart(const AudioMsgId&, qint64)), _loader, SLOT(onStart(const AudioMsgId&, qint64))); - connect(this, SIGNAL(loaderOnCancel(const AudioMsgId&)), _loader, SLOT(onCancel(const AudioMsgId&)), Qt::QueuedConnection); + connect(this, SIGNAL(loaderOnStart(AudioMsgId,qint64)), _loader, SLOT(onStart(AudioMsgId,qint64))); + connect(this, SIGNAL(loaderOnCancel(AudioMsgId)), _loader, SLOT(onCancel(AudioMsgId)), Qt::QueuedConnection); connect(_loader, SIGNAL(needToCheck()), _fader, SLOT(onTimer())); - connect(_loader, SIGNAL(error(const AudioMsgId&)), this, SLOT(onError(const AudioMsgId&))); - connect(_fader, SIGNAL(needToPreload(const AudioMsgId&)), _loader, SLOT(onLoad(const AudioMsgId&))); - connect(_fader, SIGNAL(playPositionUpdated(const AudioMsgId&)), this, SIGNAL(updated(const AudioMsgId&))); - connect(_fader, SIGNAL(audioStopped(const AudioMsgId&)), this, SLOT(onStopped(const AudioMsgId&))); - connect(_fader, SIGNAL(error(const AudioMsgId&)), this, SLOT(onError(const AudioMsgId&))); - connect(this, SIGNAL(stoppedOnError(const AudioMsgId&)), this, SIGNAL(updated(const AudioMsgId&)), Qt::QueuedConnection); - connect(this, SIGNAL(updated(const AudioMsgId&)), this, SLOT(onUpdated(const AudioMsgId&))); + connect(_loader, SIGNAL(error(AudioMsgId)), this, SLOT(onError(AudioMsgId))); + connect(_fader, SIGNAL(needToPreload(AudioMsgId)), _loader, SLOT(onLoad(AudioMsgId))); + connect(_fader, SIGNAL(playPositionUpdated(AudioMsgId)), this, SIGNAL(updated(AudioMsgId))); + connect(_fader, SIGNAL(audioStopped(AudioMsgId)), this, SLOT(onStopped(AudioMsgId))); + connect(_fader, SIGNAL(error(AudioMsgId)), this, SLOT(onError(AudioMsgId))); + connect(this, SIGNAL(stoppedOnError(AudioMsgId)), this, SIGNAL(updated(AudioMsgId)), Qt::QueuedConnection); + connect(this, SIGNAL(updated(AudioMsgId)), this, SLOT(onUpdated(AudioMsgId))); _loaderThread.start(); _faderThread.start(); diff --git a/Telegram/SourceFiles/media/audio/media_audio_capture.h b/Telegram/SourceFiles/media/audio/media_audio_capture.h index 107274c0cade1..4bb23c2b08d87 100644 --- a/Telegram/SourceFiles/media/audio/media_audio_capture.h +++ b/Telegram/SourceFiles/media/audio/media_audio_capture.h @@ -57,7 +57,7 @@ class Instance final : public QObject { friend class Inner; bool _available = false; - rpl::variable _started = false;; + rpl::variable _started = false; rpl::event_stream _updates; QThread _thread; std::unique_ptr _inner; diff --git a/Telegram/SourceFiles/media/clip/media_clip_reader.cpp b/Telegram/SourceFiles/media/clip/media_clip_reader.cpp index 7227ef7bcf3e1..ee7487269bbce 100644 --- a/Telegram/SourceFiles/media/clip/media_clip_reader.cpp +++ b/Telegram/SourceFiles/media/clip/media_clip_reader.cpp @@ -180,7 +180,7 @@ struct Worker { Manager manager; }; -std::vector> Workers; +std::vector> Workers; } // namespace diff --git a/Telegram/SourceFiles/media/player/media_player_instance.cpp b/Telegram/SourceFiles/media/player/media_player_instance.cpp index 5e1655cc08688..4dc687a2779b0 100644 --- a/Telegram/SourceFiles/media/player/media_player_instance.cpp +++ b/Telegram/SourceFiles/media/player/media_player_instance.cpp @@ -503,6 +503,9 @@ bool Instance::moveInPlaylist( } const auto jumpByItem = [&](not_null item) { if (const auto media = item->media()) { + if (media->ttlSeconds()) { + return false; + } if (const auto document = media->document()) { if (autonext) { _switchToNext.fire({ diff --git a/Telegram/SourceFiles/media/player/media_player_panel.h b/Telegram/SourceFiles/media/player/media_player_panel.h index 17446fdc5bc71..9123834cee210 100644 --- a/Telegram/SourceFiles/media/player/media_player_panel.h +++ b/Telegram/SourceFiles/media/player/media_player_panel.h @@ -78,7 +78,7 @@ class Panel : public Ui::RpWidget, private Info::AbstractController { return width() - contentLeft() - contentRight(); } int contentHeight() const { - return height() - contentTop() - contentBottom();; + return height() - contentTop() - contentBottom(); } void startAnimation(); diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp index 6eabefc353164..76e8806648078 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp @@ -879,7 +879,7 @@ void Controller::show( const auto document = story->document(); _header->show({ .peer = peer, - .repostPeer = story->repostSourcePeer(), + .repostPeer = _repostView ? _repostView->fromPeer() : nullptr, .repostFrom = _repostView ? _repostView->fromName() : nullptr, .date = story->date(), .fullIndex = _sliderCount ? _index : 0, @@ -903,8 +903,11 @@ void Controller::show( _recentViews->show({ .list = story->recentViewers(), .reactions = story->reactions(), - .total = story->views(), + .forwards = story->forwards(), + .views = story->views(), + .total = story->interactions(), .type = RecentViewsTypeFor(peer), + .canViewReactions = CanViewReactionsFor(peer), }, _reactions->likedValue()); if (const auto nowLikeButton = _recentViews->likeButton()) { if (wasLikeButton != nowLikeButton) { @@ -996,11 +999,15 @@ void Controller::subscribeToSession() { show(update.story, _context); _delegate->storiesRedisplay(update.story); } else { + const auto peer = update.story->peer(); _recentViews->show({ .list = update.story->recentViewers(), .reactions = update.story->reactions(), - .total = update.story->views(), - .type = RecentViewsTypeFor(update.story->peer()), + .forwards = update.story->forwards(), + .views = update.story->views(), + .total = update.story->interactions(), + .type = RecentViewsTypeFor(peer), + .canViewReactions = CanViewReactionsFor(peer), }); updateAreas(update.story); } @@ -1017,10 +1024,17 @@ void Controller::updateAreas(Data::Story *story) { const auto &suggestedReactions = story ? story->suggestedReactions() : std::vector(); + const auto &channelPosts = story + ? story->channelPosts() + : std::vector(); if (_locations != locations) { _locations = locations; _areas.clear(); } + if (_channelPosts != channelPosts) { + _channelPosts = channelPosts; + _areas.clear(); + } const auto reactionsCount = int(suggestedReactions.size()); if (_suggestedReactions.size() == reactionsCount && !_areas.empty()) { for (auto i = 0; i != reactionsCount; ++i) { @@ -1039,10 +1053,6 @@ void Controller::updateAreas(Data::Story *story) { _suggestedReactions = suggestedReactions; _areas.clear(); } - if (_areas.empty() || _suggestedReactions.empty()) { - return; - } - } PauseState Controller::pauseState() const { @@ -1165,10 +1175,16 @@ void Controller::updatePlayback(const Player::TrackState &state) { ClickHandlerPtr Controller::lookupAreaHandler(QPoint point) const { const auto &layout = _layout.current(); - if ((_locations.empty() && _suggestedReactions.empty()) || !layout) { + if (!layout + || (_locations.empty() + && _suggestedReactions.empty() + && _channelPosts.empty())) { return nullptr; } else if (_areas.empty()) { - _areas.reserve(_locations.size() + _suggestedReactions.size()); + const auto now = story(); + _areas.reserve(_locations.size() + + _suggestedReactions.size() + + _channelPosts.size()); for (const auto &location : _locations) { _areas.push_back({ .original = location.area.geometry, @@ -1198,6 +1214,17 @@ ClickHandlerPtr Controller::lookupAreaHandler(QPoint point) const { .reaction = std::move(widget), }); } + if (const auto session = now ? &now->session() : nullptr) { + for (const auto &channelPost : _channelPosts) { + _areas.push_back({ + .original = channelPost.area.geometry, + .rotation = channelPost.area.rotation, + .handler = MakeChannelPostHandler( + session, + channelPost.itemId), + }); + } + } rebuildActiveAreas(*layout); } @@ -1410,11 +1437,19 @@ const Data::StoryViews &Controller::views(int limit, bool initial) { const auto done = viewsGotMoreCallback(); const auto peer = shownPeer(); auto &stories = peer->owner().stories(); - stories.loadViewsSlice( - peer, - _shown.story, - _viewsSlice.nextOffset, - done); + if (peer->isChannel()) { + stories.loadReactionsSlice( + peer, + _shown.story, + _viewsSlice.nextOffset, + done); + } else { + stories.loadViewsSlice( + peer, + _shown.story, + _viewsSlice.nextOffset, + done); + } } return _viewsSlice; } @@ -1429,7 +1464,11 @@ Fn Controller::viewsGotMoreCallback() { const auto peer = shownPeer(); auto &stories = peer->owner().stories(); if (const auto maybeStory = stories.lookup(_shown)) { - _viewsSlice = (*maybeStory)->viewsList(); + if (peer->isChannel()) { + _viewsSlice = (*maybeStory)->channelReactionsList(); + } else { + _viewsSlice = (*maybeStory)->viewsList(); + } } else { _viewsSlice = {}; } @@ -1475,7 +1514,7 @@ StoryId Controller::shownId(int index) const { std::unique_ptr Controller::validateRepostView( not_null story) { - return story->repost() + return (story->repost() || !story->channelPosts().empty()) ? std::make_unique(this, story) : nullptr; } @@ -1592,8 +1631,12 @@ void Controller::refreshViewsFromData() { const auto peer = shownPeer(); auto &stories = peer->owner().stories(); const auto maybeStory = stories.lookup(_shown); - if (!maybeStory || !peer->isSelf()) { + const auto check = peer->isSelf() + || CanViewReactionsFor(peer); + if (!maybeStory || !check) { _viewsSlice = {}; + } else if (peer->isChannel()) { + _viewsSlice = (*maybeStory)->channelReactionsList(); } else { _viewsSlice = (*maybeStory)->viewsList(); } @@ -1779,4 +1822,23 @@ object_ptr PrepareShortInfoBox(not_null peer) { &st::storiesShortInfoBox); } +ClickHandlerPtr MakeChannelPostHandler( + not_null session, + FullMsgId item) { + return std::make_shared(crl::guard(session, [=] { + const auto peer = session->data().peer(item.peer); + if (const auto window = Core::App().windowFor(peer)) { + if (const auto controller = window->sessionController()) { + if (&controller->session() == &peer->session()) { + Core::App().hideMediaView(); + controller->showPeerHistory( + item.peer, + Window::SectionShow::Way::ClearStack, + item.msg); + } + } + } + })); +} + } // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h index db494d9eef0d4..0a6c4a8fe4079 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.h +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h @@ -302,6 +302,7 @@ class Controller final : public base::has_weak_ptr { std::vector _locations; std::vector _suggestedReactions; + std::vector _channelPosts; mutable std::vector _areas; std::vector _cachedSourcesList; @@ -336,5 +337,8 @@ void ReportRequested( const style::ReportBox *stOverride = nullptr); [[nodiscard]] object_ptr PrepareShortInfoBox( not_null peer); +[[nodiscard]] ClickHandlerPtr MakeChannelPostHandler( + not_null session, + FullMsgId item); } // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/stories/media_stories_header.cpp b/Telegram/SourceFiles/media/stories/media_stories_header.cpp index 635370eef3423..8b14a984b66dc 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_header.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_header.cpp @@ -256,7 +256,7 @@ struct MadePrivacyBadge { QString::fromUtf8(" \xE2\x80\xA2 ") + tr::lng_edited(tr::now)); } if (!data.repostFrom.isEmpty()) { - result.text = QString::fromUtf8("\xC2\xA0\xE2\x80\xA2 ") + result.text = QString::fromUtf8("\xE2\x80\xA2 ") + result.text; } return result; @@ -500,7 +500,8 @@ void Header::show(HeaderData data) { _repost->resizeToNaturalWidth(repostAvailable); } _repost->move(dateLeft, dateTop); - _date->move(dateLeft + _repost->width(), dateTop); + const auto space = st::normalFont->spacew; + _date->move(dateLeft + _repost->width() + space, dateTop); } else { _date->move(dateLeft, dateTop); } diff --git a/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp b/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp index ed8f4f7fa86a7..2521de47fde81 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp @@ -10,8 +10,11 @@ For license and copyright information please follow this link: #include "api/api_who_reacted.h" // FormatReadDate. #include "chat_helpers/compose/compose_show.h" #include "data/stickers/data_custom_emoji.h" +#include "data/data_channel.h" #include "data/data_peer.h" +#include "data/data_session.h" #include "data/data_stories.h" +#include "history/history_item.h" #include "main/main_session.h" #include "media/stories/media_stories_controller.h" #include "lang/lang_keys.h" @@ -121,6 +124,16 @@ constexpr auto kLoadViewsPages = 2; }; } +[[nodiscard]] QString ComposeRepostStatus( + const QString &date, + not_null repost) { + return date + (repost->repostModified() + ? (QString::fromUtf8(" \xE2\x80\xA2 ") + tr::lng_edited(tr::now)) + : !repost->caption().empty() + ? (QString::fromUtf8(" \xE2\x80\xA2 ") + tr::lng_commented(tr::now)) + : QString()); +} + } // namespace RecentViewsType RecentViewsTypeFor(not_null peer) { @@ -133,6 +146,13 @@ RecentViewsType RecentViewsTypeFor(not_null peer) { : RecentViewsType::Other; } +bool CanViewReactionsFor(not_null peer) { + if (const auto channel = peer->asChannel()) { + return channel->amCreator() || channel->hasAdminRights(); + } + return false; +} + RecentViews::RecentViews(not_null controller) : _controller(controller) { } @@ -162,10 +182,14 @@ void RecentViews::show( } const auto countersChanged = _text.isEmpty() || (_data.total != data.total) + || (_data.views != data.views) + || (_data.forwards != data.forwards) || (_data.reactions != data.reactions); const auto usersChanged = !_userpics || (_data.list != data.list); + const auto canViewReactions = data.canViewReactions + && (data.reactions > 0 || data.forwards > 0); _data = data; - if (_data.type != RecentViewsType::Self) { + if (_data.type != RecentViewsType::Self && !canViewReactions) { _text = {}; _clickHandlerLifetime.destroy(); _userpicsLifetime.destroy(); @@ -194,7 +218,7 @@ void RecentViews::show( _viewsWrap = nullptr; } else { _viewsCounter = (_data.type == RecentViewsType::Channel) - ? Lang::FormatCountDecimal(std::max(_data.total, 1)) + ? Lang::FormatCountDecimal(std::max(_data.views, 1)) : tr::lng_stories_cant_reply(tr::now); _likesCounter = ((_data.type == RecentViewsType::Channel) && _data.reactions) @@ -215,7 +239,8 @@ Ui::RpWidget *RecentViews::likeIconWidget() const { } void RecentViews::refreshClickHandler() { - const auto nowEmpty = _data.list.empty(); + const auto nowEmpty = (_data.type != RecentViewsType::Channel) + && _data.list.empty(); const auto wasEmpty = !_clickHandlerLifetime; const auto raw = _widget.get(); if (wasEmpty == nowEmpty) { @@ -375,13 +400,16 @@ void RecentViews::updateViewsReactionsGeometry() { void RecentViews::updatePartsGeometry() { const auto skip = st::storiesRecentViewsSkip; const auto full = _userpicsWidth + skip + _text.maxWidth(); + const auto add = (_data.type == RecentViewsType::Channel) + ? st::storiesViewsTextPosition.y() + : 0; const auto use = std::min(full, _outer.width()); const auto ux = _outer.x() + (_outer.width() - use) / 2; const auto uheight = st::storiesWhoViewed.userpics.size; - const auto uy = _outer.y() + (_outer.height() - uheight) / 2; + const auto uy = _outer.y() + (_outer.height() - uheight) / 2 + add; const auto tx = ux + _userpicsWidth + skip; const auto theight = st::normalFont->height; - const auto ty = _outer.y() + (_outer.height() - theight) / 2; + const auto ty = _outer.y() + (_outer.height() - theight) / 2 + add; const auto my = std::min(uy, ty); const auto mheight = std::max(uheight, theight); const auto padding = skip; @@ -392,8 +420,10 @@ void RecentViews::updatePartsGeometry() { } void RecentViews::updateText() { - const auto text = _data.total - ? (tr::lng_stories_views(tr::now, lt_count, _data.total) + const auto text = (_data.type == RecentViewsType::Channel) + ? u"View reactions"_q + : _data.views + ? (tr::lng_stories_views(tr::now, lt_count, _data.views) + (_data.reactions ? (u" "_q + QChar(10084) + QString::number(_data.reactions)) : QString())) @@ -403,7 +433,8 @@ void RecentViews::updateText() { } void RecentViews::showMenu() { - if (_menu || _data.list.empty()) { + if (_menu + || (_data.type != RecentViewsType::Channel && _data.list.empty())) { return; } @@ -422,7 +453,7 @@ void RecentViews::showMenu() { const auto added = std::min(int(views.list.size()), kAddPerPage); const auto add = std::min(views.total, kAddPerPage); const auto now = QDateTime::currentDateTime(); - for (const auto &entry : views.list) { + for (const auto &entry : views.list) { addMenuRow(entry, now); if (++count >= add) { break; @@ -473,7 +504,21 @@ void RecentViews::addMenuRow(Data::StoryView entry, const QDateTime &now) { Expects(_menu != nullptr); const auto peer = entry.peer; - const auto date = Api::FormatReadDate(entry.date, now); + const auto repost = entry.repostId + ? peer->owner().stories().lookup({ peer->id, entry.repostId }) + : base::make_unexpected(Data::NoStory::Deleted); + const auto forward = entry.forwardId + ? peer->owner().message({ peer->id, entry.forwardId }) + : nullptr; + const auto date = Api::FormatReadDate( + repost ? (*repost)->date() : forward ? forward->date() : entry.date, + now); + const auto type = forward + ? Ui::WhoReactedType::Forwarded + : repost + ? Ui::WhoReactedType::Reposted + : Ui::WhoReactedType::Viewed; + const auto status = repost ? ComposeRepostStatus(date, *repost) : date; const auto show = _controller->uiShow(); const auto prepare = [&](Ui::PeerUserpicView &view) { const auto size = st::storiesWhoViewed.photoSize; @@ -483,7 +528,8 @@ void RecentViews::addMenuRow(Data::StoryView entry, const QDateTime &now) { userpic.setDevicePixelRatio(style::DevicePixelRatio()); return Ui::WhoReactedEntryData{ .text = peer->name(), - .date = date, + .date = status, + .type = type, .customEntityData = Data::ReactionEntityData(entry.reaction), .userpic = std::move(userpic), .callback = [=] { show->show(PrepareShortInfoBox(peer)); }, @@ -493,7 +539,8 @@ void RecentViews::addMenuRow(Data::StoryView entry, const QDateTime &now) { const auto i = _menuEntries.end() - (_menuPlaceholderCount--); auto data = prepare(i->view); i->peer = peer; - i->date = date; + i->type = type; + i->status = status; i->customEntityData = data.customEntityData; i->callback = data.callback; i->action->setData(std::move(data)); @@ -512,7 +559,8 @@ void RecentViews::addMenuRow(Data::StoryView entry, const QDateTime &now) { _menuEntries.push_back({ .action = raw, .peer = peer, - .date = date, + .type = type, + .status = status, .customEntityData = std::move(customEntityData), .callback = std::move(callback), .view = std::move(view), @@ -533,7 +581,7 @@ void RecentViews::addMenuRowPlaceholder(not_null session) { _menu->menu(), Data::ReactedMenuFactory(session), _menu->menu()->st(), - Ui::WhoReactedEntryData{ .preloader = true }); + Ui::WhoReactedEntryData{ .type = Ui::WhoReactedType::Preloader }); const auto raw = action.get(); _menu->addAction(std::move(action)); _menuEntries.push_back({ .action = raw }); @@ -550,11 +598,15 @@ void RecentViews::rebuildMenuTail() { const auto added = std::min( _menuPlaceholderCount + kAddPerPage, int(views.list.size() - elements)); + const auto height = _menu->height(); for (auto i = elements, till = i + added; i != till; ++i) { const auto &entry = views.list[i]; addMenuRow(entry, now); } _menuEntriesCount = _menuEntriesCount.current() + added; + if (const auto delta = _menu->height() - height) { + _menu->move(_menu->x(), _menu->y() - delta); + } } void RecentViews::subscribeToMenuUserpicsLoading( @@ -590,7 +642,8 @@ void RecentViews::subscribeToMenuUserpicsLoading( userpic.setDevicePixelRatio(style::DevicePixelRatio()); entry.action->setData({ .text = peer->name(), - .date = entry.date, + .date = entry.status, + .type = entry.type, .customEntityData = entry.customEntityData, .userpic = std::move(userpic), .callback = entry.callback, diff --git a/Telegram/SourceFiles/media/stories/media_stories_recent_views.h b/Telegram/SourceFiles/media/stories/media_stories_recent_views.h index ed4617952285f..ea8d69313c980 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_recent_views.h +++ b/Telegram/SourceFiles/media/stories/media_stories_recent_views.h @@ -23,6 +23,7 @@ class RpWidget; class GroupCallUserpics; class PopupMenu; class WhoReactedEntryAction; +enum class WhoReactedType : uchar; } // namespace Ui namespace Main { @@ -43,8 +44,11 @@ enum class RecentViewsType { struct RecentViewsData { std::vector> list; int reactions = 0; + int forwards = 0; + int views = 0; int total = 0; RecentViewsType type = RecentViewsType::Other; + bool canViewReactions = false; friend inline auto operator<=>( const RecentViewsData &, @@ -55,6 +59,7 @@ struct RecentViewsData { }; [[nodiscard]] RecentViewsType RecentViewsTypeFor(not_null peer); +[[nodiscard]] bool CanViewReactionsFor(not_null peer); class RecentViews final { public: @@ -72,7 +77,8 @@ class RecentViews final { struct MenuEntry { not_null action; PeerData *peer = nullptr; - QString date; + Ui::WhoReactedType type = {}; + QString status; QString customEntityData; Fn callback; Ui::PeerUserpicView view; diff --git a/Telegram/SourceFiles/media/stories/media_stories_repost_view.cpp b/Telegram/SourceFiles/media/stories/media_stories_repost_view.cpp index 6b65ea45b9f97..dd5c55ea7c9b6 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_repost_view.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_repost_view.cpp @@ -32,8 +32,18 @@ RepostView::RepostView( not_null controller, not_null story) : _controller(controller) -, _story(story) { - Expects(_story->repost()); +, _story(story) +, _sourcePeer(_story->repost() + ? _story->repostSourcePeer() + : _story->owner().peer( + _story->channelPosts().front().itemId.peer).get()) { + Expects(_story->repost() || !_story->channelPosts().empty()); + + if (!_story->repost()) { + _link = MakeChannelPostHandler( + &_story->session(), + _story->channelPosts().front().itemId); + } _story->session().colorIndicesValue( ) | rpl::start_with_next([=](Ui::ColorIndicesCompressed &&indices) { @@ -67,9 +77,8 @@ void RepostView::draw(Painter &p, int x, int y, int availableWidth) { const auto w = _lastWidth = std::min(int(_maxWidth), availableWidth); const auto h = height() - (simple ? st::normalFont->height : 0); const auto rect = QRect(x, y, w, h); - const auto colorPeer = _story->repostSourcePeer(); - const auto backgroundEmojiId = (!simple && colorPeer) - ? colorPeer->backgroundEmojiId() + const auto backgroundEmojiId = (!simple && _sourcePeer) + ? _sourcePeer->backgroundEmojiId() : DocumentId(); const auto cache = &_quoteCache; const auto "eSt = simple @@ -183,19 +192,23 @@ RepostClickHandler RepostView::lookupHandler(QPoint position) { return { _link, this }; } +PeerData *RepostView::fromPeer() const { + return _sourcePeer; +} + QString RepostView::fromName() const { - const auto sender = _story->repostSourcePeer(); - return sender ? sender->name() : _story->repostSourceName(); + return _sourcePeer ? _sourcePeer->name() : _story->repostSourceName(); } void RepostView::recountDimensions() { - const auto sender = _story->repostSourcePeer(); - const auto name = sender ? sender->name() : _story->repostSourceName(); + const auto name = _sourcePeer + ? _sourcePeer->name() + : _story->repostSourceName(); const auto owner = &_story->owner(); - const auto repostId = _story->repostSourceId(); + const auto repostId = _story->repost() ? _story->repostSourceId() : 0; - const auto colorIndexPlusOne = sender - ? (sender->colorIndex() + 1) + const auto colorIndexPlusOne = _sourcePeer + ? (_sourcePeer->colorIndex() + 1) : 1; const auto dark = true; const auto colorPattern = colorIndexPlusOne @@ -211,8 +224,9 @@ void RepostView::recountDimensions() { auto text = TextWithEntities(); auto unavailable = false; - if (sender && repostId) { - const auto of = owner->stories().lookup({ sender->id, repostId }); + if (_sourcePeer && repostId) { + const auto senderId = _sourcePeer->id; + const auto of = owner->stories().lookup({ senderId, repostId }); unavailable = !of && (of.error() == Data::NoStory::Deleted); if (of) { text = (*of)->caption(); @@ -221,12 +235,12 @@ void RepostView::recountDimensions() { _maxWidth = 0; _controller->repaint(); }); - owner->stories().resolve({ sender->id, repostId }, done); + owner->stories().resolve({ _sourcePeer->id, repostId }, done); } } auto nameFull = TextWithEntities(); - nameFull.append(HistoryView::Reply::PeerEmoji(owner, sender)); + nameFull.append(HistoryView::Reply::PeerEmoji(owner, _sourcePeer)); nameFull.append(name); auto context = Core::MarkedTextContext{ .session = &_story->session(), diff --git a/Telegram/SourceFiles/media/stories/media_stories_repost_view.h b/Telegram/SourceFiles/media/stories/media_stories_repost_view.h index f98fe0350f361..a77569df22660 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_repost_view.h +++ b/Telegram/SourceFiles/media/stories/media_stories_repost_view.h @@ -38,6 +38,7 @@ class RepostView final void draw(Painter &p, int x, int y, int availableWidth); [[nodiscard]] RepostClickHandler lookupHandler(QPoint position); + [[nodiscard]] PeerData *fromPeer() const; [[nodiscard]] QString fromName() const; private: @@ -49,6 +50,7 @@ class RepostView final const not_null _controller; const not_null _story; + PeerData *_sourcePeer = nullptr; ClickHandlerPtr _link; std::unique_ptr _ripple; diff --git a/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp b/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp index ebfe59024cc47..e57825ea15bb7 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp @@ -143,7 +143,7 @@ Sibling::LoaderVideo::LoaderVideo( Fn update) : _video(video) , _origin(origin) -, _update(std::move( update)) +, _update(std::move(update)) , _media(_video->createMediaView()) { _media->goodThumbnailWanted(); } diff --git a/Telegram/SourceFiles/media/system_media_controls_manager.cpp b/Telegram/SourceFiles/media/system_media_controls_manager.cpp index 4fb87dc90636b..d166858608c30 100644 --- a/Telegram/SourceFiles/media/system_media_controls_manager.cpp +++ b/Telegram/SourceFiles/media/system_media_controls_manager.cpp @@ -303,4 +303,4 @@ SystemMediaControlsManager::SystemMediaControlsManager() SystemMediaControlsManager::~SystemMediaControlsManager() = default; -} // namespace Media +} // namespace Media diff --git a/Telegram/SourceFiles/media/system_media_controls_manager.h b/Telegram/SourceFiles/media/system_media_controls_manager.h index f7740771eeb4d..e7c6ececf9332 100644 --- a/Telegram/SourceFiles/media/system_media_controls_manager.h +++ b/Telegram/SourceFiles/media/system_media_controls_manager.h @@ -48,4 +48,4 @@ class SystemMediaControlsManager { rpl::lifetime _lifetime; }; -} // namespace Media +} // namespace Media diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp index 6df2da00c13f8..31ea352379ba5 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp @@ -317,7 +317,7 @@ void OverlayWidget::RendererGL::paintBackground() { _contentBuffer->bind(); if (const auto notch = _owner->topNotchSkip()) { const auto top = transformRect(QRect(0, 0, _owner->width(), notch)); - const GLfloat coords[] = { + const GLfloat coords[] = { top.left(), top.top(), top.right(), top.top(), top.right(), top.bottom(), diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index 2295982fe7937..c533fd805d55f 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -560,7 +560,7 @@ OverlayWidget::OverlayWidget() || type == QEvent::TouchEnd || type == QEvent::TouchCancel) { if (handleTouchEvent(static_cast(e.get()))) { - return base::EventFilterResult::Cancel;; + return base::EventFilterResult::Cancel; } } else if (type == QEvent::Wheel) { handleWheelEvent(static_cast(e.get())); @@ -1509,7 +1509,7 @@ void OverlayWidget::fillContextMenuActions(const MenuCallback &addAction) { if ((!story || story->canDownloadChecked()) && _document && !_document->filepath(true).isEmpty()) { - const auto text = Platform::IsMac() + const auto text = Platform::IsMac() ? tr::lng_context_show_in_finder(tr::now) : tr::lng_context_show_in_folder(tr::now); addAction( @@ -3050,7 +3050,7 @@ void OverlayWidget::refreshMediaViewer() { void OverlayWidget::refreshFromLabel() { if (_message) { _from = _message->originalSender(); - if (const auto info = _message->hiddenSenderInfo()) { + if (const auto info = _message->originalHiddenSenderInfo()) { _fromName = info->name; } else { Assert(_from != nullptr); @@ -4879,7 +4879,6 @@ void OverlayWidget::paintControls( const style::icon &icon; bool nonbright = false; }; - const QRect kEmpty; // When adding / removing controls please update RendererGL. const Control controls[] = { { @@ -5019,6 +5018,7 @@ void OverlayWidget::paintCaptionContent( const auto inner = full.marginsRemoved( _stories ? _stories->repostCaptionPadding() : QMargins()); if (_stories) { + p.setOpacity(1.); if (_stories->repost()) { _stories->drawRepostInfo(p, full.x(), full.y(), full.width()); } diff --git a/Telegram/SourceFiles/mtproto/connection_http.cpp b/Telegram/SourceFiles/mtproto/connection_http.cpp index 37b9d2198e250..baaad903e923b 100644 --- a/Telegram/SourceFiles/mtproto/connection_http.cpp +++ b/Telegram/SourceFiles/mtproto/connection_http.cpp @@ -55,7 +55,8 @@ void HttpConnection::disconnectFromServer() { if (_status == Status::Finished) return; _status = Status::Finished; - for (const auto request : base::take(_requests)) { + const auto requests = base::take(_requests); + for (const auto request : requests) { request->abort(); request->deleteLater(); } @@ -85,8 +86,7 @@ void HttpConnection::connectToServer( if (Logs::DebugEnabled()) { _debugId = u"%1(dc:%2,%3)"_q .arg(_debugId.toInt()) - .arg(ProtocolDcDebugId(protocolDcId)) - .arg(url().toDisplayString()); + .arg(ProtocolDcDebugId(protocolDcId), url().toDisplayString()); } _pingTime = crl::now(); diff --git a/Telegram/SourceFiles/mtproto/connection_tcp.cpp b/Telegram/SourceFiles/mtproto/connection_tcp.cpp index 6fa51a9993350..26dff1dd9752e 100644 --- a/Telegram/SourceFiles/mtproto/connection_tcp.cpp +++ b/Telegram/SourceFiles/mtproto/connection_tcp.cpp @@ -536,9 +536,10 @@ void TcpConnection::connectToServer( const auto postfix = _socket->debugPostfix(); _debugId = u"%1(dc:%2,%3%4:%5%6)"_q .arg(_debugId.toInt()) - .arg(ProtocolDcDebugId(_protocolDcId)) - .arg((_proxy.type == ProxyData::Type::Mtproto) ? "mtproxy " : "") - .arg(_address) + .arg( + ProtocolDcDebugId(_protocolDcId), + (_proxy.type == ProxyData::Type::Mtproto) ? "mtproxy " : "", + _address) .arg(_port) .arg(postfix.isEmpty() ? _protocol->debugPostfix() : postfix); _socket->setDebugId(_debugId); diff --git a/Telegram/SourceFiles/mtproto/details/mtproto_domain_resolver.cpp b/Telegram/SourceFiles/mtproto/details/mtproto_domain_resolver.cpp index f20bb78e0a38e..d34d6e90d4b77 100644 --- a/Telegram/SourceFiles/mtproto/details/mtproto_domain_resolver.cpp +++ b/Telegram/SourceFiles/mtproto/details/mtproto_domain_resolver.cpp @@ -104,8 +104,9 @@ std::vector ParseDnsResponse( return {}; } + const auto array = (*answerIt).toArray(); auto result = std::vector(); - for (const auto elem : (*answerIt).toArray()) { + for (const auto elem : array) { if (!elem.isObject()) { LOG(("Config Error: Not an object found " "in Answer array in dns response JSON.")); diff --git a/Telegram/SourceFiles/mtproto/mtp_instance.cpp b/Telegram/SourceFiles/mtproto/mtp_instance.cpp index e5b5cc8ec8a8b..7a9908d9925ff 100644 --- a/Telegram/SourceFiles/mtproto/mtp_instance.cpp +++ b/Telegram/SourceFiles/mtproto/mtp_instance.cpp @@ -1354,8 +1354,11 @@ bool Instance::Private::onErrorDefault( const auto &type = error.type(); const auto code = error.code(); auto badGuestDc = (code == 400) && (type == u"FILE_ID_INVALID"_q); + static const auto MigrateRegExp = QRegularExpression("^(FILE|PHONE|NETWORK|USER)_MIGRATE_(\\d+)$"); + static const auto FloodWaitRegExp = QRegularExpression("^FLOOD_WAIT_(\\d+)$"); + static const auto SlowmodeWaitRegExp = QRegularExpression("^SLOWMODE_WAIT_(\\d+)$"); QRegularExpressionMatch m1, m2; - if ((m1 = QRegularExpression("^(FILE|PHONE|NETWORK|USER)_MIGRATE_(\\d+)$").match(type)).hasMatch()) { + if ((m1 = MigrateRegExp.match(type)).hasMatch()) { if (!requestId) return false; auto dcWithShift = ShiftedDcId(0); @@ -1458,8 +1461,8 @@ bool Instance::Private::onErrorDefault( return true; } else if (code < 0 || code >= 500 - || (m1 = QRegularExpression("^FLOOD_WAIT_(\\d+)$").match(type)).hasMatch() - || ((m2 = QRegularExpression("^SLOWMODE_WAIT_(\\d+)$").match(type)).hasMatch() + || (m1 = FloodWaitRegExp.match(type)).hasMatch() + || ((m2 = SlowmodeWaitRegExp.match(type)).hasMatch() && m2.captured(1).toInt() < 3)) { if (!requestId) return false; diff --git a/Telegram/SourceFiles/mtproto/mtproto_dc_options.cpp b/Telegram/SourceFiles/mtproto/mtproto_dc_options.cpp index c659f8509a679..048845a8f2bcb 100644 --- a/Telegram/SourceFiles/mtproto/mtproto_dc_options.cpp +++ b/Telegram/SourceFiles/mtproto/mtproto_dc_options.cpp @@ -757,8 +757,9 @@ bool DcOptions::loadFromFile(const QString &path) { stream.setCodec("UTF-8"); #endif // Qt < 6.0.0 while (!stream.atEnd()) { + static const auto RegExp = QRegularExpression(R"(\s)"); auto line = stream.readLine(); - auto components = line.split(QRegularExpression(R"(\s)"), Qt::SkipEmptyParts); + auto components = line.split(RegExp, Qt::SkipEmptyParts); if (components.isEmpty() || components[0].startsWith('#')) { continue; } diff --git a/Telegram/SourceFiles/mtproto/mtproto_proxy_data.cpp b/Telegram/SourceFiles/mtproto/mtproto_proxy_data.cpp index 260fe0b12e126..8ed267514cee1 100644 --- a/Telegram/SourceFiles/mtproto/mtproto_proxy_data.cpp +++ b/Telegram/SourceFiles/mtproto/mtproto_proxy_data.cpp @@ -157,11 +157,12 @@ bool ProxyData::supportsCalls() const { } bool ProxyData::tryCustomResolve() const { + static const auto RegExp = QRegularExpression( + QStringLiteral("^\\d+\\.\\d+\\.\\d+\\.\\d+$") + ); return (type == Type::Socks5 || type == Type::Mtproto) && !qthelp::is_ipv6(host) - && !QRegularExpression( - QStringLiteral("^\\d+\\.\\d+\\.\\d+\\.\\d+$") - ).match(host).hasMatch(); + && !RegExp.match(host).hasMatch(); } bytes::vector ProxyData::secretFromMtprotoPassword() const { diff --git a/Telegram/SourceFiles/mtproto/mtproto_response.cpp b/Telegram/SourceFiles/mtproto/mtproto_response.cpp index 0c0a7160405e7..772904433454e 100644 --- a/Telegram/SourceFiles/mtproto/mtproto_response.cpp +++ b/Telegram/SourceFiles/mtproto/mtproto_response.cpp @@ -25,11 +25,11 @@ namespace { Error::Error(const MTPrpcError &error) : _code(error.c_rpc_error().verror_code().v) { QString text = qs(error.c_rpc_error().verror_message()); - const auto expression = QRegularExpression( + static const auto Expression = QRegularExpression( "^([A-Z0-9_]+)(: .*)?$", (QRegularExpression::DotMatchesEverythingOption | QRegularExpression::MultilineOption)); - const auto match = expression.match(text); + const auto match = Expression.match(text); if (match.hasMatch()) { _type = match.captured(1); _description = match.captured(2).mid(2); diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index 27b9a2c3ebed4..87ce36bbc94e2 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -97,11 +97,11 @@ userStatusLastMonth#77ebc742 = UserStatus; chatEmpty#29562865 id:long = Chat; chat#41cbf256 flags:# creator:flags.0?true left:flags.2?true deactivated:flags.5?true call_active:flags.23?true call_not_empty:flags.24?true noforwards:flags.25?true id:long title:string photo:ChatPhoto participants_count:int date:int version:int migrated_to:flags.6?InputChannel admin_rights:flags.14?ChatAdminRights default_banned_rights:flags.18?ChatBannedRights = Chat; chatForbidden#6592a1a7 id:long title:string = Chat; -channel#8e87ccd8 flags:# creator:flags.0?true left:flags.2?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true signatures:flags.11?true min:flags.12?true scam:flags.19?true has_link:flags.20?true has_geo:flags.21?true slowmode_enabled:flags.22?true call_active:flags.23?true call_not_empty:flags.24?true fake:flags.25?true gigagroup:flags.26?true noforwards:flags.27?true join_to_send:flags.28?true join_request:flags.29?true forum:flags.30?true flags2:# stories_hidden:flags2.1?true stories_hidden_min:flags2.2?true stories_unavailable:flags2.3?true id:long access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int restriction_reason:flags.9?Vector admin_rights:flags.14?ChatAdminRights banned_rights:flags.15?ChatBannedRights default_banned_rights:flags.18?ChatBannedRights participants_count:flags.17?int usernames:flags2.0?Vector stories_max_id:flags2.4?int color:flags2.7?PeerColor = Chat; +channel#aadfc8f flags:# creator:flags.0?true left:flags.2?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true signatures:flags.11?true min:flags.12?true scam:flags.19?true has_link:flags.20?true has_geo:flags.21?true slowmode_enabled:flags.22?true call_active:flags.23?true call_not_empty:flags.24?true fake:flags.25?true gigagroup:flags.26?true noforwards:flags.27?true join_to_send:flags.28?true join_request:flags.29?true forum:flags.30?true flags2:# stories_hidden:flags2.1?true stories_hidden_min:flags2.2?true stories_unavailable:flags2.3?true id:long access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int restriction_reason:flags.9?Vector admin_rights:flags.14?ChatAdminRights banned_rights:flags.15?ChatBannedRights default_banned_rights:flags.18?ChatBannedRights participants_count:flags.17?int usernames:flags2.0?Vector stories_max_id:flags2.4?int color:flags2.7?PeerColor profile_color:flags2.8?PeerColor emoji_status:flags2.9?EmojiStatus level:flags2.10?int = Chat; channelForbidden#17d493d5 flags:# broadcast:flags.5?true megagroup:flags.8?true id:long access_hash:long title:string until_date:flags.16?int = Chat; chatFull#c9d31138 flags:# can_set_username:flags.7?true has_scheduled:flags.8?true translations_disabled:flags.19?true id:long about:string participants:ChatParticipants chat_photo:flags.2?Photo notify_settings:PeerNotifySettings exported_invite:flags.13?ExportedChatInvite bot_info:flags.3?Vector pinned_msg_id:flags.6?int folder_id:flags.11?int call:flags.12?InputGroupCall ttl_period:flags.14?int groupcall_default_join_as:flags.15?Peer theme_emoticon:flags.16?string requests_pending:flags.17?int recent_requesters:flags.17?Vector available_reactions:flags.18?ChatReactions = ChatFull; -channelFull#723027bd flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_set_location:flags.16?true has_scheduled:flags.19?true can_view_stats:flags.20?true blocked:flags.22?true flags2:# can_delete_channel:flags2.0?true antispam:flags2.1?true participants_hidden:flags2.2?true translations_disabled:flags2.3?true stories_pinned_available:flags2.5?true view_forum_as_messages:flags2.6?true id:long about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:flags.23?ExportedChatInvite bot_info:Vector migrated_from_chat_id:flags.4?long migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int folder_id:flags.11?int linked_chat_id:flags.14?long location:flags.15?ChannelLocation slowmode_seconds:flags.17?int slowmode_next_send_date:flags.18?int stats_dc:flags.12?int pts:int call:flags.21?InputGroupCall ttl_period:flags.24?int pending_suggestions:flags.25?Vector groupcall_default_join_as:flags.26?Peer theme_emoticon:flags.27?string requests_pending:flags.28?int recent_requesters:flags.28?Vector default_send_as:flags.29?Peer available_reactions:flags.30?ChatReactions stories:flags2.4?PeerStories = ChatFull; +channelFull#f2bcb6f flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_set_location:flags.16?true has_scheduled:flags.19?true can_view_stats:flags.20?true blocked:flags.22?true flags2:# can_delete_channel:flags2.0?true antispam:flags2.1?true participants_hidden:flags2.2?true translations_disabled:flags2.3?true stories_pinned_available:flags2.5?true view_forum_as_messages:flags2.6?true id:long about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:flags.23?ExportedChatInvite bot_info:Vector migrated_from_chat_id:flags.4?long migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int folder_id:flags.11?int linked_chat_id:flags.14?long location:flags.15?ChannelLocation slowmode_seconds:flags.17?int slowmode_next_send_date:flags.18?int stats_dc:flags.12?int pts:int call:flags.21?InputGroupCall ttl_period:flags.24?int pending_suggestions:flags.25?Vector groupcall_default_join_as:flags.26?Peer theme_emoticon:flags.27?string requests_pending:flags.28?int recent_requesters:flags.28?Vector default_send_as:flags.29?Peer available_reactions:flags.30?ChatReactions stories:flags2.4?PeerStories wallpaper:flags2.7?WallPaper = ChatFull; chatParticipant#c02d4007 user_id:long inviter_id:long date:int = ChatParticipant; chatParticipantCreator#e46bcee4 user_id:long = ChatParticipant; @@ -114,7 +114,7 @@ chatPhotoEmpty#37c1011c = ChatPhoto; chatPhoto#1c6e1c11 flags:# has_video:flags.0?true photo_id:long stripped_thumb:flags.1?bytes dc_id:int = ChatPhoto; messageEmpty#90a6ca84 flags:# id:int peer_id:flags.0?Peer = Message; -message#38116ee0 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true noforwards:flags.26?true invert_media:flags.27?true id:int from_id:flags.8?Peer peer_id:Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long reactions:flags.20?MessageReactions restriction_reason:flags.22?Vector ttl_period:flags.25?int = Message; +message#76bec211 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true noforwards:flags.26?true invert_media:flags.27?true id:int from_id:flags.8?Peer peer_id:Peer saved_peer_id:flags.28?Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long reactions:flags.20?MessageReactions restriction_reason:flags.22?Vector ttl_period:flags.25?int = Message; messageService#2b085862 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true legacy:flags.19?true id:int from_id:flags.8?Peer peer_id:Peer reply_to:flags.3?MessageReplyHeader date:int action:MessageAction ttl_period:flags.25?int = Message; messageMediaEmpty#3ded6320 = MessageMedia; @@ -122,7 +122,7 @@ messageMediaPhoto#695150d7 flags:# spoiler:flags.3?true photo:flags.0?Photo ttl_ messageMediaGeo#56e0d474 geo:GeoPoint = MessageMedia; messageMediaContact#70322949 phone_number:string first_name:string last_name:string vcard:string user_id:long = MessageMedia; messageMediaUnsupported#9f84f49e = MessageMedia; -messageMediaDocument#4cf4d72d flags:# nopremium:flags.3?true spoiler:flags.4?true document:flags.0?Document alt_document:flags.5?Document ttl_seconds:flags.2?int = MessageMedia; +messageMediaDocument#4cf4d72d flags:# nopremium:flags.3?true spoiler:flags.4?true video:flags.6?true round:flags.7?true voice:flags.8?true document:flags.0?Document alt_document:flags.5?Document ttl_seconds:flags.2?int = MessageMedia; messageMediaWebPage#ddf10c3b flags:# force_large_media:flags.0?true force_small_media:flags.1?true manual:flags.3?true safe:flags.4?true webpage:WebPage = MessageMedia; messageMediaVenue#2ec0533f geo:GeoPoint title:string address:string provider:string venue_id:string venue_type:string = MessageMedia; messageMediaGame#fdb19008 game:Game = MessageMedia; @@ -131,7 +131,8 @@ messageMediaGeoLive#b940c666 flags:# geo:GeoPoint heading:flags.0?int period:int messageMediaPoll#4bd6e798 poll:Poll results:PollResults = MessageMedia; messageMediaDice#3f7ee58b value:int emoticon:string = MessageMedia; messageMediaStory#68cb6283 flags:# via_mention:flags.1?true peer:Peer id:int story:flags.0?StoryItem = MessageMedia; -messageMediaGiveaway#58260664 flags:# only_new_subscribers:flags.0?true channels:Vector countries_iso2:flags.1?Vector quantity:int months:int until_date:int = MessageMedia; +messageMediaGiveaway#daad85b0 flags:# only_new_subscribers:flags.0?true winners_are_visible:flags.2?true channels:Vector countries_iso2:flags.1?Vector prize_description:flags.3?string quantity:int months:int until_date:int = MessageMedia; +messageMediaGiveawayResults#c6991068 flags:# only_new_subscribers:flags.0?true refunded:flags.2?true channel_id:long additional_peers_count:flags.3?int launch_msg_id:int winners_count:int unclaimed_count:int winners:Vector months:int prize_description:flags.1?string until_date:int = MessageMedia; messageActionEmpty#b6aef7b0 = MessageAction; messageActionChatCreate#bd47cbad title:string users:Vector = MessageAction; @@ -169,9 +170,9 @@ messageActionGiftPremium#c83d6aec flags:# currency:string amount:long months:int messageActionTopicCreate#d999256 flags:# title:string icon_color:int icon_emoji_id:flags.0?long = MessageAction; messageActionTopicEdit#c0944820 flags:# title:flags.0?string icon_emoji_id:flags.1?long closed:flags.2?Bool hidden:flags.3?Bool = MessageAction; messageActionSuggestProfilePhoto#57de635e photo:Photo = MessageAction; -messageActionRequestedPeer#fe77345d button_id:int peer:Peer = MessageAction; +messageActionRequestedPeer#31518e9b button_id:int peers:Vector = MessageAction; messageActionSetChatWallPaper#5060a3f4 flags:# same:flags.0?true for_both:flags.1?true wallpaper:WallPaper = MessageAction; -messageActionGiftCode#d2cfdb0e flags:# via_giveaway:flags.0?true unclaimed:flags.2?true boost_peer:flags.1?Peer months:int slug:string = MessageAction; +messageActionGiftCode#678c2e09 flags:# via_giveaway:flags.0?true unclaimed:flags.2?true boost_peer:flags.1?Peer months:int slug:string currency:flags.2?string amount:flags.2?long crypto_currency:flags.3?string crypto_amount:flags.3?long = MessageAction; messageActionGiveawayLaunch#332ba9ed = MessageAction; messageActionGiveawayResults#2a9fadc5 winners_count:int unclaimed_count:int = MessageAction; @@ -395,6 +396,10 @@ updateSentStoryReaction#7d627683 peer:Peer story_id:int reaction:Reaction = Upda updateBotChatBoost#904dd49c peer:Peer boost:Boost qts:int = Update; updateChannelViewForumAsMessages#7b68920 channel_id:long enabled:Bool = Update; updatePeerWallpaper#ae3f101d flags:# wallpaper_overridden:flags.1?true peer:Peer wallpaper:flags.0?WallPaper = Update; +updateBotMessageReaction#ac21d3ce peer:Peer msg_id:int date:int actor:Peer old_reactions:Vector new_reactions:Vector qts:int = Update; +updateBotMessageReactions#9cb7759 peer:Peer msg_id:int date:int reactions:Vector qts:int = Update; +updateSavedDialogPinned#aeaf9e74 flags:# pinned:flags.0?true peer:DialogPeer = Update; +updatePinnedSavedDialogs#686c85a6 flags:# order:flags.0?Vector = Update; updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State; @@ -591,8 +596,9 @@ inputStickerSetPremiumGifts#c88b3b02 = InputStickerSet; inputStickerSetEmojiGenericAnimations#4c4d4ce = InputStickerSet; inputStickerSetEmojiDefaultStatuses#29d0f5ee = InputStickerSet; inputStickerSetEmojiDefaultTopicIcons#44c1f8e9 = InputStickerSet; +inputStickerSetEmojiChannelDefaultStatuses#49748553 = InputStickerSet; -stickerSet#2dd14edc flags:# archived:flags.1?true official:flags.2?true masks:flags.3?true animated:flags.5?true videos:flags.6?true emojis:flags.7?true text_color:flags.9?true installed_date:flags.0?int id:long access_hash:long title:string short_name:string thumbs:flags.4?Vector thumb_dc_id:flags.4?int thumb_version:flags.4?int thumb_document_id:flags.8?long count:int hash:int = StickerSet; +stickerSet#2dd14edc flags:# archived:flags.1?true official:flags.2?true masks:flags.3?true animated:flags.5?true videos:flags.6?true emojis:flags.7?true text_color:flags.9?true channel_emoji_status:flags.10?true installed_date:flags.0?int id:long access_hash:long title:string short_name:string thumbs:flags.4?Vector thumb_dc_id:flags.4?int thumb_version:flags.4?int thumb_document_id:flags.8?long count:int hash:int = StickerSet; messages.stickerSet#6e153f16 set:StickerSet packs:Vector keywords:Vector documents:Vector = messages.StickerSet; messages.stickerSetNotModified#d3f924eb = messages.StickerSet; @@ -616,7 +622,7 @@ inputKeyboardButtonUserProfile#e988037b text:string user_id:InputUser = Keyboard keyboardButtonUserProfile#308660c1 text:string user_id:long = KeyboardButton; keyboardButtonWebView#13767230 text:string url:string = KeyboardButton; keyboardButtonSimpleWebView#a0c0505c text:string url:string = KeyboardButton; -keyboardButtonRequestPeer#d0b468c text:string button_id:int peer_type:RequestPeerType = KeyboardButton; +keyboardButtonRequestPeer#53d7bfd8 text:string button_id:int peer_type:RequestPeerType max_quantity:int = KeyboardButton; keyboardButtonRow#77608b83 buttons:Vector = KeyboardButtonRow; @@ -717,7 +723,7 @@ messages.botResults#e021f2f6 flags:# gallery:flags.0?true query_id:long next_off exportedMessageLink#5dab1af4 link:string html:string = ExportedMessageLink; -messageFwdHeader#5f777dce flags:# imported:flags.7?true from_id:flags.0?Peer from_name:flags.5?string date:int channel_post:flags.2?int post_author:flags.3?string saved_from_peer:flags.4?Peer saved_from_msg_id:flags.4?int psa_type:flags.6?string = MessageFwdHeader; +messageFwdHeader#4e4df4bb flags:# imported:flags.7?true saved_out:flags.11?true from_id:flags.0?Peer from_name:flags.5?string date:int channel_post:flags.2?int post_author:flags.3?string saved_from_peer:flags.4?Peer saved_from_msg_id:flags.4?int saved_from_id:flags.8?Peer saved_from_name:flags.9?string saved_date:flags.10?int psa_type:flags.6?string = MessageFwdHeader; auth.codeTypeSms#72a3158c = auth.CodeType; auth.codeTypeCall#741cd3e3 = auth.CodeType; @@ -969,8 +975,10 @@ channelAdminLogEventActionEditTopic#f06fe208 prev_topic:ForumTopic new_topic:For channelAdminLogEventActionDeleteTopic#ae168909 topic:ForumTopic = ChannelAdminLogEventAction; channelAdminLogEventActionPinTopic#5d8d353b flags:# prev_topic:flags.0?ForumTopic new_topic:flags.1?ForumTopic = ChannelAdminLogEventAction; channelAdminLogEventActionToggleAntiSpam#64f36dfc new_value:Bool = ChannelAdminLogEventAction; -channelAdminLogEventActionChangeColor#3c2b247b prev_value:int new_value:int = ChannelAdminLogEventAction; -channelAdminLogEventActionChangeBackgroundEmoji#445fc434 prev_value:long new_value:long = ChannelAdminLogEventAction; +channelAdminLogEventActionChangePeerColor#5796e780 prev_value:PeerColor new_value:PeerColor = ChannelAdminLogEventAction; +channelAdminLogEventActionChangeProfilePeerColor#5e477b25 prev_value:PeerColor new_value:PeerColor = ChannelAdminLogEventAction; +channelAdminLogEventActionChangeWallpaper#31bb5d52 prev_value:WallPaper new_value:WallPaper = ChannelAdminLogEventAction; +channelAdminLogEventActionChangeEmojiStatus#3ea9feb1 prev_value:EmojiStatus new_value:EmojiStatus = ChannelAdminLogEventAction; channelAdminLogEvent#1fad68cd id:long date:int user_id:long action:ChannelAdminLogEventAction = ChannelAdminLogEvent; @@ -1147,7 +1155,7 @@ account.wallPapers#cdc3858c hash:long wallpapers:Vector = account.Wal codeSettings#ad253d78 flags:# allow_flashcall:flags.0?true current_number:flags.1?true allow_app_hash:flags.4?true allow_missed_call:flags.5?true allow_firebase:flags.7?true logout_tokens:flags.6?Vector token:flags.8?string app_sandbox:flags.8?Bool = CodeSettings; -wallPaperSettings#1dc1bca4 flags:# blur:flags.1?true motion:flags.2?true background_color:flags.0?int second_background_color:flags.4?int third_background_color:flags.5?int fourth_background_color:flags.6?int intensity:flags.3?int rotation:flags.4?int = WallPaperSettings; +wallPaperSettings#372efcd0 flags:# blur:flags.1?true motion:flags.2?true background_color:flags.0?int second_background_color:flags.4?int third_background_color:flags.5?int fourth_background_color:flags.6?int intensity:flags.3?int rotation:flags.4?int emoticon:flags.7?string = WallPaperSettings; autoDownloadSettings#baa57628 flags:# disabled:flags.0?true video_preload_large:flags.1?true audio_preload_next:flags.2?true phonecalls_less_data:flags.3?true stories_preload:flags.4?true photo_size_max:int video_size_max:long file_size_max:long video_upload_maxbitrate:int small_queue_active_operations_max:int large_queue_active_operations_max:int = AutoDownloadSettings; @@ -1426,7 +1434,7 @@ help.premiumPromo#5334759c status_text:string status_entities:Vector boost_peer:flags.0?InputPeer currency:string amount:long = InputStorePaymentPurpose; -inputStorePaymentPremiumGiveaway#7c9375e6 flags:# only_new_subscribers:flags.0?true boost_peer:InputPeer additional_peers:flags.1?Vector countries_iso2:flags.2?Vector random_id:long until_date:int currency:string amount:long = InputStorePaymentPurpose; +inputStorePaymentPremiumGiveaway#160544ca flags:# only_new_subscribers:flags.0?true winners_are_visible:flags.3?true boost_peer:InputPeer additional_peers:flags.1?Vector countries_iso2:flags.2?Vector prize_description:flags.4?string random_id:long until_date:int currency:string amount:long = InputStorePaymentPurpose; premiumGiftOption#74c34319 flags:# months:int currency:string amount:long bot_url:string store_product:flags.0?string = PremiumGiftOption; @@ -1553,8 +1561,10 @@ stories.allStories#6efc5e81 flags:# has_more:flags.0?true count:int state:string stories.stories#5dd8c3c8 count:int stories:Vector chats:Vector users:Vector = stories.Stories; storyView#b0bdeac5 flags:# blocked:flags.0?true blocked_my_stories_from:flags.1?true user_id:long date:int reaction:flags.2?Reaction = StoryView; +storyViewPublicForward#9083670b flags:# blocked:flags.0?true blocked_my_stories_from:flags.1?true message:Message = StoryView; +storyViewPublicRepost#bd74cf49 flags:# blocked:flags.0?true blocked_my_stories_from:flags.1?true peer_id:Peer story:StoryItem = StoryView; -stories.storyViewsList#46e9b9ec flags:# count:int reactions_count:int views:Vector users:Vector next_offset:flags.0?string = stories.StoryViewsList; +stories.storyViewsList#59d78fc5 flags:# count:int views_count:int forwards_count:int reactions_count:int views:Vector chats:Vector users:Vector next_offset:flags.0?string = stories.StoryViewsList; stories.storyViews#de9eed1d views:Vector users:Vector = stories.StoryViews; @@ -1571,6 +1581,8 @@ mediaAreaVenue#be82db9c coordinates:MediaAreaCoordinates geo:GeoPoint title:stri inputMediaAreaVenue#b282217f coordinates:MediaAreaCoordinates query_id:long result_id:string = MediaArea; mediaAreaGeoPoint#df8b3b22 coordinates:MediaAreaCoordinates geo:GeoPoint = MediaArea; mediaAreaSuggestedReaction#14455871 flags:# dark:flags.0?true flipped:flags.1?true coordinates:MediaAreaCoordinates reaction:Reaction = MediaArea; +mediaAreaChannelPost#770416af coordinates:MediaAreaCoordinates channel_id:long msg_id:int = MediaArea; +inputMediaAreaChannelPost#2271f2bf coordinates:MediaAreaCoordinates channel:InputChannel msg_id:int = MediaArea; peerStories#9a35e999 flags:# peer:Peer max_read_id:flags.0?int stories:Vector = PeerStories; @@ -1580,7 +1592,7 @@ messages.webPage#fd5e12bd webpage:WebPage chats:Vector users:Vector premiumGiftCodeOption#257e962b flags:# users:int months:int store_product:flags.0?string store_quantity:flags.1?int currency:string amount:long = PremiumGiftCodeOption; -payments.checkedGiftCode#b722f158 flags:# via_giveaway:flags.2?true from_id:Peer giveaway_msg_id:flags.3?int to_id:flags.0?long date:int months:int used_date:flags.1?int chats:Vector users:Vector = payments.CheckedGiftCode; +payments.checkedGiftCode#284a1096 flags:# via_giveaway:flags.2?true from_id:flags.4?Peer giveaway_msg_id:flags.3?int to_id:flags.0?long date:int months:int used_date:flags.1?int chats:Vector users:Vector = payments.CheckedGiftCode; payments.giveawayInfo#4367daa0 flags:# participating:flags.0?true preparing_results:flags.3?true start_date:int joined_too_early_date:flags.1?int admin_disallowed_chat_id:flags.2?long disallowed_country:flags.4?string = payments.GiveawayInfo; payments.giveawayInfoResults#cd5570 flags:# winner:flags.0?true refunded:flags.1?true start_date:int gift_code_slug:flags.0?string finish_date:int winners_count:int activated_count:int = payments.GiveawayInfo; @@ -1614,11 +1626,23 @@ peerColor#b54b5acf flags:# color:flags.0?int background_emoji_id:flags.1?long = help.peerColorSet#26219a58 colors:Vector = help.PeerColorSet; help.peerColorProfileSet#767d61eb palette_colors:Vector bg_colors:Vector story_colors:Vector = help.PeerColorSet; -help.peerColorOption#135bd42f flags:# hidden:flags.0?true color_id:int colors:flags.1?help.PeerColorSet dark_colors:flags.2?help.PeerColorSet = help.PeerColorOption; +help.peerColorOption#ef8430ab flags:# hidden:flags.0?true color_id:int colors:flags.1?help.PeerColorSet dark_colors:flags.2?help.PeerColorSet channel_min_level:flags.3?int = help.PeerColorOption; help.peerColorsNotModified#2ba1f5ce = help.PeerColors; help.peerColors#f8ed08 hash:int colors:Vector = help.PeerColors; +storyReaction#6090d6d5 peer_id:Peer date:int reaction:Reaction = StoryReaction; +storyReactionPublicForward#bbab2643 message:Message = StoryReaction; +storyReactionPublicRepost#cfcd0f13 peer_id:Peer story:StoryItem = StoryReaction; + +stories.storyReactionsList#aa5f789c flags:# count:int reactions:Vector chats:Vector users:Vector next_offset:flags.0?string = stories.StoryReactionsList; + +savedDialog#bd87cb6c flags:# pinned:flags.2?true peer:Peer top_message:int = SavedDialog; + +messages.savedDialogs#f83ae221 dialogs:Vector messages:Vector chats:Vector users:Vector = messages.SavedDialogs; +messages.savedDialogsSlice#44ba9dd9 count:int dialogs:Vector messages:Vector chats:Vector users:Vector = messages.SavedDialogs; +messages.savedDialogsNotModified#c01f6fe8 count:int = messages.SavedDialogs; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -1742,6 +1766,8 @@ account.deleteAutoSaveExceptions#53bc0020 = Bool; account.invalidateSignInCodes#ca8ae8ba codes:Vector = Bool; account.updateColor#7cefa15d flags:# for_profile:flags.1?true color:flags.2?int background_emoji_id:flags.0?long = Bool; account.getDefaultBackgroundEmojis#a60ab9ce hash:long = EmojiList; +account.getChannelDefaultEmojiStatuses#7727a7d5 hash:long = account.EmojiStatuses; +account.getChannelRestrictedStatusEmojis#35a9e0d5 hash:long = EmojiList; users.getUsers#d91a548 id:Vector = Vector; users.getFullUser#b60f5918 id:InputUser = users.UserFull; @@ -1776,7 +1802,7 @@ contacts.setBlocked#94c65c76 flags:# my_stories_from:flags.0?true id:Vector = messages.Messages; messages.getDialogs#a0f4cb4f flags:# exclude_pinned:flags.0?true folder_id:flags.1?int offset_date:int offset_id:int offset_peer:InputPeer limit:int hash:long = messages.Dialogs; messages.getHistory#4423e6c5 peer:InputPeer offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int hash:long = messages.Messages; -messages.search#a0fda762 flags:# peer:InputPeer q:string from_id:flags.0?InputPeer top_msg_id:flags.1?int filter:MessagesFilter min_date:int max_date:int offset_id:int add_offset:int limit:int max_id:int min_id:int hash:long = messages.Messages; +messages.search#a7b4e929 flags:# peer:InputPeer q:string from_id:flags.0?InputPeer saved_peer_id:flags.2?InputPeer top_msg_id:flags.1?int filter:MessagesFilter min_date:int max_date:int offset_id:int add_offset:int limit:int max_id:int min_id:int hash:long = messages.Messages; messages.readHistory#e306d3a peer:InputPeer max_id:int = messages.AffectedMessages; messages.deleteHistory#b08f922a flags:# just_clear:flags.0?true revoke:flags.1?true peer:InputPeer max_id:int min_date:flags.2?int max_date:flags.3?int = messages.AffectedHistory; messages.deleteMessages#e58e95d2 flags:# revoke:flags.0?true id:Vector = messages.AffectedMessages; @@ -1879,7 +1905,7 @@ messages.getEmojiKeywords#35a0e062 lang_code:string = EmojiKeywordsDifference; messages.getEmojiKeywordsDifference#1508b6af lang_code:string from_version:int = EmojiKeywordsDifference; messages.getEmojiKeywordsLanguages#4e9963b2 lang_codes:Vector = Vector; messages.getEmojiURL#d5b10c26 lang_code:string = EmojiURL; -messages.getSearchCounters#ae7cc1 flags:# peer:InputPeer top_msg_id:flags.0?int filters:Vector = Vector; +messages.getSearchCounters#1bbcf300 flags:# peer:InputPeer saved_peer_id:flags.2?InputPeer top_msg_id:flags.0?int filters:Vector = Vector; messages.requestUrlAuth#198fb446 flags:# peer:flags.1?InputPeer msg_id:flags.1?int button_id:flags.1?int url:flags.2?string = UrlAuthResult; messages.acceptUrlAuth#b12c7125 flags:# write_allowed:flags.0?true peer:flags.1?InputPeer msg_id:flags.1?int button_id:flags.1?int url:flags.2?string = UrlAuthResult; messages.hidePeerSettingsBar#4facb138 peer:InputPeer = Bool; @@ -1915,8 +1941,8 @@ messages.setHistoryTTL#b80e5fe4 peer:InputPeer period:int = Updates; messages.checkHistoryImportPeer#5dc60f03 peer:InputPeer = messages.CheckedHistoryImportPeer; messages.setChatTheme#e63be13f peer:InputPeer emoticon:string = Updates; messages.getMessageReadParticipants#31c1c44f peer:InputPeer msg_id:int = Vector; -messages.getSearchResultsCalendar#49f0bde9 peer:InputPeer filter:MessagesFilter offset_id:int offset_date:int = messages.SearchResultsCalendar; -messages.getSearchResultsPositions#6e9583a3 peer:InputPeer filter:MessagesFilter offset_id:int limit:int = messages.SearchResultsPositions; +messages.getSearchResultsCalendar#6aa3f6bd flags:# peer:InputPeer saved_peer_id:flags.2?InputPeer filter:MessagesFilter offset_id:int offset_date:int = messages.SearchResultsCalendar; +messages.getSearchResultsPositions#9c7f2f10 flags:# peer:InputPeer saved_peer_id:flags.2?InputPeer filter:MessagesFilter offset_id:int limit:int = messages.SearchResultsPositions; messages.hideChatJoinRequest#7fe7e815 flags:# approved:flags.0?true peer:InputPeer user_id:InputUser = Updates; messages.hideAllChatJoinRequests#e085f4ea flags:# approved:flags.0?true peer:InputPeer link:flags.1?string = Updates; messages.toggleNoForwards#b11eafa2 peer:InputPeer enabled:Bool = Updates; @@ -1951,7 +1977,7 @@ messages.clearRecentReactions#9dfeefb4 = Bool; messages.getExtendedMedia#84f80814 peer:InputPeer id:Vector = Updates; messages.setDefaultHistoryTTL#9eb51445 period:int = Bool; messages.getDefaultHistoryTTL#658b7188 = DefaultHistoryTTL; -messages.sendBotRequestedPeer#fe38d01b peer:InputPeer msg_id:int button_id:int requested_peer:InputPeer = Updates; +messages.sendBotRequestedPeer#91b2d060 peer:InputPeer msg_id:int button_id:int requested_peers:Vector = Updates; messages.getEmojiGroups#7488ce5b hash:int = messages.EmojiGroups; messages.getEmojiStatusGroups#2ecd56cd hash:int = messages.EmojiGroups; messages.getEmojiProfilePhotoGroups#21a548f3 hash:int = messages.EmojiGroups; @@ -1961,6 +1987,12 @@ messages.getBotApp#34fdc5c3 app:InputBotApp hash:long = messages.BotApp; messages.requestAppWebView#8c5a3b3c flags:# write_allowed:flags.0?true peer:InputPeer app:InputBotApp start_param:flags.1?string theme_params:flags.2?DataJSON platform:string = AppWebViewResult; messages.setChatWallPaper#8ffacae1 flags:# for_both:flags.3?true revert:flags.4?true peer:InputPeer wallpaper:flags.0?InputWallPaper settings:flags.2?WallPaperSettings id:flags.1?int = Updates; messages.searchEmojiStickerSets#92b4494c flags:# exclude_featured:flags.0?true q:string hash:long = messages.FoundStickerSets; +messages.getSavedDialogs#5381d21a flags:# exclude_pinned:flags.0?true offset_date:int offset_id:int offset_peer:InputPeer limit:int hash:long = messages.SavedDialogs; +messages.getSavedHistory#3d9a414d peer:InputPeer offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int hash:long = messages.Messages; +messages.deleteSavedHistory#6e98102b flags:# peer:InputPeer max_id:int min_date:flags.2?int max_date:flags.3?int = messages.AffectedHistory; +messages.getPinnedSavedDialogs#d63d94e0 = messages.SavedDialogs; +messages.toggleSavedDialogPin#ac81bbde flags:# pinned:flags.0?true peer:InputDialogPeer = Bool; +messages.reorderPinnedSavedDialogs#8b716587 flags:# force:flags.0?true order:Vector = Bool; updates.getState#edd4882a = updates.State; updates.getDifference#19c2f763 flags:# pts:int pts_limit:flags.1?int pts_total_limit:flags.0?int date:int qts:int qts_limit:flags.2?int = updates.Difference; @@ -1986,7 +2018,6 @@ help.getNearestDc#1fb33026 = NearestDc; help.getAppUpdate#522d5a7d source:string = help.AppUpdate; help.getInviteText#4d392343 = help.InviteText; help.getSupport#9cdf08cd = help.Support; -help.getAppChangelog#9010ef6f prev_app_version:string = Updates; help.setBotUpdatesStatus#ec22cfcd pending_updates_count:int message:string = Bool; help.getCdnConfig#52029342 = CdnConfig; help.getRecentMeUrls#3dc0f114 referer:string = help.RecentMeUrls; @@ -2063,9 +2094,10 @@ channels.toggleAntiSpam#68f3e4eb channel:InputChannel enabled:Bool = Updates; channels.reportAntiSpamFalsePositive#a850a693 channel:InputChannel msg_id:int = Bool; channels.toggleParticipantsHidden#6a6e7854 channel:InputChannel enabled:Bool = Updates; channels.clickSponsoredMessage#18afbc93 channel:InputChannel random_id:bytes = Bool; -channels.updateColor#621a201f flags:# channel:InputChannel color:int background_emoji_id:flags.0?long = Updates; +channels.updateColor#d8aa3671 flags:# for_profile:flags.1?true channel:InputChannel color:flags.2?int background_emoji_id:flags.0?long = Updates; channels.toggleViewForumAsMessages#9738bb15 channel:InputChannel enabled:Bool = Updates; channels.getChannelRecommendations#83b70d97 channel:InputChannel = messages.Chats; +channels.updateEmojiStatus#f0d3e6a8 channel:InputChannel emoji_status:EmojiStatus = Updates; bots.sendCustomRequest#aa2769ed custom_method:string params:DataJSON = DataJSON; bots.answerWebhookJSONQuery#e6213f4d query_id:long data:DataJSON = Bool; @@ -2155,7 +2187,7 @@ folders.editPeerFolders#6847d0ab folder_peers:Vector = Updates; stats.getBroadcastStats#ab42441a flags:# dark:flags.0?true channel:InputChannel = stats.BroadcastStats; stats.loadAsyncGraph#621d5fa0 flags:# token:string x:flags.0?long = StatsGraph; stats.getMegagroupStats#dcdf8607 flags:# dark:flags.0?true channel:InputChannel = stats.MegagroupStats; -stats.getMessagePublicForwards#5630281b channel:InputChannel msg_id:int offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages; +stats.getMessagePublicForwards#5f150144 channel:InputChannel msg_id:int offset:string limit:int = stats.PublicForwards; stats.getMessageStats#b6e0a3f5 flags:# dark:flags.0?true channel:InputChannel msg_id:int = stats.MessageStats; stats.getStoryStats#374fef40 flags:# dark:flags.0?true peer:InputPeer id:int = stats.StoryStats; stats.getStoryPublicForwards#a6437ef6 peer:InputPeer id:int offset:string limit:int = stats.PublicForwards; @@ -2184,7 +2216,7 @@ stories.getStoriesByID#5774ca74 peer:InputPeer id:Vector = stories.Stories; stories.toggleAllStoriesHidden#7c2557c4 hidden:Bool = Bool; stories.readStories#a556dac8 peer:InputPeer max_id:int = Vector; stories.incrementStoryViews#b2028afb peer:InputPeer id:Vector = Bool; -stories.getStoryViewsList#7ed23c57 flags:# just_contacts:flags.0?true reactions_first:flags.2?true peer:InputPeer q:flags.1?string id:int offset:string limit:int = stories.StoryViewsList; +stories.getStoryViewsList#7ed23c57 flags:# just_contacts:flags.0?true reactions_first:flags.2?true forwards_first:flags.3?true peer:InputPeer q:flags.1?string id:int offset:string limit:int = stories.StoryViewsList; stories.getStoriesViews#28e16cc8 peer:InputPeer id:Vector = stories.StoryViews; stories.exportStoryLink#7b8def20 peer:InputPeer id:int = ExportedStoryLink; stories.report#1923fa8c peer:InputPeer id:Vector reason:ReportReason message:string = Bool; @@ -2195,6 +2227,7 @@ stories.getAllReadPeerStories#9b5ae7f9 = Updates; stories.getPeerMaxIDs#535983c3 id:Vector = Vector; stories.getChatsToSend#a56a8b60 = messages.Chats; stories.togglePeerStoriesHidden#bd0415c4 peer:InputPeer hidden:Bool = Bool; +stories.getStoryReactionsList#b9b2881f flags:# forwards_first:flags.2?true peer:InputPeer id:int reaction:flags.0?Reaction offset:flags.1?string limit:int = stories.StoryReactionsList; premium.getBoostsList#60f67660 flags:# gifts:flags.0?true peer:InputPeer offset:string limit:int = premium.BoostsList; premium.getMyBoosts#be77b4a = premium.MyBoosts; @@ -2202,4 +2235,4 @@ premium.applyBoost#6b7da746 flags:# slots:flags.0?Vector peer:InputPeer = p premium.getBoostsStatus#42f1f61 peer:InputPeer = premium.BoostsStatus; premium.getUserBoosts#39854d1f peer:InputPeer user_id:InputUser = premium.BoostsList; -// LAYER 167 +// LAYER 170 diff --git a/Telegram/SourceFiles/mtproto/special_config_request.cpp b/Telegram/SourceFiles/mtproto/special_config_request.cpp index d0ca191879c4b..9736518110b08 100644 --- a/Telegram/SourceFiles/mtproto/special_config_request.cpp +++ b/Telegram/SourceFiles/mtproto/special_config_request.cpp @@ -62,8 +62,9 @@ QString InstanceId() { } bool CheckPhoneByPrefixesRules(const QString &phone, const QString &rules) { + static const auto RegExp = QRegularExpression("[^0-9]"); const auto check = QString(phone).replace( - QRegularExpression("[^0-9]"), + RegExp, QString()); auto result = false; for (const auto &prefix : rules.split(',')) { diff --git a/Telegram/SourceFiles/overview/overview_layout.cpp b/Telegram/SourceFiles/overview/overview_layout.cpp index 26413eb002d14..a7eee4c1955b4 100644 --- a/Telegram/SourceFiles/overview/overview_layout.cpp +++ b/Telegram/SourceFiles/overview/overview_layout.cpp @@ -1693,7 +1693,7 @@ Link::Link( domain = parts.at(2); } - parts = domain.split('@').back().split('.', Qt::SkipEmptyParts); + parts = domain.split('@').constLast().split('.', Qt::SkipEmptyParts); if (parts.size() > 1) { _letter = parts.at(parts.size() - 2).at(0).toUpper(); if (_title.isEmpty()) { diff --git a/Telegram/SourceFiles/passport/passport_panel_controller.cpp b/Telegram/SourceFiles/passport/passport_panel_controller.cpp index ddfd24ce429d8..14ee9bd789f7a 100644 --- a/Telegram/SourceFiles/passport/passport_panel_controller.cpp +++ b/Telegram/SourceFiles/passport/passport_panel_controller.cpp @@ -152,11 +152,12 @@ EditDocumentScheme GetDocumentScheme( }; using Result = std::optional; const auto NameValidate = [](const QString &value) -> Result { + static const auto RegExp = QRegularExpression( + "^[a-zA-Z0-9\\.,/&\\-' ]+$" + ); if (value.isEmpty() || value.size() > kMaxNameSize) { return QString(); - } else if (!QRegularExpression( - "^[a-zA-Z0-9\\.,/&\\-' ]+$" - ).match(value).hasMatch()) { + } else if (!RegExp.match(value).hasMatch()) { return tr::lng_passport_bad_name(tr::now); } return std::nullopt; @@ -167,14 +168,16 @@ EditDocumentScheme GetDocumentScheme( const auto StreetValidate = LimitedValidate(kMaxStreetSize); const auto CityValidate = LimitedValidate(kMaxCitySize, kMinCitySize); const auto PostcodeValidate = FromBoolean([](const QString &value) { - return QRegularExpression( + static const auto RegExp = QRegularExpression( QString("^[a-zA-Z0-9\\-]{2,%1}$").arg(kMaxPostcodeSize) - ).match(value).hasMatch(); + ); + return RegExp.match(value).hasMatch(); }); const auto DateValidateBoolean = [](const QString &value) { - return QRegularExpression( + static const auto RegExp = QRegularExpression( "^\\d{2}\\.\\d{2}\\.\\d{4}$" - ).match(value).hasMatch(); + ); + return RegExp.match(value).hasMatch(); }; const auto DateValidate = FromBoolean(DateValidateBoolean); const auto DateOrEmptyValidate = FromBoolean([=](const QString &value) { @@ -479,9 +482,8 @@ EditContactScheme GetContactScheme(Scope::Type type) { result.newHeader = tr::lng_passport_new_phone(tr::now); result.aboutNew = tr::lng_passport_new_phone_code(tr::now); result.validate = [](const QString &value) { - return QRegularExpression( - "^\\d{2,12}$" - ).match(value).hasMatch(); + static const auto RegExp = QRegularExpression("^\\d{2,12}$"); + return RegExp.match(value).hasMatch(); }; result.format = [](const QString &value) { return Ui::FormatPhone(value); diff --git a/Telegram/SourceFiles/passport/ui/passport_details_row.cpp b/Telegram/SourceFiles/passport/ui/passport_details_row.cpp index 2fc7c3698a5e4..b50ff5164b5c1 100644 --- a/Telegram/SourceFiles/passport/ui/passport_details_row.cpp +++ b/Telegram/SourceFiles/passport/ui/passport_details_row.cpp @@ -51,7 +51,8 @@ PostcodeInput::PostcodeInput( rpl::producer placeholder, const QString &val) : MaskedInputField(parent, st, std::move(placeholder), val) { - if (!QRegularExpression("^[a-zA-Z0-9\\-]+$").match(val).hasMatch()) { + static const auto RegExp = QRegularExpression("^[a-zA-Z0-9\\-]+$"); + if (!RegExp.match(val).hasMatch()) { setText(QString()); } } @@ -414,8 +415,9 @@ void CountryRow::chooseCountry() { } QDate ValidateDate(const QString &value) { - const auto match = QRegularExpression( - "^([0-9]{2})\\.([0-9]{2})\\.([0-9]{4})$").match(value); + static const auto RegExp = QRegularExpression( + "^([0-9]{2})\\.([0-9]{2})\\.([0-9]{4})$"); + const auto match = RegExp.match(value); if (!match.hasMatch()) { return QDate(); } diff --git a/Telegram/SourceFiles/payments/payments_form.cpp b/Telegram/SourceFiles/payments/payments_form.cpp index 0ad30366919d7..9ca9bc66811f9 100644 --- a/Telegram/SourceFiles/payments/payments_form.cpp +++ b/Telegram/SourceFiles/payments/payments_form.cpp @@ -145,7 +145,13 @@ MTPinputStorePaymentPurpose InvoicePremiumGiftCodeGiveawayToTL( : Flag::f_additional_peers) | (giveaway.countries.empty() ? Flag() - : Flag::f_countries_iso2)), + : Flag::f_countries_iso2) + | (giveaway.showWinners + ? Flag::f_winners_are_visible + : Flag()) + | (giveaway.additionalPrize.isEmpty() + ? Flag() + : Flag::f_prize_description)), giveaway.boostPeer->input, MTP_vector_from_range(ranges::views::all( giveaway.additionalChannels @@ -157,6 +163,7 @@ MTPinputStorePaymentPurpose InvoicePremiumGiftCodeGiveawayToTL( ) | ranges::views::transform([](QString value) { return MTP_string(value); })), + MTP_string(giveaway.additionalPrize), MTP_long(invoice.randomId), MTP_int(giveaway.untilDate), MTP_string(invoice.currency), @@ -668,6 +675,7 @@ void Form::fillSmartGlocalNativeMethod(QJsonObject object) { _paymentMethod.native = NativePaymentMethod{ .data = SmartGlocalPaymentMethod{ .publicToken = key, + .tokenizeUrl = value(u"tokenize_url").toString(), }, }; _paymentMethod.ui.native = Ui::NativeMethodDetails{ @@ -977,8 +985,7 @@ void Form::validateCard( if (error) { LOG(("Stripe Error %1: %2 (%3)" ).arg(int(error.code()) - ).arg(error.description() - ).arg(error.message())); + ).arg(error.description(), error.message())); _updates.fire(Error{ Error::Type::Stripe, error.description() }); } else { setPaymentCredentials({ @@ -1004,6 +1011,7 @@ void Form::validateCard( } auto configuration = SmartGlocal::PaymentConfiguration{ .publicToken = method.publicToken, + .tokenizeUrl = method.tokenizeUrl, .isTest = _invoice.isTest, }; _smartglocal = std::make_unique( @@ -1027,8 +1035,7 @@ void Form::validateCard( if (error) { LOG(("SmartGlocal Error %1: %2 (%3)" ).arg(int(error.code()) - ).arg(error.description() - ).arg(error.message())); + ).arg(error.description(), error.message())); _updates.fire(Error{ Error::Type::SmartGlocal, error.description(), diff --git a/Telegram/SourceFiles/payments/payments_form.h b/Telegram/SourceFiles/payments/payments_form.h index acf3750439e54..414eb0b021637 100644 --- a/Telegram/SourceFiles/payments/payments_form.h +++ b/Telegram/SourceFiles/payments/payments_form.h @@ -95,6 +95,7 @@ struct StripePaymentMethod { struct SmartGlocalPaymentMethod { QString publicToken; + QString tokenizeUrl; }; struct NativePaymentMethod { @@ -190,8 +191,10 @@ struct InvoicePremiumGiftCodeGiveaway { not_null boostPeer; std::vector> additionalChannels; std::vector countries; + QString additionalPrize; TimeId untilDate = 0; bool onlyNewSubscribers = false; + bool showWinners = false; }; struct InvoicePremiumGiftCodeUsers { diff --git a/Telegram/SourceFiles/payments/smartglocal/smartglocal_api_client.cpp b/Telegram/SourceFiles/payments/smartglocal/smartglocal_api_client.cpp index 0fca1b7f05554..690438c085d5c 100644 --- a/Telegram/SourceFiles/payments/smartglocal/smartglocal_api_client.cpp +++ b/Telegram/SourceFiles/payments/smartglocal/smartglocal_api_client.cpp @@ -44,10 +44,21 @@ namespace { }).toJson(QJsonDocument::Compact); } +[[nodiscard]] QString ComputeApiUrl(PaymentConfiguration configuration) { + const auto url = configuration.tokenizeUrl; + if (url.startsWith("https://") + && url.endsWith(".smart-glocal.com/cds/v1/tokenize/card")) { + return url; + } + return QString("https://%1/%2") + .arg(APIURLBase(configuration.isTest)) + .arg(TokenEndpoint()); +} + } // namespace APIClient::APIClient(PaymentConfiguration configuration) -: _apiUrl("https://" + APIURLBase(configuration.isTest)) +: _apiUrl(ComputeApiUrl(configuration)) , _configuration(configuration) { _additionalHttpHeaders = { { "X-PUBLIC-TOKEN", _configuration.publicToken }, @@ -67,7 +78,7 @@ void APIClient::createTokenWithCard( void APIClient::createTokenWithData( QByteArray data, TokenCompletionCallback completion) { - const auto url = QUrl(_apiUrl + '/' + TokenEndpoint()); + const auto url = QUrl(_apiUrl); auto request = QNetworkRequest(url); request.setHeader( QNetworkRequest::ContentTypeHeader, diff --git a/Telegram/SourceFiles/payments/smartglocal/smartglocal_api_client.h b/Telegram/SourceFiles/payments/smartglocal/smartglocal_api_client.h index b418188cb06cb..3166b386dae10 100644 --- a/Telegram/SourceFiles/payments/smartglocal/smartglocal_api_client.h +++ b/Telegram/SourceFiles/payments/smartglocal/smartglocal_api_client.h @@ -19,6 +19,7 @@ namespace SmartGlocal { struct PaymentConfiguration { QString publicToken; + QString tokenizeUrl; bool isTest = false; }; diff --git a/Telegram/SourceFiles/payments/smartglocal/smartglocal_card.cpp b/Telegram/SourceFiles/payments/smartglocal/smartglocal_card.cpp index 0423216ec6c45..a08b0980dad10 100644 --- a/Telegram/SourceFiles/payments/smartglocal/smartglocal_card.cpp +++ b/Telegram/SourceFiles/payments/smartglocal/smartglocal_card.cpp @@ -54,8 +54,9 @@ bool Card::empty() const { } QString Last4(const Card &card) { + static const auto RegExp = QRegularExpression("[^\\d]\\d*(\\d{4})$"); const auto masked = card.maskedNumber(); - const auto m = QRegularExpression("[^\\d]\\d*(\\d{4})$").match(masked); + const auto m = RegExp.match(masked); return m.hasMatch() ? m.captured(1) : QString(); } diff --git a/Telegram/SourceFiles/payments/stripe/stripe_card_validator.cpp b/Telegram/SourceFiles/payments/stripe/stripe_card_validator.cpp index e6542adf0fbb1..599a3922df18c 100644 --- a/Telegram/SourceFiles/payments/stripe/stripe_card_validator.cpp +++ b/Telegram/SourceFiles/payments/stripe/stripe_card_validator.cpp @@ -90,11 +90,13 @@ struct BinRange { } [[nodiscard]] bool IsNumeric(const QString &value) { - return QRegularExpression("^[0-9]*$").match(value).hasMatch(); + static const auto RegExp = QRegularExpression("^[0-9]*$"); + return RegExp.match(value).hasMatch(); } [[nodiscard]] QString RemoveWhitespaces(QString value) { - return value.replace(QRegularExpression("\\s"), QString()); + static const auto RegExp = QRegularExpression("\\s"); + return value.replace(RegExp, QString()); } [[nodiscard]] std::vector BinRangesForNumber( @@ -233,7 +235,7 @@ ExpireDateValidationResult ValidateExpireDate( return { ValidationState::Incomplete }; } const auto normalized = (sanitized[0] > '1' ? "0" : "") + sanitized; - const auto month = normalized.mid(0, 2).toInt(); + const auto month = base::StringViewMid(normalized, 0, 2).toInt(); if (month < 1 || month > 12) { return { ValidationState::Invalid }; } else if (normalized.size() < 4) { @@ -241,7 +243,7 @@ ExpireDateValidationResult ValidateExpireDate( } else if (normalized.size() > 4) { return { ValidationState::Invalid }; } - const auto year = 2000 + normalized.mid(2).toInt(); + const auto year = 2000 + base::StringViewMid(normalized, 2).toInt(); const auto thresholdDate = overrideExpireDateThreshold.value_or( QDate::currentDate()); diff --git a/Telegram/SourceFiles/payments/ui/payments_edit_card.cpp b/Telegram/SourceFiles/payments/ui/payments_edit_card.cpp index 34efdc5ccce3d..ec18ef6d270a2 100644 --- a/Telegram/SourceFiles/payments/ui/payments_edit_card.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_edit_card.cpp @@ -39,7 +39,8 @@ struct SimpleFieldState { } [[nodiscard]] QString RemoveNonNumbers(QString value) { - return value.replace(QRegularExpression("[^0-9]"), QString()); + static const auto RegExp = QRegularExpression("[^0-9]"); + return value.replace(RegExp, QString()); } [[nodiscard]] SimpleFieldState NumbersOnlyState(SimpleFieldState state) { diff --git a/Telegram/SourceFiles/payments/ui/payments_field.cpp b/Telegram/SourceFiles/payments/ui/payments_field.cpp index 73c87dff0b392..e182714e30674 100644 --- a/Telegram/SourceFiles/payments/ui/payments_field.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_field.cpp @@ -36,7 +36,8 @@ struct SimpleFieldState { } [[nodiscard]] QString RemoveNonNumbers(QString value) { - return value.replace(QRegularExpression("[^0-9]"), QString()); + static const auto RegExp = QRegularExpression("[^0-9]"); + return value.replace(RegExp, QString()); } [[nodiscard]] SimpleFieldState CleanMoneyState( @@ -216,6 +217,7 @@ struct SimpleFieldState { const FieldConfig &config, const QString &parsed, const QString &countryIso2) { + static const auto RegExp = QRegularExpression("[^0-9]\\."); if (config.type == FieldType::Country) { return countryIso2; } else if (config.type == FieldType::Money) { @@ -227,16 +229,14 @@ struct SimpleFieldState { QChar(','), QChar('.') ).replace( - QRegularExpression("[^0-9\\.]"), + RegExp, QString() ).toDouble(); return QString::number( int64(base::SafeRound(real * std::pow(10., rule.exponent)))); } else if (config.type == FieldType::CardNumber || config.type == FieldType::CardCVC) { - return QString(parsed).replace( - QRegularExpression("[^0-9\\.]"), - QString()); + return QString(parsed).replace(RegExp, QString()); } return parsed; } diff --git a/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp b/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp index a8f6c3d63971e..ae68d8bdde9b0 100644 --- a/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp @@ -388,7 +388,6 @@ void FormSummary::setupSuggestedTips(not_null layout) { st::paymentsTipButtonsPadding); const auto state = outer->lifetime().make_state(); for (const auto amount : _invoice.suggestedTips) { - const auto text = formatAmount(amount, true); const auto selected = (amount == _invoice.tipsSelected); const auto &st = selected ? _tipChosen diff --git a/Telegram/SourceFiles/payments/ui/payments_panel.cpp b/Telegram/SourceFiles/payments/ui/payments_panel.cpp index ec436f9e354f9..5e5c872de2afa 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_panel.cpp @@ -567,10 +567,17 @@ bool Panel::createWebview(const Webview::ThemeParams ¶ms) { if (!raw->widget()) { return false; } + QObject::connect(raw->widget(), &QObject::destroyed, [=] { + crl::on_main(this, [=] { + showCriticalError({ "Error: WebView has crashed." }); + }); + }); container->geometryValue( ) | rpl::start_with_next([=](QRect geometry) { - raw->widget()->setGeometry(geometry); + if (raw->widget()) { + raw->widget()->setGeometry(geometry); + } }, _webview->lifetime); raw->setMessageHandler([=](const QJsonDocument &message) { diff --git a/Telegram/SourceFiles/payments/ui/payments_panel.h b/Telegram/SourceFiles/payments/ui/payments_panel.h index 6136dace48ac0..b65def0156e53 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel.h +++ b/Telegram/SourceFiles/payments/ui/payments_panel.h @@ -8,6 +8,7 @@ For license and copyright information please follow this link: #pragma once #include "base/object_ptr.h" +#include "base/weak_ptr.h" namespace Ui { class Show; @@ -39,7 +40,7 @@ struct PaymentMethodDetails; struct PaymentMethodAdditional; struct NativeMethodDetails; -class Panel final { +class Panel final : public base::has_weak_ptr { public: explicit Panel(not_null delegate); ~Panel(); diff --git a/Telegram/SourceFiles/platform/linux/integration_linux.cpp b/Telegram/SourceFiles/platform/linux/integration_linux.cpp index 6c461ec732fae..1087e09fbe302 100644 --- a/Telegram/SourceFiles/platform/linux/integration_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/integration_linux.cpp @@ -55,17 +55,17 @@ class Application : public Gio::impl::ApplicationImpl { }); } - void open_(GFile **files, int n_files, const char*) noexcept override { - Core::Sandbox::Instance().customEnterFromEventLoop([&] { - for (int i = 0; i < n_files; ++i) { - QFileOpenEvent e( - QUrl(QString::fromUtf8(g_file_get_uri(files[i])))); - QGuiApplication::sendEvent(qApp, &e); - } - }); + void open_( + gi::Collection files, + const gi::cstring_v hint) noexcept override { + for (auto file : files) { + QFileOpenEvent e(QUrl(QString::fromStdString(file.get_uri()))); + QGuiApplication::sendEvent(qApp, &e); + } } - void add_platform_data_(GLib::VariantBuilder builder) noexcept override { + void add_platform_data_( + GLib::VariantBuilder_Ref builder) noexcept override { if (Platform::IsWayland()) { const auto token = qgetenv("XDG_ACTIVATION_TOKEN"); if (!token.isEmpty()) { @@ -113,8 +113,7 @@ Application::Application() Glib::create_variant( NotificationId().toTuple() ).get_type().gobj_copy(), - gi::transfer_full, - gi::direction_out + gi::transfer_full ); } catch (...) { return GLib::VariantType(); @@ -239,11 +238,7 @@ void LinuxIntegration::initInhibit() { return; } - auto uniqueName = _inhibitProxy - .get_connection() - .get_unique_name() - .value_or(""); - + std::string uniqueName = _inhibitProxy.get_connection().get_unique_name(); uniqueName.erase(0, 1); uniqueName.replace(uniqueName.find('.'), 1, 1, '_'); @@ -277,20 +272,18 @@ void LinuxIntegration::initInhibit() { ); }); - const auto options = std::array{ - GLib::Variant::new_dict_entry( - GLib::Variant::new_string("handle_token"), - GLib::Variant::new_variant( - GLib::Variant::new_string(handleToken))), - GLib::Variant::new_dict_entry( - GLib::Variant::new_string("session_handle_token"), - GLib::Variant::new_variant( - GLib::Variant::new_string(sessionHandleToken))), - }; - inhibit().call_create_monitor( - {}, - GLib::Variant::new_array(options.data(), options.size()), + "", + GLib::Variant::new_array({ + GLib::Variant::new_dict_entry( + GLib::Variant::new_string("handle_token"), + GLib::Variant::new_variant( + GLib::Variant::new_string(handleToken))), + GLib::Variant::new_dict_entry( + GLib::Variant::new_string("session_handle_token"), + GLib::Variant::new_variant( + GLib::Variant::new_string(sessionHandleToken))), + }), nullptr); } diff --git a/Telegram/SourceFiles/platform/linux/launcher_linux.cpp b/Telegram/SourceFiles/platform/linux/launcher_linux.cpp index d7a6fe9425daf..9afde3b19c656 100644 --- a/Telegram/SourceFiles/platform/linux/launcher_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/launcher_linux.cpp @@ -111,14 +111,14 @@ bool Launcher::launchUpdater(UpdaterLaunch action) { return GLib::spawn_async( initialWorkingDir().toStdString(), argumentsList, - std::nullopt, + {}, GLib::SpawnFlags::FILE_AND_ARGV_ZERO_, nullptr, nullptr, nullptr); } else if (!GLib::spawn_sync( argumentsList, - std::nullopt, + {}, // if the spawn is sync, working directory is not set // and GLib::SpawnFlags::LEAVE_DESCRIPTORS_OPEN_ is set, // it goes through an optimized code path diff --git a/Telegram/SourceFiles/platform/mac/specific_mac_p.mm b/Telegram/SourceFiles/platform/mac/specific_mac_p.mm index de1f1d1ecd0dc..5ec2ed4a03095 100644 --- a/Telegram/SourceFiles/platform/mac/specific_mac_p.mm +++ b/Telegram/SourceFiles/platform/mac/specific_mac_p.mm @@ -306,9 +306,10 @@ void objc_start() { _sharedDelegate = [[ApplicationDelegate alloc] init]; [[NSApplication sharedApplication] setDelegate:_sharedDelegate]; - [[[NSWorkspace sharedWorkspace] notificationCenter] addObserver: _sharedDelegate - selector: @selector(receiveWakeNote:) - name: NSWorkspaceDidWakeNotification object: NULL]; + [[[NSWorkspace sharedWorkspace] notificationCenter] + addObserver: _sharedDelegate + selector: @selector(receiveWakeNote:) + name: NSWorkspaceDidWakeNotification object: NULL]; } void objc_ignoreApplicationActivationRightNow() { diff --git a/Telegram/SourceFiles/settings/settings_experimental.cpp b/Telegram/SourceFiles/settings/settings_experimental.cpp index 3b9a5a069994b..0149872459e6f 100644 --- a/Telegram/SourceFiles/settings/settings_experimental.cpp +++ b/Telegram/SourceFiles/settings/settings_experimental.cpp @@ -17,7 +17,6 @@ For license and copyright information please follow this link: #include "ui/chat/chat_style_radius.h" #include "base/options.h" #include "core/application.h" -#include "core/sandbox.h" #include "core/launcher.h" #include "chat_helpers/tabbed_panel.h" #include "dialogs/dialogs_widget.h" @@ -144,7 +143,6 @@ void SetupExperimental( addToggle(ChatHelpers::kOptionTabbedPanelShowOnClick); addToggle(Dialogs::kOptionForumHideChatsList); addToggle(Core::kOptionFractionalScalingEnabled); - addToggle(Core::kOptionForceWaylandFractionalScaling); addToggle(Window::kOptionViewProfileInChatsListContextMenu); addToggle(Info::Profile::kOptionShowPeerIdBelowAbout); addToggle(Ui::GL::kOptionAllowLinuxNvidiaOpenGL); diff --git a/Telegram/SourceFiles/settings/settings_intro.cpp b/Telegram/SourceFiles/settings/settings_intro.cpp index ed9477ef0e2ec..bd38b8c3ff2c6 100644 --- a/Telegram/SourceFiles/settings/settings_intro.cpp +++ b/Telegram/SourceFiles/settings/settings_intro.cpp @@ -177,7 +177,7 @@ class IntroWidget : public Ui::RpWidget { void updateGeometry(QRect newGeometry, int additionalScroll); int scrollTillBottom(int forHeight) const; - rpl::producer scrollTillBottomChanges() const; + rpl::producer scrollTillBottomChanges() const; void setInnerFocus(); diff --git a/Telegram/SourceFiles/settings/settings_main.cpp b/Telegram/SourceFiles/settings/settings_main.cpp index 5e440a58183ec..b6aa84b1957c8 100644 --- a/Telegram/SourceFiles/settings/settings_main.cpp +++ b/Telegram/SourceFiles/settings/settings_main.cpp @@ -32,6 +32,7 @@ For license and copyright information please follow this link: #include "ui/widgets/buttons.h" #include "ui/text/text_utilities.h" #include "ui/toast/toast.h" +#include "ui/new_badges.h" #include "ui/vertical_list.h" #include "info/profile/info_profile_badge.h" #include "info/profile/info_profile_emoji_status_panel.h" @@ -60,6 +61,7 @@ For license and copyright information please follow this link: #include "core/file_utilities.h" #include "core/application.h" #include "base/call_delayed.h" +#include "base/unixtime.h" #include "base/platform/base_platform_info.h" #include "styles/style_settings.h" #include "styles/style_boxes.h" @@ -417,6 +419,21 @@ void SetupPremium( controller->setPremiumRef("settings"); showOther(PremiumId()); }); + { + const auto button = AddButtonWithIcon( + container, + tr::lng_settings_gift_premium(), + st::settingsButton, + { .icon = &st::menuIconGiftPremium } + ); + button->addClickHandler([=] { + controller->showGiftPremiumsBox(); + }); + constexpr auto kNewExpiresAt = int(1735689600); + if (base::unixtime::now() < kNewExpiresAt) { + Ui::NewBadge::AddToRight(button); + } + } Ui::AddSkip(container); } diff --git a/Telegram/SourceFiles/settings/settings_power_saving.cpp b/Telegram/SourceFiles/settings/settings_power_saving.cpp index d3ca4792ee377..4d523e07a079a 100644 --- a/Telegram/SourceFiles/settings/settings_power_saving.cpp +++ b/Telegram/SourceFiles/settings/settings_power_saving.cpp @@ -172,7 +172,7 @@ EditFlagsDescriptor PowerSavingLabels() { { tr::lng_settings_power_emoji(), std::move(emoji) }, { tr::lng_settings_power_chat(), std::move(chat) }, { std::nullopt, std::move(calls) }, - { std::nullopt, std::move(animations), }, + { std::nullopt, std::move(animations) }, }, .st = &st::powerSavingButton }; } diff --git a/Telegram/SourceFiles/settings/settings_premium.cpp b/Telegram/SourceFiles/settings/settings_premium.cpp index 319a0830a2f50..a64e004c68047 100644 --- a/Telegram/SourceFiles/settings/settings_premium.cpp +++ b/Telegram/SourceFiles/settings/settings_premium.cpp @@ -47,6 +47,7 @@ For license and copyright information please follow this link: #include "ui/wrap/padding_wrap.h" #include "ui/wrap/slide_wrap.h" #include "ui/wrap/vertical_layout.h" +#include "ui/new_badges.h" #include "ui/painter.h" #include "ui/power_saving.h" #include "ui/vertical_list.h" @@ -870,6 +871,7 @@ void Premium::setStepDataReference(std::any &data) { void Premium::setupSubscriptionOptions( not_null container) { const auto isEmojiStatus = (!!Ref::EmojiStatus::Parse(_ref)); + const auto isGift = (!!Ref::Gift::Parse(_ref)); const auto options = container->add( object_ptr>( @@ -899,15 +901,19 @@ void Premium::setupSubscriptionOptions( Ui::AddSkip(content, lastSkip - st::defaultVerticalListSkip); Ui::AddSkip(skip->entity(), lastSkip); + if (isEmojiStatus || isGift) { + options->toggle(false, anim::type::instant); + skip->toggle(true, anim::type::instant); + return; + } auto toggleOn = rpl::combine( Data::AmPremiumValue(&_controller->session()), - rpl::single(isEmojiStatus), apiPremium->statusTextValue( ) | rpl::map([=] { return apiPremium->subscriptionOptions().size() < 2; }) - ) | rpl::map([=](bool premium, bool isEmojiStatus, bool noOptions) { - return !premium && !isEmojiStatus && !noOptions; + ) | rpl::map([=](bool premium, bool noOptions) { + return !premium && !noOptions; }); options->toggleOn(rpl::duplicate(toggleOn), anim::type::instant); skip->toggleOn(std::move( @@ -918,187 +924,15 @@ void Premium::setupSubscriptionOptions( void Premium::setupContent() { const auto content = Ui::CreateChild(this); - const auto &stDefault = st::settingsButton; - const auto &stLabel = st::defaultFlatLabel; - const auto iconSize = st::settingsPremiumIconDouble.size(); - const auto &titlePadding = st::settingsPremiumRowTitlePadding; - const auto &descriptionPadding = st::settingsPremiumRowAboutPadding; - setupSubscriptionOptions(content); - auto entryMap = EntryMap(); - auto iconContainers = std::vector(); - iconContainers.reserve(int(entryMap.size())); - - const auto addRow = [&](Entry &entry) { - const auto labelAscent = stLabel.style.font->ascent; - const auto button = Ui::CreateChild( - content, - rpl::single(QString())); - - const auto label = content->add( - object_ptr( - content, - std::move(entry.title) | rpl::map(Ui::Text::Bold), - stLabel), - titlePadding); - label->setAttribute(Qt::WA_TransparentForMouseEvents); - const auto description = content->add( - object_ptr( - content, - std::move(entry.description), - st::boxDividerLabel), - descriptionPadding); - description->setAttribute(Qt::WA_TransparentForMouseEvents); - - const auto badge = entry.newBadge - ? Ui::CreateChild>( - content, - object_ptr( - content, - tr::lng_premium_summary_new_badge(), - st::settingsPremiumNewBadge), - st::settingsPremiumNewBadgePadding) - : nullptr; - if (badge) { - badge->setAttribute(Qt::WA_TransparentForMouseEvents); - badge->paintRequest() | rpl::start_with_next([=] { - auto p = QPainter(badge); - auto hq = PainterHighQualityEnabler(p); - p.setPen(Qt::NoPen); - p.setBrush(st::windowBgActive); - const auto r = st::settingsPremiumNewBadgePadding.left(); - p.drawRoundedRect(badge->rect(), r, r); - }, badge->lifetime()); - - label->geometryValue( - ) | rpl::start_with_next([=](QRect geometry) { - badge->move(st::settingsPremiumNewBadgePosition - + QPoint(label->x() + label->width(), label->y())); - }, badge->lifetime()); - } - const auto dummy = Ui::CreateChild(content); - dummy->setAttribute(Qt::WA_TransparentForMouseEvents); - - content->sizeValue( - ) | rpl::start_with_next([=](const QSize &s) { - dummy->resize(s.width(), iconSize.height()); - }, dummy->lifetime()); - - label->geometryValue( - ) | rpl::start_with_next([=](const QRect &r) { - dummy->moveToLeft(0, r.y() + (r.height() - labelAscent)); - }, dummy->lifetime()); + auto buttonCallback = [=](PremiumPreview section) { + _setPaused(true); + const auto hidden = crl::guard(this, [=] { _setPaused(false); }); - rpl::combine( - content->widthValue(), - label->heightValue(), - description->heightValue() - ) | rpl::start_with_next([=, - topPadding = titlePadding, - bottomPadding = descriptionPadding]( - int width, - int topHeight, - int bottomHeight) { - button->resize( - width, - topPadding.top() - + topHeight - + topPadding.bottom() - + bottomPadding.top() - + bottomHeight - + bottomPadding.bottom()); - }, button->lifetime()); - label->topValue( - ) | rpl::start_with_next([=, padding = titlePadding.top()](int top) { - button->moveToLeft(0, top - padding); - }, button->lifetime()); - const auto arrow = Ui::CreateChild( - button, - st::backButton); - arrow->setIconOverride( - &st::settingsPremiumArrow, - &st::settingsPremiumArrowOver); - arrow->setAttribute(Qt::WA_TransparentForMouseEvents); - button->sizeValue( - ) | rpl::start_with_next([=](const QSize &s) { - const auto &point = st::settingsPremiumArrowShift; - arrow->moveToRight( - -point.x(), - point.y() + (s.height() - arrow->height()) / 2); - }, arrow->lifetime()); - - const auto section = entry.section; - button->setClickedCallback([=, controller = _controller] { - _setPaused(true); - const auto hidden = crl::guard(this, [=] { - _setPaused(false); - }); - - ShowPremiumPreviewToBuy(controller, section, hidden); - }); - - iconContainers.push_back(dummy); + ShowPremiumPreviewToBuy(_controller, section, hidden); }; - - auto icons = std::vector(); - icons.reserve(int(entryMap.size())); - { - const auto &account = _controller->session().account(); - const auto mtpOrder = account.appConfig().get( - "premium_promo_order", - FallbackOrder()); - const auto processEntry = [&](Entry &entry) { - icons.push_back(entry.icon); - addRow(entry); - }; - - for (const auto &key : mtpOrder) { - auto it = entryMap.find(key); - if (it == end(entryMap)) { - continue; - } - processEntry(it->second); - } - - SendScreenShow(_controller, mtpOrder, _ref); - } - - content->resizeToWidth(content->height()); - - // Icons. - Assert(iconContainers.size() > 2); - const auto from = iconContainers.front()->y(); - const auto to = iconContainers.back()->y() + iconSize.height(); - auto gradient = QLinearGradient(0, 0, 0, to - from); - gradient.setStops(Ui::Premium::FullHeightGradientStops()); - for (auto i = 0; i < int(icons.size()); i++) { - const auto &iconContainer = iconContainers[i]; - - const auto pointTop = iconContainer->y() - from; - const auto pointBottom = pointTop + iconContainer->height(); - const auto ratioTop = pointTop / float64(to - from); - const auto ratioBottom = pointBottom / float64(to - from); - - auto resultGradient = QLinearGradient( - QPointF(), - QPointF(0, pointBottom - pointTop)); - - resultGradient.setColorAt( - .0, - anim::gradient_color_at(gradient, ratioTop)); - resultGradient.setColorAt( - .1, - anim::gradient_color_at(gradient, ratioBottom)); - - const auto brush = QBrush(resultGradient); - AddButtonIcon( - iconContainer, - stDefault, - { .icon = icons[i], .backgroundBrush = brush }); - } - - Ui::AddSkip(content, descriptionPadding.bottom()); + AddSummaryPremium(content, _controller, _ref, std::move(buttonCallback)); #if 0 Ui::AddSkip(content); Ui::AddDivider(content); @@ -1679,4 +1513,161 @@ not_null CreateSubscribeButton( }) | ranges::to_vector; } +void AddSummaryPremium( + not_null content, + not_null controller, + const QString &ref, + Fn buttonCallback) { + const auto &stDefault = st::settingsButton; + const auto &stLabel = st::defaultFlatLabel; + const auto iconSize = st::settingsPremiumIconDouble.size(); + const auto &titlePadding = st::settingsPremiumRowTitlePadding; + const auto &descriptionPadding = st::settingsPremiumRowAboutPadding; + + auto entryMap = EntryMap(); + auto iconContainers = std::vector(); + iconContainers.reserve(int(entryMap.size())); + + const auto addRow = [&](Entry &entry) { + const auto labelAscent = stLabel.style.font->ascent; + const auto button = Ui::CreateChild( + content.get(), + rpl::single(QString())); + + const auto label = content->add( + object_ptr( + content, + std::move(entry.title) | rpl::map(Ui::Text::Bold), + stLabel), + titlePadding); + label->setAttribute(Qt::WA_TransparentForMouseEvents); + const auto description = content->add( + object_ptr( + content, + std::move(entry.description), + st::boxDividerLabel), + descriptionPadding); + description->setAttribute(Qt::WA_TransparentForMouseEvents); + + if (entry.newBadge) { + Ui::NewBadge::AddAfterLabel(content, label); + } + const auto dummy = Ui::CreateChild(content.get()); + dummy->setAttribute(Qt::WA_TransparentForMouseEvents); + + content->sizeValue( + ) | rpl::start_with_next([=](const QSize &s) { + dummy->resize(s.width(), iconSize.height()); + }, dummy->lifetime()); + + label->geometryValue( + ) | rpl::start_with_next([=](const QRect &r) { + dummy->moveToLeft(0, r.y() + (r.height() - labelAscent)); + }, dummy->lifetime()); + + rpl::combine( + content->widthValue(), + label->heightValue(), + description->heightValue() + ) | rpl::start_with_next([=, + topPadding = titlePadding, + bottomPadding = descriptionPadding]( + int width, + int topHeight, + int bottomHeight) { + button->resize( + width, + topPadding.top() + + topHeight + + topPadding.bottom() + + bottomPadding.top() + + bottomHeight + + bottomPadding.bottom()); + }, button->lifetime()); + label->topValue( + ) | rpl::start_with_next([=, padding = titlePadding.top()](int top) { + button->moveToLeft(0, top - padding); + }, button->lifetime()); + const auto arrow = Ui::CreateChild( + button, + st::backButton); + arrow->setIconOverride( + &st::settingsPremiumArrow, + &st::settingsPremiumArrowOver); + arrow->setAttribute(Qt::WA_TransparentForMouseEvents); + button->sizeValue( + ) | rpl::start_with_next([=](const QSize &s) { + const auto &point = st::settingsPremiumArrowShift; + arrow->moveToRight( + -point.x(), + point.y() + (s.height() - arrow->height()) / 2); + }, arrow->lifetime()); + + const auto section = entry.section; + button->setClickedCallback([=] { buttonCallback(section); }); + + iconContainers.push_back(dummy); + }; + + auto icons = std::vector(); + icons.reserve(int(entryMap.size())); + { + const auto &account = controller->session().account(); + const auto mtpOrder = account.appConfig().get( + "premium_promo_order", + FallbackOrder()); + const auto processEntry = [&](Entry &entry) { + icons.push_back(entry.icon); + addRow(entry); + }; + + for (const auto &key : mtpOrder) { + auto it = entryMap.find(key); + if (it == end(entryMap)) { + continue; + } + processEntry(it->second); + } + + SendScreenShow(controller, mtpOrder, ref); + } + + content->resizeToWidth(content->height()); + + // Icons. + Assert(iconContainers.size() > 2); + const auto from = iconContainers.front()->y(); + const auto to = iconContainers.back()->y() + iconSize.height(); + auto gradient = QLinearGradient(0, 0, 0, to - from); + gradient.setStops(Ui::Premium::FullHeightGradientStops()); + for (auto i = 0; i < int(icons.size()); i++) { + const auto &iconContainer = iconContainers[i]; + + const auto pointTop = iconContainer->y() - from; + const auto pointBottom = pointTop + iconContainer->height(); + const auto ratioTop = pointTop / float64(to - from); + const auto ratioBottom = pointBottom / float64(to - from); + + auto resultGradient = QLinearGradient( + QPointF(), + QPointF(0, pointBottom - pointTop)); + + resultGradient.setColorAt( + .0, + anim::gradient_color_at(gradient, ratioTop)); + resultGradient.setColorAt( + .1, + anim::gradient_color_at(gradient, ratioBottom)); + + const auto brush = QBrush(resultGradient); + AddButtonIcon( + iconContainer, + stDefault, + { .icon = icons[i], .backgroundBrush = brush }); + } + + Ui::AddSkip(content, descriptionPadding.bottom()); + +} + } // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings_premium.h b/Telegram/SourceFiles/settings/settings_premium.h index 44d9c8ea4c755..52a9e2b9e011d 100644 --- a/Telegram/SourceFiles/settings/settings_premium.h +++ b/Telegram/SourceFiles/settings/settings_premium.h @@ -23,6 +23,7 @@ namespace Ui { class RpWidget; class RoundButton; class GradientButton; +class VerticalLayout; } // namespace Ui namespace Main { @@ -84,6 +85,11 @@ struct SubscribeButtonArgs final { [[nodiscard]] std::vector PremiumPreviewOrder( not_null<::Main::Session*> session); +void AddSummaryPremium( + not_null content, + not_null controller, + const QString &ref, + Fn buttonCallback); } // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp b/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp index a0335f537b918..cd4aee4336ed6 100644 --- a/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp +++ b/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp @@ -171,6 +171,7 @@ AdminLog::OwnedItem GenerateForwardedItem( MTP_int(0), // Not used (would've been trimmed to 32 bits). peerToMTP(history->peer->id), peerToMTP(history->peer->id), + MTPPeer(), // saved_peer_id MTP_messageFwdHeader( MTP_flags(MTPDmessageFwdHeader::Flag::f_from_id), peerToMTP(history->session().userPeerId()), @@ -180,6 +181,9 @@ AdminLog::OwnedItem GenerateForwardedItem( MTPstring(), // post_author MTPPeer(), // saved_from_peer MTPint(), // saved_from_msg_id + MTPPeer(), // saved_from_id + MTPstring(), // saved_from_name + MTPint(), // saved_date MTPstring()), // psa_type MTPlong(), // via_bot_id MTPMessageReplyHeader(), diff --git a/Telegram/SourceFiles/settings/settings_websites.cpp b/Telegram/SourceFiles/settings/settings_websites.cpp index a963a0488e5a9..bff91b1554274 100644 --- a/Telegram/SourceFiles/settings/settings_websites.cpp +++ b/Telegram/SourceFiles/settings/settings_websites.cpp @@ -32,7 +32,6 @@ For license and copyright information please follow this link: namespace { constexpr auto kShortPollTimeout = 60 * crl::time(1000); -constexpr auto kMaxDeviceModelLength = 32; using EntryData = Api::Websites::Entry; diff --git a/Telegram/SourceFiles/statistics/chart_widget.cpp b/Telegram/SourceFiles/statistics/chart_widget.cpp index 4b13f1316b0c2..ef7319581fe63 100644 --- a/Telegram/SourceFiles/statistics/chart_widget.cpp +++ b/Telegram/SourceFiles/statistics/chart_widget.cpp @@ -107,7 +107,8 @@ void PaintBottomLine( startXIndex, xPercentageLimits.max); - const auto edgeAlphaSize = st::statisticsChartBottomCaptionMaxWidth / 4.; + const auto captionMaxWidth = chartData.dayStringMaxWidth; + const auto edgeAlphaSize = captionMaxWidth / 4.; for (auto k = 0; k < dates.size(); k++) { const auto &date = dates[k]; @@ -145,9 +146,9 @@ void PaintBottomLine( / float64(chartData.x.back() - chartData.x.front()); const auto xPoint = xPercentage * fullWidth - offset; const auto r = QRectF( - xPoint - st::statisticsChartBottomCaptionMaxWidth / 2., + xPoint - captionMaxWidth / 2., y, - st::statisticsChartBottomCaptionMaxWidth, + captionMaxWidth, st::statisticsChartBottomCaptionHeight); const auto edgeAlpha = (r.x() < 0) ? std::max( @@ -969,7 +970,7 @@ void ChartWidget::setupChartArea() { [[maybe_unused]] const auto o = ScopedPainterOpacity( p, p.opacity() * kRulerLineAlpha); - const auto bottom = r + const auto bottom = rect() - QMargins{ 0, rect::bottom(chartRect), 0, 0 }; p.fillRect(bottom, st::boxBg); p.fillRect( @@ -1018,7 +1019,7 @@ void ChartWidget::updateBottomDates() { const auto by = int(_chartArea->width() / float64(_chartData.x.size())); _bottomLine.captionIndicesOffset = 0 - + st::statisticsChartBottomCaptionMaxWidth / std::max(by, 1); + + _chartData.dayStringMaxWidth / std::max(by, 1); const auto isCurrentNull = (_bottomLine.current.stepMinFast == 0); if (!isCurrentNull diff --git a/Telegram/SourceFiles/statistics/statistics.style b/Telegram/SourceFiles/statistics/statistics.style index 83f6c143e09ea..395d458caee1f 100644 --- a/Telegram/SourceFiles/statistics/statistics.style +++ b/Telegram/SourceFiles/statistics/statistics.style @@ -37,8 +37,6 @@ statisticsChartRulerCaptionSkip: 4px; statisticsChartBottomCaptionHeight: 15px; statisticsChartBottomCaptionSkip: 6px; -statisticsChartBottomCaptionMaxWidth: 44px; - statisticsChartFlatCheckboxMargins: margins(4px, 3px, 4px, 5px); statisticsChartFlatCheckboxCheckWidth: 3px; statisticsChartFlatCheckboxShrinkkWidth: 4px; diff --git a/Telegram/SourceFiles/statistics/statistics_data_deserialize.cpp b/Telegram/SourceFiles/statistics/statistics_data_deserialize.cpp index 09fb3aa93e1f1..355c500c16575 100644 --- a/Telegram/SourceFiles/statistics/statistics_data_deserialize.cpp +++ b/Telegram/SourceFiles/statistics/statistics_data_deserialize.cpp @@ -136,11 +136,11 @@ Data::StatisticalChart StatisticalChartFromJSON(const QByteArray &json) { const auto colors = root.value(u"colors"_q).toObject(); const auto names = root.value(u"names"_q).toObject(); - const auto colorPattern = u"(.*)(#.*)"_q; for (auto &line : result.lines) { const auto colorIt = colors.constFind(line.idString); if (colorIt != colors.constEnd() && (*colorIt).isString()) { - const auto match = QRegularExpression(colorPattern).match( + static const auto RegExp = QRegularExpression(u"(.*)(#.*)"_q); + const auto match = RegExp.match( colorIt->toString()); if (match.hasMatch()) { line.colorKey = match.captured(1); diff --git a/Telegram/SourceFiles/statistics/view/bar_chart_view.cpp b/Telegram/SourceFiles/statistics/view/bar_chart_view.cpp index 5e75d4d058fe0..e4ce9616070aa 100644 --- a/Telegram/SourceFiles/statistics/view/bar_chart_view.cpp +++ b/Telegram/SourceFiles/statistics/view/bar_chart_view.cpp @@ -69,7 +69,7 @@ void BarChartView::paintChartAndSelected( const auto &line = c.chartData.lines[i]; auto path = QPainterPath(); for (auto x = localStart; x <= localEnd; x++) { - if (line.y[x] <= 0) { + if (line.y[x] <= 0 && _isStack) { continue; } const auto yPercentage = (line.y[x] - c.heightLimits.min) diff --git a/Telegram/SourceFiles/statistics/view/stack_linear_chart_view.h b/Telegram/SourceFiles/statistics/view/stack_linear_chart_view.h index 8e59798dcab6d..cc147a7362052 100644 --- a/Telegram/SourceFiles/statistics/view/stack_linear_chart_view.h +++ b/Telegram/SourceFiles/statistics/view/stack_linear_chart_view.h @@ -132,7 +132,7 @@ class StackLinearChartView final : public AbstractChartView { [[nodiscard]] bool isFinished() const; private: - crl::time _startedAt = 0;; + crl::time _startedAt = 0; std::vector _animValues; PiePartData _current; bool _isFinished = true; diff --git a/Telegram/SourceFiles/storage/localimageloader.h b/Telegram/SourceFiles/storage/localimageloader.h index 97b0ad9ce9628..d2e64b536f186 100644 --- a/Telegram/SourceFiles/storage/localimageloader.h +++ b/Telegram/SourceFiles/storage/localimageloader.h @@ -36,7 +36,7 @@ enum class SendMediaType { Secure, }; -using UploadFileParts = QMap; +using UploadFileParts = QMap; struct SendMediaReady { SendMediaReady() = default; // temp SendMediaReady( diff --git a/Telegram/SourceFiles/storage/localstorage.cpp b/Telegram/SourceFiles/storage/localstorage.cpp index c3f88807a2305..2958024b5ebd8 100644 --- a/Telegram/SourceFiles/storage/localstorage.cpp +++ b/Telegram/SourceFiles/storage/localstorage.cpp @@ -579,8 +579,9 @@ void writeAutoupdatePrefix(const QString &prefix) { QString readAutoupdatePrefix() { Expects(!Core::UpdaterDisabled()); + static const auto RegExp = QRegularExpression("/+$"); auto result = readAutoupdatePrefixRaw(); - return result.replace(QRegularExpression("/+$"), QString()); + return result.replace(RegExp, QString()); } void writeBackground(const Data::WallPaper &paper, const QImage &image) { @@ -613,7 +614,7 @@ void writeBackground(const Data::WallPaper &paper, const QImage &image) { dst = dst.subspan(sizeof(qint32)); bytes::copy(dst, bytes::object_as_span(&height)); dst = dst.subspan(sizeof(qint32)); - const auto src = bytes::make_span(image.constBits(), srcsize); + const auto src = bytes::make_span(copy.constBits(), srcsize); if (srcsize == dstsize) { bytes::copy(dst, src); } else { diff --git a/Telegram/SourceFiles/storage/storage_account.cpp b/Telegram/SourceFiles/storage/storage_account.cpp index fe8049ff6d73a..c961c891b6a4a 100644 --- a/Telegram/SourceFiles/storage/storage_account.cpp +++ b/Telegram/SourceFiles/storage/storage_account.cpp @@ -2502,7 +2502,7 @@ void Account::writeRecentHashtagsAndBots() { writeMapQueued(); } quint32 size = sizeof(quint32) * 3, writeCnt = 0, searchCnt = 0, botsCnt = cRecentInlineBots().size(); - for (auto i = write.cbegin(), e = write.cend(); i != e; ++i) { + for (auto i = write.cbegin(), e = write.cend(); i != e; ++i) { if (!i->first.isEmpty()) { size += Serialize::stringSize(i->first) + sizeof(quint16); ++writeCnt; diff --git a/Telegram/SourceFiles/storage/storage_media_prepare.cpp b/Telegram/SourceFiles/storage/storage_media_prepare.cpp index 362455cacdf94..17ebe996f3d98 100644 --- a/Telegram/SourceFiles/storage/storage_media_prepare.cpp +++ b/Telegram/SourceFiles/storage/storage_media_prepare.cpp @@ -132,7 +132,6 @@ MimeDataState ComputeMimeDataState(const QMimeData *data) { return MimeDataState::None; } - auto files = QStringList(); auto allAreSmallImages = true; for (const auto &url : urls) { if (!url.isLocalFile()) { diff --git a/Telegram/SourceFiles/support/support_templates.cpp b/Telegram/SourceFiles/support/support_templates.cpp index ccfac3bf1286c..cb1515ee30eab 100644 --- a/Telegram/SourceFiles/support/support_templates.cpp +++ b/Telegram/SourceFiles/support/support_templates.cpp @@ -518,7 +518,6 @@ void Templates::ensureUpdatesCreated() { } void Templates::update() { - auto errors = QStringList(); const auto sendRequest = [&](const QString &path, const QString &url) { ensureUpdatesCreated(); if (_updates->requests.find(path) != end(_updates->requests)) { diff --git a/Telegram/SourceFiles/ui/boxes/boost_box.cpp b/Telegram/SourceFiles/ui/boxes/boost_box.cpp index 592436a1392bc..5a226eea406af 100644 --- a/Telegram/SourceFiles/ui/boxes/boost_box.cpp +++ b/Telegram/SourceFiles/ui/boxes/boost_box.cpp @@ -467,15 +467,32 @@ void AskBoostBox( box->addTopButton(st::boxTitleClose, [=] { box->closeBox(); }); - auto title = v::is(data.reason.data) - ? tr::lng_boost_channel_title_color() - : tr::lng_boost_channel_title_reactions(); + auto title = v::match(data.reason.data, [&]( + AskBoostChannelColor data) { + return tr::lng_boost_channel_title_color(); + }, [&](AskBoostWallpaper data) { + return tr::lng_boost_channel_title_wallpaper(); + }, [&](AskBoostEmojiStatus data) { + return tr::lng_boost_channel_title_status(); + }, [&](AskBoostCustomReactions data) { + return tr::lng_boost_channel_title_reactions(); + }); auto reasonText = v::match(data.reason.data, [&]( AskBoostChannelColor data) { return tr::lng_boost_channel_needs_level_color( lt_count, rpl::single(float64(data.requiredLevel)), Ui::Text::RichLangValue); + }, [&](AskBoostWallpaper data) { + return tr::lng_boost_channel_needs_level_wallpaper( + lt_count, + rpl::single(float64(data.requiredLevel)), + Ui::Text::RichLangValue); + }, [&](AskBoostEmojiStatus data) { + return tr::lng_boost_channel_needs_level_status( + lt_count, + rpl::single(float64(data.requiredLevel)), + Ui::Text::RichLangValue); }, [&](AskBoostCustomReactions data) { return tr::lng_boost_channel_needs_level_reactions( lt_count, diff --git a/Telegram/SourceFiles/ui/boxes/boost_box.h b/Telegram/SourceFiles/ui/boxes/boost_box.h index 84d0b65b3c509..68ff1d7716e5f 100644 --- a/Telegram/SourceFiles/ui/boxes/boost_box.h +++ b/Telegram/SourceFiles/ui/boxes/boost_box.h @@ -54,6 +54,14 @@ struct AskBoostChannelColor { int requiredLevel = 0; }; +struct AskBoostWallpaper { + int requiredLevel = 0; +}; + +struct AskBoostEmojiStatus { + int requiredLevel = 0; +}; + struct AskBoostCustomReactions { int count = 0; }; @@ -61,6 +69,8 @@ struct AskBoostCustomReactions { struct AskBoostReason { std::variant< AskBoostChannelColor, + AskBoostWallpaper, + AskBoostEmojiStatus, AskBoostCustomReactions> data; }; diff --git a/Telegram/SourceFiles/ui/boxes/country_select_box.cpp b/Telegram/SourceFiles/ui/boxes/country_select_box.cpp index 0d21dacdb504c..bb7cc70bf50fc 100644 --- a/Telegram/SourceFiles/ui/boxes/country_select_box.cpp +++ b/Telegram/SourceFiles/ui/boxes/country_select_box.cpp @@ -234,13 +234,14 @@ void CountrySelectBox::Inner::init() { } auto index = 0; for (const auto &info : _list) { + static const auto RegExp = QRegularExpression("[\\s\\-]"); auto full = info.country + ' ' + (!info.alternativeName.isEmpty() ? info.alternativeName : QString()); const auto namesList = std::move(full).toLower().split( - QRegularExpression("[\\s\\-]"), + RegExp, Qt::SkipEmptyParts); auto &names = _namesList.emplace_back(); names.reserve(namesList.size()); diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp index aa18ca2071fed..209444ff10b00 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp @@ -603,10 +603,17 @@ bool Panel::createWebview(const Webview::ThemeParams ¶ms) { if (!raw->widget()) { return false; } + QObject::connect(raw->widget(), &QObject::destroyed, [=] { + crl::on_main(this, [=] { + showCriticalError({ "Error: WebView has crashed." }); + }); + }); container->geometryValue( ) | rpl::start_with_next([=](QRect geometry) { - raw->widget()->setGeometry(geometry); + if (raw->widget()) { + raw->widget()->setGeometry(geometry); + } }, _webview->lifetime); raw->setMessageHandler([=](const QJsonDocument &message) { @@ -726,8 +733,9 @@ void Panel::switchInlineQueryMessage(const QJsonObject &args) { u"groups"_q, u"channels"_q, }; + const auto typeArray = args["chat_types"].toArray(); auto types = std::vector(); - for (const auto &value : args["chat_types"].toArray()) { + for (const auto &value : typeArray) { const auto type = value.toString(); if (valid.contains(type)) { types.push_back(type); @@ -803,8 +811,9 @@ void Panel::openPopup(const QJsonObject &args) { { "cancel", Type::Cancel }, { "destructive", Type::Destructive }, }; + const auto buttonArray = args["buttons"].toArray(); auto buttons = std::vector(); - for (const auto button : args["buttons"].toArray()) { + for (const auto button : buttonArray) { const auto fields = button.toObject(); const auto i = types.find(fields["type"].toString()); if (i == end(types)) { diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index 122daf9e3d28e..b025a57b84bbe 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -791,6 +791,10 @@ whoReadDateChecks: icon{{ "menu/read_ticks_s", windowSubTextFg }}; whoReadDateChecksOver: icon{{ "menu/read_ticks_s", windowSubTextFgOver }}; whoLikedDateHeart: icon{{ "menu/read_react_s", windowSubTextFg }}; whoLikedDateHeartOver: icon{{ "menu/read_react_s", windowSubTextFgOver }}; +whoRepostedDateHeart: icon{{ "mediaview/mini_repost", groupCallMemberActiveIcon, point(4px, 4px) }}; +whoRepostedDateHeartOver: icon{{ "mediaview/mini_repost", groupCallMemberActiveIcon, point(4px, 4px) }}; +whoForwardedDateHeart: icon{{ "statistics/mini_stats_share", groupCallMemberActiveIcon, point(4px, 4px) }}; +whoForwardedDateHeartOver: icon{{ "statistics/mini_stats_share", groupCallMemberActiveIcon, point(4px, 4px) }}; whoReadDateChecksPosition: point(-7px, -4px); whoReadDateStyle: TextStyle(defaultTextStyle) { font: font(12px); @@ -974,22 +978,22 @@ storyMentionButtonSkip: 5px; chatGiveawayWidth: 292px; chatGiveawayStickerTop: -16px; +chatGiveawayWinnersTopSkip: 25px; chatGiveawayBadgeFont: font(12px bold); chatGiveawayBadgeTop: 106px; chatGiveawayBadgePadding: margins(7px, 1px, 5px, 3px); chatGiveawayBadgeStroke: 2px; -chatGiveawayPrizesTop: 16px; -chatGiveawayPrizesSkip: 4px; -chatGiveawayParticipantsTop: 16px; -chatGiveawayParticipantsSkip: 4px; -chatGiveawayChannelTop: 6px; -chatGiveawayChannelSize: 32px; -chatGiveawayChannelPadding: margins(5px, 7px, 12px, 0px); -chatGiveawayChannelSkip: 8px; -chatGiveawayCountriesSkip: 16px; -chatGiveawayDateTop: 6px; -chatGiveawayDateSkip: 4px; -chatGiveawayBottomSkip: 16px; +chatGiveawayPrizesTitleMargin: margins(11px, 16px, 11px, 4px); +chatGiveawayPrizesMargin: margins(11px, 0px, 11px, 0px); +chatGiveawayPrizesWithPadding: margins(22px, 2px, 22px, 2px); +chatGiveawayPrizesWithSkip: 8px; +chatGiveawayPrizesWithLineTop: 9px; +chatGiveawayParticipantsMargin: margins(11px, 0px, 11px, 6px); +chatGiveawayNoCountriesTitleMargin: margins(11px, 6px, 11px, 4px); +chatGiveawayEndDateMargin: margins(11px, 0px, 11px, 16px); +chatGiveawayPeerSize: 32px; +chatGiveawayPeerPadding: margins(5px, 7px, 12px, 0px); +chatGiveawayPeerSkip: 8px; chatSimilarRadius: 12px; chatSimilarArrowSize: 6px; diff --git a/Telegram/SourceFiles/ui/chat/group_call_bar.cpp b/Telegram/SourceFiles/ui/chat/group_call_bar.cpp index 4b0ffb092c974..239028b1fe8b7 100644 --- a/Telegram/SourceFiles/ui/chat/group_call_bar.cpp +++ b/Telegram/SourceFiles/ui/chat/group_call_bar.cpp @@ -244,6 +244,7 @@ void GroupCallBar::setupInner() { } void GroupCallBar::setupRightButton(not_null button) { + button->setFullRadius(true); rpl::combine( _inner->widthValue(), button->widthValue() diff --git a/Telegram/SourceFiles/ui/controls/download_bar.cpp b/Telegram/SourceFiles/ui/controls/download_bar.cpp index 1f6e25af20991..3b3db11f9be68 100644 --- a/Telegram/SourceFiles/ui/controls/download_bar.cpp +++ b/Telegram/SourceFiles/ui/controls/download_bar.cpp @@ -287,7 +287,7 @@ void DownloadBar::paint(Painter &p, QRect clip) { } float64 DownloadBar::computeProgress() const { - const auto now = _progress.current(); + const auto now = _progress.current(); return now.total ? (now.ready / float64(now.total)) : 0.; } diff --git a/Telegram/SourceFiles/ui/controls/silent_toggle.cpp b/Telegram/SourceFiles/ui/controls/silent_toggle.cpp index 2968c80fd8cc5..9c5f88401d2b4 100644 --- a/Telegram/SourceFiles/ui/controls/silent_toggle.cpp +++ b/Telegram/SourceFiles/ui/controls/silent_toggle.cpp @@ -16,11 +16,6 @@ For license and copyright information please follow this link: #include "styles/style_chat_helpers.h" namespace Ui { -namespace { - -constexpr auto kAnimationDuration = crl::time(120); - -} // namespace SilentToggle::SilentToggle(QWidget *parent, not_null channel) : RippleButton(parent, st::historySilentToggle.ripple) diff --git a/Telegram/SourceFiles/ui/controls/who_reacted_context_action.cpp b/Telegram/SourceFiles/ui/controls/who_reacted_context_action.cpp index 3be58146ad13a..3d6ae76382391 100644 --- a/Telegram/SourceFiles/ui/controls/who_reacted_context_action.cpp +++ b/Telegram/SourceFiles/ui/controls/who_reacted_context_action.cpp @@ -509,8 +509,7 @@ void WhoReactedEntryAction::setData(Data &&data) { { data.date }, MenuTextOptions); } - _dateReacted = data.dateReacted; - _preloader = data.preloader; + _type = data.type; _custom = _customEmojiFactory ? _customEmojiFactory(data.customEntityData, [=] { update(); }) : nullptr; @@ -545,13 +544,14 @@ void WhoReactedEntryAction::paint(Painter &&p) { const auto photoSize = st::defaultWhoRead.photoSize; const auto photoLeft = st::defaultWhoRead.photoLeft; const auto photoTop = (height() - photoSize) / 2; - const auto preloaderBrush = _preloader + const auto preloader = (_type == WhoReactedType::Preloader); + const auto preloaderBrush = preloader ? [&] { auto color = _st.itemFg->c; color.setAlphaF(color.alphaF() * kPreloaderAlpha); return QBrush(color); }() : QBrush(); - if (_preloader) { + if (preloader) { auto hq = PainterHighQualityEnabler(p); p.setPen(Qt::NoPen); p.setBrush(preloaderBrush); @@ -568,7 +568,7 @@ void WhoReactedEntryAction::paint(Painter &&p) { const auto textTop = withDate ? st::whoReadNameWithDateTop : (height() - _st.itemStyle.font->height) / 2; - if (_preloader) { + if (_type == WhoReactedType::Preloader) { auto hq = PainterHighQualityEnabler(p); p.setPen(Qt::NoPen); p.setBrush(preloaderBrush); @@ -597,10 +597,28 @@ void WhoReactedEntryAction::paint(Painter &&p) { const auto iconPosition = QPoint( st::defaultWhoRead.nameLeft, st::whoReadDateTop) + st::whoReadDateChecksPosition; - const auto &icon = _dateReacted - ? (selected ? st::whoLikedDateHeartOver : st::whoLikedDateHeart) - : (selected ? st::whoReadDateChecksOver : st::whoReadDateChecks); - icon.paint(p, iconPosition, width()); + const auto icon = [&] { + switch (_type) { + case WhoReactedType::Viewed: + return &(selected + ? st::whoReadDateChecksOver + : st::whoReadDateChecks); + case WhoReactedType::Reacted: + return &(selected + ? st::whoLikedDateHeartOver + : st::whoLikedDateHeart); + case WhoReactedType::Reposted: + return &(selected + ? st::whoRepostedDateHeartOver + : st::whoRepostedDateHeart); + case WhoReactedType::Forwarded: + return &(selected + ? st::whoForwardedDateHeartOver + : st::whoForwardedDateHeart); + } + Unexpected("Type in WhoReactedEntryAction::paint."); + }(); + icon->paint(p, iconPosition, width()); p.setPen(selected ? _st.itemFgShortcutOver : _st.itemFgShortcut); _date.drawLeftElided( p, @@ -708,7 +726,9 @@ void WhoReactedListMenu::populate( append({ .text = participant.name, .date = participant.date, - .dateReacted = participant.dateReacted, + .type = (participant.dateReacted + ? WhoReactedType::Reacted + : WhoReactedType::Viewed), .customEntityData = participant.customEntityData, .userpic = participant.userpicLarge, .callback = chosen, diff --git a/Telegram/SourceFiles/ui/controls/who_reacted_context_action.h b/Telegram/SourceFiles/ui/controls/who_reacted_context_action.h index fa4d3a08ffbfc..0b668b52429fb 100644 --- a/Telegram/SourceFiles/ui/controls/who_reacted_context_action.h +++ b/Telegram/SourceFiles/ui/controls/who_reacted_context_action.h @@ -54,11 +54,18 @@ struct WhoReadContent { Fn participantChosen, Fn showAllChosen); +enum class WhoReactedType : uchar { + Viewed, + Reacted, + Reposted, + Forwarded, + Preloader, +}; + struct WhoReactedEntryData { QString text; QString date; - bool dateReacted = false; - bool preloader = false; + WhoReactedType type = WhoReactedType::Viewed; QString customEntityData; QImage userpic; Fn callback; @@ -95,8 +102,7 @@ class WhoReactedEntryAction final : public Menu::ItemBase { QImage _userpic; int _textWidth = 0; int _customSize = 0; - bool _dateReacted = false; - bool _preloader = false; + WhoReactedType _type = WhoReactedType::Viewed; }; diff --git a/Telegram/SourceFiles/ui/effects/premium.style b/Telegram/SourceFiles/ui/effects/premium.style index b847e5cc81671..c9c9ccd84838b 100644 --- a/Telegram/SourceFiles/ui/effects/premium.style +++ b/Telegram/SourceFiles/ui/effects/premium.style @@ -230,6 +230,16 @@ premiumGiftBox: Box(premiumPreviewBox) { buttonPadding: margins(12px, 12px, 12px, 12px); } +premiumGiftsUserpicButton: UserpicButton(defaultUserpicButton) { + size: size(66px, 66px); + photoSize: 66px; + photoPosition: point(-1px, -1px); +} +premiumGiftsUserpicBadgeSize: size(26px, 26px); +premiumGiftsUserpicBadgeInner: 2px; +premiumGiftsUserpicBadgeFont: font(14px bold); +premiumGiftsBoostIcon: icon{{ "stories/boost_mini", windowBgActive }}; + boostSkipTop: 37px; boostLimits: PremiumLimits(defaultPremiumLimits) { gradientFromLeft: true; diff --git a/Telegram/SourceFiles/ui/empty_userpic.cpp b/Telegram/SourceFiles/ui/empty_userpic.cpp index 7c46b7826ed82..2e3d9cbf0877f 100644 --- a/Telegram/SourceFiles/ui/empty_userpic.cpp +++ b/Telegram/SourceFiles/ui/empty_userpic.cpp @@ -151,6 +151,22 @@ void PaintRepliesMessagesInner( fg); } +void PaintHiddenAuthorInner( + QPainter &p, + int x, + int y, + int size, + const style::color &fg) { + PaintIconInner( + p, + x, + y, + size, + st::defaultDialogRow.photoSize, + st::dialogsHiddenAuthorUserpic, + fg); +} + void PaintExternalMessagesInner( QPainter &p, int x, @@ -397,6 +413,45 @@ QImage EmptyUserpic::GenerateRepliesMessages(int size) { }); } +void EmptyUserpic::PaintHiddenAuthor( + QPainter &p, + int x, + int y, + int outerWidth, + int size) { + auto bg = QLinearGradient(x, y, x, y + size); + bg.setStops({ + { 0., st::historyPeerSavedMessagesBg->c }, + { 1., st::historyPeerSavedMessagesBg2->c } + }); + const auto &fg = st::historyPeerUserpicFg; + PaintHiddenAuthor(p, x, y, outerWidth, size, QBrush(bg), fg); +} + +void EmptyUserpic::PaintHiddenAuthor( + QPainter &p, + int x, + int y, + int outerWidth, + int size, + QBrush bg, + const style::color &fg) { + x = style::RightToLeft() ? (outerWidth - x - size) : x; + + PainterHighQualityEnabler hq(p); + p.setBrush(bg); + p.setPen(Qt::NoPen); + p.drawEllipse(x, y, size, size); + + PaintHiddenAuthorInner(p, x, y, size, fg); +} + +QImage EmptyUserpic::GenerateHiddenAuthor(int size) { + return Generate(size, [&](QPainter &p) { + PaintHiddenAuthor(p, 0, 0, size, size); + }); +} + std::pair EmptyUserpic::uniqueKey() const { const auto first = (uint64(0xFFFFFFFFU) << 32) | anim::getPremultiplied(_colors.color1->c); diff --git a/Telegram/SourceFiles/ui/empty_userpic.h b/Telegram/SourceFiles/ui/empty_userpic.h index f71335a6a930c..5fc5598186059 100644 --- a/Telegram/SourceFiles/ui/empty_userpic.h +++ b/Telegram/SourceFiles/ui/empty_userpic.h @@ -81,6 +81,22 @@ class EmptyUserpic final : public base::has_weak_ptr { const style::color &fg); [[nodiscard]] static QImage GenerateRepliesMessages(int size); + static void PaintHiddenAuthor( + QPainter &p, + int x, + int y, + int outerWidth, + int size); + static void PaintHiddenAuthor( + QPainter &p, + int x, + int y, + int outerWidth, + int size, + QBrush bg, + const style::color &fg); + [[nodiscard]] static QImage GenerateHiddenAuthor(int size); + ~EmptyUserpic(); private: diff --git a/Telegram/SourceFiles/ui/new_badges.cpp b/Telegram/SourceFiles/ui/new_badges.cpp new file mode 100644 index 0000000000000..aa1cadafa3666 --- /dev/null +++ b/Telegram/SourceFiles/ui/new_badges.cpp @@ -0,0 +1,69 @@ +/* +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 "ui/new_badges.h" + +#include "lang/lang_keys.h" +#include "ui/painter.h" +#include "ui/widgets/labels.h" +#include "styles/style_window.h" +#include "styles/style_settings.h" + +namespace Ui::NewBadge { +namespace { + +[[nodiscard]] not_null CreateNewBadge( + not_null parent, + rpl::producer text) { + const auto badge = Ui::CreateChild>( + parent.get(), + object_ptr( + parent, + std::move(text), + st::settingsPremiumNewBadge), + st::settingsPremiumNewBadgePadding); + badge->setAttribute(Qt::WA_TransparentForMouseEvents); + badge->paintRequest() | rpl::start_with_next([=] { + auto p = QPainter(badge); + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + p.setBrush(st::windowBgActive); + const auto r = st::settingsPremiumNewBadgePadding.left(); + p.drawRoundedRect(badge->rect(), r, r); + }, badge->lifetime()); + return badge; +} + +} // namespace + +void AddToRight(not_null parent) { + const auto badge = CreateNewBadge(parent, tr::lng_bot_side_menu_new()); + + parent->sizeValue( + ) | rpl::start_with_next([=](QSize size) { + badge->moveToRight( + st::mainMenuButton.padding.right(), + (size.height() - badge->height()) / 2, + size.width()); + }, badge->lifetime()); +} + +void AddAfterLabel( + not_null parent, + not_null label) { + const auto badge = CreateNewBadge( + parent, + tr::lng_premium_summary_new_badge()); + + label->geometryValue( + ) | rpl::start_with_next([=](QRect geometry) { + badge->move(st::settingsPremiumNewBadgePosition + + QPoint(label->x() + label->width(), label->y())); + }, badge->lifetime()); +} + +} // namespace Ui::NewBadge diff --git a/Telegram/SourceFiles/ui/new_badges.h b/Telegram/SourceFiles/ui/new_badges.h new file mode 100644 index 0000000000000..0d2161251f657 --- /dev/null +++ b/Telegram/SourceFiles/ui/new_badges.h @@ -0,0 +1,21 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +namespace Ui { +class RpWidget; +} // namespace Ui + +namespace Ui::NewBadge { + +void AddToRight(not_null parent); +void AddAfterLabel( + not_null parent, + not_null label); + +} // namespace Ui::NewBadge diff --git a/Telegram/SourceFiles/ui/unread_badge.cpp b/Telegram/SourceFiles/ui/unread_badge.cpp index 62e03a5b2e2c4..375beb5c68d6c 100644 --- a/Telegram/SourceFiles/ui/unread_badge.cpp +++ b/Telegram/SourceFiles/ui/unread_badge.cpp @@ -132,7 +132,7 @@ int PeerBadge::drawGetWidth( Expects(descriptor.customEmojiRepaint != nullptr); const auto peer = descriptor.peer; - if ((peer->isScam() || peer->isFake()) && descriptor.scam) { + if (descriptor.scam && (peer->isScam() || peer->isFake())) { const auto phrase = peer->isScam() ? tr::lng_scam_badge(tr::now) : tr::lng_fake_badge(tr::now); @@ -159,27 +159,15 @@ int PeerBadge::drawGetWidth( phrase, phraseWidth); return st::dialogsScamSkip + width; - } else if (peer->isVerified() && descriptor.verified) { - const auto iconw = descriptor.verified->width(); - descriptor.verified->paint( - p, - rectForName.x() + qMin(nameWidth, rectForName.width() - iconw), - rectForName.y(), - outerWidth); - return iconw; - } else if (peer->isPremium() - && descriptor.premium + } else if (descriptor.premium + && peer->emojiStatusId() + && (peer->isPremium() || peer->isChannel()) && peer->session().premiumBadgesShown()) { - const auto id = peer->isUser() ? peer->asUser()->emojiStatusId() : 0; + const auto id = peer->emojiStatusId(); const auto iconw = descriptor.premium->width(); const auto iconx = rectForName.x() + qMin(nameWidth, rectForName.width() - iconw); const auto icony = rectForName.y(); - if (!id) { - _emojiStatus = nullptr; - descriptor.premium->paint(p, iconx, icony, outerWidth); - return iconw; - } if (!_emojiStatus) { _emojiStatus = std::make_unique(); const auto size = st::emojiSize; @@ -205,6 +193,24 @@ int PeerBadge::drawGetWidth( .paused = descriptor.paused || On(PowerSaving::kEmojiStatus), }); return iconw - 4 * _emojiStatus->skip; + } else if (descriptor.verified && peer->isVerified()) { + const auto iconw = descriptor.verified->width(); + descriptor.verified->paint( + p, + rectForName.x() + qMin(nameWidth, rectForName.width() - iconw), + rectForName.y(), + outerWidth); + return iconw; + } else if (descriptor.premium + && peer->isPremium() + && peer->session().premiumBadgesShown()) { + const auto iconw = descriptor.premium->width(); + const auto iconx = rectForName.x() + + qMin(nameWidth, rectForName.width() - iconw); + const auto icony = rectForName.y(); + _emojiStatus = nullptr; + descriptor.premium->paint(p, iconx, icony, outerWidth); + return iconw; } return 0; } diff --git a/Telegram/SourceFiles/ui/vertical_list.cpp b/Telegram/SourceFiles/ui/vertical_list.cpp index 0fcf7012f6f48..34c6c0916bae1 100644 --- a/Telegram/SourceFiles/ui/vertical_list.cpp +++ b/Telegram/SourceFiles/ui/vertical_list.cpp @@ -7,6 +7,7 @@ For license and copyright information please follow this link: */ #include "ui/vertical_list.h" +#include "ui/text/text_utilities.h" #include "ui/widgets/box_content_divider.h" #include "ui/widgets/labels.h" #include "ui/wrap/padding_wrap.h" @@ -30,6 +31,12 @@ void AddDivider(not_null container) { void AddDividerText( not_null container, rpl::producer text) { + AddDividerText(container, std::move(text) | Ui::Text::ToWithEntities()); +} + +void AddDividerText( + not_null container, + rpl::producer text) { container->add(object_ptr( container, object_ptr( diff --git a/Telegram/SourceFiles/ui/vertical_list.h b/Telegram/SourceFiles/ui/vertical_list.h index 66fc03bffabb2..970e73f4c0784 100644 --- a/Telegram/SourceFiles/ui/vertical_list.h +++ b/Telegram/SourceFiles/ui/vertical_list.h @@ -22,6 +22,9 @@ void AddDivider(not_null container); void AddDividerText( not_null container, rpl::producer text); +void AddDividerText( + not_null container, + rpl::producer text); not_null AddSubsectionTitle( not_null container, rpl::producer text, diff --git a/Telegram/SourceFiles/ui/widgets/multi_select.cpp b/Telegram/SourceFiles/ui/widgets/multi_select.cpp index 99b7eaf7db365..8c9649f375f81 100644 --- a/Telegram/SourceFiles/ui/widgets/multi_select.cpp +++ b/Telegram/SourceFiles/ui/widgets/multi_select.cpp @@ -381,7 +381,7 @@ void Item::prepareCache() { paintOnce( p, _width * (kWideScale - 1) / 2, - _st.height * (kWideScale - 1) / 2, + _st.height * (kWideScale - 1) / 2, cacheWidth); } _cache = Ui::PixmapFromImage(std::move(data)); diff --git a/Telegram/SourceFiles/window/notifications_manager.cpp b/Telegram/SourceFiles/window/notifications_manager.cpp index 440703d2fa817..e0dc8d1937d66 100644 --- a/Telegram/SourceFiles/window/notifications_manager.cpp +++ b/Telegram/SourceFiles/window/notifications_manager.cpp @@ -900,7 +900,6 @@ TextWithEntities Manager::ComposeReactionEmoji( return TextWithEntities{ *emoji }; } const auto id = v::get(reaction.data); - auto entities = EntitiesInText(); const auto document = session->data().document(id); const auto sticker = document->sticker(); const auto text = sticker ? sticker->alt : PlaceholderReactionText(); diff --git a/Telegram/SourceFiles/window/section_widget.cpp b/Telegram/SourceFiles/window/section_widget.cpp index d08a28c593dbd..47e1196eaacc5 100644 --- a/Telegram/SourceFiles/window/section_widget.cpp +++ b/Telegram/SourceFiles/window/section_widget.cpp @@ -50,13 +50,22 @@ struct ResolvedPaper { std::shared_ptr media; }; -[[nodiscard]] rpl::producer> PeerWallPaperValue( +[[nodiscard]] rpl::producer PeerWallPaperMapped( not_null peer) { return peer->session().changes().peerFlagsValue( peer, Data::PeerUpdate::Flag::ChatWallPaper - ) | rpl::map([=]() -> rpl::producer> { - const auto paper = peer->wallPaper(); + ) | rpl::map([=]() -> rpl::producer { + return WallPaperResolved(&peer->owner(), peer->wallPaper()); + }) | rpl::flatten_latest(); +} + +[[nodiscard]] rpl::producer> PeerWallPaperValue( + not_null peer) { + return PeerWallPaperMapped( + peer + ) | rpl::map([=](const Data::WallPaper *paper) + -> rpl::producer> { const auto single = [](std::optional value) { return rpl::single(std::move(value)); }; @@ -157,6 +166,47 @@ struct ResolvedTheme { } // namespace +rpl::producer WallPaperResolved( + not_null owner, + const Data::WallPaper *paper) { + const auto id = paper ? paper->emojiId() : QString(); + if (id.isEmpty()) { + return rpl::single(paper); + } + const auto themes = &owner->cloudThemes(); + auto fromThemes = [=](bool force) + -> rpl::producer { + if (themes->chatThemes().empty() && !force) { + return nullptr; + } + return Window::Theme::IsNightModeValue( + ) | rpl::map([=](bool dark) -> const Data::WallPaper* { + const auto &list = themes->chatThemes(); + const auto i = ranges::find( + list, + id, + &Data::CloudTheme::emoticon); + if (i != end(list)) { + using Type = Data::CloudThemeType; + const auto type = dark ? Type::Dark : Type::Light; + const auto j = i->settings.find(type); + if (j != end(i->settings) && j->second.paper) { + return &*j->second.paper; + } + } + return nullptr; + }); + }; + if (auto result = fromThemes(false)) { + return result; + } + themes->refreshChatThemes(); + return themes->chatThemesUpdated( + ) | rpl::take(1) | rpl::map([=] { + return fromThemes(true); + }) | rpl::flatten_latest(); +} + AbstractSectionWidget::AbstractSectionWidget( QWidget *parent, not_null controller, @@ -476,7 +526,8 @@ bool ShowReactPremiumError( not_null item, const Data::ReactionId &id) { if (controller->session().premium() - || ranges::contains(item->chosenReactions(), id)) { + || ranges::contains(item->chosenReactions(), id) + || item->history()->peer->isBroadcast()) { return false; } const auto &list = controller->session().data().reactions().list( diff --git a/Telegram/SourceFiles/window/section_widget.h b/Telegram/SourceFiles/window/section_widget.h index d8cfda8147790..52b94f70252ea 100644 --- a/Telegram/SourceFiles/window/section_widget.h +++ b/Telegram/SourceFiles/window/section_widget.h @@ -23,6 +23,8 @@ class Show; namespace Data { struct ReactionId; class ForumTopic; +class WallPaper; +class Session; } // namespace Data namespace Main { @@ -251,4 +253,8 @@ class SectionWidget : public AbstractSectionWidget { not_null item, const Data::ReactionId &id); +[[nodiscard]] rpl::producer WallPaperResolved( + not_null owner, + const Data::WallPaper *paper); + } // namespace Window diff --git a/Telegram/SourceFiles/window/themes/window_theme_editor_block.cpp b/Telegram/SourceFiles/window/themes/window_theme_editor_block.cpp index ac217b30b91d6..bd1e5c311d967 100644 --- a/Telegram/SourceFiles/window/themes/window_theme_editor_block.cpp +++ b/Telegram/SourceFiles/window/themes/window_theme_editor_block.cpp @@ -819,7 +819,7 @@ EditorBlock::Row &EditorBlock::rowAtIndex(int index) { } int EditorBlock::findRowIndex(const QString &name) const { - return _indices.value(name, -1);; + return _indices.value(name, -1); } EditorBlock::Row *EditorBlock::findRow(const QString &name) { diff --git a/Telegram/SourceFiles/window/window_main_menu.cpp b/Telegram/SourceFiles/window/window_main_menu.cpp index 9de683e4a77e7..051a99d8b862c 100644 --- a/Telegram/SourceFiles/window/window_main_menu.cpp +++ b/Telegram/SourceFiles/window/window_main_menu.cpp @@ -15,22 +15,18 @@ For license and copyright information please follow this link: #include "ui/controls/userpic_button.h" #include "ui/effects/snowflakes.h" #include "ui/effects/toggle_arrow.h" -#include "ui/widgets/buttons.h" -#include "ui/widgets/labels.h" #include "ui/widgets/popup_menu.h" #include "ui/widgets/scroll_area.h" #include "ui/widgets/shadow.h" #include "ui/widgets/tooltip.h" #include "ui/wrap/slide_wrap.h" -#include "ui/wrap/vertical_layout.h" #include "ui/text/text_utilities.h" #include "ui/text/text_options.h" +#include "ui/new_badges.h" #include "ui/painter.h" -#include "ui/empty_userpic.h" #include "ui/vertical_list.h" #include "ui/unread_badge_paint.h" #include "inline_bots/bot_attach_web_view.h" -#include "mainwindow.h" #include "storage/localstorage.h" #include "storage/storage_account.h" #include "support/support_templates.h" @@ -50,7 +46,6 @@ For license and copyright information please follow this link: #include "calls/calls_box_controller.h" #include "lang/lang_keys.h" #include "core/click_handler_types.h" -#include "core/core_settings.h" #include "core/application.h" #include "main/main_session.h" #include "main/main_session_settings.h" @@ -66,7 +61,6 @@ For license and copyright information please follow this link: #include "mainwidget.h" #include "styles/style_chat.h" // popupMenuExpandedSeparator #include "styles/style_window.h" -#include "styles/style_widgets.h" #include "styles/style_settings.h" #include "styles/style_info.h" // infoTopBarMenu #include "styles/style_layers.h" @@ -109,7 +103,7 @@ class VersionLabel final } bool tooltipWindowActive() const override { - return Ui::AppInFocus() && Ui::InFocusChain(window()); + return Ui::AppInFocus() && Ui::InFocusChain(window()); } }; @@ -301,33 +295,8 @@ void SetupMenuBots( } }, button->lifetime()); - const auto badge = bots->showMainMenuNewBadge(bot) - ? Ui::CreateChild>( - button, - object_ptr( - button, - tr::lng_bot_side_menu_new(), - st::settingsPremiumNewBadge), - st::settingsPremiumNewBadgePadding) - : nullptr; - if (badge) { - badge->setAttribute(Qt::WA_TransparentForMouseEvents); - badge->paintRequest() | rpl::start_with_next([=] { - auto p = QPainter(badge); - auto hq = PainterHighQualityEnabler(p); - p.setPen(Qt::NoPen); - p.setBrush(st::windowBgActive); - const auto r = st::settingsPremiumNewBadgePadding.left(); - p.drawRoundedRect(badge->rect(), r, r); - }, badge->lifetime()); - - button->sizeValue( - ) | rpl::start_with_next([=](QSize size) { - badge->moveToRight( - st::mainMenuButton.padding.right(), - (size.height() - badge->height()) / 2, - size.width()); - }, badge->lifetime()); + if (bots->showMainMenuNewBadge(bot)) { + Ui::NewBadge::AddToRight(button); } } wrap->resizeToWidth(width); diff --git a/Telegram/SourceFiles/window/window_media_preview.cpp b/Telegram/SourceFiles/window/window_media_preview.cpp index 41fe410e0edcd..be931148b817b 100644 --- a/Telegram/SourceFiles/window/window_media_preview.cpp +++ b/Telegram/SourceFiles/window/window_media_preview.cpp @@ -46,6 +46,13 @@ MediaPreviewWidget::MediaPreviewWidget( ) | rpl::start_with_next([=] { update(); }, lifetime()); + + style::PaletteChanged( + ) | rpl::start_with_next([=] { + if (_document && _document->emojiUsesTextColor()) { + _cache = QPixmap(); + } + }, lifetime()); } QRect MediaPreviewWidget::updateArea() const { @@ -68,7 +75,12 @@ void MediaPreviewWidget::paintEvent(QPaintEvent *e) { const auto factor = cIntRetinaFactor(); const auto dimensions = currentDimensions(); const auto frame = (_lottie && _lottie->ready()) - ? _lottie->frameInfo({ dimensions * factor }) + ? _lottie->frameInfo({ + .box = dimensions * factor, + .colored = ((_document && _document->emojiUsesTextColor()) + ? st::windowFg->c + : QColor(0, 0, 0, 0)), + }) : Lottie::Animation::FrameInfo(); const auto effect = (_effect && _effect->ready()) ? _effect->frameInfo({ dimensions * kPremiumMultiplier * factor }) @@ -370,6 +382,12 @@ QPixmap MediaPreviewWidget::currentImage() const { && _documentMedia->thumbnail()) { QSize s = currentDimensions(); _cache = _documentMedia->thumbnail()->pix(s, blur); + if (_document && _document->emojiUsesTextColor()) { + _cache = Ui::PixmapFromImage( + Images::Colored( + _cache.toImage(), + st::windowFg->c)); + } _cacheStatus = CacheThumbLoaded; } } diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index 77213a5c6a10a..4b8ce6409a933 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -76,6 +76,7 @@ For license and copyright information please follow this link: #include "data/data_forum.h" #include "data/data_forum_topic.h" #include "data/data_user.h" +#include "data/data_saved_sublist.h" #include "data/data_scheduled_messages.h" #include "data/data_histories.h" #include "data/data_chat_filters.h" @@ -244,6 +245,7 @@ class Filler { void fillRepliesActions(); void fillScheduledActions(); void fillArchiveActions(); + void fillSavedSublistActions(); void fillContextMenuActions(); void addHidePromotion(); @@ -293,6 +295,7 @@ class Filler { Data::ForumTopic *_topic = nullptr; PeerData *_peer = nullptr; Data::Folder *_folder = nullptr; + Data::SavedSublist *_sublist = nullptr; const PeerMenuCallback &_addAction; }; @@ -319,17 +322,21 @@ void AddChatMembers( bool PinnedLimitReached( not_null controller, - not_null thread) { - const auto owner = &thread->owner(); - if (owner->pinnedCanPin(thread)) { + not_null entry) { + const auto owner = &entry->owner(); + if (owner->pinnedCanPin(entry)) { return false; } // Some old chat, that was converted, maybe is still pinned. - const auto history = thread->asHistory(); - if (!history) { - controller->show(Box(ForumPinsLimitBox, thread->asTopic()->forum())); + if (const auto sublist = entry->asSublist()) { + controller->show(Box(SublistsPinsLimitBox, &sublist->session())); + return true; + } else if (const auto topic = entry->asTopic()) { + controller->show(Box(ForumPinsLimitBox, topic->forum())); return true; } + const auto history = entry->asHistory(); + Assert(history != nullptr); const auto folder = history->folder(); const auto wasted = FindWastedPin(owner, folder); if (wasted) { @@ -359,18 +366,18 @@ bool PinnedLimitReached( void TogglePinnedThread( not_null controller, - not_null thread) { - if (!thread->folderKnown()) { + not_null entry) { + if (!entry->folderKnown()) { return; } - const auto owner = &thread->owner(); - const auto isPinned = !thread->isPinnedDialog(FilterId()); - if (isPinned && PinnedLimitReached(controller, thread)) { + const auto owner = &entry->owner(); + const auto isPinned = !entry->isPinnedDialog(FilterId()); + if (isPinned && PinnedLimitReached(controller, entry)) { return; } - owner->setChatPinned(thread, FilterId(), isPinned); - if (const auto history = thread->asHistory()) { + owner->setChatPinned(entry, FilterId(), isPinned); + if (const auto history = entry->asHistory()) { const auto flags = isPinned ? MTPmessages_ToggleDialogPin::Flag::f_pinned : MTPmessages_ToggleDialogPin::Flag(0); @@ -383,7 +390,7 @@ void TogglePinnedThread( if (isPinned) { controller->content()->dialogsToUp(); } - } else if (const auto topic = thread->asTopic()) { + } else if (const auto topic = entry->asTopic()) { owner->session().api().request(MTPchannels_UpdatePinnedForumTopic( topic->channel()->inputChannel, MTP_int(topic->rootId()), @@ -391,17 +398,30 @@ void TogglePinnedThread( )).done([=](const MTPUpdates &result) { owner->session().api().applyUpdates(result); }).send(); + } else if (const auto sublist = entry->asSublist()) { + const auto flags = isPinned + ? MTPmessages_ToggleSavedDialogPin::Flag::f_pinned + : MTPmessages_ToggleSavedDialogPin::Flag(0); + owner->session().api().request(MTPmessages_ToggleSavedDialogPin( + MTP_flags(flags), + MTP_inputDialogPeer(sublist->peer()->input) + )).done([=] { + owner->notifyPinnedDialogsOrderUpdated(); + }).send(); + //if (isPinned) { + // controller->content()->dialogsToUp(); + //} } } void TogglePinnedThread( not_null controller, - not_null thread, + not_null entry, FilterId filterId) { if (!filterId) { - return TogglePinnedThread(controller, thread); + return TogglePinnedThread(controller, entry); } - const auto history = thread->asHistory(); + const auto history = entry->asHistory(); if (!history) { return; } @@ -438,6 +458,7 @@ Filler::Filler( , _topic(request.key.topic()) , _peer(request.key.peer()) , _folder(request.key.folder()) +, _sublist(request.key.sublist()) , _addAction(addAction) { } @@ -471,21 +492,21 @@ void Filler::addToggleTopicClosed() { } void Filler::addTogglePin() { - if (!_peer || (_topic && !_topic->canTogglePinned())) { + if ((!_sublist && !_peer) || (_topic && !_topic->canTogglePinned())) { return; } const auto controller = _controller; const auto filterId = _request.filterId; - const auto thread = _request.key.thread(); - if (!thread || thread->fixedOnTopIndex()) { + const auto entry = _thread ? (Dialogs::Entry*)_thread : _sublist; + if (!entry || entry->fixedOnTopIndex()) { return; } const auto pinText = [=] { - return thread->isPinnedDialog(filterId) + return entry->isPinnedDialog(filterId) ? tr::lng_context_unpin_from_top(tr::now) : tr::lng_context_pin_to_top(tr::now); }; - const auto weak = base::make_weak(thread); + const auto weak = base::make_weak(entry); const auto pinToggle = [=] { if (const auto strong = weak.get()) { TogglePinnedThread(controller, strong, filterId); @@ -494,13 +515,13 @@ void Filler::addTogglePin() { _addAction( pinText(), pinToggle, - (thread->isPinnedDialog(filterId) + (entry->isPinnedDialog(filterId) ? &st::menuIconUnpin : &st::menuIconPin)); } void Filler::addToggleMuteSubmenu(bool addSeparator) { - if (_thread->peer()->isSelf()) { + if (!_thread || _thread->peer()->isSelf()) { return; } PeerMenuAddMuteSubmenuAction(_controller, _thread, _addAction); @@ -526,6 +547,8 @@ void Filler::addSupportInfo() { void Filler::addInfo() { if (_peer && (_peer->isSelf() || _peer->isRepliesChat())) { return; + } else if (!_thread) { + return; } else if (_controller->adaptive().isThreeColumn()) { const auto thread = _controller->activeChatCurrent().thread(); if (thread && thread == _thread) { @@ -534,8 +557,6 @@ void Filler::addInfo() { return; } } - } else if (!_thread) { - return; } const auto controller = _controller; const auto weak = base::make_weak(_thread); @@ -652,8 +673,8 @@ void Filler::addToggleArchive() { ? tr::lng_archived_remove(tr::now) : tr::lng_archived_add(tr::now); }; - const auto toggle = [=] { - ToggleHistoryArchived(history, !isArchived()); + const auto toggle = [=, show = _controller->uiShow()] { + ToggleHistoryArchived(show, history, !isArchived()); }; const auto archiveAction = _addAction( label(), @@ -1005,7 +1026,9 @@ void Filler::addViewStatistics() { const auto controller = _controller; const auto weak = base::make_weak(_thread); const auto peer = _peer; - if (channel->flags() & ChannelDataFlag::CanGetStatistics) { + using Flag = ChannelDataFlag; + const auto canGetStats = (channel->flags() & Flag::CanGetStatistics); + if (canGetStats) { _addAction(tr::lng_stats_title(tr::now), [=] { if (const auto strong = weak.get()) { using namespace Info; @@ -1014,7 +1037,9 @@ void Filler::addViewStatistics() { }, &st::menuIconStats); } if (!channel->isMegagroup() - && (channel->amCreator() || channel->canPostStories())) { + && (canGetStats + || channel->amCreator() + || channel->canPostStories())) { _addAction(tr::lng_boosts_title(tr::now), [=] { if (const auto strong = weak.get()) { controller->showSection(Info::Boosts::Make(peer)); @@ -1112,9 +1137,9 @@ void Filler::addGiftPremium() { void Filler::fill() { if (_folder) { fillArchiveActions(); - return; - } - switch (_request.section) { + } else if (_sublist) { + fillSavedSublistActions(); + } else switch (_request.section) { case Section::ChatsList: fillChatsListActions(); break; case Section::History: fillHistoryActions(); break; case Section::Profile: fillProfileActions(); break; @@ -1349,6 +1374,10 @@ void Filler::fillArchiveActions() { }, &st::menuIconManage); } +void Filler::fillSavedSublistActions() { + addTogglePin(); +} + } // namespace void PeerMenuExportChat(not_null peer) { @@ -2402,9 +2431,12 @@ void MenuAddMarkAsReadChatListAction( &st::menuIconMarkRead); } -void ToggleHistoryArchived(not_null history, bool archived) { +void ToggleHistoryArchived( + std::shared_ptr show, + not_null history, + bool archived) { const auto callback = [=] { - Ui::Toast::Show(Ui::Toast::Config{ + show->showToast(Ui::Toast::Config{ .text = { (archived ? tr::lng_archived_added(tr::now) : tr::lng_archived_removed(tr::now)) }, diff --git a/Telegram/SourceFiles/window/window_peer_menu.h b/Telegram/SourceFiles/window/window_peer_menu.h index f5fff7f1f085c..44503042b822b 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.h +++ b/Telegram/SourceFiles/window/window_peer_menu.h @@ -116,7 +116,10 @@ void BlockSenderFromRepliesBox( not_null controller, FullMsgId id); -void ToggleHistoryArchived(not_null history, bool archived); +void ToggleHistoryArchived( + std::shared_ptr show, + not_null history, + bool archived); Fn ClearHistoryHandler( not_null controller, not_null peer); diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 90ff70a2f1452..3a348ade3e8a9 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -623,6 +623,7 @@ void SessionNavigation::resolveBoostState(not_null channel) { channel->input )).done([=](const MTPpremium_BoostsStatus &result) { _boostStateResolving = nullptr; + channel->updateLevelHint(result.data().vlevel().v); const auto submit = [=](Fn done) { applyBoost(channel, done); }; @@ -730,6 +731,7 @@ void SessionNavigation::applyBoostsChecked( _api.request(MTPpremium_GetBoostsStatus( channel->input )).done([=](const MTPpremium_BoostsStatus &result) { + channel->updateLevelHint(result.data().vlevel().v); done(ParseBoostCounters(result)); }).fail([=](const MTP::Error &error) { showToast(u"Error: "_q + error.type()); @@ -1216,6 +1218,10 @@ void SessionController::showGiftPremiumBox(UserData *user) { } } +void SessionController::showGiftPremiumsBox() { + _giftPremiumValidator.showChoosePeerBox(); +} + void SessionController::init() { if (session().supportMode()) { initSupportMode(); diff --git a/Telegram/SourceFiles/window/window_session_controller.h b/Telegram/SourceFiles/window/window_session_controller.h index 33b3c44a5b3d5..885eb812eeaff 100644 --- a/Telegram/SourceFiles/window/window_session_controller.h +++ b/Telegram/SourceFiles/window/window_session_controller.h @@ -380,6 +380,7 @@ class SessionController : public SessionNavigation { void showEditPeerBox(PeerData *peer); void showGiftPremiumBox(UserData *user); + void showGiftPremiumsBox(); void enableGifPauseReason(GifPauseReason reason); void disableGifPauseReason(GifPauseReason reason); diff --git a/Telegram/ThirdParty/hunspell b/Telegram/ThirdParty/hunspell index 2969be996acad..22c3381e2066b 160000 --- a/Telegram/ThirdParty/hunspell +++ b/Telegram/ThirdParty/hunspell @@ -1 +1 @@ -Subproject commit 2969be996acad84b91ab3875b1816636fe61a40e +Subproject commit 22c3381e2066bed616250d373fc5c935598b564a diff --git a/Telegram/ThirdParty/minizip/ioapi.c b/Telegram/ThirdParty/minizip/ioapi.c index 814a6fd38c26b..0ca29db6af61c 100644 --- a/Telegram/ThirdParty/minizip/ioapi.c +++ b/Telegram/ThirdParty/minizip/ioapi.c @@ -231,8 +231,7 @@ static int ZCALLBACK ferror_file_func (voidpf opaque, voidpf stream) return ret; } -void fill_fopen_filefunc (pzlib_filefunc_def) - zlib_filefunc_def* pzlib_filefunc_def; +void fill_fopen_filefunc (zlib_filefunc_def* pzlib_filefunc_def) { pzlib_filefunc_def->zopen_file = fopen_file_func; pzlib_filefunc_def->zread_file = fread_file_func; diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile index fbe9e0d162ef3..253b0207a991c 100644 --- a/Telegram/build/docker/centos_env/Dockerfile +++ b/Telegram/build/docker/centos_env/Dockerfile @@ -24,14 +24,14 @@ RUN dnf -y install epel-release \ && dnf -y install autoconf automake libtool pkgconfig make patch git \ python3.11-pip python3.11-devel gperf flex bison clang lld yasm \ file which perl-open perl-XML-Parser xorg-x11-util-macros \ - gcc-toolset-13-gcc gcc-toolset-13-gcc-c++ gcc-toolset-13-binutils \ + gcc-toolset-12-gcc gcc-toolset-12-gcc-c++ gcc-toolset-12-binutils \ libffi-devel fontconfig-devel freetype-devel libX11-devel \ alsa-lib-devel pulseaudio-libs-devel mesa-libGL-devel mesa-libEGL-devel \ mesa-libgbm-devel libdrm-devel vulkan-devel libva-devel libvdpau-devel \ glib2-devel at-spi2-core-devel gtk3-devel boost1.78-devel fmt-devel \ && dnf clean all -SHELL [ "bash", "-c", ". /opt/rh/gcc-toolset-13/enable; exec bash -c \"$@\"", "-s"] +SHELL [ "bash", "-c", ". /opt/rh/gcc-toolset-12/enable; exec bash -c \"$@\"", "-s"] WORKDIR {{ LibrariesPath }} @@ -54,7 +54,7 @@ FROM builder AS patches RUN git init patches \ && cd patches \ && git remote add origin {{ GIT }}/desktop-app/patches.git \ - && git fetch --depth=1 origin 12780ae73abe21d00ae125e284d8bca9d8993948 \ + && git fetch --depth=1 origin 78fe199b743df0358c04502d6947cd42bb5d16ed \ && git reset --hard FETCH_HEAD \ && rm -rf .git @@ -820,7 +820,7 @@ COPY --link --from=pipewire {{ LibrariesPath }}/pipewire-cache / RUN git init tg_owt \ && cd tg_owt \ && git remote add origin {{ GIT }}/desktop-app/tg_owt.git \ - && git fetch --depth=1 origin 3bb3d757681e6cc5135aec6529a753dc3dcdcfb9 \ + && git fetch --depth=1 origin afd9d5d31798d3eacf9ed6c30601e91d0f1e4d60 \ && git reset --hard FETCH_HEAD \ && git submodule update --init --recursive --depth=1 \ && rm -rf .git \ @@ -906,5 +906,5 @@ ENV BOOST_INCLUDEDIR /usr/include/boost1.78 ENV BOOST_LIBRARYDIR /usr/lib64/boost1.78 VOLUME [ "/usr/src/tdesktop" ] -ENTRYPOINT [ "scl", "enable", "gcc-toolset-13", "--" ] +ENTRYPOINT [ "scl", "enable", "gcc-toolset-12", "--" ] CMD [ "/usr/src/tdesktop/Telegram/build/docker/centos_env/build.sh" ] diff --git a/Telegram/build/prepare/prepare.py b/Telegram/build/prepare/prepare.py index 1c80d10dc3b25..c29d931785d9e 100644 --- a/Telegram/build/prepare/prepare.py +++ b/Telegram/build/prepare/prepare.py @@ -1426,7 +1426,7 @@ def runStages(): stage('tg_owt', """ git clone https://github.com/desktop-app/tg_owt.git cd tg_owt - git checkout 76a3513d7f + git checkout afd9d5d317 git submodule init git submodule update win: diff --git a/Telegram/build/version b/Telegram/build/version index e2d583a7cca4a..58ece976683a4 100644 --- a/Telegram/build/version +++ b/Telegram/build/version @@ -1,7 +1,7 @@ -AppVersion 4012002 -AppVersionStrMajor 4.12 -AppVersionStrSmall 4.12.2 -AppVersionStr 4.12.2 +AppVersion 4014000 +AppVersionStrMajor 4.14 +AppVersionStrSmall 4.14 +AppVersionStr 4.14.0 BetaChannel 0 AlphaVersion 0 -AppVersionOriginal 4.12.2 +AppVersionOriginal 4.14 diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index bacd9fe15029d..4df7b5c6ef1e0 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -377,6 +377,8 @@ PRIVATE ui/empty_userpic.h ui/grouped_layout.cpp ui/grouped_layout.h + ui/new_badges.cpp + ui/new_badges.h ui/power_saving.cpp ui/power_saving.h ui/vertical_list.cpp diff --git a/Telegram/codegen b/Telegram/codegen index 805b851d69444..afed06a4c04d1 160000 --- a/Telegram/codegen +++ b/Telegram/codegen @@ -1 +1 @@ -Subproject commit 805b851d69444ab3659b619566d186d0be83dcd9 +Subproject commit afed06a4c04d1a1cf7cfce4faca273e1f574462e diff --git a/Telegram/lib_base b/Telegram/lib_base index f69758da1906b..0d111bd463332 160000 --- a/Telegram/lib_base +++ b/Telegram/lib_base @@ -1 +1 @@ -Subproject commit f69758da1906b204c156ea6ad254eacd61210a42 +Subproject commit 0d111bd4633324533195aa0a840730b4bf6ba75b diff --git a/Telegram/lib_spellcheck b/Telegram/lib_spellcheck index 10cd8056be61a..96543c1716d37 160000 --- a/Telegram/lib_spellcheck +++ b/Telegram/lib_spellcheck @@ -1 +1 @@ -Subproject commit 10cd8056be61aa2ba79e7580f8f459285ab63559 +Subproject commit 96543c1716d3790ef12bdec6b113958427710441 diff --git a/Telegram/lib_ui b/Telegram/lib_ui index 06360dea67358..e9901b17192f3 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit 06360dea67358b62e21c014c4b0d20461d752c98 +Subproject commit e9901b17192f35c8205751f889ef14e5859b946b diff --git a/Telegram/lib_webview b/Telegram/lib_webview index 3495eb8dd8f4e..1bb91474c2337 160000 --- a/Telegram/lib_webview +++ b/Telegram/lib_webview @@ -1 +1 @@ -Subproject commit 3495eb8dd8f4e2dab9493b4a698e5006fadbca47 +Subproject commit 1bb91474c2337396673dd8a7c68e65ea317b6db5 diff --git a/changelog.txt b/changelog.txt index 7ddf4f2939778..bcbc088ae324f 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,19 @@ +4.14 (31.12.23) + +- Improved saved messages. +- One-time voice messages. + +4.13.1 (23.12.23) + +- Fix crash in chat history right click. +- Fix user emoji status display in main menu, profile and settings. + +4.13 (22.12.23) + +- Support setting channel wallpaper. +- Support setting channel emoji status. +- Allow gifting premium to several recipients at once. + 4.12.2 (01.12.23) - Fix choosing custom reactions in channels. diff --git a/cmake b/cmake index 92f27add11ae4..4005d7befb3ff 160000 --- a/cmake +++ b/cmake @@ -1 +1 @@ -Subproject commit 92f27add11ae4280939079249d0f9da933ece6ad +Subproject commit 4005d7befb3ffbbbb7851ed767d5b58373958e6d