diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 210c88d0d..b3a50984f 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -189,6 +189,8 @@ PRIVATE api/api_common.h api/api_confirm_phone.cpp api/api_confirm_phone.h + api/api_credits.cpp + api/api_credits.h api/api_earn.cpp api/api_earn.h api/api_editing.cpp @@ -368,6 +370,8 @@ PRIVATE boxes/ringtones_box.h boxes/self_destruction_box.cpp boxes/self_destruction_box.h + boxes/send_credits_box.cpp + boxes/send_credits_box.h boxes/send_files_box.cpp boxes/send_files_box.h boxes/sessions_box.cpp @@ -510,6 +514,8 @@ PRIVATE core/launcher.h core/local_url_handlers.cpp core/local_url_handlers.h + core/phone_click_handler.cpp + core/phone_click_handler.h core/sandbox.cpp core/sandbox.h core/shortcuts.cpp @@ -531,6 +537,8 @@ PRIVATE data/business/data_business_info.h data/business/data_shortcut_messages.cpp data/business/data_shortcut_messages.h + data/components/factchecks.cpp + data/components/factchecks.h data/components/recent_peers.cpp data/components/recent_peers.h data/components/scheduled_messages.cpp @@ -608,6 +616,8 @@ PRIVATE data/data_groups.h data/data_histories.cpp data/data_histories.h + data/data_history_messages.cpp + data/data_history_messages.h data/data_lastseen_status.h data/data_location.cpp data/data_location.h @@ -850,6 +860,8 @@ PRIVATE history/view/history_view_about_view.h history/view/history_view_bottom_info.cpp history/view/history_view_bottom_info.h + history/view/history_view_chat_preview.cpp + history/view/history_view_chat_preview.h history/view/history_view_contact_status.cpp history/view/history_view_contact_status.h history/view/history_view_context_menu.cpp @@ -864,6 +876,8 @@ PRIVATE history/view/history_view_emoji_interactions.h history/view/history_view_empty_list_bubble.cpp history/view/history_view_empty_list_bubble.h + history/view/history_view_fake_items.cpp + history/view/history_view_fake_items.h history/view/history_view_group_call_bar.cpp history/view/history_view_group_call_bar.h history/view/history_view_item_preview.h @@ -894,14 +908,14 @@ PRIVATE history/view/history_view_send_action.h history/view/history_view_service_message.cpp history/view/history_view_service_message.h - history/view/history_view_spoiler_click_handler.cpp - history/view/history_view_spoiler_click_handler.h history/view/history_view_sponsored_click_handler.cpp 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_text_helper.cpp + history/view/history_view_text_helper.h history/view/history_view_transcribe_button.cpp history/view/history_view_transcribe_button.h history/view/history_view_translate_bar.cpp @@ -1278,6 +1292,8 @@ PRIVATE payments/payments_checkout_process.h payments/payments_form.cpp payments/payments_form.h + payments/payments_non_panel_process.cpp + payments/payments_non_panel_process.h platform/linux/file_utilities_linux.cpp platform/linux/file_utilities_linux.h platform/linux/launcher_linux.cpp @@ -1423,6 +1439,10 @@ PRIVATE settings/settings_codes.h settings/settings_common_session.cpp settings/settings_common_session.h + settings/settings_credits.cpp + settings/settings_credits.h + settings/settings_credits_graphics.cpp + settings/settings_credits_graphics.h settings/settings_experimental.cpp settings/settings_experimental.h settings/settings_folders.cpp @@ -1518,6 +1538,8 @@ PRIVATE ui/controls/silent_toggle.h ui/controls/userpic_button.cpp ui/controls/userpic_button.h + ui/effects/credits_graphics.cpp + ui/effects/credits_graphics.h ui/effects/emoji_fly_animation.cpp ui/effects/emoji_fly_animation.h ui/effects/message_sending_animation_common.h @@ -1565,6 +1587,8 @@ PRIVATE window/section_widget.h window/window_adaptive.cpp window/window_adaptive.h + window/window_chat_preview.cpp + window/window_chat_preview.h window/window_connecting_widget.cpp window/window_connecting_widget.h window/window_controller.cpp diff --git a/Telegram/Resources/icons/menu/factcheck.png b/Telegram/Resources/icons/menu/factcheck.png new file mode 100644 index 000000000..e9115a4cf Binary files /dev/null and b/Telegram/Resources/icons/menu/factcheck.png differ diff --git a/Telegram/Resources/icons/menu/factcheck@2x.png b/Telegram/Resources/icons/menu/factcheck@2x.png new file mode 100644 index 000000000..bcc80e45e Binary files /dev/null and b/Telegram/Resources/icons/menu/factcheck@2x.png differ diff --git a/Telegram/Resources/icons/menu/factcheck@3x.png b/Telegram/Resources/icons/menu/factcheck@3x.png new file mode 100644 index 000000000..139e30eb8 Binary files /dev/null and b/Telegram/Resources/icons/menu/factcheck@3x.png differ diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 34e5cfaed..bd153b6e7 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -251,6 +251,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_caption_limit2#other" = "Make the caption shorter or subscribe to **Telegram Premium** to double the limit to **{count}** characters."; "lng_caption_limit_reached#one" = "You've reached the media caption limit. Please make the caption shorter by {count} character."; "lng_caption_limit_reached#other" = "You've reached the media caption limit. Please make the caption shorter by {count} characters."; +"lng_caption_move_up" = "Move Caption Up"; +"lng_caption_move_down" = "Move Caption Down"; "lng_file_size_limit_title" = "File Too Large"; "lng_file_size_limit#one" = "{count} Gb"; @@ -302,6 +304,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_sure_ban_admin" = "This user is an admin. Are you sure you want to go ahead and restrict them?"; "lng_sure_enable_socks" = "Are you sure you want to enable this proxy?\n\nServer: {server}\nPort: {port}\n\nYou can change your proxy server later in the Settings (Connection Type)."; "lng_sure_enable" = "Enable"; +"lng_proxy_box_title" = "Enable proxy"; +"lng_proxy_box_server" = "Server"; +"lng_proxy_box_port" = "Port"; +"lng_proxy_box_secret" = "Secret"; +"lng_proxy_box_status" = "Status"; +"lng_proxy_box_username" = "Username"; +"lng_proxy_box_password" = "Password"; "lng_proxy_invalid" = "The proxy link is invalid."; "lng_proxy_unsupported" = "Your Telegram Desktop version doesn't support this proxy type or the proxy link is invalid. Please update Telegram Desktop to the latest version."; @@ -561,6 +570,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_reaction_invoice" = "{reaction} to your invoice"; "lng_reaction_gif" = "{reaction} to your GIF"; +"lng_effect_add_title" = "Add an animated effect"; +"lng_effect_stickers_title" = "Effects from stickers"; +"lng_effect_send" = "Send with Effect"; +"lng_effect_none" = "No effects found."; +"lng_effect_premium" = "Subscribe to {link} to add this animated effect."; +"lng_effect_premium_link" = "Telegram Premium"; + "lng_languages" = "Languages"; "lng_languages_none" = "No languages found."; "lng_languages_count#one" = "{count} language"; @@ -769,6 +785,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_settings_power_chat" = "Animations in Chats"; "lng_settings_power_chat_background" = "Background rotation"; "lng_settings_power_chat_spoiler" = "Animated spoiler effect"; +"lng_settings_power_chat_effects" = "Effects in messages"; "lng_settings_power_calls" = "Animations in Calls"; "lng_settings_power_ui" = "Interface animations"; "lng_settings_power_auto" = "Save Power on Low Battery"; @@ -1053,6 +1070,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_proxy_sponsor" = "Proxy sponsor"; "lng_proxy_sponsor_about" = "This channel is shown by your proxy server.\nTo remove this channel from your chats list,\ndisable the proxy in Telegram Settings."; "lng_proxy_sponsor_warning" = "This proxy may display a sponsored channel in your chat list. This doesn't reveal any of your Telegram traffic."; +"lng_proxy_add_from_clipboard" = "Add proxy from clipboard"; +"lng_proxy_add_from_clipboard_good_toast" = "Proxy was added from clipboard."; +"lng_proxy_add_from_clipboard_failed_toast" = "This is not a proxy link."; +"lng_proxy_add_from_clipboard_existing_toast" = "This proxy is already in the list."; "lng_badge_psa_default" = "PSA"; "lng_about_psa_default" = "This message provides you with a public service announcement. To remove it from your chats list, right click it and select **Hide**."; "lng_tooltip_psa_default" = "This message provides you with a public service announcement."; @@ -1522,8 +1543,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_manage_peer_link_invite" = "Invite link"; "lng_manage_peer_link_expired" = "Expired link"; "lng_manage_private_group_title" = "Private"; +"lng_manage_private_group_noforwards_title" = "Private restricted"; "lng_manage_public_group_title" = "Public"; "lng_manage_private_peer_title" = "Private"; +"lng_manage_private_peer_noforwards_title" = "Private restricted"; "lng_manage_public_peer_title" = "Public"; "lng_manage_peer_send_title" = "Who can send new messages?"; "lng_manage_peer_send_only_members" = "Only members"; @@ -2285,6 +2308,38 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_business_about_sponsored_link" = "Telegram Ad Platform {emoji}"; "lng_business_about_sponsored_url" = "https://ads.telegram.org"; +"lng_credits_summary_title" = "Telegram Stars"; +"lng_credits_summary_about" = "Buy Stars to unlock content and services in miniapps on Telegram."; +"lng_credits_summary_options_subtitle" = "Choose package"; +"lng_credits_summary_options_credits#one" = "{count} Star"; +"lng_credits_summary_options_credits#other" = "{count} Stars"; +"lng_credits_summary_options_more" = "More Options"; +"lng_credits_summary_options_about" = "By proceeding and purchasing Stars, you agree with the {link}."; +"lng_credits_summary_options_about_link" = "Terms and Conditions"; +"lng_credits_summary_history_tab_full" = "All Transactions"; +"lng_credits_summary_history_tab_in" = "Incoming"; +"lng_credits_summary_history_tab_out" = "Outgoing"; +"lng_credits_summary_history_entry_inner_in" = "In-App Purchase"; +"lng_credits_summary_balance" = "Balance"; +"lng_credits_box_out_title" = "Confirm Your Purchase"; +"lng_credits_box_out_sure#one" = "Do you want to buy **\"{text}\"** in **{bot}** for **{count} Star**?"; +"lng_credits_box_out_sure#other" = "Do you want to buy **\"{text}\"** in **{bot}** for **{count} Stars**?"; +"lng_credits_box_out_confirm#one" = "Confirm and Pay {emoji} {count} Star"; +"lng_credits_box_out_confirm#other" = "Confirm and Pay {emoji} {count} Stars"; +"lng_credits_box_out_about" = "Review the {link} for Stars."; +"lng_credits_summary_in_toast_title" = "Stars Acquired"; +"lng_credits_summary_in_toast_about#one" = "**{count}** Star added to your balance."; +"lng_credits_summary_in_toast_about#other" = "**{count}** Stars added to your balance."; +"lng_credits_box_history_entry_peer" = "Recipient"; +"lng_credits_box_history_entry_id" = "Transaction ID"; +"lng_credits_box_history_entry_id_copied" = "Transaction ID copied to clipboard."; +"lng_credits_box_history_entry_about" = "You can dispute this transaction {link}."; +"lng_credits_box_history_entry_about_link" = "here"; +"lng_credits_small_balance_title#one" = "{count} Star Needed"; +"lng_credits_small_balance_title#other" = "{count} Stars Needed"; +"lng_credits_small_balance_about" = "Buy **Stars** and use them on **{bot}** and other miniapps."; +"lng_credits_purchase_blocked" = "Sorry, you can't purchase this item with Telegram Stars."; + "lng_location_title" = "Location"; "lng_location_about" = "Display the location of your business on your account."; "lng_location_address" = "Enter Address"; @@ -3177,6 +3232,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_context_reply_msg" = "Reply"; "lng_context_quote_and_reply" = "Quote & Reply"; "lng_context_edit_msg" = "Edit"; +"lng_context_add_factcheck" = "Add Fact Check"; +"lng_context_edit_factcheck" = "Edit Fact Check"; "lng_context_forward_msg" = "Forward Message"; "lng_context_send_now_msg" = "Send now"; "lng_context_reschedule" = "Reschedule"; @@ -3248,6 +3305,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_context_spoiler_effect" = "Hide with Spoiler"; "lng_context_disable_spoiler" = "Remove Spoiler"; +"lng_factcheck_title" = "Fact Check"; +"lng_factcheck_placeholder" = "Add Facts or Context"; +"lng_factcheck_whats_this" = "what's this?"; +"lng_factcheck_about" = "This clarification was provided by a fact checking agency assigned by the department of the government of your country ({country}) responsible for combatting misinformation."; +"lng_factcheck_add_done" = "Fact check added."; +"lng_factcheck_edit_done" = "Fact check edited."; +"lng_factcheck_remove_done" = "Fact check removed."; +"lng_factcheck_bottom" = "This clarification was provided by a fact checking agency assigned by the department of the government of your country ({country}) responsible for combatting misinformation."; +"lng_factcheck_links" = "Only **t.me/** links are allowed."; + "lng_translate_show_original" = "Show Original"; "lng_translate_bar_to" = "Translate to {name}"; "lng_translate_bar_to_other" = "Translate to {name}"; @@ -3368,6 +3435,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_add_contact" = "Create"; "lng_add_contact_button" = "New contact"; "lng_contacts_header" = "Contacts"; +"lng_menu_not_contact" = "This number is not on Telegram"; "lng_contacts_hidden_stories" = "Hidden Stories"; "lng_contacts_stories_status#one" = "{count} story"; "lng_contacts_stories_status#other" = "{count} stories"; @@ -5161,6 +5229,32 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_font_system" = "System font"; "lng_font_not_found" = "Font not found."; +"lng_search_tab_my_messages" = "My Messages"; +"lng_search_tab_this_topic" = "This Topic"; +"lng_search_tab_this_chat" = "This Chat"; +"lng_search_tab_this_channel" = "This Channel"; +"lng_search_tab_this_group" = "This Group"; +"lng_search_tab_public_posts" = "Public Posts"; +"lng_search_tab_no_results" = "No Results"; +"lng_search_tab_no_results_text" = "There were no results for \"{query}\"."; +"lng_search_tab_no_results_retry" = "Try another hashtag."; +"lng_search_tab_by_hashtag" = "Enter a hashtag to find messages containing it."; + +"lng_contact_details_button" = "View Contact"; +"lng_contact_details_title" = "Contact details"; +"lng_contact_details_phone" = "Phone"; +"lng_contact_details_phone_main" = "Main Phone"; +"lng_contact_details_phone_home" = "Home Phone"; +"lng_contact_details_phone_mobile" = "Mobile Phone"; +"lng_contact_details_phone_work" = "Work Phone"; +"lng_contact_details_phone_other" = "Other Phone"; +"lng_contact_details_email" = "Email"; +"lng_contact_details_address" = "Address"; +"lng_contact_details_url" = "URL"; +"lng_contact_details_note" = "Note"; +"lng_contact_details_birthday" = "Birthday"; +"lng_contact_details_organization" = "Organization"; + // Wnd specific "lng_wnd_choose_program_menu" = "Choose Default Program..."; diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml index e22af137a..8068a8279 100644 --- a/Telegram/Resources/uwp/AppX/AppxManifest.xml +++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml @@ -10,7 +10,7 @@ + Version="5.1.2.0" /> Telegram Desktop Telegram Messenger LLP diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc index 22f106414..e37585ba0 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 5,0,2,0 - PRODUCTVERSION 5,0,2,0 + FILEVERSION 5,1,2,0 + PRODUCTVERSION 5,1,2,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -62,10 +62,10 @@ BEGIN BEGIN VALUE "CompanyName", "Radolyn Labs" VALUE "FileDescription", "AyuGram Desktop" - VALUE "FileVersion", "5.0.2.0" + VALUE "FileVersion", "5.1.2.0" VALUE "LegalCopyright", "Copyright (C) 2014-2024" VALUE "ProductName", "AyuGram Desktop" - VALUE "ProductVersion", "5.0.2.0" + VALUE "ProductVersion", "5.1.2.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc index 402c438bf..dccfed972 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 5,0,2,0 - PRODUCTVERSION 5,0,2,0 + FILEVERSION 5,1,2,0 + PRODUCTVERSION 5,1,2,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -53,10 +53,10 @@ BEGIN BEGIN VALUE "CompanyName", "Radolyn Labs" VALUE "FileDescription", "AyuGram Desktop Updater" - VALUE "FileVersion", "5.0.2.0" + VALUE "FileVersion", "5.1.2.0" VALUE "LegalCopyright", "Copyright (C) 2014-2024" VALUE "ProductName", "AyuGram Desktop" - VALUE "ProductVersion", "5.0.2.0" + VALUE "ProductVersion", "5.1.2.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/SourceFiles/api/api_bot.cpp b/Telegram/SourceFiles/api/api_bot.cpp index 0284ae719..cf4611563 100644 --- a/Telegram/SourceFiles/api/api_bot.cpp +++ b/Telegram/SourceFiles/api/api_bot.cpp @@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item_components.h" #include "inline_bots/bot_attach_web_view.h" #include "payments/payments_checkout_process.h" +#include "payments/payments_non_panel_process.h" #include "main/main_session.h" #include "mainwidget.h" #include "mainwindow.h" @@ -335,7 +336,8 @@ void ActivateBotCommand(ClickHandlerContext context, int row, int column) { Payments::Mode::Payment, crl::guard(controller, [=](auto) { controller->widget()->activate(); - })); + }), + Payments::ProcessNonPanelPaymentFormFactory(controller, item)); } break; case ButtonType::Url: { diff --git a/Telegram/SourceFiles/api/api_common.h b/Telegram/SourceFiles/api/api_common.h index fe0d489b0..efd92a9bc 100644 --- a/Telegram/SourceFiles/api/api_common.h +++ b/Telegram/SourceFiles/api/api_common.h @@ -23,8 +23,10 @@ struct SendOptions { PeerData *sendAs = nullptr; TimeId scheduled = 0; BusinessShortcutId shortcutId = 0; + EffectId effectId = 0; bool silent = false; bool handleSupportSwitch = false; + bool invertCaption = false; bool hideViaBot = false; crl::time ttlSeconds = 0; }; diff --git a/Telegram/SourceFiles/api/api_confirm_phone.cpp b/Telegram/SourceFiles/api/api_confirm_phone.cpp index f4788e128..3a42a793d 100644 --- a/Telegram/SourceFiles/api/api_confirm_phone.cpp +++ b/Telegram/SourceFiles/api/api_confirm_phone.cpp @@ -97,8 +97,10 @@ void ConfirmPhone::resolve( box->resendRequests( ) | rpl::start_with_next([=] { _api.request(MTPauth_ResendCode( + MTP_flags(0), MTP_string(phone), - MTP_string(phoneHash) + MTP_string(phoneHash), + MTPstring() // reason )).done([=] { if (boxWeak) { boxWeak->callDone(); diff --git a/Telegram/SourceFiles/api/api_credits.cpp b/Telegram/SourceFiles/api/api_credits.cpp new file mode 100644 index 000000000..00f9e789f --- /dev/null +++ b/Telegram/SourceFiles/api/api_credits.cpp @@ -0,0 +1,222 @@ +/* +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 "api/api_credits.h" + +#include "apiwrap.h" +#include "api/api_updates.h" +#include "base/unixtime.h" +#include "data/data_peer.h" +#include "data/data_photo.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "main/main_app_config.h" +#include "main/main_session.h" + +namespace Api { +namespace { + +[[nodiscard]] Data::CreditsHistoryEntry HistoryFromTL( + const MTPStarsTransaction &tl, + not_null peer) { + using HistoryPeerTL = MTPDstarsTransactionPeer; + const auto photo = tl.data().vphoto() + ? peer->owner().photoFromWeb(*tl.data().vphoto(), ImageLocation()) + : nullptr; + return Data::CreditsHistoryEntry{ + .id = qs(tl.data().vid()), + .title = qs(tl.data().vtitle().value_or_empty()), + .description = qs(tl.data().vdescription().value_or_empty()), + .date = base::unixtime::parse(tl.data().vdate().v), + .photoId = photo ? photo->id : 0, + .credits = tl.data().vstars().v, + .bareId = tl.data().vpeer().match([](const HistoryPeerTL &p) { + return peerFromMTP(p.vpeer()); + }, [](const auto &) { + return PeerId(0); + }).value, + .peerType = tl.data().vpeer().match([](const HistoryPeerTL &) { + return Data::CreditsHistoryEntry::PeerType::Peer; + }, [](const MTPDstarsTransactionPeerPlayMarket &) { + return Data::CreditsHistoryEntry::PeerType::PlayMarket; + }, [](const MTPDstarsTransactionPeerFragment &) { + return Data::CreditsHistoryEntry::PeerType::Fragment; + }, [](const MTPDstarsTransactionPeerAppStore &) { + return Data::CreditsHistoryEntry::PeerType::AppStore; + }, [](const MTPDstarsTransactionPeerUnsupported &) { + return Data::CreditsHistoryEntry::PeerType::Unsupported; + }, [](const MTPDstarsTransactionPeerPremiumBot &) { + return Data::CreditsHistoryEntry::PeerType::PremiumBot; + }), + .refunded = tl.data().is_refund(), + }; +} + +[[nodiscard]] Data::CreditsStatusSlice StatusFromTL( + const MTPpayments_StarsStatus &status, + not_null peer) { + peer->owner().processUsers(status.data().vusers()); + peer->owner().processChats(status.data().vchats()); + return Data::CreditsStatusSlice{ + .list = ranges::views::all( + status.data().vhistory().v + ) | ranges::views::transform([&](const MTPStarsTransaction &tl) { + return HistoryFromTL(tl, peer); + }) | ranges::to_vector, + .balance = status.data().vbalance().v, + .allLoaded = !status.data().vnext_offset().has_value(), + .token = qs(status.data().vnext_offset().value_or_empty()), + }; +} + +} // namespace + +CreditsTopupOptions::CreditsTopupOptions(not_null peer) +: _peer(peer) +, _api(&peer->session().api().instance()) { +} + +rpl::producer CreditsTopupOptions::request() { + return [=](auto consumer) { + auto lifetime = rpl::lifetime(); + + using TLOption = MTPStarsTopupOption; + _api.request(MTPpayments_GetStarsTopupOptions( + )).done([=](const MTPVector &result) { + _options = ranges::views::all( + result.v + ) | ranges::views::transform([](const TLOption &option) { + return Data::CreditTopupOption{ + .credits = option.data().vstars().v, + .product = qs( + option.data().vstore_product().value_or_empty()), + .currency = qs(option.data().vcurrency()), + .amount = option.data().vamount().v, + .extended = option.data().is_extended(), + }; + }) | ranges::to_vector; + consumer.put_done(); + }).fail([=](const MTP::Error &error) { + consumer.put_error_copy(error.type()); + }).send(); + + return lifetime; + }; +} + +CreditsStatus::CreditsStatus(not_null peer) +: _peer(peer) +, _api(&peer->session().api().instance()) { +} + +void CreditsStatus::request( + const Data::CreditsStatusSlice::OffsetToken &token, + Fn done) { + if (_requestId) { + return; + } + + using TLResult = MTPpayments_StarsStatus; + + _requestId = _api.request(MTPpayments_GetStarsStatus( + _peer->isSelf() ? MTP_inputPeerSelf() : _peer->input + )).done([=](const TLResult &result) { + _requestId = 0; + done(StatusFromTL(result, _peer)); + }).fail([=] { + _requestId = 0; + done({}); + }).send(); +} + +CreditsHistory::CreditsHistory(not_null peer, bool in, bool out) +: _peer(peer) +, _flags((in == out) + ? HistoryTL::Flags(0) + : HistoryTL::Flags(0) + | (in ? HistoryTL::Flag::f_inbound : HistoryTL::Flags(0)) + | (out ? HistoryTL::Flag::f_outbound : HistoryTL::Flags(0))) +, _api(&peer->session().api().instance()) { +} + +void CreditsHistory::request( + const Data::CreditsStatusSlice::OffsetToken &token, + Fn done) { + if (_requestId) { + return; + } + _requestId = _api.request(MTPpayments_GetStarsTransactions( + MTP_flags(_flags), + _peer->isSelf() ? MTP_inputPeerSelf() : _peer->input, + MTP_string(token) + )).done([=](const MTPpayments_StarsStatus &result) { + _requestId = 0; + done(StatusFromTL(result, _peer)); + }).fail([=] { + _requestId = 0; + done({}); + }).send(); +} + +Data::CreditTopupOptions CreditsTopupOptions::options() const { + return _options; +} + +rpl::producer> PremiumPeerBot( + not_null session) { + const auto username = session->appConfig().get( + u"premium_bot_username"_q, + QString()); + if (username.isEmpty()) { + return rpl::never>(); + } + if (const auto p = session->data().peerByUsername(username)) { + return rpl::single>(p); + } + return [=](auto consumer) { + auto lifetime = rpl::lifetime(); + + const auto api = lifetime.make_state(&session->mtp()); + + api->request(MTPcontacts_ResolveUsername( + MTP_string(username) + )).done([=](const MTPcontacts_ResolvedPeer &result) { + session->data().processUsers(result.data().vusers()); + session->data().processChats(result.data().vchats()); + const auto botPeer = session->data().peerLoaded( + peerFromMTP(result.data().vpeer())); + if (!botPeer) { + return consumer.put_done(); + } + consumer.put_next(not_null{ botPeer }); + }).send(); + + return lifetime; + }; +} + +void CreditsRefund( + not_null peer, + const QString &entryId, + Fn done, + Fn fail) { + const auto user = peer->asUser(); + if (!user) { + return; + } + peer->session().api().request(MTPpayments_RefundStarsCharge( + user->inputUser, + MTP_string(entryId) + )).done([=](const MTPUpdates &result) { + peer->session().api().updates().applyUpdates(result); + done(); + }).fail([=](const MTP::Error &error) { + fail(error.type()); + }).send(); +} + +} // namespace Api diff --git a/Telegram/SourceFiles/api/api_credits.h b/Telegram/SourceFiles/api/api_credits.h new file mode 100644 index 000000000..bd064a65b --- /dev/null +++ b/Telegram/SourceFiles/api/api_credits.h @@ -0,0 +1,80 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "data/data_credits.h" +#include "mtproto/sender.h" + +namespace Main { +class Session; +} // namespace Main + +namespace Api { + +class CreditsTopupOptions final { +public: + CreditsTopupOptions(not_null peer); + + [[nodiscard]] rpl::producer request(); + [[nodiscard]] Data::CreditTopupOptions options() const; + +private: + const not_null _peer; + + Data::CreditTopupOptions _options; + + MTP::Sender _api; + +}; + +class CreditsStatus final { +public: + CreditsStatus(not_null peer); + + void request( + const Data::CreditsStatusSlice::OffsetToken &token, + Fn done); + +private: + const not_null _peer; + + mtpRequestId _requestId = 0; + + MTP::Sender _api; + +}; + +class CreditsHistory final { +public: + CreditsHistory(not_null peer, bool in, bool out); + + void request( + const Data::CreditsStatusSlice::OffsetToken &token, + Fn done); + +private: + using HistoryTL = MTPpayments_GetStarsTransactions; + const not_null _peer; + const HistoryTL::Flags _flags; + + mtpRequestId _requestId = 0; + + MTP::Sender _api; + +}; + +void CreditsRefund( + not_null peer, + const QString &entryId, + Fn done, + Fn fail); + +[[nodiscard]] rpl::producer> PremiumPeerBot( + not_null session); + +} // namespace Api diff --git a/Telegram/SourceFiles/api/api_editing.cpp b/Telegram/SourceFiles/api/api_editing.cpp index 7dcd768cb..f326275fd 100644 --- a/Telegram/SourceFiles/api/api_editing.cpp +++ b/Telegram/SourceFiles/api/api_editing.cpp @@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_histories.h" #include "data/data_session.h" #include "data/data_web_page.h" +#include "history/view/controls/history_view_compose_media_edit_manager.h" #include "history/history.h" #include "lang/lang_keys.h" #include "main/main_session.h" @@ -81,7 +82,8 @@ mtpRequestId EditMessage( | ((!webpage.removed && !webpage.url.isEmpty()) ? MTPmessages_EditMessage::Flag::f_media : emptyFlag) - | ((!webpage.removed && !webpage.url.isEmpty() && webpage.invert) + | (((!webpage.removed && !webpage.url.isEmpty() && webpage.invert) + || options.invertCaption) ? MTPmessages_EditMessage::Flag::f_invert_media : emptyFlag) | (!sentEntities.v.isEmpty() @@ -203,6 +205,7 @@ void RescheduleMessage( not_null item, SendOptions options) { const auto empty = [] {}; + options.invertCaption = item->invertMedia(); EditMessage(item, options, empty, empty); } @@ -254,85 +257,85 @@ mtpRequestId EditTextMessage( SendOptions options, Fn done, Fn fail, - std::optional spoilerMediaOverride) { - if (spoilerMediaOverride) { - const auto spoiler = *spoilerMediaOverride; - if (const auto media = item->media()) { - auto takeInputMedia = Fn()>(nullptr); - auto takeFileReference = Fn(nullptr); - if (const auto photo = media->photo()) { - using Flag = MTPDinputMediaPhoto::Flag; - const auto flags = Flag() - | (media->ttlSeconds() ? Flag::f_ttl_seconds : Flag()) - | (spoiler ? Flag::f_spoiler : Flag()); - takeInputMedia = [=] { - return MTP_inputMediaPhoto( - MTP_flags(flags), - photo->mtpInput(), - MTP_int(media->ttlSeconds())); - }; - takeFileReference = [=] { return photo->fileReference(); }; - } else if (const auto document = media->document()) { - using Flag = MTPDinputMediaDocument::Flag; - const auto flags = Flag() - | (media->ttlSeconds() ? Flag::f_ttl_seconds : Flag()) - | (spoiler ? Flag::f_spoiler : Flag()); - takeInputMedia = [=] { - return MTP_inputMediaDocument( - MTP_flags(flags), - document->mtpInput(), - MTP_int(media->ttlSeconds()), - MTPstring()); // query - }; - takeFileReference = [=] { return document->fileReference(); }; - } - - const auto usedFileReference = takeFileReference - ? takeFileReference() - : QByteArray(); - const auto origin = item->fullId(); - const auto api = &item->history()->session().api(); - const auto performRequest = [=]( - const auto &repeatRequest, - mtpRequestId originalRequestId) -> mtpRequestId { - const auto handleReference = [=]( - const QString &error, - mtpRequestId requestId) { - if (error.startsWith(u"FILE_REFERENCE_"_q)) { - api->refreshFileReference(origin, [=](const auto &) { - if (takeFileReference && - (takeFileReference() != usedFileReference)) { - repeatRequest( - repeatRequest, - originalRequestId - ? originalRequestId - : requestId); - } else { - fail(error, requestId); - } - }); - } else { - fail(error, requestId); - } - }; - const auto callback = [=]( - Fn applyUpdates, - mtpRequestId requestId) { - applyUpdates(); - done(originalRequestId ? originalRequestId : requestId); - }; - const auto requestId = EditMessage( - item, - caption, - webpage, - options, - callback, - handleReference, - takeInputMedia ? takeInputMedia() : std::nullopt); - return originalRequestId ? originalRequestId : requestId; + bool spoilered) { + const auto media = item->media(); + if (media + && HistoryView::MediaEditManager::CanBeSpoilered(item) + && spoilered != media->hasSpoiler()) { + auto takeInputMedia = Fn()>(nullptr); + auto takeFileReference = Fn(nullptr); + if (const auto photo = media->photo()) { + using Flag = MTPDinputMediaPhoto::Flag; + const auto flags = Flag() + | (media->ttlSeconds() ? Flag::f_ttl_seconds : Flag()) + | (spoilered ? Flag::f_spoiler : Flag()); + takeInputMedia = [=] { + return MTP_inputMediaPhoto( + MTP_flags(flags), + photo->mtpInput(), + MTP_int(media->ttlSeconds())); }; - return performRequest(performRequest, 0); + takeFileReference = [=] { return photo->fileReference(); }; + } else if (const auto document = media->document()) { + using Flag = MTPDinputMediaDocument::Flag; + const auto flags = Flag() + | (media->ttlSeconds() ? Flag::f_ttl_seconds : Flag()) + | (spoilered ? Flag::f_spoiler : Flag()); + takeInputMedia = [=] { + return MTP_inputMediaDocument( + MTP_flags(flags), + document->mtpInput(), + MTP_int(media->ttlSeconds()), + MTPstring()); // query + }; + takeFileReference = [=] { return document->fileReference(); }; } + + const auto usedFileReference = takeFileReference + ? takeFileReference() + : QByteArray(); + const auto origin = item->fullId(); + const auto api = &item->history()->session().api(); + const auto performRequest = [=]( + const auto &repeatRequest, + mtpRequestId originalRequestId) -> mtpRequestId { + const auto handleReference = [=]( + const QString &error, + mtpRequestId requestId) { + if (error.startsWith(u"FILE_REFERENCE_"_q)) { + api->refreshFileReference(origin, [=](const auto &) { + if (takeFileReference && + (takeFileReference() != usedFileReference)) { + repeatRequest( + repeatRequest, + originalRequestId + ? originalRequestId + : requestId); + } else { + fail(error, requestId); + } + }); + } else { + fail(error, requestId); + } + }; + const auto callback = [=]( + Fn applyUpdates, + mtpRequestId requestId) { + applyUpdates(); + done(originalRequestId ? originalRequestId : requestId); + }; + const auto requestId = EditMessage( + item, + caption, + webpage, + options, + callback, + handleReference, + takeInputMedia ? takeInputMedia() : std::nullopt); + return originalRequestId ? originalRequestId : requestId; + }; + return performRequest(performRequest, 0); } const auto callback = [=](Fn applyUpdates, mtpRequestId id) { diff --git a/Telegram/SourceFiles/api/api_editing.h b/Telegram/SourceFiles/api/api_editing.h index c8d0f6c50..630e1cd8d 100644 --- a/Telegram/SourceFiles/api/api_editing.h +++ b/Telegram/SourceFiles/api/api_editing.h @@ -56,6 +56,6 @@ mtpRequestId EditTextMessage( SendOptions options, Fn done, Fn fail, - std::optional spoilerMediaOverride); + bool spoilered); } // namespace Api diff --git a/Telegram/SourceFiles/api/api_polls.cpp b/Telegram/SourceFiles/api/api_polls.cpp index ef6f054a0..a56f25d0e 100644 --- a/Telegram/SourceFiles/api/api_polls.cpp +++ b/Telegram/SourceFiles/api/api_polls.cpp @@ -73,6 +73,9 @@ void Polls::create( if (action.options.shortcutId) { sendFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut; } + if (action.options.effectId) { + sendFlags |= MTPmessages_SendMedia::Flag::f_effect; + } const auto sendAs = action.options.sendAs; if (sendAs) { sendFlags |= MTPmessages_SendMedia::Flag::f_send_as; @@ -94,7 +97,8 @@ void Polls::create( MTPVector(), MTP_int(action.options.scheduled), (sendAs ? sendAs->input : MTP_inputPeerEmpty()), - Data::ShortcutIdToMTP(_session, action.options.shortcutId) + Data::ShortcutIdToMTP(_session, action.options.shortcutId), + MTP_long(action.options.effectId) ), [=](const MTPUpdates &result, const MTP::Response &response) { if (clearCloudDraft) { history->finishSavingCloudDraft( diff --git a/Telegram/SourceFiles/api/api_sending.cpp b/Telegram/SourceFiles/api/api_sending.cpp index d91943fe0..ade419248 100644 --- a/Telegram/SourceFiles/api/api_sending.cpp +++ b/Telegram/SourceFiles/api/api_sending.cpp @@ -133,6 +133,13 @@ void SendExistingMedia( flags |= MessageFlag::ShortcutMessage; sendFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut; } + if (action.options.effectId) { + sendFlags |= MTPmessages_SendMedia::Flag::f_effect; + } + if (action.options.invertCaption) { + flags |= MessageFlag::InvertMedia; + sendFlags |= MTPmessages_SendMedia::Flag::f_invert_media; + } session->data().registerMessageRandomId(randomId, newId); @@ -144,6 +151,7 @@ void SendExistingMedia( .date = HistoryItem::NewMessageDate(action.options), .shortcutId = action.options.shortcutId, .postAuthor = messagePostAuthor, + .effectId = action.options.effectId, }, media, caption); const auto performRequest = [=](const auto &repeatRequest) -> void { @@ -165,7 +173,8 @@ void SendExistingMedia( sentEntities, MTP_int(action.options.scheduled), (sendAs ? sendAs->input : MTP_inputPeerEmpty()), - Data::ShortcutIdToMTP(session, action.options.shortcutId) + Data::ShortcutIdToMTP(session, action.options.shortcutId), + MTP_long(action.options.effectId) ), [=](const MTPUpdates &result, const MTP::Response &response) { }, [=](const MTP::Error &error, const MTP::Response &response) { if (error.code() == 400 @@ -306,6 +315,13 @@ bool SendDice(MessageToSend &message) { flags |= MessageFlag::ShortcutMessage; sendFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut; } + if (action.options.effectId) { + sendFlags |= MTPmessages_SendMedia::Flag::f_effect; + } + if (action.options.invertCaption) { + flags |= MessageFlag::InvertMedia; + sendFlags |= MTPmessages_SendMedia::Flag::f_invert_media; + } session->data().registerMessageRandomId(randomId, newId); @@ -317,6 +333,7 @@ bool SendDice(MessageToSend &message) { .date = HistoryItem::NewMessageDate(action.options), .shortcutId = action.options.shortcutId, .postAuthor = messagePostAuthor, + .effectId = action.options.effectId, }, TextWithEntities(), MTP_messageMediaDice( MTP_int(0), MTP_string(emoji))); @@ -335,7 +352,8 @@ bool SendDice(MessageToSend &message) { MTP_vector(), MTP_int(action.options.scheduled), (sendAs ? sendAs->input : MTP_inputPeerEmpty()), - Data::ShortcutIdToMTP(session, action.options.shortcutId) + Data::ShortcutIdToMTP(session, action.options.shortcutId), + MTP_long(action.options.effectId) ), [=](const MTPUpdates &result, const MTP::Response &response) { }, [=](const MTP::Error &error, const MTP::Response &response) { api->sendMessageFail(error, peer, randomId, newId); @@ -430,6 +448,9 @@ void SendConfirmedFile( flags |= MessageFlag::MediaIsUnread; } } + if (file->to.options.invertCaption) { + flags |= MessageFlag::InvertMedia; + } const auto messageFromId = file->to.options.sendAs ? file->to.options.sendAs->id @@ -493,6 +514,7 @@ void SendConfirmedFile( edition.ttl = 0; edition.mtpMedia = &media; edition.textWithEntities = caption; + edition.invertMedia = file->to.options.invertCaption; edition.useSameViews = true; edition.useSameForwards = true; edition.useSameMarkup = true; @@ -510,6 +532,7 @@ void SendConfirmedFile( .shortcutId = file->to.options.shortcutId, .postAuthor = messagePostAuthor, .groupedId = groupId, + .effectId = file->to.options.effectId, }, caption, media); } diff --git a/Telegram/SourceFiles/api/api_text_entities.cpp b/Telegram/SourceFiles/api/api_text_entities.cpp index 44a689ff4..067cc6c0c 100644 --- a/Telegram/SourceFiles/api/api_text_entities.cpp +++ b/Telegram/SourceFiles/api/api_text_entities.cpp @@ -178,7 +178,11 @@ EntitiesInText EntitiesFromMTP( }); } }, [&](const MTPDmessageEntityPhone &d) { - // Skipping phones. + result.push_back({ + EntityType::Phone, + d.voffset().v, + d.vlength().v, + }); }, [&](const MTPDmessageEntityCashtag &d) { result.push_back({ EntityType::Cashtag, @@ -217,6 +221,7 @@ EntitiesInText EntitiesFromMTP( EntityType::Blockquote, d.voffset().v, d.vlength().v, + d.is_collapsed() ? u"1"_q : QString(), }); }); } @@ -265,6 +270,9 @@ MTPVector EntitiesToMTP( case EntityType::Email: { v.push_back(MTP_messageEntityEmail(offset, length)); } break; + case EntityType::Phone: { + v.push_back(MTP_messageEntityPhone(offset, length)); + } break; case EntityType::Hashtag: { v.push_back(MTP_messageEntityHashtag(offset, length)); } break; @@ -311,7 +319,13 @@ MTPVector EntitiesToMTP( MTP_string(entity.data()))); } break; case EntityType::Blockquote: { - v.push_back(MTP_messageEntityBlockquote(offset, length)); + using Flag = MTPDmessageEntityBlockquote::Flag; + const auto collapsed = !entity.data().isEmpty(); + v.push_back( + MTP_messageEntityBlockquote( + MTP_flags(collapsed ? Flag::f_collapsed : Flag()), + offset, + length)); } break; case EntityType::Spoiler: { v.push_back(MTP_messageEntitySpoiler(offset, length)); diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index 8821bb245..4a9339dfe 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -1145,7 +1145,9 @@ void Updates::applyUpdatesNoPtsCheck(const MTPUpdates &updates) { MTPMessageReactions(), MTPVector(), MTP_int(d.vttl_period().value_or_empty()), - MTPint()), // quick_reply_shortcut_id + MTPint(), // quick_reply_shortcut_id + MTPlong(), // effect + MTPFactCheck()), MessageFlags(), NewMessageType::Unread); } break; @@ -1180,7 +1182,9 @@ void Updates::applyUpdatesNoPtsCheck(const MTPUpdates &updates) { MTPMessageReactions(), MTPVector(), MTP_int(d.vttl_period().value_or_empty()), - MTPint()), // quick_reply_shortcut_id + MTPint(), // quick_reply_shortcut_id + MTPlong(), // effect + MTPFactCheck()), MessageFlags(), NewMessageType::Unread); } break; @@ -2618,6 +2622,11 @@ void Updates::feedUpdate(const MTPUpdate &update) { _session->data().stories().apply(data.vstealth_mode()); } break; + case mtpc_updateStarsBalance: { + const auto &data = update.c_updateStarsBalance(); + _session->setCredits(data.vbalance().v); + } break; + } } diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index eba067c85..dc7aa1b06 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -50,6 +50,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_user.h" #include "data/data_chat_filters.h" #include "data/data_histories.h" +#include "data/data_history_messages.h" #include "core/core_cloud_password.h" #include "core/application.h" #include "base/unixtime.h" @@ -3117,6 +3118,46 @@ void ApiWrap::resolveJumpToHistoryDate( } } +void ApiWrap::requestHistory( + not_null history, + MsgId messageId, + SliceType slice) { + const auto peer = history->peer; + const auto key = HistoryRequest{ + peer, + messageId, + slice, + }; + if (_historyRequests.contains(key)) { + return; + } + + const auto prepared = Api::PrepareHistoryRequest(peer, messageId, slice); + auto &histories = history->owner().histories(); + const auto requestType = Data::Histories::RequestType::History; + histories.sendRequest(history, requestType, [=](Fn finish) { + return request( + std::move(prepared) + ).done([=](const Api::HistoryRequestResult &result) { + _historyRequests.remove(key); + auto parsed = Api::ParseHistoryResult( + peer, + messageId, + slice, + result); + history->messages().addSlice( + std::move(parsed.messageIds), + parsed.noSkipRange, + parsed.fullCount); + finish(); + }).fail([=] { + _historyRequests.remove(key); + finish(); + }).send(); + }); + _historyRequests.emplace(key); +} + void ApiWrap::requestSharedMedia( not_null peer, MsgId topicRootId, @@ -3388,6 +3429,9 @@ void ApiWrap::forwardMessages( .date = HistoryItem::NewMessageDate(action.options), .shortcutId = action.options.shortcutId, .postAuthor = messagePostAuthor, + + // forwarded messages don't have effects + //.effectId = action.options.effectId, }, item); _session->data().registerMessageRandomId(randomId, newId); if (!localIds) { @@ -3488,6 +3532,7 @@ void ApiWrap::sendSharedContact( .date = HistoryItem::NewMessageDate(action.options), .shortcutId = action.options.shortcutId, .postAuthor = messagePostAuthor, + .effectId = action.options.effectId, }, TextWithEntities(), MTP_messageMediaContact( MTP_string(phone), MTP_string(firstName), @@ -3790,7 +3835,8 @@ void ApiWrap::sendMessage(MessageToSend &&message) { const auto anonymousPost = peer->amAnonymous(); const auto silentPost = ShouldSendSilent(peer, action.options); FillMessagePostFlags(action, peer, flags); - if (exactWebPage && !ignoreWebPage && message.webPage.invert) { + if ((exactWebPage && !ignoreWebPage && message.webPage.invert) + || action.options.invertCaption) { flags |= MessageFlag::InvertMedia; sendFlags |= MTPmessages_SendMessage::Flag::f_invert_media; mediaFlags |= MTPmessages_SendMedia::Flag::f_invert_media; @@ -3836,6 +3882,10 @@ void ApiWrap::sendMessage(MessageToSend &&message) { sendFlags |= MTPmessages_SendMessage::Flag::f_quick_reply_shortcut; mediaFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut; } + if (action.options.effectId) { + sendFlags |= MTPmessages_SendMessage::Flag::f_effect; + mediaFlags |= MTPmessages_SendMedia::Flag::f_effect; + } lastMessage = history->addNewLocalMessage({ .id = newId.msg, .flags = flags, @@ -3844,6 +3894,7 @@ void ApiWrap::sendMessage(MessageToSend &&message) { .date = HistoryItem::NewMessageDate(action.options), .shortcutId = action.options.shortcutId, .postAuthor = messagePostAuthor, + .effectId = action.options.effectId, }, sending, media); const auto done = [=]( const MTPUpdates &result, @@ -3891,7 +3942,8 @@ void ApiWrap::sendMessage(MessageToSend &&message) { sentEntities, MTP_int(action.options.scheduled), (sendAs ? sendAs->input : MTP_inputPeerEmpty()), - mtpShortcut + mtpShortcut, + MTP_long(action.options.effectId) ), done, fail); } else { histories.sendPreparedMessage( @@ -3908,7 +3960,8 @@ void ApiWrap::sendMessage(MessageToSend &&message) { sentEntities, MTP_int(action.options.scheduled), (sendAs ? sendAs->input : MTP_inputPeerEmpty()), - mtpShortcut + mtpShortcut, + MTP_long(action.options.effectId) ), done, fail); } isFirst = false; @@ -4191,7 +4244,9 @@ void ApiWrap::sendMediaWithRandomId( | (!sentEntities.v.isEmpty() ? Flag::f_entities : Flag(0)) | (options.scheduled ? Flag::f_schedule_date : Flag(0)) | (options.sendAs ? Flag::f_send_as : Flag(0)) - | (options.shortcutId ? Flag::f_quick_reply_shortcut : Flag(0)); + | (options.shortcutId ? Flag::f_quick_reply_shortcut : Flag(0)) + | (options.effectId ? Flag::f_effect : Flag(0)) + | (options.invertCaption ? Flag::f_invert_media : Flag(0)); auto &histories = history->owner().histories(); const auto peer = history->peer; @@ -4211,7 +4266,8 @@ void ApiWrap::sendMediaWithRandomId( sentEntities, MTP_int(options.scheduled), (options.sendAs ? options.sendAs->input : MTP_inputPeerEmpty()), - Data::ShortcutIdToMTP(_session, options.shortcutId) + Data::ShortcutIdToMTP(_session, options.shortcutId), + MTP_long(options.effectId) ), [=](const MTPUpdates &result, const MTP::Response &response) { if (done) done(true); if (updateRecentStickers) { @@ -4309,7 +4365,9 @@ void ApiWrap::sendAlbumIfReady(not_null album) { | (sendAs ? Flag::f_send_as : Flag(0)) | (album->options.shortcutId ? Flag::f_quick_reply_shortcut - : Flag(0)); + : Flag(0)) + | (album->options.effectId ? Flag::f_effect : Flag(0)) + | (album->options.invertCaption ? Flag::f_invert_media : Flag(0)); auto &histories = history->owner().histories(); const auto peer = history->peer; histories.sendPreparedMessage( @@ -4323,7 +4381,8 @@ void ApiWrap::sendAlbumIfReady(not_null album) { MTP_vector(medias), MTP_int(album->options.scheduled), (sendAs ? sendAs->input : MTP_inputPeerEmpty()), - Data::ShortcutIdToMTP(_session, album->options.shortcutId) + Data::ShortcutIdToMTP(_session, album->options.shortcutId), + MTP_long(album->options.effectId) ), [=](const MTPUpdates &result, const MTP::Response &response) { _sendingAlbums.remove(groupId); diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h index b1c844702..9710ee1bd 100644 --- a/Telegram/SourceFiles/apiwrap.h +++ b/Telegram/SourceFiles/apiwrap.h @@ -274,6 +274,10 @@ public: Fn, MsgId)> callback); using SliceType = Data::LoadDirection; + void requestHistory( + not_null history, + MsgId messageId, + SliceType slice); void requestSharedMedia( not_null peer, MsgId topicRootId, @@ -511,7 +515,8 @@ private: not_null peer, bool justClear, bool revoke); - void applyAffectedMessages(const MTPmessages_AffectedMessages &result) const; + void applyAffectedMessages( + const MTPmessages_AffectedMessages &result) const; void deleteAllFromParticipantSend( not_null channel, @@ -645,6 +650,17 @@ private: }; base::flat_set _sharedMediaRequests; + struct HistoryRequest { + not_null peer; + MsgId aroundId = 0; + SliceType sliceType = {}; + + friend inline auto operator<=>( + const HistoryRequest&, + const HistoryRequest&) = default; + }; + base::flat_set _historyRequests; + std::unique_ptr _dialogsLoadState; TimeId _dialogsLoadTill = 0; rpl::variable _dialogsLoadMayBlockByDate = false; diff --git a/Telegram/SourceFiles/boxes/add_contact_box.cpp b/Telegram/SourceFiles/boxes/add_contact_box.cpp index 45d4b1e99..6b9a0852c 100644 --- a/Telegram/SourceFiles/boxes/add_contact_box.cpp +++ b/Telegram/SourceFiles/boxes/add_contact_box.cpp @@ -192,20 +192,27 @@ void ShowAddParticipantsError( && channel && !channel->isMegagroup() && channel->canAddAdmins()) { - const auto makeAdmin = [=] { + const auto makeAdmin = [=](Fn close) { const auto user = forbidden.users.front(); const auto weak = std::make_shared>(); - const auto close = [=](auto&&...) { - if (*weak) { - (*weak)->closeBox(); + const auto done = [=](auto&&...) { + if (const auto strong = weak->data()) { + strong->uiShow()->showToast( + tr::lng_box_done(tr::now)); + strong->closeBox(); + } + }; + const auto fail = [=] { + if (const auto strong = weak->data()) { + strong->closeBox(); } }; const auto saveCallback = SaveAdminCallback( show, channel, user, - close, - close); + done, + fail); auto box = Box( channel, user, @@ -214,6 +221,7 @@ void ShowAddParticipantsError( box->setSaveCallback(saveCallback); *weak = box.data(); show->showBox(std::move(box)); + close(); }; show->showBox( Ui::MakeConfirmBox({ diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style index 87fa1556f..363a64079 100644 --- a/Telegram/SourceFiles/boxes/boxes.style +++ b/Telegram/SourceFiles/boxes/boxes.style @@ -642,6 +642,10 @@ proxyDropdownUpPosition: point(-2px, 20px); proxyAboutPadding: margins(22px, 7px, 22px, 14px); proxyAboutSponsorPadding: margins(22px, 7px, 22px, 0px); +proxyApplyBoxLabel : FlatLabel(defaultFlatLabel) { + maxHeight: 30px; +} + markdownLinkFieldPadding: margins(22px, 0px, 22px, 10px); termsContent: FlatLabel(defaultFlatLabel) { diff --git a/Telegram/SourceFiles/boxes/connection_box.cpp b/Telegram/SourceFiles/boxes/connection_box.cpp index cf9d91f8c..9775ae058 100644 --- a/Telegram/SourceFiles/boxes/connection_box.cpp +++ b/Telegram/SourceFiles/boxes/connection_box.cpp @@ -7,32 +7,37 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "boxes/connection_box.h" -#include "ui/boxes/confirm_box.h" -#include "lang/lang_keys.h" -#include "storage/localstorage.h" -#include "base/qthelp_url.h" #include "base/call_delayed.h" +#include "base/qthelp_regex.h" +#include "base/qthelp_url.h" #include "core/application.h" #include "core/core_settings.h" +#include "core/local_url_handlers.h" +#include "lang/lang_keys.h" #include "main/main_account.h" #include "mtproto/facade.h" -#include "ui/widgets/checkbox.h" +#include "storage/localstorage.h" +#include "ui/basic_click_handlers.h" +#include "ui/boxes/confirm_box.h" +#include "ui/effects/animations.h" +#include "ui/effects/radial_animation.h" +#include "ui/painter.h" +#include "ui/text/text_options.h" +#include "ui/text/text_utilities.h" +#include "ui/toast/toast.h" #include "ui/widgets/buttons.h" +#include "ui/widgets/checkbox.h" +#include "ui/widgets/dropdown_menu.h" #include "ui/widgets/fields/input_field.h" #include "ui/widgets/fields/number_input.h" #include "ui/widgets/fields/password_input.h" #include "ui/widgets/labels.h" -#include "ui/widgets/dropdown_menu.h" +#include "ui/widgets/popup_menu.h" #include "ui/wrap/slide_wrap.h" #include "ui/wrap/vertical_layout.h" -#include "ui/toast/toast.h" -#include "ui/effects/animations.h" -#include "ui/effects/radial_animation.h" -#include "ui/text/text_options.h" -#include "ui/text/text_utilities.h" -#include "ui/basic_click_handlers.h" -#include "ui/painter.h" +#include "ui/vertical_list.h" #include "boxes/abstract_box.h" // Ui::show(). +#include "window/window_session_controller.h" #include "styles/style_layers.h" #include "styles/style_boxes.h" #include "styles/style_chat_helpers.h" @@ -48,6 +53,22 @@ constexpr auto kSaveSettingsDelayedTimeout = crl::time(1000); using ProxyData = MTP::ProxyData; +[[nodiscard]] ProxyData ProxyDataFromFields( + ProxyData::Type type, + const QMap &fields) { + auto proxy = ProxyData(); + proxy.type = type; + proxy.host = fields.value(u"server"_q); + proxy.port = fields.value(u"port"_q).toUInt(); + if (type == ProxyData::Type::Socks5) { + proxy.user = fields.value(u"user"_q); + proxy.password = fields.value(u"pass"_q); + } else if (type == ProxyData::Type::Mtproto) { + proxy.password = fields.value(u"secret"_q); + } + return proxy; +}; + class HostInput : public Ui::MaskedInputField { public: HostInput( @@ -203,6 +224,7 @@ protected: private: void setupContent(); + void setupTopButton(); void createNoRowsLabel(); void addNewProxy(); void applyView(View &&view); @@ -600,9 +622,80 @@ void ProxiesBox::prepare() { addButton(tr::lng_proxy_add(), [=] { addNewProxy(); }); addButton(tr::lng_close(), [=] { closeBox(); }); + setupTopButton(); setupContent(); } +void ProxiesBox::setupTopButton() { + const auto top = addTopButton(st::infoTopBarMenu); + const auto menu + = top->lifetime().make_state>(); + const auto callback = [=] { + const auto maybeUrl = QGuiApplication::clipboard()->text(); + const auto local = Core::TryConvertUrlToLocal(maybeUrl); + + const auto proxyString = u"proxy"_q; + const auto socksString = u"socks"_q; + const auto protocol = u"tg://"_q; + const auto command = base::StringViewMid( + local, + protocol.size(), + 8192); + + if (local.startsWith(protocol + proxyString) + || local.startsWith(protocol + socksString)) { + + using namespace qthelp; + const auto options = RegExOption::CaseInsensitive; + for (const auto &[expression, _] : Core::LocalUrlHandlers()) { + const auto midExpression = base::StringViewMid( + expression, + 1); + const auto isSocks = midExpression.startsWith( + socksString); + if (!midExpression.startsWith(proxyString) + && !isSocks) { + continue; + } + const auto match = regex_match( + expression, + command, + options); + if (!match) { + continue; + } + const auto type = isSocks + ? ProxyData::Type::Socks5 + : ProxyData::Type::Mtproto; + const auto fields = url_parse_params( + match->captured(1), + qthelp::UrlParamNameTransform::ToLower); + const auto proxy = ProxyDataFromFields(type, fields); + const auto contains = _controller->contains(proxy); + const auto toast = (contains + ? tr::lng_proxy_add_from_clipboard_existing_toast + : tr::lng_proxy_add_from_clipboard_good_toast)(tr::now); + uiShow()->showToast(toast); + if (!contains) { + _controller->addNewItem(proxy); + } + break; + } + } else { + uiShow()->showToast( + tr::lng_proxy_add_from_clipboard_failed_toast(tr::now)); + } + }; + top->setClickedCallback([=] { + *menu = base::make_unique_q(top, st::defaultPopupMenu); + (*menu)->addAction( + tr::lng_proxy_add_from_clipboard(tr::now), + callback); + (*menu)->popup(QCursor::pos()); + return true; + }); +} + void ProxiesBox::setupContent() { const auto inner = setInnerWidget(object_ptr(this)); @@ -1094,70 +1187,84 @@ ProxiesBoxController::ProxiesBoxController(not_null account) } void ProxiesBoxController::ShowApplyConfirmation( + Window::SessionController *controller, Type type, const QMap &fields) { - const auto server = fields.value(u"server"_q); - const auto port = fields.value(u"port"_q).toUInt(); - auto proxy = ProxyData(); - proxy.type = type; - proxy.host = server; - proxy.port = port; - if (type == Type::Socks5) { - proxy.user = fields.value(u"user"_q); - proxy.password = fields.value(u"pass"_q); - } else if (type == Type::Mtproto) { - proxy.password = fields.value(u"secret"_q); + const auto proxy = ProxyDataFromFields(type, fields); + if (!proxy) { + auto box = Ui::MakeInformBox( + (proxy.status() == ProxyData::Status::Unsupported + ? tr::lng_proxy_unsupported(tr::now) + : tr::lng_proxy_invalid(tr::now))); + if (controller) { + controller->uiShow()->showBox(std::move(box)); + } else { + Ui::show(std::move(box)); + } + return; } - 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) - ? displayed - : parsed.isValid() - ? QString::fromUtf8(parsed.toEncoded()) - : UrlClickHandler::ShowEncoded(displayed); - const auto displayServer = QString( - displayUrl - ).replace( - UrlStartRegExp, - QString() - ).replace(UrlEndRegExp, QString()); - const auto text = tr::lng_sure_enable_socks( - tr::now, - lt_server, - displayServer, - lt_port, - QString::number(port)) - + (proxy.type == Type::Mtproto - ? "\n\n" + tr::lng_proxy_sponsor_warning(tr::now) - : QString()); - auto callback = [=](Fn &&close) { + static const auto UrlStartRegExp = QRegularExpression( + "^https://", + QRegularExpression::CaseInsensitiveOption); + static const auto UrlEndRegExp = QRegularExpression("/$"); + const auto displayed = "https://" + proxy.host + "/"; + const auto parsed = QUrl::fromUserInput(displayed); + const auto displayUrl = !UrlClickHandler::IsSuspicious(displayed) + ? displayed + : parsed.isValid() + ? QString::fromUtf8(parsed.toEncoded()) + : UrlClickHandler::ShowEncoded(displayed); + const auto displayServer = QString( + displayUrl + ).replace( + UrlStartRegExp, + QString() + ).replace(UrlEndRegExp, QString()); + const auto box = [=](not_null box) { + box->setTitle(tr::lng_proxy_box_title()); + if (type == Type::Mtproto) { + box->addRow(object_ptr( + box, + tr::lng_proxy_sponsor_warning(), + st::boxDividerLabel)); + Ui::AddSkip(box->verticalLayout()); + Ui::AddSkip(box->verticalLayout()); + } + const auto &stL = st::proxyApplyBoxLabel; + const auto &stSubL = st::boxDividerLabel; + const auto add = [&](const QString &s, tr::phrase<> phrase) { + if (!s.isEmpty()) { + box->addRow(object_ptr(box, s, stL)); + box->addRow(object_ptr(box, phrase(), stSubL)); + Ui::AddSkip(box->verticalLayout()); + Ui::AddSkip(box->verticalLayout()); + } + }; + if (!displayServer.isEmpty()) { + add(displayServer, tr::lng_proxy_box_server); + } + add(QString::number(proxy.port), tr::lng_proxy_box_port); + if (type == Type::Socks5) { + add(proxy.user, tr::lng_proxy_box_username); + add(proxy.password, tr::lng_proxy_box_password); + } else if (type == Type::Mtproto) { + add(proxy.password, tr::lng_proxy_box_secret); + } + box->addButton(tr::lng_sure_enable(), [=] { auto &proxies = Core::App().settings().proxy().list(); if (!ranges::contains(proxies, proxy)) { proxies.push_back(proxy); } - Core::App().setCurrentProxy( - proxy, - ProxyData::Settings::Enabled); + Core::App().setCurrentProxy(proxy, ProxyData::Settings::Enabled); Local::writeSettings(); - close(); - }; - Ui::show( - Ui::MakeConfirmBox({ - .text = text, - .confirmed = std::move(callback), - .confirmText = tr::lng_sure_enable(), - }), - Ui::LayerOption::KeepOther); + box->closeBox(); + }); + box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); + }; + if (controller) { + controller->uiShow()->showBox(Box(box)); } else { - Ui::show(Ui::MakeInformBox( - (proxy.status() == ProxyData::Status::Unsupported - ? tr::lng_proxy_unsupported(tr::now) - : tr::lng_proxy_invalid(tr::now)))); + Ui::show(Box(box)); } } @@ -1448,6 +1555,14 @@ object_ptr ProxiesBoxController::addNewItemBox() { }); } +bool ProxiesBoxController::contains(const ProxyData &proxy) const { + const auto j = ranges::find( + _list, + proxy, + [](const Item &item) { return item.data; }); + return (j != end(_list)); +} + void ProxiesBoxController::addNewItem(const ProxyData &proxy) { auto &proxies = _settings.list(); proxies.push_back(proxy); diff --git a/Telegram/SourceFiles/boxes/connection_box.h b/Telegram/SourceFiles/boxes/connection_box.h index 8c78ebf34..25da43458 100644 --- a/Telegram/SourceFiles/boxes/connection_box.h +++ b/Telegram/SourceFiles/boxes/connection_box.h @@ -30,6 +30,10 @@ namespace Main { class Account; } // namespace Main +namespace Window { +class SessionController; +} // namespace Window + class ProxiesBoxController { public: using ProxyData = MTP::ProxyData; @@ -38,6 +42,7 @@ public: explicit ProxiesBoxController(not_null account); static void ShowApplyConfirmation( + Window::SessionController *controller, Type type, const QMap &fields); @@ -77,6 +82,9 @@ public: void setTryIPv6(bool enabled); rpl::producer proxySettingsValue() const; + [[nodiscard]] bool contains(const ProxyData &proxy) const; + void addNewItem(const ProxyData &proxy); + rpl::producer views() const; ~ProxiesBoxController(); @@ -109,7 +117,6 @@ private: void replaceItemValue( std::vector::iterator which, const ProxyData &proxy); - void addNewItem(const ProxyData &proxy); const not_null _account; Core::SettingsProxy &_settings; diff --git a/Telegram/SourceFiles/boxes/create_poll_box.cpp b/Telegram/SourceFiles/boxes/create_poll_box.cpp index 1d02b5e52..46138e3fd 100644 --- a/Telegram/SourceFiles/boxes/create_poll_box.cpp +++ b/Telegram/SourceFiles/boxes/create_poll_box.cpp @@ -910,12 +910,12 @@ CreatePollBox::CreatePollBox( PollData::Flags chosen, PollData::Flags disabled, Api::SendType sendType, - SendMenu::Type sendMenuType) + SendMenu::Details sendMenuDetails) : _controller(controller) , _chosen(chosen) , _disabled(disabled) , _sendType(sendType) -, _sendMenuType(sendMenuType) { +, _sendMenuDetails([result = sendMenuDetails] { return result; }) { } rpl::producer CreatePollBox::submitRequests() const { @@ -1044,7 +1044,7 @@ not_null CreatePollBox::setupSolution( solution->setInstantReplaces(Ui::InstantReplaces::Default()); solution->setInstantReplacesEnabled( Core::App().settings().replaceEmojiValue()); - solution->setMarkdownReplacesEnabled(rpl::single(true)); + solution->setMarkdownReplacesEnabled(true); solution->setEditLinkCallback( DefaultEditLinkCallback(_controller->uiShow(), solution)); solution->customTab(true); @@ -1288,19 +1288,9 @@ object_ptr CreatePollBox::setupContent() { _submitRequests.fire({ collectResult(), sendOptions }); } }; - const auto sendSilent = [=] { - send({ .silent = true }); - }; - const auto sendScheduled = [=] { - _controller->show( - HistoryView::PrepareScheduleBox( - this, - SendMenu::Type::Scheduled, - send)); - }; - const auto sendWhenOnline = [=] { - send(Api::DefaultSendWhenOnlineOptions()); - }; + const auto sendAction = SendMenu::DefaultCallback( + _controller->uiShow(), + crl::guard(this, send)); options->scrollToWidget( ) | rpl::start_with_next([=](not_null widget) { @@ -1313,24 +1303,25 @@ object_ptr CreatePollBox::setupContent() { }, lifetime()); const auto isNormal = (_sendType == Api::SendType::Normal); - + const auto schedule = [=] { + sendAction( + { .type = SendMenu::ActionType::Schedule }, + _sendMenuDetails()); + }; const auto submit = addButton( - isNormal + (isNormal ? tr::lng_polls_create_button() - : tr::lng_schedule_button(), - [=] { isNormal ? send({}) : sendScheduled(); }); - const auto sendMenuType = [=] { + : tr::lng_schedule_button()), + [=] { isNormal ? send({}) : schedule(); }); + const auto sendMenuDetails = [=] { collectError(); - return (*error) - ? SendMenu::Type::Disabled - : _sendMenuType; + return (*error) ? SendMenu::Details() : _sendMenuDetails(); }; SendMenu::SetupMenuAndShortcuts( submit.data(), - sendMenuType, - sendSilent, - sendScheduled, - sendWhenOnline); + _controller->uiShow(), + sendMenuDetails, + sendAction); addButton(tr::lng_cancel(), [=] { closeBox(); }); return result; diff --git a/Telegram/SourceFiles/boxes/create_poll_box.h b/Telegram/SourceFiles/boxes/create_poll_box.h index 91805f0d8..91fc290ca 100644 --- a/Telegram/SourceFiles/boxes/create_poll_box.h +++ b/Telegram/SourceFiles/boxes/create_poll_box.h @@ -27,7 +27,7 @@ class SessionController; } // namespace Window namespace SendMenu { -enum class Type; +struct Details; } // namespace SendMenu class CreatePollBox : public Ui::BoxContent { @@ -43,7 +43,7 @@ public: PollData::Flags chosen, PollData::Flags disabled, Api::SendType sendType, - SendMenu::Type sendMenuType); + SendMenu::Details sendMenuDetails); [[nodiscard]] rpl::producer submitRequests() const; void submitFailed(const QString &error); @@ -75,7 +75,7 @@ private: const PollData::Flags _chosen = PollData::Flags(); const PollData::Flags _disabled = PollData::Flags(); const Api::SendType _sendType = Api::SendType(); - const SendMenu::Type _sendMenuType; + const Fn _sendMenuDetails; base::unique_qptr _emojiPanel; Fn _setInnerFocus; Fn()> _dataIsValidValue; diff --git a/Telegram/SourceFiles/boxes/edit_caption_box.cpp b/Telegram/SourceFiles/boxes/edit_caption_box.cpp index be4d4e7fa..d46121c2f 100644 --- a/Telegram/SourceFiles/boxes/edit_caption_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_caption_box.cpp @@ -38,6 +38,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "main/main_session_settings.h" #include "mainwidget.h" // controller->content() -> QWidget* +#include "menu/menu_send.h" #include "mtproto/mtproto_config.h" #include "platform/platform_specific.h" #include "storage/localimageloader.h" // SendMediaType @@ -175,7 +176,7 @@ void ChooseReplacement( void EditPhotoImage( not_null controller, std::shared_ptr media, - bool wasSpoiler, + bool spoilered, Fn done) { const auto large = media ? media->image(Data::PhotoSize::Large) @@ -198,7 +199,7 @@ void EditPhotoImage( using ImageInfo = Ui::PreparedFileInformation::Image; auto &file = list.files.front(); - file.spoiler = wasSpoiler; + file.spoiler = spoilered; const auto image = std::get_if(&file.information->media); image->modifications = mods; @@ -225,25 +226,18 @@ void EditPhotoImage( } // namespace -EditCaptionBox::EditCaptionBox( - QWidget*, - not_null controller, - not_null item) -: EditCaptionBox({}, controller, item, PrepareEditText(item), {}, {}) { -} - EditCaptionBox::EditCaptionBox( QWidget*, not_null controller, not_null item, TextWithTags &&text, + bool spoilered, + bool invertCaption, Ui::PreparedList &&list, Fn saved) : _controller(controller) , _historyItem(item) -, _isAllowedEditMedia(item->media() - ? item->media()->allowsEditMedia() - : false) +, _isAllowedEditMedia(item->media() && item->media()->allowsEditMedia()) , _albumType(ComputeAlbumType(item)) , _controls(base::make_unique_q(this)) , _scroll(base::make_unique_q(this, st::boxScroll)) @@ -261,6 +255,8 @@ EditCaptionBox::EditCaptionBox( Expects(item->media() != nullptr); Expects(item->media()->allowsEditCaption()); + _mediaEditManager.start(item, spoilered, invertCaption); + _controller->session().data().itemRemoved( _historyItem->fullId() ) | rpl::start_with_next([=] { @@ -274,6 +270,8 @@ void EditCaptionBox::StartMediaReplace( not_null controller, FullMsgId itemId, TextWithTags text, + bool spoilered, + bool invertCaption, Fn saved) { const auto session = &controller->session(); const auto item = session->data().message(itemId); @@ -285,6 +283,8 @@ void EditCaptionBox::StartMediaReplace( controller, item, std::move(text), + spoilered, + invertCaption, std::move(list), std::move(saved))); }; @@ -299,6 +299,8 @@ void EditCaptionBox::StartMediaReplace( FullMsgId itemId, Ui::PreparedList &&list, TextWithTags text, + bool spoilered, + bool invertCaption, Fn saved) { const auto session = &controller->session(); const auto item = session->data().message(itemId); @@ -332,6 +334,8 @@ void EditCaptionBox::StartMediaReplace( controller, item, std::move(text), + spoilered, + invertCaption, std::move(list), std::move(saved))); } @@ -342,14 +346,15 @@ void EditCaptionBox::StartPhotoEdit( std::shared_ptr media, FullMsgId itemId, TextWithTags text, + bool spoilered, + bool invertCaption, Fn saved) { const auto session = &controller->session(); const auto item = session->data().message(itemId); if (!item) { return; } - const auto hasSpoiler = item->media() && item->media()->hasSpoiler(); - EditPhotoImage(controller, media, hasSpoiler, [=]( + EditPhotoImage(controller, media, spoilered, [=]( Ui::PreparedList &&list) mutable { const auto item = session->data().message(itemId); if (!item) { @@ -359,15 +364,48 @@ void EditCaptionBox::StartPhotoEdit( controller, item, std::move(text), + spoilered, + invertCaption, std::move(list), std::move(saved))); }); } void EditCaptionBox::prepare() { - addButton(tr::lng_settings_save(), [=] { save(); }); + const auto button = addButton(tr::lng_settings_save(), [=] { save(); }); addButton(tr::lng_cancel(), [=] { closeBox(); }); + const auto details = crl::guard(this, [=] { + auto result = SendMenu::Details(); + const auto allWithSpoilers = ranges::all_of( + _preparedList.files, + &Ui::PreparedFile::spoiler); + result.spoiler = !_preparedList.hasSpoilerMenu(!_asFile) + ? SendMenu::SpoilerState::None + : allWithSpoilers + ? SendMenu::SpoilerState::Enabled + : SendMenu::SpoilerState::Possible; + const auto canMoveCaption = _preparedList.canMoveCaption( + false, + !_asFile + ) && _field && HasSendText(_field); + result.caption = !canMoveCaption + ? SendMenu::CaptionState::None + : _mediaEditManager.invertCaption() + ? SendMenu::CaptionState::Above + : SendMenu::CaptionState::Below; + return result; + }); + const auto callback = [=](SendMenu::Action action, const auto &) { + _mediaEditManager.apply(action); + rebuildPreview(); + }; + SendMenu::SetupMenuAndShortcuts( + button, + nullptr, + details, + crl::guard(this, callback)); + updateBoxSize(); setupField(); @@ -396,7 +434,6 @@ void EditCaptionBox::rebuildPreview() { applyChanges(); - _previewHasSpoiler = nullptr; if (_preparedList.files.empty()) { const auto media = _historyItem->media(); const auto photo = media->photo(); @@ -430,7 +467,13 @@ void EditCaptionBox::rebuildPreview() { _isPhoto = (media && media->isPhoto()); const auto withCheckbox = _isPhoto && CanBeCompressed(_albumType); if (media && (!withCheckbox || !_asFile)) { - _previewHasSpoiler = [media] { return media->hasSpoiler(); }; + media->spoileredChanges( + ) | rpl::start_with_next([=](bool spoilered) { + _mediaEditManager.apply({ .type = spoilered + ? SendMenu::ActionType::SpoilerOn + : SendMenu::ActionType::SpoilerOff + }); + }, media->lifetime()); _content.reset(media); } else { _content.reset(Ui::CreateChild( @@ -757,10 +800,7 @@ bool EditCaptionBox::setPreparedList(Ui::PreparedList &&list) { } bool EditCaptionBox::hasSpoiler() const { - return _preparedList.files.empty() - ? (_historyItem->media() - && _historyItem->media()->hasSpoiler()) - : _preparedList.files.front().spoiler; + return _mediaEditManager.spoilered(); } void EditCaptionBox::captionResized() { @@ -869,8 +909,8 @@ bool EditCaptionBox::validateLength(const QString &text) const { } void EditCaptionBox::applyChanges() { - if (!_preparedList.files.empty() && _previewHasSpoiler) { - _preparedList.files.front().spoiler = _previewHasSpoiler(); + if (!_preparedList.files.empty()) { + _preparedList.files.front().spoiler = _mediaEditManager.spoilered(); } } @@ -899,6 +939,7 @@ void EditCaptionBox::save() { auto options = Api::SendOptions(); options.scheduled = item->isScheduled() ? item->date() : 0; options.shortcutId = item->shortcutId(); + options.invertCaption = _mediaEditManager.invertCaption(); if (!_preparedList.files.empty()) { if ((_albumType != Ui::AlbumType::None) diff --git a/Telegram/SourceFiles/boxes/edit_caption_box.h b/Telegram/SourceFiles/boxes/edit_caption_box.h index 110c0e588..af72a250f 100644 --- a/Telegram/SourceFiles/boxes/edit_caption_box.h +++ b/Telegram/SourceFiles/boxes/edit_caption_box.h @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "history/view/controls/history_view_compose_media_edit_manager.h" #include "ui/layers/box_content.h" #include "ui/chat/attach/attach_prepare.h" @@ -32,15 +33,13 @@ enum class AlbumType; class EditCaptionBox final : public Ui::BoxContent { public: - EditCaptionBox( - QWidget*, - not_null controller, - not_null item); EditCaptionBox( QWidget*, not_null controller, not_null item, TextWithTags &&text, + bool spoilered, + bool invertCaption, Ui::PreparedList &&list, Fn saved); ~EditCaptionBox(); @@ -49,18 +48,24 @@ public: not_null controller, FullMsgId itemId, TextWithTags text, + bool spoilered, + bool invertCaption, Fn saved); static void StartMediaReplace( not_null controller, FullMsgId itemId, Ui::PreparedList &&list, TextWithTags text, + bool spoilered, + bool invertCaption, Fn saved); static void StartPhotoEdit( not_null controller, std::shared_ptr media, FullMsgId itemId, TextWithTags text, + bool spoilered, + bool invertCaption, Fn saved); protected: @@ -111,7 +116,6 @@ private: const base::unique_qptr _emojiToggle; base::unique_qptr _content; - Fn _previewHasSpoiler; base::unique_qptr _emojiPanel; base::unique_qptr _emojiFilter; @@ -122,6 +126,7 @@ private: std::shared_ptr _photoMedia; Ui::PreparedList _preparedList; + HistoryView::MediaEditManager _mediaEditManager; mtpRequestId _saveRequestId = 0; diff --git a/Telegram/SourceFiles/boxes/gift_premium_box.cpp b/Telegram/SourceFiles/boxes/gift_premium_box.cpp index 10ad9409d..ef87c6423 100644 --- a/Telegram/SourceFiles/boxes/gift_premium_box.cpp +++ b/Telegram/SourceFiles/boxes/gift_premium_box.cpp @@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_boosts.h" #include "data/data_changes.h" #include "data/data_channel.h" +#include "data/data_credits.h" #include "data/data_media_types.h" // Data::GiveawayStart. #include "data/data_peer_values.h" // Data::PeerPremiumValue. #include "data/data_session.h" @@ -1147,16 +1148,18 @@ void GiftCodeBox( object_ptr( box, st::giveawayGiftCodeCover, - nullptr, - rpl::conditional( - state->used.value(), - tr::lng_gift_link_used_title(), - tr::lng_gift_link_title()), - rpl::conditional( - state->used.value(), - tr::lng_gift_link_used_about(Ui::Text::RichLangValue), - tr::lng_gift_link_about(Ui::Text::RichLangValue)), - true)); + Ui::Premium::TopBarDescriptor{ + .clickContextOther = nullptr, + .title = rpl::conditional( + state->used.value(), + tr::lng_gift_link_used_title(), + tr::lng_gift_link_title()), + .about = rpl::conditional( + state->used.value(), + tr::lng_gift_link_used_about(Ui::Text::RichLangValue), + tr::lng_gift_link_about(Ui::Text::RichLangValue)), + .light = true, + })); const auto max = st::giveawayGiftCodeTopHeight; bar->setMaximumHeight(max); @@ -1283,13 +1286,15 @@ void GiftCodePendingBox( object_ptr( box, st, - clickContext, - tr::lng_gift_link_title(), - tr::lng_gift_link_pending_about( - lt_user, - rpl::single(Ui::Text::Link(resultToName)), - Ui::Text::RichLangValue), - true)); + Ui::Premium::TopBarDescriptor{ + .clickContextOther = clickContext, + .title = tr::lng_gift_link_title(), + .about = tr::lng_gift_link_pending_about( + lt_user, + rpl::single(Ui::Text::Link(resultToName)), + Ui::Text::RichLangValue), + .light = true, + })); const auto max = st::giveawayGiftCodeTopHeight; bar->setMaximumHeight(max); @@ -1629,3 +1634,50 @@ void ResolveGiveawayInfo( messageId, crl::guard(controller, show)); } + +void AddCreditsHistoryEntryTable( + not_null controller, + not_null container, + const Data::CreditsHistoryEntry &entry) { + auto table = container->add( + object_ptr( + container, + st::giveawayGiftCodeTable), + st::giveawayGiftCodeTableMargin); + if (entry.bareId) { + AddTableRow( + table, + tr::lng_credits_box_history_entry_peer(), + controller, + PeerId(entry.bareId)); + } + if (!entry.id.isEmpty()) { + constexpr auto kOneLineCount = 18; + const auto oneLine = entry.id.length() <= kOneLineCount; + auto label = object_ptr( + table, + rpl::single( + Ui::Text::Wrapped({ entry.id }, EntityType::Code, {})), + oneLine + ? st::giveawayGiftCodeValue + : st::giveawayGiftCodeValueMultiline); + label->setClickHandlerFilter([=](const auto &...) { + TextUtilities::SetClipboardText( + TextForMimeData::Simple(entry.id)); + controller->showToast( + tr::lng_credits_box_history_entry_id_copied(tr::now)); + return false; + }); + AddTableRow( + table, + tr::lng_credits_box_history_entry_id(), + std::move(label), + st::giveawayGiftCodeValueMargin); + } + if (!entry.date.isNull()) { + AddTableRow( + table, + tr::lng_gift_link_label_date(), + rpl::single(Ui::Text::WithEntities(langDateTime(entry.date)))); + } +} diff --git a/Telegram/SourceFiles/boxes/gift_premium_box.h b/Telegram/SourceFiles/boxes/gift_premium_box.h index b054bc3a2..e3194b273 100644 --- a/Telegram/SourceFiles/boxes/gift_premium_box.h +++ b/Telegram/SourceFiles/boxes/gift_premium_box.h @@ -16,12 +16,14 @@ struct GiftCode; } // namespace Api namespace Data { +struct CreditsHistoryEntry; struct GiveawayStart; struct GiveawayResults; } // namespace Data namespace Ui { class GenericBox; +class VerticalLayout; } // namespace Ui namespace Window { @@ -71,3 +73,8 @@ void ResolveGiveawayInfo( MsgId messageId, std::optional start, std::optional results); + +void AddCreditsHistoryEntryTable( + not_null controller, + not_null container, + const Data::CreditsHistoryEntry &entry); diff --git a/Telegram/SourceFiles/boxes/peer_list_box.cpp b/Telegram/SourceFiles/boxes/peer_list_box.cpp index dca4a065b..22cc40411 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_box.cpp @@ -36,6 +36,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_widgets.h" #include // XXH64. +#include [[nodiscard]] PeerListRowId UniqueRowIdFromString(const QString &d) { return XXH64(d.data(), d.size() * sizeof(ushort), 0); @@ -1552,15 +1553,44 @@ void PeerListContent::handleMouseMove(QPoint globalPosition) { && *_lastMousePosition == globalPosition) { return; } + if (_trackPressStart + && ((*_trackPressStart - globalPosition).manhattanLength() + > QApplication::startDragDistance())) { + _trackPressStart = {}; + _controller->rowTrackPressCancel(); + } + if (!_controller->rowTrackPressSkipMouseSelection()) { + selectByMouse(globalPosition); + } +} + +void PeerListContent::pressLeftToContextMenu(bool shown) { + if (shown) { + setContexted(_pressed); + setPressed(Selected()); + } else { + setContexted(Selected()); + } +} + +bool PeerListContent::trackRowPressFromGlobal(QPoint globalPosition) { selectByMouse(globalPosition); + if (const auto row = getRow(_selected.index)) { + if (_controller->rowTrackPress(row)) { + _trackPressStart = globalPosition; + return true; + } + } + return false; } void PeerListContent::mousePressEvent(QMouseEvent *e) { _pressButton = e->button(); selectByMouse(e->globalPos()); setPressed(_selected); - if (auto row = getRow(_selected.index)) { - auto updateCallback = [this, row, hint = _selected.index] { + _trackPressStart = {}; + if (const auto row = getRow(_selected.index)) { + const auto updateCallback = [this, row, hint = _selected.index] { updateRow(row, hint); }; if (_selected.element) { @@ -1586,8 +1616,11 @@ void PeerListContent::mousePressEvent(QMouseEvent *e) { row->addRipple(_st.item, maskGenerator, point, std::move(updateCallback)); } } + if (_pressButton == Qt::LeftButton && _controller->rowTrackPress(row)) { + _trackPressStart = e->globalPos(); + } } - if (anim::Disabled() && !_selected.element) { + if (anim::Disabled() && !_trackPressStart && !_selected.element) { mousePressReleased(e->button()); } } @@ -1597,6 +1630,9 @@ void PeerListContent::mouseReleaseEvent(QMouseEvent *e) { } void PeerListContent::mousePressReleased(Qt::MouseButton button) { + _trackPressStart = {}; + _controller->rowTrackPressCancel(); + updateRow(_pressed.index); updateRow(_selected.index); diff --git a/Telegram/SourceFiles/boxes/peer_list_box.h b/Telegram/SourceFiles/boxes/peer_list_box.h index 4349fd8c6..aa24856e2 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.h +++ b/Telegram/SourceFiles/boxes/peer_list_box.h @@ -348,6 +348,9 @@ public: virtual int peerListPartitionRows(Fn border) = 0; virtual std::shared_ptr peerListUiShow() = 0; + virtual void peerListPressLeftToContextMenu(bool shown) = 0; + virtual bool peerListTrackRowPressFromGlobal(QPoint globalPosition) = 0; + template void peerListAddSelectedPeers(PeerDataRange &&range) { for (const auto peer : range) { @@ -478,6 +481,15 @@ public: } } + virtual bool rowTrackPress(not_null row) { + return false; + } + virtual void rowTrackPressCancel() { + } + virtual bool rowTrackPressSkipMouseSelection() { + return false; + } + virtual void loadMoreRows() { } virtual void itemDeselectedHook(not_null peer) { @@ -655,6 +667,8 @@ public: void refreshRows(); void mouseLeftGeometry(); + void pressLeftToContextMenu(bool shown); + bool trackRowPressFromGlobal(QPoint globalPosition); void setSearchMode(PeerListSearchMode mode); void changeCheckState( @@ -829,6 +843,7 @@ private: bool _mouseSelection = false; std::optional _lastMousePosition; Qt::MouseButton _pressButton = Qt::LeftButton; + std::optional _trackPressStart; rpl::event_stream _scrollToRequests; @@ -992,6 +1007,13 @@ public: bool highlightRow, Fn)> destroyed = nullptr) override; + void peerListPressLeftToContextMenu(bool shown) override { + _content->pressLeftToContextMenu(shown); + } + bool peerListTrackRowPressFromGlobal(QPoint globalPosition) override { + return _content->trackRowPressFromGlobal(globalPosition); + } + protected: not_null content() const { return _content; diff --git a/Telegram/SourceFiles/boxes/peers/edit_forum_topic_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_forum_topic_box.cpp index 2a4882588..ed8a77787 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_forum_topic_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_forum_topic_box.cpp @@ -42,16 +42,14 @@ namespace { constexpr auto kDefaultIconId = DocumentId(0x7FFF'FFFF'FFFF'FFFFULL); -struct DefaultIcon { - QString title; - int32 colorId = 0; -}; +using DefaultIcon = Data::TopicIconDescriptor; class DefaultIconEmoji final : public Ui::Text::CustomEmoji { public: DefaultIconEmoji( rpl::producer value, - Fn repaint); + Fn repaint, + Data::CustomEmojiSizeTag tag); int width() override; QString entityData() override; @@ -64,14 +62,17 @@ public: private: DefaultIcon _icon = {}; QImage _image; + Data::CustomEmojiSizeTag _tag = {}; rpl::lifetime _lifetime; }; DefaultIconEmoji::DefaultIconEmoji( - rpl::producer value, - Fn repaint) { + rpl::producer value, + Fn repaint, + Data::CustomEmojiSizeTag tag) +: _tag(tag) { std::move(value) | rpl::start_with_next([=](DefaultIcon value) { _icon = value; _image = QImage(); @@ -88,15 +89,22 @@ QString DefaultIconEmoji::entityData() { } void DefaultIconEmoji::paint(QPainter &p, const Context &context) { + const auto &st = (_tag == Data::CustomEmojiSizeTag::Normal) + ? st::normalForumTopicIcon + : st::defaultForumTopicIcon; if (_image.isNull()) { - _image = Data::ForumTopicIconFrame( - _icon.colorId, - _icon.title, - st::defaultForumTopicIcon); + _image = Data::IsForumGeneralIconTitle(_icon.title) + ? Data::ForumTopicGeneralIconFrame( + st.size, + Data::ParseForumGeneralIconColor(_icon.colorId)) + : Data::ForumTopicIconFrame(_icon.colorId, _icon.title, st); } - const auto esize = Ui::Emoji::GetSizeLarge() / style::DevicePixelRatio(); + const auto full = (_tag == Data::CustomEmojiSizeTag::Normal) + ? Ui::Emoji::GetSizeNormal() + : Ui::Emoji::GetSizeLarge(); + const auto esize = full / style::DevicePixelRatio(); const auto customSize = Ui::Text::AdjustCustomEmojiSize(esize); - const auto skip = (customSize - st::defaultForumTopicIcon.size) / 2; + const auto skip = (customSize - st.size) / 2; p.drawImage(context.position + QPoint(skip, skip), _image); } @@ -212,7 +220,7 @@ bool DefaultIconEmoji::readyInDefaultState() { ) | rpl::start_with_next([=] { state->frame = Data::ForumTopicGeneralIconFrame( st::largeForumTopicIcon.size, - st::windowSubTextFg); + st::windowSubTextFg->c); result->update(); }, result->lifetime()); @@ -261,7 +269,8 @@ struct IconSelector { if (id == kDefaultIconId) { return std::make_unique( rpl::duplicate(defaultIcon), - repaint); + std::move(repaint), + tag); } return manager->create(id, std::move(repaint), tag); }; @@ -572,3 +581,13 @@ void EditForumTopicBox( box->closeBox(); }); } + +std::unique_ptr MakeTopicIconEmoji( + Data::TopicIconDescriptor descriptor, + Fn repaint, + Data::CustomEmojiSizeTag tag) { + return std::make_unique( + rpl::single(descriptor), + std::move(repaint), + tag); +} diff --git a/Telegram/SourceFiles/boxes/peers/edit_forum_topic_box.h b/Telegram/SourceFiles/boxes/peers/edit_forum_topic_box.h index fddc3c052..81758e84c 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_forum_topic_box.h +++ b/Telegram/SourceFiles/boxes/peers/edit_forum_topic_box.h @@ -11,6 +11,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL class History; +namespace Data { +struct TopicIconDescriptor; +enum class CustomEmojiSizeTag : uchar; +} // namespace Data + namespace Window { class SessionController; } // namespace Window @@ -25,3 +30,8 @@ void EditForumTopicBox( not_null controller, not_null forum, MsgId rootId); + +[[nodiscard]] std::unique_ptr MakeTopicIconEmoji( + Data::TopicIconDescriptor descriptor, + Fn repaint, + Data::CustomEmojiSizeTag tag); diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp index c79ef325d..99427aca0 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp @@ -415,7 +415,12 @@ private: std::deque> _saveStagesQueue; Saving _savingData; - const rpl::event_stream _privacyTypeUpdates; + struct PrivacyAndForwards { + Privacy privacy; + bool noForwards = false; + }; + + const rpl::event_stream _privacyTypeUpdates; const rpl::event_stream _linkedChatUpdates; mtpRequestId _linkedChatsRequestId = 0; @@ -761,7 +766,7 @@ void Controller::refreshHistoryVisibility() { void Controller::showEditPeerTypeBox( std::optional> error) { const auto boxCallback = crl::guard(this, [=](EditPeerTypeData data) { - _privacyTypeUpdates.fire_copy(data.privacy); + _privacyTypeUpdates.fire({ data.privacy, data.noForwards }); _typeDataSavedValue = data; refreshHistoryVisibility(); }); @@ -882,7 +887,8 @@ void Controller::fillPrivacyTypeButton() { ? tr::lng_manage_peer_group_type : tr::lng_manage_peer_channel_type)(), _privacyTypeUpdates.events( - ) | rpl::map([=](Privacy flag) { + ) | rpl::map([=](PrivacyAndForwards data) { + const auto flag = data.privacy; if (flag == Privacy::HasUsername) { _peer->session().api().usernames().requestToCache(_peer); } @@ -894,14 +900,21 @@ void Controller::fillPrivacyTypeButton() { : tr::lng_manage_public_peer_title)() : (hasLocation ? tr::lng_manage_peer_link_invite - : isGroup + : ((!data.noForwards) && isGroup) ? tr::lng_manage_private_group_title - : tr::lng_manage_private_peer_title)(); + : ((!data.noForwards) && !isGroup) + ? tr::lng_manage_private_peer_title + : isGroup + ? tr::lng_manage_private_group_noforwards_title + : tr::lng_manage_private_peer_noforwards_title)(); }) | rpl::flatten_latest(), [=] { showEditPeerTypeBox(); }, { &st::menuIconCustomize }); - _privacyTypeUpdates.fire_copy(_typeDataSavedValue->privacy); + _privacyTypeUpdates.fire_copy({ + _typeDataSavedValue->privacy, + _typeDataSavedValue->noForwards, + }); } void Controller::fillLinkedChatButton() { diff --git a/Telegram/SourceFiles/boxes/peers/peer_short_info_box.cpp b/Telegram/SourceFiles/boxes/peers/peer_short_info_box.cpp index 47afccd2f..45d30a7ed 100644 --- a/Telegram/SourceFiles/boxes/peers/peer_short_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/peer_short_info_box.cpp @@ -398,7 +398,7 @@ void PeerShortInfoCover::paintRadial(QPainter &p) { QImage PeerShortInfoCover::currentVideoFrame() const { const auto size = QSize(_st.size, _st.size); const auto request = Media::Streaming::FrameRequest{ - .resize = size * style::DevicePixelRatio(), + .resize = size, .outer = size, }; return (_videoInstance diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.cpp b/Telegram/SourceFiles/boxes/premium_preview_box.cpp index 30f312745..76a2a18b7 100644 --- a/Telegram/SourceFiles/boxes/premium_preview_box.cpp +++ b/Telegram/SourceFiles/boxes/premium_preview_box.cpp @@ -286,7 +286,7 @@ void PreloadSticker(const std::shared_ptr &media) { document, media->videoThumbnailContent(), QString(), - true); + Stickers::EffectType::PremiumSticker); const auto update = [=] { if (!state->readyInvoked diff --git a/Telegram/SourceFiles/boxes/reactions_settings_box.cpp b/Telegram/SourceFiles/boxes/reactions_settings_box.cpp index e33624403..8a71e395b 100644 --- a/Telegram/SourceFiles/boxes/reactions_settings_box.cpp +++ b/Telegram/SourceFiles/boxes/reactions_settings_box.cpp @@ -16,8 +16,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/admin_log/history_admin_log_item.h" #include "history/history.h" #include "history/history_item.h" -#include "history/view/history_view_element.h" #include "history/view/reactions/history_view_reactions_strip.h" +#include "history/view/history_view_element.h" +#include "history/view/history_view_fake_items.h" #include "lang/lang_keys.h" #include "boxes/premium_preview_box.h" #include "main/main_session.h" @@ -43,53 +44,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace { -PeerId GenerateUser(not_null history, const QString &name) { - Expects(history->peer->isUser()); - - const auto peerId = Data::FakePeerIdForJustName(name); - history->owner().processUser(MTP_user( - MTP_flags(MTPDuser::Flag::f_first_name | MTPDuser::Flag::f_min), - peerToBareMTPInt(peerId), - MTP_long(0), - MTP_string(tr::lng_settings_chat_message_reply_from(tr::now)), - MTPstring(), // last name - MTPstring(), // username - MTPstring(), // phone - MTPUserProfilePhoto(), // profile photo - MTPUserStatus(), // status - MTP_int(0), // bot info version - MTPVector(), // restrictions - MTPstring(), // bot placeholder - MTPstring(), // lang code - MTPEmojiStatus(), - MTPVector(), - MTPint(), // stories_max_id - MTPPeerColor(), // color - MTPPeerColor())); // profile_color - return peerId; -} - -AdminLog::OwnedItem GenerateItem( - not_null delegate, - not_null history, - PeerId from, - FullMsgId replyTo, - const QString &text) { - Expects(history->peer->isUser()); - - const auto item = history->addNewLocalMessage({ - .id = history->nextNonHistoryEntryId(), - .flags = (MessageFlag::FakeHistoryItem - | MessageFlag::HasFromId - | MessageFlag::HasReplyInfo), - .from = from, - .replyTo = FullReplyTo{ .messageId = replyTo }, - .date = base::unixtime::now(), - }, TextWithEntities{ .text = text }, MTP_messageMediaEmpty()); - - return AdminLog::OwnedItem(delegate, item); -} - void AddMessage( not_null container, not_null controller, @@ -135,15 +89,15 @@ void AddMessage( const auto history = controller->session().data().history( PeerData::kServiceNotificationsId); - state->reply = GenerateItem( + state->reply = HistoryView::GenerateItem( state->delegate.get(), history, - GenerateUser( + HistoryView::GenerateUser( history, tr::lng_settings_chat_message_reply_from(tr::now)), FullMsgId(), tr::lng_settings_chat_message_reply(tr::now)); - auto message = GenerateItem( + auto message = HistoryView::GenerateItem( state->delegate.get(), history, history->peer->id, diff --git a/Telegram/SourceFiles/boxes/send_credits_box.cpp b/Telegram/SourceFiles/boxes/send_credits_box.cpp new file mode 100644 index 000000000..8820c0c65 --- /dev/null +++ b/Telegram/SourceFiles/boxes/send_credits_box.cpp @@ -0,0 +1,250 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "boxes/send_credits_box.h" + +#include "api/api_credits.h" +#include "apiwrap.h" +#include "core/ui_integration.h" // Core::MarkedTextContext. +#include "data/data_credits.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "data/stickers/data_custom_emoji.h" +#include "history/history.h" +#include "history/history_item.h" +#include "info/channel_statistics/boosts/giveaway/boost_badge.h" // InfiniteRadialAnimationWidget. +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "payments/payments_checkout_process.h" +#include "payments/payments_form.h" +#include "settings/settings_credits_graphics.h" +#include "ui/controls/userpic_button.h" +#include "ui/effects/premium_graphics.h" +#include "ui/effects/premium_top_bar.h" // Ui::Premium::ColorizedSvg. +#include "ui/image/image_prepare.h" +#include "ui/layers/generic_box.h" +#include "ui/rect.h" +#include "ui/text/text_utilities.h" +#include "ui/vertical_list.h" +#include "ui/widgets/buttons.h" +#include "styles/style_boxes.h" +#include "styles/style_credits.h" +#include "styles/style_giveaway.h" +#include "styles/style_layers.h" +#include "styles/style_premium.h" +#include "styles/style_settings.h" + +namespace Ui { + +void SendCreditsBox( + not_null box, + std::shared_ptr form, + Fn sent) { + if (!form) { + return; + } + struct State { + rpl::variable confirmButtonBusy = false; + }; + const auto state = box->lifetime().make_state(); + box->setStyle(st::giveawayGiftCodeBox); + box->setNoContentMargin(true); + + const auto session = form->invoice.session; + + const auto photoSize = st::defaultUserpicButton.photoSize; + + const auto content = box->verticalLayout(); + Ui::AddSkip(content, photoSize / 2); + + { + const auto ministarsContainer = Ui::CreateChild(box); + const auto fullHeight = photoSize * 2; + using MiniStars = Ui::Premium::ColoredMiniStars; + const auto ministars = box->lifetime().make_state( + ministarsContainer, + false, + Ui::Premium::MiniStars::Type::BiStars); + ministars->setColorOverride(Ui::Premium::CreditsIconGradientStops()); + + ministarsContainer->paintRequest( + ) | rpl::start_with_next([=] { + auto p = QPainter(ministarsContainer); + ministars->paint(p); + }, ministarsContainer->lifetime()); + + box->widthValue( + ) | rpl::start_with_next([=](int width) { + ministarsContainer->resize(width, fullHeight); + const auto w = fullHeight / 3 * 2; + ministars->setCenter(QRect( + (width - w) / 2, + (fullHeight - w) / 2, + w, + w)); + }, ministarsContainer->lifetime()); + } + + const auto bot = session->data().user(form->botId); + + if (form->photo) { + box->addRow(object_ptr>( + content, + Settings::HistoryEntryPhoto(content, form->photo, photoSize))); + } else { + const auto widget = box->addRow( + object_ptr>( + content, + object_ptr( + content, + bot, + st::defaultUserpicButton))); + widget->setAttribute(Qt::WA_TransparentForMouseEvents); + } + + Ui::AddSkip(content); + box->addRow(object_ptr>( + box, + object_ptr( + box, + tr::lng_credits_box_out_title(), + st::settingsPremiumUserTitle))); + Ui::AddSkip(content); + box->addRow(object_ptr>( + box, + object_ptr( + box, + tr::lng_credits_box_out_sure( + lt_count, + rpl::single(form->invoice.amount) | tr::to_count(), + lt_text, + rpl::single(TextWithEntities{ form->title }), + lt_bot, + rpl::single(TextWithEntities{ bot->name() }), + Ui::Text::RichLangValue), + st::creditsBoxAbout))); + Ui::AddSkip(content); + Ui::AddSkip(content); + + const auto button = box->addButton(rpl::single(QString()), [=] { + if (state->confirmButtonBusy.current()) { + return; + } + state->confirmButtonBusy = true; + session->api().request( + MTPpayments_SendStarsForm( + MTP_flags(0), + MTP_long(form->formId), + form->inputInvoice) + ).done([=](auto result) { + state->confirmButtonBusy = false; + box->closeBox(); + sent(); + }).fail([=](const MTP::Error &error) { + state->confirmButtonBusy = false; + box->uiShow()->showToast(error.type()); + }).send(); + }); + { + using namespace Info::Statistics; + const auto loadingAnimation = InfiniteRadialAnimationWidget( + button, + st::giveawayGiftCodeStartButton.height / 2); + AddChildToWidgetCenter(button.data(), loadingAnimation); + loadingAnimation->showOn(state->confirmButtonBusy.value()); + } + { + const auto emojiMargin = QMargins( + 0, + -st::moderateBoxExpandInnerSkip, + 0, + 0); + const auto buttonEmoji = Ui::Text::SingleCustomEmoji( + session->data().customEmojiManager().registerInternalEmoji( + st::settingsPremiumIconStar, + emojiMargin, + true)); + auto buttonText = tr::lng_credits_box_out_confirm( + lt_count, + rpl::single(form->invoice.amount) | tr::to_count(), + lt_emoji, + rpl::single(buttonEmoji), + Ui::Text::RichLangValue); + const auto buttonLabel = Ui::CreateChild( + button, + rpl::single(QString()), + st::defaultFlatLabel); + std::move( + buttonText + ) | rpl::start_with_next([=](const TextWithEntities &text) { + buttonLabel->setMarkedText( + text, + Core::MarkedTextContext{ + .session = session, + .customEmojiRepaint = [=] { buttonLabel->update(); }, + }); + }, buttonLabel->lifetime()); + buttonLabel->setTextColorOverride( + box->getDelegate()->style().button.textFg->c); + button->sizeValue( + ) | rpl::start_with_next([=](const QSize &size) { + buttonLabel->moveToLeft( + (size.width() - buttonLabel->width()) / 2, + (size.height() - buttonLabel->height()) / 2); + }, buttonLabel->lifetime()); + buttonLabel->setAttribute(Qt::WA_TransparentForMouseEvents); + state->confirmButtonBusy.value( + ) | rpl::start_with_next([=](bool busy) { + buttonLabel->setVisible(!busy); + }, buttonLabel->lifetime()); + } + + const auto buttonWidth = st::boxWidth + - rect::m::sum::h(st::giveawayGiftCodeBox.buttonPadding); + button->widthValue() | rpl::filter([=] { + return (button->widthNoMargins() != buttonWidth); + }) | rpl::start_with_next([=] { + button->resizeToWidth(buttonWidth); + }, button->lifetime()); + + { + const auto close = Ui::CreateChild( + box.get(), + st::boxTitleClose); + close->setClickedCallback([=] { + box->closeBox(); + }); + box->widthValue( + ) | rpl::start_with_next([=](int width) { + close->moveToRight(0, 0); + close->raise(); + }, close->lifetime()); + } + + { + const auto balance = Settings::AddBalanceWidget( + content, + session->creditsValue(), + false); + const auto api = balance->lifetime().make_state( + session->user()); + api->request({}, [=](Data::CreditsStatusSlice slice) { + session->setCredits(slice.balance); + }); + rpl::combine( + balance->sizeValue(), + content->sizeValue() + ) | rpl::start_with_next([=](const QSize &, const QSize &) { + balance->moveToLeft( + st::creditsHistoryRightSkip * 2, + st::creditsHistoryRightSkip); + balance->update(); + }, balance->lifetime()); + } +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/boxes/send_credits_box.h b/Telegram/SourceFiles/boxes/send_credits_box.h new file mode 100644 index 000000000..25ceb1d56 --- /dev/null +++ b/Telegram/SourceFiles/boxes/send_credits_box.h @@ -0,0 +1,25 @@ +/* +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 + +class HistoryItem; + +namespace Payments { +struct CreditsFormData; +} // namespace Payments + +namespace Ui { + +class GenericBox; + +void SendCreditsBox( + not_null box, + std::shared_ptr data, + Fn sent); + +} // namespace Ui diff --git a/Telegram/SourceFiles/boxes/send_files_box.cpp b/Telegram/SourceFiles/boxes/send_files_box.cpp index 199b65941..75345a44c 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.cpp +++ b/Telegram/SourceFiles/boxes/send_files_box.cpp @@ -336,7 +336,7 @@ SendFilesBox::SendFilesBox( const TextWithTags &caption, not_null toPeer, Api::SendType sendType, - SendMenu::Type sendMenuType) + SendMenu::Details sendMenuDetails) : SendFilesBox(nullptr, { .show = controller->uiShow(), .list = std::move(list), @@ -345,7 +345,7 @@ SendFilesBox::SendFilesBox( .limits = DefaultLimitsForPeer(toPeer), .check = DefaultCheckForPeer(controller, toPeer), .sendType = sendType, - .sendMenuType = sendMenuType, + .sendMenuDetails = [=] { return sendMenuDetails; }, }) { } @@ -358,7 +358,8 @@ SendFilesBox::SendFilesBox(QWidget*, SendFilesBoxDescriptor &&descriptor) , _titleHeight(st::boxTitleHeight) , _list(std::move(descriptor.list)) , _limits(descriptor.limits) -, _sendMenuType(descriptor.sendMenuType) +, _sendMenuDetails(prepareSendMenuDetails(descriptor)) +, _sendMenuCallback(prepareSendMenuCallback()) , _captionToPeer(descriptor.captionToPeer) , _check(std::move(descriptor.check)) , _confirmedCallback(std::move(descriptor.confirmed)) @@ -372,6 +373,50 @@ SendFilesBox::SendFilesBox(QWidget*, SendFilesBoxDescriptor &&descriptor) enqueueNextPrepare(); } +Fn SendFilesBox::prepareSendMenuDetails( + const SendFilesBoxDescriptor &descriptor) { + auto initial = descriptor.sendMenuDetails; + return crl::guard(this, [=] { + auto result = initial ? initial() : SendMenu::Details(); + result.spoiler = !hasSpoilerMenu() + ? SendMenu::SpoilerState::None + : allWithSpoilers() + ? SendMenu::SpoilerState::Enabled + : SendMenu::SpoilerState::Possible; + const auto way = _sendWay.current(); + const auto canMoveCaption = _list.canMoveCaption( + way.groupFiles() && way.sendImagesAsPhotos(), + way.sendImagesAsPhotos() + ) && _caption && HasSendText(_caption); + result.caption = !canMoveCaption + ? SendMenu::CaptionState::None + : _invertCaption + ? SendMenu::CaptionState::Above + : SendMenu::CaptionState::Below; + return result; + }); +} + +auto SendFilesBox::prepareSendMenuCallback() +-> Fn { + return crl::guard(this, [=](MenuAction action, MenuDetails details) { + using Type = SendMenu::ActionType; + switch (action.type) { + case Type::CaptionDown: _invertCaption = false; break; + case Type::CaptionUp: _invertCaption = true; break; + case Type::SpoilerOn: toggleSpoilers(true); break; + case Type::SpoilerOff: toggleSpoilers(false); break; + default: + SendMenu::DefaultCallback( + _show, + sendCallback())( + action, + details); + break; + } + }); +} + void SendFilesBox::initPreview() { using namespace rpl::mappers; @@ -537,10 +582,9 @@ void SendFilesBox::refreshButtons() { if (_sendType == Api::SendType::Normal) { SendMenu::SetupMenuAndShortcuts( _send, - [=] { return _sendMenuType; }, - [=] { sendSilent(); }, - [=] { sendScheduled(); }, - [=] { sendWhenOnline(); }); + _show, + _sendMenuDetails, + _sendMenuCallback); } addButton(tr::lng_cancel(), [=] { closeBox(); }); _addFile = addLeftButton( @@ -552,21 +596,14 @@ void SendFilesBox::refreshButtons() { addMenuButton(); } -bool SendFilesBox::hasSendMenu() const { - return (_sendMenuType != SendMenu::Type::Disabled); +bool SendFilesBox::hasSendMenu(const SendMenu::Details &details) const { + return (details.type != SendMenu::Type::Disabled) + || (details.spoiler != SendMenu::SpoilerState::None) + || (details.caption != SendMenu::CaptionState::None); } bool SendFilesBox::hasSpoilerMenu() const { - const auto allAreVideo = !ranges::any_of(_list.files, [](const auto &f) { - using Type = Ui::PreparedFile::Type; - return (f.type != Type::Video); - }); - const auto allAreMedia = !ranges::any_of(_list.files, [](const auto &f) { - using Type = Ui::PreparedFile::Type; - return (f.type != Type::Photo) && (f.type != Type::Video); - }); - return allAreVideo - || (allAreMedia && _sendWay.current().sendImagesAsPhotos()); + return _list.hasSpoilerMenu(_sendWay.current().sendImagesAsPhotos()); } void SendFilesBox::applyBlockChanges() { @@ -590,36 +627,23 @@ void SendFilesBox::toggleSpoilers(bool enabled) { } void SendFilesBox::addMenuButton() { - if (!hasSendMenu() && !hasSpoilerMenu()) { + const auto details = _sendMenuDetails(); + if (!hasSendMenu(details)) { return; } const auto top = addTopButton(_st.files.menu); top->setClickedCallback([=] { const auto &tabbed = _st.tabbed; - const auto &icons = tabbed.icons; _menu = base::make_unique_q(top, tabbed.menu); - if (hasSpoilerMenu()) { - const auto spoilered = allWithSpoilers(); - _menu->addAction( - (spoilered - ? tr::lng_context_disable_spoiler(tr::now) - : tr::lng_context_spoiler_effect(tr::now)), - [=] { toggleSpoilers(!spoilered); }, - spoilered ? &icons.menuSpoilerOff : &icons.menuSpoiler); - if (hasSendMenu()) { - _menu->addSeparator(&tabbed.expandedSeparator); - } - } - if (hasSendMenu()) { - SendMenu::FillSendMenu( - _menu.get(), - _sendMenuType, - [=] { sendSilent(); }, - [=] { sendScheduled(); }, - [=] { sendWhenOnline(); }, - &_st.tabbed.icons); - } + const auto position = QCursor::pos(); + SendMenu::FillSendMenu( + _menu.get(), + _show, + _sendMenuDetails(), + _sendMenuCallback, + &_st.tabbed.icons, + position); using ImageInfo = Ui::PreparedFileInformation::Image; if (_list.files.size() == 1 && std::get_if(&_list.files[0].information->media)) { @@ -650,10 +674,9 @@ void SendFilesBox::addMenuButton() { }, &st::menuIconStickers); } - _menu->popup(QCursor::pos()); + _menu->popup(position); return true; }); - } void SendFilesBox::initSendWay() { @@ -695,9 +718,7 @@ void SendFilesBox::initSendWay() { for (auto &block : _blocks) { block.setSendWay(value); } - if (!hasSendMenu()) { - refreshButtons(); - } + refreshButtons(); if (was != hidden()) { updateBoxSize(); updateControlsGeometry(); @@ -909,9 +930,7 @@ void SendFilesBox::pushBlock(int from, int till) { } void SendFilesBox::refreshControls(bool initial) { - if (initial || !hasSendMenu()) { - refreshButtons(); - } + refreshButtons(); refreshTitleText(); updateSendWayControls(); updateCaptionPlaceholder(); @@ -1477,7 +1496,12 @@ void SendFilesBox::send( if ((_sendType == Api::SendType::Scheduled || _sendType == Api::SendType::ScheduledToUser) && !options.scheduled) { - return sendScheduled(); + auto child = _sendMenuDetails(); + child.spoiler = SendMenu::SpoilerState::None; + child.caption = SendMenu::CaptionState::None; + return SendMenu::DefaultCallback(_show, sendCallback())( + { .type = SendMenu::ActionType::Schedule }, + child); } if (_preparing) { _whenReadySend = [=] { @@ -1502,6 +1526,7 @@ void SendFilesBox::send( auto caption = (_caption && !_caption->isHidden()) ? _caption->getTextWithAppliedMarkdown() : TextWithTags(); + options.invertCaption = _invertCaption; if (!validateLength(caption.text)) { return; } @@ -1515,25 +1540,10 @@ void SendFilesBox::send( closeBox(); } -void SendFilesBox::sendSilent() { - send({ .silent = true }); -} - -void SendFilesBox::sendScheduled() { - const auto type = (_sendType == Api::SendType::ScheduledToUser) - ? SendMenu::Type::ScheduledToUser - : _sendMenuType; - const auto callback = [=](Api::SendOptions options) { send(options); }; - auto box = HistoryView::PrepareScheduleBox(this, type, callback); - const auto weak = Ui::MakeWeak(box.data()); - _show->showBox(std::move(box)); - if (const auto strong = weak.data()) { - strong->setCloseByOutsideClick(false); - } -} - -void SendFilesBox::sendWhenOnline() { - send(Api::DefaultSendWhenOnlineOptions()); +Fn SendFilesBox::sendCallback() { + return crl::guard(this, [=](Api::SendOptions options) { + send(options, false); + }); } SendFilesBox::~SendFilesBox() = default; diff --git a/Telegram/SourceFiles/boxes/send_files_box.h b/Telegram/SourceFiles/boxes/send_files_box.h index af712eda1..1e95a1aa8 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.h +++ b/Telegram/SourceFiles/boxes/send_files_box.h @@ -47,7 +47,8 @@ class SessionController; } // namespace Window namespace SendMenu { -enum class Type; +struct Details; +struct Action; } // namespace SendMenu namespace HistoryView::Controls { @@ -96,7 +97,7 @@ struct SendFilesBoxDescriptor { SendFilesLimits limits = {}; SendFilesCheck check; Api::SendType sendType = {}; - SendMenu::Type sendMenuType = {}; + Fn sendMenuDetails = nullptr; const style::ComposeControls *stOverride = nullptr; SendFilesConfirmed confirmed; Fn cancelled; @@ -115,7 +116,7 @@ public: const TextWithTags &caption, not_null toPeer, Api::SendType sendType, - SendMenu::Type sendMenuType); + SendMenu::Details sendMenuDetails); SendFilesBox(QWidget*, SendFilesBoxDescriptor &&descriptor); void setConfirmedCallback(SendFilesConfirmed callback) { @@ -136,6 +137,9 @@ protected: void resizeEvent(QResizeEvent *e) override; private: + using MenuAction = SendMenu::Action; + using MenuDetails = SendMenu::Details; + class Block final { public: Block( @@ -173,7 +177,7 @@ private: void initSendWay(); void initPreview(); - [[nodiscard]] bool hasSendMenu() const; + [[nodiscard]] bool hasSendMenu(const MenuDetails &details) const; [[nodiscard]] bool hasSpoilerMenu() const; [[nodiscard]] bool allWithSpoilers(); [[nodiscard]] bool checkWithWay( @@ -202,9 +206,7 @@ private: void generatePreviewFrom(int fromBlock); void send(Api::SendOptions options, bool ctrlShiftEnter = false); - void sendSilent(); - void sendScheduled(); - void sendWhenOnline(); + [[nodiscard]] Fn sendCallback(); void captionResized(); void saveSendWaySettings(); @@ -227,6 +229,11 @@ private: void checkCharsLimitation(); + [[nodiscard]] Fn prepareSendMenuDetails( + const SendFilesBoxDescriptor &descriptor); + [[nodiscard]] auto prepareSendMenuCallback() + -> Fn; + const std::shared_ptr _show; const style::ComposeControls &_st; const Api::SendType _sendType = Api::SendType(); @@ -238,12 +245,14 @@ private: std::optional _removingIndex; SendFilesLimits _limits = {}; - SendMenu::Type _sendMenuType = {}; + Fn _sendMenuDetails; + Fn _sendMenuCallback; PeerData *_captionToPeer = nullptr; SendFilesCheck _check; SendFilesConfirmed _confirmedCallback; Fn _cancelledCallback; bool _confirmed = false; + bool _invertCaption = false; object_ptr _caption = { nullptr }; TextWithTags _prefilledCaptionText; diff --git a/Telegram/SourceFiles/boxes/share_box.cpp b/Telegram/SourceFiles/boxes/share_box.cpp index f4d691cb9..d86e24510 100644 --- a/Telegram/SourceFiles/boxes/share_box.cpp +++ b/Telegram/SourceFiles/boxes/share_box.cpp @@ -478,15 +478,18 @@ void ShareBox::keyPressEvent(QKeyEvent *e) { } } -SendMenu::Type ShareBox::sendMenuType() const { +SendMenu::Details ShareBox::sendMenuDetails() const { const auto selected = _inner->selected(); - return ranges::all_of( + const auto type = ranges::all_of( selected | ranges::views::transform(&Data::Thread::peer), HistoryView::CanScheduleUntilOnline) ? SendMenu::Type::ScheduledToUser : (selected.size() == 1 && selected.front()->peer()->isSelf()) ? SendMenu::Type::Reminder : SendMenu::Type::Scheduled; + + // We can't support effect here because we don't have ChatHelpers::Show. + return { .type = type, .effectAllowed = false }; } void ShareBox::showMenu(not_null parent) { @@ -523,15 +526,32 @@ void ShareBox::showMenu(not_null parent) { _menu->addSeparator(); } - const auto result = SendMenu::FillSendMenu( + using namespace SendMenu; + const auto sendAction = crl::guard(this, [=](Action action, Details) { + if (action.type == ActionType::Send) { + submit(action.options); + return; + } + uiShow()->showBox( + HistoryView::PrepareScheduleBox( + this, + nullptr, // ChatHelpers::Show for effect attachment. + sendMenuDetails(), + [=](Api::SendOptions options) { submit(options); }, + action.options, + HistoryView::DefaultScheduleTime(), + _descriptor.scheduleBoxStyle)); + }); + _menu->setForcedVerticalOrigin(Ui::PopupMenu::VerticalOrigin::Bottom); + const auto result = FillSendMenu( _menu.get(), - sendMenuType(), - [=] { submitSilent(); }, - [=] { submitScheduled(); }, - [=] { submitWhenOnline(); }); - const auto success = (result == SendMenu::FillMenuResult::Success); - if (_descriptor.forwardOptions.show || success) { - _menu->setForcedVerticalOrigin(Ui::PopupMenu::VerticalOrigin::Bottom); + nullptr, // showForEffect. + sendMenuDetails(), + sendAction); + if (result == SendMenu::FillMenuResult::Prepared) { + _menu->popupPrepared(); + } else if (_descriptor.forwardOptions.show + && result != SendMenu::FillMenuResult::Failed) { _menu->popup(QCursor::pos()); } } @@ -612,25 +632,6 @@ void ShareBox::submit(Api::SendOptions options) { } } -void ShareBox::submitSilent() { - submit({ .silent = true }); -} - -void ShareBox::submitScheduled() { - const auto callback = [=](Api::SendOptions options) { submit(options); }; - uiShow()->showBox( - HistoryView::PrepareScheduleBox( - this, - sendMenuType(), - callback, - HistoryView::DefaultScheduleTime(), - _descriptor.scheduleBoxStyle)); -} - -void ShareBox::submitWhenOnline() { - submit(Api::DefaultSendWhenOnlineOptions()); -} - void ShareBox::copyLink() const { if (const auto onstack = _descriptor.copyCallback) { onstack(); diff --git a/Telegram/SourceFiles/boxes/share_box.h b/Telegram/SourceFiles/boxes/share_box.h index 253a8228e..32e824b15 100644 --- a/Telegram/SourceFiles/boxes/share_box.h +++ b/Telegram/SourceFiles/boxes/share_box.h @@ -24,7 +24,7 @@ struct PeerList; } // namespace style namespace SendMenu { -enum class Type; +struct Details; } // namespace SendMenu namespace Window { @@ -130,13 +130,10 @@ private: void scrollAnimationCallback(); void submit(Api::SendOptions options); - void submitSilent(); - void submitScheduled(); - void submitWhenOnline(); void copyLink() const; bool searchByUsername(bool useCache = false); - SendMenu::Type sendMenuType() const; + [[nodiscard]] SendMenu::Details sendMenuDetails() const; void scrollTo(Ui::ScrollToRequest request); void needSearchByUsername(); diff --git a/Telegram/SourceFiles/boxes/sticker_set_box.cpp b/Telegram/SourceFiles/boxes/sticker_set_box.cpp index 54fb913fb..a099fcfa6 100644 --- a/Telegram/SourceFiles/boxes/sticker_set_box.cpp +++ b/Telegram/SourceFiles/boxes/sticker_set_box.cpp @@ -79,7 +79,9 @@ using Data::StickersSet; using Data::StickersPack; using SetFlag = Data::StickersSetFlag; -[[nodiscard]] std::optional ComputeImageColor(const QImage &frame) { +[[nodiscard]] std::optional ComputeImageColor( + const style::icon &lockIcon, + const QImage &frame) { if (frame.isNull() || frame.format() != QImage::Format_ARGB32_Premultiplied) { return {}; @@ -89,7 +91,7 @@ using SetFlag = Data::StickersSetFlag; auto sb = int64(); auto sa = int64(); const auto factor = frame.devicePixelRatio(); - const auto size = st::stickersPremiumLock.size() * factor; + const auto size = lockIcon.size() * factor; const auto width = std::min(frame.width(), size.width()); const auto height = std::min(frame.height(), size.height()); const auto skipx = (frame.width() - width) / 2; @@ -116,22 +118,30 @@ using SetFlag = Data::StickersSetFlag; } -[[nodiscard]] QColor ComputeLockColor(const QImage &frame) { - return ComputeImageColor(frame).value_or(st::windowSubTextFg->c); +[[nodiscard]] QColor ComputeLockColor( + const style::icon &lockIcon, + const QImage &frame) { + return ComputeImageColor( + lockIcon, + frame + ).value_or(st::windowSubTextFg->c); } -void ValidatePremiumLockBg(QImage &image, const QImage &frame) { +void ValidatePremiumLockBg( + const style::icon &lockIcon, + QImage &image, + const QImage &frame) { if (!image.isNull()) { return; } const auto factor = style::DevicePixelRatio(); - const auto size = st::stickersPremiumLock.size(); + const auto size = lockIcon.size(); image = QImage( size * factor, QImage::Format_ARGB32_Premultiplied); image.setDevicePixelRatio(factor); auto p = QPainter(&image); - const auto color = ComputeLockColor(frame); + const auto color = ComputeLockColor(lockIcon, frame); p.fillRect( QRect(QPoint(), size), anim::color(color, st::windowSubTextFg, kGrayLockOpacity)); @@ -140,12 +150,12 @@ void ValidatePremiumLockBg(QImage &image, const QImage &frame) { image = Images::Circle(std::move(image)); } -void ValidatePremiumStarFg(QImage &image) { +void ValidatePremiumStarFg(const style::icon &lockIcon, QImage &image) { if (!image.isNull()) { return; } const auto factor = style::DevicePixelRatio(); - const auto size = st::stickersPremiumLock.size(); + const auto size = lockIcon.size(); image = QImage( size * factor, QImage::Format_ARGB32_Premultiplied); @@ -182,7 +192,10 @@ void ValidatePremiumStarFg(QImage &image) { } // namespace -StickerPremiumMark::StickerPremiumMark(not_null session) { +StickerPremiumMark::StickerPremiumMark( + not_null session, + const style::icon &lockIcon) +: _lockIcon(lockIcon) { style::PaletteChanged( ) | rpl::start_with_next([=] { _lockGray = QImage(); @@ -208,16 +221,14 @@ void StickerPremiumMark::paint( const auto factor = style::DevicePixelRatio(); const auto radius = st::roundRadiusSmall; const auto point = position + QPoint( - (_premium - ? (singleSize.width() - (bg.width() / factor) - radius) - : (singleSize.width() - (bg.width() / factor)) / 2), + (singleSize.width() - (bg.width() / factor) - radius), singleSize.height() - (bg.height() / factor) - radius); p.drawImage(point, bg); if (_premium) { validateStar(); p.drawImage(point, _star); } else { - st::stickersPremiumLock.paint(p, point, outerWidth); + _lockIcon.paint(p, point, outerWidth); } } @@ -225,11 +236,11 @@ void StickerPremiumMark::validateLock( const QImage &frame, QImage &backCache) { auto &image = frame.isNull() ? _lockGray : backCache; - ValidatePremiumLockBg(image, frame); + ValidatePremiumLockBg(_lockIcon, image, frame); } void StickerPremiumMark::validateStar() { - ValidatePremiumStarFg(_star); + ValidatePremiumStarFg(_lockIcon, _star); } class StickerSetBox::Inner final : public Ui::RpWidget { @@ -708,7 +719,7 @@ StickerSetBox::Inner::Inner( st::windowBgRipple, st::windowBgOver, [=] { repaintItems(); })) -, _premiumMark(_session) +, _premiumMark(_session, st::stickersPremiumLock) , _updateItemsTimer([=] { updateItems(); }) , _input(set) , _padding((type == Data::StickersType::Emoji) @@ -1058,7 +1069,7 @@ void StickerSetBox::Inner::contextMenuEvent(QContextMenuEvent *e) { _menu = base::make_unique_q( this, st::popupMenuWithIcons); - const auto type = _show->sendMenuType(); + const auto details = _show->sendMenuDetails(); if (setType() == Data::StickersType::Emoji) { if (const auto t = PrepareTextFromEmoji(_pack[index]); !t.empty()) { _menu->addAction(tr::lng_mediaview_copy(tr::now), [=] { @@ -1067,17 +1078,16 @@ void StickerSetBox::Inner::contextMenuEvent(QContextMenuEvent *e) { } }, &st::menuIconCopy); } - } else if (type != SendMenu::Type::Disabled) { + } else if (details.type != SendMenu::Type::Disabled) { const auto document = _pack[index]; - const auto sendSelected = [=](Api::SendOptions options) { + const auto send = crl::guard(this, [=](Api::SendOptions options) { chosen(index, document, options); - }; + }); SendMenu::FillSendMenu( _menu.get(), - type, - SendMenu::DefaultSilentCallback(sendSelected), - SendMenu::DefaultScheduleCallback(_show, type, sendSelected), - SendMenu::DefaultWhenOnlineCallback(sendSelected)); + _show, + details, + SendMenu::DefaultCallback(_show, send)); const auto show = _show; const auto toggleFavedSticker = [=] { diff --git a/Telegram/SourceFiles/boxes/sticker_set_box.h b/Telegram/SourceFiles/boxes/sticker_set_box.h index c57a1f484..e718cc45e 100644 --- a/Telegram/SourceFiles/boxes/sticker_set_box.h +++ b/Telegram/SourceFiles/boxes/sticker_set_box.h @@ -23,10 +23,6 @@ namespace Data { class StickersSet; } // namespace Data -namespace SendMenu { -enum class Type; -} // namespace SendMenu - namespace ChatHelpers { struct FileChosen; class Show; @@ -34,7 +30,9 @@ class Show; class StickerPremiumMark final { public: - explicit StickerPremiumMark(not_null session); + StickerPremiumMark( + not_null session, + const style::icon &lockIcon); void paint( QPainter &p, @@ -48,6 +46,7 @@ private: void validateLock(const QImage &frame, QImage &backCache); void validateStar(); + const style::icon &_lockIcon; QImage _lockGray; QImage _star; bool _premium = false; diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index 0631332f9..270bed675 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -69,6 +69,8 @@ ComposeIcons { menuWhenOnline: icon; menuSpoiler: icon; menuSpoilerOff: icon; + menuBelow: icon; + menuAbove: icon; stripBubble: icon; stripExpandPanel: icon; @@ -489,6 +491,7 @@ hashtagClose: IconButton { stickerPanWidthMin: 64px; stickerPanSize: size(stickerPanWidthMin, stickerPanWidthMin); +stickerEffectWidthMin: 48px; stickerPanPadding: 11px; stickerPanDeleteIconBg: icon {{ "emoji/emoji_delete_bg", stickerPanDeleteBg }}; stickerPanDeleteIconFg: icon {{ "emoji/emoji_delete", stickerPanDeleteFg }}; @@ -605,6 +608,8 @@ defaultComposeIcons: ComposeIcons { menuWhenOnline: menuIconWhenOnline; menuSpoiler: menuIconSpoiler; menuSpoilerOff: menuIconSpoilerOff; + menuBelow: menuIconBelow; + menuAbove: menuIconAbove; stripBubble: icon{ { "chat/reactions_bubble_shadow", windowShadowFg }, @@ -669,7 +674,7 @@ defaultEmojiPan: EmojiPan { boxLabel: boxLabel; icons: defaultComposeIcons; about: defaultEmojiPanAbout; - aboutPadding: margins(12px, 2px, 12px, 2px); + aboutPadding: margins(12px, 3px, 12px, 2px); autocompleteBottomSkip: 0px; } statusEmojiPan: EmojiPan(defaultEmojiPan) { @@ -753,6 +758,7 @@ inlineResultsMinWidth: 48px; inlineDurationMargin: 3px; stickersPremiumLock: icon{{ "emoji/premium_lock", premiumButtonFg }}; +emojiPremiumLock: icon{{ "chat/mini_lock", premiumButtonFg }}; reactStripExtend: margins(21px, 49px, 39px, 0px); reactStripHeight: 40px; @@ -924,7 +930,12 @@ historyPinnedBotButton: RoundButton(defaultActiveButton) { textTop: 6px; padding: margins(2px, 10px, 10px, 9px); } -historyPinnedBotButtonMaxWidth: 150px; +historyPinnedBotLabel: FlatLabel(defaultFlatLabel) { + style: semiboldTextStyle; + align: align(center); + maxHeight: 30px; +} +historyPinnedBotButtonMaxWidth: 120px; historyToDownPosition: point(12px, 10px); historyToDownAbove: icon {{ "history_down_arrow", historyToDownFg }}; diff --git a/Telegram/SourceFiles/chat_helpers/compose/compose_show.h b/Telegram/SourceFiles/chat_helpers/compose/compose_show.h index 7dcbab513..28fc7ff47 100644 --- a/Telegram/SourceFiles/chat_helpers/compose/compose_show.h +++ b/Telegram/SourceFiles/chat_helpers/compose/compose_show.h @@ -22,7 +22,7 @@ class SessionController; } // namespace Window namespace SendMenu { -enum class Type; +struct Details; } // namespace SendMenu namespace ChatHelpers { @@ -57,7 +57,7 @@ public: [[nodiscard]] virtual rpl::producer<> pauseChanged() const = 0; [[nodiscard]] virtual rpl::producer adjustShadowLeft() const; - [[nodiscard]] virtual SendMenu::Type sendMenuType() const = 0; + [[nodiscard]] virtual SendMenu::Details sendMenuDetails() const = 0; virtual bool showMediaPreview( Data::FileOrigin origin, diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp index e2b41060e..443592ea6 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp @@ -136,6 +136,7 @@ struct EmojiListWidget::CustomEmojiInstance { struct EmojiListWidget::RecentOne { Ui::Text::CustomEmoji *custom = nullptr; RecentEmojiId id; + mutable QImage premiumLock; }; EmojiColorPicker::EmojiColorPicker( @@ -478,8 +479,12 @@ EmojiListWidget::EmojiListWidget( , _localSetsManager( std::make_unique(&session())) , _customRecentFactory(std::move(descriptor.customRecentFactory)) +, _freeEffects(std::move(descriptor.freeEffects)) , _customTextColor(std::move(descriptor.customTextColor)) , _overBg(st::emojiPanRadius, st().overBg) +, _premiumMark(std::make_unique( + &session(), + st::emojiPremiumLock)) , _collapsedBg(st::emojiPanExpand.height / 2, st().headerFg) , _picker(this, st()) , _showPickerTimer([=] { showPicker(); }) @@ -583,9 +588,18 @@ void EmojiListWidget::setupSearch() { InvokeQueued(this, [=] { applyNextSearchQuery(); }); + _searchQueries.fire_copy(_nextSearchQuery); }, session, type); } +rpl::producer> EmojiListWidget::searchQueries() const { + return _searchQueries.events(); +} + +rpl::producer EmojiListWidget::recentShownCount() const { + return _recentShownCount.value(); +} + void EmojiListWidget::applyNextSearchQuery() { if (_searchQuery == _nextSearchQuery) { return; @@ -607,6 +621,9 @@ void EmojiListWidget::applyNextSearchQuery() { _searchCustomIds.clear(); } resizeToWidth(width()); + _recentShownCount = searching + ? _searchResults.size() + : _recent.size(); update(); if (modeChanged) { visibleTopBottomUpdated(getVisibleTop(), getVisibleBottom()); @@ -834,7 +851,8 @@ void EmojiListWidget::unloadCustomIn(const SectionInfo &info) { object_ptr EmojiListWidget::createFooter() { Expects(_footer == nullptr); - if (_mode == EmojiListMode::RecentReactions) { + if (_mode == EmojiListMode::RecentReactions + || _mode == EmojiListMode::MessageEffects) { return { nullptr }; } @@ -1018,9 +1036,10 @@ int EmojiListWidget::countDesiredHeight(int newWidth) { const auto minimalLastHeight = std::max( minimalHeight - padding.bottom(), 0); - return qMax( - minimalHeight, - countResult(minimalLastHeight) + padding.bottom()); + const auto result = countResult(minimalLastHeight); + return result + ? qMax(minimalHeight, result + padding.bottom()) + : 0; } int EmojiListWidget::defaultMinimalHeight() const { @@ -1104,7 +1123,7 @@ void EmojiListWidget::fillRecentFrom(const std::vector &list) { } base::unique_qptr EmojiListWidget::fillContextMenu( - SendMenu::Type type) { + const SendMenu::Details &details) { if (v::is_null(_selected)) { return nullptr; } @@ -1285,6 +1304,8 @@ void EmojiListWidget::paint( QRect clip) { validateEmojiPaintContext(context); + _paintAsPremium = session().premium(); + auto fromColumn = floorclamp( clip.x() - _rowsLeft, _singleSize.width(), @@ -1449,16 +1470,44 @@ void EmojiListWidget::drawRecent( QPoint position, const RecentOne &recent) { _recentPainted = true; + const auto locked = (_mode == Mode::MessageEffects) + && !_paintAsPremium + && v::is(recent.id.data) + && !_freeEffects.contains( + v::get(recent.id.data).id); + auto lockedPainted = false; + if (locked) { + if (_premiumMarkFrameCache.isNull()) { + const auto ratio = style::DevicePixelRatio(); + _premiumMarkFrameCache = QImage( + QSize(_customSingleSize, _customSingleSize) * ratio, + QImage::Format_ARGB32_Premultiplied); + _premiumMarkFrameCache.setDevicePixelRatio(ratio); + } + _premiumMarkFrameCache.fill(Qt::transparent); + } if (const auto custom = recent.custom) { - _emojiPaintContext->scale = context.progress; - _emojiPaintContext->position = position + const auto exactPosition = position + _innerPosition + _customPosition; + _emojiPaintContext->scale = context.progress; if (_mode == Mode::ChannelStatus) { _emojiPaintContext->internal.forceFirstFrame = (recent.id == _recent.front().id); } - custom->paint(p, *_emojiPaintContext); + if (locked) { + lockedPainted = custom->ready(); + + auto q = Painter(&_premiumMarkFrameCache); + _emojiPaintContext->position = QPoint(); + custom->paint(q, *_emojiPaintContext); + q.end(); + + p.drawImage(exactPosition, _premiumMarkFrameCache); + } else { + _emojiPaintContext->position = exactPosition; + custom->paint(p, *_emojiPaintContext); + } } else if (const auto emoji = std::get_if(&recent.id.data)) { if (_mode == Mode::EmojiStatus) { position += QPoint( @@ -1472,6 +1521,16 @@ void EmojiListWidget::drawRecent( } else { Unexpected("Empty custom emoji in EmojiListWidget::drawRecent."); } + + if (locked) { + _premiumMark->paint( + p, + lockedPainted ? _premiumMarkFrameCache : QImage(), + recent.premiumLock, + position, + _singleSize, + width()); + } } void EmojiListWidget::drawEmoji( @@ -2131,7 +2190,7 @@ void EmojiListWidget::refreshRecent() { } void EmojiListWidget::refreshCustom() { - if (_mode == Mode::RecentReactions) { + if (_mode == Mode::RecentReactions || _mode == Mode::MessageEffects) { return; } auto old = base::take(_custom); diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h index 3159d340e..3aef95ac0 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h @@ -13,6 +13,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/round_rect.h" #include "base/timer.h" +class StickerPremiumMark; + namespace style { struct EmojiPan; } // namespace style @@ -77,6 +79,7 @@ enum class EmojiListMode { UserpicBuilder, BackgroundEmoji, PeerTitle, + MessageEffects, }; struct EmojiListDescriptor { @@ -88,6 +91,7 @@ struct EmojiListDescriptor { Fn( DocumentId, Fn)> customRecentFactory; + base::flat_set freeEffects; const style::EmojiPan *st = nullptr; ComposeFeatures features; }; @@ -144,7 +148,10 @@ public: RectPart origin); base::unique_qptr fillContextMenu( - SendMenu::Type type) override; + const SendMenu::Details &details) override; + + [[nodiscard]] rpl::producer> searchQueries() const; + [[nodiscard]] rpl::producer recentShownCount() const; protected: void visibleTopBottomUpdated( @@ -397,10 +404,13 @@ private: int _counts[kEmojiSectionCount]; std::vector _recent; base::flat_set _recentCustomIds; + base::flat_set _freeEffects; base::flat_set _repaintsScheduled; + rpl::variable _recentShownCount; std::unique_ptr _emojiPaintContext; bool _recentPainted = false; bool _grabbingChosen = false; + bool _paintAsPremium = false; QVector _emoji[kEmojiSectionCount]; std::vector _custom; base::flat_set _restrictedCustomList; @@ -414,10 +424,13 @@ private: Ui::RoundRect _overBg; QImage _searchExpandCache; + std::unique_ptr _premiumMark; + QImage _premiumMarkFrameCache; mutable std::unique_ptr _colorAllRipple; bool _colorAllRippleForced = false; rpl::lifetime _colorAllRippleForcedLifetime; + rpl::event_stream> _searchQueries; std::vector _nextSearchQuery; std::vector _searchQuery; base::flat_set _searchEmoji; diff --git a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp index 20f32f6e8..bece81b64 100644 --- a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp +++ b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp @@ -87,7 +87,7 @@ public: Api::SendOptions options = {}) const; void setRecentInlineBotsInRows(int32 bots); - void setSendMenuType(Fn &&callback); + void setSendMenuDetails(Fn &&callback); void rowsUpdated(); rpl::producer mentionChosen() const; @@ -155,7 +155,7 @@ private: const std::unique_ptr _pathGradient; StickerPremiumMark _premiumMark; - Fn _sendMenuType; + Fn _sendMenuDetails; rpl::event_stream _mentionChosen; rpl::event_stream _hashtagChosen; @@ -835,8 +835,9 @@ bool FieldAutocomplete::chooseSelected(ChooseMethod method) const { return _inner->chooseSelected(method); } -void FieldAutocomplete::setSendMenuType(Fn &&callback) { - _inner->setSendMenuType(std::move(callback)); +void FieldAutocomplete::setSendMenuDetails( + Fn &&callback) { + _inner->setSendMenuDetails(std::move(callback)); } bool FieldAutocomplete::eventFilter(QObject *obj, QEvent *e) { @@ -890,7 +891,7 @@ FieldAutocomplete::Inner::Inner( _st.pathBg, _st.pathFg, [=] { update(); })) -, _premiumMark(_session) +, _premiumMark(_session, st::stickersPremiumLock) , _previewTimer([=] { showPreview(); }) { _session->downloaderTaskFinished( ) | rpl::start_with_next([=] { @@ -1364,24 +1365,22 @@ void FieldAutocomplete::Inner::contextMenuEvent(QContextMenuEvent *e) { return; } const auto index = _sel; - const auto type = _sendMenuType - ? _sendMenuType() - : SendMenu::Type::Disabled; + const auto details = _sendMenuDetails + ? _sendMenuDetails() + : SendMenu::Details(); const auto method = FieldAutocomplete::ChooseMethod::ByClick; _menu = base::make_unique_q( this, st::popupMenuWithIcons); - const auto send = [=](Api::SendOptions options) { + const auto send = crl::guard(this, [=](Api::SendOptions options) { chooseAtIndex(method, index, options); - }; + }); SendMenu::FillSendMenu( _menu, - type, - SendMenu::DefaultSilentCallback(send), - SendMenu::DefaultScheduleCallback(_show, type, send), - SendMenu::DefaultWhenOnlineCallback(send)); - + _show, + details, + SendMenu::DefaultCallback(_show, send)); if (!_menu->empty()) { _menu->popup(QCursor::pos()); } @@ -1604,9 +1603,9 @@ void FieldAutocomplete::Inner::showPreview() { } } -void FieldAutocomplete::Inner::setSendMenuType( - Fn &&callback) { - _sendMenuType = std::move(callback); +void FieldAutocomplete::Inner::setSendMenuDetails( + Fn &&callback) { + _sendMenuDetails = std::move(callback); } auto FieldAutocomplete::Inner::mentionChosen() const diff --git a/Telegram/SourceFiles/chat_helpers/field_autocomplete.h b/Telegram/SourceFiles/chat_helpers/field_autocomplete.h index 5c9e291f3..d95b7e394 100644 --- a/Telegram/SourceFiles/chat_helpers/field_autocomplete.h +++ b/Telegram/SourceFiles/chat_helpers/field_autocomplete.h @@ -42,7 +42,7 @@ class DocumentMedia; } // namespace Data namespace SendMenu { -enum class Type; +struct Details; } // namespace SendMenu namespace ChatHelpers { @@ -123,7 +123,7 @@ public: void setModerateKeyActivateCallback(Fn callback) { _moderateKeyActivateCallback = std::move(callback); } - void setSendMenuType(Fn &&callback); + void setSendMenuDetails(Fn &&callback); void hideFast(); void showAnimated(); diff --git a/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp index 222efa961..4bab1bc3f 100644 --- a/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp @@ -387,22 +387,31 @@ void GifsListWidget::mousePressEvent(QMouseEvent *e) { } base::unique_qptr GifsListWidget::fillContextMenu( - SendMenu::Type type) { + const SendMenu::Details &details) { if (_selected < 0 || _pressed >= 0) { return nullptr; } auto menu = base::make_unique_q(this, st().menu); - const auto send = [=, selected = _selected](Api::SendOptions options) { + const auto selected = _selected; + const auto send = crl::guard(this, [=](Api::SendOptions options) { selectInlineResult(selected, options, true); - }; + }); + const auto item = _mosaic.maybeItemAt(_selected); + const auto isInlineResult = !item->getPhoto() + && !item->getDocument() + && item->getResult(); const auto icons = &st().icons; + auto copyDetails = details; + if (isInlineResult) { + // inline results don't have effects + copyDetails.effectAllowed = false; + } SendMenu::FillSendMenu( menu, - type, - SendMenu::DefaultSilentCallback(send), - SendMenu::DefaultScheduleCallback(_show, type, send), - SendMenu::DefaultWhenOnlineCallback(send), + _show, + copyDetails, + SendMenu::DefaultCallback(_show, send), icons); if (const auto item = _mosaic.maybeItemAt(_selected)) { diff --git a/Telegram/SourceFiles/chat_helpers/gifs_list_widget.h b/Telegram/SourceFiles/chat_helpers/gifs_list_widget.h index 84e20c864..ad6c26ae6 100644 --- a/Telegram/SourceFiles/chat_helpers/gifs_list_widget.h +++ b/Telegram/SourceFiles/chat_helpers/gifs_list_widget.h @@ -40,7 +40,7 @@ class SessionController; } // namespace Window namespace SendMenu { -enum class Type; +struct Details; } // namespace SendMenu namespace Data { @@ -102,7 +102,7 @@ public: rpl::producer<> cancelRequests() const; base::unique_qptr fillContextMenu( - SendMenu::Type type) override; + const SendMenu::Details &details) override; ~GifsListWidget(); diff --git a/Telegram/SourceFiles/chat_helpers/message_field.cpp b/Telegram/SourceFiles/chat_helpers/message_field.cpp index 75d9ae53b..1eedbbcf2 100644 --- a/Telegram/SourceFiles/chat_helpers/message_field.cpp +++ b/Telegram/SourceFiles/chat_helpers/message_field.cpp @@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/shortcuts.h" #include "core/application.h" #include "core/core_settings.h" +#include "ui/text/text_utilities.h" #include "ui/toast/toast.h" #include "ui/wrap/vertical_layout.h" #include "ui/widgets/buttons.h" @@ -60,60 +61,43 @@ constexpr auto kTypesDuration = 4 * crl::time(1000); // For mention / custom emoji tags save and validate selfId, // ignore tags for different users. -class FieldTagMimeProcessor final { -public: - FieldTagMimeProcessor( - not_null _session, - Fn)> allowPremiumEmoji); - - QString operator()(QStringView mimeTag); - -private: - const not_null _session; - const Fn)> _allowPremiumEmoji; - -}; - -FieldTagMimeProcessor::FieldTagMimeProcessor( - not_null session, - Fn)> allowPremiumEmoji) -: _session(session) -, _allowPremiumEmoji(allowPremiumEmoji) { -} - -QString FieldTagMimeProcessor::operator()(QStringView mimeTag) { - const auto id = _session->userId().bare; - auto all = TextUtilities::SplitTags(mimeTag); - auto premiumSkipped = (DocumentData*)nullptr; - for (auto i = all.begin(); i != all.end();) { - const auto tag = *i; - if (TextUtilities::IsMentionLink(tag) - && TextUtilities::MentionNameDataToFields(tag).selfId != id) { - i = all.erase(i); - continue; - } else if (Ui::InputField::IsCustomEmojiLink(tag)) { - const auto data = Ui::InputField::CustomEmojiEntityData(tag); - const auto emoji = Data::ParseCustomEmojiData(data); - if (!emoji) { +[[nodiscard]] Fn FieldTagMimeProcessor( + not_null session, + Fn)> allowPremiumEmoji) { + return [=](QStringView mimeTag) { + const auto id = session->userId().bare; + auto all = TextUtilities::SplitTags(mimeTag); + auto premiumSkipped = (DocumentData*)nullptr; + for (auto i = all.begin(); i != all.end();) { + const auto tag = *i; + if (TextUtilities::IsMentionLink(tag) + && TextUtilities::MentionNameDataToFields(tag).selfId != id) { i = all.erase(i); continue; - } else if (!_session->premium()) { - const auto document = _session->data().document(emoji); - if (document->isPremiumEmoji()) { - if (!_allowPremiumEmoji - || premiumSkipped - || !_session->premiumPossible() - || !_allowPremiumEmoji(document)) { - premiumSkipped = document; - i = all.erase(i); - continue; + } else if (Ui::InputField::IsCustomEmojiLink(tag)) { + const auto data = Ui::InputField::CustomEmojiEntityData(tag); + const auto emoji = Data::ParseCustomEmojiData(data); + if (!emoji) { + i = all.erase(i); + continue; + } else if (!session->premium()) { + const auto document = session->data().document(emoji); + if (document->isPremiumEmoji()) { + if (!allowPremiumEmoji + || premiumSkipped + || !session->premiumPossible() + || !allowPremiumEmoji(document)) { + premiumSkipped = document; + i = all.erase(i); + continue; + } } } } + ++i; } - ++i; - } - return TextUtilities::JoinTag(all); + return TextUtilities::JoinTag(all); + }; } //bool ValidateUrl(const QString &value) { @@ -132,7 +116,8 @@ void EditLinkBox( const QString &startText, const QString &startLink, Fn callback, - const style::InputField *fieldStyle) { + const style::InputField *fieldStyle, + Fn validate) { Expects(callback != nullptr); const auto &fieldSt = fieldStyle ? *fieldStyle : st::defaultInputField; @@ -177,7 +162,7 @@ void EditLinkBox( const auto submit = [=] { const auto linkText = text->getLastText(); - const auto linkUrl = qthelp::validate_url(url->getLastText()); + const auto linkUrl = validate(url->getLastText()); if (linkText.isEmpty()) { text->showError(); return; @@ -329,7 +314,8 @@ FnsetInstantReplaces(Ui::InstantReplaces::Default()); field->setInstantReplacesEnabled( Core::App().settings().replaceEmojiValue()); - field->setMarkdownReplacesEnabled(rpl::single(true)); + field->setMarkdownReplacesEnabled(true); if (show) { field->setEditLinkCallback( DefaultEditLinkCallback(show, field, fieldStyle)); @@ -360,6 +346,88 @@ void InitMessageFieldHandlers( } } +[[nodiscard]] bool IsGoodFactcheckUrl(QStringView url) { + return url.startsWith(u"t.me/"_q) || url.startsWith(u"https://t.me/"_q); +} + +[[nodiscard]] Fn FactcheckEditLinkCallback( + std::shared_ptr show, + not_null field) { + const auto weak = Ui::MakeWeak(field); + return [=]( + EditLinkSelection selection, + QString text, + QString link, + EditLinkAction action) { + const auto validate = [=](QString url) { + if (IsGoodFactcheckUrl(url)) { + const auto start = u"https://"_q; + return url.startsWith(start) ? url : (start + url); + } + show->showToast( + tr::lng_factcheck_links(tr::now, Ui::Text::RichLangValue)); + return QString(); + }; + if (action == EditLinkAction::Check) { + return IsGoodFactcheckUrl(link); + } + auto callback = [=](const QString &text, const QString &link) { + if (const auto strong = weak.data()) { + strong->commitMarkdownLinkEdit(selection, text, link); + } + }; + show->showBox(Box( + EditLinkBox, + show, + text, + link, + std::move(callback), + nullptr, + validate)); + return true; + }; +} + +Fn)> FactcheckFieldIniter( + std::shared_ptr show) { + Expects(show != nullptr); + + return [=](not_null field) { + field->setTagMimeProcessor([](QStringView mimeTag) { + using Field = Ui::InputField; + auto all = TextUtilities::SplitTags(mimeTag); + for (auto i = all.begin(); i != all.end();) { + const auto tag = *i; + if (tag != Field::kTagBold + && tag != Field::kTagItalic + && (!Field::IsValidMarkdownLink(mimeTag) + || TextUtilities::IsMentionLink(mimeTag))) { + i = all.erase(i); + continue; + } + ++i; + } + return TextUtilities::JoinTag(all); + }); + field->setInstantReplaces(Ui::InstantReplaces::Default()); + field->setInstantReplacesEnabled( + Core::App().settings().replaceEmojiValue()); + field->setMarkdownReplacesEnabled(rpl::single( + Ui::MarkdownEnabledState{ + Ui::MarkdownEnabled{ + { Ui::InputField::kTagBold, Ui::InputField::kTagItalic } + } + } + )); + field->setEditLinkCallback(FactcheckEditLinkCallback(show, field)); + InitSpellchecker(show, field); + }; +} + void InitMessageFieldHandlers( not_null controller, not_null field, @@ -431,10 +499,7 @@ bool HasSendText(not_null field) { const auto &text = field->getTextWithTags().text; for (const auto &ch : text) { const auto code = ch.unicode(); - if (code != ' ' - && code != '\n' - && code != '\r' - && !IsReplacedBySpace(code)) { + if (!IsTrimmed(ch) && !IsReplacedBySpace(code)) { return true; } } @@ -732,7 +797,8 @@ void MessageLinksParser::parse() { || (tag == Ui::InputField::kTagUnderline) || (tag == Ui::InputField::kTagStrikeOut) || (tag == Ui::InputField::kTagSpoiler) - || (tag == Ui::InputField::kTagBlockquote); + || (tag == Ui::InputField::kTagBlockquote) + || (tag == Ui::InputField::kTagBlockquoteCollapsed); }; _ranges.clear(); diff --git a/Telegram/SourceFiles/chat_helpers/message_field.h b/Telegram/SourceFiles/chat_helpers/message_field.h index 0c9d8ff85..1e67642a7 100644 --- a/Telegram/SourceFiles/chat_helpers/message_field.h +++ b/Telegram/SourceFiles/chat_helpers/message_field.h @@ -77,6 +77,9 @@ void InitSpellchecker( not_null field, bool skipDictionariesManager = false); +[[nodiscard]] Fn)> FactcheckFieldIniter( + std::shared_ptr show); + bool HasSendText(not_null field); void InitMessageFieldFade( diff --git a/Telegram/SourceFiles/chat_helpers/stickers_emoji_pack.cpp b/Telegram/SourceFiles/chat_helpers/stickers_emoji_pack.cpp index f4cc13c4c..af07736a4 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_emoji_pack.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_emoji_pack.cpp @@ -269,10 +269,10 @@ std::unique_ptr EmojiPack::effectPlayer( not_null document, QByteArray data, QString filepath, - bool premium) { + EffectType type) { // Shortened copy from stickers_lottie module. const auto baseKey = document->bigFileBaseCacheKey(); - const auto tag = uint8(0); + const auto tag = uint8(type); const auto keyShift = ((tag << 4) & 0xF0) | (uint8(ChatHelpers::StickerLottieSize::EmojiInteraction) & 0x0F); const auto key = Storage::Cache::Key{ @@ -292,19 +292,24 @@ std::unique_ptr EmojiPack::effectPlayer( std::move(data)); }); }; - const auto size = premium + const auto size = (type == EffectType::PremiumSticker) ? HistoryView::Sticker::PremiumEffectSize(document) - : HistoryView::Sticker::EmojiEffectSize(); + : (type == EffectType::EmojiInteraction) + ? HistoryView::Sticker::EmojiEffectSize() + : HistoryView::Sticker::MessageEffectSize(); const auto request = Lottie::FrameRequest{ size * style::DevicePixelRatio(), }; - auto &weakProvider = _sharedProviders[document]; + auto &weakProvider = _sharedProviders[{ document, type }]; auto shared = [&] { if (const auto result = weakProvider.lock()) { return result; } + const auto count = (type == EffectType::PremiumSticker) + ? kPremiumCachesCount + : kEmojiCachesCount; const auto result = Lottie::SinglePlayer::SharedProvider( - premium ? kPremiumCachesCount : kEmojiCachesCount, + count, get, put, Lottie::ReadContent(data, filepath), diff --git a/Telegram/SourceFiles/chat_helpers/stickers_emoji_pack.h b/Telegram/SourceFiles/chat_helpers/stickers_emoji_pack.h index 0226b04ef..133880e1a 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_emoji_pack.h +++ b/Telegram/SourceFiles/chat_helpers/stickers_emoji_pack.h @@ -50,6 +50,12 @@ struct LargeEmojiImage { [[nodiscard]] static QSize Size(); }; +enum class EffectType : uint8 { + EmojiInteraction, + PremiumSticker, + MessageEffect, +}; + class EmojiPack final { public: using ViewElement = HistoryView::Element; @@ -95,11 +101,23 @@ public: not_null document, QByteArray data, QString filepath, - bool premium); + EffectType type); private: class ImageLoader; + struct ProviderKey { + not_null document; + Stickers::EffectType type = {}; + + friend inline auto operator<=>( + const ProviderKey &, + const ProviderKey &) = default; + friend inline bool operator==( + const ProviderKey &, + const ProviderKey &) = default; + }; + void refresh(); void refreshDelayed(); void refreshAnimations(); @@ -135,7 +153,7 @@ private: mtpRequestId _animationsRequestId = 0; base::flat_map< - not_null, + ProviderKey, std::weak_ptr> _sharedProviders; rpl::event_stream<> _refreshed; diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp index f8039617e..6768054a4 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp @@ -195,8 +195,10 @@ StickersListWidget::StickersListWidget( , _overBg(st::roundRadiusLarge, st().overBg) , _api(&session().mtp()) , _localSetsManager(std::make_unique(&session())) +, _customRecentIds(std::move(descriptor.customRecentList)) , _section(Section::Stickers) , _isMasks(_mode == Mode::Masks) +, _isEffects(_mode == Mode::MessageEffects) , _updateItemsTimer([=] { updateItems(); }) , _updateSetsTimer([=] { updateSets(); }) , _trendingAddBgOver( @@ -223,12 +225,16 @@ StickersListWidget::StickersListWidget( , _installedWidth(st::stickersTrendingInstalled.font->width(_installedText)) , _settings(this, tr::lng_stickers_you_have(tr::now)) , _previewTimer([=] { showPreview(); }) -, _premiumMark(std::make_unique(&session())) +, _premiumMark(std::make_unique( + &session(), + st::stickersPremiumLock)) , _searchRequestTimer([=] { sendSearchRequest(); }) { setMouseTracking(true); - setAttribute(Qt::WA_OpaquePaintEvent); + if (st().bg->c.alpha() > 0) { + setAttribute(Qt::WA_OpaquePaintEvent); + } - if (!_isMasks) { + if (!_isMasks && !_isEffects) { setupSearch(); } @@ -260,23 +266,30 @@ StickersListWidget::StickersListWidget( refreshStickers(); }, lifetime()); - session().data().stickers().recentUpdated( - _isMasks ? Data::StickersType::Masks : Data::StickersType::Stickers - ) | rpl::start_with_next([=] { - refreshRecent(); - }, lifetime()); + if (!_isEffects) { + session().data().stickers().recentUpdated(_isMasks + ? Data::StickersType::Masks + : Data::StickersType::Stickers + ) | rpl::start_with_next([=] { + refreshRecent(); + }, lifetime()); + } positionValue( ) | rpl::skip(1) | rpl::map_to( TabbedSelector::Action::Update ) | rpl::start_to_stream(_choosingUpdated, lifetime()); - rpl::merge( - Data::AmPremiumValue(&session()) | rpl::to_empty, - session().api().premium().cloudSetUpdated() - ) | rpl::start_with_next([=] { + if (_isEffects) { refreshStickers(); - }, lifetime()); + } else { + rpl::merge( + Data::AmPremiumValue(&session()) | rpl::to_empty, + session().api().premium().cloudSetUpdated() + ) | rpl::start_with_next([=] { + refreshStickers(); + }, lifetime()); + } } rpl::producer StickersListWidget::chosen() const { @@ -504,11 +517,14 @@ StickersListWidget::SectionInfo StickersListWidget::sectionInfoByOffset(int yOff } int StickersListWidget::countDesiredHeight(int newWidth) { - if (newWidth <= st::stickerPanWidthMin) { + const auto minSize = _isEffects + ? st::stickerEffectWidthMin + : st::stickerPanWidthMin; + if (newWidth < 2 * minSize) { return 0; } auto availableWidth = newWidth - (st::stickerPanPadding - st().margin.left()); - auto columnCount = availableWidth / st::stickerPanWidthMin; + auto columnCount = availableWidth / minSize; auto singleWidth = availableWidth / columnCount; auto fullWidth = (st().margin.left() + newWidth + st::emojiScroll.width); auto rowsRight = (fullWidth - columnCount * singleWidth) / 2; @@ -534,12 +550,12 @@ int StickersListWidget::countDesiredHeight(int newWidth) { const auto minimalLastHeight = (_section == Section::Stickers) ? minimalHeight : 0; - return qMax(minimalHeight, countResult(minimalLastHeight)) - + st::stickerPanPadding; + const auto result = qMax(minimalHeight, countResult(minimalLastHeight)); + return result ? (result + st::stickerPanPadding) : 0; } void StickersListWidget::sendSearchRequest() { - if (_searchRequestId || _searchNextQuery.isEmpty()) { + if (_searchRequestId || _searchNextQuery.isEmpty() || _isEffects) { return; } @@ -548,14 +564,12 @@ void StickersListWidget::sendSearchRequest() { auto it = _searchCache.find(_searchQuery); if (it != _searchCache.cend()) { - _search->setLoading(false); + toggleSearchLoading(false); return; } - - _search->setLoading(true); - + toggleSearchLoading(true); if (_searchQuery == Ui::PremiumGroupFakeEmoticon()) { - _search->setLoading(false); + toggleSearchLoading(false); _searchRequestId = 0; _searchCache.emplace(_searchQuery, std::vector()); showSearchResults(); @@ -571,7 +585,7 @@ void StickersListWidget::sendSearchRequest() { searchResultsDone(result); }).fail([=] { // show error? - _search->setLoading(false); + toggleSearchLoading(false); _searchRequestId = 0; }).handleAllErrors().send(); } @@ -585,7 +599,10 @@ void StickersListWidget::searchForSets( return; } - if (query == Ui::PremiumGroupFakeEmoticon()) { + _filterStickersCornerEmoji.clear(); + if (_isEffects) { + filterEffectsByEmoji(std::move(emoji)); + } else if (query == Ui::PremiumGroupFakeEmoticon()) { _filteredStickers = session().data().stickers().getPremiumList(0); } else { _filteredStickers = session().data().stickers().getListByEmoji( @@ -594,7 +611,7 @@ void StickersListWidget::searchForSets( true); } if (_searchQuery != cleaned) { - _search->setLoading(false); + toggleSearchLoading(false); if (const auto requestId = base::take(_searchRequestId)) { _api.request(requestId).cancel(); } @@ -610,13 +627,14 @@ void StickersListWidget::searchForSets( } void StickersListWidget::cancelSetsSearch() { - _search->setLoading(false); + toggleSearchLoading(false); if (const auto requestId = base::take(_searchRequestId)) { _api.request(requestId).cancel(); } _searchRequestTimer.cancel(); _searchQuery = _searchNextQuery = QString(); _filteredStickers.clear(); + _filterStickersCornerEmoji.clear(); _searchCache.clear(); refreshSearchRows(nullptr); } @@ -647,8 +665,9 @@ void StickersListWidget::refreshSearchRows( }); fillFilteredStickersRow(); - fillLocalSearchRows(_searchNextQuery); - + if (!_isEffects) { + fillLocalSearchRows(_searchNextQuery); + } if (!cloudSets && _searchNextQuery.isEmpty()) { showStickerSet(!_mySets.empty() ? _mySets[0].id @@ -657,17 +676,21 @@ void StickersListWidget::refreshSearchRows( } setSection(Section::Search); - if (cloudSets) { + if (!_isEffects && cloudSets) { fillCloudSearchRows(*cloudSets); } refreshIcons(ValidateIconAnimations::Scroll); - _lastMousePosition = QCursor::pos(); resizeToWidth(width()); + _recentShownCount = _filteredStickers.size(); updateSelected(); } +rpl::producer StickersListWidget::recentShownCount() const { + return _recentShownCount.value(); +} + void StickersListWidget::fillLocalSearchRows(const QString &query) { const auto searchWordsList = TextUtilities::PrepareSearchWords(query); if (searchWordsList.isEmpty()) { @@ -727,7 +750,7 @@ void StickersListWidget::fillFilteredStickersRow() { SearchEmojiSectionSetId(), nullptr, Data::StickersSetFlag::Special, - QString(), // title + _isEffects ? tr::lng_effect_stickers_title(tr::now) : QString(), QString(), // shortName _filteredStickers.size(), false, // externalLayout @@ -750,6 +773,12 @@ void StickersListWidget::addSearchRow(not_null set) { std::move(elements)); } +void StickersListWidget::toggleSearchLoading(bool loading) { + if (_search) { + _search->setLoading(loading); + } +} + void StickersListWidget::takeHeavyData( std::vector &to, std::vector &from) { @@ -831,7 +860,7 @@ auto StickersListWidget::shownSets() -> std::vector & { void StickersListWidget::searchResultsDone( const MTPmessages_FoundStickerSets &result) { - _search->setLoading(false); + toggleSearchLoading(false); _searchRequestId = 0; if (result.type() == mtpc_messages_foundStickerSetsNotModified) { @@ -878,7 +907,9 @@ QRect StickersListWidget::stickerRect(int section, int sel) { void StickersListWidget::paintEvent(QPaintEvent *e) { Painter p(this); auto clip = e->rect(); - p.fillRect(clip, st().bg); + if (st().bg->c.alpha() > 0) { + p.fillRect(clip, st().bg); + } paintStickers(p, clip); } @@ -892,6 +923,7 @@ void StickersListWidget::paintStickers(Painter &p, QRect clip) { toColumn = _columnCount - toColumn; } + _paintAsPremium = session().premium(); _pathGradient->startFrame(0, width(), width() / 2); auto &sets = shownSets(); @@ -1465,7 +1497,26 @@ void StickersListWidget::paintSticker( p.setOpacity(1.); } - if (premium) { + auto cornerPainted = false; + const auto corner = (set.id == Data::Stickers::RecentSetId) + ? &_cornerEmoji + : (set.id == SearchEmojiSectionSetId()) + ? &_filterStickersCornerEmoji + : nullptr; + if (corner && !corner->empty() && _paintAsPremium) { + Assert(index < corner->size()); + if (const auto emoji = (*corner)[index]) { + const auto size = Ui::Emoji::GetSizeNormal(); + const auto ratio = style::DevicePixelRatio(); + const auto radius = st::roundRadiusSmall; + const auto position = pos + + QPoint(_singleSize.width(), _singleSize.height()) + - QPoint(size / ratio + radius, size / ratio + radius); + Ui::Emoji::Draw(p, emoji, size, position.x(), position.y()); + cornerPainted = true; + } + } + if (!cornerPainted && premium) { _premiumMark->paint( p, lottieFrame, @@ -1640,7 +1691,7 @@ void StickersListWidget::showStickerSetBox(not_null document) { } base::unique_qptr StickersListWidget::fillContextMenu( - SendMenu::Type type) { + const SendMenu::Details &details) { auto selected = _selected; auto &sets = shownSets(); if (v::is_null(selected) || !v::is_null(_pressed)) { @@ -1659,7 +1710,7 @@ base::unique_qptr StickersListWidget::fillContextMenu( auto menu = base::make_unique_q(this, st().menu); const auto document = set.stickers[sticker->index].document; - const auto send = [=](Api::SendOptions options) { + const auto send = crl::guard(this, [=](Api::SendOptions options) { _chosen.fire({ .document = document, .options = options, @@ -1667,14 +1718,13 @@ base::unique_qptr StickersListWidget::fillContextMenu( ? Ui::MessageSendingAnimationFrom() : messageSentAnimationInfo(section, index, document), }); - }; + }); const auto icons = &st().icons; SendMenu::FillSendMenu( menu, - type, - SendMenu::DefaultSilentCallback(send), - SendMenu::DefaultScheduleCallback(_show, type, send), - SendMenu::DefaultWhenOnlineCallback(send), + _show, + details, + SendMenu::DefaultCallback(_show, send), icons); const auto show = _show; @@ -1948,6 +1998,11 @@ void StickersListWidget::setSection(Section section) { } clearHeavyData(); _section = section; + _recentShownCount = (section == Section::Search) + ? _filteredStickers.size() + : _mySets.empty() + ? 0 + : _mySets.front().stickers.size(); } void StickersListWidget::clearHeavyData() { @@ -1959,10 +2014,13 @@ void StickersListWidget::clearHeavyData() { void StickersListWidget::refreshStickers() { clearSelection(); - refreshMySets(); - refreshFeaturedSets(); - refreshSearchSets(); - + if (_isEffects) { + refreshEffects(); + } else { + refreshMySets(); + refreshFeaturedSets(); + refreshSearchSets(); + } resizeToWidth(width()); if (_footer) { @@ -1977,6 +2035,13 @@ void StickersListWidget::refreshStickers() { visibleTopBottomUpdated(getVisibleTop(), getVisibleBottom()); } +void StickersListWidget::refreshEffects() { + auto wasSets = base::take(_mySets); + _mySets.reserve(1); + refreshRecentStickers(false); + takeHeavyData(_mySets, wasSets); +} + void StickersListWidget::refreshMySets() { auto wasSets = base::take(_mySets); _favedStickersMap.clear(); @@ -2138,7 +2203,26 @@ void StickersListWidget::refreshRecent() { } } +auto StickersListWidget::collectCustomRecents() -> std::vector { + _custom.clear(); + _cornerEmoji.clear(); + auto result = std::vector(); + + result.reserve(_customRecentIds.size()); + for (const auto &descriptor : _customRecentIds) { + if (const auto document = descriptor.document; document->sticker()) { + result.push_back(Sticker{ document }); + _custom.push_back(false); + _cornerEmoji.push_back(Ui::Emoji::Find(descriptor.cornerEmoji)); + } + } + return result; +} + auto StickersListWidget::collectRecentStickers() -> std::vector { + if (_isEffects) { + return collectCustomRecents(); + } _custom.clear(); auto result = std::vector(); @@ -2202,6 +2286,9 @@ void StickersListWidget::refreshRecentStickers(bool performResize) { clearSelection(); auto recentPack = collectRecentStickers(); + if (_section == Section::Stickers) { + _recentShownCount = recentPack.size(); + } auto recentIt = std::find_if(_mySets.begin(), _mySets.end(), [](auto &set) { return set.id == Data::Stickers::RecentSetId; }); @@ -2212,7 +2299,9 @@ void StickersListWidget::refreshRecentStickers(bool performResize) { Data::Stickers::RecentSetId, nullptr, (SetFlag::Official | SetFlag::Special), - tr::lng_recent_stickers(tr::now), + (_isEffects + ? tr::lng_effect_stickers_title(tr::now) + : tr::lng_recent_stickers(tr::now)), shortName, recentPack.size(), externalLayout, @@ -2463,7 +2552,9 @@ void StickersListWidget::updateSelected() { } bool StickersListWidget::setHasTitle(const Set &set) const { - if (set.id == Data::Stickers::FavedSetId + if (_isEffects) { + return true; + } else if (set.id == Data::Stickers::FavedSetId || set.id == SearchEmojiSectionSetId()) { return false; } else if (set.id == Data::Stickers::RecentSetId) { @@ -2550,9 +2641,10 @@ void StickersListWidget::showStickerSet(uint64 setId) { const auto guard = gsl::finally([&] { _showingSetById = false; }); clearSelection(); - if (_search - && (!_searchQuery.isEmpty() || !_searchNextQuery.isEmpty())) { - _search->cancel(); + if (!_searchQuery.isEmpty() || !_searchNextQuery.isEmpty()) { + if (_search) { + _search->cancel(); + } cancelSetsSearch(); } @@ -2655,14 +2747,18 @@ void StickersListWidget::setupSearch() { ? TabbedSearchType::Greeting : TabbedSearchType::Stickers; _search = MakeSearch(this, st(), [=](std::vector &&query) { - auto set = base::flat_set(); - auto text = ranges::accumulate(query, QString(), []( + applySearchQuery(std::move(query)); + }, session, type); +} + +void StickersListWidget::applySearchQuery(std::vector &&query) { + auto set = base::flat_set(); + auto text = ranges::accumulate(query, QString(), []( QString a, QString b) { - return a.isEmpty() ? b : (a + ' ' + b); - }); - searchForSets(std::move(text), SearchEmoji(query, set)); - }, session, type); + return a.isEmpty() ? b : (a + ' ' + b); + }); + searchForSets(std::move(text), SearchEmoji(query, set)); } void StickersListWidget::displaySet(uint64 setId) { @@ -2737,6 +2833,32 @@ bool StickersListWidget::mySetsEmpty() const { return _mySets.empty(); } +void StickersListWidget::filterEffectsByEmoji( + const std::vector &emoji) { + _filteredStickers.clear(); + _filterStickersCornerEmoji.clear(); + if (_mySets.empty() + || _mySets.front().id != Data::Stickers::RecentSetId + || _mySets.front().stickers.empty()) { + return; + } + const auto &list = _mySets.front().stickers; + auto all = base::flat_set(); + for (const auto &one : emoji) { + all.emplace(one->original()); + } + const auto count = int(list.size()); + _filteredStickers.reserve(count); + _filterStickersCornerEmoji.reserve(count); + for (auto i = 0; i != count; ++i) { + Assert(i < _cornerEmoji.size()); + if (all.contains(_cornerEmoji[i])) { + _filteredStickers.push_back(list[i].document); + _filterStickersCornerEmoji.push_back(_cornerEmoji[i]); + } + } +} + StickersListWidget::~StickersListWidget() = default; object_ptr MakeConfirmRemoveSetBox( diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h index aff2cf287..f1a89b210 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h @@ -66,12 +66,19 @@ enum class StickersListMode { Masks, UserpicBuilder, ChatIntro, + MessageEffects, +}; + +struct StickerCustomRecentDescriptor { + not_null document; + QString cornerEmoji; }; struct StickersListDescriptor { std::shared_ptr show; StickersListMode mode = StickersListMode::Full; Fn paused; + std::vector customRecentList; const style::EmojiPan *st = nullptr; ComposeFeatures features; }; @@ -116,10 +123,13 @@ public: std::shared_ptr getLottieRenderer(); base::unique_qptr fillContextMenu( - SendMenu::Type type) override; + const SendMenu::Details &details) override; bool mySetsEmpty() const; + void applySearchQuery(std::vector &&query); + [[nodiscard]] rpl::producer recentShownCount() const; + ~StickersListWidget(); protected: @@ -239,8 +249,10 @@ private: bool setHasTitle(const Set &set) const; bool stickerHasDeleteButton(const Set &set, int index) const; - std::vector collectRecentStickers(); + [[nodiscard]] std::vector collectRecentStickers(); + [[nodiscard]] std::vector collectCustomRecents(); void refreshRecentStickers(bool resize = true); + void refreshEffects(); void refreshFavedStickers(); enum class GroupStickersPlace { Visible, @@ -252,12 +264,13 @@ private: void updateSelected(); void setSelected(OverState newSelected); void setPressed(OverState newPressed); - std::unique_ptr createButtonRipple(int section); - QPoint buttonRippleTopLeft(int section) const; + [[nodiscard]] std::unique_ptr createButtonRipple( + int section); + [[nodiscard]] QPoint buttonRippleTopLeft(int section) const; - std::vector &shownSets(); - const std::vector &shownSets() const; - int featuredRowHeight() const; + [[nodiscard]] std::vector &shownSets(); + [[nodiscard]] const std::vector &shownSets() const; + [[nodiscard]] int featuredRowHeight() const; void checkVisibleFeatured(int visibleTop, int visibleBottom); void readVisibleFeatured(int visibleTop, int visibleBottom); @@ -315,6 +328,7 @@ private: [[nodiscard]] const Data::StickersSetsOrder &defaultSetsOrder() const; [[nodiscard]] Data::StickersSetsOrder &defaultSetsOrderRef(); + void filterEffectsByEmoji(const std::vector &emoji); enum class AppendSkip { None, @@ -347,6 +361,7 @@ private: void fillLocalSearchRows(const QString &query); void fillCloudSearchRows(const std::vector &cloudSets); void addSearchRow(not_null set); + void toggleSearchLoading(bool loading); void showPreview(); @@ -364,14 +379,17 @@ private: std::unique_ptr _localSetsManager; ChannelData *_megagroupSet = nullptr; uint64 _megagroupSetIdRequested = 0; + std::vector _customRecentIds; std::vector _mySets; std::vector _officialSets; std::vector _searchSets; int _featuredSetsCount = 0; std::vector _custom; + std::vector _cornerEmoji; base::flat_set> _favedStickersMap; std::weak_ptr _lottieRenderer; + bool _paintAsPremium = false; bool _showingSetById = false; crl::time _lastScrolledAt = 0; crl::time _lastFullUpdatedAt = 0; @@ -381,6 +399,7 @@ private: Section _section = Section::Stickers; const bool _isMasks; + const bool _isEffects; base::Timer _updateItemsTimer; base::Timer _updateSetsTimer; @@ -419,6 +438,8 @@ private: std::unique_ptr _premiumMark; std::vector> _filteredStickers; + std::vector _filterStickersCornerEmoji; + rpl::variable _recentShownCount; std::map> _searchCache; std::vector> _searchIndex; base::Timer _searchRequestTimer; diff --git a/Telegram/SourceFiles/chat_helpers/tabbed_section.cpp b/Telegram/SourceFiles/chat_helpers/tabbed_section.cpp index 3759697e3..92f6aaa00 100644 --- a/Telegram/SourceFiles/chat_helpers/tabbed_section.cpp +++ b/Telegram/SourceFiles/chat_helpers/tabbed_section.cpp @@ -28,6 +28,9 @@ TabbedSection::TabbedSection( not_null controller) : Window::SectionWidget(parent, controller) , _selector(controller->tabbedSelector()) { + if (Ui::InFocusChain(_selector)) { + parent->window()->setFocus(); + } _selector->setParent(this); _selector->setRoundRadius(0); _selector->setGeometry(rect()); diff --git a/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp b/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp index b63ae6967..7fb8c327d 100644 --- a/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp +++ b/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp @@ -516,7 +516,8 @@ TabbedSelector::TabbedSelector( emoji()->showSet(setId); _showRequests.fire({}); }, lifetime()); - + } + if (hasEmojiTab()) { emoji()->refreshEmoji(); } //setAttribute(Qt::WA_AcceptTouchEvents); @@ -1297,8 +1298,8 @@ void TabbedSelector::scrollToY(int y) { } } -void TabbedSelector::showMenuWithType(SendMenu::Type type) { - _menu = currentTab()->widget()->fillContextMenu(type); +void TabbedSelector::showMenuWithDetails(SendMenu::Details details) { + _menu = currentTab()->widget()->fillContextMenu(details); if (_menu && !_menu->empty()) { _menu->popup(QCursor::pos()); } @@ -1460,9 +1461,7 @@ int TabbedSelector::Inner::resizeGetHeight(int newWidth) { } int TabbedSelector::Inner::minimalHeight() const { - return (_minimalHeight > 0) - ? _minimalHeight - : defaultMinimalHeight(); + return _minimalHeight.value_or(defaultMinimalHeight()); } int TabbedSelector::Inner::defaultMinimalHeight() const { diff --git a/Telegram/SourceFiles/chat_helpers/tabbed_selector.h b/Telegram/SourceFiles/chat_helpers/tabbed_selector.h index b9b03b33b..b5355dafc 100644 --- a/Telegram/SourceFiles/chat_helpers/tabbed_selector.h +++ b/Telegram/SourceFiles/chat_helpers/tabbed_selector.h @@ -36,7 +36,7 @@ class TabbedSearch; } // namespace Ui namespace SendMenu { -enum class Type; +struct Details; } // namespace SendMenu namespace style { @@ -178,7 +178,7 @@ public: _beforeHidingCallback = std::move(callback); } - void showMenuWithType(SendMenu::Type type); + void showMenuWithDetails(SendMenu::Details details); void setDropDown(bool dropDown); // Float player interface. @@ -380,7 +380,7 @@ public: virtual void beforeHiding() { } [[nodiscard]] virtual base::unique_qptr fillContextMenu( - SendMenu::Type type) { + const SendMenu::Details &details) { return nullptr; } @@ -422,7 +422,7 @@ private: int _visibleTop = 0; int _visibleBottom = 0; - int _minimalHeight = 0; + std::optional _minimalHeight; rpl::event_stream _scrollToRequests; rpl::event_stream _disableScrollRequests; diff --git a/Telegram/SourceFiles/core/application.cpp b/Telegram/SourceFiles/core/application.cpp index 348a520d9..0109d946e 100644 --- a/Telegram/SourceFiles/core/application.cpp +++ b/Telegram/SourceFiles/core/application.cpp @@ -383,7 +383,7 @@ void Application::run() { } SetCrashAnnotationsGL(); - if (!Platform::IsMac() && Ui::GL::LastCrashCheckFailed()) { + if (Ui::GL::LastCrashCheckFailed()) { showOpenGLCrashNotification(); } @@ -427,14 +427,12 @@ void Application::checkWindowAccount(not_null window) { void Application::showOpenGLCrashNotification() { const auto enable = [=] { - Ui::GL::ForceDisable(false); Ui::GL::CrashCheckFinish(); settings().setDisableOpenGL(false); Local::writeSettings(); Restart(); }; const auto keepDisabled = [=](Fn close) { - Ui::GL::ForceDisable(true); Ui::GL::CrashCheckFinish(); settings().setDisableOpenGL(true); Local::writeSettings(); @@ -792,6 +790,7 @@ void Application::badMtprotoConfigurationError() { } void Application::startLocalStorage() { + Ui::GL::DetectLastCheckCrash(); Local::start(); _saveSettingsTimer.emplace([=] { saveSettings(); }); settings().saveDelayedRequests() | rpl::start_with_next([=] { diff --git a/Telegram/SourceFiles/core/click_handler_types.h b/Telegram/SourceFiles/core/click_handler_types.h index e064d1e2d..20879a0ab 100644 --- a/Telegram/SourceFiles/core/click_handler_types.h +++ b/Telegram/SourceFiles/core/click_handler_types.h @@ -52,6 +52,8 @@ struct ClickHandlerContext { }; Q_DECLARE_METATYPE(ClickHandlerContext); +class PhoneClickHandler; + class HiddenUrlClickHandler : public UrlClickHandler { public: HiddenUrlClickHandler(QString url) : UrlClickHandler(url, false) { diff --git a/Telegram/SourceFiles/core/core_settings.cpp b/Telegram/SourceFiles/core/core_settings.cpp index 5d00cb540..96fca9a26 100644 --- a/Telegram/SourceFiles/core/core_settings.cpp +++ b/Telegram/SourceFiles/core/core_settings.cpp @@ -917,10 +917,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) { _recentEmojiPreload = std::move(recentEmojiPreload); _emojiVariants = std::move(emojiVariants); _disableOpenGL = (disableOpenGL == 1); - if (!Platform::IsMac()) { - Ui::GL::ForceDisable(_disableOpenGL - || Ui::GL::LastCrashCheckFailed()); - } + Ui::GL::ForceDisable(_disableOpenGL); _groupCallNoiseSuppression = (groupCallNoiseSuppression == 1); const auto uncheckedWorkMode = static_cast(workMode); switch (uncheckedWorkMode) { diff --git a/Telegram/SourceFiles/core/file_utilities.cpp b/Telegram/SourceFiles/core/file_utilities.cpp index 1de1e028f..21d912f63 100644 --- a/Telegram/SourceFiles/core/file_utilities.cpp +++ b/Telegram/SourceFiles/core/file_utilities.cpp @@ -7,10 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "core/file_utilities.h" -#include "boxes/abstract_box.h" #include "storage/localstorage.h" #include "storage/storage_account.h" -#include "base/platform/base_platform_info.h" #include "base/platform/base_platform_file_utilities.h" #include "platform/platform_file_utilities.h" #include "core/application.h" @@ -159,10 +157,6 @@ void Launch(const QString &filepath) { void ShowInFolder(const QString &filepath) { crl::on_main([=] { Ui::PreventDelayedActivation(); - if (Platform::IsX11()) { - // Hide mediaview to make other apps visible. - Core::App().hideMediaView(); - } base::Platform::ShowInFolder(filepath); }); } diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp index 37ab0fe91..815bb7d6d 100644 --- a/Telegram/SourceFiles/core/local_url_handlers.cpp +++ b/Telegram/SourceFiles/core/local_url_handlers.cpp @@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/background_preview_box.h" #include "ui/boxes/confirm_box.h" #include "ui/boxes/edit_birthday_box.h" +#include "payments/payments_non_panel_process.h" #include "boxes/share_box.h" #include "boxes/connection_box.h" #include "boxes/edit_privacy_box.h" @@ -353,6 +354,7 @@ bool ApplySocksProxy( match->captured(1), qthelp::UrlParamNameTransform::ToLower); ProxiesBoxController::ShowApplyConfirmation( + controller, MTP::ProxyData::Type::Socks5, params); if (controller) { @@ -369,6 +371,7 @@ bool ApplyMtprotoProxy( match->captured(1), qthelp::UrlParamNameTransform::ToLower); ProxiesBoxController::ShowApplyConfirmation( + controller, MTP::ProxyData::Type::Mtproto, params); if (controller) { @@ -1096,7 +1099,8 @@ bool ResolveInvoice( Payments::CheckoutProcess::Start( &controller->session(), slug, - crl::guard(window, [=](auto) { window->activate(); })); + crl::guard(window, [=](auto) { window->activate(); }), + Payments::ProcessNonPanelPaymentFormFactory(controller)); return true; } diff --git a/Telegram/SourceFiles/core/phone_click_handler.cpp b/Telegram/SourceFiles/core/phone_click_handler.cpp new file mode 100644 index 000000000..19d15b78a --- /dev/null +++ b/Telegram/SourceFiles/core/phone_click_handler.cpp @@ -0,0 +1,335 @@ +/* +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 "core/phone_click_handler.h" + +#include "core/click_handler_types.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "info/profile/info_profile_values.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "mainwidget.h" +#include "mtproto/sender.h" +#include "ui/effects/ripple_animation.h" +#include "ui/painter.h" +#include "ui/rect.h" +#include "ui/widgets/menu/menu_item_base.h" +#include "ui/widgets/popup_menu.h" +#include "window/window_controller.h" +#include "window/window_session_controller.h" +#include "styles/style_calls.h" +#include "styles/style_chat.h" // popupMenuExpandedSeparator. +#include "styles/style_menu_icons.h" + +namespace { + +[[nodiscard]] QString Trim(QString text) { + return text + .replace('+', QString()) + .replace(' ', QString()) + .replace('-', QString()); +} + +class ResolvePhoneAction final : public Ui::Menu::ItemBase { +public: + ResolvePhoneAction( + not_null parent, + const style::Menu &st, + const QString &phone, + not_null controller); + + bool isEnabled() const override; + not_null action() const override; + + void handleKeyPress(not_null e) override; + +protected: + QPoint prepareRippleStartPosition() const override; + QImage prepareRippleMask() const override; + + int contentHeight() const override; + +private: + void prepare(); + void paint(Painter &p); + + const not_null _dummyAction; + const style::Menu &_st; + rpl::variable _peer; + rpl::variable _loaded; + Ui::PeerUserpicView _userpicView; + + MTP::Sender _api; + + Ui::Text::String _above; + Ui::Text::String _below; + int _aboveWidth = 0; + int _belowWidth = 0; + const int _height = 0; + +}; + +ResolvePhoneAction::ResolvePhoneAction( + not_null parent, + const style::Menu &st, + const QString &phone, + not_null controller) +: ItemBase(parent, st) +, _dummyAction(new QAction(parent)) +, _st(st) +, _api(&controller->session().mtp()) +, _height(rect::m::sum::v(st::groupCallJoinAsPadding) + + st::groupCallJoinAsPhotoSize) { + setAcceptBoth(true); + initResizeHook(parent->sizeValue()); + setClickedCallback([=] { + if (const auto peer = _peer.current()) { + controller->showPeerInfo(peer); + } + }); + + const auto formattedPhone = Trim(phone); + + const auto owner = &controller->session().data(); + + if (const auto peer = owner->userByPhone(formattedPhone)) { + _peer = peer; + _loaded.force_assign(true); + } else { + _api.request(MTPcontacts_ResolvePhone( + MTP_string(phone) + )).done([=](const MTPcontacts_ResolvedPeer &result) { + result.match([&](const MTPDcontacts_resolvedPeer &data) { + owner->processUsers(data.vusers()); + owner->processChats(data.vchats()); + if (const auto peerId = peerFromMTP(data.vpeer())) { + _peer = owner->peer(peerId); + } + _loaded.force_assign(true); + }); + }).fail([=](const MTP::Error &error) { + if (error.code() == 400) { + _peer.force_assign(nullptr); + _loaded.force_assign(true); + } + }).send(); + } + + paintRequest( + ) | rpl::start_with_next([=] { + Painter p(this); + paint(p); + }, lifetime()); + + enableMouseSelecting(); + prepare(); +} + +void ResolvePhoneAction::paint(Painter &p) { + const auto selected = isSelected() && _peer.current(); + const auto height = contentHeight(); + if (selected && _st.itemBgOver->c.alpha() < 255) { + p.fillRect(0, 0, width(), height, _st.itemBg); + } + p.fillRect(0, 0, width(), height, selected ? _st.itemBgOver : _st.itemBg); + if (isEnabled()) { + paintRipple(p, 0, 0); + } + + const auto &padding = st::groupCallJoinAsPadding; + const auto textLeft = padding.left() + + st::groupCallJoinAsPhotoSize + + padding.left(); + if (const auto peer = _peer.current()) { + peer->paintUserpic( + p, + _userpicView, + padding.left(), + padding.top(), + st::groupCallJoinAsPhotoSize); + p.setPen(selected ? _st.itemFgOver : _st.itemFg); + _above.drawLeftElided( + p, + textLeft, + st::groupCallJoinAsTextTop, + width() - textLeft - padding.right(), + width()); + p.setPen(selected ? _st.itemFgShortcutOver : _st.itemFgShortcut); + _below.drawLeftElided( + p, + textLeft, + st::groupCallJoinAsNameTop, + _belowWidth, + width()); + } else { + p.setPen(selected ? _st.itemFgShortcutOver : _st.itemFgShortcut); + const auto w = width() - padding.left() - padding.right(); + _below.draw(p, Ui::Text::PaintContext{ + .position = QPoint( + (width() - w) / 2, + (height - _below.countHeight(w)) / 2), + .outerWidth = w, + .availableWidth = w, + .align = style::al_center, + .elisionLines = 2, + }); + } +} + +void ResolvePhoneAction::prepare() { + rpl::combine( + tr::lng_context_view_profile(), + _peer.value( + ) | rpl::map([](PeerData *peer) { + return peer + ? Info::Profile::NameValue(peer) + : rpl::single(QString()); + }) | rpl::flatten_latest(), + tr::lng_menu_not_contact(), + _loaded.value( + ) | rpl::map([](bool loaded) { + return loaded + ? rpl::single(QString()) + : tr::lng_contacts_loading(); + }) | rpl::flatten_latest() + ) | rpl::start_with_next([=]( + QString text, + QString name, + QString no, + QString loading) { + const auto &padding = st::groupCallJoinAsPadding; + QWidget::setAttribute( + Qt::WA_TransparentForMouseEvents, + !_peer.current()); + const auto above = name; + const auto below = !loading.isEmpty() + ? loading + : name.isEmpty() + ? no + : text; + const auto options = kDefaultTextOptions; + const auto tempWidth = [&] { + _below.setMarkedText(_st.itemStyle, { text }, options); + return _below.maxWidth(); + }(); + const auto textLeft = padding.left() + + st::groupCallJoinAsPhotoSize + + padding.left(); + const auto w = std::clamp( + (textLeft + tempWidth + padding.right()), + _st.widthMin, + _st.widthMax); + if (!no.isEmpty()) { + _below = Ui::Text::String(w); + } + _above.setMarkedText(_st.itemStyle, { above }, options); + _below.setMarkedText(_st.itemStyle, { below }, options); + setMinWidth(w); + _aboveWidth = w - textLeft - padding.right(); + _belowWidth = w + - ((loading.isEmpty() && name.isEmpty()) ? 0 : textLeft) + - padding.right(); + update(); + }, lifetime()); +} + +bool ResolvePhoneAction::isEnabled() const { + return true; +} + +not_null ResolvePhoneAction::action() const { + return _dummyAction; +} + +QPoint ResolvePhoneAction::prepareRippleStartPosition() const { + return mapFromGlobal(QCursor::pos()); +} + +QImage ResolvePhoneAction::prepareRippleMask() const { + return Ui::RippleAnimation::RectMask(size()); +} + +int ResolvePhoneAction::contentHeight() const { + return _height; +} + +void ResolvePhoneAction::handleKeyPress(not_null e) { + if (!isSelected() || !_peer.current()) { + return; + } + const auto key = e->key(); + if (key == Qt::Key_Enter || key == Qt::Key_Return) { + setClicked(Ui::Menu::TriggeredSource::Keyboard); + } +} + +} // namespace + +PhoneClickHandler::PhoneClickHandler( + not_null session, + QString text) +: _session(session) +, _text(text) { +} + +void PhoneClickHandler::onClick(ClickContext context) const { + if (context.button != Qt::LeftButton) { + return; + } + const auto my = context.other.value(); + const auto controller = my.sessionWindow.get(); + const auto pos = QCursor::pos(); + if (!controller) { + return; + } + const auto menu = Ui::CreateChild( + controller->content(), + st::popupMenuWithIcons); + + const auto phone = _text; + +#if 0 + const auto maybeContact = [&]() -> PeerData* { + const auto &chats = controller->session().data().contactsList(); + for (const auto &row : chats->all()) { + if (const auto history = row->history()) { + if (const auto user = history->peer->asUser()) { + if (Trim(user->phone()) == Trim(phone)) { + return user; + } + } + } + } + return nullptr; + }(); +#endif + + menu->addAction(tr::lng_profile_copy_phone(tr::now), [=] { + TextUtilities::SetClipboardText( + TextForMimeData::Simple(phone.trimmed())); + }, &st::menuIconCopy); + + menu->addSeparator(&st::popupMenuExpandedSeparator.menu.separator); + + menu->addAction( + base::make_unique_q( + menu, + menu->st().menu, + phone, + controller)); + + menu->popup(pos); +} + +auto PhoneClickHandler::getTextEntity() const -> TextEntity { + return { EntityType::Phone }; +} + +QString PhoneClickHandler::tooltip() const { + return _text; +} diff --git a/Telegram/SourceFiles/core/phone_click_handler.h b/Telegram/SourceFiles/core/phone_click_handler.h new file mode 100644 index 000000000..bed2be2c5 --- /dev/null +++ b/Telegram/SourceFiles/core/phone_click_handler.h @@ -0,0 +1,30 @@ +/* +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 "ui/basic_click_handlers.h" + +namespace Main { +class Session; +} // namespace Main + +class PhoneClickHandler : public ClickHandler { +public: + PhoneClickHandler(not_null session, QString text); + + void onClick(ClickContext context) const override; + + TextEntity getTextEntity() const override; + + QString tooltip() const override; + +private: + const not_null _session; + QString _text; + +}; diff --git a/Telegram/SourceFiles/core/ui_integration.cpp b/Telegram/SourceFiles/core/ui_integration.cpp index 75a4d9e8a..d47c7feb4 100644 --- a/Telegram/SourceFiles/core/ui_integration.cpp +++ b/Telegram/SourceFiles/core/ui_integration.cpp @@ -23,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "platform/platform_specific.h" #include "boxes/url_auth_box.h" +#include "core/phone_click_handler.h" #include "main/main_account.h" #include "main/main_session.h" #include "main/main_app_config.h" @@ -221,6 +222,8 @@ std::shared_ptr UiIntegration::createLinkHandler( return std::make_shared(data.text, data.type); case EntityType::Pre: return std::make_shared(data.text, data.type); + case EntityType::Phone: + return std::make_shared(my->session, data.text); } return Integration::createLinkHandler(data, context); } diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h index 3169174b3..9f185d8b4 100644 --- a/Telegram/SourceFiles/core/version.h +++ b/Telegram/SourceFiles/core/version.h @@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D666}"_cs; constexpr auto AppNameOld = "AyuGram for Windows"_cs; constexpr auto AppName = "AyuGram Desktop"_cs; constexpr auto AppFile = "AyuGram"_cs; -constexpr auto AppVersion = 5000002; -constexpr auto AppVersionStr = "5.0.2"; +constexpr auto AppVersion = 5001002; +constexpr auto AppVersionStr = "5.1.2"; constexpr auto AppBetaVersion = false; constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION; diff --git a/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp b/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp index fbe30449b..39b211545 100644 --- a/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp +++ b/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp @@ -87,7 +87,9 @@ constexpr auto kRequestTimeLimit = 60 * crl::time(1000); MTPMessageReactions(), MTPVector(), MTP_int(data.vttl_period().value_or_empty()), - MTP_int(shortcutId)); + MTP_int(shortcutId), + MTP_long(data.veffect().value_or_empty()), + (data.vfactcheck() ? *data.vfactcheck() : MTPFactCheck())); }); } diff --git a/Telegram/SourceFiles/data/components/factchecks.cpp b/Telegram/SourceFiles/data/components/factchecks.cpp new file mode 100644 index 000000000..77251925e --- /dev/null +++ b/Telegram/SourceFiles/data/components/factchecks.cpp @@ -0,0 +1,218 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "data/components/factchecks.h" + +#include "api/api_text_entities.h" +#include "apiwrap.h" +#include "base/random.h" +#include "data/data_session.h" +#include "data/data_web_page.h" +#include "history/view/media/history_view_web_page.h" +#include "history/view/history_view_message.h" +#include "history/history.h" +#include "history/history_item.h" +#include "history/history_item_components.h" +#include "lang/lang_keys.h" +#include "main/main_app_config.h" +#include "main/main_session.h" +#include "ui/layers/show.h" + +namespace Data { +namespace { + +constexpr auto kRequestDelay = crl::time(1000); + +} // namespace + +Factchecks::Factchecks(not_null session) +: _session(session) +, _requestTimer([=] { request(); }) { +} + +void Factchecks::requestFor(not_null item) { + subscribeIfNotYet(); + + if (const auto factcheck = item->Get()) { + factcheck->requested = true; + } + if (!_requestTimer.isActive()) { + _requestTimer.callOnce(kRequestDelay); + } + const auto changed = !_pending.empty() + && (_pending.front()->history() != item->history()); + const auto added = _pending.emplace(item).second; + if (changed) { + request(); + } else if (added && _pending.size() == 1) { + _requestTimer.callOnce(kRequestDelay); + } +} + +void Factchecks::subscribeIfNotYet() { + if (_subscribed) { + return; + } + _subscribed = true; + + _session->data().itemRemoved( + ) | rpl::start_with_next([=](not_null item) { + _pending.remove(item); + const auto i = ranges::find(_requested, item.get()); + if (i != end(_requested)) { + *i = nullptr; + } + }, _lifetime); +} + +void Factchecks::request() { + _requestTimer.cancel(); + + if (!_requested.empty() || _pending.empty()) { + return; + } + _session->api().request(base::take(_requestId)).cancel(); + + auto ids = QVector(); + ids.reserve(_pending.size()); + const auto history = _pending.front()->history(); + for (auto i = begin(_pending); i != end(_pending);) { + const auto &item = *i; + if (item->history() == history) { + _requested.push_back(item); + ids.push_back(MTP_int(item->id.bare)); + i = _pending.erase(i); + } else { + ++i; + } + } + _requestId = _session->api().request(MTPmessages_GetFactCheck( + history->peer->input, + MTP_vector(std::move(ids)) + )).done([=](const MTPVector &result) { + _requestId = 0; + const auto &list = result.v; + auto index = 0; + for (const auto &item : base::take(_requested)) { + if (!item) { + } else if (index >= list.size()) { + item->setFactcheck({}); + } else { + item->setFactcheck(FromMTP(item, &list[index])); + } + ++index; + } + if (!_pending.empty()) { + request(); + } + }).fail([=] { + _requestId = 0; + for (const auto &item : base::take(_requested)) { + if (item) { + item->setFactcheck({}); + } + } + if (!_pending.empty()) { + request(); + } + }).send(); +} + +std::unique_ptr Factchecks::makeMedia( + not_null view, + not_null factcheck) { + if (!factcheck->page) { + factcheck->page = view->history()->owner().webpage( + base::RandomValue(), + tr::lng_factcheck_title(tr::now), + factcheck->data.text); + factcheck->page->type = WebPageType::Factcheck; + } + return std::make_unique( + view, + factcheck->page, + MediaWebPageFlags()); +} + +bool Factchecks::canEdit(not_null item) const { + if (!canEdit() + || !item->isRegular() + || !item->history()->peer->isBroadcast()) { + return false; + } + const auto media = item->media(); + if (!media || media->webpage() || media->photo()) { + return true; + } else if (const auto document = media->document()) { + return !document->isVideoMessage() && !document->sticker(); + } + return false; +} + +bool Factchecks::canEdit() const { + return _session->appConfig().get(u"can_edit_factcheck"_q, false); +} + +int Factchecks::lengthLimit() const { + return _session->appConfig().get(u"factcheck_length_limit"_q, 1024); +} + +void Factchecks::save( + FullMsgId itemId, + TextWithEntities text, + Fn done) { + const auto item = _session->data().message(itemId); + if (!item) { + return; + } else if (text.empty()) { + _session->api().request(MTPmessages_DeleteFactCheck( + item->history()->peer->input, + MTP_int(item->id.bare) + )).done([=](const MTPUpdates &result) { + _session->api().applyUpdates(result); + done(QString()); + }).fail([=](const MTP::Error &error) { + done(error.type()); + }).send(); + } else { + _session->api().request(MTPmessages_EditFactCheck( + item->history()->peer->input, + MTP_int(item->id.bare), + MTP_textWithEntities( + MTP_string(text.text), + Api::EntitiesToMTP( + _session, + text.entities, + Api::ConvertOption::SkipLocal)) + )).done([=](const MTPUpdates &result) { + _session->api().applyUpdates(result); + done(QString()); + }).fail([=](const MTP::Error &error) { + done(error.type()); + }).send(); + } +} + +void Factchecks::save( + FullMsgId itemId, + const TextWithEntities &was, + TextWithEntities text, + std::shared_ptr show) { + const auto wasEmpty = was.empty(); + const auto textEmpty = text.empty(); + save(itemId, std::move(text), [=](QString error) { + show->showToast(!error.isEmpty() + ? error + : textEmpty + ? tr::lng_factcheck_remove_done(tr::now) + : wasEmpty + ? tr::lng_factcheck_add_done(tr::now) + : tr::lng_factcheck_edit_done(tr::now)); + }); +} + +} // namespace Data diff --git a/Telegram/SourceFiles/data/components/factchecks.h b/Telegram/SourceFiles/data/components/factchecks.h new file mode 100644 index 000000000..a7eaca3d4 --- /dev/null +++ b/Telegram/SourceFiles/data/components/factchecks.h @@ -0,0 +1,70 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "base/timer.h" + +class HistoryItem; +struct HistoryMessageFactcheck; + +namespace HistoryView { +class Message; +class WebPage; +} // namespace HistoryView + +namespace Main { +class Session; +} // namespace Main + +namespace Ui { +class Show; +} // namespace Ui + +namespace Data { + +class Factchecks final { +public: + explicit Factchecks(not_null session); + + void requestFor(not_null item); + [[nodiscard]] std::unique_ptr makeMedia( + not_null view, + not_null factcheck); + + [[nodiscard]] bool canEdit(not_null item) const; + [[nodiscard]] int lengthLimit() const; + + void save( + FullMsgId itemId, + TextWithEntities text, + Fn done); + void save( + FullMsgId itemId, + const TextWithEntities &was, + TextWithEntities text, + std::shared_ptr show); + +private: + [[nodiscard]] bool canEdit() const; + + void subscribeIfNotYet(); + void request(); + + const not_null _session; + + base::Timer _requestTimer; + base::flat_set> _pending; + std::vector _requested; + mtpRequestId _requestId = 0; + bool _subscribed = false; + + rpl::lifetime _lifetime; + +}; + +} // namespace Data diff --git a/Telegram/SourceFiles/data/components/scheduled_messages.cpp b/Telegram/SourceFiles/data/components/scheduled_messages.cpp index 92e0da880..9e69dd35b 100644 --- a/Telegram/SourceFiles/data/components/scheduled_messages.cpp +++ b/Telegram/SourceFiles/data/components/scheduled_messages.cpp @@ -91,7 +91,9 @@ constexpr auto kRequestTimeLimit = 60 * crl::time(1000); MTPMessageReactions(), MTPVector(), MTP_int(data.vttl_period().value_or_empty()), - MTPint()); // quick_reply_shortcut_id + MTPint(), // quick_reply_shortcut_id + MTP_long(data.veffect().value_or_empty()), // effect + data.vfactcheck() ? *data.vfactcheck() : MTPFactCheck()); }); } @@ -227,6 +229,9 @@ void ScheduledMessages::sendNowSimpleMessage( : MTPDmessage::Flag(0)) | ((localFlags & MessageFlag::Outgoing) ? MTPDmessage::Flag::f_out + : MTPDmessage::Flag(0)) + | (local->effectId() + ? MTPDmessage::Flag::f_effect : MTPDmessage::Flag(0)); const auto views = 1; const auto forwards = 0; @@ -259,7 +264,9 @@ void ScheduledMessages::sendNowSimpleMessage( MTPMessageReactions(), MTPVector(), MTP_int(update.vttl_period().value_or_empty()), - MTPint()), // quick_reply_shortcut_id + MTPint(), // quick_reply_shortcut_id + MTP_long(local->effectId()), // effect + MTPFactCheck()), localFlags, NewMessageType::Unread); diff --git a/Telegram/SourceFiles/data/data_credits.h b/Telegram/SourceFiles/data/data_credits.h new file mode 100644 index 000000000..ddfd22a83 --- /dev/null +++ b/Telegram/SourceFiles/data/data_credits.h @@ -0,0 +1,51 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +namespace Data { + +struct CreditTopupOption final { + uint64 credits = 0; + QString product; + QString currency; + uint64 amount = 0; + bool extended = false; +}; + +using CreditTopupOptions = std::vector; + +struct CreditsHistoryEntry final { + using PhotoId = uint64; + enum class PeerType { + Peer, + AppStore, + PlayMarket, + Fragment, + Unsupported, + PremiumBot, + }; + QString id; + QString title; + QString description; + QDateTime date; + PhotoId photoId = 0; + uint64 credits = 0; + uint64 bareId = 0; + PeerType peerType; + bool refunded = false; +}; + +struct CreditsStatusSlice final { + using OffsetToken = QString; + std::vector list; + uint64 balance = 0; + bool allLoaded = false; + OffsetToken token; +}; + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_forum_topic.cpp b/Telegram/SourceFiles/data/data_forum_topic.cpp index 7b5391a19..7285c4c39 100644 --- a/Telegram/SourceFiles/data/data_forum_topic.cpp +++ b/Telegram/SourceFiles/data/data_forum_topic.cpp @@ -143,7 +143,7 @@ QImage ForumTopicIconFrame( return background; } -QImage ForumTopicGeneralIconFrame(int size, const style::color &color) { +QImage ForumTopicGeneralIconFrame(int size, const QColor &color) { const auto ratio = style::DevicePixelRatio(); auto svg = QSvgRenderer(ForumTopicIconPath(u"general"_q)); auto result = QImage( @@ -172,6 +172,62 @@ TextWithEntities ForumTopicIconWithTitle( : TextWithEntities{ title }; } +QString ForumGeneralIconTitle() { + return QChar(0) + u"general"_q; +} + +bool IsForumGeneralIconTitle(const QString &title) { + return !title.isEmpty() && !title[0].unicode(); +} + +int32 ForumGeneralIconColor(const QColor &color) { + return int32(uint32(color.red()) << 16 + | uint32(color.green()) << 8 + | uint32(color.blue()) + | (uint32(color.alpha() == 255 ? 0 : color.alpha()) << 24)); +} + +QColor ParseForumGeneralIconColor(int32 value) { + const auto alpha = uint32(value) >> 24; + return QColor( + (value >> 16) & 0xFF, + (value >> 8) & 0xFF, + value & 0xFF, + alpha ? alpha : 255); +} + +QString TopicIconEmojiEntity(TopicIconDescriptor descriptor) { + return IsForumGeneralIconTitle(descriptor.title) + ? u"topic_general:"_q + QString::number(uint32(descriptor.colorId)) + : (u"topic_icon:"_q + + QString::number(uint32(descriptor.colorId)) + + ' ' + + ExtractNonEmojiLetter(descriptor.title)); +} + +TopicIconDescriptor ParseTopicIconEmojiEntity(QStringView entity) { + if (!entity.startsWith(u"topic_")) { + return {}; + } + const auto general = u"topic_general:"_q; + const auto normal = u"topic_icon:"_q; + if (entity.startsWith(general)) { + return { + .title = ForumGeneralIconTitle(), + .colorId = int32(entity.mid(general.size()).toUInt()), + }; + } else if (entity.startsWith(normal)) { + const auto parts = entity.mid(normal.size()).split(' '); + if (parts.size() == 2) { + return { + .title = parts[1].toString(), + .colorId = int32(parts[0].toUInt()), + }; + } + } + return {}; +} + ForumTopic::ForumTopic(not_null forum, MsgId rootId) : Thread(&forum->history()->owner(), Type::ForumTopic) , _forum(forum) @@ -640,7 +696,7 @@ void ForumTopic::validateGeneralIcon( : context.selected ? st::dialogsTextFgOver : st::dialogsTextFg; - _defaultIcon = ForumTopicGeneralIconFrame(size, color); + _defaultIcon = ForumTopicGeneralIconFrame(size, color->c); _flags = (_flags & ~mask) | flags; } diff --git a/Telegram/SourceFiles/data/data_forum_topic.h b/Telegram/SourceFiles/data/data_forum_topic.h index 275f38fcb..06423e475 100644 --- a/Telegram/SourceFiles/data/data_forum_topic.h +++ b/Telegram/SourceFiles/data/data_forum_topic.h @@ -48,12 +48,33 @@ class Forum; const style::ForumTopicIcon &st); [[nodiscard]] QImage ForumTopicGeneralIconFrame( int size, - const style::color &color); + const QColor &color); [[nodiscard]] TextWithEntities ForumTopicIconWithTitle( MsgId rootId, DocumentId iconId, const QString &title); +[[nodiscard]] QString ForumGeneralIconTitle(); +[[nodiscard]] bool IsForumGeneralIconTitle(const QString &title); +[[nodiscard]] int32 ForumGeneralIconColor(const QColor &color); +[[nodiscard]] QColor ParseForumGeneralIconColor(int32 value); + +struct TopicIconDescriptor { + QString title; + int32 colorId = 0; + + [[nodiscard]] bool empty() const { + return !colorId && title.isEmpty(); + } + explicit operator bool() const { + return !empty(); + } +}; + +[[nodiscard]] QString TopicIconEmojiEntity(TopicIconDescriptor descriptor); +[[nodiscard]] TopicIconDescriptor ParseTopicIconEmojiEntity( + QStringView entity); + class ForumTopic final : public Thread { public: static constexpr auto kGeneralId = 1; diff --git a/Telegram/SourceFiles/data/data_history_messages.cpp b/Telegram/SourceFiles/data/data_history_messages.cpp new file mode 100644 index 000000000..66e888e28 --- /dev/null +++ b/Telegram/SourceFiles/data/data_history_messages.cpp @@ -0,0 +1,219 @@ +/* +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_history_messages.h" + +#include "apiwrap.h" +#include "data/data_chat.h" +#include "data/data_peer.h" +#include "data/data_session.h" +#include "data/data_sparse_ids.h" +#include "history/history.h" +#include "main/main_session.h" + +namespace Data { + +void HistoryMessages::addNew(MsgId messageId) { + _chat.addNew(messageId); +} + +void HistoryMessages::addExisting(MsgId messageId, MsgRange noSkipRange) { + _chat.addExisting(messageId, noSkipRange); +} + +void HistoryMessages::addSlice( + std::vector &&messageIds, + MsgRange noSkipRange, + std::optional count) { + _chat.addSlice(std::move(messageIds), noSkipRange, count); +} + +void HistoryMessages::removeOne(MsgId messageId) { + _chat.removeOne(messageId); + _oneRemoved.fire_copy(messageId); +} + +void HistoryMessages::removeAll() { + _chat.removeAll(); + _allRemoved.fire({}); +} + +void HistoryMessages::invalidateBottom() { + _chat.invalidateBottom(); + _bottomInvalidated.fire({}); +} + +Storage::SparseIdsListResult HistoryMessages::snapshot( + const Storage::SparseIdsListQuery &query) const { + return _chat.snapshot(query); +} + +auto HistoryMessages::sliceUpdated() const +-> rpl::producer { + return _chat.sliceUpdated(); +} + +rpl::producer HistoryMessages::oneRemoved() const { + return _oneRemoved.events(); +} + +rpl::producer<> HistoryMessages::allRemoved() const { + return _allRemoved.events(); +} + +rpl::producer<> HistoryMessages::bottomInvalidated() const { + return _bottomInvalidated.events(); +} + +rpl::producer HistoryViewer( + not_null history, + MsgId aroundId, + int limitBefore, + int limitAfter) { + Expects(IsServerMsgId(aroundId) || (aroundId == 0)); + Expects((aroundId != 0) || (limitBefore == 0 && limitAfter == 0)); + + return [=](auto consumer) { + auto lifetime = rpl::lifetime(); + + const auto messages = &history->messages(); + + auto builder = lifetime.make_state( + aroundId, + limitBefore, + limitAfter); + using RequestAroundInfo = SparseIdsSliceBuilder::AroundData; + builder->insufficientAround( + ) | rpl::start_with_next([=](const RequestAroundInfo &info) { + if (!info.aroundId) { + // Ignore messages-count-only requests, because we perform + // them with non-zero limit of messages and end up adding + // a broken slice with several last messages from the chat + // with a non-skip range starting at zero. + return; + } + history->session().api().requestHistory( + history, + info.aroundId, + info.direction); + }, lifetime); + + auto pushNextSnapshot = [=] { + consumer.put_next(builder->snapshot()); + }; + + using SliceUpdate = Storage::SparseIdsSliceUpdate; + messages->sliceUpdated( + ) | rpl::filter([=](const SliceUpdate &update) { + return builder->applyUpdate(update); + }) | rpl::start_with_next(pushNextSnapshot, lifetime); + + messages->oneRemoved( + ) | rpl::filter([=](MsgId messageId) { + return builder->removeOne(messageId); + }) | rpl::start_with_next(pushNextSnapshot, lifetime); + + messages->allRemoved( + ) | rpl::filter([=] { + return builder->removeAll(); + }) | rpl::start_with_next(pushNextSnapshot, lifetime); + + messages->bottomInvalidated( + ) | rpl::filter([=] { + return builder->invalidateBottom(); + }) | rpl::start_with_next(pushNextSnapshot, lifetime); + + const auto snapshot = messages->snapshot({ + aroundId, + limitBefore, + limitAfter, + }); + if (snapshot.count || !snapshot.messageIds.empty()) { + if (builder->applyInitial(snapshot)) { + pushNextSnapshot(); + } + } + builder->checkInsufficient(); + + return lifetime; + }; +} + +rpl::producer HistoryMergedViewer( + not_null history, + /*Universal*/MsgId universalAroundId, + int limitBefore, + int limitAfter) { + const auto migrateFrom = history->peer->migrateFrom(); + auto createSimpleViewer = [=]( + PeerId peerId, + MsgId topicRootId, + SparseIdsSlice::Key simpleKey, + int limitBefore, + int limitAfter) { + const auto chosen = (history->peer->id == peerId) + ? history + : history->owner().history(peerId); + return HistoryViewer(chosen, simpleKey, limitBefore, limitAfter); + }; + const auto peerId = history->peer->id; + const auto topicRootId = MsgId(); + const auto migratedPeerId = migrateFrom ? migrateFrom->id : PeerId(0); + using Key = SparseIdsMergedSlice::Key; + return SparseIdsMergedSlice::CreateViewer( + Key(peerId, topicRootId, migratedPeerId, universalAroundId), + limitBefore, + limitAfter, + std::move(createSimpleViewer)); +} + +rpl::producer HistoryMessagesViewer( + not_null history, + MessagePosition aroundId, + int limitBefore, + int limitAfter) { + const auto computeUnreadAroundId = [&] { + if (const auto migrated = history->migrateFrom()) { + if (const auto around = migrated->loadAroundId()) { + return MsgId(around - ServerMaxMsgId); + } + } + if (const auto around = history->loadAroundId()) { + return around; + } + return MsgId(ServerMaxMsgId - 1); + }; + const auto messageId = (aroundId.fullId.msg == ShowAtUnreadMsgId) + ? computeUnreadAroundId() + : (aroundId.fullId.msg == ShowAtTheEndMsgId) + ? (ServerMaxMsgId - 1) + : (aroundId.fullId.peer == history->peer->id) + ? aroundId.fullId.msg + : (aroundId.fullId.msg - ServerMaxMsgId); + return HistoryMergedViewer( + history, + messageId, + limitBefore, + limitAfter + ) | rpl::map([=](SparseIdsMergedSlice &&slice) { + auto result = Data::MessagesSlice(); + result.fullCount = slice.fullCount(); + result.skippedAfter = slice.skippedAfter(); + result.skippedBefore = slice.skippedBefore(); + const auto count = slice.size(); + result.ids.reserve(count); + if (const auto msgId = slice.nearest(messageId)) { + result.nearestToAround = *msgId; + } + for (auto i = 0; i != count; ++i) { + result.ids.push_back(slice[i]); + } + return result; + }); +} + +} // namespace Data \ No newline at end of file diff --git a/Telegram/SourceFiles/data/data_history_messages.h b/Telegram/SourceFiles/data/data_history_messages.h new file mode 100644 index 000000000..2b68462fb --- /dev/null +++ b/Telegram/SourceFiles/data/data_history_messages.h @@ -0,0 +1,67 @@ +/* +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 "storage/storage_sparse_ids_list.h" + +class History; +class SparseIdsSlice; +class SparseIdsMergedSlice; + +namespace Data { + +struct MessagesSlice; +struct MessagePosition; + +class HistoryMessages final { +public: + void addNew(MsgId messageId); + void addExisting(MsgId messageId, MsgRange noSkipRange); + void addSlice( + std::vector &&messageIds, + MsgRange noSkipRange, + std::optional count); + void removeOne(MsgId messageId); + void removeAll(); + void invalidateBottom(); + + [[nodiscard]] Storage::SparseIdsListResult snapshot( + const Storage::SparseIdsListQuery &query) const; + [[nodiscard]] auto sliceUpdated() const + -> rpl::producer; + [[nodiscard]] rpl::producer oneRemoved() const; + [[nodiscard]] rpl::producer<> allRemoved() const; + [[nodiscard]] rpl::producer<> bottomInvalidated() const; + +private: + Storage::SparseIdsList _chat; + rpl::event_stream _oneRemoved; + rpl::event_stream<> _allRemoved; + rpl::event_stream<> _bottomInvalidated; + +}; + +[[nodiscard]] rpl::producer HistoryViewer( + not_null history, + MsgId aroundId, + int limitBefore, + int limitAfter); + +[[nodiscard]] rpl::producer HistoryMergedViewer( + not_null history, + /*Universal*/MsgId universalAroundId, + int limitBefore, + int limitAfter); + +[[nodiscard]] rpl::producer HistoryMessagesViewer( + not_null history, + MessagePosition aroundId, + int limitBefore, + int limitAfter); + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp index 3a8dc1943..feb8ad16d 100644 --- a/Telegram/SourceFiles/data/data_media_types.cpp +++ b/Telegram/SourceFiles/data/data_media_types.cpp @@ -1224,14 +1224,17 @@ MediaContact::MediaContact( UserId userId, const QString &firstName, const QString &lastName, - const QString &phoneNumber) -: Media(parent) { + const QString &phoneNumber, + const SharedContact::VcardItems &vcardItems) +: Media(parent) +, _contact(SharedContact{ + .userId = userId, + .firstName = firstName, + .lastName = lastName, + .phoneNumber = phoneNumber, + .vcardItems = vcardItems, +}) { parent->history()->owner().registerContactItem(userId, parent); - - _contact.userId = userId; - _contact.firstName = firstName; - _contact.lastName = lastName; - _contact.phoneNumber = phoneNumber; } MediaContact::~MediaContact() { @@ -1246,7 +1249,8 @@ std::unique_ptr MediaContact::clone(not_null parent) { _contact.userId, _contact.firstName, _contact.lastName, - _contact.phoneNumber); + _contact.phoneNumber, + _contact.vcardItems); } const SharedContact *MediaContact::sharedContact() const { @@ -1301,12 +1305,7 @@ std::unique_ptr MediaContact::createView( not_null message, not_null realParent, HistoryView::Element *replacing) { - return std::make_unique( - message, - _contact.userId, - _contact.firstName, - _contact.lastName, - _contact.phoneNumber); + return std::make_unique(message, _contact); } MediaLocation::MediaLocation( diff --git a/Telegram/SourceFiles/data/data_media_types.h b/Telegram/SourceFiles/data/data_media_types.h index c486799cd..d9f0773c0 100644 --- a/Telegram/SourceFiles/data/data_media_types.h +++ b/Telegram/SourceFiles/data/data_media_types.h @@ -47,11 +47,30 @@ enum class CallFinishReason : char { Hangup, }; -struct SharedContact { +struct SharedContact final { UserId userId = 0; QString firstName; QString lastName; QString phoneNumber; + + enum class VcardItemType { + Phone, + PhoneMain, + PhoneHome, + PhoneMobile, + PhoneWork, + PhoneOther, + Email, + Address, + Url, + Note, + Birthday, + Organization, + Name, + }; + + using VcardItems = base::flat_map; + VcardItems vcardItems; }; struct Call { @@ -308,7 +327,8 @@ public: UserId userId, const QString &firstName, const QString &lastName, - const QString &phoneNumber); + const QString &phoneNumber, + const SharedContact::VcardItems &vcardItems); ~MediaContact(); std::unique_ptr clone(not_null parent) override; diff --git a/Telegram/SourceFiles/data/data_message_reactions.cpp b/Telegram/SourceFiles/data/data_message_reactions.cpp index 9e6d27579..e5a0f73b6 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.cpp +++ b/Telegram/SourceFiles/data/data_message_reactions.cpp @@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_changes.h" #include "data/data_document.h" #include "data/data_document_media.h" +#include "data/data_file_origin.h" #include "data/data_peer_values.h" #include "data/data_saved_sublist.h" #include "data/stickers/data_custom_emoji.h" @@ -253,6 +254,9 @@ PossibleItemReactions::PossibleItemReactions( : recent(other.recent | ranges::views::transform([](const auto &value) { return *value; }) | ranges::to_vector) +, stickers(other.stickers | ranges::views::transform([](const auto &value) { + return *value; +}) | ranges::to_vector) , customAllowed(other.customAllowed) , tags(other.tags){ } @@ -269,6 +273,7 @@ Reactions::Reactions(not_null owner) kRefreshFullListEach ) | rpl::start_with_next([=] { refreshDefault(); + requestEffects(); }, _lifetime); _owner->session().changes().messageUpdates( @@ -348,6 +353,12 @@ void Reactions::refreshTags() { requestTags(); } +void Reactions::refreshEffects() { + if (_effects.empty()) { + requestEffects(); + } +} + const std::vector &Reactions::list(Type type) const { switch (type) { case Type::Active: return _active; @@ -357,6 +368,7 @@ const std::vector &Reactions::list(Type type) const { case Type::MyTags: return _myTags.find((SavedSublist*)nullptr)->second.tags; case Type::Tags: return _tags; + case Type::Effects: return _effects; } Unexpected("Type in Reactions::list."); } @@ -557,25 +569,62 @@ rpl::producer Reactions::myTagRenamed() const { return _myTagRenamed.events(); } +rpl::producer<> Reactions::effectsUpdates() const { + return _effectsUpdated.events(); +} + +void Reactions::preloadReactionImageFor(const ReactionId &emoji) { + if (!emoji.emoji().isEmpty()) { + preloadImageFor(emoji); + } +} + +void Reactions::preloadEffectImageFor(EffectId id) { + if (id != kFakeEffectId) { + preloadImageFor({ DocumentId(id) }); + } +} + void Reactions::preloadImageFor(const ReactionId &id) { - if (_images.contains(id) || id.emoji().isEmpty()) { + if (_images.contains(id)) { return; } auto &set = _images.emplace(id).first->second; - const auto i = ranges::find(_available, id, &Reaction::id); - const auto document = (i == end(_available)) + set.effect = (id.custom() != 0); + auto &list = set.effect ? _effects : _available; + const auto i = ranges::find(list, id, &Reaction::id); + const auto document = (i == end(list)) ? nullptr : i->centerIcon ? i->centerIcon : i->selectAnimation.get(); - if (document) { - loadImage(set, document, !i->centerIcon); - } else if (!_waitingForList) { - _waitingForList = true; + if (document || (set.effect && i != end(list))) { + if (!set.effect || i->centerIcon) { + loadImage(set, document, !i->centerIcon); + } else { + generateImage(set, i->title); + } + if (set.effect) { + preloadEffect(*i); + } + } else if (set.effect && !_waitingForEffects) { + _waitingForEffects = true; + refreshEffects(); + } else if (!set.effect && !_waitingForReactions) { + _waitingForReactions = true; refreshDefault(); } } +void Reactions::preloadEffect(const Reaction &effect) { + if (effect.aroundAnimation) { + effect.aroundAnimation->createMediaView()->checkStickerLarge(); + } else { + const auto premium = effect.selectAnimation; + premium->loadVideoThumbnail(premium->stickerSetOrigin()); + } +} + void Reactions::preloadAnimationsFor(const ReactionId &id) { const auto custom = id.custom(); const auto document = custom ? _owner->document(custom).get() : nullptr; @@ -602,14 +651,28 @@ void Reactions::preloadAnimationsFor(const ReactionId &id) { preload(i->aroundAnimation); } -QImage Reactions::resolveImageFor( - const ReactionId &emoji, - ImageSize size) { - const auto i = _images.find(emoji); +QImage Reactions::resolveReactionImageFor(const ReactionId &emoji) { + Expects(!emoji.custom()); + + return resolveImageFor(emoji); +} + +QImage Reactions::resolveEffectImageFor(EffectId id) { + return (id == kFakeEffectId) + ? QImage() + : resolveImageFor({ DocumentId(id) }); +} + +QImage Reactions::resolveImageFor(const ReactionId &id) { + auto i = _images.find(id); if (i == end(_images)) { - preloadImageFor(emoji); + preloadImageFor(id); + i = _images.find(id); + Assert(i != end(_images)); } - auto &set = (i != end(_images)) ? i->second : _images[emoji]; + auto &set = i->second; + set.effect = (id.custom() != 0); + const auto resolve = [&](QImage &image, int size) { const auto factor = style::DevicePixelRatio(); const auto frameSize = set.fromSelectAnimation @@ -639,21 +702,18 @@ QImage Reactions::resolveImageFor( } image.setDevicePixelRatio(factor); }; - if (set.bottomInfo.isNull() && set.icon) { - resolve(set.bottomInfo, st::reactionInfoImage); - resolve(set.inlineList, st::reactionInlineImage); + if (set.image.isNull() && set.icon) { + resolve( + set.image, + set.effect ? st::effectInfoImage : st::reactionInlineImage); crl::async([icon = std::move(set.icon)]{}); } - switch (size) { - case ImageSize::BottomInfo: return set.bottomInfo; - case ImageSize::InlineList: return set.inlineList; - } - Unexpected("ImageSize in Reactions::resolveImageFor."); + return set.image; } -void Reactions::resolveImages() { +void Reactions::resolveReactionImages() { for (auto &[id, set] : _images) { - if (!set.bottomInfo.isNull() || set.icon || set.media) { + if (set.effect || !set.image.isNull() || set.icon || set.media) { continue; } const auto i = ranges::find(_available, id, &Reaction::id); @@ -671,14 +731,41 @@ void Reactions::resolveImages() { } } +void Reactions::resolveEffectImages() { + for (auto &[id, set] : _images) { + if (!set.effect || !set.image.isNull() || set.icon || set.media) { + continue; + } + const auto i = ranges::find(_effects, id, &Reaction::id); + const auto document = (i == end(_effects)) + ? nullptr + : i->centerIcon + ? i->centerIcon + : nullptr; + if (document) { + loadImage(set, document, false); + } else if (i != end(_effects)) { + generateImage(set, i->title); + } else { + LOG(("API Error: Effect '%1' not found!" + ).arg(ReactionIdToLog(id))); + } + if (i != end(_effects)) { + preloadEffect(*i); + } + } +} + void Reactions::loadImage( ImageSet &set, not_null document, bool fromSelectAnimation) { - if (!set.bottomInfo.isNull() || set.icon) { + if (!set.image.isNull() || set.icon) { return; } else if (!set.media) { - set.fromSelectAnimation = fromSelectAnimation; + if (!set.effect) { + set.fromSelectAnimation = fromSelectAnimation; + } set.media = document->createMediaView(); set.media->checkStickerLarge(); } @@ -692,6 +779,26 @@ void Reactions::loadImage( } } +void Reactions::generateImage(ImageSet &set, const QString &emoji) { + Expects(set.effect); + + const auto e = Ui::Emoji::Find(emoji); + Assert(e != nullptr); + + const auto large = Ui::Emoji::GetSizeLarge(); + const auto factor = style::DevicePixelRatio(); + auto image = QImage(large, large, QImage::Format_ARGB32_Premultiplied); + image.setDevicePixelRatio(factor); + image.fill(Qt::transparent); + { + QPainter p(&image); + Ui::Emoji::Draw(p, e, large, 0, 0); + } + const auto size = st::effectInfoImage; + set.image = image.scaled(size * factor, size * factor); + set.image.setDevicePixelRatio(factor); +} + void Reactions::setAnimatedIcon(ImageSet &set) { const auto size = style::ConvertScale(kSizeForDownscale); set.icon = Ui::MakeAnimatedIcon({ @@ -845,6 +952,25 @@ void Reactions::requestTags() { } +void Reactions::requestEffects() { + if (_effectsRequestId) { + return; + } + auto &api = _owner->session().api(); + _effectsRequestId = api.request(MTPmessages_GetAvailableEffects( + MTP_int(_effectsHash) + )).done([=](const MTPmessages_AvailableEffects &result) { + _effectsRequestId = 0; + result.match([&](const MTPDmessages_availableEffects &data) { + updateEffects(data); + }, [&](const MTPDmessages_availableEffectsNotModified &) { + }); + }).fail([=] { + _effectsRequestId = 0; + _effectsHash = 0; + }).send(); +} + void Reactions::updateTop(const MTPDmessages_reactions &data) { _topHash = data.vhash().v; _topIds = ListFromMTP(data); @@ -886,9 +1012,9 @@ void Reactions::updateDefault(const MTPDmessages_availableReactions &data) { } } } - if (_waitingForList) { - _waitingForList = false; - resolveImages(); + if (_waitingForReactions) { + _waitingForReactions = false; + resolveReactionImages(); } defaultUpdated(); } @@ -944,6 +1070,32 @@ void Reactions::updateTags(const MTPDmessages_reactions &data) { _tagsUpdated.fire({}); } +void Reactions::updateEffects(const MTPDmessages_availableEffects &data) { + _effectsHash = data.vhash().v; + + const auto &list = data.veffects().v; + const auto toCache = [&](DocumentData *document) { + if (document) { + _iconsCache.emplace(document, document->createMediaView()); + } + }; + for (const auto &document : data.vdocuments().v) { + toCache(_owner->processDocument(document)); + } + _effects.clear(); + _effects.reserve(list.size()); + for (const auto &effect : list) { + if (const auto parsed = parse(effect)) { + _effects.push_back(*parsed); + } + } + if (_waitingForEffects) { + _waitingForEffects = false; + resolveEffectImages(); + } + effectsUpdated(); +} + void Reactions::recentUpdated() { _topRefreshTimer.callOnce(kTopRequestDelay); _recentUpdated.fire({}); @@ -957,6 +1109,7 @@ void Reactions::defaultUpdated() { } refreshMyTags(); refreshTags(); + refreshEffects(); _defaultUpdated.fire({}); } @@ -974,6 +1127,10 @@ void Reactions::tagsUpdated() { _tagsUpdated.fire({}); } +void Reactions::effectsUpdated() { + _effectsUpdated.fire({}); +} + not_null Reactions::resolveListener() { return static_cast(this); } @@ -1116,35 +1273,74 @@ void Reactions::resolve(const ReactionId &id) { } std::optional Reactions::parse(const MTPAvailableReaction &entry) { - return entry.match([&](const MTPDavailableReaction &data) { - const auto emoji = qs(data.vreaction()); - const auto known = (Ui::Emoji::Find(emoji) != nullptr); - if (!known) { - LOG(("API Error: Unknown emoji in reactions: %1").arg(emoji)); - } - return known - ? std::make_optional(Reaction{ - .id = ReactionId{ emoji }, - .title = qs(data.vtitle()), - //.staticIcon = _owner->processDocument(data.vstatic_icon()), - .appearAnimation = _owner->processDocument( - data.vappear_animation()), - .selectAnimation = _owner->processDocument( - data.vselect_animation()), - //.activateAnimation = _owner->processDocument( - // data.vactivate_animation()), - //.activateEffects = _owner->processDocument( - // data.veffect_animation()), - .centerIcon = (data.vcenter_icon() - ? _owner->processDocument(*data.vcenter_icon()).get() - : nullptr), - .aroundAnimation = (data.varound_animation() - ? _owner->processDocument( - *data.varound_animation()).get() - : nullptr), - .active = !data.is_inactive(), - }) - : std::nullopt; + const auto &data = entry.data(); + const auto emoji = qs(data.vreaction()); + const auto known = (Ui::Emoji::Find(emoji) != nullptr); + if (!known) { + LOG(("API Error: Unknown emoji in reactions: %1").arg(emoji)); + return std::nullopt; + } + return std::make_optional(Reaction{ + .id = ReactionId{ emoji }, + .title = qs(data.vtitle()), + //.staticIcon = _owner->processDocument(data.vstatic_icon()), + .appearAnimation = _owner->processDocument( + data.vappear_animation()), + .selectAnimation = _owner->processDocument( + data.vselect_animation()), + //.activateAnimation = _owner->processDocument( + // data.vactivate_animation()), + //.activateEffects = _owner->processDocument( + // data.veffect_animation()), + .centerIcon = (data.vcenter_icon() + ? _owner->processDocument(*data.vcenter_icon()).get() + : nullptr), + .aroundAnimation = (data.varound_animation() + ? _owner->processDocument(*data.varound_animation()).get() + : nullptr), + .active = !data.is_inactive(), + }); +} + +std::optional Reactions::parse(const MTPAvailableEffect &entry) { + const auto &data = entry.data(); + const auto emoji = qs(data.vemoticon()); + const auto known = (Ui::Emoji::Find(emoji) != nullptr); + if (!known) { + LOG(("API Error: Unknown emoji in effects: %1").arg(emoji)); + return std::nullopt; + } + const auto id = DocumentId(data.vid().v); + const auto stickerId = data.veffect_sticker_id().v; + const auto document = _owner->document(stickerId); + if (!document->sticker()) { + LOG(("API Error: Bad sticker in effects: %1").arg(stickerId)); + return std::nullopt; + } + const auto aroundId = data.veffect_animation_id().value_or_empty(); + const auto around = aroundId + ? _owner->document(aroundId).get() + : nullptr; + if (around && !around->sticker()) { + LOG(("API Error: Bad sticker in effects around: %1").arg(aroundId)); + return std::nullopt; + } + const auto iconId = data.vstatic_icon_id().value_or_empty(); + const auto icon = iconId ? _owner->document(iconId).get() : nullptr; + if (icon && !icon->sticker()) { + LOG(("API Error: Bad sticker in effects icon: %1").arg(iconId)); + return std::nullopt; + } + return std::make_optional(Reaction{ + .id = ReactionId{ id }, + .title = emoji, + .appearAnimation = document, + .selectAnimation = document, + .centerIcon = icon, + .aroundAnimation = around, + .active = true, + .effect = true, + .premium = data.is_premium_required(), }); } diff --git a/Telegram/SourceFiles/data/data_message_reactions.h b/Telegram/SourceFiles/data/data_message_reactions.h index 9d67e2e2c..793fcc198 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.h +++ b/Telegram/SourceFiles/data/data_message_reactions.h @@ -37,10 +37,13 @@ struct Reaction { DocumentData *aroundAnimation = nullptr; int count = 0; bool active = false; + bool effect = false; + bool premium = false; }; struct PossibleItemReactionsRef { std::vector> recent; + std::vector> stickers; bool customAllowed = false; bool tags = false; }; @@ -50,6 +53,7 @@ struct PossibleItemReactions { explicit PossibleItemReactions(const PossibleItemReactionsRef &other); std::vector recent; + std::vector stickers; bool customAllowed = false; bool tags = false; }; @@ -80,6 +84,7 @@ public: void refreshMyTags(SavedSublist *sublist = nullptr); void refreshMyTagsDelayed(); void refreshTags(); + void refreshEffects(); enum class Type { Active, @@ -88,6 +93,7 @@ public: All, MyTags, Tags, + Effects, }; [[nodiscard]] const std::vector &list(Type type) const; [[nodiscard]] const std::vector &myTagsInfo() const; @@ -108,16 +114,19 @@ public: [[nodiscard]] rpl::producer<> myTagsUpdates() const; [[nodiscard]] rpl::producer<> tagsUpdates() const; [[nodiscard]] rpl::producer myTagRenamed() const; + [[nodiscard]] rpl::producer<> effectsUpdates() const; + + void preloadReactionImageFor(const ReactionId &emoji); + [[nodiscard]] QImage resolveReactionImageFor(const ReactionId &emoji); + + // This is used to reserve space for the effect in BottomInfo but not + // actually paint anything, used in case we want to paint icon ourselves. + static constexpr auto kFakeEffectId = EffectId(1); + + void preloadEffectImageFor(EffectId id); + [[nodiscard]] QImage resolveEffectImageFor(EffectId id); - enum class ImageSize { - BottomInfo, - InlineList, - }; - void preloadImageFor(const ReactionId &emoji); void preloadAnimationsFor(const ReactionId &emoji); - [[nodiscard]] QImage resolveImageFor( - const ReactionId &emoji, - ImageSize size); void send(not_null item, bool addToRecent); [[nodiscard]] bool sending(not_null item) const; @@ -139,11 +148,11 @@ public: private: struct ImageSet { - QImage bottomInfo; - QImage inlineList; + QImage image; std::shared_ptr media; std::unique_ptr icon; bool fromSelectAnimation = false; + bool effect = false; }; struct TagsBySublist { TagsBySublist() = default; @@ -169,6 +178,7 @@ private: void requestGeneric(); void requestMyTags(SavedSublist *sublist = nullptr); void requestTags(); + void requestEffects(); void updateTop(const MTPDmessages_reactions &data); void updateRecent(const MTPDmessages_reactions &data); @@ -178,11 +188,13 @@ private: SavedSublist *sublist, const MTPDmessages_savedReactionTags &data); void updateTags(const MTPDmessages_reactions &data); + void updateEffects(const MTPDmessages_availableEffects &data); void recentUpdated(); void defaultUpdated(); void myTagsUpdated(); void tagsUpdated(); + void effectsUpdated(); [[nodiscard]] std::optional resolveById(const ReactionId &id); [[nodiscard]] std::vector resolveByIds( @@ -203,13 +215,20 @@ private: [[nodiscard]] std::optional parse( const MTPAvailableReaction &entry); + [[nodiscard]] std::optional parse( + const MTPAvailableEffect &entry); + void preloadEffect(const Reaction &effect); + void preloadImageFor(const ReactionId &id); + [[nodiscard]] QImage resolveImageFor(const ReactionId &id); void loadImage( ImageSet &set, not_null document, bool fromSelectAnimation); + void generateImage(ImageSet &set, const QString &emoji); void setAnimatedIcon(ImageSet &set); - void resolveImages(); + void resolveReactionImages(); + void resolveEffectImages(); void downloadTaskFinished(); void repaintCollected(); @@ -233,6 +252,7 @@ private: std::vector _topIds; base::flat_set _unresolvedTop; std::vector> _genericAnimations; + std::vector _effects; ReactionId _favoriteId; ReactionId _unresolvedFavoriteId; std::optional _favorite; @@ -249,6 +269,7 @@ private: rpl::event_stream _myTagsUpdated; rpl::event_stream<> _tagsUpdated; rpl::event_stream _myTagRenamed; + rpl::event_stream<> _effectsUpdated; // We need &i->second stay valid while inserting new items. // So we use std::map instead of base::flat_map here. @@ -271,9 +292,13 @@ private: mtpRequestId _tagsRequestId = 0; uint64 _tagsHash = 0; + mtpRequestId _effectsRequestId = 0; + int32 _effectsHash = 0; + base::flat_map _images; rpl::lifetime _imagesLoadLifetime; - bool _waitingForList = false; + bool _waitingForReactions = false; + bool _waitingForEffects = false; base::flat_map _sentRequests; diff --git a/Telegram/SourceFiles/data/data_replies_list.h b/Telegram/SourceFiles/data/data_replies_list.h index a33dee56b..42f56c1ae 100644 --- a/Telegram/SourceFiles/data/data_replies_list.h +++ b/Telegram/SourceFiles/data/data_replies_list.h @@ -78,10 +78,6 @@ private: [[nodiscard]] Histories &histories(); void subscribeToUpdates(); - [[nodiscard]] rpl::producer sourceFromServer( - MessagePosition aroundId, - int limitBefore, - int limitAfter); void appendClientSideMessages(MessagesSlice &slice); [[nodiscard]] bool buildFromData(not_null viewer); diff --git a/Telegram/SourceFiles/data/data_search_controller.cpp b/Telegram/SourceFiles/data/data_search_controller.cpp index 38c51f9a6..4e33a9eaa 100644 --- a/Telegram/SourceFiles/data/data_search_controller.cpp +++ b/Telegram/SourceFiles/data/data_search_controller.cpp @@ -21,7 +21,8 @@ namespace { constexpr auto kSharedMediaLimit = 100; constexpr auto kFirstSharedMediaLimit = 0; -constexpr auto kDefaultSearchTimeoutMs = crl::time(70); +constexpr auto kHistoryLimit = 50; +constexpr auto kDefaultSearchTimeoutMs = crl::time(200); } // namespace @@ -199,6 +200,60 @@ SearchResult ParseSearchResult( return result; } +HistoryRequest PrepareHistoryRequest( + not_null peer, + MsgId messageId, + Data::LoadDirection direction) { + const auto minId = 0; + const auto maxId = 0; + const auto limit = kHistoryLimit; + const auto offsetId = [&] { + switch (direction) { + case Data::LoadDirection::Before: + case Data::LoadDirection::Around: return messageId; + case Data::LoadDirection::After: return messageId + 1; + } + Unexpected("Direction in PrepareSearchRequest"); + }(); + const auto addOffset = [&] { + switch (direction) { + case Data::LoadDirection::Before: return 0; + case Data::LoadDirection::Around: return -limit / 2; + case Data::LoadDirection::After: return -limit; + } + Unexpected("Direction in PrepareSearchRequest"); + }(); + const auto hash = uint64(0); + const auto offsetDate = int32(0); + + const auto mtpOffsetId = int(std::clamp( + offsetId.bare, + int64(0), + int64(0x3FFFFFFF))); + return MTPmessages_GetHistory( + peer->input, + MTP_int(mtpOffsetId), + MTP_int(offsetDate), + MTP_int(addOffset), + MTP_int(limit), + MTP_int(maxId), + MTP_int(minId), + MTP_long(hash)); +} + +HistoryResult ParseHistoryResult( + not_null peer, + MsgId messageId, + Data::LoadDirection direction, + const HistoryRequestResult &data) { + return ParseSearchResult( + peer, + Storage::SharedMediaType::kCount, + messageId, + direction, + data); +} + SearchController::CacheEntry::CacheEntry( not_null session, const Query &query) diff --git a/Telegram/SourceFiles/data/data_search_controller.h b/Telegram/SourceFiles/data/data_search_controller.h index a938df6ef..26b930b9b 100644 --- a/Telegram/SourceFiles/data/data_search_controller.h +++ b/Telegram/SourceFiles/data/data_search_controller.h @@ -32,7 +32,11 @@ struct SearchResult { using SearchRequest = MTPmessages_Search; using SearchRequestResult = MTPmessages_Messages; -std::optional PrepareSearchRequest( +using HistoryResult = SearchResult; +using HistoryRequest = MTPmessages_GetHistory; +using HistoryRequestResult = MTPmessages_Messages; + +[[nodiscard]] std::optional PrepareSearchRequest( not_null peer, MsgId topicRootId, Storage::SharedMediaType type, @@ -40,13 +44,24 @@ std::optional PrepareSearchRequest( MsgId messageId, Data::LoadDirection direction); -SearchResult ParseSearchResult( +[[nodiscard]] SearchResult ParseSearchResult( not_null peer, Storage::SharedMediaType type, MsgId messageId, Data::LoadDirection direction, const SearchRequestResult &data); +[[nodiscard]] HistoryRequest PrepareHistoryRequest( + not_null peer, + MsgId messageId, + Data::LoadDirection direction); + +[[nodiscard]] HistoryResult ParseHistoryResult( + not_null peer, + MsgId messageId, + Data::LoadDirection direction, + const HistoryRequestResult &data); + class SearchController final { public: using IdsList = Storage::SparseIdsList; diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 779c35ca9..d37e0a27c 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -1830,11 +1830,16 @@ rpl::producer> Session::itemDataChanges() const { } void Session::requestItemTextRefresh(not_null item) { - if (const auto i = _views.find(item); i != _views.end()) { - for (const auto &view : i->second) { + const auto call = [&](not_null item) { + enumerateItemViews(item, [&](not_null view) { view->itemTextUpdated(); - } + }); requestItemResize(item); + }; + if (const auto group = groups().find(item)) { + call(group->items.front()); + } else { + call(item); } } @@ -4312,29 +4317,27 @@ void Session::notifyPollUpdateDelayed(not_null poll) { } void Session::sendWebPageGamePollNotifications() { + auto resize = std::vector>(); for (const auto &page : base::take(_webpagesUpdated)) { _webpageUpdates.fire_copy(page); - const auto i = _webpageViews.find(page); - if (i != _webpageViews.end()) { - for (const auto &view : i->second) { - requestViewResize(view); - } + if (const auto i = _webpageViews.find(page) + ; i != _webpageViews.end()) { + resize.insert(end(resize), begin(i->second), end(i->second)); } } for (const auto &game : base::take(_gamesUpdated)) { if (const auto i = _gameViews.find(game); i != _gameViews.end()) { - for (const auto &view : i->second) { - requestViewResize(view); - } + resize.insert(end(resize), begin(i->second), end(i->second)); } } for (const auto &poll : base::take(_pollsUpdated)) { if (const auto i = _pollViews.find(poll); i != _pollViews.end()) { - for (const auto &view : i->second) { - requestViewResize(view); - } + resize.insert(end(resize), begin(i->second), end(i->second)); } } + for (const auto &view : resize) { + requestViewResize(view); + } } rpl::producer> Session::webPageUpdates() const { @@ -4636,7 +4639,9 @@ void Session::insertCheckedServiceNotification( MTPMessageReactions(), MTPVector(), MTPint(), // ttl_period - MTPint()), // quick_reply_shortcut_id + MTPint(), // quick_reply_shortcut_id + MTPlong(), // effect + MTPFactCheck()), localFlags, NewMessageType::Unread); } diff --git a/Telegram/SourceFiles/data/data_types.h b/Telegram/SourceFiles/data/data_types.h index ca11c8969..66e041c0f 100644 --- a/Telegram/SourceFiles/data/data_types.h +++ b/Telegram/SourceFiles/data/data_types.h @@ -138,6 +138,7 @@ using PollId = uint64; using WallPaperId = uint64; using CallId = uint64; using BotAppId = uint64; +using EffectId = uint64; constexpr auto CancelledWebPageId = WebPageId(0xFFFFFFFFFFFFFFFFULL); @@ -320,6 +321,8 @@ enum class MessageFlag : uint64 { ReactionsAreTags = (1ULL << 43), ShortcutMessage = (1ULL << 44), + + EffectWatched = (1ULL << 45), }; inline constexpr bool is_flag_type(MessageFlag) { return true; } using MessageFlags = base::flags; diff --git a/Telegram/SourceFiles/data/data_web_page.h b/Telegram/SourceFiles/data/data_web_page.h index 8b82ed859..6497a0394 100644 --- a/Telegram/SourceFiles/data/data_web_page.h +++ b/Telegram/SourceFiles/data/data_web_page.h @@ -54,6 +54,8 @@ enum class WebPageType : uint8 { VoiceChat, Livestream, + + Factcheck, }; [[nodiscard]] WebPageType ParseWebPageType(const MTPDwebPage &type); [[nodiscard]] bool IgnoreIv(WebPageType type); diff --git a/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp b/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp index 22b877fb9..0e47aa390 100644 --- a/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp +++ b/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "data/stickers/data_custom_emoji.h" +#include "boxes/peers/edit_forum_topic_box.h" // MakeTopicIconEmoji. #include "chat_helpers/stickers_emoji_pack.h" #include "main/main_app_config.h" #include "main/main_session.h" @@ -15,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_document.h" #include "data/data_document_media.h" #include "data/data_file_origin.h" +#include "data/data_forum_topic.h" // ParseTopicIconEmojiEntity. #include "data/data_peer.h" #include "data/data_message_reactions.h" #include "data/stickers/data_stickers.h" @@ -539,6 +541,8 @@ std::unique_ptr CustomEmojiManager::create( const auto ratio = style::DevicePixelRatio(); const auto size = EmojiSizeFromTag(tag) / ratio; return userpic(data, std::move(update), size); + } else if (const auto parsed = Data::ParseTopicIconEmojiEntity(data)) { + return MakeTopicIconEmoji(parsed, std::move(update), tag); } const auto parsed = ParseCustomEmojiData(data); return parsed @@ -594,13 +598,21 @@ std::unique_ptr CustomEmojiManager::userpic( if (v.size() != 5 && v.size() != 1) { return nullptr; } - const auto id = PeerId(v[0].toULongLong()); + auto image = std::shared_ptr(); + if (v[0] == u"self"_q) { + image = Ui::MakeSavedMessagesThumbnail(); + } else if (v[0] == u"replies"_q) { + image = Ui::MakeRepliesThumbnail(); + } else { + const auto id = PeerId(v[0].toULongLong()); + image = Ui::MakeUserpicThumbnail(_owner->peer(id)); + } const auto padding = (v.size() == 5) ? QMargins(v[1].toInt(), v[2].toInt(), v[3].toInt(), v[4].toInt()) : QMargins(); return std::make_unique( data.toString(), - Ui::MakeUserpicThumbnail(_owner->peer(id)), + std::move(image), std::move(update), padding, size); @@ -988,10 +1000,16 @@ QString CustomEmojiManager::registerInternalEmoji( [[nodiscard]] QString CustomEmojiManager::peerUserpicEmojiData( not_null peer, - QMargins padding) { - return UserpicEmojiPrefix() - + QString::number(peer->id.value) - + InternalPadding(padding); + QMargins padding, + bool respectSavedRepliesEtc) { + const auto id = !respectSavedRepliesEtc + ? QString::number(peer->id.value) + : peer->isSelf() + ? u"self"_q + : peer->isRepliesChat() + ? u"replies"_q + : QString::number(peer->id.value); + return UserpicEmojiPrefix() + id + InternalPadding(padding); } int FrameSizeFromTag(SizeTag tag) { diff --git a/Telegram/SourceFiles/data/stickers/data_custom_emoji.h b/Telegram/SourceFiles/data/stickers/data_custom_emoji.h index 1cbbe0de9..d7a6c46ca 100644 --- a/Telegram/SourceFiles/data/stickers/data_custom_emoji.h +++ b/Telegram/SourceFiles/data/stickers/data_custom_emoji.h @@ -94,7 +94,8 @@ public: [[nodiscard]] QString peerUserpicEmojiData( not_null peer, - QMargins padding = {}); + QMargins padding = {}, + bool respectSavedRepliesEtc = false); [[nodiscard]] uint64 coloredSetId() const; diff --git a/Telegram/SourceFiles/dialogs/dialogs.style b/Telegram/SourceFiles/dialogs/dialogs.style index 8adb2901f..5f2ae6217 100644 --- a/Telegram/SourceFiles/dialogs/dialogs.style +++ b/Telegram/SourceFiles/dialogs/dialogs.style @@ -46,6 +46,11 @@ defaultForumTopicIcon: ForumTopicIcon { font: font(bold 11px); textTop: 2px; } +normalForumTopicIcon: ForumTopicIcon { + size: 19px; + font: font(bold 10px); + textTop: 2px; +} largeForumTopicIcon: ForumTopicIcon { size: 26px; font: font(bold 13px); @@ -517,7 +522,7 @@ forumTopicRow: DialogRow(defaultDialogRow) { photoSize: 20px; nameLeft: 39px; nameTop: 7px; - textLeft: 68px; + textLeft: 39px; textTop: 29px; unreadMarkDiameter: 8px; } @@ -649,7 +654,7 @@ dialogsSearchTabs: SettingsSlider(defaultSettingsSlider) { barRadius: 2px; barFg: transparent; barSnapToLabel: true; - strictSkip: 34px; + strictSkip: 18px; labelTop: 7px; labelStyle: semiboldTextStyle; labelFg: windowSubTextFg; @@ -659,7 +664,7 @@ dialogsSearchTabs: SettingsSlider(defaultSettingsSlider) { rippleBgActive: lightButtonBgOver; ripple: defaultRippleAnimation; } - +dialogsSearchTabsPadding: 8px; dialogsStoriesList: DialogsStoriesList { small: dialogsStories; diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index 542d41118..3216e512c 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -8,6 +8,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "dialogs/dialogs_inner_widget.h" #include "dialogs/dialogs_three_state_icon.h" +#include "dialogs/ui/chat_search_empty.h" +#include "dialogs/ui/chat_search_tabs.h" #include "dialogs/ui/dialogs_layout.h" #include "dialogs/ui/dialogs_stories_content.h" #include "dialogs/ui/dialogs_video_userpic.h" @@ -15,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "dialogs/dialogs_widget.h" #include "dialogs/dialogs_search_from_controllers.h" #include "dialogs/dialogs_search_tags.h" +#include "history/view/history_view_chat_preview.h" #include "history/view/history_view_context_menu.h" #include "history/history.h" #include "history/history_item.h" @@ -61,6 +64,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_controller.h" #include "window/window_session_controller.h" #include "window/window_peer_menu.h" +#include "ui/effects/loading_element.h" #include "ui/widgets/multi_select.h" #include "ui/widgets/menu/menu_add_action_callback_factory.h" #include "ui/empty_userpic.h" @@ -75,13 +79,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_window.h" #include "styles/style_menu_icons.h" +#include + namespace Dialogs { namespace { constexpr auto kHashtagResultsLimit = 5; constexpr auto kStartReorderThreshold = 30; -int FixedOnTopDialogsCount(not_null list) { +[[nodiscard]] int FixedOnTopDialogsCount(not_null list) { auto result = 0; for (const auto &row : *list) { if (!row->entry()->fixedOnTopIndex()) { @@ -92,7 +98,7 @@ int FixedOnTopDialogsCount(not_null list) { return result; } -int PinnedDialogsCount( +[[nodiscard]] int PinnedDialogsCount( FilterId filterId, not_null list) { auto result = 0; @@ -107,6 +113,59 @@ int PinnedDialogsCount( return result; } +[[nodiscard]] object_ptr MakeSearchEmpty( + QWidget *parent, + SearchState state) { + const auto query = state.query.trimmed(); + const auto hashtag = !query.isEmpty() && (query[0] == '#'); + const auto trimmed = hashtag ? query.mid(1).trimmed() : query; + const auto fromPeer = (state.tab == ChatSearchTab::MyMessages + || state.tab == ChatSearchTab::PublicPosts + || !state.inChat.peer() + || !(state.inChat.peer()->isChat() + || state.inChat.peer()->isMegagroup())) + ? nullptr + : state.fromPeer; + const auto waiting = trimmed.isEmpty() + && state.tags.empty() + && !fromPeer; + const auto icon = waiting + ? SearchEmptyIcon::Search + : SearchEmptyIcon::NoResults; + auto text = TextWithEntities(); + if (waiting) { + if (hashtag) { + text.append(tr::lng_search_tab_by_hashtag(tr::now)); + } else { + text.append( + tr::lng_dlg_search_for_messages(tr::now) + ).append('\n').append(Ui::Text::Link(tr::lng_cancel(tr::now))); + } + } else { + text.append(tr::lng_search_tab_no_results( + tr::now, + Ui::Text::Bold)); + if (!trimmed.isEmpty()) { + text.append("\n").append( + tr::lng_search_tab_no_results_text( + tr::now, + lt_query, + trimmed)); + if (hashtag) { + text.append("\n").append( + tr::lng_search_tab_no_results_retry(tr::now)); + } + } + } + auto result = object_ptr( + parent, + icon, + rpl::single(std::move(text))); + result->show(); + result->resizeToWidth(parent->width()); + return result; +} + } // namespace struct InnerWidget::CollapsedRow { @@ -155,12 +214,10 @@ InnerWidget::InnerWidget( , _narrowWidth(st::defaultDialogRow.padding.left() + st::defaultDialogRow.photoSize + st::defaultDialogRow.padding.left()) -, _cancelSearchInChat(this, st::dialogsCancelSearchInPeer) , _cancelSearchFromUser(this, st::dialogsCancelSearchInPeer) , _childListShown(std::move(childListShown)) { setAttribute(Qt::WA_OpaquePaintEvent, true); - _cancelSearchInChat->hide(); _cancelSearchFromUser->hide(); style::PaletteChanged( @@ -184,7 +241,7 @@ InnerWidget::InnerWidget( session().data().contactsLoaded().changes( ) | rpl::start_with_next([=] { refresh(); - refreshEmptyLabel(); + refreshEmpty(); }, lifetime()); session().data().itemRemoved( @@ -329,6 +386,11 @@ InnerWidget::InnerWidget( switchToFilter(filterId); }, lifetime()); + _controller->window().widget()->globalForceClicks( + ) | rpl::start_with_next([=](QPoint globalPosition) { + processGlobalForceClick(globalPosition); + }, lifetime()); + session().data().stories().incrementPreloadingMainSources(); handleChatListEntryRefreshes(); @@ -481,7 +543,7 @@ int InnerWidget::peerSearchOffset() const { + st::searchedBarHeight; } -int InnerWidget::searchInChatOffset() const { +int InnerWidget::searchTagsOffset() const { auto result = peerSearchOffset() - st::searchedBarHeight; if (!_peerSearchResults.empty()) { result += (_peerSearchResults.size() * st::dialogsRowHeight) @@ -490,6 +552,14 @@ int InnerWidget::searchInChatOffset() const { return result; } +int InnerWidget::searchInChatOffset() const { + auto result = searchTagsOffset(); + if (_searchTags) { + result += _searchTags->height(); + } + return result; +} + int InnerWidget::searchedOffset() const { return searchInChatOffset() + searchInChatSkip() @@ -498,16 +568,7 @@ int InnerWidget::searchedOffset() const { int InnerWidget::searchInChatSkip() const { auto result = 0; - if (_searchTags) { - result += _searchTags->height(); - } - if (_searchInChat) { - result += st::searchedBarHeight + st::dialogsSearchInHeight; - } if (_searchFromShown) { - if (_searchInChat) { - result += st::lineWidth; - } result += st::dialogsSearchInHeight; } return result; @@ -651,6 +712,8 @@ void InnerWidget::paintEvent(QPaintEvent *e) { context.active = active; context.selected = _menuRow.key ? (row->key() == _menuRow.key) + : _chatPreviewRow.key + ? (row->key() == _chatPreviewRow.key) : selected; context.topicJumpSelected = selected && _selectedTopicJump @@ -745,6 +808,7 @@ void InnerWidget::paintEvent(QPaintEvent *e) { p.fillRect(dialogsClip, currentBg()); } } else if (_state == WidgetState::Filtered) { + [[maybe_unused]] auto top = 0; if (!_hashtagResults.empty()) { auto from = floorclamp(r.y(), st::mentionHeight, 0, _hashtagResults.size()); auto to = ceilclamp(r.y() + r.height(), st::mentionHeight, 0, _hashtagResults.size()); @@ -789,6 +853,7 @@ void InnerWidget::paintEvent(QPaintEvent *e) { p.drawText(htagleft + firstwidth, st::mentionTop + st::mentionFont->ascent, second); } p.translate(0, st::mentionHeight); + top += st::mentionHeight; } } } @@ -798,7 +863,9 @@ void InnerWidget::paintEvent(QPaintEvent *e) { auto to = std::min( filteredIndex(r.y() + r.height() - skip) + 1, int(_filterResults.size())); - p.translate(0, filteredHeight(from)); + const auto height = filteredHeight(from); + p.translate(0, height); + top += height; for (; from < to; ++from) { const auto selected = isPressed() ? (from == _filteredPressed) @@ -806,6 +873,7 @@ void InnerWidget::paintEvent(QPaintEvent *e) { const auto row = _filterResults[from].row; paintRow(row, selected, !activeEntry.fullId); p.translate(0, row->height()); + top += row->height(); } } @@ -815,11 +883,13 @@ void InnerWidget::paintEvent(QPaintEvent *e) { p.setPen(st::searchedBarFg); p.drawTextLeft(st::searchedBarPosition.x(), st::searchedBarPosition.y(), width(), tr::lng_search_global_results(tr::now)); p.translate(0, st::searchedBarHeight); + top += st::searchedBarHeight; auto skip = peerSearchOffset(); auto from = floorclamp(r.y() - skip, st::dialogsRowHeight, 0, _peerSearchResults.size()); auto to = ceilclamp(r.y() + r.height() - skip, st::dialogsRowHeight, 0, _peerSearchResults.size()); p.translate(0, from * st::dialogsRowHeight); + top += from * st::dialogsRowHeight; if (from < _peerSearchResults.size()) { const auto activePeer = activeEntry.key.peer(); for (; from < to; ++from) { @@ -842,11 +912,23 @@ void InnerWidget::paintEvent(QPaintEvent *e) { .paused = videoPaused, }); p.translate(0, st::dialogsRowHeight); + top += st::dialogsRowHeight; } } } - if (_searchInChat || _searchFromPeer) { + if (_searchTags) { + paintSearchTags(p, { + .st = &st::forumTopicRow, + .currentBg = currentBg(), + .now = ms, + .width = fullWidth, + .paused = videoPaused, + }); + p.translate(0, _searchTags->height()); + top += _searchTags->height(); + } + if (_searchFromShown) { paintSearchInChat(p, { .st = &st::forumTopicRow, .currentBg = currentBg(), @@ -855,29 +937,25 @@ void InnerWidget::paintEvent(QPaintEvent *e) { .paused = videoPaused, }); p.translate(0, searchInChatSkip()); - if (_waitingForSearch && _searchResults.empty()) { - p.fillRect( - 0, - 0, - fullWidth, - st::searchedBarHeight, - st::searchedBarBg); - p.setFont(st::searchedBarFont); - p.setPen(st::searchedBarFg); - p.drawTextLeft( - st::searchedBarPosition.x(), - st::searchedBarPosition.y(), - width(), - tr::lng_dlg_search_for_messages(tr::now)); - p.translate(0, st::searchedBarHeight); + top += searchInChatSkip(); + if (_searchResults.empty()) { + p.fillRect(0, 0, fullWidth, st::lineWidth, st::shadowFg); } } const auto showUnreadInSearchResults = uniqueSearchResults(); - if (!_waitingForSearch || !_searchResults.empty()) { - const auto text = _searchResults.empty() - ? tr::lng_search_no_results(tr::now) - : showUnreadInSearchResults + if (_searchResults.empty()) { + if (_loadingAnimation) { + const auto text = tr::lng_contacts_loading(tr::now); + p.fillRect(0, 0, fullWidth, st::searchedBarHeight, st::searchedBarBg); + p.setFont(st::searchedBarFont); + p.setPen(st::searchedBarFg); + p.drawTextLeft(st::searchedBarPosition.x(), st::searchedBarPosition.y(), width(), text); + p.translate(0, st::searchedBarHeight); + top += st::searchedBarHeight; + } + } else { + const auto text = showUnreadInSearchResults ? u"Search results"_q : tr::lng_search_found_results( tr::now, @@ -888,17 +966,21 @@ void InnerWidget::paintEvent(QPaintEvent *e) { p.setPen(st::searchedBarFg); p.drawTextLeft(st::searchedBarPosition.x(), st::searchedBarPosition.y(), width(), text); p.translate(0, st::searchedBarHeight); + top += st::searchedBarHeight; auto skip = searchedOffset(); auto from = floorclamp(r.y() - skip, _st->height, 0, _searchResults.size()); auto to = ceilclamp(r.y() + r.height() - skip, _st->height, 0, _searchResults.size()); p.translate(0, from * _st->height); + top += from * _st->height; if (from < _searchResults.size()) { for (; from < to; ++from) { const auto &result = _searchResults[from]; const auto active = isSearchResultActive(result.get(), activeEntry); const auto selected = _menuRow.key ? isSearchResultActive(result.get(), _menuRow) + : _chatPreviewRow.key + ? isSearchResultActive(result.get(), _chatPreviewRow) : (from == (isPressed() ? _searchedPressed : _searchedSelected)); @@ -918,6 +1000,7 @@ void InnerWidget::paintEvent(QPaintEvent *e) { .displayUnreadInfo = showUnreadInSearchResults, }); p.translate(0, _st->height); + top += _st->height; } } } @@ -1117,51 +1200,27 @@ QBrush InnerWidget::currentBg() const { _childListShown.current().shown); } +void InnerWidget::paintSearchTags( + Painter &p, + const Ui::PaintContext &context) const { + Expects(_searchTags != nullptr); + + const auto height = _searchTags->height(); + p.fillRect(0, 0, width(), height, currentBg()); + const auto top = st::dialogsSearchTagBottom / 2; + const auto position = QPoint(_searchTagsLeft, top); + _searchTags->paint(p, position, context.now, context.paused); +} + void InnerWidget::paintSearchInChat( Painter &p, const Ui::PaintContext &context) const { auto height = searchInChatSkip(); auto top = 0; - if (_searchTags) { - const auto height = _searchTags->height(); - p.fillRect(0, top, width(), height, currentBg()); - const auto position = QPoint(_searchTagsLeft, 0); - _searchTags->paint(p, position, context.now, context.paused); - top += height; - } p.setFont(st::searchedBarFont); - if (_searchInChat) { - const auto bar = st::searchedBarHeight; - p.fillRect(0, top, width(), top + bar, st::searchedBarBg); - p.setPen(st::searchedBarFg); - p.drawTextLeft(st::searchedBarPosition.x(), top + st::searchedBarPosition.y(), width(), tr::lng_dlg_search_in(tr::now)); - top += bar; - } auto fullRect = QRect(0, top, width(), height - top); p.fillRect(fullRect, currentBg()); - if (_searchInChat) { - if (_searchFromShown) { - p.fillRect(QRect(0, top + st::dialogsSearchInHeight, width(), st::lineWidth), st::shadowFg); - } - p.setPen(st::dialogsNameFg); - if (const auto topic = _searchInChat.topic()) { - paintSearchInTopic(p, context, topic, _searchInChatUserpic, top, _searchInChatText); - } else if (const auto peer = _searchInChat.peer()) { - if (peer->isSelf()) { - paintSearchInSaved(p, top, _searchInChatText); - } else if (peer->isRepliesChat()) { - paintSearchInReplies(p, top, _searchInChatText); - } else { - paintSearchInPeer(p, peer, _searchInChatUserpic, top, _searchInChatText); - } - } else if (const auto sublist = _searchInChat.sublist()) { - paintSearchInSaved(p, top, _searchInChatText); - } else { - Unexpected("Empty Key in paintSearchInChat."); - } - top += st::dialogsSearchInHeight + st::lineWidth; - } if (_searchFromShown) { p.setPen(st::dialogsTextFg); p.setTextPalette(st::dialogsSearchFromPalette); @@ -1259,6 +1318,9 @@ void InnerWidget::paintSearchInTopic( } void InnerWidget::mouseMoveEvent(QMouseEvent *e) { + if (_chatPreviewTouchGlobal || _touchDragStartGlobal) { + return; + } const auto globalPosition = e->globalPos(); if (!_lastMousePosition) { _lastMousePosition = globalPosition; @@ -1268,6 +1330,18 @@ void InnerWidget::mouseMoveEvent(QMouseEvent *e) { return; } selectByMouse(globalPosition); + if (_chatPreviewScheduled && !isUserpicPress()) { + cancelChatPreview(); + } +} + +void InnerWidget::cancelChatPreview() { + _chatPreviewTouchGlobal = {}; + _chatPreviewScheduled = false; + if (_chatPreviewRow.key) { + updateDialogRow(base::take(_chatPreviewRow)); + } + _controller->cancelScheduledPreview(); } void InnerWidget::clearIrrelevantState() { @@ -1298,7 +1372,9 @@ void InnerWidget::selectByMouse(QPoint globalPosition) { _lastMousePosition = globalPosition; _lastRowLocalMouseX = local.x(); - const auto tagBase = QPoint(_searchTagsLeft, searchInChatOffset()); + const auto tagBase = QPoint( + _searchTagsLeft, + searchTagsOffset() + st::dialogsSearchTagBottom / 2); const auto tagPoint = local - tagBase; const auto inTags = _searchTags && QRect( @@ -1330,8 +1406,7 @@ void InnerWidget::selectByMouse(QPoint globalPosition) { const auto selectedTopicJump = selected && selected->lookupIsInTopicJump( local.x(), - mouseY - offset - selected->top()) - && _controller->adaptive().isOneColumn(); + mouseY - offset - selected->top()); if (_collapsedSelected != collapsedSelected || _selected != selected || _selectedTopicJump != selectedTopicJump) { @@ -1373,8 +1448,7 @@ void InnerWidget::selectByMouse(QPoint globalPosition) { const auto selectedTopicJump = (filteredSelected >= 0) && _filterResults[filteredSelected].row->lookupIsInTopicJump( local.x(), - mouseY - skip - _filterResults[filteredSelected].top) - && _controller->adaptive().isOneColumn(); + mouseY - skip - _filterResults[filteredSelected].top); if (_filteredSelected != filteredSelected || _selectedTopicJump != selectedTopicJump) { updateSelectedRow(); @@ -1395,7 +1469,7 @@ void InnerWidget::selectByMouse(QPoint globalPosition) { updateSelectedRow(); } } - if (!_waitingForSearch && !_searchResults.empty()) { + if (!_searchResults.empty()) { auto skip = searchedOffset(); auto searchedSelected = (mouseY >= skip) ? ((mouseY - skip) / _st->height) : -1; if (searchedSelected < 0 || searchedSelected >= _searchResults.size()) { @@ -1413,6 +1487,27 @@ void InnerWidget::selectByMouse(QPoint globalPosition) { } } +RowDescriptor InnerWidget::computeChatPreviewRow() const { + auto result = computeChosenRow(); + if (const auto peer = result.key.peer()) { + const auto topicId = _pressedTopicJump + ? _pressedTopicJumpRootId + : 0; + if (const auto topic = peer->forumTopicFor(topicId)) { + return { topic, FullMsgId() }; + } + } + return { result.key, result.message.fullId }; +} + +void InnerWidget::processGlobalForceClick(QPoint globalPosition) { + const auto parent = parentWidget(); + if (_pressButton == Qt::LeftButton + && parent->rect().contains(parent->mapFromGlobal(globalPosition))) { + showChatPreview(); + } +} + void InnerWidget::mousePressEvent(QMouseEvent *e) { selectByMouse(e->globalPos()); @@ -1424,6 +1519,14 @@ void InnerWidget::mousePressEvent(QMouseEvent *e) { setFilteredPressed(_filteredSelected, _selectedTopicJump); setPeerSearchPressed(_peerSearchSelected); setSearchedPressed(_searchedSelected); + + const auto alt = (e->modifiers() & Qt::AltModifier); + if (alt && showChatPreview()) { + return; + } else if (!alt && isUserpicPress()) { + scheduleChatPreview(e->globalPos()); + } + if (base::in_range(_collapsedSelected, 0, _collapsedRows.size())) { auto row = &_collapsedRows[_collapsedSelected]->row; row->addRipple(e->pos(), QSize(width(), st::dialogsImportantBarHeight), [this, index = _collapsedSelected] { @@ -1493,10 +1596,12 @@ void InnerWidget::mousePressEvent(QMouseEvent *e) { } ClickHandler::pressed(); if (anim::Disabled() + && !_chatPreviewScheduled && (!_pressed || !_pressed->entry()->isPinnedDialog(_filterId))) { mousePressReleased(e->globalPos(), e->button(), e->modifiers()); } } + const std::vector &InnerWidget::pinnedChatsOrder() const { const auto owner = &session().data(); return _savedSublists @@ -1516,6 +1621,13 @@ void InnerWidget::checkReorderPinnedStart(QPoint localPosition) { return; } _dragging = _pressed; + startReorderPinned(localPosition); +} + +void InnerWidget::startReorderPinned(QPoint localPosition) { + Expects(_dragging != nullptr); + + cancelChatPreview(); if (updateReorderIndexGetCount() < 2) { _dragging = nullptr; } else { @@ -1574,6 +1686,7 @@ void InnerWidget::finishReorderPinned() { if (wasDragging) { savePinnedOrder(); _dragging = nullptr; + _touchDragStartGlobal = {}; } _draggingIndex = -1; @@ -1586,6 +1699,22 @@ void InnerWidget::finishReorderPinned() { } } +bool InnerWidget::finishReorderOnRelease() { + if (!_dragging) { + return false; + } + updateReorderIndexGetCount(); + if (_draggingIndex >= 0) { + _pinnedRows[_draggingIndex].yadd.start(0.); + _pinnedRows[_draggingIndex].animStartTime = crl::now(); + if (!_pinnedShiftAnimation.animating()) { + _pinnedShiftAnimation.start(); + } + } + finishReorderPinned(); + return true; +} + void InnerWidget::stopReorderPinned() { _pinnedShiftAnimation.stop(); finishReorderPinned(); @@ -1734,18 +1863,12 @@ void InnerWidget::mousePressReleased( QPoint globalPosition, Qt::MouseButton button, Qt::KeyboardModifiers modifiers) { - auto wasDragging = (_dragging != nullptr); - if (wasDragging) { - updateReorderIndexGetCount(); - if (_draggingIndex >= 0) { - _pinnedRows[_draggingIndex].yadd.start(0.); - _pinnedRows[_draggingIndex].animStartTime = crl::now(); - if (!_pinnedShiftAnimation.animating()) { - _pinnedShiftAnimation.start(); - } - } - finishReorderPinned(); + if (_chatPreviewScheduled) { + _controller->cancelScheduledPreview(); } + _pressButton = Qt::NoButton; + + const auto wasDragging = finishReorderOnRelease(); auto collapsedPressed = _collapsedPressed; setCollapsedPressed(-1); @@ -1865,7 +1988,7 @@ void InnerWidget::resizeEvent(QResizeEvent *e) { if (_searchTags) { _searchTags->resizeToWidth(width() - 2 * _searchTagsLeft); } - resizeEmptyLabel(); + resizeEmpty(); moveCancelSearchButtons(); } @@ -1873,12 +1996,10 @@ void InnerWidget::moveCancelSearchButtons() { const auto widthForCancelButton = qMax( width(), st::columnMinimalWidthLeft - _narrowWidth); - const auto left = widthForCancelButton - st::dialogsSearchInSkip - _cancelSearchInChat->width(); + const auto left = widthForCancelButton - st::dialogsSearchInSkip - _cancelSearchFromUser->width(); const auto top = (st::dialogsSearchInHeight - st::dialogsCancelSearchInPeer.height) / 2; - const auto skip = st::searchedBarHeight + (_searchTags ? _searchTags->height() : 0); - _cancelSearchInChat->moveToLeft(left, skip + top); - const auto next = _searchInChat ? (skip + st::dialogsSearchInHeight + st::lineWidth) : 0; - _cancelSearchFromUser->moveToLeft(left, next + top); + const auto skip = (_searchTags ? _searchTags->height() : 0); + _cancelSearchFromUser->moveToLeft(left, skip + top); } void InnerWidget::dialogRowReplaced( @@ -2277,7 +2398,7 @@ void InnerWidget::fillArchiveSearchMenu(not_null menu) { const auto folder = session().data().folderLoaded(Data::Folder::kId); if (!folder || !folder->chatsList()->fullSize().current() - || _searchInChat) { + || _searchState.inChat) { return; } const auto skip = session().settings().skipArchiveInSearch(); @@ -2290,6 +2411,46 @@ void InnerWidget::fillArchiveSearchMenu(not_null menu) { }); } +bool InnerWidget::showChatPreview() { + const auto row = computeChatPreviewRow(); + const auto callback = crl::guard(this, [=](bool shown) { + chatPreviewShown(shown, row); + }); + return _controller->showChatPreview(row, callback); +} + +void InnerWidget::chatPreviewShown(bool shown, RowDescriptor row) { + _chatPreviewScheduled = false; + if (shown) { + _chatPreviewRow = row; + if (base::take(_chatPreviewTouchGlobal)) { + _touchCancelRequests.fire({}); + } + ClickHandler::unpressed(); + mousePressReleased(QCursor::pos(), Qt::NoButton, Qt::NoModifier); + } else { + cancelChatPreview(); + const auto globalPosition = QCursor::pos(); + if (rect().contains(mapFromGlobal(globalPosition))) { + setMouseTracking(true); + selectByMouse(globalPosition); + } + } +} + +bool InnerWidget::scheduleChatPreview(QPoint positionOverride) { + const auto row = computeChatPreviewRow(); + const auto callback = crl::guard(this, [=](bool shown) { + chatPreviewShown(shown, row); + }); + _chatPreviewScheduled = _controller->scheduleChatPreview( + row, + callback, + nullptr, + positionOverride); + return _chatPreviewScheduled; +} + void InnerWidget::contextMenuEvent(QContextMenuEvent *e) { _menu = nullptr; @@ -2318,7 +2479,9 @@ void InnerWidget::contextMenuEvent(QContextMenuEvent *e) { } return RowDescriptor(); }(); - if (!row.key) return; + if (!row.key) { + return; + } _menuRow = row; if (_pressButton != Qt::LeftButton) { @@ -2373,21 +2536,165 @@ void InnerWidget::parentGeometryChanged() { } } -void InnerWidget::applyFilterUpdate(QString newFilter, bool force) { +bool InnerWidget::processTouchEvent(not_null e) { + const auto point = e->touchPoints().empty() + ? std::optional() + : e->touchPoints().front().screenPos().toPoint(); + switch (e->type()) { + case QEvent::TouchBegin: { + if (!point) { + return false; + } + selectByMouse(*point); + if (isUserpicPressOnWide() && scheduleChatPreview(*point)) { + _chatPreviewTouchGlobal = point; + } else if (!_dragging) { + _touchDragStartGlobal = point; + _touchDragPinnedTimer.callOnce(QApplication::startDragTime()); + } + } break; + + case QEvent::TouchUpdate: { + if (!point) { + return false; + } + if (_chatPreviewTouchGlobal) { + const auto delta = (*_chatPreviewTouchGlobal - *point); + if (delta.manhattanLength() > _st->photoSize) { + cancelChatPreview(); + } + } + if (_touchDragStartGlobal && _dragging) { + updateReorderPinned(mapFromGlobal(*point)); + return _dragging != nullptr; + } else if (_touchDragStartGlobal) { + const auto delta = (*_touchDragStartGlobal - *point); + if (delta.manhattanLength() > QApplication::startDragDistance()) { + if (_touchDragPinnedTimer.isActive()) { + _touchDragPinnedTimer.cancel(); + _touchDragStartGlobal = {}; + _touchDragNowGlobal = {}; + } else { + dragPinnedFromTouch(); + } + } else { + _touchDragNowGlobal = point; + } + } + } break; + + case QEvent::TouchEnd: + case QEvent::TouchCancel: { + if (_chatPreviewTouchGlobal) { + cancelChatPreview(); + } + if (_touchDragStartGlobal) { + _touchDragStartGlobal = {}; + return finishReorderOnRelease(); + } + } break; + } + return false; +} + +void InnerWidget::dragPinnedFromTouch() { + Expects(_touchDragStartGlobal.has_value()); + + const auto global = *_touchDragStartGlobal; + _touchDragPinnedTimer.cancel(); + selectByMouse(global); + if (!_selected || _dragging || _state != WidgetState::Default) { + return; + } + _dragStart = mapFromGlobal(global); + _dragging = _selected; + const auto now = mapFromGlobal(_touchDragNowGlobal.value_or(global)); + startReorderPinned(now); + updateReorderPinned(now); +} + +void InnerWidget::applySearchState(SearchState state) { + if (_searchState == state) { + return; + } + auto withSameQuery = state; + withSameQuery.query = _searchState.query; + const auto otherChanged = (_searchState != withSameQuery); + + const auto ignoreInChat = (state.tab == ChatSearchTab::MyMessages) + || (state.tab == ChatSearchTab::PublicPosts); + const auto sublist = ignoreInChat ? nullptr : state.inChat.sublist(); + const auto peer = ignoreInChat ? nullptr : state.inChat.peer(); + if (const auto migrateFrom = peer ? peer->migrateFrom() : nullptr) { + _searchInMigrated = peer->owner().history(migrateFrom); + } else { + _searchInMigrated = nullptr; + } + if (peer && peer->isSelf()) { + const auto reactions = &peer->owner().reactions(); + _searchTags = std::make_unique( + &peer->owner(), + reactions->myTagsValue(sublist), + state.tags); + + _searchTags->repaintRequests() | rpl::start_with_next([=] { + const auto height = _searchTags->height(); + update(0, searchTagsOffset(), width(), height); + }, _searchTags->lifetime()); + + _searchTags->menuRequests( + ) | rpl::start_with_next([=](Data::ReactionId id) { + HistoryView::ShowTagInListMenu( + &_menu, + _lastMousePosition.value_or(QCursor::pos()), + this, + id, + _controller); + }, _searchTags->lifetime()); + + _searchTags->heightValue() | rpl::skip( + 1 + ) | rpl::start_with_next([=] { + refresh(); + moveCancelSearchButtons(); + }, _searchTags->lifetime()); + } else { + _searchTags = nullptr; + state.tags.clear(); + } + _searchFromShown = ignoreInChat + ? nullptr + : sublist + ? sublist->peer().get() + : state.fromPeer; + if (state.inChat) { + onHashtagFilterUpdate(QStringView()); + } + if (_searchFromShown) { + _cancelSearchFromUser->show(); + _searchFromUserUserpic = _searchFromShown->createUserpicView(); + } else { + _cancelSearchFromUser->hide(); + _searchFromUserUserpic = {}; + } + refreshSearchInChatLabel(); + moveCancelSearchButtons(); + + _searchState = std::move(state); + auto newFilter = _searchState.query; const auto mentionsSearch = (newFilter == u"@"_q); const auto words = mentionsSearch ? QStringList(newFilter) : TextUtilities::PrepareSearchWords(newFilter); newFilter = words.isEmpty() ? QString() : words.join(' '); - if (newFilter != _filter || force) { + if (newFilter != _filter || otherChanged) { _filter = newFilter; if (_filter.isEmpty() - && !_searchFromPeer - && _searchTagsSelected.empty()) { + && !_searchState.fromPeer + && _searchState.tags.empty()) { clearFilter(); } else { setState(WidgetState::Filtered); - _waitingForSearch = true; _filterResults.clear(); _filterResultsGlobal.clear(); const auto append = [&](not_null list) { @@ -2403,9 +2710,7 @@ void InnerWidget::applyFilterUpdate(QString newFilter, bool force) { top += i->row->height(); } }; - if (!_searchInChat - && !_searchFromPeer - && !words.isEmpty()) { + if (_searchState.filterChatsList() && !words.isEmpty()) { if (_savedSublists) { const auto owner = &session().data(); append(owner->savedMessages().chatsList()->indexed()); @@ -2421,17 +2726,20 @@ void InnerWidget::applyFilterUpdate(QString newFilter, bool force) { append(owner->contactsNoChatsList()); } } - refresh(true); } clearMouseSelection(true); } if (_state != WidgetState::Default) { + _searchLoading = true; _searchMessages.fire({}); + refresh(true); } } void InnerWidget::onHashtagFilterUpdate(QStringView newFilter) { - if (newFilter.isEmpty() || newFilter.at(0) != '#' || _searchInChat) { + if (newFilter.isEmpty() + || newFilter.at(0) != '#' + || _searchState.inChat) { _hashtagFilter = QString(); if (!_hashtagResults.empty()) { _hashtagResults.clear(); @@ -2481,7 +2789,9 @@ InnerWidget::~InnerWidget() { } void InnerWidget::clearSearchResults(bool clearPeerSearchResults) { - if (clearPeerSearchResults) _peerSearchResults.clear(); + if (clearPeerSearchResults) { + _peerSearchResults.clear(); + } _searchResults.clear(); _searchResultsLifetime.destroy(); _searchResultsHistories.clear(); @@ -2557,6 +2867,9 @@ void InnerWidget::trackSearchResultsHistory(not_null history) { refresh(); clearMouseSelection(true); } + if (_chatPreviewRow.key.topic() == topic) { + _chatPreviewRow = {}; + } }, _searchResultsLifetime); } } @@ -2615,6 +2928,10 @@ rpl::producer<> InnerWidget::cancelSearchFromUserRequests() const { return _cancelSearchFromUser->clicks() | rpl::to_empty; } +rpl::producer<> InnerWidget::cancelSearchRequests() const { + return _cancelSearch.events(); +} + rpl::producer InnerWidget::mustScrollTo() const { return _mustScrollTo.events(); } @@ -2627,10 +2944,6 @@ rpl::producer<> InnerWidget::searchMessages() const { return _searchMessages.events(); } -rpl::producer<> InnerWidget::cancelSearchInChatRequests() const { - return _cancelSearchInChat->clicks() | rpl::to_empty; -} - rpl::producer InnerWidget::completeHashtagRequests() const { return _completeHashtagRequests.events(); } @@ -2716,19 +3029,22 @@ void InnerWidget::searchReceived( HistoryItem *inject, SearchRequestType type, int fullCount) { + _searchLoading = false; + const auto uniquePeers = uniqueSearchResults(); - if (type == SearchRequestType::FromStart || type == SearchRequestType::PeerFromStart) { + if (type == SearchRequestType::FromStart + || type == SearchRequestType::PeerFromStart) { clearSearchResults(false); } const auto isMigratedSearch = (type == SearchRequestType::MigratedFromStart) || (type == SearchRequestType::MigratedFromOffset); - const auto key = (!_openedForum || _searchInChat.topic()) - ? _searchInChat + const auto key = (!_openedForum || _searchState.inChat.topic()) + ? _searchState.inChat : Key(_openedForum->history()); if (inject - && (!_searchInChat - || inject->history() == _searchInChat.history())) { + && (!_searchState.inChat + || inject->history() == _searchState.inChat.history())) { Assert(_searchResults.empty()); const auto index = int(_searchResults.size()); _searchResults.push_back( @@ -2759,13 +3075,6 @@ void InnerWidget::searchReceived( } else { _searchedCount = fullCount; } - if (_waitingForSearch - && (!_searchResults.empty() - || !_searchInMigrated - || type == SearchRequestType::MigratedFromStart - || type == SearchRequestType::MigratedFromOffset)) { - _waitingForSearch = false; - } refresh(); } @@ -2845,7 +3154,7 @@ void InnerWidget::refresh(bool toTop) { } else if (needCollapsedRowsRefresh()) { return refreshWithCollapsedRows(toTop); } - refreshEmptyLabel(); + refreshEmpty(); if (_searchTags) { _searchTagsLeft = st::dialogsFilterSkip + st::dialogsFilterPadding.x(); @@ -2859,8 +3168,12 @@ void InnerWidget::refresh(bool toTop) { h = dialogsOffset() + _shownList->height(); } } else if (_state == WidgetState::Filtered) { - if (_waitingForSearch) { - h = searchedOffset() + (_searchResults.size() * _st->height) + ((_searchResults.empty() && !_searchInChat) ? -st::searchedBarHeight : 0); + if (_searchEmpty && !_searchEmpty->isHidden()) { + h = searchedOffset() + st::recentPeersEmptyHeightMin; + _searchEmpty->setMinimalHeight(st::recentPeersEmptyHeightMin); + _searchEmpty->move(0, h - st::recentPeersEmptyHeightMin); + } else if (_loadingAnimation) { + h = searchedOffset() + _loadingAnimation->height(); } else { h = searchedOffset() + (_searchResults.size() * _st->height); } @@ -2874,7 +3187,46 @@ void InnerWidget::refresh(bool toTop) { update(); } -void InnerWidget::refreshEmptyLabel() { +void InnerWidget::refreshEmpty() { + if (_state == WidgetState::Filtered) { + const auto empty = _filterResults.empty() + && _searchResults.empty() + && _peerSearchResults.empty() + && _hashtagResults.empty(); + if (_searchLoading || !empty) { + if (_searchEmpty) { + _searchEmpty->hide(); + } + } else if (_searchEmptyState != _searchState) { + _searchEmptyState = _searchState; + _searchEmpty = MakeSearchEmpty(this, _searchState); + _searchEmpty->linkClicks() | rpl::start_with_next([=] { + _cancelSearch.fire({}); + }, _searchEmpty->lifetime()); + if (_controller->session().data().chatsListLoaded()) { + _searchEmpty->animate(); + } + } else if (_searchEmpty) { + _searchEmpty->show(); + } + + if (!_searchLoading || !empty) { + _loadingAnimation.destroy(); + } else if (!_loadingAnimation) { + _loadingAnimation = Ui::CreateLoadingDialogRowWidget( + this, + *_st, + 2); + _loadingAnimation->resizeToWidth(width()); + _loadingAnimation->move(0, searchedOffset()); + _loadingAnimation->show(); + } + } else { + _searchEmpty.destroy(); + _loadingAnimation.destroy(); + _searchEmptyState = {}; + } + const auto data = &session().data(); const auto state = !_shownList->empty() ? EmptyState::None @@ -2927,7 +3279,7 @@ void InnerWidget::refreshEmptyLabel() { return result; }); _empty.create(this, std::move(full), st::dialogsEmptyLabel); - resizeEmptyLabel(); + resizeEmpty(); _empty->overrideLinkClickHandler([=] { if (_emptyState == EmptyState::NoContacts) { _controller->showAddContact(); @@ -2941,13 +3293,20 @@ void InnerWidget::refreshEmptyLabel() { _empty->setVisible(_state == WidgetState::Default); } -void InnerWidget::resizeEmptyLabel() { - if (!_empty) { - return; +void InnerWidget::resizeEmpty() { + if (_empty) { + const auto skip = st::dialogsEmptySkip; + _empty->resizeToWidth(width() - 2 * skip); + _empty->move(skip, (st::dialogsEmptyHeight - _empty->height()) / 2); + } + if (_searchEmpty) { + _searchEmpty->resizeToWidth(width()); + _searchEmpty->move(0, searchedOffset()); + } + if (_loadingAnimation) { + _loadingAnimation->resizeToWidth(width()); + _loadingAnimation->move(0, searchedOffset()); } - const auto skip = st::dialogsEmptySkip; - _empty->resizeToWidth(width() - 2 * skip); - _empty->move(skip, (st::dialogsEmptyHeight - _empty->height()) / 2); } void InnerWidget::clearMouseSelection(bool clearSelection) { @@ -2976,90 +3335,6 @@ bool InnerWidget::hasFilteredResults() const { return !_filterResults.empty() && _hashtagResults.empty(); } -void InnerWidget::searchInChat( - Key key, - PeerData *from, - std::vector tags) { - _searchInMigrated = nullptr; - const auto sublist = key.sublist(); - const auto peer = sublist ? session().user().get() : key.peer(); - if (peer) { - if (const auto migrateTo = peer->migrateTo()) { - const auto to = peer->owner().history(migrateTo); - return searchInChat(to, from, tags); - } else if (const auto migrateFrom = peer->migrateFrom()) { - _searchInMigrated = peer->owner().history(migrateFrom); - } - - if (peer->isSelf()) { - const auto reactions = &peer->owner().reactions(); - _searchTags = std::make_unique( - &peer->owner(), - reactions->myTagsValue(sublist), - tags); - - _searchTags->selectedChanges( - ) | rpl::start_with_next([=](std::vector &&list) { - _searchTagsSelected = std::move(list); - }, _searchTags->lifetime()); - - _searchTags->repaintRequests() | rpl::start_with_next([=] { - const auto height = _searchTags->height(); - update(0, searchInChatOffset(), width(), height); - }, _searchTags->lifetime()); - - _searchTags->menuRequests( - ) | rpl::start_with_next([=](Data::ReactionId id) { - HistoryView::ShowTagInListMenu( - &_menu, - _lastMousePosition.value_or(QCursor::pos()), - this, - id, - _controller); - }, _searchTags->lifetime()); - - _searchTags->heightValue() | rpl::skip( - 1 - ) | rpl::start_with_next([=] { - refresh(); - moveCancelSearchButtons(); - }, _searchTags->lifetime()); - } else { - _searchTags = nullptr; - _searchTagsSelected.clear(); - } - } else { - _searchTags = nullptr; - _searchTagsSelected.clear(); - } - _searchInChat = key; - _searchFromPeer = from; - _searchFromShown = key.sublist() ? key.sublist()->peer().get() : from; - if (_searchInChat) { - onHashtagFilterUpdate(QStringView()); - _cancelSearchInChat->show(); - } else { - _cancelSearchInChat->hide(); - } - if (_searchFromShown) { - _cancelSearchFromUser->show(); - _searchFromUserUserpic = _searchFromShown->createUserpicView(); - } else { - _cancelSearchFromUser->hide(); - _searchFromUserUserpic = {}; - } - if (_searchInChat || _searchFromPeer) { - refreshSearchInChatLabel(); - } - - if (peer) { - _searchInChatUserpic = peer->createUserpicView(); - } else { - _searchInChatUserpic = {}; - } - moveCancelSearchButtons(); -} - auto InnerWidget::searchTagsChanges() const -> rpl::producer> { return _searchTags @@ -3068,27 +3343,6 @@ auto InnerWidget::searchTagsChanges() const } void InnerWidget::refreshSearchInChatLabel() { - const auto dialog = [&] { - if (const auto topic = _searchInChat.topic()) { - return topic->title(); - } else if (const auto peer = _searchInChat.peer()) { - if (peer->isSelf()) { - return tr::lng_saved_messages(tr::now); - } else if (peer->isRepliesChat()) { - return tr::lng_replies_messages(tr::now); - } - return peer->name(); - } else if (_searchInChat.sublist()) { - return tr::lng_saved_messages(tr::now); - } - return QString(); - }(); - if (!dialog.isEmpty()) { - _searchInChatText.setText( - st::semiboldTextStyle, - dialog, - Ui::DialogTextOptions()); - } const auto from = _searchFromShown ? _searchFromShown->name() : u""_q; if (!from.isEmpty()) { const auto fromUserText = tr::lng_dlg_search_from( @@ -3112,10 +3366,9 @@ void InnerWidget::repaintSearchResult(int index) { } void InnerWidget::clearFilter() { - if (_state == WidgetState::Filtered || _searchInChat) { - if (_searchInChat) { + if (_state == WidgetState::Filtered || _searchState.inChat) { + if (_searchState.inChat) { setState(WidgetState::Filtered); - _waitingForSearch = true; } else { setState(WidgetState::Default); } @@ -3413,7 +3666,7 @@ void InnerWidget::switchToFilter(FilterId filterId) { refreshShownList(); refreshWithCollapsedRows(true); } - refreshEmptyLabel(); + refreshEmpty(); { const auto skip = found // Don't save a scroll state for very flexible chat filters. @@ -3501,6 +3754,15 @@ ChosenRow InnerWidget::computeChosenRow() const { return ChosenRow(); } +bool InnerWidget::isUserpicPress() const { + return (_lastRowLocalMouseX >= 0) + && (_lastRowLocalMouseX < _st->nameLeft); +} + +bool InnerWidget::isUserpicPressOnWide() const { + return isUserpicPress() && (width() > _narrowWidth); +} + bool InnerWidget::chooseRow( Qt::KeyboardModifiers modifiers, MsgId pressedTopicRootId) { @@ -3513,9 +3775,7 @@ bool InnerWidget::chooseRow( ChosenRow row, Qt::KeyboardModifiers modifiers) { row.newWindow = (modifiers & Qt::ControlModifier); - row.userpicClick = (_lastRowLocalMouseX >= 0) - && (_lastRowLocalMouseX < _st->nameLeft) - && (width() > _narrowWidth); + row.userpicClick = isUserpicPressOnWide(); return row; }; auto chosen = modifyChosenRow(computeChosenRow(), modifiers); @@ -3854,7 +4114,8 @@ void InnerWidget::setupShortcuts() { return isActiveWindow() && !_controller->isLayerShown() && !_controller->window().locked() - && !_childListShown.current().shown; + && !_childListShown.current().shown + && !_chatPreviewRow.key; }) | rpl::start_with_next([=](not_null request) { using Command = Shortcuts::Command; @@ -3887,6 +4148,12 @@ void InnerWidget::setupShortcuts() { request->check(Command::ChatNext) && request->handle([=] { return jumpToDialogRow(next); }); + } else if (_state == WidgetState::Default + ? !_shownList->empty() + : !_filterResults.empty()) { + request->check(Command::ChatNext) && request->handle([=] { + return jumpToDialogRow(first); + }); } request->check(Command::ChatFirst) && request->handle([=] { return jumpToDialogRow(first); diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h index 7699576c8..9f069c243 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h @@ -7,14 +7,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "base/flags.h" +#include "base/object_ptr.h" +#include "base/timer.h" #include "dialogs/dialogs_key.h" #include "data/data_messages.h" #include "ui/dragging_scroll_manager.h" #include "ui/effects/animations.h" #include "ui/rp_widget.h" #include "ui/userpic_view.h" -#include "base/flags.h" -#include "base/object_ptr.h" namespace style { struct DialogRow; @@ -59,6 +60,7 @@ class Row; class FakeRow; class IndexedList; class SearchTags; +class SearchEmpty; struct ChosenRow { Key key; @@ -118,9 +120,15 @@ public: void clearFilter(); void refresh(bool toTop = false); - void refreshEmptyLabel(); - void resizeEmptyLabel(); + void refreshEmpty(); + void resizeEmpty(); + [[nodiscard]] bool isUserpicPress() const; + [[nodiscard]] bool isUserpicPressOnWide() const; + void cancelChatPreview(); + bool scheduleChatPreview(QPoint positionOverride); + bool showChatPreview(); + void chatPreviewShown(bool shown, RowDescriptor row = {}); bool chooseRow( Qt::KeyboardModifiers modifiers = {}, MsgId pressedTopicRootId = {}); @@ -134,19 +142,12 @@ public: [[nodiscard]] not_null st() const { return _st; } - [[nodiscard]] bool waitingForSearch() const { - return _waitingForSearch; - } [[nodiscard]] bool hasFilteredResults() const; - void searchInChat( - Key key, - PeerData *from, - std::vector tags); + void applySearchState(SearchState state); [[nodiscard]] auto searchTagsChanges() const -> rpl::producer>; - void applyFilterUpdate(QString newFilter, bool force = false); void onHashtagFilterUpdate(QStringView newFilter); void appendToFiltered(Key key); @@ -156,6 +157,7 @@ public: void setLoadMoreFilteredCallback(Fn callback); [[nodiscard]] rpl::producer<> listBottomReached() const; [[nodiscard]] rpl::producer<> cancelSearchFromUserRequests() const; + [[nodiscard]] rpl::producer<> cancelSearchRequests() const; [[nodiscard]] rpl::producer chosenRow() const; [[nodiscard]] rpl::producer<> updated() const; @@ -163,7 +165,6 @@ public: [[nodiscard]] rpl::producer mustScrollTo() const; [[nodiscard]] rpl::producer dialogMoved() const; [[nodiscard]] rpl::producer<> searchMessages() const; - [[nodiscard]] rpl::producer<> cancelSearchInChatRequests() const; [[nodiscard]] rpl::producer completeHashtagRequests() const; [[nodiscard]] rpl::producer<> refreshHashtagsRequests() const; @@ -174,6 +175,11 @@ public: void parentGeometryChanged(); + bool processTouchEvent(not_null e); + [[nodiscard]] rpl::producer<> touchCancelRequests() const { + return _touchCancelRequests.events(); + } + protected: void visibleTopBottomUpdated( int visibleTop, @@ -253,6 +259,7 @@ private: QPoint globalPosition, Qt::MouseButton button, Qt::KeyboardModifiers modifiers); + void processGlobalForceClick(QPoint globalPosition); void clearIrrelevantState(); void selectByMouse(QPoint globalPosition); void preloadRowsData(); @@ -332,6 +339,7 @@ private: [[nodiscard]] int filteredIndex(int y) const; [[nodiscard]] int filteredHeight(int till = -1) const; [[nodiscard]] int peerSearchOffset() const; + [[nodiscard]] int searchTagsOffset() const; [[nodiscard]] int searchInChatOffset() const; [[nodiscard]] int searchedOffset() const; [[nodiscard]] int searchInChatSkip() const; @@ -347,6 +355,9 @@ private: Painter &p, not_null result, const Ui::PaintContext &context); + void paintSearchTags( + Painter &p, + const Ui::PaintContext &context) const; void paintSearchInChat( Painter &p, const Ui::PaintContext &context) const; @@ -388,21 +399,24 @@ private: void clearSearchResults(bool clearPeerSearchResults = true); void updateSelectedRow(Key key = Key()); void trackSearchResultsHistory(not_null history); - void trackSearchResultsForum(Data::Forum *forum); [[nodiscard]] QBrush currentBg() const; + [[nodiscard]] RowDescriptor computeChatPreviewRow() const; [[nodiscard]] const std::vector &pinnedChatsOrder() const; void checkReorderPinnedStart(QPoint localPosition); + void startReorderPinned(QPoint localPosition); int updateReorderIndexGetCount(); bool updateReorderPinned(QPoint localPosition); void finishReorderPinned(); + bool finishReorderOnRelease(); void stopReorderPinned(); int countPinnedIndex(Row *ofRow); void savePinnedOrder(); bool pinnedShiftAnimationCallback(crl::time now); void handleChatListEntryRefreshes(); void moveCancelSearchButtons(); + void dragPinnedFromTouch(); void saveChatsFilterScrollState(FilterId filterId); void restoreChatsFilterScrollState(FilterId filterId); @@ -459,7 +473,6 @@ private: int _filteredSelected = -1; int _filteredPressed = -1; - bool _waitingForSearch = false; EmptyState _emptyState = EmptyState::None; QString _peerSearchQuery; @@ -477,22 +490,21 @@ private: WidgetState _state = WidgetState::Default; + object_ptr _loadingAnimation = { nullptr }; + object_ptr _searchEmpty = { nullptr }; + SearchState _searchEmptyState; object_ptr _empty = { nullptr }; - object_ptr _cancelSearchInChat; object_ptr _cancelSearchFromUser; + rpl::event_stream<> _cancelSearch; Ui::DraggingScrollManager _draggingScroll; - Key _searchInChat; + SearchState _searchState; History *_searchInMigrated = nullptr; - PeerData *_searchFromPeer = nullptr; PeerData *_searchFromShown = nullptr; - mutable Ui::PeerUserpicView _searchInChatUserpic; mutable Ui::PeerUserpicView _searchFromUserUserpic; - Ui::Text::String _searchInChatText; Ui::Text::String _searchFromUserText; std::unique_ptr _searchTags; - std::vector _searchTagsSelected; int _searchTagsLeft = 0; RowDescriptor _menuRow; @@ -514,11 +526,20 @@ private: rpl::event_stream _completeHashtagRequests; rpl::event_stream<> _refreshHashtagsRequests; + RowDescriptor _chatPreviewRow; + bool _chatPreviewScheduled = false; + std::optional _chatPreviewTouchGlobal; + base::Timer _touchDragPinnedTimer; + std::optional _touchDragStartGlobal; + std::optional _touchDragNowGlobal; + rpl::event_stream<> _touchCancelRequests; + rpl::variable _childListShown; float64 _narrowRatio = 0.; bool _geometryInited = false; bool _savedSublists = false; + bool _searchLoading = false; base::unique_qptr _menu; diff --git a/Telegram/SourceFiles/dialogs/dialogs_key.cpp b/Telegram/SourceFiles/dialogs/dialogs_key.cpp index dfa728141..7c1be62aa 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_key.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_key.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_folder.h" #include "data/data_forum_topic.h" #include "data/data_saved_sublist.h" +#include "dialogs/ui/chat_search_tabs.h" #include "history/history.h" namespace Dialogs { @@ -84,4 +85,22 @@ PeerData *Key::peer() const { return nullptr; } +[[nodiscard]] bool SearchState::empty() const { + return !inChat + && tags.empty() + && QStringView(query).trimmed().isEmpty(); +} + +ChatSearchTab SearchState::defaultTabForMe() const { + return inChat.topic() + ? ChatSearchTab::ThisTopic + : (inChat.history() || inChat.sublist()) + ? ChatSearchTab::ThisPeer + : ChatSearchTab::MyMessages; +} + +bool SearchState::filterChatsList() const { + return !inChat && (tab == ChatSearchTab::MyMessages); +} + } // namespace Dialogs diff --git a/Telegram/SourceFiles/dialogs/dialogs_key.h b/Telegram/SourceFiles/dialogs/dialogs_key.h index c43dc9f15..8b5d22ed7 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_key.h +++ b/Telegram/SourceFiles/dialogs/dialogs_key.h @@ -7,6 +7,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "base/qt/qt_compare.h" +#include "data/data_message_reaction_id.h" + class History; class PeerData; @@ -15,11 +18,13 @@ class Thread; class Folder; class ForumTopic; class SavedSublist; +struct ReactionId; } // namespace Data namespace Dialogs { class Entry; +enum class ChatSearchTab : uchar; class Key { public: @@ -120,4 +125,27 @@ struct EntryState { = default; }; +struct SearchState { + Key inChat; + PeerData *fromPeer = nullptr; + std::vector tags; + ChatSearchTab tab = {}; + QString query; + + [[nodiscard]] bool empty() const; + [[nodiscard]] ChatSearchTab defaultTabForMe() const; + [[nodiscard]] bool filterChatsList() const; + + explicit operator bool() const { + return !empty(); + } + + friend inline auto operator<=>( + const SearchState&, + const SearchState&) noexcept = default; + friend inline bool operator==( + const SearchState&, + const SearchState&) = default; +}; + } // namespace Dialogs diff --git a/Telegram/SourceFiles/dialogs/dialogs_search_tags.cpp b/Telegram/SourceFiles/dialogs/dialogs_search_tags.cpp index 276d4e5b6..c7e85c94e 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_search_tags.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_search_tags.cpp @@ -168,7 +168,7 @@ void SearchTags::fill( .selected = ranges::contains(selected, id), }); if (!customId) { - _owner->reactions().preloadImageFor(id); + _owner->reactions().preloadReactionImageFor(id); } }; if (!premium) { @@ -335,9 +335,7 @@ void SearchTags::paint( paintBackground(p, geometry, tag); paintText(p, geometry, tag); if (!tag.custom && !tag.promo && tag.image.isNull()) { - tag.image = _owner->reactions().resolveImageFor( - tag.id, - ::Data::Reactions::ImageSize::InlineList); + tag.image = _owner->reactions().resolveReactionImageFor(tag.id); } const auto inner = geometry.marginsRemoved(padding); const auto image = QRect( diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index de738708e..ba82d4d84 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/qt/qt_key_modifiers.h" #include "base/options.h" +#include "dialogs/ui/chat_search_tabs.h" #include "dialogs/ui/dialogs_stories_content.h" #include "dialogs/ui/dialogs_stories_list.h" #include "dialogs/ui/dialogs_suggestions.h" @@ -23,6 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_requests_bar.h" #include "history/view/history_view_group_call_bar.h" #include "boxes/peers/edit_peer_requests_box.h" +#include "ui/text/text_utilities.h" #include "ui/widgets/buttons.h" #include "ui/widgets/elastic_scroll.h" #include "ui/widgets/fields/input_field.h" @@ -45,6 +47,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "base/event_filter.h" #include "core/application.h" +#include "core/ui_integration.h" #include "core/update_checker.h" #include "core/shortcuts.h" #include "boxes/peer_list_box.h" @@ -62,6 +65,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_session.h" #include "data/data_channel.h" #include "data/data_chat.h" +#include "data/stickers/data_custom_emoji.h" #include "data/data_user.h" #include "data/data_folder.h" #include "data/data_forum.h" @@ -251,7 +255,7 @@ Widget::Widget( st::dialogsStoriesList, _storiesContents.events() | rpl::flatten_latest()) : nullptr) -, _searchTimer([=] { searchMessages(); }) +, _searchTimer([=] { search(); }) , _singleMessageSearch(&controller->session()) { const auto makeChildListShown = [](PeerId peerId, float64 shown) { return InnerWidget::ChildListShown{ peerId, shown }; @@ -326,11 +330,7 @@ Widget::Widget( }, lifetime()); _inner->searchMessages( ) | rpl::start_with_next([=] { - needSearchMessages(); - }, lifetime()); - _inner->cancelSearchInChatRequests( - ) | rpl::start_with_next([=] { - cancelSearchInChat(); + searchRequested(); }, lifetime()); _inner->completeHashtagRequests( ) | rpl::start_with_next([=](const QString &tag) { @@ -342,12 +342,17 @@ Widget::Widget( }, lifetime()); _inner->cancelSearchFromUserRequests( ) | rpl::start_with_next([=] { - setSearchInChat((_openedForum && !_searchInChat) - ? Key(_openedForum->history()) - : _searchInChat.sublist() - ? Key(session().data().history(session().user())) - : _searchInChat, nullptr); - applySearchUpdate(true); + auto copy = _searchState; + copy.fromPeer = nullptr; + if (copy.inChat.sublist()) { + copy.inChat = session().data().history(session().user()); + } + applySearchState(std::move(copy)); + }, lifetime()); + _inner->cancelSearchRequests( + ) | rpl::start_with_next([=] { + setInnerFocus(true); + applySearchState({}); }, lifetime()); _inner->chosenRow( ) | rpl::start_with_next([=](const ChosenRow &row) { @@ -380,7 +385,7 @@ Widget::Widget( _search->changes( ) | rpl::start_with_next([=] { - applySearchUpdate(); + crl::on_main(this, [=] { applySearchUpdate(); }); }, _search->lifetime()); _search->submits( @@ -448,7 +453,7 @@ Widget::Widget( _inner->setLoadMoreCallback([=] { const auto state = _inner->state(); if (state == WidgetState::Filtered - && (!_inner->waitingForSearch() + && (!_searchFull || (_searchInMigrated && _searchFull && !_searchFullMigrated))) { @@ -473,6 +478,7 @@ Widget::Widget( updateSearchFromVisibility(true); setupSupportMode(); setupScrollUpButton(); + setupTouchChatPreview(); const auto overscrollBg = [=] { return anim::color( @@ -529,7 +535,7 @@ Widget::Widget( void Widget::chosenRow(const ChosenRow &row) { storiesToggleExplicitExpand(false); - if (!_search->getLastText().isEmpty()) { + if (!_searchState.query.isEmpty()) { if (const auto history = row.key.history()) { session().recentPeers().bump(history->peer); } @@ -621,6 +627,7 @@ void Widget::chosenRow(const ChosenRow &row) { escape(); } } + updateForceDisplayWide(); } void Widget::setGeometryWithTopMoved( @@ -655,6 +662,17 @@ void Widget::setupScrollUpButton() { updateScrollUpVisibility(); } +void Widget::setupTouchChatPreview() { + _scroll->setCustomTouchProcess([=](not_null e) { + return _inner->processTouchEvent(e); + }); + _inner->touchCancelRequests() | rpl::start_with_next([=] { + QTouchEvent ev(QEvent::TouchCancel); + ev.setTimestamp(crl::now()); + QGuiApplication::sendEvent(_scroll, &ev); + }, _inner->lifetime()); +} + void Widget::setupMoreChatsBar() { if (_layout == Layout::Child) { return; @@ -662,9 +680,12 @@ void Widget::setupMoreChatsBar() { controller()->activeChatsFilter( ) | rpl::start_with_next([=](FilterId id) { storiesToggleExplicitExpand(false); - if (!_searchInChat) { - cancelSearch(); - } + const auto cancelled = cancelSearch(true); + const auto guard = gsl::finally([&] { + if (cancelled) { + controller()->content()->dialogsCancelled(); + } + }); if (!id) { _moreChatsBar = nullptr; @@ -1016,6 +1037,7 @@ void Widget::setupShortcuts() { ) | rpl::filter([=] { return isActiveWindow() && Ui::InFocusChain(this) + && !_childList && !controller()->isLayerShown() && !controller()->window().locked(); }) | rpl::start_with_next([=](not_null request) { @@ -1054,7 +1076,7 @@ void Widget::fullSearchRefreshOn(rpl::producer<> events) { _searchQuery = QString(); _scroll->scrollToY(0); cancelSearchRequest(); - searchMessages(); + search(); }, lifetime()); } @@ -1091,6 +1113,9 @@ void Widget::updateControlsVisibility(bool fast) { updateJumpToDateVisibility(fast); updateSearchFromVisibility(fast); } + if (_searchTabs) { + _searchTabs->show(); + } if (_connecting) { _connecting->setForceHidden(false); } @@ -1144,7 +1169,7 @@ void Widget::updateHasFocus(not_null focused) { bool Widget::cancelSearchByMouseBack() { return _searchHasFocus && !_searchSuggestionsLocked - && !_searchInChat + && !_searchState.inChat && cancelSearch(); } @@ -1157,7 +1182,7 @@ void Widget::processSearchFocusChange() { void Widget::updateSuggestions(anim::type animated) { const auto suggest = (_searchHasFocus || _searchSuggestionsLocked) - && !_searchInChat + && !_searchState.inChat && (_inner->state() == WidgetState::Default); if (anim::Disabled() || !session().data().chatsListLoaded()) { animated = anim::type::instant; @@ -1234,6 +1259,102 @@ void Widget::updateSuggestions(anim::type animated) { } } +void Widget::updateSearchTabs() { + const auto has = _searchState.inChat || _searchingHashtag; + if (!has) { + if (_searchTabs) { + _searchTabs = nullptr; + updateControlsGeometry(); + } + return; + } else if (!_searchTabs) { + const auto savedSession = &session(); + const auto markedTextContext = [=](Fn repaint) { + return Core::MarkedTextContext{ + .session = savedSession, + .customEmojiRepaint = std::move(repaint), + }; + }; + _searchTabs = std::make_unique( + this, + _searchState.tab, + std::move(markedTextContext)); + _searchTabs->setVisible(!_showAnimation); + _searchTabs->tabChanges( + ) | rpl::filter([=](ChatSearchTab tab) { + return (_searchState.tab != tab); + }) | rpl::start_with_next([=](ChatSearchTab tab) { + auto copy = _searchState; + copy.tab = tab; + applySearchState(std::move(copy)); + }, _searchTabs->lifetime()); + } + const auto sublist = _searchState.inChat.sublist(); + const auto topic = _searchState.inChat.topic(); + const auto peer = _searchState.inChat.owningHistory() + ? _searchState.inChat.owningHistory()->peer.get() + : _openedForum + ? _openedForum->channel().get() + : nullptr; + const auto topicShortLabel = !topic + ? TextWithEntities() + : topic->iconId() + ? Ui::Text::SingleCustomEmoji( + Data::SerializeCustomEmojiId(topic->iconId())) + : Ui::Text::SingleCustomEmoji(Data::TopicIconEmojiEntity({ + .title = (topic->isGeneral() + ? Data::ForumGeneralIconTitle() + : topic->title()), + .colorId = (topic->isGeneral() + ? Data::ForumGeneralIconColor(st::windowSubTextFg->c) + : topic->colorId()), + })); + const auto peerShortLabel = peer + ? Ui::Text::SingleCustomEmoji( + session().data().customEmojiManager().peerUserpicEmojiData( + peer, + {}, + true)) + : sublist + ? Ui::Text::SingleCustomEmoji( + session().data().customEmojiManager().peerUserpicEmojiData( + sublist->peer(), + {}, + true)) + : TextWithEntities(); + const auto myShortLabel = DefaultShortLabel(ChatSearchTab::MyMessages); + const auto publicShortLabel = _searchingHashtag + ? DefaultShortLabel(ChatSearchTab::PublicPosts) + : TextWithEntities(); + if ((_searchState.tab == ChatSearchTab::ThisTopic + && !_searchState.inChat.topic()) + || (_searchState.tab == ChatSearchTab::ThisPeer + && !_searchState.inChat + && !_openedForum) + || (_searchState.tab == ChatSearchTab::PublicPosts + && !_searchingHashtag)) { + _searchState.tab = _searchState.inChat.topic() + ? ChatSearchTab::ThisTopic + : (_searchState.inChat.owningHistory() + || _searchState.inChat.sublist()) + ? ChatSearchTab::ThisPeer + : ChatSearchTab::MyMessages; + } + const auto peerTabType = (peer && peer->isBroadcast()) + ? ChatSearchPeerTabType::Channel + : (peer && (peer->isChat() || peer->isMegagroup())) + ? ChatSearchPeerTabType::Group + : ChatSearchPeerTabType::Chat; + _searchTabs->setTabShortLabels({ + { ChatSearchTab::ThisTopic, topicShortLabel }, + { ChatSearchTab::ThisPeer, peerShortLabel }, + { ChatSearchTab::MyMessages, myShortLabel }, + { ChatSearchTab::PublicPosts, publicShortLabel }, + }, _searchState.tab, peerTabType); + + updateControlsGeometry(); +} + void Widget::changeOpenedSubsection( FnMut change, bool fromRight, @@ -1284,7 +1405,7 @@ void Widget::changeOpenedFolder(Data::Folder *folder, anim::type animated) { return; } changeOpenedSubsection([&] { - cancelSearch(); + cancelSearch(true); closeChildList(anim::type::instant); controller()->closeForum(); _openedFolder = folder; @@ -1338,9 +1459,12 @@ void Widget::changeOpenedForum(Data::Forum *forum, anim::type animated) { return; } changeOpenedSubsection([&] { - cancelSearch(); + cancelSearch(true); closeChildList(anim::type::instant); _openedForum = forum; + _searchState.tab = forum + ? ChatSearchTab::ThisPeer + : ChatSearchTab::MyMessages; _api.request(base::take(_topicSearchRequest)).cancel(); _inner->changeOpenedForum(forum); storiesToggleExplicitExpand(false); @@ -1401,6 +1525,7 @@ void Widget::refreshTopBars() { setFocus(); } _subsectionTopBar.destroy(); + updateSearchFromVisibility(true); } _forumSearchRequested = false; if (_openedForum) { @@ -1475,9 +1600,7 @@ void Widget::showSearchInTopBar(anim::type animated) { Expects(_subsectionTopBar != nullptr); _subsectionTopBar->toggleSearch(true, animated); - _subsectionTopBar->searchEnableChooseFromUser( - true, - !_searchFromAuthor); + updateForceDisplayWide(); } QPixmap Widget::grabForFolderSlideAnimation() { @@ -1537,12 +1660,11 @@ void Widget::checkUpdateStatus() { void Widget::setInnerFocus(bool unfocusSearch) { if (_childList) { _childList->setInnerFocus(); - } else if ((_openedFolder || _openedForum) - && _subsectionTopBar->searchSetFocus()) { + } else if (_subsectionTopBar && _subsectionTopBar->searchSetFocus()) { return; } else if (!unfocusSearch && (!_search->getLastText().isEmpty() - || _searchInChat + || _searchState.inChat || _searchHasFocus || _searchSuggestionsLocked)) { _search->setFocus(); @@ -1559,7 +1681,7 @@ void Widget::jumpToTop(bool belowPinned) { if (session().supportMode()) { return; } - if ((currentSearchQuery().trimmed().isEmpty() && !_searchInChat)) { + if ((_searchState.query.trimmed().isEmpty() && !_searchState.inChat)) { auto to = 0; if (belowPinned) { const auto list = _openedForum @@ -1704,8 +1826,8 @@ void Widget::updateStoriesVisibility() { || _childList || _searchHasFocus || _searchSuggestionsLocked - || !_search->getLastText().isEmpty() - || _searchInChat + || !_searchState.query.isEmpty() + || _searchState.inChat || _stories->empty(); if (_stories->isHidden() != hidden) { _stories->setVisible(!hidden); @@ -1842,7 +1964,7 @@ void Widget::escape() { controller()->setActiveChatsFilter(first); } } - } else if (!_searchInChat + } else if (!_searchState.inChat && controller()->activeChatEntryCurrent().key) { controller()->content()->dialogsCancelled(); } @@ -1858,11 +1980,11 @@ void Widget::submit() { const auto state = _inner->state(); if (state == WidgetState::Default || (state == WidgetState::Filtered - && (!_inner->waitingForSearch() || _inner->hasFilteredResults()))) { + && _inner->hasFilteredResults())) { _inner->selectSkip(1); _inner->chooseRow(); } else { - searchMessages(); + search(); } } @@ -1905,55 +2027,71 @@ void Widget::loadMoreBlockedByDate() { session().api().requestMoreBlockedByDateDialogs(); } -bool Widget::searchMessages(bool searchCache) { +bool Widget::search(bool inCache) { + _processingSearch = true; + const auto guard = gsl::finally([&] { + _processingSearch = false; + listScrollUpdated(); + }); + auto result = false; - auto q = currentSearchQuery().trimmed(); - if (q.isEmpty() && !_searchFromAuthor && _searchTags.empty()) { + const auto query = _searchState.query.trimmed(); + const auto trimmed = (query.isEmpty() || query[0] != '#') + ? query + : query.mid(1).trimmed(); + const auto inPeer = searchInPeer(); + const auto fromPeer = searchFromPeer(); + const auto &inTags = searchInTags(); + const auto tab = _searchState.tab; + const auto fromStartType = inPeer + ? SearchRequestType::PeerFromStart + : SearchRequestType::FromStart; + if (trimmed.isEmpty() && !fromPeer && inTags.empty()) { cancelSearchRequest(); + searchApplyEmpty(fromStartType, 0); _api.request(base::take(_peerSearchRequest)).cancel(); + _peerSearchQuery = QString(); + peerSearchApplyEmpty(0); _api.request(base::take(_topicSearchRequest)).cancel(); return true; - } - if (searchCache) { - const auto success = _singleMessageSearch.lookup(q, [=] { - needSearchMessages(); + } else if (inCache) { + const auto success = _singleMessageSearch.lookup(query, [=] { + searchRequested(); }); if (!success) { return false; } - const auto i = _searchCache.find(q); + const auto i = _searchCache.find(query); if (i != _searchCache.end()) { - _searchQuery = q; - _searchQueryFrom = _searchFromAuthor; - _searchQueryTags = _searchTags; + _searchQuery = query; + _searchQueryFrom = fromPeer; + _searchQueryTags = inTags; + _searchQueryTab = tab; _searchNextRate = 0; _searchFull = _searchFullMigrated = false; cancelSearchRequest(); - searchReceived( - ((_searchInChat || _openedForum) - ? SearchRequestType::PeerFromStart - : SearchRequestType::FromStart), - i->second, - 0); + searchReceived(fromStartType, i->second, 0); result = true; } - } else if (_searchQuery != q - || _searchQueryFrom != _searchFromAuthor - || _searchQueryTags != _searchTags) { - _searchQuery = q; - _searchQueryFrom = _searchFromAuthor; - _searchQueryTags = _searchTags; + } else if (_searchQuery != query + || _searchQueryFrom != fromPeer + || _searchQueryTags != inTags + || _searchQueryTab != tab) { + _searchQuery = query; + _searchQueryFrom = fromPeer; + _searchQueryTags = inTags; + _searchQueryTab = tab; _searchNextRate = 0; _searchFull = _searchFullMigrated = false; cancelSearchRequest(); - if (const auto peer = searchInPeer()) { + if (inPeer) { const auto topic = searchInTopic(); auto &histories = session().data().histories(); const auto type = Data::Histories::RequestType::History; - const auto history = session().data().history(peer); + const auto history = session().data().history(inPeer); const auto sublist = _openedForum ? nullptr - : _searchInChat.sublist(); + : _searchState.inChat.sublist(); const auto fromPeer = sublist ? nullptr : _searchQueryFrom; const auto savedPeer = sublist ? sublist->peer().get() @@ -1968,7 +2106,7 @@ bool Widget::searchMessages(bool searchCache) { | (_searchQueryTags.empty() ? Flag() : Flag::f_saved_reaction)), - peer->input, + inPeer->input, MTP_string(_searchQuery), (fromPeer ? fromPeer->input : MTP_inputPeerEmpty()), (savedPeer ? savedPeer->input : MTP_inputPeerEmpty()), @@ -1998,6 +2136,20 @@ bool Widget::searchMessages(bool searchCache) { _searchQueries.emplace(_searchRequest, _searchQuery); return _searchRequest; }); + } else if (_searchState.tab == ChatSearchTab::PublicPosts) { + const auto type = SearchRequestType::FromStart; + _searchRequest = session().api().request(MTPchannels_SearchPosts( + MTP_string(_searchState.query.trimmed().mid(1)), + MTP_int(0), // offset_rate + MTP_inputPeerEmpty(), // offset_peer + MTP_int(0), // offset_id + MTP_int(kSearchPerPage) + )).done([=](const MTPmessages_Messages &result) { + searchReceived(type, result, _searchRequest); + }).fail([=](const MTP::Error &error) { + searchFailed(type, error, _searchRequest); + }).send(); + _searchQueries.emplace(_searchRequest, _searchQuery); } else { const auto type = SearchRequestType::FromStart; const auto flags = session().settings().skipArchiveInSearch() @@ -2011,9 +2163,9 @@ bool Widget::searchMessages(bool searchCache) { MTP_inputMessagesFilterEmpty(), MTP_int(0), // min_date MTP_int(0), // max_date - MTP_int(0), - MTP_inputPeerEmpty(), - MTP_int(0), + MTP_int(0), // offset_rate + MTP_inputPeerEmpty(), // offset_peer + MTP_int(0), // offset_id MTP_int(kSearchPerPage) )).done([=](const MTPmessages_Messages &result) { searchReceived(type, result, _searchRequest); @@ -2023,18 +2175,18 @@ bool Widget::searchMessages(bool searchCache) { _searchQueries.emplace(_searchRequest, _searchQuery); } } - const auto query = Api::ConvertPeerSearchQuery(q); - if (searchForPeersRequired(query)) { - if (searchCache) { - auto i = _peerSearchCache.find(query); + const auto peerQuery = Api::ConvertPeerSearchQuery(query); + if (searchForPeersRequired(peerQuery)) { + if (inCache) { + auto i = _peerSearchCache.find(peerQuery); if (i != _peerSearchCache.end()) { - _peerSearchQuery = query; + _peerSearchQuery = peerQuery; _peerSearchRequest = 0; peerSearchReceived(i->second, 0); result = true; } - } else if (_peerSearchQuery != query) { - _peerSearchQuery = query; + } else if (_peerSearchQuery != peerQuery) { + _peerSearchQuery = peerQuery; _peerSearchFull = false; _peerSearchRequest = _api.request(MTPcontacts_Search( MTP_string(_peerSearchQuery), @@ -2042,61 +2194,50 @@ bool Widget::searchMessages(bool searchCache) { )).done([=](const MTPcontacts_Found &result, mtpRequestId requestId) { peerSearchReceived(result, requestId); }).fail([=](const MTP::Error &error, mtpRequestId requestId) { - peopleFailed(error, requestId); + peerSearchFailed(error, requestId); }).send(); _peerSearchQueries.emplace(_peerSearchRequest, _peerSearchQuery); } } else { _api.request(base::take(_peerSearchRequest)).cancel(); - _peerSearchQuery = query; - _peerSearchFull = true; - peerSearchReceived( - MTP_contacts_found( - MTP_vector(0), - MTP_vector(0), - MTP_vector(0), - MTP_vector(0)), - 0); + _peerSearchQuery = peerQuery; + peerSearchApplyEmpty(0); } - if (searchForTopicsRequired(query)) { - if (searchCache) { - if (_topicSearchQuery != query) { + if (searchForTopicsRequired(peerQuery)) { + if (inCache) { + if (_topicSearchQuery != peerQuery) { result = false; } - } else if (_topicSearchQuery != query) { - _topicSearchQuery = query; + } else if (_topicSearchQuery != peerQuery) { + _topicSearchQuery = peerQuery; _topicSearchFull = false; searchTopics(); } } else { _api.request(base::take(_topicSearchRequest)).cancel(); - _topicSearchQuery = query; + _topicSearchQuery = peerQuery; _topicSearchFull = true; } return result; } bool Widget::searchForPeersRequired(const QString &query) const { - return !_searchInChat - && !_searchFromAuthor - && _searchTags.empty() + return _searchState.filterChatsList() && !_openedForum && !query.isEmpty() - && (query[0] != '#'); + && !IsHashtagSearchQuery(query); } bool Widget::searchForTopicsRequired(const QString &query) const { - return !_searchInChat - && !_searchFromAuthor - && _searchTags.empty() + return _searchState.filterChatsList() && _openedForum && !query.isEmpty() - && (query[0] != '#') + && !IsHashtagSearchQuery(query) && !_openedForum->topicsList()->loaded(); } -void Widget::needSearchMessages() { - if (!searchMessages(true)) { +void Widget::searchRequested() { + if (!search(true)) { _searchTimer.callOnce(AutoSearchTimeout); } } @@ -2105,68 +2246,9 @@ void Widget::showMainMenu() { controller()->widget()->showMainMenu(); } -void Widget::searchMessages(QString query, Key inChat, UserData *from) { - if (_childList) { - const auto forum = controller()->shownForum().current(); - const auto topic = inChat.topic(); - if ((forum && forum->channel() == inChat.peer()) - || (topic && topic->forum() == forum)) { - _childList->searchMessages(query, inChat, from); - return; - } - hideChildList(); - } - if (_openedFolder) { - controller()->closeFolder(); - } - - auto tags = Data::SearchTagsFromQuery(query); - if (!tags.empty()) { - if (!inChat.sublist()) { - inChat = session().data().history(session().user()); - } - query = QString(); - } - const auto inChatChanged = [&] { - const auto inPeer = inChat.peer(); - const auto inTopic = inChat.topic(); - if (!inTopic - && _openedForum - && inPeer == _openedForum->channel() - && _subsectionTopBar - && _subsectionTopBar->searchMode()) { - return false; - } else if ((inTopic || (inPeer && !inPeer->isForum())) - && (inChat == _searchInChat)) { - return false; - } else if (inPeer) { - if (const auto to = inPeer->migrateTo()) { - if (to == _searchInChat.peer() && !_searchInChat.topic()) { - return false; - } - } - } - return true; - }(); - if ((currentSearchQuery() != query) - || inChatChanged - || _searchTags != tags) { - if (inChat) { - cancelSearch(); - setSearchInChat(inChat, nullptr, tags); - } - setSearchQuery(query); - applySearchUpdate(true); - _searchTimer.cancel(); - searchMessages(); - - session().local().saveRecentSearchHashtags(query); - } - - if (inChat && from) { - setSearchInChat(inChat, from); - applySearchUpdate(true); - } +void Widget::searchMessages(SearchState state) { + applySearchState(std::move(state)); + session().local().saveRecentSearchHashtags(_searchState.query); } void Widget::searchTopics() { @@ -2222,7 +2304,7 @@ void Widget::searchMore() { const auto history = session().data().history(peer); const auto sublist = _openedForum ? nullptr - : _searchInChat.sublist(); + : _searchState.inChat.sublist(); const auto fromPeer = sublist ? nullptr : _searchQueryFrom; const auto savedPeer = sublist ? sublist->peer().get() @@ -2513,18 +2595,34 @@ void Widget::peerSearchReceived( } } +void Widget::searchApplyEmpty(SearchRequestType type, mtpRequestId id) { + _searchFull = _searchFullMigrated = true; + searchReceived( + type, + MTP_messages_messages( + MTP_vector(), + MTP_vector(), + MTP_vector()), + id); +} + +void Widget::peerSearchApplyEmpty(mtpRequestId id) { + _peerSearchFull = true; + peerSearchReceived( + MTP_contacts_found( + MTP_vector(0), + MTP_vector(0), + MTP_vector(0), + MTP_vector(0)), + id); +} + void Widget::searchFailed( SearchRequestType type, const MTP::Error &error, mtpRequestId requestId) { if (error.type() == u"SEARCH_QUERY_EMPTY"_q) { - searchReceived( - type, - MTP_messages_messages( - MTP_vector(), - MTP_vector(), - MTP_vector()), - requestId); + searchApplyEmpty(type, requestId); } else if (_searchRequest == requestId) { _searchRequest = 0; if (type == SearchRequestType::MigratedFromStart || type == SearchRequestType::MigratedFromOffset) { @@ -2535,8 +2633,8 @@ void Widget::searchFailed( } } -void Widget::peopleFailed(const MTP::Error &error, mtpRequestId requestId) { - if (_peerSearchRequest == requestId) { +void Widget::peerSearchFailed(const MTP::Error &error, mtpRequestId id) { + if (_peerSearchRequest == id) { _peerSearchRequest = 0; _peerSearchFull = true; } @@ -2641,59 +2739,58 @@ void Widget::listScrollUpdated() { } void Widget::updateCancelSearch() { - const auto shown = !_search->getLastText().isEmpty() - || (!_searchInChat + const auto shown = !_searchState.query.isEmpty() + || (!_searchState.inChat && (_searchHasFocus || _searchSuggestionsLocked)); _cancelSearch->toggle(shown, anim::type::normal); } -void Widget::applySearchUpdate(bool force) { - if (_showAnimation && !force) { - return; - } - - updateLockUnlockVisibility(anim::type::normal); - updateStoriesVisibility(); - const auto filterText = currentSearchQuery(); - _inner->applyFilterUpdate(filterText, force); - if (filterText.isEmpty() && !_searchFromAuthor && _searchTags.empty()) { - clearSearchCache(); - } - updateCancelSearch(); - if (!_postponeProcessSearchFocusChange) { - updateSuggestions(anim::type::instant); - } - updateLoadMoreChatsVisibility(); - updateJumpToDateVisibility(); - updateLockUnlockPosition(); - - if (filterText.isEmpty()) { - _peerSearchCache.clear(); - for (const auto &[requestId, query] : base::take(_peerSearchQueries)) { - _api.request(requestId).cancel(); +QString Widget::validateSearchQuery() { + const auto query = currentSearchQuery(); + if (_searchState.tab == ChatSearchTab::PublicPosts) { + _searchingHashtag = true; + const auto fixed = FixHashtagSearchQuery( + query, + currentSearchQueryCursorPosition()); + if (fixed.text != query) { + setSearchQuery(fixed.text, fixed.cursorPosition); } - _peerSearchQuery = QString(); + return fixed.text; + } else if (_searchingHashtag != IsHashtagSearchQuery(query)) { + _searchingHashtag = !_searchingHashtag; + updateSearchTabs(); } + return query; +} + +void Widget::applySearchUpdate() { + auto copy = _searchState; + copy.query = validateSearchQuery(); + applySearchState(std::move(copy)); if (_chooseFromUser->toggled() - || _searchFromAuthor - || !_searchTags.empty()) { + || _searchState.fromPeer + || !_searchState.tags.empty()) { auto switchToChooseFrom = HistoryView::SwitchToChooseFromQuery(); if (_lastSearchText != switchToChooseFrom && switchToChooseFrom.startsWith(_lastSearchText) - && filterText == switchToChooseFrom) { + && _searchState.query == switchToChooseFrom) { showSearchFrom(); } } - _lastSearchText = filterText; - updateForceDisplayWide(); + _lastSearchText = _searchState.query; } void Widget::updateForceDisplayWide() { + if (_childList) { + _childList->updateForceDisplayWide(); + return; + } controller()->setChatsForceDisplayWide(_searchHasFocus + || (_subsectionTopBar && _subsectionTopBar->searchHasFocus()) || _searchSuggestionsLocked - || !_search->getLastText().isEmpty() - || _searchInChat); + || !_searchState.query.isEmpty() + || _searchState.inChat); } void Widget::showForum( @@ -2707,7 +2804,7 @@ void Widget::showForum( changeOpenedForum(forum, params.animated); return; } - cancelSearch(); + cancelSearch(true); openChildList(forum, params); } @@ -2752,6 +2849,7 @@ void Widget::openChildList( *opacity = value; update(); _inner->update(); + _search->setVisible(value < 1.); if (!value && _childListShadow.get() != shadow) { delete shadow; } @@ -2769,6 +2867,7 @@ void Widget::openChildList( if (hasFocus()) { setInnerFocus(); } + updateForceDisplayWide(); } void Widget::closeChildList(anim::type animated) { @@ -2824,37 +2923,59 @@ void Widget::closeChildList(anim::type animated) { _childListShadow = nullptr; } updateStoriesVisibility(); + updateForceDisplayWide(); } -void Widget::searchInChat(Key chat) { - searchMessages(QString(), chat); -} - -bool Widget::setSearchInChat( - Key chat, - PeerData *from, - std::vector tags) { - if (_childList) { - if (_childList->setSearchInChat(chat, from, tags)) { +bool Widget::applySearchState(SearchState state) { + if (_searchState == state) { + return true; + } else if (_childList) { + if (_childList->applySearchState(state)) { return true; } hideChildList(); } - const auto peer = chat.peer(); - const auto topic = chat.topic(); + + // Adjust state to be consistent. + if (const auto peer = state.inChat.peer()) { + if (const auto to = peer->migrateTo()) { + state.inChat = peer->owner().history(to); + } + } + const auto peer = state.inChat.peer(); + const auto topic = state.inChat.topic(); const auto forum = peer ? peer->forum() : nullptr; - if (chat.folder() || (forum && !topic)) { - chat = Key(); + if (state.inChat.folder() || (forum && !topic)) { + state.inChat = {}; } - const auto searchInPeerUpdated = (_searchInChat != chat); - if (searchInPeerUpdated) { - from = nullptr; - } else if (!chat && !forum) { - from = nullptr; + if (!state.inChat && !forum && !_openedForum) { + state.fromPeer = nullptr; } - const auto searchFromUpdated = searchInPeerUpdated - || (_searchFromAuthor != from); - _searchFromAuthor = from; + if (state.tab == ChatSearchTab::PublicPosts + && !IsHashtagSearchQuery(state.query)) { + state.tab = (_openedForum && !state.inChat) + ? ChatSearchTab::ThisPeer + : ChatSearchTab::MyMessages; + } else if (!state.inChat && !_searchTabs) { + state.tab = (forum || _openedForum) + ? ChatSearchTab::ThisPeer + : ChatSearchTab::MyMessages; + } + if (!state.tags.empty()) { + state.inChat = session().data().history(session().user()); + } + + const auto clearQuery = state.fromPeer + && (_lastSearchText == HistoryView::SwitchToChooseFromQuery()); + if (clearQuery) { + state.query = _lastSearchText = QString(); + } + + const auto inChatChanged = (_searchState.inChat != state.inChat); + const auto fromPeerChanged = (_searchState.fromPeer != state.fromPeer); + const auto tagsChanged = (_searchState.tags != state.tags); + const auto queryChanged = (_searchState.query != state.query); + const auto tabChanged = (_searchState.tab != state.tab); if (forum) { if (_openedForum == forum) { @@ -2865,70 +2986,84 @@ bool Widget::setSearchInChat( } else { return false; } + } else if (peer && (_layout != Layout::Main)) { + return false; } - _searchInMigrated = nullptr; - if (peer && !forum) { - if (_layout != Layout::Main) { - return false; - } else if (const auto migrateTo = peer->migrateTo()) { - const auto to = peer->owner().history(migrateTo); - return setSearchInChat(to, from, tags); - } else if (const auto migrateFrom = peer->migrateFrom()) { - _searchInMigrated = peer->owner().history(migrateFrom); - } + + const auto migrateFrom = (peer && !topic) + ? peer->migrateFrom() + : nullptr; + _searchInMigrated = migrateFrom + ? peer->owner().history(migrateFrom).get() + : nullptr; + _searchState = state; + if (queryChanged) { + updateLockUnlockVisibility(anim::type::normal); + updateLoadMoreChatsVisibility(); } - if (searchInPeerUpdated) { - _searchInChat = chat; - controller()->setSearchInChat(_searchInChat); - updateSuggestions(anim::type::instant); - updateJumpToDateVisibility(); + if (inChatChanged) { + controller()->setSearchInChat(_searchState.inChat); + updateSearchTabs(); + } + if (queryChanged || inChatChanged) { + updateCancelSearch(); updateStoriesVisibility(); } - if (searchFromUpdated) { - updateSearchFromVisibility(); + updateJumpToDateVisibility(); + updateSearchFromVisibility(); + updateLockUnlockPosition(); + + if ((state.query.isEmpty() && !state.fromPeer && state.tags.empty()) + || inChatChanged + || fromPeerChanged + || tagsChanged + || tabChanged) { clearSearchCache(); } - updateLockUnlockPosition(); - if (_searchInChat && _layout == Layout::Main) { + if (state.query.isEmpty()) { + _peerSearchCache.clear(); + for (const auto &[requestId, query] : base::take(_peerSearchQueries)) { + _api.request(requestId).cancel(); + } + _peerSearchQuery = QString(); + } + + if (_searchState.inChat && _layout == Layout::Main) { controller()->closeFolder(); } - _searchTags = std::move(tags); - _inner->searchInChat(_searchInChat, _searchFromAuthor, _searchTags); + + if (_searchState.query != currentSearchQuery()) { + setSearchQuery(_searchState.query); + } + _inner->applySearchState(_searchState); + + if (!_postponeProcessSearchFocusChange) { + // Suggestions depend on _inner->state(), not on _searchState. + updateSuggestions(anim::type::instant); + } + _searchTagsLifetime = _inner->searchTagsChanges( ) | rpl::start_with_next([=](std::vector &&list) { - if (_searchTags != list) { - clearSearchCache(); - _searchTags = std::move(list); - if (_searchTags.empty()) { - applySearchUpdate(true); - } else { - searchMessages(); - } - } + auto copy = _searchState; + copy.tags = std::move(list); + applySearchState(std::move(copy)); }); if (_subsectionTopBar) { _subsectionTopBar->searchEnableJumpToDate( - _openedForum && _searchInChat); + _openedForum && _searchState.inChat); } - if (_searchFromAuthor - && _lastSearchText == HistoryView::SwitchToChooseFromQuery()) { - cancelSearch(); - } - if (_searchInChat || !_search->getLastText().isEmpty()) { + if (!_searchState.inChat && _searchState.query.isEmpty()) { + setInnerFocus(); + } else if (!_subsectionTopBar) { _search->setFocus(); - } else { - setInnerFocus(true); + } else if (_openedForum && !_subsectionTopBar->searchSetFocus()) { + _subsectionTopBar->toggleSearch(true, anim::type::normal); } updateForceDisplayWide(); + applySearchUpdate(); return true; } -bool Widget::setSearchInChat( - Key chat, - PeerData *from) { - return setSearchInChat(chat, from, {}); -} - void Widget::clearSearchCache() { _searchCache.clear(); _singleMessageSearch.clear(); @@ -2947,27 +3082,31 @@ void Widget::clearSearchCache() { } void Widget::showCalendar() { - if (_searchInChat) { - controller()->showCalendar(_searchInChat, QDate()); + if (_searchState.inChat) { + controller()->showCalendar(_searchState.inChat, QDate()); } } void Widget::showSearchFrom() { if (const auto peer = searchInPeer()) { - const auto weak = base::make_weak(_searchInChat.topic()); - const auto chat = (!_searchInChat && _openedForum) + const auto weak = base::make_weak(_searchState.inChat.topic()); + const auto chat = (!_searchState.inChat && _openedForum) ? Key(_openedForum->history()) - : _searchInChat; + : _searchState.inChat; auto box = SearchFromBox( peer, crl::guard(this, [=](not_null from) { controller()->hideLayer(); + auto copy = _searchState; if (!chat.topic()) { - setSearchInChat(chat, from); + copy.inChat = chat; + copy.fromPeer = from; + applySearchState(std::move(copy)); } else if (const auto strong = weak.get()) { - setSearchInChat(strong, from); + copy.inChat = strong; + copy.fromPeer = from; + applySearchState(std::move(copy)); } - applySearchUpdate(true); }), crl::guard(this, [=] { _search->setFocus(); })); if (box) { @@ -3013,9 +3152,8 @@ void Widget::completeHashtag(QString tag) { } if (cur - start - 1 == tag.size() && cur < t.size() && t.at(cur) == ' ') ++cur; hashtag = t.mid(0, start + 1) + tag + ' ' + t.mid(cur); - _search->setText(hashtag); - _search->setCursorPosition(start + 1 + tag.size() + 1); - applySearchUpdate(true); + setSearchQuery(hashtag, start + 1 + tag.size() + 1); + applySearchUpdate(); return; } break; @@ -3023,9 +3161,10 @@ void Widget::completeHashtag(QString tag) { break; } } - _search->setText(t.mid(0, cur) + '#' + tag + ' ' + t.mid(cur)); - _search->setCursorPosition(cur + 1 + tag.size() + 1); - applySearchUpdate(true); + setSearchQuery( + t.mid(0, cur) + '#' + tag + ' ' + t.mid(cur), + cur + 1 + tag.size() + 1); + applySearchUpdate(); } void Widget::resizeEvent(QResizeEvent *e) { @@ -3043,8 +3182,8 @@ void Widget::updateLockUnlockVisibility(anim::type animated) { || _childList || _searchHasFocus || _searchSuggestionsLocked - || _searchInChat - || !_search->getLastText().isEmpty(); + || _searchState.inChat + || !_searchState.query.isEmpty(); if (_lockUnlock->toggled() == hidden) { const auto stories = _stories && !_stories->empty(); _lockUnlock->toggle( @@ -3063,7 +3202,7 @@ void Widget::updateLoadMoreChatsVisibility() { } const auto hidden = (_openedFolder != nullptr) || (_openedForum != nullptr) - || !currentSearchQuery().isEmpty(); + || !_searchState.query.isEmpty(); if (_loadMoreChats->isHidden() != hidden) { _loadMoreChats->setVisible(!hidden); updateControlsGeometry(); @@ -3076,7 +3215,7 @@ void Widget::updateJumpToDateVisibility(bool fast) { } _jumpToDate->toggle( - (_searchInChat && _search->getLastText().isEmpty()), + (searchInPeer() && _searchState.query.isEmpty()), fast ? anim::type::instant : anim::type::normal); } @@ -3084,16 +3223,18 @@ void Widget::updateSearchFromVisibility(bool fast) { auto visible = [&] { if (const auto peer = searchInPeer()) { if (peer->isChat() || peer->isMegagroup()) { - return !_searchFromAuthor; + return !_searchState.fromPeer; } } return false; }(); - auto changed = (visible == !_chooseFromUser->toggled()); + const auto changed = (visible == !_chooseFromUser->toggled()); _chooseFromUser->toggle( visible, fast ? anim::type::instant : anim::type::normal); - if (changed) { + if (_subsectionTopBar) { + _subsectionTopBar->searchEnableChooseFromUser(true, visible); + } else if (changed) { auto additional = QMargins(); if (visible) { additional.setRight(_chooseFromUser->width()); @@ -3217,6 +3358,9 @@ void Widget::updateControlsGeometry() { if (_forumRequestsBar) { _forumRequestsBar->resizeToWidth(barw); } + if (_searchTabs) { + _searchTabs->resizeToWidth(barw); + } _updateScrollGeometryCached = [=] { const auto moreChatsBarTop = expandedStoriesTop + ((!_stories || _stories->isHidden()) ? 0 : _aboveScrollAdded); @@ -3238,8 +3382,13 @@ void Widget::updateControlsGeometry() { if (_forumReportBar) { _forumReportBar->bar().move(0, forumReportTop); } - const auto scrollTop = forumReportTop + const auto searchTabsTop = forumReportTop + (_forumReportBar ? _forumReportBar->bar().height() : 0); + if (_searchTabs) { + _searchTabs->move(0, searchTabsTop); + } + const auto scrollTop = searchTabsTop + + (_searchTabs ? _searchTabs->height() : 0); const auto scrollHeight = height() - scrollTop - bottomSkip; const auto wasScrollHeight = _scroll->height(); _scroll->setGeometry(0, scrollTop, scrollWidth, scrollHeight); @@ -3300,7 +3449,8 @@ void Widget::keyPressEvent(QKeyEvent *e) { //} } else if ((e->key() == Qt::Key_Backspace || e->key() == Qt::Key_Tab) && _searchHasFocus - && !_searchInChat) { + && !_searchState.inChat + && _searchState.query.isEmpty()) { escape(); } else if (e->key() == Qt::Key_Return || e->key() == Qt::Key_Enter) { submit(); @@ -3459,15 +3609,39 @@ void Widget::cancelSearchRequest() { } PeerData *Widget::searchInPeer() const { - return _openedForum + return (_searchState.tab == ChatSearchTab::MyMessages + || _searchState.tab == ChatSearchTab::PublicPosts) + ? nullptr + : _openedForum ? _openedForum->channel().get() - : _searchInChat.sublist() + : _searchState.inChat.sublist() ? session().user().get() - : _searchInChat.peer(); + : _searchState.inChat.peer(); } Data::ForumTopic *Widget::searchInTopic() const { - return _searchInChat.topic(); + return (_searchState.tab != ChatSearchTab::ThisTopic) + ? nullptr + : _searchState.inChat.topic(); +} + +PeerData *Widget::searchFromPeer() const { + if (const auto peer = searchInPeer()) { + if (peer->isChat() || peer->isMegagroup()) { + return _searchState.fromPeer; + } + } + return nullptr; +} + +const std::vector &Widget::searchInTags() const { + if (const auto peer = searchInPeer()) { + if (peer->isSelf() && _searchState.tab == ChatSearchTab::ThisPeer) { + return _searchState.tags; + } + } + static const auto kEmpty = std::vector(); + return kEmpty; } QString Widget::currentSearchQuery() const { @@ -3476,6 +3650,12 @@ QString Widget::currentSearchQuery() const { : _search->getLastText(); } +int Widget::currentSearchQueryCursorPosition() const { + return _subsectionTopBar + ? _subsectionTopBar->searchQueryCursorPosition() + : _search->textCursor().position(); +} + void Widget::clearSearchField() { if (_subsectionTopBar) { _subsectionTopBar->searchClear(); @@ -3484,28 +3664,44 @@ void Widget::clearSearchField() { } } -void Widget::setSearchQuery(const QString &query) { +void Widget::setSearchQuery(const QString &query, int cursorPosition) { + if (query.isEmpty()) { + clearSearchField(); + return; + } + if (cursorPosition < 0) { + cursorPosition = query.size(); + } if (_subsectionTopBar) { - _subsectionTopBar->searchSetText(query); + _subsectionTopBar->searchSetText(query, cursorPosition); } else { _search->setText(query); + _search->setCursorPosition(cursorPosition); } } -bool Widget::cancelSearch() { - auto clearingQuery = !currentSearchQuery().isEmpty(); - auto clearingInChat = false; +bool Widget::cancelSearch(bool forceFullCancel) { cancelSearchRequest(); - if (!clearingQuery && (_searchInChat || _searchFromAuthor)) { - if (_searchInChat && controller()->adaptive().isOneColumn()) { - if (const auto thread = _searchInChat.thread()) { + auto updatedState = _searchState; + const auto clearingQuery = !updatedState.query.isEmpty(); + auto clearingInChat = (forceFullCancel || !clearingQuery) + && (updatedState.inChat + || updatedState.fromPeer + || !updatedState.tags.empty()); + if (clearingQuery) { + updatedState.query = QString(); + } + if (clearingInChat) { + if (updatedState.inChat && controller()->adaptive().isOneColumn()) { + if (const auto thread = updatedState.inChat.thread()) { controller()->showThread(thread); } else { Unexpected("Empty key in cancelSearch()."); } } - setSearchInChat(Key()); - clearingInChat = true; + updatedState.inChat = {}; + updatedState.fromPeer = nullptr; + updatedState.tags = {}; } if (!clearingQuery && _subsectionTopBar @@ -3513,9 +3709,9 @@ bool Widget::cancelSearch() { setInnerFocus(true); clearingInChat = true; } - const auto clearSearchFocus = !_searchInChat + const auto clearSearchFocus = (forceFullCancel || !updatedState.inChat) && (_searchHasFocus || _searchSuggestionsLocked); - if (!_searchInChat && _suggestions) { + if (!updatedState.inChat && _suggestions) { _suggestions->clearPersistance(); _searchSuggestionsLocked = false; } @@ -3526,33 +3722,14 @@ bool Widget::cancelSearch() { _lastSearchPeer = nullptr; _lastSearchId = _lastSearchMigratedId = 0; _inner->clearFilter(); - clearSearchField(); - applySearchUpdate(); + applySearchState(std::move(updatedState)); if (_suggestions && clearSearchFocus) { setInnerFocus(true); } + updateForceDisplayWide(); return clearingQuery || clearingInChat || clearSearchFocus; } -void Widget::cancelSearchInChat() { - cancelSearchRequest(); - const auto isOneColumn = controller()->adaptive().isOneColumn(); - if (_searchInChat) { - if (isOneColumn && currentSearchQuery().trimmed().isEmpty()) { - if (const auto thread = _searchInChat.thread()) { - controller()->showThread(thread); - } else { - Unexpected("Empty key in cancelSearchInPeer()."); - } - } - setSearchInChat(Key()); - } - applySearchUpdate(true); - if (!isOneColumn && _search->getLastText().isEmpty()) { - controller()->content()->dialogsCancelled(); - } -} - Widget::~Widget() { cancelSearchRequest(); diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.h b/Telegram/SourceFiles/dialogs/dialogs_widget.h index 903ddfba2..33d239a48 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.h @@ -76,6 +76,8 @@ struct ChosenRow; class InnerWidget; enum class SearchRequestType; class Suggestions; +class ChatSearchTabs; +enum class ChatSearchTab : uchar; class Widget final : public Window::AbstractSectionWidget { public: @@ -98,7 +100,6 @@ public: void showForum( not_null forum, const Window::SectionShow ¶ms); - void searchInChat(Key chat); void setInnerFocus(bool unfocusSearch = false); [[nodiscard]] bool searchHasFocus() const; @@ -120,9 +121,7 @@ public: void scrollToEntry(const RowDescriptor &entry); - void searchMessages(QString query, Key inChat = {}, UserData *from = nullptr); - void searchTopics(); - void searchMore(); + void searchMessages(SearchState state); [[nodiscard]] RowDescriptor resolveChatNext(RowDescriptor from = {}) const; [[nodiscard]] RowDescriptor resolveChatPrevious(RowDescriptor from = {}) const; @@ -132,7 +131,7 @@ public: bool floatPlayerHandleWheelEvent(QEvent *e) override; QRect floatPlayerAvailableRect() override; - bool cancelSearch(); + bool cancelSearch(bool forceFullCancel = false); bool cancelSearchByMouseBack(); QVariant inputMethodQuery(Qt::InputMethodQuery query) const override; @@ -152,14 +151,16 @@ protected: private: void chosenRow(const ChosenRow &row); void listScrollUpdated(); - void cancelSearchInChat(); void searchCursorMoved(); void completeHashtag(QString tag); [[nodiscard]] QString currentSearchQuery() const; + [[nodiscard]] int currentSearchQueryCursorPosition() const; void clearSearchField(); - bool searchMessages(bool searchCache = false); - void needSearchMessages(); + void searchRequested(); + bool search(bool inCache = false); + void searchTopics(); + void searchMore(); void slideFinished(); void searchReceived( @@ -174,8 +175,11 @@ private: void cancelSearchRequest(); [[nodiscard]] PeerData *searchInPeer() const; [[nodiscard]] Data::ForumTopic *searchInTopic() const; + [[nodiscard]] PeerData *searchFromPeer() const; + [[nodiscard]] const std::vector &searchInTags() const; void setupSupportMode(); + void setupTouchChatPreview(); void setupConnectingWidget(); void setupMainMenuToggle(); void setupMoreChatsBar(); @@ -188,18 +192,15 @@ private: void trackScroll(not_null widget); [[nodiscard]] bool searchForPeersRequired(const QString &query) const; [[nodiscard]] bool searchForTopicsRequired(const QString &query) const; - bool setSearchInChat( - Key chat, - PeerData *from, - std::vector tags); - bool setSearchInChat( - Key chat, - PeerData *from = nullptr); + + // Child list may be unable to set specific search state. + bool applySearchState(SearchState state); + void showCalendar(); void showSearchFrom(); void showMainMenu(); void clearSearchCache(); - void setSearchQuery(const QString &query); + void setSearchQuery(const QString &query, int cursorPosition = -1); void updateControlsVisibility(bool fast = false); void updateLockUnlockVisibility( anim::type animated = anim::type::instant); @@ -232,7 +233,8 @@ private: void fullSearchRefreshOn(rpl::producer<> events); void updateCancelSearch(); - void applySearchUpdate(bool force = false); + [[nodiscard]] QString validateSearchQuery(); + void applySearchUpdate(); void refreshLoadMoreButton(bool mayBlock, bool isBlocked); void loadMoreBlockedByDate(); @@ -240,7 +242,9 @@ private: SearchRequestType type, const MTP::Error &error, mtpRequestId requestId); - void peopleFailed(const MTP::Error &error, mtpRequestId requestId); + void peerSearchFailed(const MTP::Error &error, mtpRequestId requestId); + void searchApplyEmpty(SearchRequestType type, mtpRequestId id); + void peerSearchApplyEmpty(mtpRequestId id); void updateForceDisplayWide(); void scrollToDefault(bool verytop = false); @@ -251,6 +255,7 @@ private: void updateScrollUpPosition(); void updateLockUnlockPosition(); void updateSuggestions(anim::type animated); + void updateSearchTabs(); void processSearchFocusChange(); [[nodiscard]] bool redirectToSearchPossible() const; @@ -266,7 +271,7 @@ private: Layout _layout = Layout::Main; int _narrowWidth = 0; object_ptr _searchControls; - object_ptr _subsectionTopBar = { nullptr } ; + object_ptr _subsectionTopBar = { nullptr }; struct { object_ptr toggle; object_ptr under; @@ -289,6 +294,7 @@ private: QPointer _inner; std::unique_ptr _suggestions; std::vector> _hidingSuggestions; + std::unique_ptr _searchTabs; class BottomButton; object_ptr _updateTelegram = { nullptr }; object_ptr _loadMoreChats = { nullptr }; @@ -304,17 +310,17 @@ private: object_ptr _scrollToTop; bool _scrollToTopIsShown = false; bool _forumSearchRequested = false; + bool _searchingHashtag = false; Data::Folder *_openedFolder = nullptr; Data::Forum *_openedForum = nullptr; - Key _searchInChat; + SearchState _searchState; History *_searchInMigrated = nullptr; - PeerData *_searchFromAuthor = nullptr; - std::vector _searchTags; rpl::lifetime _searchTagsLifetime; QString _lastSearchText; bool _searchSuggestionsLocked = false; bool _searchHasFocus = false; + bool _processingSearch = false; rpl::event_stream> _storiesContents; base::flat_map _storiesUserpicsViewsHidden; @@ -344,6 +350,7 @@ private: QString _searchQuery; PeerData *_searchQueryFrom = nullptr; std::vector _searchQueryTags; + ChatSearchTab _searchQueryTab = {}; int32 _searchNextRate = 0; bool _searchFull = false; bool _searchFullMigrated = false; diff --git a/Telegram/SourceFiles/dialogs/ui/chat_search_empty.cpp b/Telegram/SourceFiles/dialogs/ui/chat_search_empty.cpp new file mode 100644 index 000000000..855112f85 --- /dev/null +++ b/Telegram/SourceFiles/dialogs/ui/chat_search_empty.cpp @@ -0,0 +1,82 @@ +/* +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 "dialogs/ui/chat_search_empty.h" + +#include "base/object_ptr.h" +#include "lottie/lottie_icon.h" +#include "settings/settings_common.h" +#include "ui/widgets/labels.h" +#include "styles/style_dialogs.h" + +namespace Dialogs { + +SearchEmpty::SearchEmpty( + QWidget *parent, + Icon icon, + rpl::producer text) +: RpWidget(parent) { + setup(icon, std::move(text)); +} + +void SearchEmpty::setMinimalHeight(int minimalHeight) { + const auto minimal = st::recentPeersEmptyHeightMin; + resize(width(), std::max(minimalHeight, minimal)); +} + +void SearchEmpty::setup(Icon icon, rpl::producer text) { + const auto label = Ui::CreateChild( + this, + std::move(text), + st::defaultPeerListAbout); + label->setClickHandlerFilter([=](const auto &, Qt::MouseButton button) { + if (button == Qt::LeftButton) { + _linkClicks.fire({}); + } + return false; + }); + const auto size = st::recentPeersEmptySize; + const auto animation = [&] { + switch (icon) { + case Icon::Search: return u"search"_q; + case Icon::NoResults: return u"noresults"_q; + } + Unexpected("Icon in SearchEmpty::setup."); + }(); + const auto [widget, animate] = Settings::CreateLottieIcon( + this, + { + .name = animation, + .sizeOverride = { size, size }, + }, + st::recentPeersEmptyMargin); + const auto animated = widget.data(); + + sizeValue() | rpl::start_with_next([=](QSize size) { + const auto padding = st::recentPeersEmptyMargin; + const auto paddings = padding.left() + padding.right(); + label->resizeToWidth(size.width() - paddings); + const auto x = (size.width() - animated->width()) / 2; + const auto y = (size.height() - animated->height()) / 3; + const auto top = y + animated->height() + st::recentPeersEmptySkip; + const auto sub = std::max(top + label->height() - size.height(), 0); + animated->move(x, y - sub); + label->move((size.width() - label->width()) / 2, top - sub); + }, lifetime()); + + _animate = [animate] { + animate(anim::repeat::once); + }; +} + +void SearchEmpty::animate() { + if (const auto onstack = _animate) { + onstack(); + } +} + +} // namespace Dialogs diff --git a/Telegram/SourceFiles/dialogs/ui/chat_search_empty.h b/Telegram/SourceFiles/dialogs/ui/chat_search_empty.h new file mode 100644 index 000000000..6ddf52676 --- /dev/null +++ b/Telegram/SourceFiles/dialogs/ui/chat_search_empty.h @@ -0,0 +1,43 @@ +/* +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 "ui/rp_widget.h" + +namespace Dialogs { + +enum class SearchEmptyIcon { + Search, + NoResults, +}; + +class SearchEmpty final : public Ui::RpWidget { +public: + using Icon = SearchEmptyIcon; + + SearchEmpty( + QWidget *parent, + Icon icon, + rpl::producer text); + + void setMinimalHeight(int minimalHeight); + [[nodiscard]] rpl::producer<> linkClicks() const { + return _linkClicks.events(); + } + + void animate(); + +private: + void setup(Icon icon, rpl::producer text); + + Fn _animate; + rpl::event_stream<> _linkClicks; + +}; + +} // namespace Dialogs diff --git a/Telegram/SourceFiles/dialogs/ui/chat_search_tabs.cpp b/Telegram/SourceFiles/dialogs/ui/chat_search_tabs.cpp new file mode 100644 index 000000000..24fc21507 --- /dev/null +++ b/Telegram/SourceFiles/dialogs/ui/chat_search_tabs.cpp @@ -0,0 +1,201 @@ +/* +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 "dialogs/ui/chat_search_tabs.h" + +#include "lang/lang_keys.h" +#include "ui/widgets/discrete_sliders.h" +#include "ui/widgets/shadow.h" +#include "styles/style_dialogs.h" + +namespace Dialogs { +namespace { + +[[nodiscard]] QString TabLabel( + ChatSearchTab tab, + ChatSearchPeerTabType type = {}) { + switch (tab) { + case ChatSearchTab::MyMessages: + return tr::lng_search_tab_my_messages(tr::now); + case ChatSearchTab::ThisTopic: + return tr::lng_search_tab_this_topic(tr::now); + case ChatSearchTab::ThisPeer: + switch (type) { + case ChatSearchPeerTabType::Chat: + return tr::lng_search_tab_this_chat(tr::now); + case ChatSearchPeerTabType::Channel: + return tr::lng_search_tab_this_channel(tr::now); + case ChatSearchPeerTabType::Group: + return tr::lng_search_tab_this_group(tr::now); + } + Unexpected("Type in Dialogs::TabLabel."); + case ChatSearchTab::PublicPosts: + return tr::lng_search_tab_public_posts(tr::now); + } + Unexpected("Tab in Dialogs::TabLabel."); +} + +} // namespace + +TextWithEntities DefaultShortLabel(ChatSearchTab tab) { + // Return them in QString::fromUtf8 format. + switch (tab) { + case ChatSearchTab::MyMessages: + return { QString::fromUtf8("\xf0\x9f\x93\xa8") }; + case ChatSearchTab::PublicPosts: + return { QString::fromUtf8("\xf0\x9f\x8c\x8e") }; + } + Unexpected("Tab in Dialogs::DefaultShortLabel."); +} + +FixedHashtagSearchQuery FixHashtagSearchQuery( + const QString &query, + int cursorPosition) { + const auto trimmed = query.trimmed(); + const auto hash = int(trimmed.isEmpty() + ? query.size() + : query.indexOf(trimmed)); + const auto start = std::min(cursorPosition, hash); + auto result = query.mid(0, start); + for (const auto &ch : query.mid(start)) { + if (ch.isSpace()) { + if (cursorPosition > result.size()) { + --cursorPosition; + } + continue; + } else if (result.size() == start) { + result += '#'; + if (ch != '#') { + ++cursorPosition; + } + } + if (ch != '#') { + result += ch; + } + } + if (result.size() == start) { + result += '#'; + ++cursorPosition; + } + return { result, cursorPosition }; +} + +bool IsHashtagSearchQuery(const QString &query) { + const auto trimmed = query.trimmed(); + if (trimmed.isEmpty() || trimmed[0] != '#') { + return false; + } + for (const auto &ch : trimmed) { + if (ch.isSpace()) { + return false; + } + } + return true; +} + +ChatSearchTabs::ChatSearchTabs( + QWidget *parent, + ChatSearchTab active, + Fn)> markedTextContext) +: RpWidget(parent) +, _tabs(std::make_unique(this, st::dialogsSearchTabs)) +, _shadow(std::make_unique(this)) +, _markedTextContext(std::move(markedTextContext)) +, _active(active) { + _tabs->move(st::dialogsSearchTabsPadding, 0); + _tabs->sectionActivated( + ) | rpl::start_with_next([=](int index) { + _active = _list[index].value; + }, lifetime()); +} + +ChatSearchTabs::~ChatSearchTabs() = default; + +void ChatSearchTabs::setTabShortLabels( + std::vector labels, + ChatSearchTab active, + ChatSearchPeerTabType peerTabType) { + const auto &st = st::dialogsSearchTabs; + const auto &font = st.labelStyle.font; + _list.clear(); + _list.reserve(labels.size()); + + auto widthTotal = 0; + for (const auto tab : { + ChatSearchTab::ThisTopic, + ChatSearchTab::ThisPeer, + ChatSearchTab::MyMessages, + ChatSearchTab::PublicPosts, + }) { + const auto i = ranges::find(labels, tab, &ShortLabel::tab); + if (i != end(labels) && !i->label.empty()) { + const auto label = TabLabel(tab, peerTabType); + const auto widthFull = font->width(label) + st.strictSkip; + _list.push_back({ + .value = tab, + .label = label, + .shortLabel = i->label, + .widthFull = widthFull, + }); + widthTotal += widthFull; + } + } + const auto widthSingleEmoji = st::emojiSize + st.strictSkip; + for (const auto tab : { + ChatSearchTab::PublicPosts, + ChatSearchTab::ThisTopic, + ChatSearchTab::ThisPeer, + ChatSearchTab::MyMessages, + }) { + const auto i = ranges::find(_list, tab, &Tab::value); + if (i != end(_list)) { + i->widthThresholdForShort = widthTotal; + widthTotal -= i->widthFull; + widthTotal += widthSingleEmoji; + } + } + refillTabs(active, width()); +} + +rpl::producer ChatSearchTabs::tabChanges() const { + return _active.changes(); +} + +void ChatSearchTabs::refillTabs( + ChatSearchTab active, + int newWidth) { + auto labels = std::vector(); + const auto available = newWidth - 2 * st::dialogsSearchTabsPadding; + for (const auto &tab : _list) { + auto label = (available < tab.widthThresholdForShort) + ? tab.shortLabel + : TextWithEntities{ tab.label }; + labels.push_back(std::move(label)); + } + _tabs->setSections(labels, _markedTextContext([=] { update(); })); + + const auto i = ranges::find(_list, active, &Tab::value); + Assert(i != end(_list)); + _tabs->setActiveSectionFast(i - begin(_list)); + _tabs->resizeToWidth(newWidth); +} + +int ChatSearchTabs::resizeGetHeight(int newWidth) { + refillTabs(_active.current(), newWidth); + _shadow->setGeometry( + 0, + _tabs->y() + _tabs->height() - st::lineWidth, + newWidth, + st::lineWidth); + return _tabs->height(); +} + +void ChatSearchTabs::paintEvent(QPaintEvent *e) { + QPainter(this).fillRect(e->rect(), st::dialogsBg); +} + +} // namespace Dialogs diff --git a/Telegram/SourceFiles/dialogs/ui/chat_search_tabs.h b/Telegram/SourceFiles/dialogs/ui/chat_search_tabs.h new file mode 100644 index 000000000..720bf5734 --- /dev/null +++ b/Telegram/SourceFiles/dialogs/ui/chat_search_tabs.h @@ -0,0 +1,89 @@ +/* +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 "ui/rp_widget.h" + +namespace Ui { +class SettingsSlider; +class PlainShadow; +} // namespace Ui + +namespace Dialogs { + +enum class ChatSearchTab : uchar { + MyMessages, + ThisTopic, + ThisPeer, + PublicPosts, +}; + +enum class ChatSearchPeerTabType : uchar { + Chat, + Channel, + Group, +}; + +// Available for MyMessages and PublicPosts. +[[nodiscard]] TextWithEntities DefaultShortLabel(ChatSearchTab tab); + +class ChatSearchTabs final : public Ui::RpWidget { +public: + ChatSearchTabs( + QWidget *parent, + ChatSearchTab active, + Fn)> markedTextContext); + ~ChatSearchTabs(); + + // A [custom] emoji to use when there is not enough space for text. + // Only tabs with available short labels are shown. + struct ShortLabel { + ChatSearchTab tab = {}; + TextWithEntities label; + }; + void setTabShortLabels( + std::vector labels, + ChatSearchTab active, + ChatSearchPeerTabType peerTabType); + + [[nodiscard]] rpl::producer tabChanges() const; + +private: + struct Tab { + ChatSearchTab value = {}; + QString label; + TextWithEntities shortLabel; + int widthFull = 0; + int widthThresholdForShort = 0; + }; + + void refreshTabs(ChatSearchTab active); + void refillTabs(ChatSearchTab active, int newWidth); + int resizeGetHeight(int newWidth) override; + void paintEvent(QPaintEvent *e) override; + + const std::unique_ptr _tabs; + const std::unique_ptr _shadow; + const Fn)> _markedTextContext; + + std::vector _list; + rpl::variable _active; + +}; + +struct FixedHashtagSearchQuery { + QString text; + int cursorPosition = 0; +}; +[[nodiscard]] FixedHashtagSearchQuery FixHashtagSearchQuery( + const QString &query, + int cursorPosition); + +[[nodiscard]] bool IsHashtagSearchQuery(const QString &query); + +} // namespace Dialogs diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp index f7cbff178..809a74eaf 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp @@ -259,9 +259,9 @@ void PaintRow( not_null entry, VideoUserpic *videoUserpic, PeerData *from, - PeerBadge &fromBadge, + PeerBadge &rowBadge, Fn customEmojiRepaint, - const Text::String &fromName, + const Text::String &rowName, const HiddenSenderInfo *hiddenSenderInfo, HistoryItem *item, const Data::Draft *draft, @@ -626,10 +626,10 @@ void PaintRow( p.drawTextLeft(rectForName.left(), rectForName.top(), context.width, text); } else if (from) { if ((history || sublist) && !context.search) { - const auto badgeWidth = fromBadge.drawGetWidth( + const auto badgeWidth = rowBadge.drawGetWidth( p, rectForName, - fromName.maxWidth(), + rowName.maxWidth(), context.width, { .peer = from, @@ -663,7 +663,7 @@ void PaintRow( : context.selected ? st::dialogsNameFgOver : st::dialogsNameFg); - fromName.drawElided(p, rectForName.left(), rectForName.top(), rectForName.width()); + rowName.drawElided(p, rectForName.left(), rectForName.top(), rectForName.width()); } else if (hiddenSenderInfo) { p.setPen(context.active ? st::dialogsNameFgActive @@ -681,12 +681,7 @@ void PaintRow( : (context.selected ? st::dialogsNameFgOver : st::dialogsNameFg)); - auto text = entry->chatListName(); // TODO feed name with emoji - auto textWidth = st::semiboldFont->width(text); - if (textWidth > rectForName.width()) { - text = st::semiboldFont->elided(text, rectForName.width()); - } - p.drawTextLeft(rectForName.left(), rectForName.top(), context.width, text); + rowName.drawElided(p, rectForName.left(), rectForName.top(), rectForName.width()); } } diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp index 37be201fa..ad6fe1f69 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_chat_participants.h" #include "apiwrap.h" #include "base/unixtime.h" +#include "base/qt/qt_key_modifiers.h" #include "boxes/peer_list_box.h" #include "data/components/recent_peers.h" #include "data/components/top_peers.h" @@ -20,12 +21,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_peer_values.h" #include "data/data_session.h" #include "data/data_user.h" +#include "dialogs/ui/chat_search_empty.h" #include "history/history.h" #include "lang/lang_keys.h" -#include "lottie/lottie_icon.h" #include "main/main_session.h" #include "settings/settings_common.h" #include "ui/boxes/confirm_box.h" +#include "ui/text/text_utilities.h" #include "ui/widgets/menu/menu_add_action_callback_factory.h" #include "ui/widgets/buttons.h" #include "ui/widgets/discrete_sliders.h" @@ -84,9 +86,33 @@ private: }; -class RecentsController final +class ControllerWithPreviews : public PeerListController , public base::has_weak_ptr { +public: + explicit ControllerWithPreviews( + not_null window); + + [[nodiscard]] not_null window() const { + return _window; + } + + bool rowTrackPress(not_null row) override; + void rowTrackPressCancel() override; + bool rowTrackPressSkipMouseSelection() override; + + bool processTouchEvent(not_null e); + void setupTouchChatPreview(not_null scroll); + +private: + const not_null _window; + + std::optional _chatPreviewTouchGlobal; + rpl::event_stream<> _touchCancelRequests; + +}; + +class RecentsController final : public ControllerWithPreviews { public: RecentsController( not_null window, @@ -113,7 +139,6 @@ private: void subscribeToEvents(); [[nodiscard]] Fn removeAllCallback(); - const not_null _window; RecentPeersList _recent; rpl::variable _count; rpl::event_stream> _chosen; @@ -135,9 +160,7 @@ private: }; -class MyChannelsController final - : public PeerListController - , public base::has_weak_ptr { +class MyChannelsController final : public ControllerWithPreviews { public: explicit MyChannelsController( not_null window); @@ -161,7 +184,6 @@ private: void appendRow(not_null channel); void fill(bool force = false); - const not_null _window; std::vector> _channels; rpl::variable _toggleExpanded = nullptr; rpl::variable _count = 0; @@ -171,9 +193,7 @@ private: }; -class RecommendationsController final - : public PeerListController - , public base::has_weak_ptr { +class RecommendationsController final : public ControllerWithPreviews { public: explicit RecommendationsController( not_null window); @@ -199,7 +219,6 @@ private: void setupDivider(); void appendRow(not_null channel); - const not_null _window; rpl::variable _count; History *_activeHistory = nullptr; bool _requested = false; @@ -402,10 +421,97 @@ const style::PeerListItem &ChannelRow::computeSt( return _active ? st::recentPeersItemActive : st::recentPeersItem; } +ControllerWithPreviews::ControllerWithPreviews( + not_null window) +: _window(window) { +} + +bool ControllerWithPreviews::rowTrackPress(not_null row) { + const auto peer = row->peer(); + const auto history = peer->owner().history(peer); + const auto callback = crl::guard(this, [=](bool shown) { + delegate()->peerListPressLeftToContextMenu(shown); + }); + if (base::IsAltPressed()) { + _window->showChatPreview( + { history, FullMsgId() }, + callback, + nullptr, + _chatPreviewTouchGlobal); + return false; + } + const auto point = delegate()->peerListLastRowMousePosition(); + const auto &st = computeListSt().item; + if (point && point->x() < st.photoPosition.x() + st.photoSize) { + _window->scheduleChatPreview( + { history, FullMsgId() }, + callback, + nullptr, + _chatPreviewTouchGlobal); + return true; + } + return false; +} + +void ControllerWithPreviews::rowTrackPressCancel() { + _chatPreviewTouchGlobal = {}; + _window->cancelScheduledPreview(); +} + +bool ControllerWithPreviews::rowTrackPressSkipMouseSelection() { + return _chatPreviewTouchGlobal.has_value(); +} + +bool ControllerWithPreviews::processTouchEvent(not_null e) { + const auto point = e->touchPoints().empty() + ? std::optional() + : e->touchPoints().front().screenPos().toPoint(); + switch (e->type()) { + case QEvent::TouchBegin: { + if (!point) { + return false; + } + _chatPreviewTouchGlobal = point; + if (!delegate()->peerListTrackRowPressFromGlobal(*point)) { + _chatPreviewTouchGlobal = {}; + } + } break; + + case QEvent::TouchUpdate: { + if (!point) { + return false; + } + if (_chatPreviewTouchGlobal) { + const auto delta = (*_chatPreviewTouchGlobal - *point); + if (delta.manhattanLength() > computeListSt().item.photoSize) { + rowTrackPressCancel(); + } + } + } break; + + case QEvent::TouchEnd: + case QEvent::TouchCancel: { + if (_chatPreviewTouchGlobal) { + rowTrackPressCancel(); + } + } break; + } + return false; +} + +void ControllerWithPreviews::setupTouchChatPreview( + not_null scroll) { + _touchCancelRequests.events() | rpl::start_with_next([=] { + QTouchEvent ev(QEvent::TouchCancel); + ev.setTimestamp(crl::now()); + QGuiApplication::sendEvent(scroll, &ev); + }, lifetime()); +} + RecentsController::RecentsController( not_null window, RecentPeersList list) -: _window(window) +: ControllerWithPreviews(window) , _recent(std::move(list)) { } @@ -427,7 +533,7 @@ void RecentsController::rowClicked(not_null row) { Fn RecentsController::removeAllCallback() { const auto weak = base::make_weak(this); - const auto session = &_window->session(); + const auto session = &this->session(); return crl::guard(session, [=] { if (weak) { _count = 0; @@ -448,7 +554,7 @@ base::unique_qptr RecentsController::rowContextMenu( st::popupMenuWithIcons); const auto peer = row->peer(); const auto weak = base::make_weak(this); - const auto session = &_window->session(); + const auto session = &this->session(); const auto removeOne = crl::guard(session, [=] { if (weak) { const auto rowId = peer->id.value; @@ -461,7 +567,7 @@ base::unique_qptr RecentsController::rowContextMenu( session->recentPeers().remove(peer); }); FillEntryMenu(Ui::Menu::CreateAddActionCallback(result), { - .controller = _window, + .controller = window(), .peer = peer, .removeOneText = tr::lng_recent_remove(tr::now), .removeOne = removeOne, @@ -473,7 +579,7 @@ base::unique_qptr RecentsController::rowContextMenu( } Main::Session &RecentsController::session() const { - return _window->session(); + return window()->session(); } QString RecentsController::savedMessagesChatStatus() const { @@ -494,7 +600,7 @@ void RecentsController::setupDivider() { tr::lng_recent_clear(tr::now), st::searchedBarLink); clear->setClickedCallback(RemoveAllConfirm( - _window, + window(), tr::lng_recent_clear_sure(tr::now), removeAllCallback())); rpl::combine( @@ -553,7 +659,7 @@ void RecentsController::subscribeToEvents() { MyChannelsController::MyChannelsController( not_null window) -: _window(window) { +: ControllerWithPreviews(window) { } void MyChannelsController::prepare() { @@ -677,7 +783,7 @@ base::unique_qptr MyChannelsController::rowContextMenu( const auto peer = row->peer(); const auto addAction = Ui::Menu::CreateAddActionCallback(result); Window::FillDialogsEntryMenu( - _window, + window(), Dialogs::EntryState{ .key = peer->owner().history(peer), .section = Dialogs::EntryState::Section::ContextMenu, @@ -687,7 +793,7 @@ base::unique_qptr MyChannelsController::rowContextMenu( } Main::Session &MyChannelsController::session() const { - return _window->session(); + return window()->session(); } void MyChannelsController::setupDivider() { @@ -758,7 +864,7 @@ void MyChannelsController::setupDivider() { RecommendationsController::RecommendationsController( not_null window) -: _window(window) { +: ControllerWithPreviews(window) { } void RecommendationsController::prepare() { @@ -793,7 +899,7 @@ void RecommendationsController::fill() { delegate()->peerListRefreshRows(); _count = delegate()->peerListFullRowsCount(); - _window->activeChatValue() | rpl::start_with_next([=](const Key &key) { + window()->activeChatValue() | rpl::start_with_next([=](const Key &key) { const auto history = key.history(); if (_activeHistory == history) { return; @@ -839,7 +945,7 @@ base::unique_qptr RecommendationsController::rowContextMenu( } Main::Session &RecommendationsController::session() const { - return _window->session(); + return window()->session(); } void RecommendationsController::setupDivider() { @@ -903,9 +1009,13 @@ void Suggestions::setupTabs() { const auto shadow = Ui::CreateChild(this); shadow->lower(); - _tabs->sizeValue() | rpl::start_with_next([=](QSize size) { + _tabs->move(st::dialogsSearchTabsPadding, 0); + rpl::combine( + widthValue(), + _tabs->heightValue() + ) | rpl::start_with_next([=](int width, int height) { const auto line = st::lineWidth; - shadow->setGeometry(0, size.height() - line, size.width(), line); + shadow->setGeometry(0, height - line, width, line); }, shadow->lifetime()); shadow->showOn(_tabs->shownValue()); @@ -935,6 +1045,16 @@ void Suggestions::setupChats() { _topPeerChosen.fire(_controller->session().data().peer(peerId)); }, _topPeers->lifetime()); + _topPeers->pressed() | rpl::start_with_next([=](uint64 peerIdRaw) { + handlePressForChatPreview(PeerId(peerIdRaw), [=](bool shown) { + _topPeers->pressLeftToContextMenu(shown); + }); + }, _topPeers->lifetime()); + + _topPeers->pressCancelled() | rpl::start_with_next([=] { + _controller->cancelScheduledPreview(); + }, _topPeers->lifetime()); + _topPeers->showMenuRequests( ) | rpl::start_with_next([=](const ShowTopPeerMenuRequest &request) { const auto weak = Ui::MakeWeak(this); @@ -976,6 +1096,21 @@ void Suggestions::setupChats() { }, _topPeers->lifetime()); _chatsScroll->setVisible(_tab.current() == Tab::Chats); + _chatsScroll->setCustomTouchProcess(_recentProcessTouch); +} + +void Suggestions::handlePressForChatPreview( + PeerId id, + Fn callback) { + callback = crl::guard(this, callback); + const auto row = RowDescriptor( + _controller->session().data().history(id), + FullMsgId()); + if (base::IsAltPressed()) { + _controller->showChatPreview(row, callback); + } else { + _controller->scheduleChatPreview(row, callback); + } } void Suggestions::setupChannels() { @@ -995,6 +1130,11 @@ void Suggestions::setupChannels() { anim::type::instant); _channelsScroll->setVisible(_tab.current() == Tab::Channels); + _channelsScroll->setCustomTouchProcess([=](not_null e) { + const auto myChannels = _myChannelsProcessTouch(e); + const auto recommendations = _recommendationsProcessTouch(e); + return myChannels || recommendations; + }); } void Suggestions::selectJump(Qt::Key direction, int pageSize) { @@ -1303,6 +1443,9 @@ object_ptr> Suggestions::setupRecentPeers( controller->setStyleOverrides(&st::recentPeersList); _recentCount = controller->count(); + _recentProcessTouch = [=](not_null e) { + return controller->processTouchEvent(e); + }; controller->chosen( ) | rpl::start_with_next([=](not_null peer) { @@ -1351,58 +1494,36 @@ object_ptr> Suggestions::setupRecentPeers( delegate->setContent(raw); controller->setDelegate(delegate); + controller->setupTouchChatPreview(_chatsScroll.get()); return object_ptr>(this, std::move(content)); } object_ptr> Suggestions::setupEmptyRecent() { - return setupEmpty(_chatsContent, "search", tr::lng_recent_none()); + const auto icon = SearchEmptyIcon::Search; + return setupEmpty(_chatsContent, icon, tr::lng_recent_none()); } object_ptr> Suggestions::setupEmptyChannels() { - return setupEmpty( - _channelsContent, - "noresults", - tr::lng_channels_none_about()); + const auto icon = SearchEmptyIcon::NoResults; + return setupEmpty(_channelsContent, icon, tr::lng_channels_none_about()); } object_ptr> Suggestions::setupEmpty( not_null parent, - const QString &animation, + SearchEmptyIcon icon, rpl::producer text) { - auto content = object_ptr(parent); + auto content = object_ptr( + parent, + icon, + std::move(text) | Ui::Text::ToWithEntities()); + const auto raw = content.data(); - - const auto label = Ui::CreateChild( - raw, - std::move(text), - st::defaultPeerListAbout); - const auto size = st::recentPeersEmptySize; - const auto [widget, animate] = Settings::CreateLottieIcon( - raw, - { - .name = animation, - .sizeOverride = { size, size }, - }, - st::recentPeersEmptyMargin); - const auto icon = widget.data(); - rpl::combine( _chatsScroll->heightValue(), _topPeersWrap->heightValue() ) | rpl::start_with_next([=](int height, int top) { - raw->resize( - raw->width(), - std::max(height - top, st::recentPeersEmptyHeightMin)); - }, raw->lifetime()); - - raw->sizeValue() | rpl::start_with_next([=](QSize size) { - const auto x = (size.width() - icon->width()) / 2; - const auto y = (size.height() - icon->height()) / 3; - icon->move(x, y); - label->move( - (size.width() - label->width()) / 2, - y + icon->height() + st::recentPeersEmptySkip); + raw->setMinimalHeight(height - top); }, raw->lifetime()); auto result = object_ptr>( @@ -1413,7 +1534,7 @@ object_ptr> Suggestions::setupEmpty( result->toggledValue() | rpl::filter([=](bool shown) { return shown && _controller->session().data().chatsListLoaded(); }) | rpl::start_with_next([=] { - animate(anim::repeat::once); + raw->animate(); }, raw->lifetime()); return result; @@ -1429,6 +1550,9 @@ object_ptr> Suggestions::setupMyChannels() { controller->setStyleOverrides(&st::recentPeersList); _myChannelsCount = controller->count(); + _myChannelsProcessTouch = [=](not_null e) { + return controller->processTouchEvent(e); + }; controller->chosen( ) | rpl::start_with_next([=](not_null peer) { @@ -1490,6 +1614,7 @@ object_ptr> Suggestions::setupMyChannels() { delegate->setContent(raw); controller->setDelegate(delegate); + controller->setupTouchChatPreview(_channelsScroll.get()); return object_ptr>(this, std::move(content)); } @@ -1504,6 +1629,9 @@ object_ptr> Suggestions::setupRecommendations() { controller->setStyleOverrides(&st::recentPeersList); _recommendationsCount = controller->count(); + _recommendationsProcessTouch = [=](not_null e) { + return controller->processTouchEvent(e); + }; _tab.value() | rpl::filter( rpl::mappers::_1 == Tab::Channels @@ -1558,6 +1686,7 @@ object_ptr> Suggestions::setupRecommendations() { delegate->setContent(raw); controller->setDelegate(delegate); + controller->setupTouchChatPreview(_channelsScroll.get()); return object_ptr>(this, std::move(content)); } diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h index 947bf2fa9..8b33718e6 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h @@ -34,6 +34,8 @@ class SessionController; namespace Dialogs { +enum class SearchEmptyIcon; + struct RecentPeersList { std::vector> list; }; @@ -112,7 +114,7 @@ private: -> object_ptr>; [[nodiscard]] object_ptr> setupEmpty( not_null parent, - const QString &animation, + SearchEmptyIcon icon, rpl::producer text); void switchTab(Tab tab); @@ -120,6 +122,8 @@ private: void startSlideAnimation(); void finishShow(); + void handlePressForChatPreview(PeerId id, Fn callback); + const not_null _controller; const std::unique_ptr _tabs; @@ -135,6 +139,7 @@ private: Fn _recentSelectJump; Fn _recentUpdateFromParentDrag; Fn _recentDragLeft; + Fn)> _recentProcessTouch; const not_null*> _recentPeers; const not_null*> _emptyRecent; @@ -146,6 +151,7 @@ private: Fn _myChannelsSelectJump; Fn _myChannelsUpdateFromParentDrag; Fn _myChannelsDragLeft; + Fn)> _myChannelsProcessTouch; const not_null*> _myChannels; rpl::variable _recommendationsCount; @@ -153,6 +159,7 @@ private: Fn _recommendationsSelectJump; Fn _recommendationsUpdateFromParentDrag; Fn _recommendationsDragLeft; + Fn)> _recommendationsProcessTouch; const not_null*> _recommendations; const not_null*> _emptyChannels; diff --git a/Telegram/SourceFiles/dialogs/ui/top_peers_strip.cpp b/Telegram/SourceFiles/dialogs/ui/top_peers_strip.cpp index 40e758ad2..67cafb98c 100644 --- a/Telegram/SourceFiles/dialogs/ui/top_peers_strip.cpp +++ b/Telegram/SourceFiles/dialogs/ui/top_peers_strip.cpp @@ -286,6 +286,8 @@ void TopPeersStrip::stripMousePressEvent(QMouseEvent *e) { entry.ripple->add(e->pos() - QPoint( x + st::topPeersMargin.left(), y + st::topPeersMargin.top())); + + _presses.fire_copy(entry.id); } } @@ -304,6 +306,7 @@ void TopPeersStrip::stripMouseMoveEvent(QMouseEvent *e) { if (!_dragging && _mouseDownPosition) { if ((*_lastMousePosition - *_mouseDownPosition).manhattanLength() >= QApplication::startDragDistance()) { + _pressCancelled.fire({}); if (!_expandAnimation.animating()) { _dragging = true; _startDraggingLeft = _scrollLeft; @@ -371,19 +374,14 @@ void TopPeersStrip::subscribeUserpic(Entry &entry) { } void TopPeersStrip::stripMouseReleaseEvent(QMouseEvent *e) { + _pressCancelled.fire({}); + _lastMousePosition = e->globalPos(); const auto guard = gsl::finally([&] { _mouseDownPosition = std::nullopt; }); - const auto pressed = std::exchange(_pressed, -1); - if (pressed >= 0) { - Assert(pressed < _entries.size()); - auto &entry = _entries[pressed]; - if (entry.ripple) { - entry.ripple->lastStop(); - } - } + const auto pressed = clearPressed(); if (finishDragging()) { return; } @@ -395,6 +393,18 @@ void TopPeersStrip::stripMouseReleaseEvent(QMouseEvent *e) { } } +int TopPeersStrip::clearPressed() { + const auto pressed = std::exchange(_pressed, -1); + if (pressed >= 0) { + Assert(pressed < _entries.size()); + auto &entry = _entries[pressed]; + if (entry.ripple) { + entry.ripple->lastStop(); + } + } + return pressed; +} + void TopPeersStrip::updateScrollMax(int newWidth) { if (_expandAnimation.animating()) { return; @@ -428,6 +438,27 @@ rpl::producer TopPeersStrip::clicks() const { return _clicks.events(); } +rpl::producer TopPeersStrip::pressed() const { + return _presses.events(); +} + +rpl::producer<> TopPeersStrip::pressCancelled() const { + return _pressCancelled.events(); +} + +void TopPeersStrip::pressLeftToContextMenu(bool shown) { + if (!shown) { + _contexted = -1; + update(); + return; + } + _contexted = clearPressed(); + if (finishDragging()) { + return; + } + _mouseDownPosition = std::nullopt; +} + auto TopPeersStrip::showMenuRequests() const -> rpl::producer { return _showMenuRequests.events(); @@ -463,6 +494,9 @@ void TopPeersStrip::removeLocally(uint64 id) { if (_pressed > index) { --_pressed; } + if (_contexted > index) { + --_contexted; + } updateScrollMax(); _count = int(_entries.size()); update(); @@ -575,8 +609,17 @@ void TopPeersStrip::apply(const TopPeersList &list) { } auto now = std::vector(); - auto selectedId = (_selected >= 0) ? _entries[_selected].id : 0; - auto pressedId = (_pressed >= 0) ? _entries[_pressed].id : 0; + const auto selectedId = (_selected >= 0) ? _entries[_selected].id : 0; + const auto pressedId = (_pressed >= 0) ? _entries[_pressed].id : 0; + const auto contextedId = (_contexted >= 0) ? _entries[_contexted].id : 0; + const auto restoreIndex = [&](uint64 id) { + if (!id) { + return -1; + } + const auto i = ranges::find(_entries, id, &Entry::id); + return (i != end(_entries)) ? int(i - begin(_entries)) : -1; + }; + for (const auto &entry : list.entries) { if (_removed.contains(entry.id)) { continue; @@ -599,18 +642,9 @@ void TopPeersStrip::apply(const TopPeersList &list) { } } _entries = std::move(now); - if (selectedId) { - const auto i = ranges::find(_entries, selectedId, &Entry::id); - if (i != end(_entries)) { - _selected = int(i - begin(_entries)); - } - } - if (pressedId) { - const auto i = ranges::find(_entries, pressedId, &Entry::id); - if (i != end(_entries)) { - _pressed = int(i - begin(_entries)); - } - } + _selected = restoreIndex(selectedId); + _pressed = restoreIndex(pressedId); + _contexted = restoreIndex(contextedId); updateScrollMax(); unsubscribeUserpics(); _count = int(_entries.size()); @@ -708,7 +742,11 @@ void TopPeersStrip::paintStrip(QRect clip) { auto x = int(base::SafeRound(-shift + from * fsingle + added)); auto y = row * st.height; - const auto highlighted = (_pressed >= 0) ? _pressed : _selected; + const auto highlighted = (_contexted >= 0) + ? _contexted + : (_pressed >= 0) + ? _pressed + : _selected; for (auto i = from; i != till; ++i) { auto &entry = _entries[i]; const auto selected = (i == highlighted); diff --git a/Telegram/SourceFiles/dialogs/ui/top_peers_strip.h b/Telegram/SourceFiles/dialogs/ui/top_peers_strip.h index 23db15d1f..f33ec722b 100644 --- a/Telegram/SourceFiles/dialogs/ui/top_peers_strip.h +++ b/Telegram/SourceFiles/dialogs/ui/top_peers_strip.h @@ -50,6 +50,8 @@ public: [[nodiscard]] bool empty() const; [[nodiscard]] rpl::producer emptyValue() const; [[nodiscard]] rpl::producer clicks() const; + [[nodiscard]] rpl::producer pressed() const; + [[nodiscard]] rpl::producer<> pressCancelled() const; [[nodiscard]] auto showMenuRequests() const -> rpl::producer; [[nodiscard]] auto scrollToRequests() const @@ -61,6 +63,7 @@ public: bool selectByKeyboard(Qt::Key direction); void deselectByKeyboard(); bool chooseRow(); + void pressLeftToContextMenu(bool shown); uint64 updateFromParentDrag(QPoint globalPosition); void dragLeft(); @@ -102,6 +105,7 @@ private: [[nodiscard]] QRect innerRounded() const; [[nodiscard]] int scrollLeft() const; [[nodiscard]] Layout currentLayout() const; + int clearPressed(); void apply(const TopPeersList &list); void apply(Entry &entry, const TopPeersEntry &data); @@ -114,6 +118,8 @@ private: rpl::variable _toggleExpanded = nullptr; rpl::event_stream _clicks; + rpl::event_stream _presses; + rpl::event_stream<> _pressCancelled; rpl::event_stream _showMenuRequests; rpl::event_stream> _verticalScrollEvents; @@ -127,6 +133,7 @@ private: int _selected = -1; int _pressed = -1; + int _contexted = -1; bool _selectionByKeyboard = false; bool _hiddenLocally = false; diff --git a/Telegram/SourceFiles/export/data/export_data_types.cpp b/Telegram/SourceFiles/export/data/export_data_types.cpp index c4b55f346..71750a575 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.cpp +++ b/Telegram/SourceFiles/export/data/export_data_types.cpp @@ -307,6 +307,8 @@ std::vector ParseText( return NumberToString(data.vuser_id().v); }, [](const MTPDmessageEntityCustomEmoji &data) { return NumberToString(data.vdocument_id().v); + }, [](const MTPDmessageEntityBlockquote &data) { + return data.is_collapsed() ? Utf8String("1") : Utf8String(); }, [](const auto &) { return Utf8String(); }); result.push_back(std::move(part)); diff --git a/Telegram/SourceFiles/export/output/export_output_json.cpp b/Telegram/SourceFiles/export/output/export_output_json.cpp index 67814e5b2..b485e615b 100644 --- a/Telegram/SourceFiles/export/output/export_output_json.cpp +++ b/Telegram/SourceFiles/export/output/export_output_json.cpp @@ -195,6 +195,8 @@ QByteArray SerializeText( ? "language" : (part.type == Type::TextUrl) ? "href" + : (part.type == Type::Blockquote) + ? "collapsed" : "none"; const auto additionalValue = (part.type == Type::MentionName) ? part.additional @@ -202,6 +204,8 @@ QByteArray SerializeText( || part.type == Type::TextUrl || part.type == Type::CustomEmoji) ? SerializeString(part.additional) + : (part.type == Type::Blockquote) + ? (part.additional.isEmpty() ? "false" : "true") : QByteArray(); return SerializeObject(context, { { "type", SerializeString(typeString) }, 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 21030589a..ea7b637c0 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp @@ -674,6 +674,11 @@ void InnerWidget::elementStartPremium( void InnerWidget::elementCancelPremium(not_null view) { } +void InnerWidget::elementStartEffect( + not_null view, + Element *replacing) { +} + QString InnerWidget::elementAuthorRank(not_null view) { return {}; } @@ -1301,6 +1306,7 @@ void InnerWidget::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { && !link && (view->hasVisibleText() || mediaHasTextForCopy + || !item->factcheckText().empty() || item->Has())) { _menu->addAction(tr::lng_context_copy_text(tr::now), [=] { copyContextText(itemId); diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h index f47afddfa..bbe3c0381 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h @@ -136,6 +136,9 @@ public: HistoryView::Element *replacing) override; void elementCancelPremium( not_null view) override; + void elementStartEffect( + not_null view, + HistoryView::Element *replacing) override; QString elementAuthorRank( not_null view) override; 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 72d797460..19f76be07 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp @@ -111,7 +111,8 @@ MTPMessage PrepareLogMessage(const MTPMessage &message, TimeId newDate) { | MTPDmessage::Flag::f_forwards //| MTPDmessage::Flag::f_reactions | MTPDmessage::Flag::f_restriction_reason - | MTPDmessage::Flag::f_ttl_period; + | MTPDmessage::Flag::f_ttl_period + | MTPDmessage::Flag::f_factcheck; return MTP_message( MTP_flags(data.vflags().v & ~removeFlags), data.vid(), @@ -139,7 +140,9 @@ MTPMessage PrepareLogMessage(const MTPMessage &message, TimeId newDate) { MTPMessageReactions(), MTPVector(), MTPint(), // ttl_period - MTPint()); // quick_reply_shortcut_id + MTPint(), // quick_reply_shortcut_id + MTP_long(data.veffect().value_or_empty()), + MTPFactCheck()); }); } diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index 3843a5f2c..9e2dfd879 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -41,6 +41,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_user.h" #include "data/data_document.h" #include "data/data_histories.h" +#include "data/data_history_messages.h" #include "lang/lang_keys.h" #include "apiwrap.h" #include "api/api_chat_participants.h" @@ -488,7 +489,7 @@ not_null History::insertItem( std::unique_ptr item) { Expects(item != nullptr); - const auto &[i, ok] = _messages.insert(std::move(item)); + const auto &[i, ok] = _items.insert(std::move(item)); const auto result = i->get(); owner().registerMessage(result); @@ -508,6 +509,9 @@ void History::destroyMessage(not_null item) { // All this must be done for all items manually in History::clear()! item->destroyHistoryEntry(); if (item->isRegular()) { + if (const auto messages = _messages.get()) { + messages->removeOne(item->id); + } if (const auto types = item->sharedMediaTypes()) { session().storage().remove(Storage::SharedMediaRemoveOne( peerId, @@ -532,11 +536,11 @@ void History::destroyMessage(not_null item) { Core::App().notifications().clearFromItem(item); auto hack = std::unique_ptr(item.get()); - const auto i = _messages.find(hack); + const auto i = _items.find(hack); hack.release(); - Assert(i != end(_messages)); - _messages.erase(i); + Assert(i != end(_items)); + _items.erase(i); if (documentToCancel) { session().data().documentMessageRemoved(documentToCancel); @@ -545,8 +549,8 @@ void History::destroyMessage(not_null item) { void History::destroyMessagesByDates(TimeId minDate, TimeId maxDate) { auto toDestroy = std::vector>(); - toDestroy.reserve(_messages.size()); - for (const auto &message : _messages) { + toDestroy.reserve(_items.size()); + for (const auto &message : _items) { if (message->isRegular() && message->date() > minDate && message->date() < maxDate) { @@ -560,8 +564,8 @@ void History::destroyMessagesByDates(TimeId minDate, TimeId maxDate) { void History::destroyMessagesByTopic(MsgId topicRootId) { auto toDestroy = std::vector>(); - toDestroy.reserve(_messages.size()); - for (const auto &message : _messages) { + toDestroy.reserve(_items.size()); + for (const auto &message : _items) { if (message->topicRootId() == topicRootId) { toDestroy.push_back(message.get()); } @@ -583,7 +587,7 @@ void History::unpinMessagesFor(MsgId topicRootId) { topic->setHasPinnedMessages(false); }); } - for (const auto &item : _messages) { + for (const auto &item : _items) { if (item->isPinned()) { item->setIsPinned(false); } @@ -597,7 +601,7 @@ void History::unpinMessagesFor(MsgId topicRootId) { if (const auto topic = peer->forumTopicFor(topicRootId)) { topic->setHasPinnedMessages(false); } - for (const auto &item : _messages) { + for (const auto &item : _items) { if (item->isPinned() && item->topicRootId() == topicRootId) { item->setIsPinned(false); } @@ -789,9 +793,12 @@ not_null History::addNewToBack( addItemToBlock(item); if (!unread && item->isRegular()) { + const auto from = loadedAtTop() ? 0 : minMsgId(); + const auto till = loadedAtBottom() ? ServerMaxMsgId : maxMsgId(); + if (_messages) { + _messages->addExisting(item->id, { from, till }); + } if (const auto types = item->sharedMediaTypes()) { - auto from = loadedAtTop() ? 0 : minMsgId(); - auto till = loadedAtBottom() ? ServerMaxMsgId : maxMsgId(); auto &storage = session().storage(); storage.add(Storage::SharedMediaAddExisting( peer->id, @@ -1198,6 +1205,7 @@ void History::mainViewRemoved( void History::newItemAdded(not_null item) { item->indexAsNewItem(); + item->addToMessagesIndex(); if (const auto from = item->from() ? item->from()->asUser() : nullptr) { if (from == item->author()) { _sendActionPainter.clear(from); @@ -1753,6 +1761,10 @@ MsgId History::loadAroundId() const { return MsgId(0); } +bool History::inboxReadTillKnown() const { + return _inboxReadBefore.has_value(); +} + MsgId History::inboxReadTillId() const { return _inboxReadBefore.value_or(1) - 1; } @@ -2393,6 +2405,9 @@ void History::setNotLoadedAtBottom() { session().storage().invalidate( Storage::SharedMediaInvalidateBottom(peer->id)); + if (const auto messages = _messages.get()) { + messages->invalidateBottom(); + } } void History::clearSharedMedia() { @@ -2924,6 +2939,9 @@ void History::setInboxReadTill(MsgId upTo) { accumulate_max(*_inboxReadBefore, upTo + 1); } else { _inboxReadBefore = upTo + 1; + for (const auto &item : _items) { + item->applyEffectWatchedOnUnreadKnown(); + } } } @@ -3100,6 +3118,48 @@ MsgRange History::rangeForDifferenceRequest() const { return MsgRange(); } +Data::HistoryMessages &History::messages() { + if (!_messages) { + _messages = std::make_unique(); + + const auto max = maxMsgId(); + const auto from = loadedAtTop() ? 0 : minMsgId(); + const auto till = loadedAtBottom() ? ServerMaxMsgId : max; + auto list = std::vector(); + list.reserve(std::min( + int(_items.size()), + int(blocks.size()) * kNewBlockEachMessage)); + auto sort = false; + for (const auto &block : blocks) { + for (const auto &view : block->messages) { + const auto item = view->data(); + if (item->isRegular()) { + const auto id = item->id; + if (!list.empty() && list.back() >= id) { + sort = true; + } + list.push_back(id); + } + } + } + if (sort) { + ranges::sort(list); + } + if (max || (loadedAtTop() && loadedAtBottom())) { + _messages->addSlice(std::move(list), { from, till }, {}); + } + } + return *_messages; +} + +const Data::HistoryMessages &History::messages() const { + return const_cast(this)->messages(); +} + +Data::HistoryMessages *History::maybeMessages() { + return _messages.get(); +} + HistoryItem *History::insertJoinedMessage() { const auto channel = peer->asChannel(); if (!channel @@ -3202,11 +3262,11 @@ void History::removeJoinedMessage() { void History::reactionsEnabledChanged(bool enabled) { if (!enabled) { - for (const auto &item : _messages) { + for (const auto &item : _items) { item->updateReactions(nullptr); } } else { - for (const auto &item : _messages) { + for (const auto &item : _items) { item->updateReactionsUnknown(); } } @@ -3380,6 +3440,9 @@ void History::clear(ClearType type) { } _loadedAtTop = _loadedAtBottom = _lastMessage.has_value(); clearSharedMedia(); + if (const auto messages = _messages.get()) { + messages->removeAll(); + } clearLastKeyboard(); } @@ -3396,8 +3459,8 @@ void History::clear(ClearType type) { void History::clearUpTill(MsgId availableMinId) { auto remove = std::vector>(); - remove.reserve(_messages.size()); - for (const auto &item : _messages) { + remove.reserve(_items.size()); + for (const auto &item : _items) { const auto itemId = item->id; if (!item->isRegular()) { continue; diff --git a/Telegram/SourceFiles/history/history.h b/Telegram/SourceFiles/history/history.h index e00c23b8d..73695785f 100644 --- a/Telegram/SourceFiles/history/history.h +++ b/Telegram/SourceFiles/history/history.h @@ -26,12 +26,14 @@ class HistoryMainElementDelegateMixin; struct LanguageId; namespace Data { + struct Draft; class Session; class Folder; class ChatFilter; struct SponsoredFrom; class SponsoredMessages; +class HistoryMessages; enum class ForwardOptions { PreserveInfo, @@ -79,7 +81,7 @@ public: History(not_null owner, PeerId peerId); ~History(); - not_null owningHistory() override { + [[nodiscard]] not_null owningHistory() override { return this; } [[nodiscard]] Data::Thread *threadFor(MsgId topicRootId); @@ -93,23 +95,27 @@ public: void forumChanged(Data::Forum *old); [[nodiscard]] bool isForum() const; - not_null migrateToOrMe() const; - History *migrateFrom() const; - MsgRange rangeForDifferenceRequest() const; + [[nodiscard]] not_null migrateToOrMe() const; + [[nodiscard]] History *migrateFrom() const; + [[nodiscard]] MsgRange rangeForDifferenceRequest() const; - HistoryItem *joinedMessageInstance() const; + [[nodiscard]] Data::HistoryMessages &messages(); + [[nodiscard]] const Data::HistoryMessages &messages() const; + [[nodiscard]] Data::HistoryMessages *maybeMessages(); + + [[nodiscard]] HistoryItem *joinedMessageInstance() const; void checkLocalMessages(); void removeJoinedMessage(); void reactionsEnabledChanged(bool enabled); - bool isEmpty() const; - bool isDisplayedEmpty() const; - Element *findFirstNonEmpty() const; - Element *findFirstDisplayed() const; - Element *findLastNonEmpty() const; - Element *findLastDisplayed() const; - bool hasOrphanMediaGroupPart() const; + [[nodiscard]] bool isEmpty() const; + [[nodiscard]] bool isDisplayedEmpty() const; + [[nodiscard]] Element *findFirstNonEmpty() const; + [[nodiscard]] Element *findFirstDisplayed() const; + [[nodiscard]] Element *findLastNonEmpty() const; + [[nodiscard]] Element *findLastDisplayed() const; + [[nodiscard]] bool hasOrphanMediaGroupPart() const; [[nodiscard]] std::vector collectMessagesFromParticipantToDelete( not_null participant) const; @@ -212,6 +218,7 @@ public: void outboxRead(MsgId upTo); void outboxRead(not_null wasRead); [[nodiscard]] MsgId loadAroundId() const; + [[nodiscard]] bool inboxReadTillKnown() const; [[nodiscard]] MsgId inboxReadTillId() const; [[nodiscard]] MsgId outboxReadTillId() const; @@ -590,7 +597,9 @@ private: std::optional _lastMessage; std::optional _lastServerMessage; base::flat_set> _clientSideMessages; - std::unordered_set> _messages; + std::unordered_set> _items; + + std::unique_ptr _messages; // This almost always is equal to _lastMessage. The only difference is // for a group that migrated to a supergroup. Then _lastMessage can diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 6915d7e87..51f1b6deb 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -35,6 +35,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/reaction_fly_animation.h" #include "ui/text/text_options.h" #include "ui/text/text_isolated_emoji.h" +#include "ui/boxes/edit_factcheck_box.h" #include "ui/boxes/report_box.h" #include "ui/layers/generic_box.h" #include "ui/controls/delete_message_context_action.h" @@ -63,6 +64,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/call_delayed.h" #include "main/main_session.h" #include "main/main_session_settings.h" +#include "mainwidget.h" #include "menu/menu_item_download_files.h" #include "core/application.h" #include "apiwrap.h" @@ -71,6 +73,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_who_reacted.h" #include "api/api_views.h" #include "lang/lang_keys.h" +#include "data/components/factchecks.h" #include "data/components/sponsored_messages.h" #include "data/data_session.h" #include "data/data_document.h" @@ -166,7 +169,13 @@ void FillSponsoredMessagesMenu( menu->addSeparator(&st::expandedMenuSeparator); } menu->addAction(tr::lng_sponsored_hide_ads(tr::now), [=] { - ShowPremiumPreviewBox(controller, PremiumFeature::NoAds); + if (controller->session().premium()) { + using Result = Data::SponsoredReportResult; + controller->session().sponsoredMessages().createReportCallback( + itemId)(Result::Id("-1"), [](const auto &) {}); + } else { + ShowPremiumPreviewBox(controller, PremiumFeature::NoAds); + } }, &st::menuIconCancel); } @@ -303,12 +312,18 @@ public: _widget->elementStartPremium(view, replacing); } } - void elementCancelPremium(not_null view) override { if (_widget) { _widget->elementCancelPremium(view); } } + void elementStartEffect( + not_null view, + Element *replacing) override { + if (_widget) { + _widget->elementStartEffect(view, replacing); + } + } QString elementAuthorRank(not_null view) override { return {}; @@ -333,6 +348,8 @@ HistoryInner::HistoryInner( , _history(history) , _elementDelegate(_history->delegateMixin()->delegate()) , _emojiInteractions(std::make_unique( + this, + controller->content(), &controller->session(), [=](not_null view) { return itemTop(view); })) , _migrated(history->migrateFrom()) @@ -344,8 +361,7 @@ HistoryInner::HistoryInner( , _reactionsManager( std::make_unique( this, - [=](QRect updated) { update(updated); }, - controller->cachedReactionIconFactory().createMethod())) + [=](QRect updated) { update(updated); })) , _touchSelectTimer([=] { onTouchSelect(); }) , _touchScrollTimer([=] { onTouchScrollTimer(); }) , _scrollDateCheck([this] { scrollDateCheck(); }) @@ -387,10 +403,6 @@ HistoryInner::HistoryInner( _emojiInteractions->play(std::move(request), view); } }, lifetime()); - _emojiInteractions->updateRequests( - ) | rpl::start_with_next([=](QRect rect) { - update(rect); - }, lifetime()); _emojiInteractions->playStarted( ) | rpl::start_with_next([=](QString &&emoji) { _controller->emojiInteractions().playStarted(_peer, std::move(emoji)); @@ -902,6 +914,14 @@ Ui::ChatPaintContext HistoryInner::preparePaintContext( }); } +void HistoryInner::startEffectOnRead(not_null item) { + if (item->history() == _history) { + if (const auto view = item->mainView()) { + _emojiInteractions->playEffectOnRead(view); + } + } +} + void HistoryInner::paintEvent(QPaintEvent *e) { if (_controller->contentOverlapped(this, e) || hasPendingResizedItems()) { @@ -949,12 +969,18 @@ void HistoryInner::paintEvent(QPaintEvent *e) { _translateTracker->startBunch(); auto readTill = (HistoryItem*)nullptr; auto readContents = base::flat_set>(); + auto startEffects = base::flat_set>(); const auto markingAsViewed = _widget->markingContentsRead(); const auto guard = gsl::finally([&] { if (_pinnedItem) { _translateTracker->add(_pinnedItem); } _translateTracker->finishBunch(); + if (!startEffects.empty()) { + for (const auto &view : startEffects) { + _emojiInteractions->playEffectOnRead(view); + } + } if (readTill && _widget->markingMessagesRead()) { session().data().histories().readInboxTill(readTill); } @@ -991,6 +1017,9 @@ void HistoryInner::paintEvent(QPaintEvent *e) { } else if (isUnread) { readTill = item; } + if (markingAsViewed && item->hasUnwatchedEffect()) { + startEffects.emplace(view); + } if (markingAsViewed && item->hasViews()) { session().api().views().scheduleIncrement(item); } @@ -1215,7 +1244,6 @@ void HistoryInner::paintEvent(QPaintEvent *e) { p.setOpacity(1.); _reactionsManager->paint(p, context); - _emojiInteractions->paint(p); } bool HistoryInner::eventHook(QEvent *e) { @@ -1715,14 +1743,24 @@ std::unique_ptr HistoryInner::prepareDrag() { return mimeData; } else if (pressedView) { auto forwardIds = MessageIdsList(); - if (_mouseCursorState == CursorState::Date) { + const auto tryForwardSelection = uponSelected + && !_controller->adaptive().isOneColumn(); + const auto forwardSelectionState = tryForwardSelection + ? getSelectionState() + : HistoryView::TopBarWidget::SelectedState(); + if (forwardSelectionState.count > 0 + && (forwardSelectionState.count + == forwardSelectionState.canForwardCount)) { + forwardIds = getSelectedItems(); + } else if (_mouseCursorState == CursorState::Date) { forwardIds = session().data().itemOrItsGroup(_mouseActionItem); - } else if (pressedView->isHiddenByGroup() && pressedHandler) { - forwardIds = MessageIdsList(1, _mouseActionItem->fullId()); - } else if (const auto media = pressedView->media()) { - if (media->dragItemByHandler(pressedHandler)) { - forwardIds = MessageIdsList(1, _mouseActionItem->fullId()); - } + } else if ((pressedView->isHiddenByGroup() && pressedHandler) + || (pressedView->media() + && pressedView->media()->dragItemByHandler(pressedHandler))) { + const auto item = _dragStateItem + ? _dragStateItem + : _mouseActionItem; + forwardIds = MessageIdsList(1, item->fullId()); } if (forwardIds.empty()) { return nullptr; @@ -2150,6 +2188,20 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { } }, &st::menuIconEdit); } + if (session->factchecks().canEdit(item)) { + const auto text = item->factcheckText(); + const auto phrase = text.empty() + ? tr::lng_context_add_factcheck(tr::now) + : tr::lng_context_edit_factcheck(tr::now); + _menu->addAction(phrase, [=] { + const auto limit = session->factchecks().lengthLimit(); + controller->show(Box(EditFactcheckBox, text, limit, [=]( + TextWithEntities result) { + const auto show = controller->uiShow(); + session->factchecks().save(itemId, text, result, show); + }, FactcheckFieldIniter(controller->uiShow()))); + }, &st::menuIconFactcheck); + } const auto pinItem = (item->canPin() && item->isPinned()) ? item : groupLeaderOrSelf(item); @@ -2411,7 +2463,11 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { const auto quoteOffset = selected.offset; text.replace('&', u"&&"_q); _menu->addAction(text, [=] { - if (canSendReply) { + const auto still = session->data().message(itemId); + const auto forceAnotherChat = base::IsCtrlPressed() + && still + && still->allowsForward(); + if (canSendReply && !forceAnotherChat) { _widget->replyToMessage({ .messageId = itemId, .quote = quote, @@ -2769,8 +2825,7 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { desiredPosition, reactItem, [=](ChosenReaction reaction) { reactionChosen(reaction); }, - ItemReactionsAbout(reactItem), - _controller->cachedReactionIconFactory().createMethod()) + ItemReactionsAbout(reactItem)) : AttachSelectorResult::Skipped; if (attached == AttachSelectorResult::Failed) { _menu = nullptr; @@ -3574,6 +3629,12 @@ void HistoryInner::elementCancelPremium(not_null view) { _emojiInteractions->cancelPremiumEffect(view); } +void HistoryInner::elementStartEffect( + not_null view, + Element *replacing) { + _emojiInteractions->playEffect(view); +} + auto HistoryInner::getSelectionState() const -> HistoryView::TopBarWidget::SelectedState { auto result = HistoryView::TopBarWidget::SelectedState {}; diff --git a/Telegram/SourceFiles/history/history_inner_widget.h b/Telegram/SourceFiles/history/history_inner_widget.h index 11eb81a5a..5a11b2944 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.h +++ b/Telegram/SourceFiles/history/history_inner_widget.h @@ -170,7 +170,11 @@ public: not_null view, Element *replacing); void elementCancelPremium(not_null view); + void elementStartEffect( + not_null view, + Element *replacing); + void startEffectOnRead(not_null item); void updateBotInfo(bool recount = true); bool wasSelectedText() const; diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 9a62d4a34..36108ce0d 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -55,6 +55,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_channel.h" #include "data/data_chat.h" #include "data/data_game.h" +#include "data/data_history_messages.h" #include "data/data_user.h" #include "data/data_group_call.h" // Data::GroupCall::id(). #include "data/data_poll.h" // PollData::publicVotes. @@ -62,6 +63,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_web_page.h" #include "chat_helpers/stickers_gift_box_pack.h" #include "payments/payments_checkout_process.h" // CheckoutProcess::Start. +#include "payments/payments_non_panel_process.h" // ProcessNonPanelPaymentFormFactory. #include "platform/platform_notifications_manager.h" #include "spellcheck/spellcheck_highlight_syntax.h" #include "styles/style_dialogs.h" @@ -216,12 +218,59 @@ std::unique_ptr HistoryItem::CreateMedia( const MTPMessageMedia &media) { using Result = std::unique_ptr; return media.match([&](const MTPDmessageMediaContact &media) -> Result { + + const auto vcardItems = [&] { + using Type = Data::SharedContact::VcardItemType; + auto items = Data::SharedContact::VcardItems(); + for (const auto &item : qs(media.vvcard()).split('\n')) { + const auto parts = item.split(':'); + if (parts.size() == 2) { + const auto &type = parts.front(); + const auto &value = parts[1]; + + if (type.startsWith("TEL")) { + const auto telType = type.contains("PREF") + ? Type::PhoneMain + : type.contains("HOME") + ? Type::PhoneHome + : type.contains("WORK") + ? Type::PhoneWork + : (type.contains("CELL") + || type.contains("MOBILE")) + ? Type::PhoneMobile + : type.contains("OTHER") + ? Type::PhoneOther + : Type::Phone; + items[telType] = value; + } else if (type.startsWith("EMAIL")) { + items[Type::Email] = value; + } else if (type.startsWith("URL")) { + items[Type::Url] = value; + } else if (type.startsWith("NOTE")) { + items[Type::Note] = value; + } else if (type.startsWith("ORG")) { + items[Type::Organization] = value; + items[Type::Organization].replace(';', ' '); + } else if (type.startsWith("ADR")) { + items[Type::Address] = value; + } else if (type.startsWith("BDAY")) { + items[Type::Birthday] = value; + } else if (type.startsWith("N")) { + items[Type::Birthday] = value; + items[Type::Birthday].replace(';', ' '); + } + } + } + return items; + }(); + return std::make_unique( item, media.vuser_id().v, qs(media.vfirst_name()), qs(media.vlast_name()), - qs(media.vphone_number())); + qs(media.vphone_number()), + vcardItems); }, [&](const MTPDmessageMediaGeo &media) -> Result { return media.vgeo().match([&](const MTPDgeoPoint &point) -> Result { return std::make_unique( @@ -371,9 +420,13 @@ HistoryItem::HistoryItem( .from = data.vfrom_id() ? peerFromMTP(*data.vfrom_id()) : PeerId(0), .date = data.vdate().v, .shortcutId = data.vquick_reply_shortcut_id().value_or_empty(), + .effectId = data.veffect().value_or_empty(), }) { _boostsApplied = data.vfrom_boosts_applied().value_or_empty(); + // Called only for server-received messages, not locally created ones. + applyInitialEffectWatched(); + const auto media = data.vmedia(); const auto checked = media ? CheckMessageMedia(*media) @@ -450,6 +503,11 @@ HistoryItem::HistoryItem( } setReactions(data.vreactions()); applyTTL(data); + + if (const auto check = FromMTP(this, data.vfactcheck())) { + AddComponents(HistoryMessageFactcheck::Bit()); + Get()->data = check; + } } } @@ -720,10 +778,14 @@ HistoryItem::HistoryItem( : history->peer) , _flags(FinalizeMessageFlags(history, fields.flags)) , _date(fields.date) -, _shortcutId(fields.shortcutId) { +, _shortcutId(fields.shortcutId) +, _effectId(fields.effectId) { if (isHistoryEntry() && IsClientMsgId(id)) { _history->registerClientSideMessage(this); } + if (_effectId) { + _history->owner().reactions().preloadEffectImageFor(_effectId); + } } HistoryItem::HistoryItem( @@ -1335,6 +1397,18 @@ bool HistoryItem::hasUnreadReaction() const { return (_flags & MessageFlag::HasUnreadReaction); } +bool HistoryItem::hasUnwatchedEffect() const { + return effectId() && !(_flags & MessageFlag::EffectWatched); +} + +bool HistoryItem::markEffectWatched() { + if (!hasUnwatchedEffect()) { + return false; + } + _flags |= MessageFlag::EffectWatched; + return true; +} + bool HistoryItem::mentionsMe() const { if (Has() && !Core::App().settings().notifyAboutPinned()) { @@ -1528,6 +1602,44 @@ void HistoryItem::addLogEntryOriginal( content); } +void HistoryItem::setFactcheck(MessageFactcheck info) { + if (!info) { + if (Has()) { + RemoveComponents(HistoryMessageFactcheck::Bit()); + history()->owner().requestItemResize(this); + } + } else { + AddComponents(HistoryMessageFactcheck::Bit()); + const auto factcheck = Get(); + const auto textChanged = (factcheck->data.text != info.text); + if (factcheck->data.hash == info.hash + && (info.needCheck || !factcheck->data.needCheck)) { + return; + } else if (textChanged + || factcheck->data.country != info.country + || factcheck->data.hash != info.hash) { + factcheck->data = std::move(info); + factcheck->requested = false; + if (textChanged) { + factcheck->page = nullptr; + } + history()->owner().requestItemResize(this); + } + } +} + +bool HistoryItem::hasUnrequestedFactcheck() const { + const auto factcheck = Get(); + return factcheck && factcheck->data.needCheck && !factcheck->requested; +} + +TextWithEntities HistoryItem::factcheckText() const { + if (const auto factcheck = Get()) { + return factcheck->data.text; + } + return {}; +} + PeerData *HistoryItem::specialNotificationPeer() const { return (mentionsMe() && !_history->peer->isUser()) ? from().get() @@ -1727,6 +1839,7 @@ void HistoryItem::applyEdition(HistoryMessageEdition &&edition) { } applyTTL(edition.ttl); + setFactcheck(FromMTP(this, edition.mtpFactcheck)); finishEdition(keyboardTop); } @@ -1846,6 +1959,7 @@ void HistoryItem::applySentMessage(const MTPDmessage &data) { setIsPinned(data.is_pinned()); contributeToSlowmode(data.vdate().v); addToSharedMediaIndex(); + addToMessagesIndex(); invalidateChatListEntry(); if (const auto period = data.vttl_period(); period && period->v > 0) { applyTTL(data.vdate().v + period->v); @@ -1870,6 +1984,7 @@ void HistoryItem::applySentMessage( contributeToSlowmode(data.vdate().v); if (!wasAlready) { addToSharedMediaIndex(); + addToMessagesIndex(); } invalidateChatListEntry(); if (const auto period = data.vttl_period(); period && period->v > 0) { @@ -2066,6 +2181,14 @@ void HistoryItem::removeFromSharedMediaIndex() { } } +void HistoryItem::addToMessagesIndex() { + if (isRegular()) { + if (const auto messages = _history->maybeMessages()) { + messages->addNew(id); + } + } +} + void HistoryItem::incrementReplyToTopCounter() { if (isRegular() && _history->peer->isMegagroup()) { _history->session().changes().messageUpdated( @@ -3151,6 +3274,7 @@ void HistoryItem::setText(const TextWithEntities &textWithEntities) { auto type = entity.type(); if (type == EntityType::Url || type == EntityType::CustomUrl + || type == EntityType::Phone || type == EntityType::Email) { _flags |= MessageFlag::HasTextLinks; break; @@ -3205,6 +3329,10 @@ MessageGroupId HistoryItem::groupId() const { return _groupId; } +EffectId HistoryItem::effectId() const { + return _effectId; +} + bool HistoryItem::isEmpty() const { if (isMessageHidden(const_cast(this))) { return true; @@ -3212,6 +3340,8 @@ bool HistoryItem::isEmpty() const { return _text.empty() && !_media + && (!Has() + || Get()->data.text.empty()) && !Has(); } @@ -3549,6 +3679,23 @@ void HistoryItem::setupForwardedComponent(const CreateConfig &config) { forwarded->imported = config.imported; } +void HistoryItem::applyInitialEffectWatched() { + if (!effectId()) { + return; + } else if (out()) { + // If this message came from the server, not generated on send. + _flags |= MessageFlag::EffectWatched; + } else if (_history->inboxReadTillId() && !unread(_history)) { + _flags |= MessageFlag::EffectWatched; + } +} + +void HistoryItem::applyEffectWatchedOnUnreadKnown() { + if (effectId() && !out() && !unread(_history)) { + _flags |= MessageFlag::EffectWatched; + } +} + bool HistoryItem::generateLocalEntitiesByReply() const { using namespace HistoryView; if (!_media) { @@ -3946,6 +4093,7 @@ void HistoryItem::createServiceFromMtp(const MTPDmessageService &message) { payment->slug = data.vinvoice_slug().value_or_empty(); payment->recurringInit = data.is_recurring_init(); payment->recurringUsed = data.is_recurring_used(); + payment->isCreditsCurrency = (currency == Ui::kCreditsCurrency); payment->amount = Ui::FillAmountAndCurrency(amount, currency); payment->invoiceLink = std::make_shared([=]( ClickContext context) { @@ -3956,7 +4104,10 @@ void HistoryItem::createServiceFromMtp(const MTPDmessageService &message) { CheckoutProcess::Start( item, Mode::Receipt, - crl::guard(weak, [=](auto) { weak->window().activate(); })); + crl::guard(weak, [=](auto) { weak->window().activate(); }), + Payments::ProcessNonPanelPaymentFormFactory( + weak.get(), + item)); } }); } else if (type == mtpc_messageActionGroupCall @@ -5065,7 +5216,7 @@ void HistoryItem::applyAction(const MTPMessageAction &action) { } void HistoryItem::setSelfDestruct( - HistoryServiceSelfDestruct::Type type, + HistorySelfDestructType type, MTPint mtpTTLvalue) { UpdateComponents(HistoryServiceSelfDestruct::Bit()); const auto selfdestruct = Get(); diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index fad1ea966..f716a3780 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -23,9 +23,11 @@ struct HistoryMessageReplyMarkup; struct HistoryMessageTranslation; struct HistoryMessageForwarded; struct HistoryMessageSavedMediaData; +struct HistoryMessageFactcheck; struct HistoryServiceDependentData; enum class HistorySelfDestructType; struct PreparedServiceText; +struct MessageFactcheck; class ReplyKeyboard; struct LanguageId; @@ -101,6 +103,7 @@ struct HistoryItemCommonFields { UserId viaBotId = 0; QString postAuthor; uint64 groupedId = 0; + EffectId effectId = 0; HistoryMessageMarkupData markup; }; @@ -203,6 +206,9 @@ public: WebPageId localId, const QString &label, const TextWithEntities &content); + void setFactcheck(MessageFactcheck info); + [[nodiscard]] bool hasUnrequestedFactcheck() const; + [[nodiscard]] TextWithEntities factcheckText() const; [[nodiscard]] not_null notificationThread() const; [[nodiscard]] not_null history() const { @@ -240,6 +246,8 @@ public: [[nodiscard]] bool mentionsMe() const; [[nodiscard]] bool isUnreadMention() const; [[nodiscard]] bool hasUnreadReaction() const; + [[nodiscard]] bool hasUnwatchedEffect() const; + bool markEffectWatched(); [[nodiscard]] bool isUnreadMedia() const; [[nodiscard]] bool isIncomingUnreadMedia() const; [[nodiscard]] bool hasUnreadMediaFlag() const; @@ -359,6 +367,7 @@ public: void indexAsNewItem(); void addToSharedMediaIndex(); + void addToMessagesIndex(); void removeFromSharedMediaIndex(); struct NotificationTextOptions { @@ -397,6 +406,7 @@ public: void setAyuHint(const QString &hint); void setRealId(MsgId newId); void incrementReplyToTopCounter(); + void applyEffectWatchedOnUnreadKnown(); [[nodiscard]] bool emptyText() const { return _text.empty(); @@ -493,8 +503,8 @@ public: not_null forwarded) const; [[nodiscard]] bool isEmpty() const; - [[nodiscard]] MessageGroupId groupId() const; + [[nodiscard]] EffectId effectId() const; [[nodiscard]] const HistoryMessageReplyMarkup *inlineReplyMarkup() const { return const_cast(this)->inlineReplyMarkup(); @@ -544,6 +554,7 @@ private: void createComponentsHelper(HistoryItemCommonFields &&fields); void createComponents(CreateConfig &&config); void setupForwardedComponent(const CreateConfig &config); + void applyInitialEffectWatched(); [[nodiscard]] bool generateLocalEntitiesByReply() const; [[nodiscard]] TextWithEntities withLocalEntities( @@ -647,8 +658,9 @@ private: int _boostsApplied = 0; BusinessShortcutId _shortcutId = 0; - HistoryView::Element *_mainView = nullptr; MessageGroupId _groupId = MessageGroupId(); + EffectId _effectId = 0; + HistoryView::Element *_mainView = nullptr; friend class HistoryView::Element; friend class HistoryView::Message; diff --git a/Telegram/SourceFiles/history/history_item_components.cpp b/Telegram/SourceFiles/history/history_item_components.cpp index 223aa7aad..9abd00f82 100644 --- a/Telegram/SourceFiles/history/history_item_components.cpp +++ b/Telegram/SourceFiles/history/history_item_components.cpp @@ -187,8 +187,11 @@ bool HiddenSenderInfo::paintCustomUserpic( return valid; } -void HistoryMessageForwarded::create(const HistoryMessageVia *via) const { +void HistoryMessageForwarded::create( + const HistoryMessageVia *via, + not_null item) const { auto phrase = TextWithEntities(); + auto context = Core::MarkedTextContext{}; const auto fromChannel = originalSender && originalSender->isChannel() && !originalSender->isMegagroup(); @@ -197,29 +200,48 @@ void HistoryMessageForwarded::create(const HistoryMessageVia *via) const { ? originalSender->name() : originalHiddenSenderInfo->name) }; + if (const auto copy = originalSender) { + context.session = ©->owner().session(); + context.customEmojiRepaint = [=] { + // It is important to capture here originalSender by value, + // not capture the HistoryMessageForwarded* and read the + // originalSender field, because the components themselves + // get moved from place to place and the captured `this` + // pointer may become invalid, resulting in a crash. + copy->owner().requestItemRepaint(item); + }; + phrase = Ui::Text::SingleCustomEmoji( + context.session->data().customEmojiManager().peerUserpicEmojiData( + copy, + st::fwdTextUserpicPadding)); + } if (!originalPostAuthor.isEmpty()) { - phrase = tr::lng_forwarded_signed( - tr::now, - lt_channel, - name, - lt_user, - { .text = originalPostAuthor }, - Ui::Text::WithEntities); + phrase.append( + tr::lng_forwarded_signed( + tr::now, + lt_channel, + name, + lt_user, + { .text = originalPostAuthor }, + Ui::Text::WithEntities)); } else { - phrase = name; + phrase.append(name); } if (story) { phrase = tr::lng_forwarded_story( tr::now, lt_user, - Ui::Text::Link(phrase.text, QString()), // Link 1. + Ui::Text::Wrapped(phrase, EntityType::CustomUrl, QString()), // Link 1. Ui::Text::WithEntities); } else if (via && psaType.isEmpty()) { + const auto linkData = Ui::Text::Link( + QString(), + 1).entities.front().data(); // Link 1. if (fromChannel) { phrase = tr::lng_forwarded_channel_via( tr::now, lt_channel, - Ui::Text::Link(phrase.text, 1), // Link 1. + Ui::Text::Wrapped(phrase, EntityType::CustomUrl, linkData), // Link 1. lt_inline_bot, Ui::Text::Link('@' + via->bot->username(), 2), // Link 2. Ui::Text::WithEntities); @@ -227,7 +249,7 @@ void HistoryMessageForwarded::create(const HistoryMessageVia *via) const { phrase = tr::lng_forwarded_via( tr::now, lt_user, - Ui::Text::Link(phrase.text, 1), // Link 1. + Ui::Text::Wrapped(phrase, EntityType::CustomUrl, linkData), // Link 1. lt_inline_bot, Ui::Text::Link('@' + via->bot->username(), 2), // Link 2. Ui::Text::WithEntities); @@ -252,18 +274,21 @@ void HistoryMessageForwarded::create(const HistoryMessageVia *via) const { : tr::lng_forwarded_psa_default)( tr::now, lt_channel, - Ui::Text::Link(phrase.text, QString()), // Link 1. + Ui::Text::Wrapped( + phrase, + EntityType::CustomUrl, + QString()), // Link 1. Ui::Text::WithEntities); } } else { phrase = tr::lng_forwarded( tr::now, lt_user, - Ui::Text::Link(phrase.text, QString()), // Link 1. + Ui::Text::Wrapped(phrase, EntityType::CustomUrl, QString()), // Link 1. Ui::Text::WithEntities); } } - text.setMarkedText(st::fwdTextStyle, phrase); + text.setMarkedText(st::fwdTextStyle, phrase, kMarkupTextOptions, context); text.setLink(1, fromChannel ? JumpToMessageClickHandler(originalSender, originalId) @@ -1067,6 +1092,35 @@ HistoryMessageLogEntryOriginal &HistoryMessageLogEntryOriginal::operator=( HistoryMessageLogEntryOriginal::~HistoryMessageLogEntryOriginal() = default; +MessageFactcheck FromMTP( + not_null item, + const tl::conditional &factcheck) { + return FromMTP(&item->history()->session(), factcheck); +} + +MessageFactcheck FromMTP( + not_null session, + const tl::conditional &factcheck) { + auto result = MessageFactcheck(); + if (!factcheck) { + return result; + } + const auto &data = factcheck->data(); + if (const auto text = data.vtext()) { + const auto &data = text->data(); + result.text = { + qs(data.vtext()), + Api::EntitiesFromMTP(session, data.ventities().v), + }; + } + if (const auto country = data.vcountry()) { + result.country = qs(country->v); + } + result.hash = data.vhash().v; + result.needCheck = data.is_need_check(); + return result; +} + HistoryDocumentCaptioned::HistoryDocumentCaptioned() : caption(st::msgFileMinWidth - st::msgPadding.left() - st::msgPadding.right()) { } diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h index e35a804a2..8d262a52e 100644 --- a/Telegram/SourceFiles/history/history_item_components.h +++ b/Telegram/SourceFiles/history/history_item_components.h @@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL struct WebPageData; class VoiceSeekClickHandler; +class ReplyKeyboard; namespace Ui { struct ChatPaintContext; @@ -31,6 +32,7 @@ struct GeometryDescriptor; namespace Data { class Session; class Story; +class SavedSublist; } // namespace Data namespace Media::Player { @@ -47,6 +49,10 @@ class Document; class TranscribeButton; } // namespace HistoryView +namespace style { +struct BotKeyboardButton; +} // namespace style + struct HistoryMessageVia : public RuntimeComponent { void create(not_null owner, UserId userId); void resize(int32 availw) const; @@ -125,7 +131,9 @@ private: }; struct HistoryMessageForwarded : public RuntimeComponent { - void create(const HistoryMessageVia *via) const; + void create( + const HistoryMessageVia *via, + not_null item) const; [[nodiscard]] bool forwardOfForward() const { return savedFromSender || savedFromHiddenSenderInfo; @@ -560,6 +568,34 @@ struct HistoryMessageLogEntryOriginal }; +struct MessageFactcheck { + TextWithEntities text; + QString country; + uint64 hash = 0; + bool needCheck = false; + + [[nodiscard]] bool empty() const { + return text.empty() && country.isEmpty() && !hash; + } + explicit operator bool() const { + return !empty(); + } +}; + +[[nodiscard]] MessageFactcheck FromMTP( + not_null item, + const tl::conditional &factcheck); +[[nodiscard]] MessageFactcheck FromMTP( + not_null session, + const tl::conditional &factcheck); + +struct HistoryMessageFactcheck +: public RuntimeComponent { + MessageFactcheck data; + WebPageData *page = nullptr; + bool requested = false; +}; + struct HistoryServiceData : public RuntimeComponent { std::vector textLinks; @@ -617,6 +653,7 @@ struct HistoryServicePayment ClickHandlerPtr invoiceLink; bool recurringInit = false; bool recurringUsed = false; + bool isCreditsCurrency = false; }; struct HistoryServiceSameBackground diff --git a/Telegram/SourceFiles/history/history_item_edition.cpp b/Telegram/SourceFiles/history/history_item_edition.cpp index 4349a3c93..d66d0cf7b 100644 --- a/Telegram/SourceFiles/history/history_item_edition.cpp +++ b/Telegram/SourceFiles/history/history_item_edition.cpp @@ -24,6 +24,7 @@ HistoryMessageEdition::HistoryMessageEdition( replyMarkup = HistoryMessageMarkupData(message.vreply_markup()); mtpMedia = message.vmedia(); mtpReactions = message.vreactions(); + mtpFactcheck = message.vfactcheck(); views = message.vviews().value_or(-1); forwards = message.vforwards().value_or(-1); if (const auto mtpReplies = message.vreplies()) { diff --git a/Telegram/SourceFiles/history/history_item_edition.h b/Telegram/SourceFiles/history/history_item_edition.h index b8f577d80..a44109299 100644 --- a/Telegram/SourceFiles/history/history_item_edition.h +++ b/Telegram/SourceFiles/history/history_item_edition.h @@ -36,4 +36,5 @@ struct HistoryMessageEdition { HistoryMessageRepliesData replies; const MTPMessageMedia *mtpMedia = nullptr; const MTPMessageReactions *mtpReactions = nullptr; + const MTPFactCheck *mtpFactcheck = nullptr; }; diff --git a/Telegram/SourceFiles/history/history_item_helpers.cpp b/Telegram/SourceFiles/history/history_item_helpers.cpp index 0b0f55d13..681db97e1 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.cpp +++ b/Telegram/SourceFiles/history/history_item_helpers.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_text_entities.h" #include "boxes/premium_preview_box.h" #include "calls/calls_instance.h" +#include "data/components/sponsored_messages.h" #include "data/stickers/data_custom_emoji.h" #include "data/notify/data_notify_settings.h" #include "data/data_channel.h" @@ -367,7 +368,14 @@ ClickHandlerPtr HideSponsoredClickHandler() { return std::make_shared([=](ClickContext context) { const auto my = context.other.value(); if (const auto controller = my.sessionWindow.get()) { - ShowPremiumPreviewBox(controller, PremiumFeature::NoAds); + const auto &session = controller->session(); + if (session.premium()) { + using Result = Data::SponsoredReportResult; + session.sponsoredMessages().createReportCallback( + my.itemId)(Result::Id("-1"), [](const auto &) {}); + } else { + ShowPremiumPreviewBox(controller, PremiumFeature::NoAds); + } } }); } diff --git a/Telegram/SourceFiles/history/history_item_text.cpp b/Telegram/SourceFiles/history/history_item_text.cpp index 1c66c59a4..ba35168cf 100644 --- a/Telegram/SourceFiles/history/history_item_text.cpp +++ b/Telegram/SourceFiles/history/history_item_text.cpp @@ -46,6 +46,12 @@ TextForMimeData HistoryItemText(not_null item) { titleResult.append('\n').append(std::move(descriptionResult)); return titleResult; }(); + auto factcheckResult = [&] { + const auto factcheck = item->Get(); + return factcheck + ? TextForMimeData::Rich(base::duplicate(factcheck->data.text)) + : TextForMimeData(); + }(); auto result = textResult; if (result.empty()) { result = std::move(mediaResult); @@ -57,6 +63,11 @@ TextForMimeData HistoryItemText(not_null item) { } else if (!logEntryOriginalResult.empty()) { result.append(u"\n\n"_q).append(std::move(logEntryOriginalResult)); } + if (result.empty()) { + result = std::move(factcheckResult); + } else if (!factcheckResult.empty()) { + result.append(u"\n\n"_q).append(std::move(factcheckResult)); + } return result; } diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index a8ca276ff..c90d99fcb 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -74,6 +74,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_file_origin.h" #include "data/data_histories.h" #include "data/data_group_call.h" +#include "data/data_message_reactions.h" #include "data/data_peer_values.h" // Data::AmPremiumValue. #include "data/data_premium_limits.h" // Data::PremiumLimits. #include "data/stickers/data_stickers.h" @@ -328,13 +329,26 @@ HistoryWidget::HistoryWidget( _fieldBarCancel->addClickHandler([=] { cancelFieldAreaState(); }); _send->addClickHandler([=] { sendButtonClicked(); }); - SendMenu::SetupMenuAndShortcuts( - _send.get(), - [=] { return sendButtonMenuType(); }, - [=] { sendSilent(); }, - [=] { sendScheduled(); }, - [=] { sendWhenOnline(); }); - + { + using namespace SendMenu; + const auto sendAction = [=](Action action, Details) { + if (action.type == ActionType::CaptionUp + || action.type == ActionType::CaptionDown + || action.type == ActionType::SpoilerOn + || action.type == ActionType::SpoilerOff) { + _mediaEditManager.apply(action); + } else if (action.type == ActionType::Send) { + send(action.options); + } else { + sendScheduled(action.options); + } + }; + SetupMenuAndShortcuts( + _send.get(), + controller->uiShow(), + [=] { return sendButtonMenuDetails(); }, + sendAction); + } _unblock->addClickHandler([=] { unblockUser(); }); _botStart->addClickHandler([=] { sendBotStartCommand(); }); _joinChannel->addClickHandler([=] { joinChannel(); }); @@ -520,7 +534,9 @@ HistoryWidget::HistoryWidget( } }, lifetime()); - _fieldAutocomplete->setSendMenuType([=] { return sendMenuType(); }); + _fieldAutocomplete->setSendMenuDetails([=] { + return sendMenuDetails(); + }); if (_supportAutocomplete) { supportInitAutocomplete(); @@ -1208,7 +1224,7 @@ void HistoryWidget::initTabbedSelector() { selector->contextMenuRequested( ) | filter | rpl::start_with_next([=] { - selector->showMenuWithType(sendMenuType()); + selector->showMenuWithDetails(sendMenuDetails()); }, lifetime()); selector->choosingStickerUpdated( @@ -1413,7 +1429,10 @@ int HistoryWidget::itemTopForHighlight( const auto itemTop = _list->itemTop(view); Assert(itemTop >= 0); - const auto reactionCenter = view->data()->hasUnreadReaction() + const auto item = view->data(); + const auto unwatchedEffect = item->hasUnwatchedEffect(); + const auto showReactions = item->hasUnreadReaction() || unwatchedEffect; + const auto reactionCenter = showReactions ? view->reactionButtonParameters({}, {}).center.y() : -1; @@ -1423,7 +1442,7 @@ int HistoryWidget::itemTopForHighlight( if (heightLeft >= 0) { return std::max(itemTop - (heightLeft / 2), 0); } else if (reactionCenter >= 0) { - const auto maxSize = st::reactionInfoImage; + const auto maxSize = st::reactionInlineImage; // Show message right till the bottom. const auto forBottom = itemTop + viewHeight - visibleAreaHeight; @@ -1584,7 +1603,9 @@ void HistoryWidget::applyInlineBotQuery(UserData *bot, const QString &query) { sendInlineResult(result); } }); - _inlineResults->setSendMenuType([=] { return sendMenuType(); }); + _inlineResults->setSendMenuDetails([=] { + return sendMenuDetails(); + }); _inlineResults->requesting( ) | rpl::start_with_next([=](bool requesting) { _tabbedSelectorToggle->setLoading(requesting); @@ -2681,7 +2702,7 @@ void HistoryWidget::setEditMsgId(MsgId msgId) { unregisterDraftSources(); _editMsgId = msgId; if (!msgId) { - _mediaEditSpoiler.setSpoilerOverride(std::nullopt); + _mediaEditManager.cancel(); _canReplaceMedia = false; if (_preview) { _preview->setDisabled(false); @@ -2753,6 +2774,8 @@ bool HistoryWidget::updateReplaceMediaButton() { controller(), { _history->peer->id, _editMsgId }, _field->getTextWithTags(), + _mediaEditManager.spoilered(), + _mediaEditManager.invertCaption(), crl::guard(_list, [=] { cancelEdit(); })); }); }); @@ -3263,6 +3286,9 @@ void HistoryWidget::newItemAdded(not_null item) { if (item->showNotification()) { destroyUnreadBar(); if (markingMessagesRead()) { + if (_list && item->hasUnwatchedEffect()) { + _list->startEffectOnRead(item); + } if (item->isUnreadMention() && !item->isUnreadMedia()) { session().api().markContentsRead(item); } @@ -4065,15 +4091,14 @@ void HistoryWidget::saveEditMsg() { })(); }; - auto options = Api::SendOptions(); _saveEditMsgRequestId = Api::EditTextMessage( item, sending, webPageDraft, - options, + { .invertCaption = _mediaEditManager.invertCaption() }, done, fail, - _mediaEditSpoiler.spoilerOverride()); + _mediaEditManager.spoilered()); } void HistoryWidget::hideChildWidgets() { @@ -4212,11 +4237,7 @@ void HistoryWidget::sendWithModifiers(Qt::KeyboardModifiers modifiers) { send({ .handleSupportSwitch = Support::HandleSwitch(modifiers) }); } -void HistoryWidget::sendSilent() { - send({ .silent = true }); -} - -void HistoryWidget::sendScheduled() { +void HistoryWidget::sendScheduled(Api::SendOptions initialOptions) { if (!_list) { return; } @@ -4226,23 +4247,31 @@ void HistoryWidget::sendScheduled() { ignoreSlowmodeCountdown)) { return; } - const auto callback = [=](Api::SendOptions options) { send(options); }; controller()->show( - HistoryView::PrepareScheduleBox(_list, sendMenuType(), callback)); + HistoryView::PrepareScheduleBox( + _list, + controller()->uiShow(), + sendButtonDefaultDetails(), + [=](Api::SendOptions options) { send(options); }, + initialOptions)); } -void HistoryWidget::sendWhenOnline() { - send(Api::DefaultSendWhenOnlineOptions()); -} - -SendMenu::Type HistoryWidget::sendMenuType() const { - return !_peer +SendMenu::Details HistoryWidget::sendMenuDetails() const { + const auto type = !_peer ? SendMenu::Type::Disabled : _peer->isSelf() ? SendMenu::Type::Reminder : HistoryView::CanScheduleUntilOnline(_peer) ? SendMenu::Type::ScheduledToUser : SendMenu::Type::Scheduled; + const auto effectAllowed = _peer && _peer->isUser(); + return { .type = type, .effectAllowed = effectAllowed }; +} + +SendMenu::Details HistoryWidget::saveMenuDetails() const { + return (_editMsgId && _replyEditMsg) + ? _mediaEditManager.sendMenuDetails(HasSendText(_field)) + : SendMenu::Details(); } auto HistoryWidget::computeSendButtonType() const { @@ -4258,10 +4287,23 @@ auto HistoryWidget::computeSendButtonType() const { return Type::Send; } -SendMenu::Type HistoryWidget::sendButtonMenuType() const { - return (computeSendButtonType() == Ui::SendButton::Type::Send) - ? sendMenuType() - : SendMenu::Type::Disabled; +SendMenu::Details HistoryWidget::sendButtonMenuDetails() const { + using Type = Ui::SendButton::Type; + const auto type = computeSendButtonType(); + if (type == Type::Save) { + return saveMenuDetails(); + } else if (type != Type::Send) { + return {}; + } + return sendButtonDefaultDetails(); +} + +SendMenu::Details HistoryWidget::sendButtonDefaultDetails() const { + auto result = sendMenuDetails(); + if (!HasSendText(_field) && !_previewDrawPreview) { + result.effectAllowed = false; + } + return result; } void HistoryWidget::unblockUser() { @@ -5621,6 +5663,8 @@ bool HistoryWidget::confirmSendingFiles( { _history->peer->id, _editMsgId }, std::move(list), _field->getTextWithTags(), + _mediaEditManager.spoilered(), + _mediaEditManager.invertCaption(), crl::guard(_list, [=] { cancelEdit(); })); return true; } @@ -5640,7 +5684,7 @@ bool HistoryWidget::confirmSendingFiles( text, _peer, Api::SendType::Normal, - sendMenuType()); + sendMenuDetails()); _field->setTextWithTags({}); box->setConfirmedCallback(crl::guard(this, [=]( Ui::PreparedList &&list, @@ -6607,16 +6651,18 @@ void HistoryWidget::mousePressEvent(QMouseEvent *e) { if (_editMsgId && (_inDetails || _inPhotoEdit) && (e->button() == Qt::RightButton)) { - _mediaEditSpoiler.showMenu( + _mediaEditManager.showMenu( _list, - session().data().message(_history->peer, _editMsgId), - [=](bool) { mouseMoveEvent(nullptr); }); + [=] { mouseMoveEvent(nullptr); }, + HasSendText(_field)); } else if (_inPhotoEdit && _photoEditMedia) { EditCaptionBox::StartPhotoEdit( controller(), _photoEditMedia, { _history->peer->id, _editMsgId }, _field->getTextWithTags(), + _mediaEditManager.spoilered(), + _mediaEditManager.invertCaption(), crl::guard(_list, [=] { cancelEdit(); })); } else if (!_inDetails) { return; @@ -7243,10 +7289,27 @@ void HistoryWidget::refreshPinnedBarButton(bool many, HistoryItem *item) { if ((rows.size() == 1) && (rows.front().size() == 1)) { const auto text = rows.front().front().text; if (!text.isEmpty()) { + const auto &st = st::historyPinnedBotButton; auto button = object_ptr( this, - rpl::single(text), - st::historyPinnedBotButton); + rpl::never(), + st); + const auto label = Ui::CreateChild( + button.data(), + text, + st::historyPinnedBotLabel); + if (label->width() > st::historyPinnedBotButtonMaxWidth) { + label->resizeToWidth(st::historyPinnedBotButtonMaxWidth); + } + button->setFullWidth(label->width() + + st.padding.left() + + st.padding.right() + + st.height); + label->moveToLeft( + st.padding.left() + st.height / 2, + (button->height() - label->height()) / 2); + label->setTextColorOverride(st.textFg->c); + label->setAttribute(Qt::WA_TransparentForMouseEvents); button->setTextTransform( Ui::RoundButton::TextTransform::NoTransform); button->setFullRadius(true); @@ -7256,9 +7319,6 @@ void HistoryWidget::refreshPinnedBarButton(bool many, HistoryItem *item) { 0, 0); }); - if (button->width() > st::historyPinnedBotButtonMaxWidth) { - button->setFullWidth(st::historyPinnedBotButtonMaxWidth); - } struct State { base::unique_qptr menu; }; @@ -8221,6 +8281,9 @@ void HistoryWidget::updateReplyEditTexts(bool force) { const auto editMedia = _editMsgId ? _replyEditMsg->media() : nullptr; + if (_editMsgId && _replyEditMsg) { + _mediaEditManager.start(_replyEditMsg); + } _canReplaceMedia = editMedia && editMedia->allowsEditMedia(); _photoEditMedia = (_canReplaceMedia && editMedia->photo() @@ -8314,14 +8377,12 @@ void HistoryWidget::drawField(Painter &p, const QRect &rect) { ? drawMsgText->media() : nullptr; const auto hasPreview = media && media->hasReplyPreview(); - const auto preview = _mediaEditSpoiler.spoilerOverride() - ? _mediaEditSpoiler.mediaPreview(drawMsgText) + const auto preview = _mediaEditManager + ? _mediaEditManager.mediaPreview() : hasPreview ? media->replyPreview() : nullptr; - const auto spoilered = _mediaEditSpoiler.spoilerOverride() - ? (*_mediaEditSpoiler.spoilerOverride()) - : (preview && media->hasSpoiler()); + const auto spoilered = _mediaEditManager.spoilered(); if (!spoilered) { _replySpoiler = nullptr; } else if (!_replySpoiler) { diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index 47bdaecfa..fd34d78cc 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -33,7 +33,7 @@ class PhotoMedia; } // namespace Data namespace SendMenu { -enum class Type; +struct Details; } // namespace SendMenu namespace Api { @@ -269,7 +269,8 @@ public: void messageShotSelected(); void clearSelected(); - [[nodiscard]] SendMenu::Type sendMenuType() const; + [[nodiscard]] SendMenu::Details sendMenuDetails() const; + [[nodiscard]] SendMenu::Details saveMenuDetails() const; bool sendExistingDocument( not_null document, Api::SendOptions options, @@ -396,10 +397,9 @@ private: Api::SendOptions options) const; void send(Api::SendOptions options); void sendWithModifiers(Qt::KeyboardModifiers modifiers); - void sendSilent(); - void sendScheduled(); - void sendWhenOnline(); - [[nodiscard]] SendMenu::Type sendButtonMenuType() const; + void sendScheduled(Api::SendOptions initialOptions); + [[nodiscard]] SendMenu::Details sendButtonMenuDetails() const; + [[nodiscard]] SendMenu::Details sendButtonDefaultDetails() const; void handlePendingHistoryUpdate(); void fullInfoUpdated(); void toggleTabbedSelectorMode(); @@ -663,7 +663,7 @@ private: MsgId _editMsgId = 0; std::shared_ptr _photoEditMedia; bool _canReplaceMedia = false; - HistoryView::MediaEditSpoilerManager _mediaEditSpoiler; + HistoryView::MediaEditManager _mediaEditManager; HistoryItem *_replyEditMsg = nullptr; Ui::Text::String _replyEditMsgText; diff --git a/Telegram/SourceFiles/history/view/controls/compose_controls_common.h b/Telegram/SourceFiles/history/view/controls/compose_controls_common.h index 01237072b..e51bae134 100644 --- a/Telegram/SourceFiles/history/view/controls/compose_controls_common.h +++ b/Telegram/SourceFiles/history/view/controls/compose_controls_common.h @@ -21,7 +21,7 @@ struct MessageToEdit { FullMsgId fullId; Api::SendOptions options; TextWithTags textWithTags; - std::optional spoilerMediaOverride; + bool spoilered = false; }; struct VoiceToSend { QByteArray bytes; diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp index 5e988e51e..846c85a01 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -124,7 +124,8 @@ class FieldHeader final : public Ui::RpWidget { public: FieldHeader( QWidget *parent, - std::shared_ptr show); + std::shared_ptr show, + Fn hasSendText); void setHistory(const SetHistoryArgs &args); void updateTopicRootId(MsgId topicRootId); @@ -138,6 +139,8 @@ public: void previewReady(rpl::producer parsed); void previewUnregister(); + void mediaEditManagerApply(SendMenu::Action action); + [[nodiscard]] bool isDisplayed() const; [[nodiscard]] bool isEditingMessage() const; [[nodiscard]] bool readyToForward() const; @@ -149,6 +152,7 @@ public: [[nodiscard]] rpl::producer<> editPhotoRequests() const; [[nodiscard]] rpl::producer<> editOptionsRequests() const; [[nodiscard]] MessageToEdit queryToEdit(); + [[nodiscard]] SendMenu::Details saveMenuDetails(bool hasSendText) const; [[nodiscard]] FullReplyTo getDraftReply() const; [[nodiscard]] rpl::producer<> editCancelled() const { @@ -187,6 +191,8 @@ private: }; const std::shared_ptr _show; + const Fn _hasSendText; + History *_history = nullptr; MsgId _topicRootId = 0; @@ -213,7 +219,7 @@ private: bool _repaintScheduled : 1 = false; bool _inClickable : 1 = false; - HistoryView::MediaEditSpoilerManager _mediaEditSpoiler; + HistoryView::MediaEditManager _mediaEditManager; const not_null _data; const not_null _cancel; @@ -230,9 +236,11 @@ private: FieldHeader::FieldHeader( QWidget *parent, - std::shared_ptr show) + std::shared_ptr show, + Fn hasSendText) : RpWidget(parent) , _show(std::move(show)) +, _hasSendText(std::move(hasSendText)) , _forwardPanel( std::make_unique([=] { customEmojiRepaint(); })) , _data(&_show->session().data()) @@ -404,10 +412,10 @@ void FieldHeader::init() { } } else if (!isLeftButton) { if (inPreviewRect && isEditingMessage()) { - _mediaEditSpoiler.showMenu( + _mediaEditManager.showMenu( this, - _data->message(_editMsgId.current()), - [=](bool) { update(); }); + [=] { update(); }, + _hasSendText()); } else if (const auto reply = replyingToMessage()) { _jumpToItemRequests.fire_copy(reply); } @@ -524,6 +532,10 @@ void FieldHeader::previewUnregister() { _previewLifetime.destroy(); } +void FieldHeader::mediaEditManagerApply(SendMenu::Action action) { + _mediaEditManager.apply(action); +} + void FieldHeader::paintWebPage(Painter &p, not_null context) { Expects(!!_preview.parsed); @@ -582,14 +594,12 @@ void FieldHeader::paintEditOrReplyToMessage(Painter &p) { const auto media = _shownMessage->media(); _shownMessageHasPreview = media && media->hasReplyPreview(); - const auto preview = _mediaEditSpoiler.spoilerOverride() - ? _mediaEditSpoiler.mediaPreview(_shownMessage) + const auto preview = _mediaEditManager + ? _mediaEditManager.mediaPreview() : _shownMessageHasPreview ? media->replyPreview() : nullptr; - const auto spoilered = _mediaEditSpoiler.spoilerOverride() - ? (*_mediaEditSpoiler.spoilerOverride()) - : (preview && media->hasSpoiler()); + const auto spoilered = _mediaEditManager.spoilered(); if (!spoilered) { _shownPreviewSpoiler = nullptr; } else if (!_shownPreviewSpoiler) { @@ -733,8 +743,12 @@ void FieldHeader::updateControlsGeometry(QSize size) { void FieldHeader::editMessage(FullMsgId id, bool photoEditAllowed) { _photoEditAllowed = photoEditAllowed; _editMsgId = id; + if (!id) { + _mediaEditManager.cancel(); + } else if (const auto item = _show->session().data().message(id)) { + _mediaEditManager.start(item); + } if (!photoEditAllowed) { - _mediaEditSpoiler.setSpoilerOverride(std::nullopt); _inPhotoEdit = false; _inPhotoEditOver.stop(); } @@ -781,11 +795,18 @@ MessageToEdit FieldHeader::queryToEdit() { .options = { .scheduled = item->isScheduled() ? item->date() : 0, .shortcutId = item->shortcutId(), + .invertCaption = _mediaEditManager.invertCaption(), }, - .spoilerMediaOverride = _mediaEditSpoiler.spoilerOverride(), + .spoilered = _mediaEditManager.spoilered(), }; } +SendMenu::Details FieldHeader::saveMenuDetails(bool hasSendText) const { + return isEditingMessage() + ? _mediaEditManager.sendMenuDetails(hasSendText) + : SendMenu::Details(); +} + ComposeControls::ComposeControls( not_null parent, ComposeControlsDescriptor descriptor) @@ -842,7 +863,10 @@ ComposeControls::ComposeControls( parent, _show, &_st.tabbed)) -, _header(std::make_unique(_wrap.get(), _show)) +, _header(std::make_unique( + _wrap.get(), + _show, + [=] { return HasSendText(_field); })) , _voiceRecordBar(std::make_unique( _wrap.get(), Controls::VoiceRecordBarDescriptor{ @@ -854,7 +878,7 @@ ComposeControls::ComposeControls( .recorderHeight = st::historySendSize.height(), .lockFromBottom = descriptor.voiceLockFromBottom, })) -, _sendMenuType(descriptor.sendMenuType) +, _sendMenuDetails(descriptor.sendMenuDetails) , _unavailableEmojiPasted(std::move(descriptor.unavailableEmojiPasted)) , _saveDraftTimer([=] { saveDraft(); }) , _saveCloudDraftTimer([=] { saveCloudDraft(); }) { @@ -1131,11 +1155,14 @@ bool ComposeControls::confirmMediaEdit(Ui::PreparedList &list) { if (!isEditingMessage() || !_regularWindow) { return false; } else if (_canReplaceMedia) { + const auto queryToEdit = _header->queryToEdit(); EditCaptionBox::StartMediaReplace( _regularWindow, _editingId, std::move(list), _field->getTextWithTags(), + queryToEdit.spoilered, + queryToEdit.options.invertCaption, crl::guard(_wrap.get(), [=] { cancelEditMessage(); })); } else { _show->showToast(tr::lng_edit_caption_attach(tr::now)); @@ -1394,11 +1421,14 @@ void ComposeControls::init() { _header->editPhotoRequests( ) | rpl::start_with_next([=] { + const auto queryToEdit = _header->queryToEdit(); EditCaptionBox::StartPhotoEdit( _regularWindow, _photoEditMedia, _editingId, _field->getTextWithTags(), + queryToEdit.spoilered, + queryToEdit.options.invertCaption, crl::guard(_wrap.get(), [=] { cancelEditMessage(); })); }, _wrap->lifetime()); @@ -1719,7 +1749,7 @@ void ComposeControls::initAutocomplete() { } }, _autocomplete->lifetime()); - _autocomplete->setSendMenuType([=] { return sendMenuType(); }); + _autocomplete->setSendMenuDetails([=] { return sendMenuDetails(); }); //_autocomplete->setModerateKeyActivateCallback([=](int key) { // return _keyboard->isHidden() @@ -2162,7 +2192,7 @@ void ComposeControls::initTabbedSelector() { _selector->contextMenuRequested( ) | rpl::start_with_next([=] { - _selector->showMenuWithType(sendMenuType()); + _selector->showMenuWithDetails(sendMenuDetails()); }, wrap->lifetime()); _selector->choosingStickerUpdated( @@ -2191,16 +2221,28 @@ void ComposeControls::initSendButton() { cancelInlineBot(); }, _send->lifetime()); - const auto send = [=](Api::SendOptions options) { + const auto send = crl::guard(_send.get(), [=](Api::SendOptions options) { _sendCustomRequests.fire(std::move(options)); + }); + + using namespace SendMenu; + const auto sendAction = [=](Action action, Details details) { + if (action.type == ActionType::CaptionUp + || action.type == ActionType::CaptionDown + || action.type == ActionType::SpoilerOn + || action.type == ActionType::SpoilerOff) { + _header->mediaEditManagerApply(action); + } else { + SendMenu::DefaultCallback(_show, send)(action, details); + } }; + SendMenu::SetupMenuAndShortcuts( _send.get(), - [=] { return sendButtonMenuType(); }, - SendMenu::DefaultSilentCallback(send), - SendMenu::DefaultScheduleCallback(_show, sendMenuType(), send), - SendMenu::DefaultWhenOnlineCallback(send)); + _show, + [=] { return sendButtonMenuDetails(); }, + SendMenu::DefaultCallback(_show, send)); } void ComposeControls::initSendAsButton(not_null peer) { @@ -2393,6 +2435,10 @@ void ComposeControls::initWriteRestriction() { updateWrappingVisibility(); return; } + if (_like && _like->parentWidget() == _writeRestricted.get()) { + // Fix a crash because of _like destruction with its parent. + _like->setParent(_wrap.get()); + } _writeRestricted = std::make_unique(_parent); _writeRestricted->move(_wrap->pos()); _writeRestricted->resizeToWidth(_wrap->widthNoMargins()); @@ -2509,14 +2555,20 @@ auto ComposeControls::computeSendButtonType() const { return (_mode == Mode::Normal) ? Type::Send : Type::Schedule; } -SendMenu::Type ComposeControls::sendMenuType() const { - return !_history ? SendMenu::Type::Disabled : _sendMenuType; +SendMenu::Details ComposeControls::sendMenuDetails() const { + return !_history ? SendMenu::Details() : _sendMenuDetails(); } -SendMenu::Type ComposeControls::sendButtonMenuType() const { - return (computeSendButtonType() == Ui::SendButton::Type::Send) - ? sendMenuType() - : SendMenu::Type::Disabled; +SendMenu::Details ComposeControls::saveMenuDetails() const { + return _header->saveMenuDetails(HasSendText(_field)); +} + +SendMenu::Details ComposeControls::sendButtonMenuDetails() const { + return (computeSendButtonType() == Ui::SendButton::Type::Save) + ? saveMenuDetails() + : (computeSendButtonType() == Ui::SendButton::Type::Send) + ? sendMenuDetails() + : SendMenu::Details(); } void ComposeControls::updateSendButtonType() { @@ -2937,10 +2989,13 @@ bool ComposeControls::updateReplaceMediaButton() { const auto hideDuration = st::historyReplaceMedia.ripple.hideDuration; _replaceMedia->setClickedCallback([=] { base::call_delayed(hideDuration, _wrap.get(), [=] { + const auto queryToEdit = _header->queryToEdit(); EditCaptionBox::StartMediaReplace( _regularWindow, _editingId, _field->getTextWithTags(), + queryToEdit.spoilered, + queryToEdit.options.invertCaption, crl::guard(_wrap.get(), [=] { cancelEditMessage(); })); }); }); @@ -3324,7 +3379,9 @@ void ComposeControls::applyInlineBotQuery( _inlineResultChosen.fire_copy(result); } }); - _inlineResults->setSendMenuType([=] { return sendMenuType(); }); + _inlineResults->setSendMenuDetails([=] { + return sendMenuDetails(); + }); _inlineResults->requesting( ) | rpl::start_with_next([=](bool requesting) { _tabbedSelectorToggle->setLoading(requesting); diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h index f6c915968..fb373a0f7 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h @@ -29,7 +29,7 @@ struct ComposeControls; } // namespace style namespace SendMenu { -enum class Type; +struct Details; } // namespace SendMenu namespace ChatHelpers { @@ -104,7 +104,7 @@ struct ComposeControlsDescriptor { std::shared_ptr show; Fn)> unavailableEmojiPasted; ComposeControlsMode mode = ComposeControlsMode::Normal; - SendMenu::Type sendMenuType = {}; + Fn sendMenuDetails = nullptr; Window::SessionController *regularWindow = nullptr; rpl::producer stickerOrEmojiChosen; rpl::producer customPlaceholder; @@ -282,8 +282,9 @@ private: void paintBackground(QPainter &p, QRect full, QRect clip); [[nodiscard]] auto computeSendButtonType() const; - [[nodiscard]] SendMenu::Type sendMenuType() const; - [[nodiscard]] SendMenu::Type sendButtonMenuType() const; + [[nodiscard]] SendMenu::Details sendMenuDetails() const; + [[nodiscard]] SendMenu::Details saveMenuDetails() const; + [[nodiscard]] SendMenu::Details sendButtonMenuDetails() const; [[nodiscard]] auto sendContentRequests( SendRequestType requestType = SendRequestType::Text) const; @@ -396,7 +397,7 @@ private: const std::unique_ptr _header; const std::unique_ptr _voiceRecordBar; - const SendMenu::Type _sendMenuType; + const Fn _sendMenuDetails; const Fn)> _unavailableEmojiPasted; rpl::event_stream _sendCustomRequests; diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_media_edit_manager.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_media_edit_manager.cpp index 940ff7ff2..0f765fe69 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_media_edit_manager.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_media_edit_manager.cpp @@ -10,74 +10,149 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_document.h" #include "data/data_file_origin.h" #include "data/data_photo.h" +#include "data/data_session.h" #include "history/history.h" #include "history/history_item.h" #include "lang/lang_keys.h" +#include "menu/menu_send.h" #include "ui/widgets/popup_menu.h" +#include "styles/style_chat_helpers.h" #include "styles/style_menu_icons.h" namespace HistoryView { -MediaEditSpoilerManager::MediaEditSpoilerManager() = default; +MediaEditManager::MediaEditManager() = default; -void MediaEditSpoilerManager::showMenu( - not_null parent, +void MediaEditManager::start( not_null item, - Fn callback) { + std::optional spoilered, + std::optional invertCaption) { const auto media = item->media(); - const auto hasPreview = media && media->hasReplyPreview(); - const auto preview = hasPreview ? media->replyPreview() : nullptr; - if (!preview) { + if (!media) { return; } - const auto spoilered = _spoilerOverride - ? (*_spoilerOverride) - : (preview && media->hasSpoiler()); - const auto menu = Ui::CreateChild( - parent, - st::popupMenuWithIcons); - menu->addAction( - spoilered - ? tr::lng_context_disable_spoiler(tr::now) - : tr::lng_context_spoiler_effect(tr::now), - [=] { - _spoilerOverride = (!spoilered); - if (callback) { - callback(!spoilered); - } - }, - spoilered ? &st::menuIconSpoilerOff : &st::menuIconSpoiler); - menu->popup(QCursor::pos()); + _item = item; + _spoilered = spoilered.value_or(media->hasSpoiler()); + _invertCaption = invertCaption.value_or(item->invertMedia()); + _lifetime = item->history()->owner().itemRemoved( + ) | rpl::start_with_next([=](not_null removed) { + if (removed == _item) { + cancel(); + } + }); } -[[nodiscard]] Image *MediaEditSpoilerManager::mediaPreview( - not_null item) { - if (!_spoilerOverride) { - return nullptr; +void MediaEditManager::apply(SendMenu::Action action) { + using Type = SendMenu::Action::Type; + if (action.type == Type::CaptionUp) { + _invertCaption = true; + } else if (action.type == Type::CaptionDown) { + _invertCaption = false; + } else if (action.type == Type::SpoilerOn) { + _spoilered = true; + } else if (action.type == Type::SpoilerOff) { + _spoilered = false; } - if (const auto media = item->media()) { +} + +void MediaEditManager::cancel() { + _menu = nullptr; + _item = nullptr; + _lifetime.destroy(); +} + +void MediaEditManager::showMenu( + not_null parent, + Fn finished, + bool hasCaptionText) { + if (!_item) { + return; + } + const auto media = _item->media(); + const auto hasPreview = media && media->hasReplyPreview(); + const auto preview = hasPreview ? media->replyPreview() : nullptr; + if (!preview || (media && media->webpage())) { + return; + } + _menu = base::make_unique_q( + parent, + st::popupMenuWithIcons); + const auto callback = [=](SendMenu::Action action, const auto &) { + apply(action); + }; + const auto position = QCursor::pos(); + SendMenu::FillSendMenu( + _menu.get(), + nullptr, + sendMenuDetails(hasCaptionText), + callback, + &st::defaultComposeIcons, + position); + _menu->popup(position); +} + +Image *MediaEditManager::mediaPreview() { + if (const auto media = _item ? _item->media() : nullptr) { if (const auto photo = media->photo()) { return photo->getReplyPreview( - item->fullId(), - item->history()->peer, - *_spoilerOverride); + _item->fullId(), + _item->history()->peer, + _spoilered); } else if (const auto document = media->document()) { return document->getReplyPreview( - item->fullId(), - item->history()->peer, - *_spoilerOverride); + _item->fullId(), + _item->history()->peer, + _spoilered); } } return nullptr; } -void MediaEditSpoilerManager::setSpoilerOverride( - std::optional spoilerOverride) { - _spoilerOverride = spoilerOverride; +bool MediaEditManager::spoilered() const { + return _spoilered; } -std::optional MediaEditSpoilerManager::spoilerOverride() const { - return _spoilerOverride; +bool MediaEditManager::invertCaption() const { + return _invertCaption; +} + +SendMenu::Details MediaEditManager::sendMenuDetails( + bool hasCaptionText) const { + const auto media = _item ? _item->media() : nullptr; + if (!media) { + return {}; + } + const auto editingMedia = media->allowsEditMedia(); + const auto editPhoto = editingMedia ? media->photo() : nullptr; + const auto editDocument = editingMedia ? media->document() : nullptr; + const auto canSaveSpoiler = CanBeSpoilered(_item); + const auto canMoveCaption = media->allowsEditCaption() + && hasCaptionText + && (editPhoto + || (editDocument + && (editDocument->isVideoFile() || editDocument->isGifv()))); + return { + .spoiler = (!canSaveSpoiler + ? SendMenu::SpoilerState::None + : _spoilered + ? SendMenu::SpoilerState::Enabled + : SendMenu::SpoilerState::Possible), + .caption = (!canMoveCaption + ? SendMenu::CaptionState::None + : _invertCaption + ? SendMenu::CaptionState::Above + : SendMenu::CaptionState::Below), + }; +} + +bool MediaEditManager::CanBeSpoilered(not_null item) { + const auto media = item ? item->media() : nullptr; + const auto editingMedia = media && media->allowsEditMedia(); + const auto editPhoto = editingMedia ? media->photo() : nullptr; + const auto editDocument = editingMedia ? media->document() : nullptr; + return (editPhoto && !editPhoto->isNull()) + || (editDocument + && (editDocument->isVideoFile() || editDocument->isGifv())); } } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_media_edit_manager.h b/Telegram/SourceFiles/history/view/controls/history_view_compose_media_edit_manager.h index 8bc34b7c4..0ca33f854 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_media_edit_manager.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_media_edit_manager.h @@ -7,8 +7,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "base/unique_qptr.h" + +namespace SendMenu { +struct Details; +struct Action; +} // namespace SendMenu + namespace Ui { class RpWidget; +class PopupMenu; } // namespace Ui class Image; @@ -16,23 +24,43 @@ class HistoryItem; namespace HistoryView { -class MediaEditSpoilerManager final { +class MediaEditManager final { public: - MediaEditSpoilerManager(); + MediaEditManager(); + + void start( + not_null item, + std::optional spoilered = {}, + std::optional invertCaption = {}); + void apply(SendMenu::Action action); + void cancel(); void showMenu( not_null parent, - not_null item, - Fn callback); + Fn finished, + bool hasCaptionText); - [[nodiscard]] Image *mediaPreview(not_null item); + [[nodiscard]] Image *mediaPreview(); - void setSpoilerOverride(std::optional spoilerOverride); + [[nodiscard]] bool spoilered() const; + [[nodiscard]] bool invertCaption() const; - std::optional spoilerOverride() const; + [[nodiscard]] SendMenu::Details sendMenuDetails( + bool hasCaptionText) const; + + [[nodiscard]] explicit operator bool() const { + return _item != nullptr; + } + + [[nodiscard]] static bool CanBeSpoilered(not_null item); private: - std::optional _spoilerOverride; + base::unique_qptr _menu; + HistoryItem *_item = nullptr; + bool _spoilered = false; + bool _invertCaption = false; + + rpl::lifetime _lifetime; }; diff --git a/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp b/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp index 973ec463f..8773c866f 100644 --- a/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp +++ b/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp @@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history.h" #include "history/view/history_view_message.h" #include "history/view/history_view_cursor_state.h" +#include "chat_helpers/emoji_interactions.h" #include "core/click_handler_types.h" #include "main/main_session.h" #include "lottie/lottie_icon.h" @@ -35,14 +36,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace HistoryView { -struct BottomInfo::Reaction { +struct BottomInfo::Effect { mutable std::unique_ptr animation; mutable QImage image; - ReactionId id; - QString countText; - int count = 0; - int countTextWidth = 0; - bool chosen = false; + EffectId id = 0; }; BottomInfo::BottomInfo( @@ -63,17 +60,11 @@ void BottomInfo::update(Data &&data, int availableWidth) { } } -int BottomInfo::countReactionsMaxWidth() const { +int BottomInfo::countEffectMaxWidth() const { auto result = 0; - for (const auto &reaction : _reactions) { + if (_effect) { result += st::reactionInfoSize; - if (reaction.countTextWidth > 0) { - result += st::reactionInfoSkip - + reaction.countTextWidth - + st::reactionInfoDigitSkip; - } else { - result += st::reactionInfoBetween; - } + result += st::reactionInfoBetween; } if (result) { result += (st::reactionInfoSkip - st::reactionInfoBetween); @@ -81,19 +72,14 @@ int BottomInfo::countReactionsMaxWidth() const { return result; } -int BottomInfo::countReactionsHeight(int newWidth) const { +int BottomInfo::countEffectHeight(int newWidth) const { const auto left = 0; auto x = 0; auto y = 0; auto widthLeft = newWidth; - for (const auto &reaction : _reactions) { - const auto add = (reaction.countTextWidth > 0) - ? st::reactionInfoDigitSkip - : st::reactionInfoBetween; - const auto width = st::reactionInfoSize - + (reaction.countTextWidth > 0 - ? (st::reactionInfoSkip + reaction.countTextWidth) - : 0); + if (_effect) { + const auto add = st::reactionInfoBetween; + const auto width = st::reactionInfoSize; if (x > left && widthLeft < width) { x = left; y += st::msgDateFont->height; @@ -112,7 +98,7 @@ int BottomInfo::firstLineWidth() const { if (height() == minHeight()) { return width(); } - return maxWidth() - _reactionsMaxWidth; + return maxWidth() - _effectMaxWidth; } bool BottomInfo::isWide() const { @@ -120,14 +106,15 @@ bool BottomInfo::isWide() const { || !_data.author.isEmpty() || !_views.isEmpty() || !_replies.isEmpty() - || !_reactions.empty(); + || _effect; } TextState BottomInfo::textState( - not_null item, + not_null view, QPoint position) const { + const auto item = view->data(); auto result = TextState(item); - if (const auto link = revokeReactionLink(item, position)) { + if (const auto link = replayEffectLink(view, position)) { result.link = link; return result; } @@ -177,72 +164,44 @@ TextState BottomInfo::textState( return result; } -ClickHandlerPtr BottomInfo::revokeReactionLink( - not_null item, +ClickHandlerPtr BottomInfo::replayEffectLink( + not_null view, QPoint position) const { - if (_reactions.empty()) { + if (!_effect) { return nullptr; } auto left = 0; auto top = 0; auto available = width(); if (height() != minHeight()) { - available = std::min(available, _reactionsMaxWidth); + available = std::min(available, _effectMaxWidth); left += width() - available; top += st::msgDateFont->height; } - auto x = left; - auto y = top; - auto widthLeft = available; - for (const auto &reaction : _reactions) { - const auto chosen = reaction.chosen; - const auto add = (reaction.countTextWidth > 0) - ? st::reactionInfoDigitSkip - : st::reactionInfoBetween; - const auto width = st::reactionInfoSize - + (reaction.countTextWidth > 0 - ? (st::reactionInfoSkip + reaction.countTextWidth) - : 0); - if (x > left && widthLeft < width) { - x = left; - y += st::msgDateFont->height; - widthLeft = available; - } + if (_effect) { const auto image = QRect( - x, - y, + left, + top, st::reactionInfoSize, st::msgDateFont->height); - if (chosen && image.contains(position)) { - if (!_revokeLink) { - _revokeLink = revokeReactionLink(item); + if (image.contains(position)) { + if (!_replayLink) { + _replayLink = replayEffectLink(view); } - return _revokeLink; + return _replayLink; } - x += width + add; - widthLeft -= width + add; } return nullptr; } -ClickHandlerPtr BottomInfo::revokeReactionLink( - not_null item) const { - const auto itemId = item->fullId(); - const auto sessionId = item->history()->session().uniqueId(); - return std::make_shared([=]( - ClickContext context) { +ClickHandlerPtr BottomInfo::replayEffectLink( + not_null view) const { + const auto weak = base::make_weak(view); + return std::make_shared([=](ClickContext context) { const auto my = context.other.value(); if (const auto controller = my.sessionWindow.get()) { - if (controller->session().uniqueId() == sessionId) { - auto &owner = controller->session().data(); - if (const auto item = owner.message(itemId)) { - const auto chosen = item->chosenReactions(); - if (!chosen.empty()) { - item->toggleReaction( - chosen.front(), - HistoryItem::ReactionSource::Existing); - } - } + if (const auto strong = weak.get()) { + strong->delegate()->elementStartEffect(strong, nullptr); } } }); @@ -345,20 +304,20 @@ void BottomInfo::paint( firstLineBottom + st::historyViewsTop, outerWidth); } - if (!_reactions.empty()) { + if (_effect) { auto left = position.x(); auto top = position.y(); auto available = width(); if (height() != minHeight()) { - available = std::min(available, _reactionsMaxWidth); + available = std::min(available, _effectMaxWidth); left += width() - available; top += st::msgDateFont->height; } - paintReactions(p, position, left, top, available, context); + paintEffect(p, position, left, top, available, context); } } -void BottomInfo::paintReactions( +void BottomInfo::paintEffect( Painter &p, QPoint origin, int left, @@ -374,52 +333,33 @@ void BottomInfo::paintReactions( auto x = left; auto y = top; auto widthLeft = availableWidth; - for (const auto &reaction : _reactions) { - if (context.reactionInfo - && reaction.animation - && reaction.animation->finished()) { - reaction.animation = nullptr; - } - const auto animating = (reaction.animation != nullptr); - const auto add = (reaction.countTextWidth > 0) - ? st::reactionInfoDigitSkip - : st::reactionInfoBetween; - const auto width = st::reactionInfoSize - + (reaction.countTextWidth > 0 - ? (st::reactionInfoSkip + reaction.countTextWidth) - : 0); + if (_effect) { + const auto animating = (_effect->animation != nullptr); + const auto add = st::reactionInfoBetween; + const auto width = st::reactionInfoSize; if (x > left && widthLeft < width) { x = left; y += st::msgDateFont->height; widthLeft = availableWidth; } - if (reaction.image.isNull()) { - reaction.image = _reactionsOwner->resolveImageFor( - reaction.id, - ::Data::Reactions::ImageSize::BottomInfo); + if (_effect->image.isNull()) { + _effect->image = _reactionsOwner->resolveEffectImageFor( + _effect->id); } const auto image = QRect( - x + (st::reactionInfoSize - st::reactionInfoImage) / 2, - y + (st::msgDateFont->height - st::reactionInfoImage) / 2, - st::reactionInfoImage, - st::reactionInfoImage); - const auto skipImage = animating - && (reaction.count < 2 || !reaction.animation->flying()); - if (!reaction.image.isNull() && !skipImage) { - p.drawImage(image.topLeft(), reaction.image); + x + (st::reactionInfoSize - st::effectInfoImage) / 2, + y + (st::msgDateFont->height - st::effectInfoImage) / 2, + st::effectInfoImage, + st::effectInfoImage); + if (!_effect->image.isNull()) { + p.drawImage(image.topLeft(), _effect->image); } if (animating) { animations.push_back({ - .animation = reaction.animation.get(), + .animation = _effect->animation.get(), .target = image, }); } - if (reaction.countTextWidth > 0) { - p.drawText( - x + st::reactionInfoSize + st::reactionInfoSkip, - y + st::msgDateFont->ascent, - reaction.countText); - } x += width + add; widthLeft -= width + add; } @@ -453,18 +393,18 @@ QSize BottomInfo::countCurrentSize(int newWidth) { const auto dateHeight = (_data.flags & Data::Flag::Sponsored) ? 0 : st::msgDateFont->height; - const auto noReactionsWidth = maxWidth() - _reactionsMaxWidth; - accumulate_min(newWidth, std::max(noReactionsWidth, _reactionsMaxWidth)); + const auto noReactionsWidth = maxWidth() - _effectMaxWidth; + accumulate_min(newWidth, std::max(noReactionsWidth, _effectMaxWidth)); return QSize( newWidth, - dateHeight + countReactionsHeight(newWidth)); + dateHeight + countEffectHeight(newWidth)); } void BottomInfo::layout() { layoutDateText(); layoutViewsText(); layoutRepliesText(); - layoutReactionsText(); + layoutEffectText(); initDimensions(); } @@ -530,33 +470,12 @@ void BottomInfo::layoutRepliesText() { Ui::NameTextOptions()); } -void BottomInfo::layoutReactionsText() { - if (_data.reactions.empty()) { - _reactions.clear(); +void BottomInfo::layoutEffectText() { + if (!_data.effectId) { + _effect = nullptr; return; } - auto sorted = ranges::views::all( - _data.reactions - ) | ranges::views::transform([](const MessageReaction &reaction) { - return not_null{ &reaction }; - }) | ranges::to_vector; - ranges::sort( - sorted, - std::greater<>(), - &MessageReaction::count); - - auto reactions = std::vector(); - reactions.reserve(sorted.size()); - for (const auto &reaction : sorted) { - const auto &id = reaction->id; - const auto i = ranges::find(_reactions, id, &Reaction::id); - reactions.push_back((i != end(_reactions)) - ? std::move(*i) - : prepareReactionWithId(id)); - reactions.back().chosen = reaction->my; - setReactionCount(reactions.back(), reaction->count); - } - _reactions = std::move(reactions); + _effect = std::make_unique(prepareEffectWithId(_data.effectId)); } QSize BottomInfo::countOptimalSize() { @@ -581,81 +500,71 @@ QSize BottomInfo::countOptimalSize() { if (_data.flags & Data::Flag::Pinned) { width += st::historyPinWidth; } - _reactionsMaxWidth = countReactionsMaxWidth(); - width += _reactionsMaxWidth; + _effectMaxWidth = countEffectMaxWidth(); + width += _effectMaxWidth; const auto dateHeight = (_data.flags & Data::Flag::Sponsored) ? 0 : st::msgDateFont->height; return QSize(width, dateHeight); } -BottomInfo::Reaction BottomInfo::prepareReactionWithId( - const ReactionId &id) { - auto result = Reaction{ .id = id }; - _reactionsOwner->preloadImageFor(id); +BottomInfo::Effect BottomInfo::prepareEffectWithId(EffectId id) { + auto result = Effect{ .id = id }; + _reactionsOwner->preloadEffectImageFor(id); return result; } -void BottomInfo::setReactionCount(Reaction &reaction, int count) { - if (reaction.count == count) { - return; - } - reaction.count = count; - reaction.countText = (count > 1) - ? Lang::FormatCountToShort(count).string - : QString(); - reaction.countTextWidth = (count > 1) - ? st::msgDateFont->width(reaction.countText) - : 0; -} - -void BottomInfo::animateReaction( - Ui::ReactionFlyAnimationArgs &&args, +void BottomInfo::animateEffect( + Ui::ReactionFlyAnimationArgs &&args, Fn repaint) { - const auto i = ranges::find(_reactions, args.id, &Reaction::id); - if (i == end(_reactions)) { + if (!_effect || args.id.custom() != _effect->id) { return; } - i->animation = std::make_unique( + _effect->animation = std::make_unique( _reactionsOwner, args.translated(QPoint(width(), height())), std::move(repaint), - st::reactionInfoImage); + st::effectInfoImage); } -auto BottomInfo::takeReactionAnimations() --> base::flat_map> { - auto result = base::flat_map< - ReactionId, - std::unique_ptr>(); - for (auto &reaction : _reactions) { - if (reaction.animation) { - result.emplace(reaction.id, std::move(reaction.animation)); - } - } - return result; +auto BottomInfo::takeEffectAnimation() +-> std::unique_ptr { + return _effect ? std::move(_effect->animation) : nullptr; } -void BottomInfo::continueReactionAnimations(base::flat_map< - ReactionId, - std::unique_ptr> animations) { - for (auto &[id, animation] : animations) { - const auto i = ranges::find(_reactions, id, &Reaction::id); - if (i != end(_reactions)) { - i->animation = std::move(animation); - } +void BottomInfo::continueEffectAnimation( + std::unique_ptr animation) { + if (_effect) { + _effect->animation = std::move(animation); } } +QRect BottomInfo::effectIconGeometry() const { + if (!_effect) { + return {}; + } + auto left = 0; + auto top = 0; + auto available = width(); + if (height() != minHeight()) { + available = std::min(available, _effectMaxWidth); + left += width() - available; + top += st::msgDateFont->height; + } + return QRect( + left + (st::reactionInfoSize - st::effectInfoImage) / 2, + top + (st::msgDateFont->height - st::effectInfoImage) / 2, + st::effectInfoImage, + st::effectInfoImage); +} + BottomInfo::Data BottomInfoDataFromMessage(not_null message) { using Flag = BottomInfo::Data::Flag; const auto item = message->data(); auto result = BottomInfo::Data(); result.date = message->dateTime(); - if (message->embedReactionsInBottomInfo()) { - result.reactions = item->reactions(); - } + result.effectId = item->effectId(); if (message->hasOutLayout()) { result.flags |= Flag::OutLayout; } diff --git a/Telegram/SourceFiles/history/view/history_view_bottom_info.h b/Telegram/SourceFiles/history/view/history_view_bottom_info.h index efdab3334..594a488f1 100644 --- a/Telegram/SourceFiles/history/view/history_view_bottom_info.h +++ b/Telegram/SourceFiles/history/view/history_view_bottom_info.h @@ -20,8 +20,6 @@ class ReactionFlyAnimation; namespace Data { class Reactions; -struct ReactionId; -struct MessageReaction; } // namespace Data namespace HistoryView { @@ -33,8 +31,6 @@ struct TextState; class BottomInfo final : public Object { public: - using ReactionId = ::Data::ReactionId; - using MessageReaction = ::Data::MessageReaction; struct Data { enum class Flag : uchar { Edited = 0x01, @@ -52,7 +48,7 @@ public: QDateTime date; QString author; - std::vector reactions; + EffectId effectId = 0; std::optional views; std::optional replies; std::optional forwardsCount; @@ -66,7 +62,7 @@ public: [[nodiscard]] int firstLineWidth() const; [[nodiscard]] bool isWide() const; [[nodiscard]] TextState textState( - not_null item, + not_null view, QPoint position) const; [[nodiscard]] bool isSignedAuthorElided() const; @@ -78,29 +74,28 @@ public: bool inverted, const PaintContext &context) const; - void animateReaction( + void animateEffect( Ui::ReactionFlyAnimationArgs &&args, Fn repaint); - [[nodiscard]] auto takeReactionAnimations() - -> base::flat_map< - ReactionId, - std::unique_ptr>; - void continueReactionAnimations(base::flat_map< - ReactionId, - std::unique_ptr> animations); + [[nodiscard]] auto takeEffectAnimation() + -> std::unique_ptr; + void continueEffectAnimation( + std::unique_ptr animation); + + QRect effectIconGeometry() const; private: - struct Reaction; + struct Effect; void layout(); void layoutDateText(); void layoutViewsText(); void layoutRepliesText(); - void layoutReactionsText(); + void layoutEffectText(); - [[nodiscard]] int countReactionsMaxWidth() const; - [[nodiscard]] int countReactionsHeight(int newWidth) const; - void paintReactions( + [[nodiscard]] int countEffectMaxWidth() const; + [[nodiscard]] int countEffectHeight(int newWidth) const; + void paintEffect( Painter &p, QPoint origin, int left, @@ -111,23 +106,21 @@ private: QSize countOptimalSize() override; QSize countCurrentSize(int newWidth) override; - void setReactionCount(Reaction &reaction, int count); - [[nodiscard]] Reaction prepareReactionWithId( - const ReactionId &id); - [[nodiscard]] ClickHandlerPtr revokeReactionLink( - not_null item, + [[nodiscard]] Effect prepareEffectWithId(EffectId id); + [[nodiscard]] ClickHandlerPtr replayEffectLink( + not_null view, QPoint position) const; - [[nodiscard]] ClickHandlerPtr revokeReactionLink( - not_null item) const; + [[nodiscard]] ClickHandlerPtr replayEffectLink( + not_null view) const; const not_null<::Data::Reactions*> _reactionsOwner; Data _data; Ui::Text::String _authorEditedDate; Ui::Text::String _views; Ui::Text::String _replies; - std::vector _reactions; - mutable ClickHandlerPtr _revokeLink; - int _reactionsMaxWidth = 0; + std::unique_ptr _effect; + mutable ClickHandlerPtr _replayLink; + int _effectMaxWidth = 0; bool _authorElided = false; }; diff --git a/Telegram/SourceFiles/history/view/history_view_chat_preview.cpp b/Telegram/SourceFiles/history/view/history_view_chat_preview.cpp new file mode 100644 index 000000000..91540a729 --- /dev/null +++ b/Telegram/SourceFiles/history/view/history_view_chat_preview.cpp @@ -0,0 +1,805 @@ +/* +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_chat_preview.h" + +#include "base/unixtime.h" +#include "data/data_changes.h" +#include "data/data_channel.h" +#include "data/data_chat.h" +#include "data/data_forum_topic.h" +#include "data/data_history_messages.h" +#include "data/data_peer.h" +#include "data/data_peer_values.h" +#include "data/data_replies_list.h" +#include "data/data_session.h" +#include "data/data_thread.h" +#include "history/view/reactions/history_view_reactions_button.h" +#include "history/view/history_view_list_widget.h" +#include "history/history.h" +#include "history/history_item.h" +#include "history/history_item_components.h" +#include "info/profile/info_profile_cover.h" +#include "info/profile/info_profile_values.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "ui/chat/chat_style.h" +#include "ui/chat/chat_theme.h" +#include "ui/controls/userpic_button.h" +#include "ui/widgets/menu/menu_item_base.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/elastic_scroll.h" +#include "ui/widgets/labels.h" +#include "ui/widgets/popup_menu.h" +#include "ui/widgets/shadow.h" +#include "window/themes/window_theme.h" +#include "window/section_widget.h" +#include "window/window_session_controller.h" +#include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" + +namespace HistoryView { +namespace { + +class Item final + : public Ui::Menu::ItemBase + , private HistoryView::ListDelegate { +public: + Item(not_null parent, not_null thread); + + [[nodiscard]] not_null action() const override; + [[nodiscard]] bool isEnabled() const override; + + [[nodiscard]] rpl::producer actions() { + return _actions.events(); + } + +private: + int contentHeight() const override; + void paintEvent(QPaintEvent *e) override; + + void setupTop(); + void setupMarkRead(); + void setupBackground(); + void setupHistory(); + void updateInnerVisibleArea(); + + 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 listSearch( + const QString &query, + const FullMsgId &context) override; + void listHandleViaClick(not_null bot) override; + not_null listChatTheme() override; + CopyRestrictionType listCopyRestrictionType( + HistoryItem *item) override; + CopyRestrictionType listCopyRestrictionType() { + return listCopyRestrictionType(nullptr); + } + 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; + not_null listWindow() override; + not_null listEmojiInteractionsParent() override; + not_null listChatStyle() override; + rpl::producer listChatWideValue() override; + std::unique_ptr listMakeReactionsManager( + QWidget *wheelEventsTarget, + Fn update) override; + void listVisibleAreaUpdated() override; + std::shared_ptr listUiShow() override; + void listShowPollResults( + not_null poll, + FullMsgId context) override; + void listCancelUploadLayer(not_null item) override; + bool listAnimationsPaused() override; + auto listSendingAnimation() + -> Ui::MessageSendingAnimationController* override; + Ui::ChatPaintContext listPreparePaintContext( + Ui::ChatPaintContextArgs &&args) override; + bool listMarkingContentRead() override; + bool listIgnorePaintEvent(QWidget *w, QPaintEvent *e) override; + bool listShowReactPremiumError( + not_null item, + const Data::ReactionId &id) override; + void listWindowSetInnerFocus() override; + bool listAllowsDragForward() override; + void listLaunchDrag( + std::unique_ptr data, + Fn finished) override; + + const not_null _dummyAction; + const not_null _session; + const not_null _thread; + const std::shared_ptr _replies; + const not_null _history; + const not_null _peer; + const std::shared_ptr _theme; + const std::unique_ptr _chatStyle; + const std::unique_ptr _top; + const std::unique_ptr _scroll; + const std::unique_ptr _markRead; + + QPointer _inner; + rpl::event_stream _actions; + + QImage _bg; + +}; + +struct StatusFields { + QString text; + bool active = false; +}; + +[[nodiscard]] rpl::producer StatusValue( + not_null peer) { + peer->updateFull(); + + using UpdateFlag = Data::PeerUpdate::Flag; + return peer->session().changes().peerFlagsValue( + peer, + UpdateFlag::OnlineStatus | UpdateFlag::Members + ) | rpl::map([=](const Data::PeerUpdate &update) + -> StatusFields { + const auto wrap = [](QString text) { + return StatusFields{ .text = text }; + }; + if (const auto user = peer->asUser()) { + const auto now = base::unixtime::now(); + return { + .text = Data::OnlineText(user, now), + .active = Data::OnlineTextActive(user, now), + }; + } else if (const auto chat = peer->asChat()) { + return wrap(!chat->amIn() + ? tr::lng_chat_status_unaccessible(tr::now) + : (chat->count <= 0) + ? tr::lng_group_status(tr::now) + : tr::lng_chat_status_members( + tr::now, + lt_count_decimal, + chat->count)); + } else if (const auto channel = peer->asChannel()) { + return wrap((channel->membersCount() > 0) + ? ((channel->isMegagroup() + ? tr::lng_chat_status_members + : tr::lng_chat_status_subscribers)( + tr::now, + lt_count_decimal, + channel->membersCount())) + : (channel->isMegagroup() + ? tr::lng_group_status(tr::now) + : tr::lng_channel_status(tr::now))); + } + Unexpected("Peer type in ChatPreview Item."); + }); + +} + +Item::Item(not_null parent, not_null thread) +: Ui::Menu::ItemBase(parent, st::previewMenu.menu) +, _dummyAction(new QAction(parent)) +, _session(&thread->session()) +, _thread(thread) +, _replies(thread->asTopic() ? thread->asTopic()->replies() : nullptr) +, _history(thread->owningHistory()) +, _peer(thread->peer()) +, _theme(Window::Theme::DefaultChatThemeOn(lifetime())) +, _chatStyle(std::make_unique(_session->colorIndicesValue())) +, _top(std::make_unique(this)) +, _scroll(std::make_unique(this)) +, _markRead( + std::make_unique( + this, + tr::lng_context_mark_read(tr::now), + st::previewMarkRead)) { + _chatStyle->apply(_theme.get()); + setPointerCursor(false); + setMinWidth(st::previewMenu.menu.widthMin); + resize(minWidth(), contentHeight()); + setupTop(); + setupMarkRead(); + setupBackground(); + setupHistory(); +} + +not_null Item::action() const { + return _dummyAction; +} + +bool Item::isEnabled() const { + return false; +} + +int Item::contentHeight() const { + return st::previewMenu.maxHeight; +} + +void Item::setupTop() { + _top->setGeometry(0, 0, width(), st::previewTop.height); + _top->setClickedCallback([=] { + _actions.fire({ .openInfo = true }); + }); + _top->paintRequest() | rpl::start_with_next([=](QRect clip) { + auto p = QPainter(_top.get()); + p.fillRect(clip, st::topBarBg); + }, _top->lifetime()); + + const auto topic = _thread->asTopic(); + auto nameValue = (topic + ? Info::Profile::TitleValue(topic) + : Info::Profile::NameValue(_thread->peer()) + ) | rpl::start_spawning(_top->lifetime()); + const auto name = Ui::CreateChild( + _top.get(), + rpl::duplicate(nameValue), + st::previewName); + name->setAttribute(Qt::WA_TransparentForMouseEvents); + auto statusFields = StatusValue( + _thread->peer() + ) | rpl::start_spawning(lifetime()); + auto statusText = rpl::duplicate( + statusFields + ) | rpl::map([](StatusFields &&fields) { + return fields.text; + }); + const auto status = Ui::CreateChild( + _top.get(), + (topic + ? Info::Profile::NameValue(topic->channel()) + : std::move(statusText)), + st::previewStatus); + std::move(statusFields) | rpl::start_with_next([=](const StatusFields &fields) { + status->setTextColorOverride(fields.active + ? st::windowActiveTextFg->c + : std::optional()); + }, status->lifetime()); + status->setAttribute(Qt::WA_TransparentForMouseEvents); + const auto userpic = topic + ? nullptr + : Ui::CreateChild( + _top.get(), + _thread->peer(), + st::previewUserpic); + if (userpic) { + userpic->setAttribute(Qt::WA_TransparentForMouseEvents); + } + const auto icon = topic + ? Ui::CreateChild( + this, + topic, + [=] { return false; }) + : nullptr; + if (icon) { + icon->setAttribute(Qt::WA_TransparentForMouseEvents); + } + + const auto shadow = Ui::CreateChild(this); + rpl::combine( + _top->widthValue(), + std::move(nameValue) + ) | rpl::start_with_next([=](int width, const auto &) { + const auto &st = st::previewTop; + name->resizeToNaturalWidth(width + - st.namePosition.x() + - st.photoPosition.x()); + name->move(st::previewTop.namePosition); + }, name->lifetime()); + + _top->geometryValue() | rpl::start_with_next([=](QRect geometry) { + const auto &st = st::previewTop; + status->resizeToWidth(geometry.width() + - st.statusPosition.x() + - st.photoPosition.x()); + status->move(st.statusPosition); + shadow->setGeometry( + geometry.x(), + geometry.y() + geometry.height(), + geometry.width(), + st::lineWidth); + if (userpic) { + userpic->move(st.photoPosition); + } else { + icon->move( + st.photoPosition.x() + (st.photoSize - icon->width()) / 2, + st.photoPosition.y() + (st.photoSize - icon->height()) / 2); + } + }, shadow->lifetime()); +} + +void Item::setupMarkRead() { + _markRead->resizeToWidth(width()); + _markRead->move(0, height() - _markRead->height()); + + rpl::single( + rpl::empty + ) | rpl::then( + _thread->owner().chatsListFor(_thread)->unreadStateChanges( + ) | rpl::to_empty + ) | rpl::start_with_next([=] { + const auto state = _thread->chatListBadgesState(); + const auto unread = (state.unreadCounter || state.unread); + const auto hidden = _thread->asTopic() + ? (!unread) + : _thread->peer()->isForum(); + if (hidden) { + _markRead->hide(); + return; + } + _markRead->setText(unread + ? tr::lng_context_mark_read(tr::now) + : tr::lng_context_mark_unread(tr::now)); + _markRead->setClickedCallback([=] { + _actions.fire({ .markRead = unread, .markUnread = !unread }); + }); + _markRead->show(); + }, _markRead->lifetime()); + + const auto shadow = Ui::CreateChild(this); + _markRead->geometryValue() | rpl::start_with_next([=](QRect geometry) { + shadow->setGeometry( + geometry.x(), + geometry.y() - st::lineWidth, + geometry.width(), + st::lineWidth); + }, shadow->lifetime()); + shadow->showOn(_markRead->shownValue()); +} + +void Item::setupBackground() { + const auto ratio = style::DevicePixelRatio(); + _bg = QImage( + size() * ratio, + QImage::Format_ARGB32_Premultiplied); + + const auto paint = [=] { + auto p = QPainter(&_bg); + Window::SectionWidget::PaintBackground( + p, + _theme.get(), + QSize(width(), height() * 2), + QRect(QPoint(), size())); + }; + paint(); + _theme->repaintBackgroundRequests() | rpl::start_with_next([=] { + paint(); + update(); + }, lifetime()); +} + +void Item::setupHistory() { + _inner = _scroll->setOwnedWidget(object_ptr( + this, + _session, + static_cast(this))); + + _markRead->shownValue() | rpl::start_with_next([=](bool shown) { + const auto top = _top->height(); + const auto bottom = shown ? _markRead->height() : 0; + _scroll->setGeometry(rect().marginsRemoved({ 0, top, 0, bottom })); + }, _markRead->lifetime()); + + _scroll->scrolls( + ) | rpl::start_with_next([=] { + updateInnerVisibleArea(); + }, lifetime()); + _scroll->setOverscrollBg(QColor(0, 0, 0, 0)); + using Type = Ui::ElasticScroll::OverscrollType; + _scroll->setOverscrollTypes(Type::Real, Type::Real); + + _scroll->events() | rpl::start_with_next([=](not_null e) { + if (e->type() == QEvent::MouseButtonDblClick) { + const auto button = static_cast(e.get())->button(); + if (button == Qt::LeftButton) { + const auto relative = Ui::MapFrom( + _inner.data(), + _scroll.get(), + static_cast(e.get())->pos()); + if (const auto view = _inner->lookupItemByY(relative.y())) { + _actions.fire(ChatPreviewAction{ + .openItemId = view->data()->fullId(), + }); + } else { + _actions.fire(ChatPreviewAction{}); + } + } + } + }, lifetime()); + + _inner->resizeToWidth(_scroll->width(), _scroll->height()); + + _inner->refreshViewer(); + + _inner->setAttribute(Qt::WA_TransparentForMouseEvents); +} + +void Item::paintEvent(QPaintEvent *e) { + auto p = QPainter(this); + p.drawImage(0, 0, _bg); +} + +void Item::updateInnerVisibleArea() { + const auto scrollTop = _scroll->scrollTop(); + _inner->setVisibleTopBottom(scrollTop, scrollTop + _scroll->height()); +} + +Context Item::listContext() { + return Context::ChatPreview; +} + +bool Item::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 Item::listCancelRequest() { +} + +void Item::listDeleteRequest() { +} + +void Item::listTryProcessKeyInput(not_null e) { +} + +rpl::producer Item::listSource( + Data::MessagePosition aroundId, + int limitBefore, + int limitAfter) { + return _replies + ? _replies->source(aroundId, limitBefore, limitAfter) + : Data::HistoryMessagesViewer( + _thread->asHistory(), + aroundId, + limitBefore, + limitAfter); +} + +bool Item::listAllowsMultiSelect() { + return false; +} + +bool Item::listIsItemGoodForSelection(not_null item) { + return false; +} + +bool Item::listIsLessInOrder( + not_null first, + not_null second) { + if (first->isRegular() && second->isRegular()) { + const auto firstPeer = first->history()->peer; + const auto secondPeer = second->history()->peer; + if (firstPeer == secondPeer) { + return first->id < second->id; + } else if (firstPeer->isChat()) { + return true; + } + return false; + } else if (first->isRegular()) { + return true; + } else if (second->isRegular()) { + return false; + } + return first->id < second->id; +} + +void Item::listSelectionChanged(SelectedItems &&items) { +} + +void Item::listMarkReadTill(not_null item) { +} + +void Item::listMarkContentsRead( + const base::flat_set> &items) { +} + +MessagesBarData Item::listMessagesBar( + const std::vector> &elements) { + if (elements.empty()) { + return {}; + } else if (!_replies && !_history->unreadCount()) { + return {}; + } + const auto repliesTill = _replies + ? _replies->computeInboxReadTillFull() + : MsgId(); + const auto migrated = _replies ? nullptr : _history->migrateFrom(); + const auto migratedTill = migrated ? migrated->inboxReadTillId() : 0; + const auto historyTill = _replies ? 0 : _history->inboxReadTillId(); + if (!_replies && !migratedTill && !historyTill) { + return {}; + } + + const auto hidden = _replies && (repliesTill < 2); + for (auto i = 0, count = int(elements.size()); i != count; ++i) { + const auto item = elements[i]->data(); + if (!item->isRegular() + || item->out() + || (_replies && !item->replyToId())) { + continue; + } + const auto inHistory = (item->history() == _history); + if ((_replies && item->id > repliesTill) + || (migratedTill && (inHistory || item->id > migratedTill)) + || (historyTill && inHistory && item->id > historyTill)) { + return { + .bar = { + .element = elements[i], + .hidden = hidden, + .focus = true, + }, + .text = tr::lng_unread_bar_some(), + }; + } + } + return {}; +} + +void Item::listContentRefreshed() { +} + +void Item::listUpdateDateLink( + ClickHandlerPtr &link, + not_null view) { +} + +bool Item::listElementHideReply(not_null view) { + if (!view->isTopicRootReply()) { + return false; + } + const auto reply = view->data()->Get(); + return reply && !reply->fields().manualQuote; +} + +bool Item::listElementShownUnread(not_null view) { + return view->data()->unread(view->data()->history()); +} + +bool Item::listIsGoodForAroundPosition(not_null view) { + return view->data()->isRegular(); +} + +void Item::listSendBotCommand( + const QString &command, + const FullMsgId &context) { +} + +void Item::listSearch( + const QString &query, + const FullMsgId &context) { +} + +void Item::listHandleViaClick(not_null bot) { +} + +not_null Item::listChatTheme() { + return _theme.get(); +} + +CopyRestrictionType Item::listCopyRestrictionType(HistoryItem *item) { + return CopyRestrictionType::None; +} + +CopyRestrictionType Item::listCopyMediaRestrictionType( + not_null item) { + return CopyRestrictionType::None; +} + +CopyRestrictionType Item::listSelectRestrictionType() { + return CopyRestrictionType::None; +} + +auto Item::listAllowedReactionsValue() +-> rpl::producer { + return rpl::single(Data::AllowedReactions()); +} + +void Item::listShowPremiumToast(not_null document) { +} + +void Item::listOpenPhoto( + not_null photo, + FullMsgId context) { +} + +void Item::listOpenDocument( + not_null document, + FullMsgId context, + bool showInMediaView) { +} + +void Item::listPaintEmpty( + Painter &p, + const Ui::ChatPaintContext &context) { + // #TODO +} + +QString Item::listElementAuthorRank(not_null view) { + return {}; +} + +History *Item::listTranslateHistory() { + return nullptr; +} + +void Item::listAddTranslatedItems( + not_null tracker) { +} + +not_null Item::listWindow() { + Unexpected("Item::listWindow."); +} + +not_null Item::listEmojiInteractionsParent() { + return this; +} + +not_null Item::listChatStyle() { + return _chatStyle.get(); +} + +rpl::producer Item::listChatWideValue() { + return rpl::single(false); +} + +std::unique_ptr Item::listMakeReactionsManager( + QWidget *wheelEventsTarget, + Fn update) { + return nullptr; +} + +void Item::listVisibleAreaUpdated() { +} + +std::shared_ptr Item::listUiShow() { + Unexpected("Item::listUiShow."); +} + +void Item::listShowPollResults( + not_null poll, + FullMsgId context) { +} + +void Item::listCancelUploadLayer(not_null item) { +} + +bool Item::listAnimationsPaused() { + return false; +} + +auto Item::listSendingAnimation() +-> Ui::MessageSendingAnimationController* { + return nullptr; +} + +Ui::ChatPaintContext Item::listPreparePaintContext( + Ui::ChatPaintContextArgs &&args) { + const auto visibleAreaTopLocal = mapFromGlobal( + args.visibleAreaPositionGlobal).y(); + const auto viewport = QRect( + 0, + args.visibleAreaTop - visibleAreaTopLocal, + args.visibleAreaWidth, + height()); + return args.theme->preparePaintContext( + _chatStyle.get(), + viewport, + args.clip, + false); +} + +bool Item::listMarkingContentRead() { + return false; +} + +bool Item::listIgnorePaintEvent(QWidget *w, QPaintEvent *e) { + return false; +} + +bool Item::listShowReactPremiumError( + not_null item, + const Data::ReactionId &id) { + return false; +} + +void Item::listWindowSetInnerFocus() { +} + +bool Item::listAllowsDragForward() { + return false; +} + +void Item::listLaunchDrag( + std::unique_ptr data, + Fn finished) { +} + +} // namespace + +ChatPreview MakeChatPreview( + QWidget *parent, + not_null entry) { + const auto thread = entry->asThread(); + if (!thread) { + return {}; + } + + auto result = ChatPreview{ + .menu = base::make_unique_q( + parent, + st::previewMenu), + }; + const auto menu = result.menu.get(); + + auto action = base::make_unique_q(menu, thread); + result.actions = action->actions(); + menu->addAction(std::move(action)); + if (const auto topic = thread->asTopic()) { + const auto weak = Ui::MakeWeak(menu); + topic->destroyed() | rpl::start_with_next([weak] { + if (const auto strong = weak.data()) { + LOG(("Preview hidden for a destroyed topic.")); + strong->hideMenu(true); + } + }, menu->lifetime()); + } + + return result; +} + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_chat_preview.h b/Telegram/SourceFiles/history/view/history_view_chat_preview.h new file mode 100644 index 000000000..0598725c2 --- /dev/null +++ b/Telegram/SourceFiles/history/view/history_view_chat_preview.h @@ -0,0 +1,38 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "base/unique_qptr.h" + +namespace Dialogs { +class Entry; +} // namespace Dialogs + +namespace Ui { +class PopupMenu; +} // namespace Ui + +namespace HistoryView { + +struct ChatPreviewAction { + FullMsgId openItemId; + bool openInfo = false; + bool markRead = false; + bool markUnread = false; +}; + +struct ChatPreview { + base::unique_qptr menu; + rpl::producer actions; +}; + +[[nodiscard]] ChatPreview MakeChatPreview( + QWidget *parent, + not_null entry); + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index 5e3ec3022..1193b3ca9 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_transcribes.h" #include "api/api_who_reacted.h" #include "api/api_toggling_media.h" // Api::ToggleFavedSticker +#include "base/qt/qt_key_modifiers.h" #include "base/unixtime.h" #include "history/view/history_view_list_widget.h" #include "history/view/history_view_cursor_state.h" @@ -38,6 +39,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/text/text_utilities.h" #include "ui/controls/delete_message_context_action.h" #include "ui/controls/who_reacted_context_action.h" +#include "ui/boxes/edit_factcheck_box.h" #include "ui/boxes/report_box.h" #include "ui/ui_utility.h" #include "menu/menu_item_download_files.h" @@ -52,6 +54,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/sticker_set_box.h" #include "boxes/stickers_box.h" #include "boxes/translate_box.h" +#include "data/components/factchecks.h" #include "data/data_photo.h" #include "data/data_photo_media.h" #include "data/data_document.h" @@ -66,6 +69,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_file_origin.h" #include "data/data_message_reactions.h" #include "data/stickers/data_custom_emoji.h" +#include "chat_helpers/message_field.h" // FactcheckFieldIniter. #include "core/file_utilities.h" #include "core/click_handler_types.h" #include "base/platform/base_platform_info.h" @@ -593,8 +597,10 @@ bool AddRescheduleAction( const auto box = request.navigation->parentController()->show( HistoryView::PrepareScheduleBox( &request.navigation->session(), - sendMenuType, + request.navigation->uiShow(), + { .type = sendMenuType, .effectAllowed = false }, callback, + {}, // initial options date)); owner->itemRemoved( @@ -637,15 +643,11 @@ bool AddReplyToMessageAction( text.replace('&', u"&&"_q); const auto itemId = item->fullId(); menu->addAction(text, [=] { - if (!item) { - return; - } else { - list->replyToMessageRequestNotify({ - .messageId = itemId, - .quote = quote.text, - .quoteOffset = quote.offset, - }); - } + list->replyToMessageRequestNotify({ + .messageId = itemId, + .quote = quote.text, + .quoteOffset = quote.offset, + }, base::IsCtrlPressed()); }, &st::menuIconReply); return true; } @@ -719,6 +721,31 @@ bool AddEditMessageAction( return true; } +void AddFactcheckAction( + not_null menu, + const ContextMenuRequest &request, + not_null list) { + const auto item = request.item; + if (!item || !item->history()->session().factchecks().canEdit(item)) { + return; + } + const auto itemId = item->fullId(); + const auto text = item->factcheckText(); + const auto session = &item->history()->session(); + const auto phrase = text.empty() + ? tr::lng_context_add_factcheck(tr::now) + : tr::lng_context_edit_factcheck(tr::now); + menu->addAction(phrase, [=] { + const auto limit = session->factchecks().lengthLimit(); + const auto controller = request.navigation->parentController(); + controller->show(Box(EditFactcheckBox, text, limit, [=]( + TextWithEntities result) { + const auto show = controller->uiShow(); + session->factchecks().save(itemId, text, result, show); + }, FactcheckFieldIniter(controller->uiShow()))); + }, &st::menuIconFactcheck); +} + bool AddPinMessageAction( not_null menu, const ContextMenuRequest &request, @@ -978,6 +1005,7 @@ void AddTopMessageActions( AddGoToMessageAction(menu, request, list); AddViewRepliesAction(menu, request, list); AddEditMessageAction(menu, request, list); + AddFactcheckAction(menu, request, list); AddPinMessageAction(menu, request, list); } @@ -1057,7 +1085,7 @@ void EditTagBox( customId, [=] { field->update(); }); } else { - owner->reactions().preloadImageFor(id); + owner->reactions().preloadReactionImageFor(id); } field->paintRequest() | rpl::start_with_next([=](QRect clip) { auto p = QPainter(field); @@ -1072,9 +1100,8 @@ void EditTagBox( }); } else { if (state->image.isNull()) { - state->image = owner->reactions().resolveImageFor( - id, - ::Data::Reactions::ImageSize::InlineList); + state->image = owner->reactions().resolveReactionImageFor( + id); } if (!state->image.isNull()) { const auto size = st::reactionInlineSize; @@ -1084,7 +1111,7 @@ void EditTagBox( } }, field->lifetime()); - AddLengthLimitLabel(field, kTagNameLimit); + Ui::AddLengthLimitLabel(field, kTagNameLimit); const auto save = [=] { const auto text = field->getLastText(); @@ -1847,40 +1874,4 @@ bool ItemHasTtl(HistoryItem *item) { return false; // AyuGram: allow downloading files with ttl } -void AddLengthLimitLabel(not_null field, int limit) { - struct State { - rpl::variable length; - }; - const auto state = field->lifetime().make_state(); - state->length = rpl::single( - rpl::empty - ) | rpl::then(field->changes()) | rpl::map([=] { - return int(field->getLastText().size()); - }); - auto warningText = state->length.value() | rpl::map([=](int count) { - const auto threshold = std::min(limit / 2, 9); - const auto left = limit - count; - return (left < threshold) ? QString::number(left) : QString(); - }); - const auto warning = Ui::CreateChild( - field.get(), - std::move(warningText), - st::editTagLimit); - state->length.value() | rpl::map( - rpl::mappers::_1 > limit - ) | rpl::start_with_next([=](bool exceeded) { - warning->setTextColorOverride(exceeded - ? st::attentionButtonFg->c - : std::optional()); - }, warning->lifetime()); - rpl::combine( - field->sizeValue(), - warning->sizeValue() - ) | rpl::start_with_next([=] { - warning->moveToRight(0, 0); - }, warning->lifetime()); - warning->setAttribute(Qt::WA_TransparentForMouseEvents); - -} - } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.h b/Telegram/SourceFiles/history/view/history_view_context_menu.h index 8582a57fb..8f00f4da8 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.h +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.h @@ -123,6 +123,4 @@ void AddEmojiPacksAction( [[nodiscard]] bool ItemHasTtl(HistoryItem *item); -void AddLengthLimitLabel(not_null field, int limit); - } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index b41c35ffc..f58b631f3 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -18,7 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/reactions/history_view_reactions.h" #include "history/view/history_view_cursor_state.h" #include "history/view/history_view_reply.h" -#include "history/view/history_view_spoiler_click_handler.h" +#include "history/view/history_view_text_helper.h" #include "history/history.h" #include "history/history_item_components.h" #include "history/history_item_helpers.h" @@ -198,6 +198,11 @@ void DefaultElementDelegate::elementCancelPremium( not_null view) { } +void DefaultElementDelegate::elementStartEffect( + not_null view, + Element *replacing) { +} + QString DefaultElementDelegate::elementAuthorRank( not_null view) { return {}; @@ -789,6 +794,10 @@ void Element::refreshMedia(Element *replacing) { } } +HistoryItem *Element::textItem() const { + return _textItem; +} + Ui::Text::IsolatedEmoji Element::isolatedEmoji() const { return _text.toIsolatedEmoji(); } @@ -966,11 +975,11 @@ auto Element::contextDependentServiceText() -> TextWithLinks { void Element::validateText() { const auto item = data(); - const auto &text = item->_text; const auto media = item->media(); const auto storyMention = media && media->storyMention(); if (media && media->storyExpired()) { _media = nullptr; + _textItem = item; if (!storyMention) { if (_text.isEmpty()) { setTextWithLinks(Ui::Text::Italic( @@ -979,6 +988,16 @@ void Element::validateText() { return; } } + + // Albums may show text of a different item than the parent one. + _textItem = _media ? _media->itemForText() : item.get(); + if (!_textItem) { + if (!_text.isEmpty()) { + setTextWithLinks({}); + } + return; + } + const auto &text = _textItem->_text; if (_text.isEmpty() == text.empty()) { } else if (_flags & Flag::ServiceMessage) { const auto contextDependentText = contextDependentServiceText(); @@ -986,11 +1005,11 @@ void Element::validateText() { ? text : contextDependentText.text; const auto &customLinks = contextDependentText.text.empty() - ? item->customTextLinks() + ? _textItem->customTextLinks() : contextDependentText.links; setTextWithLinks(markedText, customLinks); } else { - setTextWithLinks(item->translatedTextWithLocalEntities()); + setTextWithLinks(_textItem->translatedTextWithLocalEntities()); } } @@ -1027,7 +1046,7 @@ void Element::setTextWithLinks( refreshMedia(nullptr); } } - FillTextWithAnimatedSpoilers(this, _text); + InitElementTextPart(this, _text); _textWidth = -1; _textHeight = 0; } @@ -1425,6 +1444,12 @@ bool Element::hasVisibleText() const { return false; } +int Element::textualMaxWidth() const { + return st::msgPadding.left() + + (hasVisibleText() ? text().maxWidth() : 0) + + st::msgPadding.right(); +} + auto Element::verticalRepaintRange() const -> VerticalRepaintRange { return { .top = 0, @@ -1462,6 +1487,12 @@ void Element::itemTextUpdated() { } } +void Element::blockquoteExpandChanged() { + _textWidth = -1; + _textHeight = 0; + history()->owner().requestViewResize(this); +} + void Element::unloadHeavyPart() { history()->owner().unregisterHeavyViewPart(this); if (_media) { @@ -1793,6 +1824,21 @@ auto Element::takeReactionAnimations() return {}; } +void Element::animateEffect(Ui::ReactionFlyAnimationArgs &&args) { +} + +void Element::animateUnreadEffect() { +} + +auto Element::takeEffectAnimation() +-> std::unique_ptr { + return nullptr; +} + +QRect Element::effectIconGeometry() const { + return QRect(); +} + Element::~Element() { // Delete media while owner still exists. clearSpecialOnlyEmoji(); diff --git a/Telegram/SourceFiles/history/view/history_view_element.h b/Telegram/SourceFiles/history/view/history_view_element.h index 6db33ff4b..0c01e9767 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.h +++ b/Telegram/SourceFiles/history/view/history_view_element.h @@ -61,6 +61,7 @@ enum class Context : char { TTLViewer, ShortcutMessages, ScheduledTopic, + ChatPreview, }; enum class OnlyEmojiAndSpaces : char { @@ -112,6 +113,9 @@ public: not_null view, Element *replacing) = 0; virtual void elementCancelPremium(not_null view) = 0; + virtual void elementStartEffect( + not_null view, + Element *replacing) = 0; virtual QString elementAuthorRank(not_null view) = 0; virtual ~ElementDelegate() { @@ -162,6 +166,9 @@ public: not_null view, Element *replacing) override; void elementCancelPremium(not_null view) override; + void elementStartEffect( + not_null view, + Element *replacing) override; QString elementAuthorRank(not_null view) override; }; @@ -366,6 +373,7 @@ public: && _text.isOnlyCustomEmoji(); } + [[nodiscard]] HistoryItem *textItem() const; [[nodiscard]] Ui::Text::IsolatedEmoji isolatedEmoji() const; [[nodiscard]] Ui::Text::OnlyCustomEmoji onlyCustomEmoji() const; @@ -469,6 +477,7 @@ public: std::optional pressPoint) const; [[nodiscard]] virtual TimeId displayedEditDate() const; [[nodiscard]] virtual bool hasVisibleText() const; + [[nodiscard]] int textualMaxWidth() const; virtual void applyGroupAdminChanges( const base::flat_set &changes) { } @@ -489,6 +498,7 @@ public: virtual void itemDataChanged(); void itemTextUpdated(); + void blockquoteExpandChanged(); [[nodiscard]] virtual bool hasHeavyPart() const; virtual void unloadHeavyPart(); @@ -516,6 +526,7 @@ public: void previousInBlocksChanged(); void nextInBlocksRemoved(); + [[nodiscard]] virtual QRect effectIconGeometry() const; [[nodiscard]] virtual QRect innerGeometry() const = 0; void customEmojiRepaint(); @@ -544,6 +555,11 @@ public: Data::ReactionId, std::unique_ptr>; + virtual void animateEffect(Ui::ReactionFlyAnimationArgs &&args); + void animateUnreadEffect(); + [[nodiscard]] virtual auto takeEffectAnimation() + -> std::unique_ptr; + void overrideMedia(std::unique_ptr media); virtual bool consumeHorizontalScroll(QPoint position, int delta) { @@ -620,6 +636,7 @@ private: mutable ClickHandlerPtr _fromLink; const QDateTime _dateTime; + HistoryItem *_textItem = nullptr; mutable Ui::Text::String _text; mutable int _textWidth = -1; mutable int _textHeight = 0; diff --git a/Telegram/SourceFiles/history/view/history_view_emoji_interactions.cpp b/Telegram/SourceFiles/history/view/history_view_emoji_interactions.cpp index c274af095..6da16cfb0 100644 --- a/Telegram/SourceFiles/history/view/history_view_emoji_interactions.cpp +++ b/Telegram/SourceFiles/history/view/history_view_emoji_interactions.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_element.h" #include "history/view/media/history_view_sticker.h" #include "history/history.h" +#include "history/history_item.h" #include "chat_helpers/stickers_emoji_pack.h" #include "chat_helpers/emoji_interactions.h" #include "chat_helpers/stickers_lottie.h" @@ -17,9 +18,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_session.h" #include "data/data_document.h" #include "data/data_document_media.h" +#include "data/data_message_reactions.h" #include "lottie/lottie_common.h" #include "lottie/lottie_single_player.h" #include "base/random.h" +#include "ui/power_saving.h" #include "styles/style_chat.h" namespace HistoryView { @@ -43,10 +46,14 @@ constexpr auto kDropDelayedAfterDelay = crl::time(2000); } // namespace EmojiInteractions::EmojiInteractions( + not_null parent, + not_null layerParent, not_null session, Fn)> itemTop) - : _session(session) - , _itemTop(std::move(itemTop)) { +: _parent(parent) +, _layerParent(layerParent) +, _session(session) +, _itemTop(std::move(itemTop)) { _session->data().viewRemoved( ) | rpl::filter([=] { return !_plays.empty() || !_delayed.empty(); @@ -56,6 +63,11 @@ EmojiInteractions::EmojiInteractions( ranges::remove(_delayed, view, &Delayed::view), end(_delayed)); }, _lifetime); + + _session->data().reactions().effectsUpdates( + ) | rpl::start_with_next([=] { + checkPendingEffects(); + }, _lifetime); } EmojiInteractions::~EmojiInteractions() = default; @@ -112,7 +124,7 @@ bool EmojiInteractions::playPremiumEffect( document->createMediaView()->videoThumbnailContent(), QString(), false, - true); + Stickers::EffectType::PremiumSticker); } } } @@ -140,7 +152,123 @@ void EmojiInteractions::play( media->bytes(), media->owner()->filepath(), incoming, - false); + Stickers::EffectType::EmojiInteraction); +} + +void EmojiInteractions::playEffectOnRead(not_null view) { + const auto flag = PowerSaving::Flag::kChatEffects; + if (view->data()->markEffectWatched() && !PowerSaving::On(flag)) { + playEffect(view); + } +} + +void EmojiInteractions::playEffect(not_null view) { + if (const auto resolved = resolveEffect(view)) { + playEffect(view, resolved); + } else if (view->data()->effectId()) { + if (resolved.document && !_downloadLifetime) { + _downloadLifetime = _session->downloaderTaskFinished( + ) | rpl::start_with_next([=] { + checkPendingEffects(); + }); + } + addPendingEffect(view); + } +} + +EmojiInteractions::ResolvedEffect EmojiInteractions::resolveEffect( + not_null view) { + const auto item = view->data(); + const auto effectId = item->effectId(); + if (!effectId) { + return {}; + } + using Type = Data::Reactions::Type; + const auto &effects = _session->data().reactions().list(Type::Effects); + const auto i = ranges::find( + effects, + Data::ReactionId{ effectId }, + &Data::Reaction::id); + if (i == end(effects)) { + return {}; + } + auto document = (DocumentData*)nullptr; + auto content = QByteArray(); + auto filepath = QString(); + if ((document = i->aroundAnimation)) { + content = document->createMediaView()->bytes(); + filepath = document->filepath(); + } else { + document = i->selectAnimation; + content = document->createMediaView()->videoThumbnailContent(); + } + return { + .emoticon = i->title, + .document = document, + .content = content, + .filepath = filepath, + }; +} + +void EmojiInteractions::playEffect( + not_null view, + const ResolvedEffect &resolved) { + play( + resolved.emoticon, + view, + resolved.document, + resolved.content, + resolved.filepath, + false, + Stickers::EffectType::MessageEffect); +} + +void EmojiInteractions::addPendingEffect(not_null view) { + auto found = false; + const auto predicate = [&](base::weak_ptr weak) { + const auto strong = weak.get(); + if (strong == view) { + found = true; + } + return !strong; + }; + _pendingEffects.erase( + ranges::remove_if(_pendingEffects, predicate), + end(_pendingEffects)); + if (!found) { + _pendingEffects.push_back(view); + } +} + +void EmojiInteractions::checkPendingEffects() { + auto waitingDownload = false; + const auto predicate = [&](base::weak_ptr weak) { + const auto strong = weak.get(); + if (!strong) { + return true; + } + const auto resolved = resolveEffect(strong); + if (resolved) { + playEffect(strong, resolved); + return true; + } else if (!strong->data()->effectId()) { + return true; + } else if (resolved.document) { + waitingDownload = true; + } + return false; + }; + _pendingEffects.erase( + ranges::remove_if(_pendingEffects, predicate), + end(_pendingEffects)); + if (!waitingDownload) { + _downloadLifetime.destroy(); + } else if (!_downloadLifetime) { + _downloadLifetime = _session->downloaderTaskFinished( + ) | rpl::start_with_next([=] { + checkPendingEffects(); + }); + } } void EmojiInteractions::play( @@ -150,7 +278,7 @@ void EmojiInteractions::play( QByteArray data, QString filepath, bool incoming, - bool premium) { + Stickers::EffectType type) { const auto top = _itemTop(view); const auto bottom = top + view->height(); if (_visibleTop >= bottom @@ -160,16 +288,30 @@ void EmojiInteractions::play( return; } + if (!_layer) { + _layer = base::make_unique_q(_layerParent); + const auto raw = _layer.get(); + raw->setAttribute(Qt::WA_TransparentForMouseEvents); + raw->show(); + raw->paintRequest() | rpl::start_with_next([=](QRect clip) { + paint(raw, clip); + }, raw->lifetime()); + } + refreshLayerShift(); + _layer->setGeometry(_layerParent->rect()); + auto lottie = document->session().emojiStickersPack().effectPlayer( document, data, filepath, - premium); + type); - const auto inner = premium + const auto inner = (type == Stickers::EffectType::PremiumSticker) ? HistoryView::Sticker::Size(document) : HistoryView::Sticker::EmojiSize(); - const auto shift = premium ? QPoint() : GenerateRandomShift(inner); + const auto shift = (type == Stickers::EffectType::EmojiInteraction) + ? GenerateRandomShift(inner) + : QPoint(); const auto raw = lottie.get(); lottie->updates( ) | rpl::start_with_next([=](Lottie::Update update) { @@ -178,11 +320,12 @@ void EmojiInteractions::play( const auto i = ranges::find(_plays, raw, [](const Play &p) { return p.lottie.get(); }); - const auto rect = computeRect(*i).translated(shift); - if (rect.y() + rect.height() >= _visibleTop - && rect.y() <= _visibleBottom) { - _updateRequests.fire_copy(rect); + auto update = computeRect(*i).translated(shift + _layerShift); + if (!i->lastTarget.isEmpty()) { + update = i->lastTarget.united(update); } + _layer->update(update); + i->lastTarget = QRect(); }); }, lottie->lifetime()); _plays.push_back({ @@ -190,21 +333,33 @@ void EmojiInteractions::play( .lottie = std::move(lottie), .shift = shift, .inner = inner, - .outer = (premium + .outer = ((type == Stickers::EffectType::PremiumSticker) ? HistoryView::Sticker::PremiumEffectSize(document) - : HistoryView::Sticker::EmojiEffectSize()), - .premium = premium, + : (type == Stickers::EffectType::EmojiInteraction) + ? HistoryView::Sticker::EmojiEffectSize() + : HistoryView::Sticker::MessageEffectSize()), + .type = type, }); if (incoming) { _playStarted.fire(std::move(emoticon)); } if (const auto media = view->media()) { - if (!premium) { + if (type == Stickers::EffectType::EmojiInteraction) { media->stickerClearLoopPlayed(); } } } +void EmojiInteractions::refreshLayerShift() { + _layerShift = Ui::MapFrom(_layerParent, _parent, QPoint(0, 0)); +} + +void EmojiInteractions::refreshLayerGeometryAndUpdate(QRect rect) { + if (!rect.isEmpty()) { + _layer->update(rect.translated(_layerShift)); + } +} + void EmojiInteractions::visibleAreaUpdated( int visibleTop, int visibleBottom) { @@ -214,9 +369,28 @@ void EmojiInteractions::visibleAreaUpdated( QRect EmojiInteractions::computeRect(const Play &play) const { const auto view = play.view; + const auto viewTop = _itemTop(view); + if (viewTop < 0) { + return QRect(); + } + if (play.type == Stickers::EffectType::MessageEffect) { + const auto icon = view->effectIconGeometry(); + if (icon.isEmpty()) { + return QRect(); + } + const auto size = play.outer; + const auto shift = view->hasRightLayout() + ? (-size.width() / 3) + : (size.width() / 3); + return QRect( + shift + icon.x() + (icon.width() - size.width()) / 2, + viewTop + icon.y() + (icon.height() - size.height()) / 2, + size.width(), + size.height()); + } const auto sticker = play.inner; const auto size = play.outer; - const auto shift = play.premium + const auto shift = (play.type == Stickers::EffectType::PremiumSticker) ? int(sticker.width() * kPremiumShift) : (size.width() / 40); const auto inner = view->innerGeometry(); @@ -224,20 +398,40 @@ QRect EmojiInteractions::computeRect(const Play &play) const { const auto left = rightAligned ? (inner.x() + inner.width() + shift - size.width()) : (inner.x() - shift); - const auto viewTop = _itemTop(view) + inner.y(); - if (viewTop < 0) { - return QRect(); - } - const auto top = viewTop + (sticker.height() - size.height()) / 2; + const auto top = viewTop + + inner.y() + + (sticker.height() - size.height()) / 2; return QRect(QPoint(left, top), size).translated(play.shift); } -void EmojiInteractions::paint(QPainter &p) { +void EmojiInteractions::paint(not_null layer, QRect clip) { + refreshLayerShift(); + const auto factor = style::DevicePixelRatio(); + const auto whole = layer->rect(); + + auto p = QPainter(layer); + + auto updated = QRect(); + const auto addRect = [&](QRect rect) { + if (updated.isEmpty()) { + updated = rect; + } else { + updated = rect.united(updated); + } + }; for (auto &play : _plays) { if (!play.lottie->ready()) { continue; } + const auto target = computeRect(play).translated(_layerShift); + if (!target.intersects(whole)) { + play.finished = true; + addRect(target); + continue; + } else if (!target.intersects(clip)) { + continue; + } auto request = Lottie::FrameRequest(); request.box = play.outer * factor; const auto rightAligned = play.view->hasRightLayout(); @@ -251,18 +445,18 @@ void EmojiInteractions::paint(QPainter &p) { play.framesCount = information.framesCount; play.frameRate = information.frameRate; } - const auto rect = computeRect(play); if (play.started && !play.frame) { play.finished = true; - _updateRequests.fire_copy(rect); + addRect(target); continue; } else if (play.frame > 0) { play.started = true; } p.drawImage( - QRect(rect.topLeft(), frame.image.size() / factor), + QRect(target.topLeft(), frame.image.size() / factor), frame.image); play.lottie->markFrameShown(); + play.lastTarget = target.translated(_layerShift); } _plays.erase(ranges::remove_if(_plays, [](const Play &play) { if (!play.finished) { @@ -271,6 +465,18 @@ void EmojiInteractions::paint(QPainter &p) { return true; }), end(_plays)); checkDelayed(); + + if (_plays.empty()) { + layer->hide(); + if (_layer.get() == layer) { + crl::on_main([moved = std::move(_layer)] {}); + } + } else if (!updated.isEmpty()) { + const auto translated = updated.translated(_layerShift); + if (translated.intersects(whole)) { + _layer->update(translated); + } + } } void EmojiInteractions::checkDelayed() { @@ -313,10 +519,6 @@ void EmojiInteractions::checkDelayed() { good.incoming); } -rpl::producer EmojiInteractions::updateRequests() const { - return _updateRequests.events(); -} - rpl::producer EmojiInteractions::playStarted() const { return _playStarted.events(); } diff --git a/Telegram/SourceFiles/history/view/history_view_emoji_interactions.h b/Telegram/SourceFiles/history/view/history_view_emoji_interactions.h index 4d38f38eb..6c1f25e8d 100644 --- a/Telegram/SourceFiles/history/view/history_view_emoji_interactions.h +++ b/Telegram/SourceFiles/history/view/history_view_emoji_interactions.h @@ -7,6 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "base/unique_qptr.h" + namespace Data { class DocumentMedia; } // namespace Data @@ -23,6 +25,14 @@ namespace Main { class Session; } // namespace Main +namespace Stickers { +enum class EffectType : uint8; +} // namespace Stickers + +namespace Ui { +class RpWidget; +} // namespace Ui + namespace HistoryView { class Element; @@ -30,6 +40,8 @@ class Element; class EmojiInteractions final { public: EmojiInteractions( + not_null parent, + not_null layerParent, not_null session, Fn)> itemTop); ~EmojiInteractions(); @@ -43,21 +55,24 @@ public: void cancelPremiumEffect(not_null view); void visibleAreaUpdated(int visibleTop, int visibleBottom); - void paint(QPainter &p); - [[nodiscard]] rpl::producer updateRequests() const; + void playEffectOnRead(not_null view); + void playEffect(not_null view); + + void paint(not_null layer, QRect clip); [[nodiscard]] rpl::producer playStarted() const; private: struct Play { not_null view; std::unique_ptr lottie; + mutable QRect lastTarget; QPoint shift; QSize inner; QSize outer; int frame = 0; int framesCount = 0; int frameRate = 0; - bool premium = false; + Stickers::EffectType type = {}; bool started = false; bool finished = false; }; @@ -68,6 +83,16 @@ private: crl::time shouldHaveStartedAt = 0; bool incoming = false; }; + struct ResolvedEffect { + QString emoticon; + DocumentData *document = nullptr; + QByteArray content; + QString filepath; + + explicit operator bool() const { + return document && (!content.isEmpty() || !filepath.isEmpty()); + } + }; [[nodiscard]] QRect computeRect(const Play &play) const; @@ -83,20 +108,37 @@ private: QByteArray data, QString filepath, bool incoming, - bool premium); + Stickers::EffectType type); void checkDelayed(); + void addPendingEffect(not_null view); + [[nodiscard]] ResolvedEffect resolveEffect( + not_null view); + void playEffect( + not_null view, + const ResolvedEffect &resolved); + void checkPendingEffects(); + + void refreshLayerShift(); + void refreshLayerGeometryAndUpdate(QRect rect); + + const not_null _parent; + const not_null _layerParent; const not_null _session; const Fn)> _itemTop; + base::unique_qptr _layer; + QPoint _layerShift; int _visibleTop = 0; int _visibleBottom = 0; std::vector _plays; std::vector _delayed; - rpl::event_stream _updateRequests; rpl::event_stream _playStarted; + std::vector> _pendingEffects; + rpl::lifetime _downloadLifetime; + rpl::lifetime _lifetime; }; diff --git a/Telegram/SourceFiles/history/view/history_view_fake_items.cpp b/Telegram/SourceFiles/history/view/history_view_fake_items.cpp new file mode 100644 index 000000000..5a6538036 --- /dev/null +++ b/Telegram/SourceFiles/history/view/history_view_fake_items.cpp @@ -0,0 +1,66 @@ +/* +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_fake_items.h" + +#include "base/unixtime.h" +#include "data/data_session.h" +#include "history/history.h" +#include "history/history_item.h" + +namespace HistoryView { + +AdminLog::OwnedItem GenerateItem( + not_null delegate, + not_null history, + PeerId from, + FullMsgId replyTo, + const QString &text, + EffectId effectId) { + Expects(history->peer->isUser()); + + const auto item = history->addNewLocalMessage({ + .id = history->nextNonHistoryEntryId(), + .flags = (MessageFlag::FakeHistoryItem + | MessageFlag::HasFromId + | MessageFlag::HasReplyInfo), + .from = from, + .replyTo = FullReplyTo{.messageId = replyTo }, + .date = base::unixtime::now(), + .effectId = effectId, + }, TextWithEntities{ .text = text }, MTP_messageMediaEmpty()); + + return AdminLog::OwnedItem(delegate, item); +} + +PeerId GenerateUser(not_null history, const QString &name) { + Expects(history->peer->isUser()); + + const auto peerId = Data::FakePeerIdForJustName(name); + history->owner().processUser(MTP_user( + MTP_flags(MTPDuser::Flag::f_first_name | MTPDuser::Flag::f_min), + peerToBareMTPInt(peerId), + MTP_long(0), + MTP_string(name), + MTPstring(), // last name + MTPstring(), // username + MTPstring(), // phone + MTPUserProfilePhoto(), // profile photo + MTPUserStatus(), // status + MTP_int(0), // bot info version + MTPVector(), // restrictions + MTPstring(), // bot placeholder + MTPstring(), // lang code + MTPEmojiStatus(), + MTPVector(), + MTPint(), // stories_max_id + MTPPeerColor(), // color + MTPPeerColor())); // profile_color + return peerId; +} + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_fake_items.h b/Telegram/SourceFiles/history/view/history_view_fake_items.h new file mode 100644 index 000000000..266c28004 --- /dev/null +++ b/Telegram/SourceFiles/history/view/history_view_fake_items.h @@ -0,0 +1,26 @@ +/* +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 "history/admin_log/history_admin_log_item.h" + +namespace HistoryView { + +[[nodiscard]] AdminLog::OwnedItem GenerateItem( + not_null delegate, + not_null history, + PeerId from, + FullMsgId replyTo, + const QString &text, + EffectId effectId = 0); + +[[nodiscard]] PeerId GenerateUser( + not_null history, + const QString &name); + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index 69963f32d..e5f8746f3 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -94,6 +94,98 @@ constexpr auto kClearUserpicsAfter = 50; } // namespace +const crl::time ListWidget::kItemRevealDuration = crl::time(150); + +WindowListDelegate::WindowListDelegate( + not_null window) +: _window(window) { +} + +not_null WindowListDelegate::listWindow() { + return _window; +} + +not_null WindowListDelegate::listEmojiInteractionsParent() { + return _window->content(); +} + +not_null WindowListDelegate::listChatStyle() { + return _window->chatStyle(); +} + +rpl::producer WindowListDelegate::listChatWideValue() { + return _window->adaptive().chatWideValue(); +} + +auto WindowListDelegate::listMakeReactionsManager( + QWidget *wheelEventsTarget, + Fn update) +-> std::unique_ptr { + return std::make_unique( + wheelEventsTarget, + std::move(update)); +} + +void WindowListDelegate::listVisibleAreaUpdated() { + _window->floatPlayerAreaUpdated(); +} + +std::shared_ptr WindowListDelegate::listUiShow() { + return _window->uiShow(); +} + +void WindowListDelegate::listShowPollResults( + not_null poll, + FullMsgId context) { + _window->showPollResults(poll, context); +} + +void WindowListDelegate::listCancelUploadLayer(not_null item) { + _window->cancelUploadLayer(item); +} + +bool WindowListDelegate::listAnimationsPaused() { + return _window->isGifPausedAtLeastFor(Window::GifPauseReason::Any); +} + +auto WindowListDelegate::listSendingAnimation() +-> Ui::MessageSendingAnimationController * { + return &_window->sendingAnimation(); +} + +Ui::ChatPaintContext WindowListDelegate::listPreparePaintContext( + Ui::ChatPaintContextArgs &&args) { + return _window->preparePaintContext(std::move(args)); +} + +bool WindowListDelegate::listMarkingContentRead() { + return _window->widget()->markingAsRead(); +} + +bool WindowListDelegate::listIgnorePaintEvent(QWidget *w, QPaintEvent *e) { + return _window->contentOverlapped(w, e); +} + +bool WindowListDelegate::listShowReactPremiumError( + not_null item, + const Data::ReactionId &id) { + return Window::ShowReactPremiumError(_window, item, id); +} + +void WindowListDelegate::listWindowSetInnerFocus() { + _window->widget()->setInnerFocus(); +} + +bool WindowListDelegate::listAllowsDragForward() { + return _window->adaptive().isOneColumn(); +} + +void WindowListDelegate::listLaunchDrag( + std::unique_ptr data, + Fn finished) { + _window->widget()->launchDrag(std::move(data), std::move(finished)); +} + ListWidget::MouseState::MouseState() : pointState(PointState::Outside) { } @@ -108,8 +200,6 @@ ListWidget::MouseState::MouseState( , pointState(pointState) { } -const crl::time ListWidget::kItemRevealDuration = crl::time(150); - template void ListWidget::enumerateItems(Method method) { constexpr auto TopToBottom = (direction == EnumItemsDirection::TopToBottom); @@ -287,31 +377,31 @@ void ListWidget::enumerateDates(Method method) { ListWidget::ListWidget( QWidget *parent, - not_null controller, + not_null session, not_null delegate) : RpWidget(parent) , _delegate(delegate) -, _controller(controller) +, _session(session) , _emojiInteractions(std::make_unique( - &controller->session(), + this, + _delegate->listEmojiInteractionsParent(), + session, [=](not_null view) { return itemTop(view); })) , _context(_delegate->listContext()) , _itemAverageHeight(itemMinimalHeight()) , _pathGradient( MakePathShiftGradient( - controller->chatStyle(), + _delegate->listChatStyle(), [=] { update(); })) -, _reactionsManager( - std::make_unique( - this, - [=](QRect updated) { update(updated); }, - controller->cachedReactionIconFactory().createMethod())) +, _reactionsManager(_delegate->listMakeReactionsManager( + this, + [=](QRect updated) { update(updated); })) , _translateTracker(MaybeTranslateTracker(_delegate->listTranslateHistory())) , _scrollDateCheck([this] { scrollDateCheck(); }) , _applyUpdatedScrollState([this] { applyUpdatedScrollState(); }) , _selectEnabled(_delegate->listAllowsMultiSelect()) , _highlighter( - &session().data(), + &_session->data(), [=](const HistoryItem *item) { return viewForItem(item); }, [=](const Element *view) { repaintItem(view); }) , _touchSelectTimer([=] { onTouchSelect(); }) @@ -319,25 +409,25 @@ ListWidget::ListWidget( setAttribute(Qt::WA_AcceptTouchEvents); setMouseTracking(true); _scrollDateHideTimer.setCallback([this] { scrollDateHideByTimer(); }); - session().data().viewRepaintRequest( + _session->data().viewRepaintRequest( ) | rpl::start_with_next([this](auto view) { if (view->delegate() == this) { repaintItem(view); } }, lifetime()); - session().data().viewResizeRequest( + _session->data().viewResizeRequest( ) | rpl::start_with_next([this](auto view) { if (view->delegate() == this) { resizeItem(view); } }, lifetime()); - session().data().itemViewRefreshRequest( + _session->data().itemViewRefreshRequest( ) | rpl::start_with_next([this](auto item) { if (const auto view = viewForItem(item)) { refreshItem(view); } }, lifetime()); - session().data().viewLayoutChanged( + _session->data().viewLayoutChanged( ) | rpl::start_with_next([this](auto view) { if (view->delegate() == this) { if (view->isUnderCursor()) { @@ -345,37 +435,37 @@ ListWidget::ListWidget( } } }, lifetime()); - session().data().itemDataChanges( + _session->data().itemDataChanges( ) | rpl::start_with_next([=](not_null item) { if (const auto view = viewForItem(item)) { view->itemDataChanged(); } }, lifetime()); - session().downloaderTaskFinished( + _session->downloaderTaskFinished( ) | rpl::start_with_next([=] { update(); }, lifetime()); - session().data().peerDecorationsUpdated( + _session->data().peerDecorationsUpdated( ) | rpl::start_with_next([=] { update(); }, lifetime()); - session().data().itemRemoved( + _session->data().itemRemoved( ) | rpl::start_with_next([=](not_null item) { itemRemoved(item); }, lifetime()); using MessageUpdateFlag = Data::MessageUpdate::Flag; - session().changes().realtimeMessageUpdates( + _session->changes().realtimeMessageUpdates( MessageUpdateFlag::NewUnreadReaction ) | rpl::start_with_next([=](const Data::MessageUpdate &update) { maybeMarkReactionsRead(update.item); }, lifetime()); if (const auto history = _delegate->listTranslateHistory()) { - session().changes().historyUpdates( + _session->changes().historyUpdates( history, Data::HistoryUpdate::Flag::TranslatedTo ) | rpl::start_with_next([=] { @@ -383,7 +473,7 @@ ListWidget::ListWidget( }, lifetime()); } - session().data().itemVisibilityQueries( + _session->data().itemVisibilityQueries( ) | rpl::start_with_next([=]( const Data::Session::ItemVisibilityQuery &query) { if (const auto view = viewForItem(query.item)) { @@ -396,34 +486,31 @@ ListWidget::ListWidget( } }, lifetime()); - _reactionsManager->chosen( - ) | rpl::start_with_next([=](ChosenReaction reaction) { - _reactionsManager->updateButton({}); - reactionChosen(reaction); - }, lifetime()); - - Reactions::SetupManagerList( - _reactionsManager.get(), - _reactionsItem.value()); - - Core::App().settings().cornerReactionValue( - ) | rpl::start_with_next([=](bool value) { - _useCornerReaction = value; - if (!value) { + if (_reactionsManager) { + _reactionsManager->chosen( + ) | rpl::start_with_next([=](ChosenReaction reaction) { _reactionsManager->updateButton({}); - } - }, lifetime()); + reactionChosen(reaction); + }, lifetime()); - controller->adaptive().chatWideValue( + Reactions::SetupManagerList( + _reactionsManager.get(), + _reactionsItem.value()); + + Core::App().settings().cornerReactionValue( + ) | rpl::start_with_next([=](bool value) { + _useCornerReaction = value; + if (!value) { + _reactionsManager->updateButton({}); + } + }, lifetime()); + } + + _delegate->listChatWideValue( ) | rpl::start_with_next([=](bool wide) { _isChatWide = wide; }, lifetime()); - _emojiInteractions->updateRequests( - ) | rpl::start_with_next([=](QRect rect) { - update(rect); - }, lifetime()); - _selectScroll.scrolls( ) | rpl::start_with_next([=](int d) { delegate->listScrollTo(_visibleTop + d, false); @@ -431,11 +518,11 @@ ListWidget::ListWidget( } Main::Session &ListWidget::session() const { - return _controller->session(); + return *_session; } not_null ListWidget::controller() const { - return _controller; + return _delegate->listWindow(); } not_null ListWidget::delegate() const { @@ -506,6 +593,7 @@ void ListWidget::refreshRows(const Data::MessagesSlice &old) { auto destroyingBarElement = _bar.element; auto clearingOverElement = _overElement; + _itemsKnownTillEnd = (_slice.skippedAfter == 0); _resizePending = true; _items.clear(); _items.reserve(_slice.ids.size()); @@ -856,7 +944,9 @@ void ListWidget::restoreScrollState() { } _scrollInited = true; _scrollTopState.item = _bar.element->data()->position(); - _scrollTopState.shift = st::lineWidth + st::historyUnreadBarMargin; + _scrollTopState.shift = st::lineWidth + + st::historyUnreadBarMargin + + _bar.element->displayedDateHeight(); } const auto index = findNearestItem(_scrollTopState.item); if (index >= 0) { @@ -1010,7 +1100,7 @@ void ListWidget::visibleTopBottomUpdated( } else { scrollDateHideByTimer(); } - _controller->floatPlayerAreaUpdated(); + _delegate->listVisibleAreaUpdated(); session().data().itemVisibilitiesUpdated(); _applyUpdatedScrollState.call(); @@ -1022,7 +1112,7 @@ void ListWidget::applyUpdatedScrollState() { } void ListWidget::updateVisibleTopItem() { - if (_visibleBottom == height()) { + if (_itemsKnownTillEnd && _visibleBottom == height()) { _visibleTopItem = nullptr; } else if (_items.empty()) { _visibleTopItem = nullptr; @@ -1447,7 +1537,7 @@ bool ListWidget::showCopyRestriction(HistoryItem *item) { if (type == CopyRestrictionType::None) { return false; } - _controller->showToast((type == CopyRestrictionType::Channel) + _delegate->listUiShow()->showToast((type == CopyRestrictionType::Channel) ? tr::lng_error_nocopy_channel(tr::now) : tr::lng_error_nocopy_group(tr::now)); return true; @@ -1458,7 +1548,7 @@ bool ListWidget::showCopyMediaRestriction(not_null item) { if (type == CopyRestrictionType::None) { return false; } - _controller->showToast((type == CopyRestrictionType::Channel) + _delegate->listUiShow()->showToast((type == CopyRestrictionType::Channel) ? tr::lng_error_nocopy_channel(tr::now) : tr::lng_error_nocopy_group(tr::now)); return true; @@ -1502,6 +1592,10 @@ bool ListWidget::hasSelectRestriction() const { != CopyRestrictionType::None; } +Element *ListWidget::lookupItemByY(int y) const { + return strictFindItemByY(y); +} + auto ListWidget::findViewForPinnedTracking(int top) const -> std::pair { const auto findScrollTopItem = [&](int top) @@ -1684,7 +1778,7 @@ void ListWidget::elementStartStickerLoop(not_null view) { void ListWidget::elementShowPollResults( not_null poll, FullMsgId context) { - _controller->showPollResults(poll, context); + _delegate->listShowPollResults(poll, context); } void ListWidget::elementOpenPhoto( @@ -1702,7 +1796,7 @@ void ListWidget::elementOpenDocument( void ListWidget::elementCancelUpload(const FullMsgId &context) { if (const auto item = session().data().message(context)) { - _controller->cancelUploadLayer(item); + _delegate->listCancelUploadLayer(item); } } @@ -1714,7 +1808,7 @@ void ListWidget::elementShowTooltip( } bool ListWidget::elementAnimationsPaused() { - return _controller->isGifPausedAtLeastFor(Window::GifPauseReason::Any); + return _delegate->listAnimationsPaused(); } bool ListWidget::elementHideReply(not_null view) { @@ -1771,6 +1865,12 @@ void ListWidget::elementCancelPremium(not_null view) { _emojiInteractions->cancelPremiumEffect(view); } +void ListWidget::elementStartEffect( + not_null view, + Element *replacing) { + _emojiInteractions->playEffect(view); +} + QString ListWidget::elementAuthorRank(not_null view) { return _delegate->listElementAuthorRank(view); } @@ -1846,8 +1946,8 @@ void ListWidget::startItemRevealAnimations() { void ListWidget::startMessageSendingAnimation( not_null item) { - auto &sendingAnimation = controller()->sendingAnimation(); - if (!sendingAnimation.checkExpectedType(item)) { + const auto sendingAnimation = _delegate->listSendingAnimation(); + if (!sendingAnimation || !sendingAnimation->checkExpectedType(item)) { return; } @@ -1863,7 +1963,7 @@ void ListWidget::startMessageSendingAnimation( return mapToGlobal(QPoint(0, itemTop(view) - additional)); }); - sendingAnimation.startAnimation({ + sendingAnimation->startAnimation({ .globalEndTopLeft = std::move(globalEndTopLeft), .view = [=] { return viewForItem(item); }, .paintContext = [=] { return preparePaintContext({}); }, @@ -2018,7 +2118,7 @@ TextSelection ListWidget::itemRenderSelection( Ui::ChatPaintContext ListWidget::preparePaintContext( const QRect &clip) const { - return controller()->preparePaintContext({ + return _delegate->listPreparePaintContext({ .theme = _delegate->listChatTheme(), .clip = clip, .visibleAreaPositionGlobal = mapToGlobal(QPoint(0, _visibleTop)), @@ -2030,7 +2130,7 @@ Ui::ChatPaintContext ListWidget::preparePaintContext( bool ListWidget::markingContentsRead() const { return _showFinished && !_refreshingViewer - && controller()->widget()->markingAsRead(); + && _delegate->listMarkingContentRead(); } bool ListWidget::markingMessagesRead() const { @@ -2061,12 +2161,9 @@ void ListWidget::checkActivation() { } void ListWidget::paintEvent(QPaintEvent *e) { - if ((_context != Context::ShortcutMessages) - && _controller->contentOverlapped(this, e)) { + if (_delegate->listIgnorePaintEvent(this, e)) { return; - } - - if (_translateTracker) { + } else if (_translateTracker) { _translateTracker->startBunch(); } auto readTill = (HistoryItem*)nullptr; @@ -2108,20 +2205,25 @@ void ListWidget::paintEvent(QPaintEvent *e) { _delegate->listPaintEmpty(p, context); return; } - _reactionsManager->startEffectsCollection(); + if (_reactionsManager) { + _reactionsManager->startEffectsCollection(); + } - const auto session = &controller()->session(); + const auto session = &this->session(); auto top = itemTop(from->get()); context = context.translated(0, -top); p.translate(0, top); - const auto &sendingAnimation = _controller->sendingAnimation(); + const auto sendingAnimation = _delegate->listSendingAnimation(); for (auto i = from; i != to; ++i) { const auto view = *i; const auto item = view->data(); const auto height = view->height(); - if (!sendingAnimation.hasAnimatedMessage(item)) { - context.reactionInfo - = _reactionsManager->currentReactionPaintInfo(); + if (!sendingAnimation + || !sendingAnimation->hasAnimatedMessage(item)) { + if (_reactionsManager) { + context.reactionInfo + = _reactionsManager->currentReactionPaintInfo(); + } context.outbg = view->hasOutLayout(); context.selection = itemRenderSelection(view); context.highlight = _highlighter.state(item); @@ -2133,17 +2235,19 @@ void ListWidget::paintEvent(QPaintEvent *e) { const auto isSponsored = item->isSponsored(); const auto isUnread = _delegate->listElementShownUnread(view) && item->isRegular(); - const auto withReaction = item->hasUnreadReaction(); + const auto withReaction = context.reactionInfo + && item->hasUnreadReaction(); const auto yShown = [&](int y) { return (_visibleBottom >= y && _visibleTop <= y); }; - const auto markShown = isSponsored - ? view->markSponsoredViewed(_visibleBottom - top) - : withReaction - ? yShown(top + context.reactionInfo->position.y()) - : isUnread - ? yShown(top + height) - : yShown(top + height / 2); + const auto markShown = (_context != Context::ChatPreview) + && (isSponsored + ? view->markSponsoredViewed(_visibleBottom - top) + : withReaction + ? yShown(top + context.reactionInfo->position.y()) + : isUnread + ? yShown(top + height) + : yShown(top + height / 2)); if (markShown) { if (isSponsored) { session->sponsoredMessages().view(item->fullId()); @@ -2165,9 +2269,11 @@ void ListWidget::paintEvent(QPaintEvent *e) { if (item->hasExtendedMediaPreview()) { session->api().views().pollExtendedMedia(item); } - _reactionsManager->recordCurrentReactionEffect( - item->fullId(), - QPoint(0, top)); + if (_reactionsManager) { + _reactionsManager->recordCurrentReactionEffect( + item->fullId(), + QPoint(0, top)); + } top += height; context.translate(0, -height); p.translate(0, height); @@ -2178,8 +2284,9 @@ void ListWidget::paintEvent(QPaintEvent *e) { paintUserpics(p, context, clip); paintDates(p, context, clip); - _reactionsManager->paint(p, context); - _emojiInteractions->paint(p); + if (_reactionsManager) { + _reactionsManager->paint(p, context); + } } void ListWidget::paintUserpics( @@ -2189,7 +2296,7 @@ void ListWidget::paintUserpics( if (_context == Context::ShortcutMessages) { return; } - const auto session = &controller()->session(); + const auto session = &this->session(); enumerateUserpics([&](not_null view, int userpicTop) { // stop the enumeration if the userpic is below the painted rect if (userpicTop >= clip.top() + clip.height()) { @@ -2469,7 +2576,8 @@ Element *ListWidget::strictFindItemByY(int y) const { } auto ListWidget::countScrollState() const -> ScrollTopState { - if (_items.empty() || _visibleBottom == height()) { + if (_items.empty() + || (_itemsKnownTillEnd && _visibleBottom == height())) { return { Data::MessagePosition(), 0 }; } const auto topItem = findItemByY(_visibleTop); @@ -2538,7 +2646,7 @@ void ListWidget::toggleFavoriteReaction(not_null view) const { Data::LookupPossibleReactions(item).recent, favorite, &Data::Reaction::id) - || Window::ShowReactPremiumError(_controller, item, favorite)) { + || _delegate->listShowReactPremiumError(item, favorite)) { return; } else if (!ranges::contains(item->chosenReactions(), favorite)) { if (const auto top = itemTop(view); top >= 0) { @@ -2607,6 +2715,7 @@ void ListWidget::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { if (link && !link->property( kSendReactionEmojiProperty).value().empty() + && _reactionsManager && _reactionsManager->showContextMenu( this, e, @@ -2632,13 +2741,13 @@ void ListWidget::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { this, overItem, clickedReaction, - _controller, + controller(), _whoReactedMenuLifetime); e->accept(); return; } - auto request = ContextMenuRequest(_controller); + auto request = ContextMenuRequest(controller()); request.link = link; request.view = _overElement; @@ -2674,12 +2783,11 @@ void ListWidget::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { const auto attached = reactItem ? AttachSelectorToMenu( _menu.get(), - _controller, + controller(), desiredPosition, reactItem, [=](ChosenReaction reaction) { reactionChosen(reaction); }, - ItemReactionsAbout(reactItem), - _controller->cachedReactionIconFactory().createMethod()) + ItemReactionsAbout(reactItem)) : AttachSelectorResult::Skipped; if (attached == AttachSelectorResult::Failed) { _menu = nullptr; @@ -2696,10 +2804,7 @@ void ListWidget::reactionChosen(ChosenReaction reaction) { const auto item = session().data().message(reaction.context); if (!item) { return; - } else if (Window::ShowReactPremiumError( - _controller, - item, - reaction.id)) { + } else if (_delegate->listShowReactPremiumError(item, reaction.id)) { if (_menu) { _menu->hideMenu(); } @@ -2953,7 +3058,9 @@ void ListWidget::enterEventHook(QEnterEvent *e) { } void ListWidget::leaveEventHook(QEvent *e) { - _reactionsManager->updateButton({ .cursorLeft = true }); + if (_reactionsManager) { + _reactionsManager->updateButton({ .cursorLeft = true }); + } if (const auto view = _overElement) { if (_overState.pointState != PointState::Outside) { repaintItem(view); @@ -3168,9 +3275,9 @@ void ListWidget::mouseActionStart( const auto pressElement = _overElement; _mouseAction = MouseAction::None; - _pressWasInactive = Ui::WasInactivePress(_controller->widget()); + _pressWasInactive = Ui::WasInactivePress(window()); if (_pressWasInactive) { - Ui::MarkInactivePress(_controller->widget(), false); + Ui::MarkInactivePress(window(), false); } if (ClickHandler::getPressed()) { @@ -3342,7 +3449,7 @@ void ListWidget::mouseActionFinish( } else if (_selectedTextItem && !_pressWasInactive) { if (_selectedTextRange.from == _selectedTextRange.to) { clearTextSelection(); - _controller->widget()->setInnerFocus(); + _delegate->listWindowSetInnerFocus(); } } } @@ -3370,7 +3477,7 @@ ClickHandlerContext ListWidget::prepareClickHandlerContext(FullMsgId id) { ? (ElementDelegate*)weak : nullptr; }, - .sessionWindow = base::make_weak(_controller), + .sessionWindow = base::make_weak(controller()), }; } @@ -3380,7 +3487,9 @@ void ListWidget::mouseActionUpdate() { std::clamp(mousePosition.x(), 0, width()), std::clamp(mousePosition.y(), _visibleTop, _visibleBottom)); - const auto reactionState = _reactionsManager->buttonTextState(point); + const auto reactionState = _reactionsManager + ? _reactionsManager->buttonTextState(point) + : TextState(); const auto reactionItem = session().data().message(reactionState.itemId); const auto reactionView = viewForItem(reactionItem); const auto view = reactionView @@ -3400,12 +3509,14 @@ void ListWidget::mouseActionUpdate() { _overElement = view; repaintItem(_overElement); } - _reactionsManager->updateButton(view - ? reactionButtonParameters( - view, - itemPoint, - reactionState) - : Reactions::ButtonParameters()); + if (_reactionsManager) { + _reactionsManager->updateButton(view + ? reactionButtonParameters( + view, + itemPoint, + reactionState) + : Reactions::ButtonParameters()); + } if (viewChanged && view) { _reactionsItem = item; } @@ -3632,7 +3743,7 @@ std::unique_ptr ListWidget::prepareDrag() { if (!urls.isEmpty()) { mimeData->setUrls(urls); } - if (uponSelected && !_controller->adaptive().isOneColumn()) { + if (uponSelected && !_delegate->listAllowsDragForward()) { const auto canForwardAll = [&] { for (const auto &[itemId, data] : _selected) { if (!data.canForward) { @@ -3697,8 +3808,10 @@ std::unique_ptr ListWidget::prepareDrag() { void ListWidget::performDrag() { if (auto mimeData = prepareDrag()) { // This call enters event loop and can destroy any QObject. - _reactionsManager->updateButton({}); - _controller->widget()->launchDrag( + if (_reactionsManager) { + _reactionsManager->updateButton({}); + } + _delegate->listLaunchDrag( std::move(mimeData), crl::guard(this, [=] { mouseActionUpdate(QCursor::pos()); })); } @@ -3716,7 +3829,10 @@ void ListWidget::repaintItem(const Element *view) { const auto range = view->verticalRepaintRange(); update(0, top + range.top, width(), range.height); const auto id = view->data()->fullId(); - if (const auto area = _reactionsManager->lookupEffectArea(id)) { + const auto area = _reactionsManager + ? _reactionsManager->lookupEffectArea(id) + : std::nullopt; + if (area) { update(*area); } } @@ -3874,7 +3990,9 @@ void ListWidget::itemRemoved(not_null item) { viewReplaced(view, nullptr); _views.erase(i); - _reactionsManager->remove(item->fullId()); + if (_reactionsManager) { + _reactionsManager->remove(item->fullId()); + } updateItemsGeometry(); } @@ -3898,7 +4016,8 @@ void ListWidget::editMessageRequestNotify(FullMsgId item) const { bool ListWidget::lastMessageEditRequestNotify() const { const auto now = base::unixtime::now(); auto proj = [&](not_null view) { - return view->data()->allowsEdit(now); + return view->data()->allowsEdit(now) + && !view->data()->isUploading(); }; const auto &list = ranges::views::reverse(_items); const auto it = ranges::find_if(list, std::move(proj)); @@ -3912,12 +4031,15 @@ bool ListWidget::lastMessageEditRequestNotify() const { } } -rpl::producer ListWidget::replyToMessageRequested() const { +auto ListWidget::replyToMessageRequested() const +-> rpl::producer { return _requestedToReplyToMessage.events(); } -void ListWidget::replyToMessageRequestNotify(FullReplyTo id) { - _requestedToReplyToMessage.fire(std::move(id)); +void ListWidget::replyToMessageRequestNotify( + FullReplyTo to, + bool forceAnotherChat) { + _requestedToReplyToMessage.fire({ std::move(to), forceAnotherChat }); } rpl::producer ListWidget::readMessageRequested() const { @@ -3997,7 +4119,7 @@ void ConfirmDeleteSelectedItems(not_null widget) { } } auto box = Box( - &widget->controller()->session(), + &widget->session(), widget->getSelectedIds()); box->setDeleteConfirmedCallback(crl::guard(widget, [=] { widget->cancelSelection(); diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.h b/Telegram/SourceFiles/history/view/history_view_list_widget.h index b68e689e9..5cfc57c08 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.h +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.h @@ -25,11 +25,14 @@ class Session; } // namespace Main namespace Ui { +class Show; class PopupMenu; class ChatTheme; struct ChatPaintContext; +struct ChatPaintContextArgs; enum class TouchScrollState; struct PeerUserpicView; +class MessageSendingAnimationController; } // namespace Ui namespace Window { @@ -40,6 +43,7 @@ namespace Data { struct Group; struct Reaction; struct AllowedReactions; +struct ReactionId; } // namespace Data namespace HistoryView::Reactions { @@ -154,6 +158,73 @@ public: virtual History *listTranslateHistory() = 0; virtual void listAddTranslatedItems( not_null tracker) = 0; + + // Methods that use Window::SessionController by default. + virtual not_null listWindow() = 0; + virtual not_null listEmojiInteractionsParent() = 0; + virtual not_null listChatStyle() = 0; + virtual rpl::producer listChatWideValue() = 0; + virtual std::unique_ptr listMakeReactionsManager( + QWidget *wheelEventsTarget, + Fn update) = 0; + virtual void listVisibleAreaUpdated() = 0; + virtual std::shared_ptr listUiShow() = 0; + virtual void listShowPollResults( + not_null poll, + FullMsgId context) = 0; + virtual void listCancelUploadLayer(not_null item) = 0; + virtual bool listAnimationsPaused() = 0; + virtual auto listSendingAnimation() + -> Ui::MessageSendingAnimationController* = 0; + virtual Ui::ChatPaintContext listPreparePaintContext( + Ui::ChatPaintContextArgs &&args) = 0; + virtual bool listMarkingContentRead() = 0; + virtual bool listIgnorePaintEvent(QWidget *w, QPaintEvent *e) = 0; + virtual bool listShowReactPremiumError( + not_null item, + const Data::ReactionId &id) = 0; + virtual void listWindowSetInnerFocus() = 0; + virtual bool listAllowsDragForward() = 0; + virtual void listLaunchDrag( + std::unique_ptr data, + Fn finished) = 0; +}; + +class WindowListDelegate : public ListDelegate { +public: + explicit WindowListDelegate(not_null window); + + not_null listWindow() override; + not_null listEmojiInteractionsParent() override; + not_null listChatStyle() override; + rpl::producer listChatWideValue() override; + std::unique_ptr listMakeReactionsManager( + QWidget *wheelEventsTarget, + Fn update) override; + void listVisibleAreaUpdated() override; + std::shared_ptr listUiShow() override; + void listShowPollResults( + not_null poll, + FullMsgId context) override; + void listCancelUploadLayer(not_null item) override; + bool listAnimationsPaused() override; + Ui::MessageSendingAnimationController *listSendingAnimation() override; + Ui::ChatPaintContext listPreparePaintContext( + Ui::ChatPaintContextArgs &&args) override; + bool listMarkingContentRead() override; + bool listIgnorePaintEvent(QWidget *w, QPaintEvent *e) override; + bool listShowReactPremiumError( + not_null item, + const Data::ReactionId &id) override; + void listWindowSetInnerFocus() override; + bool listAllowsDragForward() override; + void listLaunchDrag( + std::unique_ptr data, + Fn finished) override; + +private: + const not_null _window; + }; struct SelectionData { @@ -211,7 +282,7 @@ class ListWidget final public: ListWidget( QWidget *parent, - not_null controller, + not_null session, not_null delegate); static const crl::time kItemRevealDuration; @@ -275,6 +346,7 @@ public: [[nodiscard]] bool hasCopyRestrictionForSelected() const; [[nodiscard]] bool showCopyRestrictionForSelected(); [[nodiscard]] bool hasSelectRestriction() const; + [[nodiscard]] Element *lookupItemByY(int y) const; [[nodiscard]] std::pair findViewForPinnedTracking( int top) const; @@ -286,11 +358,18 @@ public: QPoint tooltipPos() const override; bool tooltipWindowActive() const override; + struct ReplyToMessageRequest { + FullReplyTo to; + bool forceAnotherChat = false; + }; [[nodiscard]] rpl::producer editMessageRequested() const; void editMessageRequestNotify(FullMsgId item) const; [[nodiscard]] bool lastMessageEditRequestNotify() const; - [[nodiscard]] rpl::producer replyToMessageRequested() const; - void replyToMessageRequestNotify(FullReplyTo id); + [[nodiscard]] auto replyToMessageRequested() const + -> rpl::producer; + void replyToMessageRequestNotify( + FullReplyTo to, + bool forceAnotherChat = false); [[nodiscard]] rpl::producer readMessageRequested() const; [[nodiscard]] rpl::producer showMessageRequested() const; void replyNextMessage(FullMsgId fullId, bool next = true); @@ -342,6 +421,9 @@ public: not_null view, Element *replacing) override; void elementCancelPremium(not_null view) override; + void elementStartEffect( + not_null view, + Element *replacing) override; QString elementAuthorRank(not_null view) override; void setEmptyInfoWidget(base::unique_qptr &&w); @@ -641,7 +723,7 @@ private: static constexpr auto kMinimalIdsLimit = 24; const not_null _delegate; - const not_null _controller; + const not_null _session; const std::unique_ptr _emojiInteractions; const Context _context; @@ -651,6 +733,8 @@ private: int _aroundIndex = -1; int _idsLimit = kMinimalIdsLimit; Data::MessagesSlice _slice; + bool _itemsKnownTillEnd = false; + std::vector> _items; ViewsMap _views, _viewsCapacity; int _itemsTop = 0; @@ -760,7 +844,7 @@ private: base::Timer _touchScrollTimer; rpl::event_stream _requestedToEditMessage; - rpl::event_stream _requestedToReplyToMessage; + rpl::event_stream _requestedToReplyToMessage; rpl::event_stream _requestedToReadMessage; rpl::event_stream _requestedToShowMessage; diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 8afeff663..1683af9ae 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/round_rect.h" #include "ui/text/text_utilities.h" #include "ui/power_saving.h" +#include "data/components/factchecks.h" #include "data/components/sponsored_messages.h" #include "data/data_session.h" #include "data/data_user.h" @@ -410,7 +411,6 @@ Message::Message( not_null data, Element *replacing) : Element(delegate, data, replacing, Flag(0)) -, _invertMedia(data->invertMedia() && !data->emptyText()) , _hideReply(delegate->elementHideReply(this)) , _bottomInfo( &data->history()->owner().reactions(), @@ -428,6 +428,7 @@ Message::Message( : base::flat_map< Data::ReactionId, std::unique_ptr>(); + auto animation = replacing ? replacing->takeEffectAnimation() : nullptr; if (!animations.empty()) { const auto repainter = [=] { repaint(); }; for (const auto &[id, animation] : animations) { @@ -435,10 +436,11 @@ Message::Message( } if (_reactions) { _reactions->continueAnimations(std::move(animations)); - } else { - _bottomInfo.continueReactionAnimations(std::move(animations)); } } + if (animation) { + _bottomInfo.continueEffectAnimation(std::move(animation)); + } if (data->isSponsored()) { const auto &session = data->history()->session(); const auto details = session.sponsoredMessages().lookupDetails( @@ -596,16 +598,8 @@ void Message::animateReaction(Ui::ReactionFlyAnimationArgs &&args) { return; } - const auto animateInBottomInfo = [&](QPoint bottomRight) { - _bottomInfo.animateReaction(args.translated(-bottomRight), repainter); - }; if (bubble) { - auto entry = logEntryOriginal(); - // Entry page is always a bubble bottom. - auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || (entry/* && entry->isBubbleBottom()*/); - auto mediaOnTop = (mediaDisplayed && media->isBubbleTop()) || (entry && entry->isBubbleTop()); - auto inner = g; if (_comments) { inner.setHeight(inner.height() - st::historyCommentsButtonHeight); @@ -623,6 +617,51 @@ void Message::animateReaction(Ui::ReactionFlyAnimationArgs &&args) { _reactions->animate(args.translated(-reactionsPosition), repainter); return; } + } +} + +void Message::animateEffect(Ui::ReactionFlyAnimationArgs &&args) { + const auto item = data(); + const auto media = this->media(); + + auto g = countGeometry(); + if (g.width() < 1 || isHidden()) { + return; + } + const auto repainter = [=] { repaint(); }; + + const auto bubble = drawBubble(); + const auto reactionsInBubble = _reactions && embedReactionsInBubble(); + const auto mediaDisplayed = media && media->isDisplayed(); + const auto keyboard = item->inlineReplyKeyboard(); + auto keyboardHeight = 0; + if (keyboard) { + keyboardHeight = keyboard->naturalHeight(); + g.setHeight(g.height() - st::msgBotKbButton.margin - keyboardHeight); + } + + const auto animateInBottomInfo = [&](QPoint bottomRight) { + _bottomInfo.animateEffect(args.translated(-bottomRight), repainter); + }; + if (bubble) { + const auto entry = logEntryOriginal(); + const auto check = factcheckBlock(); + + // Entry page is always a bubble bottom. + auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || check || (entry/* && entry->isBubbleBottom()*/); + auto mediaOnTop = (mediaDisplayed && media->isBubbleTop()) || (entry && entry->isBubbleTop()); + + auto inner = g; + if (_comments) { + inner.setHeight(inner.height() - st::historyCommentsButtonHeight); + } + auto trect = inner.marginsRemoved(st::msgPadding); + const auto reactionsTop = (reactionsInBubble && !_viewButton) + ? st::mediaInBubbleSkip + : 0; + const auto reactionsHeight = reactionsInBubble + ? (reactionsTop + _reactions->height()) + : 0; if (_viewButton) { const auto belowInfo = _viewButton->belowMessageInfo(); const auto infoHeight = reactionsInBubble @@ -667,9 +706,98 @@ auto Message::takeReactionAnimations() -> base::flat_map< Data::ReactionId, std::unique_ptr> { - return _reactions - ? _reactions->takeAnimations() - : _bottomInfo.takeReactionAnimations(); + if (_reactions) { + return _reactions->takeAnimations(); + } + return {}; +} + +auto Message::takeEffectAnimation() +-> std::unique_ptr { + return _bottomInfo.takeEffectAnimation(); +} + +QRect Message::effectIconGeometry() const { + const auto item = data(); + const auto media = this->media(); + + auto g = countGeometry(); + if (g.width() < 1 || isHidden()) { + return {}; + } + const auto bubble = drawBubble(); + const auto reactionsInBubble = _reactions && embedReactionsInBubble(); + const auto mediaDisplayed = media && media->isDisplayed(); + const auto keyboard = item->inlineReplyKeyboard(); + auto keyboardHeight = 0; + if (keyboard) { + keyboardHeight = keyboard->naturalHeight(); + g.setHeight(g.height() - st::msgBotKbButton.margin - keyboardHeight); + } + + const auto fromBottomInfo = [&](QPoint bottomRight) { + const auto size = _bottomInfo.currentSize(); + return _bottomInfo.effectIconGeometry().translated( + bottomRight - QPoint(size.width(), size.height())); + }; + if (bubble) { + const auto entry = logEntryOriginal(); + const auto check = factcheckBlock(); + + // Entry page is always a bubble bottom. + auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || check || (entry/* && entry->isBubbleBottom()*/); + auto mediaOnTop = (mediaDisplayed && media->isBubbleTop()) || (entry && entry->isBubbleTop()); + + auto inner = g; + if (_comments) { + inner.setHeight(inner.height() - st::historyCommentsButtonHeight); + } + auto trect = inner.marginsRemoved(st::msgPadding); + const auto reactionsTop = (reactionsInBubble && !_viewButton) + ? st::mediaInBubbleSkip + : 0; + const auto reactionsHeight = reactionsInBubble + ? (reactionsTop + _reactions->height()) + : 0; + if (_viewButton) { + const auto belowInfo = _viewButton->belowMessageInfo(); + const auto infoHeight = reactionsInBubble + ? (reactionsHeight + 2 * st::mediaInBubbleSkip) + : _bottomInfo.height(); + const auto heightMargins = QMargins(0, 0, 0, infoHeight); + if (belowInfo) { + inner -= heightMargins; + } + trect.setHeight(trect.height() - _viewButton->height()); + if (reactionsInBubble) { + trect.setHeight(trect.height() - st::mediaInBubbleSkip + st::msgPadding.bottom()); + } else if (mediaDisplayed) { + trect.setHeight(trect.height() - st::mediaInBubbleSkip); + } + } + if (mediaOnBottom) { + trect.setHeight(trect.height() + + st::msgPadding.bottom() + - viewButtonHeight()); + } + if (mediaOnTop) { + trect.setY(trect.y() - st::msgPadding.top()); + } + if (mediaDisplayed && mediaOnBottom && media->customInfoLayout()) { + auto mediaHeight = media->height(); + auto mediaLeft = trect.x() - st::msgPadding.left(); + auto mediaTop = (trect.y() + trect.height() - mediaHeight); + return fromBottomInfo(QPoint(mediaLeft, mediaTop) + media->resolveCustomInfoRightBottom()); + } else { + return fromBottomInfo({ + inner.left() + inner.width() - (st::msgPadding.right() - st::msgDateDelta.x()), + inner.top() + inner.height() - (st::msgPadding.bottom() - st::msgDateDelta.y()), + }); + } + } else if (mediaDisplayed) { + return fromBottomInfo(g.topLeft() + media->resolveCustomInfoRightBottom()); + } + return {}; } QSize Message::performCountOptimalSize() { @@ -682,6 +810,16 @@ QSize Message::performCountOptimalSize() { RemoveComponents(Reply::Bit()); } + const auto factcheck = item->Get(); + if (factcheck && !factcheck->data.text.empty()) { + AddComponents(Factcheck::Bit()); + Get()->page = history()->session().factchecks().makeMedia( + this, + factcheck); + } else { + RemoveComponents(Factcheck::Bit()); + } + const auto markup = item->inlineReplyMarkup(); const auto reactionsKey = [&] { return embedReactionsInBottomInfo() @@ -695,6 +833,15 @@ QSize Message::performCountOptimalSize() { validateInlineKeyboard(markup); updateViewButtonExistence(); refreshTopicButton(); + + const auto media = this->media(); + const auto textItem = this->textItem(); + const auto defaultInvert = media && media->aboveTextByDefault(); + const auto invertDefault = textItem + && textItem->invertMedia() + && !textItem->emptyText(); + _invertMedia = invertDefault ? !defaultInvert : defaultInvert; + updateMediaInBubbleState(); if (oldKey != reactionsKey()) { refreshReactions(); @@ -702,7 +849,6 @@ QSize Message::performCountOptimalSize() { refreshRightBadge(); refreshInfoSkipBlock(); - const auto media = this->media(); const auto botTop = item->isFakeAboutView() ? Get() : nullptr; @@ -727,8 +873,9 @@ QSize Message::performCountOptimalSize() { const auto forwarded = item->Get(); const auto via = item->Get(); const auto entry = logEntryOriginal(); + const auto check = factcheckBlock(); if (forwarded) { - forwarded->create(via); + forwarded->create(via, item); } auto mediaDisplayed = false; @@ -736,15 +883,19 @@ QSize Message::performCountOptimalSize() { mediaDisplayed = media->isDisplayed(); media->initDimensions(); } + if (check) { + check->initDimensions(); + } if (entry) { entry->initDimensions(); } // Entry page is always a bubble bottom. const auto withVisibleText = hasVisibleText(); - auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || (entry/* && entry->isBubbleBottom()*/); + const auto textualWidth = textualMaxWidth(); + auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || check || (entry/* && entry->isBubbleBottom()*/); auto mediaOnTop = (mediaDisplayed && media->isBubbleTop()) || (entry && entry->isBubbleTop()); - maxWidth = plainMaxWidth(); + maxWidth = textualWidth; if (context() == Context::Replies && item->isDiscussionPost()) { maxWidth = std::max(maxWidth, st::msgMaxWidth); } @@ -783,6 +934,7 @@ QSize Message::performCountOptimalSize() { if (mediaDisplayed) minHeight += st::mediaInBubbleSkip; if (entry) minHeight += st::mediaInBubbleSkip; } + if (check) minHeight += st::mediaInBubbleSkip; if (mediaDisplayed) { // Parts don't participate in maxWidth() in case of media message. if (media->enforceBubbleWidth()) { @@ -794,7 +946,7 @@ QSize Message::performCountOptimalSize() { if (botTop) { minHeight += botTop->height; } - if (maxWidth < plainMaxWidth()) { + if (maxWidth < textualWidth) { minHeight -= text().minHeight(); minHeight += text().countHeight(innerWidth); } @@ -867,14 +1019,18 @@ QSize Message::performCountOptimalSize() { + st::msgPadding.right(); accumulate_max(maxWidth, replyw); } + if (check) { + accumulate_max(maxWidth, check->maxWidth()); + minHeight += check->minHeight(); + } if (entry) { accumulate_max(maxWidth, entry->maxWidth()); minHeight += entry->minHeight(); } - if (withVisibleText && botTop) { - accumulate_max(maxWidth, botTop->maxWidth); - minHeight += botTop->height; - } + } + if (withVisibleText && botTop) { + accumulate_max(maxWidth, botTop->maxWidth); + minHeight += botTop->height; } accumulate_max(maxWidth, minWidthForMedia()); } else if (media) { @@ -895,7 +1051,9 @@ QSize Message::performCountOptimalSize() { void Message::refreshTopicButton() { const auto item = data(); - if (isAttachedToPrevious() || context() != Context::History) { + if (isAttachedToPrevious() + || (context() != Context::History + && context() != Context::ChatPreview)) { _topicButton = nullptr; } else if (const auto topic = item->topic()) { if (!_topicButton) { @@ -957,6 +1115,10 @@ void Message::draw(Painter &p, const PaintContext &context) const { const auto item = data(); const auto media = this->media(); + if (item->hasUnrequestedFactcheck()) { + item->history()->session().factchecks().requestFor(item); + } + const auto stm = context.messageStyle(); const auto bubble = drawBubble(); @@ -986,11 +1148,12 @@ void Message::draw(Painter &p, const PaintContext &context) const { return; } - auto entry = logEntryOriginal(); + const auto entry = logEntryOriginal(); + const auto check = factcheckBlock(); auto mediaDisplayed = media && media->isDisplayed(); // Entry page is always a bubble bottom. - auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || (entry/* && entry->isBubbleBottom()*/); + auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || check || (entry/* && entry->isBubbleBottom()*/); auto mediaOnTop = (mediaDisplayed && media->isBubbleTop()) || (entry && entry->isBubbleTop()); const auto displayInfo = needInfoDisplay() && !AyuFeatures::MessageShot::ignoreRender(AyuFeatures::MessageShot::RenderPart::Date); @@ -1015,6 +1178,9 @@ void Message::draw(Painter &p, const PaintContext &context) const { if (!mediaOnBottom && (!_viewButton || !reactionsInBubble)) { localMediaBottom -= st::msgPadding.bottom(); } + if (check) { + localMediaBottom -= check->height(); + } if (entry) { localMediaBottom -= entry->height(); } @@ -1158,12 +1324,15 @@ void Message::draw(Painter &p, const PaintContext &context) const { paintFromName(p, trect, context); paintTopicButton(p, trect, context); paintForwardedInfo(p, trect, context); - paintReplyInfo(p, trect, context); paintViaBotIdInfo(p, trect, context); + paintReplyInfo(p, trect, context); } if (entry) { trect.setHeight(trect.height() - entry->height()); } + if (check) { + trect.setHeight(trect.height() - check->height() - st::mediaInBubbleSkip); + } if (displayInfo) { trect.setHeight(trect.height() - (_bottomInfo.height() - st::msgDateFont->height)); @@ -1223,6 +1392,19 @@ void Message::draw(Painter &p, const PaintContext &context) const { } } } + if (check) { + auto checkLeft = inner.left(); + auto checkTop = trect.y() + trect.height() + st::mediaInBubbleSkip; + p.translate(checkLeft, checkTop); + auto checkContext = context.translated(checkLeft, -checkTop); + checkContext.selection = skipTextSelection(context.selection); + if (mediaDisplayed) { + checkContext.selection = media->skipSelection( + checkContext.selection); + } + check->draw(p, checkContext); + p.translate(-checkLeft, -checkTop); + } if (entry) { auto entryLeft = inner.left(); auto entryTop = trect.y() + trect.height(); @@ -1802,10 +1984,11 @@ PointState Message::pointState(QPoint point) const { } if (const auto mediaDisplayed = media && media->isDisplayed()) { // Hack for grouped media point state. - auto entry = logEntryOriginal(); + const auto entry = logEntryOriginal(); + const auto check = factcheckBlock(); // Entry page is always a bubble bottom. - auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || (entry/* && entry->isBubbleBottom()*/); + auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || check || (entry/* && entry->isBubbleBottom()*/); if (item->repliesAreComments() || item->externalReply()) { g.setHeight(g.height() - st::historyCommentsButtonHeight); @@ -1837,6 +2020,10 @@ PointState Message::pointState(QPoint point) const { // if (getStateReplyInfo(point, trect, &result)) return result; // if (getStateViaBotIdInfo(point, trect, &result)) return result; //} + if (check) { + auto checkHeight = check->height(); + trect.setHeight(trect.height() - checkHeight - st::mediaInBubbleSkip); + } if (entry) { auto entryHeight = entry->height(); trect.setHeight(trect.height() - entryHeight); @@ -1873,6 +2060,9 @@ void Message::clickHandlerPressedChanged( } } Element::clickHandlerPressedChanged(handler, pressed); + if (const auto check = factcheckBlock()) { + check->clickHandlerPressedChanged(handler, pressed); + } if (!handler) { return; } else if (_rightAction && (handler == _rightAction->link)) { @@ -2113,6 +2303,7 @@ bool Message::hasFromPhoto() const { case Context::AdminLog: return true; case Context::History: + case Context::ChatPreview: case Context::TTLViewer: case Context::Pinned: case Context::Replies: @@ -2183,10 +2374,11 @@ TextState Message::textState( if (bubble) { const auto inBubble = g.contains(point); - auto entry = logEntryOriginal(); + const auto check = factcheckBlock(); + const auto entry = logEntryOriginal(); // Entry page is always a bubble bottom. - auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || (entry/* && entry->isBubbleBottom()*/); + auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || check || (entry/* && entry->isBubbleBottom()*/); auto mediaOnTop = (mediaDisplayed && media->isBubbleTop()) || (entry && entry->isBubbleTop()); auto inner = g; @@ -2270,9 +2462,23 @@ TextState Message::textState( + visibleMediaTextLength(); } } + if (check) { + auto checkHeight = check->height(); + trect.setHeight(trect.height() - checkHeight - st::mediaInBubbleSkip); + auto checkLeft = inner.left(); + auto checkTop = trect.y() + trect.height() + st::mediaInBubbleSkip; + if (point.y() >= checkTop && point.y() < checkTop + checkHeight) { + result = check->textState( + point - QPoint(checkLeft, checkTop), + request); + result.symbol += visibleTextLength() + + visibleMediaTextLength(); + } + } auto checkBottomInfoState = [&] { - if (mediaOnBottom && (entry || media->customInfoLayout())) { + if (mediaOnBottom + && (check || entry || media->customInfoLayout())) { return; } const auto bottomInfoResult = bottomInfoTextState( @@ -2739,6 +2945,7 @@ void Message::updatePressed(QPoint point) { TextForMimeData Message::selectedText(TextSelection selection) const { const auto media = this->media(); auto logEntryOriginalResult = TextForMimeData(); + auto factcheckResult = TextForMimeData(); const auto mediaDisplayed = (media && media->isDisplayed()); const auto mediaBefore = mediaDisplayed && invertMedia(); const auto textSelection = mediaBefore @@ -2753,7 +2960,15 @@ TextForMimeData Message::selectedText(TextSelection selection) const { auto mediaResult = (mediaDisplayed || isHiddenByGroup()) ? media->selectedText(mediaSelection) : TextForMimeData(); - if (auto entry = logEntryOriginal()) { + if (const auto check = factcheckBlock()) { + const auto checkSelection = mediaBefore + ? skipTextSelection(textSelection) + : mediaDisplayed + ? media->skipSelection(mediaSelection) + : skipTextSelection(selection); + factcheckResult = check->selectedText(checkSelection); + } + if (const auto entry = logEntryOriginal()) { const auto originalSelection = mediaBefore ? skipTextSelection(textSelection) : mediaDisplayed @@ -2769,6 +2984,11 @@ TextForMimeData Message::selectedText(TextSelection selection) const { } else if (!second.empty()) { result.append(u"\n\n"_q).append(std::move(second)); } + if (result.empty()) { + result = std::move(factcheckResult); + } else if (!factcheckResult.empty()) { + result.append(u"\n\n"_q).append(std::move(factcheckResult)); + } if (result.empty()) { result = std::move(logEntryOriginalResult); } else if (!logEntryOriginalResult.empty()) { @@ -2858,6 +3078,21 @@ TextSelection Message::adjustSelection( ? mediaAdjusted : unskipTextSelection(mediaAdjusted); } + auto checkResult = TextSelection(); + if (const auto check = factcheckBlock()) { + auto checkSelection = !mediaDisplayed + ? skipTextSelection(selection) + : mediaBefore + ? skipTextSelection(textSelection) + : media->skipSelection(mediaSelection); + auto checkAdjusted = useSelection(checkSelection, true) + ? check->adjustSelection(checkSelection, type) + : checkSelection; + checkResult = unskipTextSelection(checkAdjusted); + if (mediaDisplayed) { + checkResult = media->unskipSelection(checkResult); + } + } auto entryResult = TextSelection(); if (const auto entry = logEntryOriginal()) { auto entrySelection = !mediaDisplayed @@ -2880,6 +3115,12 @@ TextSelection Message::adjustSelection( std::max(result.to, mediaResult.to), }; } + if (!checkResult.empty()) { + result = result.empty() ? checkResult : TextSelection{ + std::min(result.from, checkResult.from), + std::max(result.to, checkResult.to), + }; + } if (!entryResult.empty()) { result = result.empty() ? entryResult : TextSelection{ std::min(result.from, entryResult.from), @@ -3020,7 +3261,7 @@ TextState Message::bottomInfoTextState( const auto infoLeft = infoRight - size.width(); const auto infoTop = infoBottom - size.height(); return _bottomInfo.textState( - data(), + this, point - QPoint{ infoLeft, infoTop }); } @@ -3044,6 +3285,8 @@ bool Message::isSignedAuthorElided() const { } bool Message::embedReactionsInBottomInfo() const { + return false; +#if 0 // legacy const auto item = data(); const auto user = item->history()->peer->asUser(); if (!user @@ -3076,6 +3319,7 @@ bool Message::embedReactionsInBottomInfo() const { } } return true; +#endif } bool Message::embedReactionsInBubble() const { @@ -3224,12 +3468,6 @@ void Message::refreshDataIdHook() { } } -int Message::plainMaxWidth() const { - return st::msgPadding.left() - + (hasVisibleText() ? text().maxWidth() : 0) - + st::msgPadding.right(); -} - int Message::monospaceMaxWidth() const { return st::msgPadding.left() + (hasVisibleText() ? text().countMaxMonospaceWidth() : 0) @@ -3284,6 +3522,13 @@ WebPage *Message::logEntryOriginal() const { return nullptr; } +WebPage *Message::factcheckBlock() const { + if (const auto entry = Get()) { + return entry->page.get(); + } + return nullptr; +} + bool Message::toggleSelectionByHandlerClick( const ClickHandlerPtr &handler) const { if (_comments && _comments->link == handler) { @@ -3313,6 +3558,7 @@ bool Message::hasFromName() const { case Context::AdminLog: return true; case Context::History: + case Context::ChatPreview: case Context::TTLViewer: case Context::Pinned: case Context::Replies: @@ -3412,7 +3658,9 @@ bool Message::drawBubble() const { const auto item = data(); if (isHidden()) { return false; - } else if (logEntryOriginal() || item->isFakeAboutView()) { + } else if (logEntryOriginal() + || factcheckBlock() + || item->isFakeAboutView()) { return true; } const auto media = this->media(); @@ -3433,7 +3681,7 @@ bool Message::unwrapped() const { const auto item = data(); if (isHidden()) { return true; - } else if (logEntryOriginal()) { + } else if (logEntryOriginal() || factcheckBlock()) { return false; } const auto media = this->media(); @@ -3796,8 +4044,22 @@ void Message::updateMediaInBubbleState() { || Has() || item->Has(); }; - auto entry = logEntryOriginal(); - if (entry) { + const auto entry = logEntryOriginal(); + const auto check = factcheckBlock(); + if (check) { + mediaHasSomethingBelow = true; + mediaHasSomethingAbove = getMediaHasSomethingAbove(); + auto checkState = (mediaHasSomethingAbove + || hasVisibleText() + || (media && media->isDisplayed())) + ? MediaInBubbleState::Bottom + : MediaInBubbleState::None; + check->setInBubbleState(checkState); + if (!media) { + check->setBubbleRounding(countBubbleRounding()); + return; + } + } else if (entry) { mediaHasSomethingBelow = true; mediaHasSomethingAbove = getMediaHasSomethingAbove(); auto entryState = (mediaHasSomethingAbove @@ -3822,7 +4084,7 @@ void Message::updateMediaInBubbleState() { return; } - if (!entry) { + if (!check && !entry) { mediaHasSomethingAbove = getMediaHasSomethingAbove(); } if (!invertMedia() && hasVisibleText()) { @@ -3917,17 +4179,17 @@ QRect Message::innerGeometry() const { + st::topicButtonPadding.bottom() + st::topicButtonSkip); } - // Skip displayForwardedFrom() until there are no animations for it. - if (const auto reply = Get()) { - // See paintReplyInfo(). - result.translate(0, reply->height()); - } if (!displayFromName() && !displayForwardedFrom()) { // See paintViaBotIdInfo(). if (data()->Has()) { result.translate(0, st::msgServiceNameFont->height); } } + // Skip displayForwardedFrom() until there are no animations for it. + if (const auto reply = Get()) { + // See paintReplyInfo(). + result.translate(0, reply->height()); + } } return result; } @@ -3959,7 +4221,7 @@ QRect Message::countGeometry() const { accumulate_min(contentWidth, maxWidth()); accumulate_min(contentWidth, int(_bubbleWidthLimit)); if (mediaWidth < contentWidth) { - const auto textualWidth = plainMaxWidth(); + const auto textualWidth = textualMaxWidth(); if (mediaWidth < textualWidth && (!media || !media->enforceBubbleWidth())) { accumulate_min(contentWidth, textualWidth); @@ -4070,7 +4332,7 @@ int Message::resizeContentGetHeight(int newWidth) { if (mediaDisplayed) { media->resizeGetHeight(contentWidth); if (media->width() < contentWidth) { - const auto textualWidth = plainMaxWidth(); + const auto textualWidth = textualMaxWidth(); if (media->width() < textualWidth && !media->enforceBubbleWidth()) { accumulate_min(contentWidth, textualWidth); @@ -4089,10 +4351,11 @@ int Message::resizeContentGetHeight(int newWidth) { if (bubble) { auto reply = Get(); auto via = item->Get(); - auto entry = logEntryOriginal(); + const auto check = factcheckBlock(); + const auto entry = logEntryOriginal(); // Entry page is always a bubble bottom. - auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || (entry/* && entry->isBubbleBottom()*/); + auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || check || (entry/* && entry->isBubbleBottom()*/); auto mediaOnTop = (mediaDisplayed && media->isBubbleTop()) || (entry && entry->isBubbleTop()); if (reactionsInBubble) { @@ -4101,12 +4364,20 @@ int Message::resizeContentGetHeight(int newWidth) { if (contentWidth == maxWidth()) { if (mediaDisplayed) { + if (check) { + newHeight += check->resizeGetHeight(contentWidth) + st::mediaInBubbleSkip; + } if (entry) { newHeight += entry->resizeGetHeight(contentWidth); } - } else if (entry) { - // In case of text-only message it is counted in minHeight already. - entry->resizeGetHeight(contentWidth); + } else { + if (check) { + check->resizeGetHeight(contentWidth); + } + if (entry) { + // In case of text-only message it is counted in minHeight already. + entry->resizeGetHeight(contentWidth); + } } } else { const auto withVisibleText = hasVisibleText(); @@ -4130,10 +4401,11 @@ int Message::resizeContentGetHeight(int newWidth) { } if (mediaDisplayed) { newHeight += media->height(); - if (entry) { - newHeight += entry->resizeGetHeight(contentWidth); - } - } else if (entry) { + } + if (check) { + newHeight += check->resizeGetHeight(contentWidth) + st::mediaInBubbleSkip; + } + if (entry) { newHeight += entry->resizeGetHeight(contentWidth); } if (reactionsInBubble) { @@ -4220,9 +4492,12 @@ int Message::resizeContentGetHeight(int newWidth) { bool Message::needInfoDisplay() const { const auto media = this->media(); const auto mediaDisplayed = media ? media->isDisplayed() : false; + const auto check = factcheckBlock(); const auto entry = logEntryOriginal(); return entry ? !entry->customInfoLayout() + : check + ? !check->customInfoLayout() : ((mediaDisplayed && media->isBubbleBottom()) ? !media->customInfoLayout() : true); @@ -4233,8 +4508,11 @@ bool Message::invertMedia() const { } bool Message::hasVisibleText() const { - if (data()->emptyText()) { - if (const auto media = data()->media()) { + const auto textItem = this->textItem(); + if (!textItem) { + return false; + } else if (textItem->emptyText()) { + if (const auto media = textItem->media()) { return media->storyExpired(); } return false; @@ -4269,7 +4547,8 @@ void Message::refreshInfoSkipBlock() { return media->storyExpired(); } return false; - } else if (item->Has()) { + } else if (item->Has() + || factcheckBlock()) { return false; } else if (media && media->isDisplayed() && !_invertMedia) { return false; diff --git a/Telegram/SourceFiles/history/view/history_view_message.h b/Telegram/SourceFiles/history/view/history_view_message.h index a84866e54..dc5f01ac1 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.h +++ b/Telegram/SourceFiles/history/view/history_view_message.h @@ -44,6 +44,12 @@ struct LogEntryOriginal std::unique_ptr page; }; +struct Factcheck +: public RuntimeComponent { + std::unique_ptr page; + bool expanded = false; +}; + struct PsaTooltipState : public RuntimeComponent { QString type; mutable ClickHandlerPtr link; @@ -158,6 +164,11 @@ public: Data::ReactionId, std::unique_ptr> override; + void animateEffect(Ui::ReactionFlyAnimationArgs &&args) override; + auto takeEffectAnimation() + -> std::unique_ptr override; + + QRect effectIconGeometry() const override; QRect innerGeometry() const override; [[nodiscard]] BottomRippleMask bottomRippleMask(int buttonHeight) const; @@ -276,7 +287,6 @@ private: void ensureRightAction() const; void refreshTopicButton(); void refreshInfoSkipBlock(); - [[nodiscard]] int plainMaxWidth() const; [[nodiscard]] int monospaceMaxWidth() const; void validateInlineKeyboard(HistoryMessageReplyMarkup *markup); @@ -284,6 +294,7 @@ private: [[nodiscard]] int viewButtonHeight() const; [[nodiscard]] WebPage *logEntryOriginal() const; + [[nodiscard]] WebPage *factcheckBlock() const; [[nodiscard]] ClickHandlerPtr createGoToCommentsLink() const; [[nodiscard]] ClickHandlerPtr psaTooltipLink() const; diff --git a/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp b/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp index 61495a93b..38b71c3ec 100644 --- a/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp @@ -98,6 +98,7 @@ PinnedWidget::PinnedWidget( not_null controller, not_null thread) : Window::SectionWidget(parent, controller, thread->peer()) +, WindowListDelegate(controller) , _thread(thread->migrateToOrMe()) , _history(thread->owningHistory()) , _migratedPeer(thread->asHistory() @@ -169,7 +170,7 @@ PinnedWidget::PinnedWidget( _inner = _scroll->setOwnedWidget(object_ptr( this, - controller, + &controller->session(), static_cast(this))); _scroll->move(0, _topBar->height()); _scroll->show(); diff --git a/Telegram/SourceFiles/history/view/history_view_pinned_section.h b/Telegram/SourceFiles/history/view/history_view_pinned_section.h index 88a592a1b..2c54e0684 100644 --- a/Telegram/SourceFiles/history/view/history_view_pinned_section.h +++ b/Telegram/SourceFiles/history/view/history_view_pinned_section.h @@ -36,7 +36,7 @@ class TranslateBar; class PinnedWidget final : public Window::SectionWidget - , private ListDelegate + , private WindowListDelegate , private CornerButtonsDelegate { public: PinnedWidget( diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp index f0b8d9990..4f208d51d 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp @@ -209,6 +209,7 @@ RepliesWidget::RepliesWidget( not_null history, MsgId rootId) : Window::SectionWidget(parent, controller, history->peer) +, WindowListDelegate(controller) , _history(history) , _rootId(rootId) , _root(lookupRoot()) @@ -227,9 +228,11 @@ RepliesWidget::RepliesWidget( listShowPremiumToast(emoji); }, .mode = ComposeControls::Mode::Normal, - .sendMenuType = _topic - ? SendMenu::Type::Scheduled - : SendMenu::Type::SilentOnly, + .sendMenuDetails = [=] { + using Type = SendMenu::Type; + const auto type = _topic ? Type::Scheduled : Type::SilentOnly; + return SendMenu::Details{ .type = type }; + }, .regularWindow = controller, .stickerOrEmojiChosen = controller->stickerOrEmojiChosen(), .scheduledToggleValue = _topic @@ -307,7 +310,7 @@ RepliesWidget::RepliesWidget( _inner = _scroll->setOwnedWidget(object_ptr( this, - controller, + &controller->session(), static_cast(this))); _scroll->move(0, _topBar->height()); _scroll->show(); @@ -331,14 +334,18 @@ RepliesWidget::RepliesWidget( }, _inner->lifetime()); _inner->replyToMessageRequested( - ) | rpl::start_with_next([=](auto fullId) { + ) | rpl::start_with_next([=](ListWidget::ReplyToMessageRequest request) { const auto canSendReply = _topic ? Data::CanSendAnything(_topic) : Data::CanSendAnything(_history->peer); - if (_joinGroup || !canSendReply) { - Controls::ShowReplyToChatBox(controller->uiShow(), { fullId }); - } else { - replyToMessage(fullId); + const auto &to = request.to; + const auto still = _history->owner().message(to.messageId); + const auto allowInAnotherChat = still && still->allowsForward(); + if (allowInAnotherChat + && (_joinGroup || !canSendReply || request.forceAnotherChat)) { + Controls::ShowReplyToChatBox(controller->uiShow(), { to }); + } else if (!_joinGroup && canSendReply) { + replyToMessage(to); _composeControls->focus(); } }, _inner->lifetime()); @@ -744,7 +751,7 @@ void RepliesWidget::setupComposeControls() { _composeControls->editRequests( ) | rpl::start_with_next([=](auto data) { if (const auto item = session().data().message(data.fullId)) { - const auto spoiler = data.spoilerMediaOverride; + const auto spoiler = data.spoilered; edit(item, data.options, saveEditMsgRequestId, spoiler); } }, lifetime()); @@ -956,7 +963,7 @@ bool RepliesWidget::confirmSendingFiles( _composeControls->getTextWithAppliedMarkdown(), _history->peer, Api::SendType::Normal, - SendMenu::Type::SilentOnly); // #TODO replies schedule + SendMenu::Details{ SendMenu::Type::SilentOnly }); // #TODO replies schedule box->setConfirmedCallback(crl::guard(this, [=]( Ui::PreparedList &&list, @@ -1217,7 +1224,7 @@ void RepliesWidget::edit( not_null item, Api::SendOptions options, mtpRequestId *const saveEditMsgRequestId, - std::optional spoilerMediaOverride) { + bool spoilered) { if (*saveEditMsgRequestId) { return; } @@ -1286,7 +1293,7 @@ void RepliesWidget::edit( options, crl::guard(this, done), crl::guard(this, fail), - spoilerMediaOverride); + spoilered); _composeControls->hidePanelsAnimated(); doSetInnerFocus(); @@ -1451,13 +1458,14 @@ void RepliesWidget::sendInlineResult( finishSending(); } -SendMenu::Type RepliesWidget::sendMenuType() const { +SendMenu::Details RepliesWidget::sendMenuDetails() const { // #TODO replies schedule - return _history->peer->isSelf() + const auto type = _history->peer->isSelf() ? SendMenu::Type::Reminder : HistoryView::CanScheduleUntilOnline(_history->peer) ? SendMenu::Type::ScheduledToUser : SendMenu::Type::Scheduled; + return { .type = type, .effectAllowed = _history->peer->isUser() }; } FullReplyTo RepliesWidget::replyTo() const { diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.h b/Telegram/SourceFiles/history/view/history_view_replies_section.h index 33b7859b4..37c8540fd 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.h +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.h @@ -19,7 +19,7 @@ enum class SendMediaType; struct SendingAlbum; namespace SendMenu { -enum class Type; +struct Details; } // namespace SendMenu namespace Api { @@ -71,7 +71,7 @@ class TranslateBar; class RepliesWidget final : public Window::SectionWidget - , private ListDelegate + , private WindowListDelegate , private CornerButtonsDelegate { public: RepliesWidget( @@ -247,9 +247,9 @@ private: not_null item, Api::SendOptions options, mtpRequestId *const saveEditMsgRequestId, - std::optional spoilerMediaOverride); + bool spoilered); void chooseAttach(std::optional overrideSendImagesAsPhotos); - [[nodiscard]] SendMenu::Type sendMenuType() const; + [[nodiscard]] SendMenu::Details sendMenuDetails() const; [[nodiscard]] FullReplyTo replyTo() const; [[nodiscard]] HistoryItem *lookupRoot() const; [[nodiscard]] Data::ForumTopic *lookupTopic(); diff --git a/Telegram/SourceFiles/history/view/history_view_schedule_box.cpp b/Telegram/SourceFiles/history/view/history_view_schedule_box.cpp index 136a3f73c..9ab0af794 100644 --- a/Telegram/SourceFiles/history/view/history_view_schedule_box.cpp +++ b/Telegram/SourceFiles/history/view/history_view_schedule_box.cpp @@ -69,46 +69,71 @@ bool CanScheduleUntilOnline(not_null peer) { void ScheduleBox( not_null box, - SendMenu::Type type, + std::shared_ptr show, + const Api::SendOptions &initialOptions, + const SendMenu::Details &details, Fn done, TimeId time, ScheduleBoxStyleArgs style) { - const auto save = [=](bool silent, TimeId scheduleDate) { - if (!scheduleDate) { + const auto submit = [=](Api::SendOptions options) { + if (!options.scheduled) { return; } - auto result = Api::SendOptions(); // Pro tip: Hold Ctrl key to send a silent scheduled message! - result.silent = silent || base::IsCtrlPressed(); - result.scheduled = scheduleDate; + if (base::IsCtrlPressed()) { + options.silent = true; + } const auto copy = done; box->closeBox(); - copy(result); + copy(options); + }; + const auto with = [=](TimeId scheduled) { + auto result = initialOptions; + result.scheduled = scheduled; + return result; }; auto descriptor = Ui::ChooseDateTimeBox(box, { - .title = (type == SendMenu::Type::Reminder + .title = (details.type == SendMenu::Type::Reminder ? tr::lng_remind_title() : tr::lng_schedule_title()), .submit = tr::lng_schedule_button(), - .done = [=](TimeId result) { save(false, result); }, + .done = [=](TimeId result) { submit(with(result)); }, .time = time, .style = style.chooseDateTimeArgs, }); - using T = SendMenu::Type; - SendMenu::SetupMenuAndShortcuts( - descriptor.submit.data(), - [t = type == T::Disabled ? T::Disabled : T::SilentOnly] { return t; }, - [=] { save(true, descriptor.collect()); }, - nullptr, - nullptr); + using namespace SendMenu; + const auto childType = (details.type == Type::Disabled) + ? Type::Disabled + : Type::SilentOnly; + const auto childDetails = Details{ + .type = childType, + .effectAllowed = details.effectAllowed, + }; + const auto sendAction = crl::guard(box, [=](Action action, Details) { + Expects(action.type == ActionType::Send); - if (type == SendMenu::Type::ScheduledToUser) { + auto options = with(descriptor.collect()); + if (action.options.silent) { + options.silent = action.options.silent; + } + if (action.options.effectId) { + options.effectId = action.options.effectId; + } + submit(options); + }); + SetupMenuAndShortcuts( + descriptor.submit.data(), + show, + [=] { return childDetails; }, + sendAction); + + if (details.type == Type::ScheduledToUser) { const auto sendUntilOnline = box->addTopButton(*style.topButtonStyle); const auto timestamp = Api::kScheduledUntilOnlineTimestamp; FillSendUntilOnlineMenu( sendUntilOnline.data(), - [=] { save(false, timestamp); }, + [=] { submit(with(timestamp)); }, style); } } diff --git a/Telegram/SourceFiles/history/view/history_view_schedule_box.h b/Telegram/SourceFiles/history/view/history_view_schedule_box.h index 5d72ed05f..5f5385c73 100644 --- a/Telegram/SourceFiles/history/view/history_view_schedule_box.h +++ b/Telegram/SourceFiles/history/view/history_view_schedule_box.h @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "api/api_common.h" #include "ui/boxes/choose_date_time.h" namespace style { @@ -14,12 +15,12 @@ struct IconButton; struct PopupMenu; } // namespace style -namespace Api { -struct SendOptions; -} // namespace Api +namespace ChatHelpers { +class Show; +} // namespace ChatHelpers namespace SendMenu { -enum class Type; +struct Details; } // namespace SendMenu namespace HistoryView { @@ -36,7 +37,9 @@ struct ScheduleBoxStyleArgs { void ScheduleBox( not_null box, - SendMenu::Type type, + std::shared_ptr show, + const Api::SendOptions &initialOptions, + const SendMenu::Details &details, Fn done, TimeId time, ScheduleBoxStyleArgs style); @@ -44,13 +47,17 @@ void ScheduleBox( template [[nodiscard]] object_ptr PrepareScheduleBox( Guard &&guard, - SendMenu::Type type, + std::shared_ptr show, + const SendMenu::Details &details, Submit &&submit, + const Api::SendOptions &initialOptions = {}, TimeId scheduleTime = DefaultScheduleTime(), ScheduleBoxStyleArgs style = ScheduleBoxStyleArgs()) { return Box( ScheduleBox, - type, + std::move(show), + initialOptions, + details, crl::guard(std::forward(guard), std::forward(submit)), scheduleTime, std::move(style)); diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp index 3a02b72fb..124db7535 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp @@ -101,6 +101,8 @@ ScheduledWidget::ScheduledWidget( not_null history, const Data::ForumTopic *forumTopic) : Window::SectionWidget(parent, controller, history->peer) +, WindowListDelegate(controller) +, _show(controller->uiShow()) , _history(history) , _forumTopic(forumTopic) , _scroll( @@ -117,7 +119,7 @@ ScheduledWidget::ScheduledWidget( listShowPremiumToast(emoji); }, .mode = ComposeControls::Mode::Scheduled, - .sendMenuType = SendMenu::Type::Disabled, + .sendMenuDetails = [] { return SendMenu::Details(); }, .regularWindow = controller, .stickerOrEmojiChosen = controller->stickerOrEmojiChosen(), })) @@ -175,7 +177,7 @@ ScheduledWidget::ScheduledWidget( _inner = _scroll->setOwnedWidget(object_ptr( this, - controller, + &controller->session(), static_cast(this))); _scroll->move(0, _topBar->height()); _scroll->show(); @@ -326,7 +328,7 @@ void ScheduledWidget::setupComposeControls() { ) | rpl::start_with_next([=](auto data) { if (const auto item = session().data().message(data.fullId)) { if (item->isScheduled()) { - const auto spoiler = data.spoilerMediaOverride; + const auto spoiler = data.spoilered; edit(item, data.options, saveEditMsgRequestId, spoiler); } } @@ -499,7 +501,7 @@ bool ScheduledWidget::confirmSendingFiles( (CanScheduleUntilOnline(_history->peer) ? Api::SendType::ScheduledToUser : Api::SendType::Scheduled), - SendMenu::Type::Disabled); + SendMenu::Details()); box->setConfirmedCallback(crl::guard(this, [=]( Ui::PreparedList &&list, @@ -607,7 +609,8 @@ void ScheduledWidget::uploadFile( type, prepareSendAction(options)); }; - controller()->show(PrepareScheduleBox(this, sendMenuType(), callback)); + controller()->show( + PrepareScheduleBox(this, _show, sendMenuDetails(), callback)); } bool ScheduledWidget::showSendingFilesError( @@ -685,7 +688,8 @@ void ScheduledWidget::send() { return; } const auto callback = [=](Api::SendOptions options) { send(options); }; - controller()->show(PrepareScheduleBox(this, sendMenuType(), callback)); + controller()->show( + PrepareScheduleBox(this, _show, sendMenuDetails(), callback)); } void ScheduledWidget::send(Api::SendOptions options) { @@ -716,7 +720,8 @@ void ScheduledWidget::sendVoice( const auto callback = [=](Api::SendOptions options) { sendVoice(bytes, waveform, duration, options); }; - controller()->show(PrepareScheduleBox(this, sendMenuType(), callback)); + controller()->show( + PrepareScheduleBox(this, _show, sendMenuDetails(), callback)); } void ScheduledWidget::sendVoice( @@ -736,7 +741,7 @@ void ScheduledWidget::edit( not_null item, Api::SendOptions options, mtpRequestId *const saveEditMsgRequestId, - std::optional spoilerMediaOverride) { + bool spoilered) { if (*saveEditMsgRequestId) { return; } @@ -805,7 +810,7 @@ void ScheduledWidget::edit( options, crl::guard(this, done), crl::guard(this, fail), - spoilerMediaOverride); + spoilered); _composeControls->hidePanelsAnimated(); _composeControls->focus(); @@ -816,7 +821,8 @@ void ScheduledWidget::sendExistingDocument( const auto callback = [=](Api::SendOptions options) { sendExistingDocument(document, options); }; - controller()->show(PrepareScheduleBox(this, sendMenuType(), callback)); + controller()->show( + PrepareScheduleBox(this, _show, sendMenuDetails(), callback)); } bool ScheduledWidget::sendExistingDocument( @@ -845,7 +851,8 @@ void ScheduledWidget::sendExistingPhoto(not_null photo) { const auto callback = [=](Api::SendOptions options) { sendExistingPhoto(photo, options); }; - controller()->show(PrepareScheduleBox(this, sendMenuType(), callback)); + controller()->show( + PrepareScheduleBox(this, _show, sendMenuDetails(), callback)); } bool ScheduledWidget::sendExistingPhoto( @@ -879,7 +886,8 @@ void ScheduledWidget::sendInlineResult( const auto callback = [=](Api::SendOptions options) { sendInlineResult(result, bot, options); }; - controller()->show(PrepareScheduleBox(this, sendMenuType(), callback)); + controller()->show( + PrepareScheduleBox(this, _show, sendMenuDetails(), callback)); } void ScheduledWidget::sendInlineResult( @@ -911,12 +919,14 @@ void ScheduledWidget::sendInlineResult( _composeControls->focus(); } -SendMenu::Type ScheduledWidget::sendMenuType() const { - return _history->peer->isSelf() +SendMenu::Details ScheduledWidget::sendMenuDetails() const { + const auto type = _history->peer->isSelf() ? SendMenu::Type::Reminder : HistoryView::CanScheduleUntilOnline(_history->peer) ? SendMenu::Type::ScheduledToUser : SendMenu::Type::Scheduled; + const auto effectAllowed = _history->peer->isUser(); + return { .type = type, .effectAllowed = effectAllowed }; } void ScheduledWidget::cornerButtonsShowAtPosition( @@ -1367,7 +1377,8 @@ void ScheduledWidget::listSendBotCommand( message.textWithTags = { text }; session().api().sendMessage(std::move(message)); }; - controller()->show(PrepareScheduleBox(this, sendMenuType(), callback)); + controller()->show( + PrepareScheduleBox(this, _show, sendMenuDetails(), callback)); } void ScheduledWidget::listSearch( diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.h b/Telegram/SourceFiles/history/view/history_view_scheduled_section.h index 6685c113a..b2b0efa41 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.h +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.h @@ -17,8 +17,12 @@ class History; enum class SendMediaType; struct SendingAlbum; +namespace ChatHelpers { +class Show; +} // namespace ChatHelpers + namespace SendMenu { -enum class Type; +struct Details; } // namespace SendMenu namespace Api { @@ -52,7 +56,7 @@ class StickerToast; class ScheduledWidget final : public Window::SectionWidget - , private ListDelegate + , private WindowListDelegate , private CornerButtonsDelegate { public: ScheduledWidget( @@ -214,10 +218,10 @@ private: not_null item, Api::SendOptions options, mtpRequestId *const saveEditMsgRequestId, - std::optional spoilerMediaOverride); + bool spoilered); void highlightSingleNewMessage(const Data::MessagesSlice &slice); void chooseAttach(); - [[nodiscard]] SendMenu::Type sendMenuType() const; + [[nodiscard]] SendMenu::Details sendMenuDetails() const; void pushReplyReturn(not_null item); void checkReplyReturns(); @@ -262,6 +266,7 @@ private: not_null bot, Api::SendOptions options); + const std::shared_ptr _show; const not_null _history; const Data::ForumTopic *_forumTopic; std::shared_ptr _theme; diff --git a/Telegram/SourceFiles/history/view/history_view_sublist_section.cpp b/Telegram/SourceFiles/history/view/history_view_sublist_section.cpp index 001ed666e..470643bdc 100644 --- a/Telegram/SourceFiles/history/view/history_view_sublist_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_sublist_section.cpp @@ -71,6 +71,7 @@ SublistWidget::SublistWidget( not_null controller, not_null sublist) : Window::SectionWidget(parent, controller, sublist->peer()) +, WindowListDelegate(controller) , _sublist(sublist) , _history(sublist->owner().history(sublist->session().user())) , _topBar(this, controller) @@ -141,7 +142,7 @@ SublistWidget::SublistWidget( _inner = _scroll->setOwnedWidget(object_ptr( this, - controller, + &controller->session(), static_cast(this))); _scroll->move(0, _topBar->height()); _scroll->show(); diff --git a/Telegram/SourceFiles/history/view/history_view_sublist_section.h b/Telegram/SourceFiles/history/view/history_view_sublist_section.h index 7ce880439..eff71d50c 100644 --- a/Telegram/SourceFiles/history/view/history_view_sublist_section.h +++ b/Telegram/SourceFiles/history/view/history_view_sublist_section.h @@ -37,7 +37,7 @@ class ComposeSearch; class SublistWidget final : public Window::SectionWidget - , private ListDelegate + , private WindowListDelegate , private CornerButtonsDelegate { public: SublistWidget( diff --git a/Telegram/SourceFiles/history/view/history_view_spoiler_click_handler.cpp b/Telegram/SourceFiles/history/view/history_view_text_helper.cpp similarity index 68% rename from Telegram/SourceFiles/history/view/history_view_spoiler_click_handler.cpp rename to Telegram/SourceFiles/history/view/history_view_text_helper.cpp index eb72db0f2..fbe97ed56 100644 --- a/Telegram/SourceFiles/history/view/history_view_spoiler_click_handler.cpp +++ b/Telegram/SourceFiles/history/view/history_view_text_helper.cpp @@ -5,7 +5,7 @@ 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_spoiler_click_handler.h" +#include "history/view/history_view_text_helper.h" #include "data/data_session.h" #include "history/view/history_view_element.h" @@ -15,9 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace HistoryView { -void FillTextWithAnimatedSpoilers( - not_null view, - Ui::Text::String &text) { +void InitElementTextPart(not_null view, Ui::Text::String &text) { if (text.hasSpoilers()) { text.setSpoilerLinkFilter([weak = base::make_weak(view)]( const ClickContext &context) { @@ -30,6 +28,14 @@ void FillTextWithAnimatedSpoilers( return true; }); } + if (text.hasCollapsedBlockquots()) { + const auto weak = base::make_weak(view); + text.setBlockquoteExpandCallback([=](int quoteIndex, bool expanded) { + if (const auto view = weak.get()) { + view->blockquoteExpandChanged(); + } + }); + } } } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_spoiler_click_handler.h b/Telegram/SourceFiles/history/view/history_view_text_helper.h similarity index 78% rename from Telegram/SourceFiles/history/view/history_view_spoiler_click_handler.h rename to Telegram/SourceFiles/history/view/history_view_text_helper.h index 3bef398b6..d7a604b53 100644 --- a/Telegram/SourceFiles/history/view/history_view_spoiler_click_handler.h +++ b/Telegram/SourceFiles/history/view/history_view_text_helper.h @@ -11,8 +11,6 @@ namespace HistoryView { class Element; -void FillTextWithAnimatedSpoilers( - not_null view, - Ui::Text::String &text); +void InitElementTextPart(not_null view, Ui::Text::String &text); } // 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 f5e059c61..806a41ec9 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp @@ -1504,15 +1504,25 @@ QString TopBarWidget::searchQueryCurrent() const { return _searchQuery.current(); } +int TopBarWidget::searchQueryCursorPosition() const { + return _searchMode + ? _searchField->textCursor().position() + : _searchQuery.current().size(); +} + void TopBarWidget::searchClear() { if (_searchMode) { _searchField->clear(); } } -void TopBarWidget::searchSetText(const QString &query) { +void TopBarWidget::searchSetText(const QString &query, int cursorPosition) { if (_searchMode) { + if (cursorPosition < 0) { + cursorPosition = query.size(); + } _searchField->setText(query); + _searchField->setCursorPosition(cursorPosition); } } diff --git a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.h b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.h index fc8045868..408bcd39d 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.h +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.h @@ -88,8 +88,9 @@ public: [[nodiscard]] rpl::producer<> searchSubmitted() const; [[nodiscard]] rpl::producer searchQuery() const; [[nodiscard]] QString searchQueryCurrent() const; + [[nodiscard]] int searchQueryCursorPosition() const; void searchClear(); - void searchSetText(const QString &query); + void searchSetText(const QString &query, int cursorPosition = -1); [[nodiscard]] rpl::producer<> forwardSelectionRequest() const { return _forwardSelection.events(); diff --git a/Telegram/SourceFiles/history/view/media/history_view_contact.cpp b/Telegram/SourceFiles/history/view/media/history_view_contact.cpp index 1e6654abd..d0fbfe730 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_contact.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_contact.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/add_contact_box.h" #include "core/click_handler_types.h" // ClickHandlerContext +#include "data/data_media_types.h" #include "data/data_session.h" #include "data/data_user.h" #include "history/history.h" @@ -18,16 +19,20 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/media/history_view_media_common.h" #include "lang/lang_keys.h" #include "main/main_session.h" -#include "styles/style_boxes.h" -#include "styles/style_chat.h" #include "ui/chat/chat_style.h" #include "ui/empty_userpic.h" +#include "ui/layers/generic_box.h" #include "ui/painter.h" #include "ui/power_saving.h" #include "ui/rect.h" #include "ui/text/format_values.h" // Ui::FormatPhone #include "ui/text/text_options.h" +#include "ui/text/text_utilities.h" // Ui::Text::Wrapped. +#include "ui/vertical_list.h" #include "window/window_session_controller.h" +#include "styles/style_boxes.h" +#include "styles/style_chat.h" +#include "styles/style_layers.h" namespace HistoryView { namespace { @@ -103,33 +108,116 @@ ClickHandlerPtr AddContactClickHandler(not_null item) { return clickHandlerPtr; } +[[nodiscard]] Fn)> VcardBoxFactory( + const Data::SharedContact::VcardItems &vcardItems) { + if (vcardItems.empty()) { + return nullptr; + } + return [=](not_null box) { + box->setTitle(tr::lng_contact_details_title()); + const auto &stL = st::proxyApplyBoxLabel; + const auto &stSubL = st::boxDividerLabel; + const auto add = [&](const QString &s, tr::phrase<> phrase) { + if (!s.isEmpty()) { + const auto label = box->addRow( + object_ptr(box, s, stL)); + box->addRow(object_ptr(box, phrase(), stSubL)); + Ui::AddSkip(box->verticalLayout()); + Ui::AddSkip(box->verticalLayout()); + return label; + } + return (Ui::FlatLabel*)(nullptr); + }; + for (const auto &[type, value] : vcardItems) { + using Type = Data::SharedContact::VcardItemType; + const auto isPhoneType = (type == Type::Phone) + || (type == Type::PhoneMain) + || (type == Type::PhoneHome) + || (type == Type::PhoneMobile) + || (type == Type::PhoneWork) + || (type == Type::PhoneOther); + const auto typePhrase = (type == Type::Phone) + ? tr::lng_contact_details_phone + : (type == Type::PhoneMain) + ? tr::lng_contact_details_phone_main + : (type == Type::PhoneHome) + ? tr::lng_contact_details_phone_home + : (type == Type::PhoneMobile) + ? tr::lng_contact_details_phone_mobile + : (type == Type::PhoneWork) + ? tr::lng_contact_details_phone_work + : (type == Type::PhoneOther) + ? tr::lng_contact_details_phone_other + : (type == Type::Email) + ? tr::lng_contact_details_email + : (type == Type::Address) + ? tr::lng_contact_details_address + : (type == Type::Url) + ? tr::lng_contact_details_url + : (type == Type::Note) + ? tr::lng_contact_details_note + : (type == Type::Birthday) + ? tr::lng_contact_details_birthday + : (type == Type::Organization) + ? tr::lng_contact_details_organization + : tr::lng_payments_info_name; + if (const auto label = add(value, typePhrase)) { + const auto copyText = isPhoneType + ? tr::lng_profile_copy_phone + : (type == Type::Email) + ? tr::lng_context_copy_email + : (type == Type::Url) + ? tr::lng_context_copy_link + : (type == Type::Name) + ? tr::lng_profile_copy_fullname + : tr::lng_context_copy_text; + label->setContextCopyText(copyText(tr::now)); + if (type == Type::Email) { + label->setMarkedText( + Ui::Text::Wrapped({ value }, EntityType::Email)); + } else if (type == Type::Url) { + label->setMarkedText( + Ui::Text::Wrapped({ value }, EntityType::Url)); + } else if (isPhoneType) { + label->setText(Ui::FormatPhone(value)); + } + using Request = Ui::FlatLabel::ContextMenuRequest; + label->setContextMenuHook([=](Request r) { + label->fillContextMenu(r.link + ? r + : Request{ .menu = r.menu, .fullSelection = true }); + }); + } + } + box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); + }; +} + } // namespace Contact::Contact( not_null parent, - UserId userId, - const QString &first, - const QString &last, - const QString &phone) + const Data::SharedContact &data) : Media(parent) , _st(st::historyPagePreview) , _pixh(st::contactsPhotoSize) -, _userId(userId) { - history()->owner().registerContactView(userId, parent); +, _userId(data.userId) +, _vcardBoxFactory(VcardBoxFactory(data.vcardItems)) { + history()->owner().registerContactView(data.userId, parent); _nameLine.setText( st::webPageTitleStyle, tr::lng_full_name( tr::now, lt_first_name, - first, + data.firstName, lt_last_name, - last).trimmed(), + data.lastName).trimmed(), Ui::WebpageTextTitleOptions()); _phoneLine.setText( st::webPageDescriptionStyle, - Ui::FormatPhone(phone), + Ui::FormatPhone(data.phoneNumber), Ui::WebpageTextTitleOptions()); #if 0 // No info. @@ -188,16 +276,22 @@ QSize Contact::countOptimalSize() { }); } _mainButton.link = _buttons.front().link; - } else { -#if 0 // Can't view contact. - const auto view = tr::lng_profile_add_contact(tr::now).toUpper(); + } else if (const auto vcardBoxFactory = _vcardBoxFactory) { + const auto view = tr::lng_contact_details_button(tr::now).toUpper(); _buttons.push_back({ view, st::semiboldFont->width(view), AddContactClickHandler(_parent->data()), }); -#endif - _mainButton.link = nullptr; + _mainButton.link = std::make_shared([=]( + const ClickContext &context) { + const auto my = context.other.value(); + if (const auto controller = my.sessionWindow.get()) { + controller->uiShow()->show(Box([=](not_null box) { + vcardBoxFactory(box); + })); + } + }); } const auto padding = inBubblePadding() + innerMargin(); diff --git a/Telegram/SourceFiles/history/view/media/history_view_contact.h b/Telegram/SourceFiles/history/view/media/history_view_contact.h index 2dd665b94..ada587721 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_contact.h +++ b/Telegram/SourceFiles/history/view/media/history_view_contact.h @@ -10,8 +10,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/media/history_view_media.h" #include "ui/userpic_view.h" +namespace Data { +struct SharedContact; +} // namespace Data + namespace Ui { class EmptyUserpic; +class GenericBox; class RippleAnimation; } // namespace Ui @@ -21,10 +26,7 @@ class Contact final : public Media { public: Contact( not_null parent, - UserId userId, - const QString &first, - const QString &last, - const QString &phone); + const Data::SharedContact &data); ~Contact(); void draw(Painter &p, const PaintContext &context) const override; @@ -76,6 +78,8 @@ private: Ui::Text::String _phoneLine; Ui::Text::String _infoLine; + Fn)> _vcardBoxFactory; + struct Button { QString text; int width = 0; diff --git a/Telegram/SourceFiles/history/view/media/history_view_document.cpp b/Telegram/SourceFiles/history/view/media/history_view_document.cpp index b00df78b4..ad0d43bbc 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_document.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_document.cpp @@ -304,9 +304,7 @@ Document::Document( _transcribedRound = entry.shown; } - auto caption = createCaption(); - - createComponents(!caption.isEmpty()); + createComponents(); if (const auto named = Get()) { fillNamedFromData(named); _tooltipFilename.setTooltipText(named->name); @@ -353,10 +351,6 @@ Document::Document( } setStatusSize(Ui::FileStatusSizeReady); - - if (const auto captioned = Get()) { - captioned->caption = std::move(caption); - } } Document::~Document() { @@ -381,7 +375,7 @@ bool Document::dataLoaded() const { return _dataMedia->loaded(); } -void Document::createComponents(bool caption) { +void Document::createComponents() { uint64 mask = 0; if (_data->isVoiceMessage() || _transcribedRound) { mask |= HistoryDocumentVoice::Bit(); @@ -392,9 +386,6 @@ void Document::createComponents(bool caption) { mask |= HistoryDocumentThumbed::Bit(); } } - if (caption) { - mask |= HistoryDocumentCaptioned::Bit(); - } UpdateComponents(mask); if (const auto thumbed = Get()) { thumbed->linksavel = std::make_shared( @@ -428,18 +419,6 @@ void Document::fillNamedFromData(not_null named) { } QSize Document::countOptimalSize() { - auto captioned = Get(); - if (_parent->media() != this && !_realParent->groupId()) { - if (captioned) { - RemoveComponents(HistoryDocumentCaptioned::Bit()); - captioned = nullptr; - } - } else if (captioned && captioned->caption.hasSkipBlock()) { - captioned->caption.updateSkipBlock( - _parent->skipBlockWidth(), - _parent->skipBlockHeight()); - } - auto hasTranscribe = false; const auto voice = Get(); if (voice) { @@ -488,7 +467,7 @@ QSize Document::countOptimalSize() { st::messageTextStyle, text); hasTranscribe = true; - if (const auto skipBlockWidth = captioned + if (const auto skipBlockWidth = _parent->hasVisibleText() ? 0 : _parent->skipBlockWidth()) { voice->transcribeText.updateSkipBlock( @@ -535,7 +514,7 @@ QSize Document::countOptimalSize() { } auto minHeight = st.padding.top() + st.thumbSize + st.padding.bottom(); - if (!captioned && !hasTranscribe && _parent->bottomInfoIsWide()) { + if (isBubbleBottom() && !hasTranscribe && _parent->bottomInfoIsWide()) { minHeight += st::msgDateFont->height - st::msgDateDelta.y(); } if (!isBubbleTop()) { @@ -547,17 +526,6 @@ QSize Document::countOptimalSize() { - st::msgPadding.left() - st::msgPadding.right(); minHeight += voice->transcribeText.countHeight(captionw); - if (captioned) { - minHeight += st::mediaCaptionSkip; - } else if (isBubbleBottom()) { - minHeight += st::msgPadding.bottom(); - } - } - if (captioned) { - auto captionw = maxWidth - - st::msgPadding.left() - - st::msgPadding.right(); - minHeight += captioned->caption.countHeight(captionw); if (isBubbleBottom()) { minHeight += st::msgPadding.bottom(); } @@ -1528,10 +1496,36 @@ QMargins Document::bubbleMargins() const { return QMargins(padding.left(), padding.top(), padding.right(), padding.bottom()); } -QSize Document::sizeForGroupingOptimal(int maxWidth) const { +void Document::refreshCaption(bool last) { + const auto now = Get(); + auto caption = createCaption(); + if (!caption.isEmpty()) { + if (now) { + return; + } + AddComponents(HistoryDocumentCaptioned::Bit()); + auto captioned = Get(); + captioned->caption = std::move(caption); + const auto skip = last ? _parent->skipBlockWidth() : 0; + if (skip) { + captioned->caption.updateSkipBlock( + _parent->skipBlockWidth(), + _parent->skipBlockHeight()); + } else { + captioned->caption.removeSkipBlock(); + } + } else if (now) { + RemoveComponents(HistoryDocumentCaptioned::Bit()); + } +} + +QSize Document::sizeForGroupingOptimal(int maxWidth, bool last) const { const auto thumbed = Get(); const auto &st = (thumbed ? st::msgFileThumbLayoutGrouped : st::msgFileLayoutGrouped); auto height = st.padding.top() + st.thumbSize + st.padding.bottom(); + + const_cast(this)->refreshCaption(last); + if (const auto captioned = Get()) { auto captionw = maxWidth - st::msgPadding.left() @@ -1654,24 +1648,7 @@ void Document::refreshParentId(not_null realParent) { } void Document::parentTextUpdated() { - auto caption = (_parent->media() == this || _realParent->groupId()) - ? createCaption() - : Ui::Text::String(); - if (!caption.isEmpty()) { - AddComponents(HistoryDocumentCaptioned::Bit()); - auto captioned = Get(); - captioned->caption = std::move(caption); - } else { - RemoveComponents(HistoryDocumentCaptioned::Bit()); - } - history()->owner().requestViewResize(_parent); -} - -TextWithEntities Document::getCaption() const { - if (const auto captioned = Get()) { - return captioned->caption.toTextWithEntities(); - } - return TextWithEntities(); + RemoveComponents(HistoryDocumentCaptioned::Bit()); } void Document::hideSpoilers() { @@ -1680,7 +1657,7 @@ void Document::hideSpoilers() { } } -Ui::Text::String Document::createCaption() { +Ui::Text::String Document::createCaption() const { return File::createCaption(_realParent); } diff --git a/Telegram/SourceFiles/history/view/media/history_view_document.h b/Telegram/SourceFiles/history/view/media/history_view_document.h index 3d29651c4..2f8ec9ac2 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_document.h +++ b/Telegram/SourceFiles/history/view/media/history_view_document.h @@ -35,6 +35,10 @@ public: not_null document); ~Document(); + bool hideMessageText() const override { + return false; + } + void draw(Painter &p, const PaintContext &context) const override; TextState textState(QPoint point, StateRequest request) const override; void updatePressed(QPoint point) override; @@ -56,7 +60,6 @@ public: return _data; } - TextWithEntities getCaption() const override; void hideSpoilers() override; bool needsBubble() const override { return true; @@ -66,7 +69,7 @@ public: } QMargins bubbleMargins() const override; - QSize sizeForGroupingOptimal(int maxWidth) const override; + QSize sizeForGroupingOptimal(int maxWidth, bool last) const override; QSize sizeForGrouping(int width) const override; void drawGrouped( Painter &p, @@ -117,12 +120,13 @@ private: LayoutMode mode) const; void ensureDataMediaCreated() const; - [[nodiscard]] Ui::Text::String createCaption(); + [[nodiscard]] Ui::Text::String createCaption() const; QSize countOptimalSize() override; QSize countCurrentSize(int newWidth) override; - void createComponents(bool caption); + void refreshCaption(bool last); + void createComponents(); void fillNamedFromData(not_null named); [[nodiscard]] Ui::BubbleRounding thumbRounding( diff --git a/Telegram/SourceFiles/history/view/media/history_view_extended_preview.cpp b/Telegram/SourceFiles/history/view/media/history_view_extended_preview.cpp index d127fc4fc..582e95265 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_extended_preview.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_extended_preview.cpp @@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/power_saving.h" #include "data/data_session.h" #include "payments/payments_checkout_process.h" +#include "payments/payments_non_panel_process.h" #include "window/window_session_controller.h" #include "mainwindow.h" #include "core/click_handler_types.h" @@ -41,7 +42,12 @@ namespace { ? crl::guard( controller, [=](auto) { controller->widget()->activate(); }) - : Fn())); + : Fn()), + (controller + ? Payments::ProcessNonPanelPaymentFormFactory( + controller, + item) + : nullptr)); }); } @@ -51,10 +57,8 @@ ExtendedPreview::ExtendedPreview( not_null parent, not_null invoice) : Media(parent) -, _invoice(invoice) -, _caption(st::minPhotoSize - st::msgPadding.left() - st::msgPadding.right()) { +, _invoice(invoice) { const auto item = parent->data(); - _caption = createCaption(item); _spoiler.link = MakeInvoiceLink(item); resolveButtonText(); } @@ -107,17 +111,9 @@ void ExtendedPreview::unloadHeavyPart() { = _spoiler.cornerCache = _buttonBackground = QImage(); _spoiler.animation = nullptr; - _caption.unloadPersistentAnimation(); } QSize ExtendedPreview::countOptimalSize() { - if (_parent->media() != this) { - _caption = Ui::Text::String(); - } else if (_caption.hasSkipBlock()) { - _caption.updateSkipBlock( - _parent->skipBlockWidth(), - _parent->skipBlockHeight()); - } const auto &preview = _invoice->extendedPreview; const auto dimensions = preview.dimensions; const auto minWidth = std::min( @@ -135,15 +131,6 @@ QSize ExtendedPreview::countOptimalSize() { if (preview.videoDuration < 0) { accumulate_max(maxWidth, scaled.height()); } - if (_parent->hasBubble() && !_caption.isEmpty()) { - maxWidth = qMax(maxWidth, st::msgPadding.left() - + _caption.maxWidth() - + st::msgPadding.right()); - minHeight += st::mediaCaptionSkip + _caption.minHeight(); - if (isBubbleBottom()) { - minHeight += st::msgPadding.bottom(); - } - } return { maxWidth, minHeight }; } @@ -151,7 +138,7 @@ QSize ExtendedPreview::countCurrentSize(int newWidth) { const auto &preview = _invoice->extendedPreview; const auto dimensions = preview.dimensions; const auto thumbMaxWidth = std::min(newWidth, st::maxMediaSize); - const auto minWidth = std::min( + const auto minWidth = std::min( std::max({ _parent->minWidthForMedia(), (_parent->hasBubble() @@ -170,20 +157,14 @@ QSize ExtendedPreview::countCurrentSize(int newWidth) { maxWidth()); newWidth = qMax(scaled.width(), minWidth); auto newHeight = qMax(scaled.height(), st::minPhotoSize); - if (_parent->hasBubble() && !_caption.isEmpty()) { + if (_parent->hasBubble()) { const auto maxWithCaption = qMin( st::msgMaxWidth, - (st::msgPadding.left() - + _caption.maxWidth() - + st::msgPadding.right())); + _parent->textualMaxWidth()); newWidth = qMin(qMax(newWidth, maxWithCaption), thumbMaxWidth); - const auto captionw = newWidth - - st::msgPadding.left() - - st::msgPadding.right(); - newHeight += st::mediaCaptionSkip + _caption.countHeight(captionw); - if (isBubbleBottom()) { - newHeight += st::msgPadding.bottom(); - } + } + if (newWidth >= maxWidth()) { + newHeight = qMin(newHeight, minHeight()); } return { newWidth, newHeight }; } @@ -196,24 +177,14 @@ int ExtendedPreview::minWidthForButton() const { void ExtendedPreview::draw(Painter &p, const PaintContext &context) const { if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) return; - const auto stm = context.messageStyle(); auto paintx = 0, painty = 0, paintw = width(), painth = height(); auto bubble = _parent->hasBubble(); - auto captionw = paintw - st::msgPadding.left() - st::msgPadding.right(); auto rthumb = style::rtlrect(paintx, painty, paintw, painth, width()); const auto inWebPage = (_parent->media() != this); const auto rounding = inWebPage ? std::optional() - : adjustedBubbleRoundingWithCaption(_caption); - if (bubble) { - if (!_caption.isEmpty()) { - painth -= st::mediaCaptionSkip + _caption.countHeight(captionw); - if (isBubbleBottom()) { - painth -= st::msgPadding.bottom(); - } - rthumb = style::rtlrect(paintx, painty, paintw, painth, width()); - } - } else { + : adjustedBubbleRounding(); + if (!bubble) { Assert(rounding.has_value()); fillImageShadow(p, rthumb, *rounding, context); } @@ -226,27 +197,7 @@ void ExtendedPreview::draw(Painter &p, const PaintContext &context) const { } // date - if (!_caption.isEmpty()) { - p.setPen(stm->historyTextFg); - _parent->prepareCustomEmojiPaint(p, context, _caption); - auto highlightRequest = context.computeHighlightCache(); - _caption.draw(p, { - .position = QPoint( - st::msgPadding.left(), - painty + painth + st::mediaCaptionSkip), - .availableWidth = captionw, - .palette = &stm->textPalette, - .pre = stm->preCache.get(), - .blockquote = context.quoteCache(parent()->contentColorIndex()), - .colors = context.st->highlightColors(), - .spoiler = Ui::Text::DefaultSpoilerCache(), - .now = context.now, - .pausedEmoji = context.paused || On(PowerSaving::kEmojiChat), - .pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler), - .selection = context.selection, - .highlight = highlightRequest ? &*highlightRequest : nullptr, - }); - } else if (!inWebPage) { + if (!inWebPage) { auto fullRight = paintx + paintw; auto fullBottom = painty + painth; if (needInfoDisplay()) { @@ -343,28 +294,10 @@ TextState ExtendedPreview::textState(QPoint point, StateRequest request) const { } auto paintx = 0, painty = 0, paintw = width(), painth = height(); auto bubble = _parent->hasBubble(); - - if (bubble && !_caption.isEmpty()) { - const auto captionw = paintw - - st::msgPadding.left() - - st::msgPadding.right(); - painth -= _caption.countHeight(captionw); - if (isBubbleBottom()) { - painth -= st::msgPadding.bottom(); - } - if (QRect(st::msgPadding.left(), painth, captionw, height() - painth).contains(point)) { - result = TextState(_parent, _caption.getState( - point - QPoint(st::msgPadding.left(), painth), - captionw, - request.forText())); - return result; - } - painth -= st::mediaCaptionSkip; - } if (QRect(paintx, painty, paintw, painth).contains(point)) { result.link = _spoiler.link; } - if (_caption.isEmpty() && _parent->media() == this) { + if (!bubble && _parent->media() == this) { auto fullRight = paintx + paintw; auto fullBottom = painty + painth; const auto bottomInfoResult = _parent->bottomInfoTextState( @@ -403,26 +336,17 @@ bool ExtendedPreview::needInfoDisplay() const { return _parent->data()->isSending() || _parent->data()->hasFailed() || _parent->isUnderCursor() + || (_parent->delegate()->elementContext() == Context::ChatPreview) || _parent->isLastAndSelfMessage(); } -TextForMimeData ExtendedPreview::selectedText(TextSelection selection) const { - return _caption.toTextForMimeData(selection); -} - -void ExtendedPreview::hideSpoilers() { - _caption.setSpoilerRevealed(false, anim::type::instant); -} - bool ExtendedPreview::needsBubble() const { - if (!_caption.isEmpty()) { - return true; - } const auto item = _parent->data(); return !item->isService() && (item->repliesAreComments() || item->externalReply() || item->viaBot() + || !item->emptyText() || _parent->displayReply() || _parent->displayForwardedFrom() || _parent->displayFromName() @@ -435,11 +359,4 @@ QPoint ExtendedPreview::resolveCustomInfoRightBottom() const { return QPoint(width() - skipx, height() - skipy); } -void ExtendedPreview::parentTextUpdated() { - _caption = (_parent->media() == this) - ? createCaption(_parent->data()) - : Ui::Text::String(); - history()->owner().requestViewResize(_parent); -} - } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/media/history_view_extended_preview.h b/Telegram/SourceFiles/history/view/media/history_view_extended_preview.h index 8bec02154..0e645456e 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_extended_preview.h +++ b/Telegram/SourceFiles/history/view/media/history_view_extended_preview.h @@ -31,6 +31,10 @@ public: not_null invoice); ~ExtendedPreview(); + bool hideMessageText() const override { + return false; + } + void draw(Painter &p, const PaintContext &context) const override; TextState textState(QPoint point, StateRequest request) const override; @@ -39,35 +43,15 @@ public: [[nodiscard]] bool dragItemByHandler( const ClickHandlerPtr &p) const override; - [[nodiscard]] TextSelection adjustSelection( - TextSelection selection, - TextSelectType type) const override { - return _caption.adjustSelection(selection, type); - } - uint16 fullSelectionLength() const override { - return _caption.length(); - } - bool hasTextForCopy() const override { - return !_caption.isEmpty(); - } - - TextForMimeData selectedText(TextSelection selection) const override; - - TextWithEntities getCaption() const override { - return _caption.toTextWithEntities(); - } - void hideSpoilers() override; bool needsBubble() const override; bool customInfoLayout() const override { - return _caption.isEmpty(); + return true; } QPoint resolveCustomInfoRightBottom() const override; bool skipBubbleTail() const override { - return isRoundedInBubbleBottom() && _caption.isEmpty(); + return isRoundedInBubbleBottom(); } - void parentTextUpdated() override; - bool hasHeavyPart() const override; void unloadHeavyPart() override; @@ -90,7 +74,6 @@ private: const PaintContext &context) const; const not_null _invoice; - Ui::Text::String _caption; mutable MediaSpoiler _spoiler; mutable QImage _inlineThumbnail; mutable QImage _buttonBackground; diff --git a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp index a89a76083..422f36656 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp @@ -139,8 +139,6 @@ Gif::Gif( , _storyId(realParent->media() ? realParent->media()->storyId() : FullStoryId()) -, _caption( - st::minPhotoSize - st::msgPadding.left() - st::msgPadding.right()) , _spoiler((spoiler || IsHiddenRoundMessage(_parent)) ? std::make_unique() : nullptr) @@ -188,7 +186,6 @@ Gif::Gif( createSpoilerLink(_spoiler.get()); } - refreshCaption(); if ((_dataMedia = _data->activeMediaView())) { dataMediaCreated(); } else { @@ -244,13 +241,6 @@ QSize Gif::countThumbSize(int &inOutWidthMax) const { } QSize Gif::countOptimalSize() { - if (_parent->media() != this) { - _caption = Ui::Text::String(); - } else if (_caption.hasSkipBlock()) { - _caption.updateSkipBlock( - _parent->skipBlockWidth(), - _parent->skipBlockHeight()); - } if (_data->isVideoMessage() && _transcribe) { const auto &entry = _data->session().api().transcribes().entry( _realParent); @@ -275,29 +265,17 @@ QSize Gif::countOptimalSize() { accumulate_max(maxWidth, gifMaxStatusWidth(_data) + 2 * (st::msgDateImgDelta + st::msgDateImgPadding.x())); } if (_parent->hasBubble()) { - if (!_caption.isEmpty()) { - maxWidth = qMax(maxWidth, st::msgPadding.left() - + _caption.maxWidth() - + st::msgPadding.right()); - minHeight = adjustHeightForLessCrop( - scaled, - { maxWidth, minHeight }); - if (const auto botTop = _parent->Get()) { - accumulate_max(maxWidth, botTop->maxWidth); - minHeight += botTop->height; - } - minHeight += st::mediaCaptionSkip + _caption.minHeight(); - if (isBubbleBottom()) { - minHeight += st::msgPadding.bottom(); - } - } + maxWidth = qMax(maxWidth, _parent->textualMaxWidth()); + minHeight = adjustHeightForLessCrop( + scaled, + { maxWidth, minHeight }); } else if (isUnwrapped()) { const auto item = _parent->data(); auto via = item->Get(); auto reply = _parent->Get(); auto forwarded = item->Get(); if (forwarded) { - forwarded->create(via); + forwarded->create(via, item); } maxWidth += additionalWidth(reply, via, forwarded); accumulate_max(maxWidth, _parent->reactionsOptimalWidth()); @@ -322,30 +300,16 @@ QSize Gif::countCurrentSize(int newWidth) { } if (_parent->hasBubble()) { accumulate_max(newWidth, _parent->minWidthForMedia()); - if (!_caption.isEmpty()) { - auto captionMaxWidth = st::msgPadding.left() - + _caption.maxWidth() - + st::msgPadding.right(); - const auto botTop = _parent->Get(); - if (botTop) { - accumulate_max(captionMaxWidth, botTop->maxWidth); - } - const auto maxWithCaption = qMin(st::msgMaxWidth, captionMaxWidth); - newWidth = qMin(qMax(newWidth, maxWithCaption), thumbMaxWidth); - newHeight = adjustHeightForLessCrop( - scaled, - { newWidth, newHeight }); - const auto captionw = newWidth - - st::msgPadding.left() - - st::msgPadding.right(); - if (botTop) { - newHeight += botTop->height; - } - newHeight += st::mediaCaptionSkip + _caption.countHeight(captionw); - if (isBubbleBottom()) { - newHeight += st::msgPadding.bottom(); - } + auto captionMaxWidth = _parent->textualMaxWidth(); + const auto botTop = _parent->Get(); + if (botTop) { + accumulate_max(captionMaxWidth, botTop->maxWidth); } + const auto maxWithCaption = qMin(st::msgMaxWidth, captionMaxWidth); + newWidth = qMin(qMax(newWidth, maxWithCaption), thumbMaxWidth); + newHeight = adjustHeightForLessCrop( + scaled, + { newWidth, newHeight }); } else if (isUnwrapped()) { accumulate_max(newWidth, _parent->reactionsOptimalWidth()); @@ -428,7 +392,6 @@ void Gif::draw(Painter &p, const PaintContext &context) const { const auto displayLoading = (item->isSending() || _data->displayLoading()); const auto st = context.st; const auto sti = context.imageStyle(); - const auto stm = context.messageStyle(); const auto cornerDownload = downloadInCorner(); const auto canBePlayed = _dataMedia->canBePlayed(_realParent); const auto autoplay = autoplayEnabled() @@ -437,27 +400,14 @@ void Gif::draw(Painter &p, const PaintContext &context) const { const auto activeRoundPlaying = activeRoundStreamed(); auto paintx = 0, painty = 0, paintw = width(), painth = height(); - auto captionw = paintw - st::msgPadding.left() - st::msgPadding.right(); const bool bubble = _parent->hasBubble(); const auto rightLayout = _parent->hasRightLayout(); const auto inWebPage = (_parent->media() != this); const auto isRound = _data->isVideoMessage(); - const auto botTop = _parent->Get(); const auto rounding = inWebPage ? std::optional() - : adjustedBubbleRoundingWithCaption(_caption); - if (bubble) { - if (!_caption.isEmpty()) { - if (botTop) { - painth -= botTop->height; - } - painth -= st::mediaCaptionSkip + _caption.countHeight(captionw); - if (isBubbleBottom()) { - painth -= st::msgPadding.bottom(); - } - } - } + : adjustedBubbleRounding(); auto usex = 0, usew = paintw; const auto unwrapped = isUnwrapped(); @@ -796,35 +746,7 @@ void Gif::draw(Painter &p, const PaintContext &context) const { } } } - if (!unwrapped && !_caption.isEmpty()) { - p.setPen(stm->historyTextFg); - _parent->prepareCustomEmojiPaint(p, context, _caption); - auto top = painty + painth + st::mediaCaptionSkip; - if (botTop) { - botTop->text.drawLeftElided( - p, - st::msgPadding.left(), - top, - captionw, - _parent->width()); - top += botTop->height; - } - auto highlightRequest = context.computeHighlightCache(); - _caption.draw(p, { - .position = QPoint(st::msgPadding.left(), top), - .availableWidth = captionw, - .palette = &stm->textPalette, - .pre = stm->preCache.get(), - .blockquote = context.quoteCache(parent()->contentColorIndex()), - .colors = context.st->highlightColors(), - .spoiler = Ui::Text::DefaultSpoilerCache(), - .now = context.now, - .pausedEmoji = context.paused || On(PowerSaving::kEmojiChat), - .pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler), - .selection = context.selection, - .highlight = highlightRequest ? &*highlightRequest : nullptr, - }); - } else if (!inWebPage && !skipDrawingSurrounding && !AyuFeatures::MessageShot::ignoreRender(AyuFeatures::MessageShot::RenderPart::Date)) { + if (!inWebPage && !skipDrawingSurrounding && !AyuFeatures::MessageShot::ignoreRender(AyuFeatures::MessageShot::RenderPart::Date)) { auto fullRight = paintx + usex + usew; auto fullBottom = painty + painth; auto maxRight = _parent->width() - st::msgMargin.left(); @@ -843,7 +765,8 @@ void Gif::draw(Painter &p, const PaintContext &context) const { fullRight = maxRight; } } - if (isRound || needInfoDisplay()) { + if (isRound + || ((!bubble || isBubbleBottom()) && needInfoDisplay())) { _parent->drawInfo( p, context, @@ -1085,24 +1008,6 @@ TextState Gif::textState(QPoint point, StateRequest request) const { auto paintx = 0, painty = 0, paintw = width(), painth = height(); auto bubble = _parent->hasBubble(); - if (bubble && !_caption.isEmpty()) { - auto captionw = paintw - st::msgPadding.left() - st::msgPadding.right(); - painth -= _caption.countHeight(captionw); - if (isBubbleBottom()) { - painth -= st::msgPadding.bottom(); - } - if (QRect(st::msgPadding.left(), painth, captionw, height() - painth).contains(point)) { - result = TextState(_parent, _caption.getState( - point - QPoint(st::msgPadding.left(), painth), - captionw, - request.forText())); - return result; - } - if (const auto botTop = _parent->Get()) { - painth -= botTop->height; - } - painth -= st::mediaCaptionSkip; - } const auto rightLayout = _parent->hasRightLayout(); const auto inWebPage = (_parent->media() != this); const auto isRound = _data->isVideoMessage(); @@ -1216,7 +1121,9 @@ TextState Gif::textState(QPoint point, StateRequest request) const { ? _cancell : _savel; } - if (unwrapped || _caption.isEmpty()) { + const auto checkBottomInfo = !inWebPage + && (unwrapped || !bubble || isBubbleBottom()); + if (checkBottomInfo) { auto fullRight = usex + paintx + usew; auto fullBottom = painty + painth; auto maxRight = _parent->width() - st::msgMargin.left(); @@ -1235,19 +1142,17 @@ TextState Gif::textState(QPoint point, StateRequest request) const { fullRight = maxRight; } } - if (!inWebPage) { - const auto bottomInfoResult = _parent->bottomInfoTextState( - fullRight, - fullBottom, - point, - (unwrapped - ? InfoDisplayType::Background - : InfoDisplayType::Image)); - if (bottomInfoResult.link - || bottomInfoResult.cursor != CursorState::None - || bottomInfoResult.customTooltip) { - return bottomInfoResult; - } + const auto bottomInfoResult = _parent->bottomInfoTextState( + fullRight, + fullBottom, + point, + (unwrapped + ? InfoDisplayType::Background + : InfoDisplayType::Image)); + if (bottomInfoResult.link + || bottomInfoResult.cursor != CursorState::None + || bottomInfoResult.customTooltip) { + return bottomInfoResult; } if (const auto size = bubble ? std::nullopt : _parent->rightActionSize()) { const auto rightActionWidth = size->width(); @@ -1293,23 +1198,11 @@ void Gif::clickHandlerPressedChanged( } } -TextForMimeData Gif::selectedText(TextSelection selection) const { - return _caption.toTextForMimeData(selection); -} - -SelectedQuote Gif::selectedQuote(TextSelection selection) const { - return Element::FindSelectedQuote(_caption, selection, _realParent); -} - -TextSelection Gif::selectionFromQuote(const SelectedQuote "e) const { - return Element::FindSelectionFromQuote(_caption, quote); -} - bool Gif::fullFeaturedGrouped(RectParts sides) const { return (sides & RectPart::Left) && (sides & RectPart::Right); } -QSize Gif::sizeForGroupingOptimal(int maxWidth) const { +QSize Gif::sizeForGroupingOptimal(int maxWidth, bool last) const { return sizeForAspectRatio(); } @@ -1592,7 +1485,6 @@ bool Gif::uploading() const { } void Gif::hideSpoilers() { - _caption.setSpoilerRevealed(false, anim::type::instant); if (_spoiler) { _spoiler->revealed = false; } @@ -1603,13 +1495,12 @@ bool Gif::needsBubble() const { return true; } else if (_data->isVideoMessage()) { return false; - } else if (!_caption.isEmpty()) { - return true; } const auto item = _parent->data(); return item->repliesAreComments() || item->externalReply() || item->viaBot() + || !item->emptyText() || _parent->displayReply() || _parent->displayForwardedFrom() || _parent->displayFromName() @@ -1814,13 +1705,6 @@ bool Gif::isReadyForOpen() const { return true; } -void Gif::parentTextUpdated() { - if (_parent->media() == this) { - refreshCaption(); - history()->owner().requestViewResize(_parent); - } -} - bool Gif::hasHeavyPart() const { return (_spoiler && _spoiler->animation) || _streamed || _dataMedia; } @@ -1834,19 +1718,11 @@ void Gif::unloadHeavyPart() { } _thumbCache = QImage(); _videoThumbnailFrame = nullptr; - _caption.unloadPersistentAnimation(); togglePollingStory(false); } -void Gif::refreshParentId(not_null realParent) { - File::refreshParentId(realParent); - if (_parent->media() == this) { - refreshCaption(); - } -} - -void Gif::refreshCaption() { - _caption = createCaption(_parent->data()); +bool Gif::enforceBubbleWidth() const { + return true; } int Gif::additionalWidth( @@ -2055,6 +1931,7 @@ bool Gif::needInfoDisplay() const { return _parent->data()->isSending() || _data->uploading() || _parent->isUnderCursor() + || (_parent->delegate()->elementContext() == Context::ChatPreview) // Don't show the GIF badge if this message has text. || (!_parent->hasBubble() && _parent->isLastAndSelfMessage()); } diff --git a/Telegram/SourceFiles/history/view/media/history_view_gif.h b/Telegram/SourceFiles/history/view/media/history_view_gif.h index dad49b351..840b12587 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_gif.h +++ b/Telegram/SourceFiles/history/view/media/history_view_gif.h @@ -54,6 +54,10 @@ public: bool spoiler); ~Gif(); + bool hideMessageText() const override { + return false; + } + void draw(Painter &p, const PaintContext &context) const override; TextState textState(QPoint point, StateRequest request) const override; @@ -61,23 +65,6 @@ public: const ClickHandlerPtr &p, bool pressed) override; - [[nodiscard]] TextSelection adjustSelection( - TextSelection selection, - TextSelectType type) const override { - return _caption.adjustSelection(selection, type); - } - uint16 fullSelectionLength() const override { - return _caption.length(); - } - bool hasTextForCopy() const override { - return !_caption.isEmpty(); - } - - TextForMimeData selectedText(TextSelection selection) const override; - SelectedQuote selectedQuote(TextSelection selection) const override; - TextSelection selectionFromQuote( - const SelectedQuote "e) const override; - bool uploading() const override; DocumentData *getDocument() const override { @@ -85,7 +72,7 @@ public: } bool fullFeaturedGrouped(RectParts sides) const; - QSize sizeForGroupingOptimal(int maxWidth) const override; + QSize sizeForGroupingOptimal(int maxWidth, bool last) const override; QSize sizeForGrouping(int width) const override; void drawGrouped( Painter &p, @@ -105,14 +92,11 @@ public: void stopAnimation() override; void checkAnimation() override; - TextWithEntities getCaption() const override { - return _caption.toTextWithEntities(); - } void hideSpoilers() override; bool needsBubble() const override; bool unwrapped() const override; bool customInfoLayout() const override { - return _caption.isEmpty(); + return true; } QRect contentRectForReactions() const override; std::optional reactionButtonCenterOverride() const override; @@ -120,16 +104,13 @@ public: QString additionalInfoString() const override; bool skipBubbleTail() const override { - return isRoundedInBubbleBottom() && _caption.isEmpty(); + return isRoundedInBubbleBottom(); } bool isReadyForOpen() const override; - void parentTextUpdated() override; - bool hasHeavyPart() const override; void unloadHeavyPart() override; - - void refreshParentId(not_null realParent) override; + bool enforceBubbleWidth() const override; [[nodiscard]] static bool CanPlayInline(not_null document); @@ -148,7 +129,6 @@ private: void ensureDataMediaCreated() const; void dataMediaCreated() const; - void refreshCaption(); [[nodiscard]] bool autoplayEnabled() const; @@ -223,7 +203,6 @@ private: const not_null _data; const FullStoryId _storyId; - Ui::Text::String _caption; std::unique_ptr _streamed; const std::unique_ptr _spoiler; mutable std::unique_ptr _transcribe; diff --git a/Telegram/SourceFiles/history/view/media/history_view_invoice.cpp b/Telegram/SourceFiles/history/view/media/history_view_invoice.cpp index 95676cc89..ace70246a 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_invoice.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_invoice.cpp @@ -34,7 +34,8 @@ Invoice::Invoice( } void Invoice::fillFromData(not_null invoice) { - if (invoice->photo) { + const auto isCreditsCurrency = false; + if (invoice->photo && !isCreditsCurrency) { const auto spoiler = false; _attach = std::make_unique( _parent, @@ -64,6 +65,9 @@ void Invoice::fillFromData(not_null invoice) { 0, int(statusText.text.size()) }); statusText.text += ' ' + labelText().toUpper(); + if (isCreditsCurrency) { + statusText = {}; + } _status.setMarkedText( st::defaultTextStyle, statusText, diff --git a/Telegram/SourceFiles/history/view/media/history_view_invoice.h b/Telegram/SourceFiles/history/view/media/history_view_invoice.h index a95c7d5dc..b9f5203ab 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_invoice.h +++ b/Telegram/SourceFiles/history/view/media/history_view_invoice.h @@ -30,6 +30,9 @@ public: return _title.toString(); } + bool aboveTextByDefault() const override { + return false; + } bool hideMessageText() const override { return false; } diff --git a/Telegram/SourceFiles/history/view/media/history_view_location.cpp b/Telegram/SourceFiles/history/view/media/history_view_location.cpp index f16d65141..5b8b18e61 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_location.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_location.cpp @@ -509,7 +509,10 @@ void Location::paintLiveRemaining( auto pen = QPen(color); pen.setWidthF(stroke); p.setPen(pen); - p.drawArc(rect, 90 * 16, int(base::SafeRound(360 * 16 * progress))); + p.drawArc( + rect, + arc::kQuarterLength, + int(base::SafeRound(arc::kFullLength * progress))); } p.setPen(stm->msgServiceFg); diff --git a/Telegram/SourceFiles/history/view/media/history_view_media.cpp b/Telegram/SourceFiles/history/view/media/history_view_media.cpp index 56720e83f..4f8c8096b 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media.cpp @@ -11,7 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item.h" #include "history/view/history_view_element.h" #include "history/view/history_view_cursor_state.h" -#include "history/view/history_view_spoiler_click_handler.h" +#include "history/view/history_view_text_helper.h" #include "history/view/media/history_view_sticker.h" #include "history/view/media/history_view_media_spoiler.h" #include "storage/storage_shared_media.h" @@ -319,7 +319,7 @@ Ui::Text::String Media::createCaption(not_null item) const { item->translatedTextWithLocalEntities(), Ui::ItemTextOptions(item), context); - FillTextWithAnimatedSpoilers(_parent, result); + InitElementTextPart(_parent, result); if (const auto width = _parent->skipBlockWidth()) { result.updateSkipBlock(width, _parent->skipBlockHeight()); } @@ -391,10 +391,8 @@ Ui::BubbleRounding Media::adjustedBubbleRounding(RectParts square) const { return result; } -Ui::BubbleRounding Media::adjustedBubbleRoundingWithCaption( - const Ui::Text::String &caption) const { - return adjustedBubbleRounding( - caption.isEmpty() ? RectParts() : RectPart::FullBottom); +HistoryItem *Media::itemForText() const { + return _parent->data(); } bool Media::isRoundedInBubbleBottom() const { diff --git a/Telegram/SourceFiles/history/view/media/history_view_media.h b/Telegram/SourceFiles/history/view/media/history_view_media.h index f9ad5c987..4926ca7c8 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media.h +++ b/Telegram/SourceFiles/history/view/media/history_view_media.h @@ -104,6 +104,10 @@ public: [[nodiscard]] virtual bool hasTextForCopy() const { return false; } + [[nodiscard]] virtual bool aboveTextByDefault() const { + return true; + } + [[nodiscard]] virtual HistoryItem *itemForText() const; [[nodiscard]] virtual bool hideMessageText() const { return true; } @@ -194,7 +198,9 @@ public: virtual void checkAnimation() { } - [[nodiscard]] virtual QSize sizeForGroupingOptimal(int maxWidth) const { + [[nodiscard]] virtual QSize sizeForGroupingOptimal( + int maxWidth, + bool last) const { Unexpected("Grouping method call."); } [[nodiscard]] virtual QSize sizeForGrouping(int width) const { @@ -221,9 +227,6 @@ public: return false; } - [[nodiscard]] virtual TextWithEntities getCaption() const { - return TextWithEntities(); - } virtual void hideSpoilers() { } [[nodiscard]] virtual bool needsBubble() const = 0; @@ -273,8 +276,6 @@ public: } [[nodiscard]] Ui::BubbleRounding adjustedBubbleRounding( RectParts square = {}) const; - [[nodiscard]] Ui::BubbleRounding adjustedBubbleRoundingWithCaption( - const Ui::Text::String &caption) const; [[nodiscard]] bool isBubbleTop() const { return (_inBubbleState == MediaInBubbleState::Top) || (_inBubbleState == MediaInBubbleState::None); 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 15a65aabf..ccec8f431 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_grouped.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media_grouped.cpp @@ -65,8 +65,7 @@ GroupedMedia::Part::Part( GroupedMedia::GroupedMedia( not_null parent, const std::vector> &medias) -: Media(parent) -, _caption(st::minPhotoSize - st::msgPadding.left() - st::msgPadding.right()) { +: Media(parent) { const auto truncated = ranges::views::all( medias ) | ranges::views::transform([](const std::unique_ptr &v) { @@ -80,8 +79,7 @@ GroupedMedia::GroupedMedia( GroupedMedia::GroupedMedia( not_null parent, const std::vector> &items) -: Media(parent) -, _caption(st::minPhotoSize - st::msgPadding.left() - st::msgPadding.right()) { +: Media(parent) { const auto medias = ranges::views::all( items ) | ranges::views::transform([](not_null item) { @@ -97,6 +95,31 @@ GroupedMedia::~GroupedMedia() { base::take(_parts); } +HistoryItem *GroupedMedia::itemForText() const { + if (_mode == Mode::Column) { + return Media::itemForText(); + } else if (!_captionItem) { + _captionItem = [&]() -> HistoryItem* { + auto result = (HistoryItem*)nullptr; + for (const auto &part : _parts) { + if (!part.item->emptyText()) { + if (result) { + return nullptr; + } else { + result = part.item; + } + } + } + return result; + }(); + } + return *_captionItem; +} + +bool GroupedMedia::hideMessageText() const { + return (_mode == Mode::Column); +} + GroupedMedia::Mode GroupedMedia::DetectMode(not_null media) { const auto document = media->document(); return (document && !document->isVideoFile()) @@ -105,12 +128,6 @@ GroupedMedia::Mode GroupedMedia::DetectMode(not_null media) { } QSize GroupedMedia::countOptimalSize() { - if (_caption.hasSkipBlock()) { - _caption.updateSkipBlock( - _parent->skipBlockWidth(), - _parent->skipBlockHeight()); - } - std::vector sizes; const auto partsCount = _parts.size(); sizes.reserve(partsCount); @@ -123,8 +140,11 @@ QSize GroupedMedia::countOptimalSize() { accumulate_max(maxWidth, media->maxWidth()); } } + auto index = 0; for (const auto &part : _parts) { - sizes.push_back(part.content->sizeForGroupingOptimal(maxWidth)); + const auto last = (++index == _parts.size()); + sizes.push_back( + part.content->sizeForGroupingOptimal(maxWidth, last)); } const auto layout = (_mode == Mode::Grid) @@ -145,13 +165,7 @@ QSize GroupedMedia::countOptimalSize() { _parts[i].sides = item.sides; } - if (!_caption.isEmpty()) { - auto captionw = maxWidth - st::msgPadding.left() - st::msgPadding.right(); - minHeight += st::mediaCaptionSkip + _caption.countHeight(captionw); - if (isBubbleBottom()) { - minHeight += st::msgPadding.bottom(); - } - } else if (_mode == Mode::Column && _parts.back().item->emptyText()) { + if (_mode == Mode::Column && _parts.back().item->emptyText()) { const auto item = _parent->data(); const auto msgsigned = item->Get(); const auto views = item->Get(); @@ -215,13 +229,7 @@ QSize GroupedMedia::countCurrentSize(int newWidth) { accumulate_max(newHeight, top + height); } } - if (!_caption.isEmpty()) { - const auto captionw = newWidth - st::msgPadding.left() - st::msgPadding.right(); - newHeight += st::mediaCaptionSkip + _caption.countHeight(captionw); - if (isBubbleBottom()) { - newHeight += st::msgPadding.bottom(); - } - } else if (_mode == Mode::Column && _parts.back().item->emptyText()) { + if (_mode == Mode::Column && _parts.back().item->emptyText()) { const auto item = _parent->data(); const auto msgsigned = item->Get(); const auto views = item->Get(); @@ -341,7 +349,7 @@ void GroupedMedia::draw(Painter &p, const PaintContext &context) const { constexpr auto kSmall = Ui::BubbleCornerRounding::Small; const auto rounding = inWebPage ? Ui::BubbleRounding{ kSmall, kSmall, kSmall, kSmall } - : adjustedBubbleRoundingWithCaption(_caption); + : adjustedBubbleRounding(); auto highlight = context.highlight.range; const auto subpartHighlight = IsSubGroupSelection(highlight); for (auto i = 0, count = int(_parts.size()); i != count; ++i) { @@ -388,33 +396,7 @@ void GroupedMedia::draw(Painter &p, const PaintContext &context) const { } // date - if (!_caption.isEmpty()) { - const auto captionw = width() - st::msgPadding.left() - st::msgPadding.right(); - const auto captiony = height() - - groupPadding.bottom() - - (isBubbleBottom() ? st::msgPadding.bottom() : 0) - - _caption.countHeight(captionw); - const auto stm = context.messageStyle(); - p.setPen(stm->historyTextFg); - _parent->prepareCustomEmojiPaint(p, context, _caption); - auto highlightRequest = context.computeHighlightCache(); - _caption.draw(p, { - .position = QPoint( - st::msgPadding.left(), - captiony), - .availableWidth = captionw, - .palette = &stm->textPalette, - .pre = stm->preCache.get(), - .blockquote = context.quoteCache(parent()->contentColorIndex()), - .colors = context.st->highlightColors(), - .spoiler = Ui::Text::DefaultSpoilerCache(), - .now = context.now, - .pausedEmoji = context.paused || On(PowerSaving::kEmojiChat), - .pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler), - .selection = context.selection, - .highlight = highlightRequest ? &*highlightRequest : nullptr, - }); - } else if (_parent->media() == this) { + if (_parent->media() == this && (!_parent->hasBubble() || isBubbleBottom())) { auto fullRight = width(); auto fullBottom = height(); if (needInfoDisplay()) { @@ -473,23 +455,7 @@ PointState GroupedMedia::pointState(QPoint point) const { TextState GroupedMedia::textState(QPoint point, StateRequest request) const { const auto groupPadding = groupedPadding(); auto result = getPartState(point - QPoint(0, groupPadding.top()), request); - if (!result.link && !_caption.isEmpty()) { - const auto captionw = width() - st::msgPadding.left() - st::msgPadding.right(); - const auto captiony = height() - - groupPadding.bottom() - - (isBubbleBottom() ? st::msgPadding.bottom() : 0) - - _caption.countHeight(captionw); - if (QRect(st::msgPadding.left(), captiony, captionw, height() - captiony).contains(point)) { - return TextState( - _captionItem - ? _captionItem - : _parent->data().get(), - _caption.getState( - point - QPoint(st::msgPadding.left(), captiony), - captionw, - request.forText())); - } - } else if (_parent->media() == this) { + if (_parent->media() == this && (!_parent->hasBubble() || isBubbleBottom())) { auto fullRight = width(); auto fullBottom = height(); const auto bottomInfoResult = _parent->bottomInfoTextState( @@ -539,7 +505,7 @@ TextSelection GroupedMedia::adjustSelection( TextSelection selection, TextSelectType type) const { if (_mode != Mode::Column) { - return _caption.adjustSelection(selection, type); + return {}; } auto checked = 0; for (const auto &part : _parts) { @@ -563,7 +529,7 @@ TextSelection GroupedMedia::adjustSelection( uint16 GroupedMedia::fullSelectionLength() const { if (_mode != Mode::Column) { - return _caption.length(); + return {}; } auto result = 0; for (const auto &part : _parts) { @@ -574,7 +540,7 @@ uint16 GroupedMedia::fullSelectionLength() const { bool GroupedMedia::hasTextForCopy() const { if (_mode != Mode::Column) { - return !_caption.isEmpty(); + return {}; } for (const auto &part : _parts) { if (part.content->hasTextForCopy()) { @@ -587,7 +553,7 @@ bool GroupedMedia::hasTextForCopy() const { TextForMimeData GroupedMedia::selectedText( TextSelection selection) const { if (_mode != Mode::Column) { - return _caption.toTextForMimeData(selection); + return {}; } auto result = TextForMimeData(); for (const auto &part : _parts) { @@ -606,9 +572,7 @@ TextForMimeData GroupedMedia::selectedText( SelectedQuote GroupedMedia::selectedQuote(TextSelection selection) const { if (_mode != Mode::Column) { - return _captionItem - ? Element::FindSelectedQuote(_caption, selection, _captionItem) - : SelectedQuote(); + return {}; } for (const auto &part : _parts) { const auto next = part.content->skipSelection(selection); @@ -630,9 +594,7 @@ TextSelection GroupedMedia::selectionFromQuote( Expects(quote.item != nullptr); if (_mode != Mode::Column) { - return (_captionItem == quote.item) - ? Element::FindSelectionFromQuote(_caption, quote) - : TextSelection(); + return {}; } const auto i = ranges::find(_parts, not_null(quote.item), &Part::item); if (i == end(_parts)) { @@ -734,7 +696,6 @@ bool GroupedMedia::applyGroup(const DataMediaRange &medias) { if (_parts.empty()) { return false; } - refreshCaption(); Ensures(_parts.size() <= kMaxSize); return true; @@ -754,43 +715,13 @@ bool GroupedMedia::validateGroupParts( return (i == count); } -void GroupedMedia::refreshCaption() { - const auto part = [&]() -> const Part* { - if (_mode == Mode::Column) { - return nullptr; - } - auto result = (const Part*)nullptr; - for (const auto &part : _parts) { - if (!part.item->emptyText()) { - if (result) { - return nullptr; - } else { - result = ∂ - } - } - } - return result; - }(); - if (part) { - _caption = createCaption(part->item); - _captionItem = part->item; - } else { - _captionItem = nullptr; - } -} - not_null GroupedMedia::main() const { Expects(!_parts.empty()); return _parts.back().content.get(); } -TextWithEntities GroupedMedia::getCaption() const { - return main()->getCaption(); -} - void GroupedMedia::hideSpoilers() { - _caption.setSpoilerRevealed(false, anim::type::instant); for (const auto &part : _parts) { part.content->hideSpoilers(); } @@ -850,13 +781,17 @@ void GroupedMedia::unloadHeavyPart() { part.cacheKey = 0; part.cache = QPixmap(); } - _caption.unloadPersistentAnimation(); } void GroupedMedia::parentTextUpdated() { if (_parent->media() == this) { - refreshCaption(); - history()->owner().requestViewResize(_parent); + if (_mode == Mode::Column) { + for (const auto &part : _parts) { + part.content->parentTextUpdated(); + } + } else { + _captionItem = std::nullopt; + } } } @@ -870,8 +805,14 @@ QPoint GroupedMedia::resolveCustomInfoRightBottom() const { return QPoint(width() - skipx, height() - skipy); } +bool GroupedMedia::enforceBubbleWidth() const { + return _mode == Mode::Grid; +} + bool GroupedMedia::computeNeedBubble() const { - if (!_caption.isEmpty() || _mode == Mode::Column) { + Expects(_mode == Mode::Column || _captionItem.has_value()); + + if (_mode == Mode::Column || *_captionItem) { return true; } if (const auto item = _parent->data()) { @@ -894,6 +835,7 @@ bool GroupedMedia::needInfoDisplay() const { && (_parent->data()->isSending() || _parent->data()->hasFailed() || _parent->isUnderCursor() + || (_parent->delegate()->elementContext() == Context::ChatPreview) || _parent->isLastAndSelfMessage()); } diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_grouped.h b/Telegram/SourceFiles/history/view/media/history_view_media_grouped.h index f8baabe8a..84dca4884 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_grouped.h +++ b/Telegram/SourceFiles/history/view/media/history_view_media_grouped.h @@ -31,6 +31,9 @@ public: void refreshParentId(not_null realParent) override; + HistoryItem *itemForText() const override; + bool hideMessageText() const override; + void drawHighlight( Painter &p, const PaintContext &context, @@ -69,7 +72,6 @@ public: const ClickHandlerPtr &p, bool pressed) override; - TextWithEntities getCaption() const override; void hideSpoilers() override; Storage::SharedMediaTypesMask sharedMediaTypes() const override; @@ -79,14 +81,12 @@ public: HistoryMessageEdited *displayedEditBadge() const override; bool skipBubbleTail() const override { - return (_mode == Mode::Grid) - && isRoundedInBubbleBottom() - && _caption.isEmpty(); + return (_mode == Mode::Grid) && isRoundedInBubbleBottom(); } void updateNeedBubbleState() override; bool needsBubble() const override; bool customInfoLayout() const override { - return _caption.isEmpty() && (_mode != Mode::Column); + return (_mode != Mode::Column); } QPoint resolveCustomInfoRightBottom() const override; @@ -96,6 +96,7 @@ public: bool customHighlight() const override { return true; } + bool enforceBubbleWidth() const override; void stopAnimation() override; void checkAnimation() override; @@ -143,15 +144,12 @@ private: QPoint point, StateRequest request) const; - void refreshCaption(); - [[nodiscard]] Ui::BubbleRounding applyRoundingSides( Ui::BubbleRounding already, RectParts sides) const; [[nodiscard]] QMargins groupedPadding() const; - Ui::Text::String _caption; - HistoryItem *_captionItem = nullptr; + mutable std::optional _captionItem; std::vector _parts; Mode _mode = Mode::Grid; bool _needBubble = false; diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.cpp b/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.cpp index 905536c90..24e571ca6 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.cpp @@ -63,7 +63,7 @@ QSize UnwrappedMedia::countOptimalSize() { const auto topic = _parent->displayedTopicButton(); const auto forwarded = getDisplayedForwardedInfo(); if (forwarded) { - forwarded->create(via); + forwarded->create(via, item); } maxWidth += additionalWidth(topic, reply, via, forwarded); accumulate_max(maxWidth, _parent->reactionsOptimalWidth()); @@ -651,6 +651,7 @@ bool UnwrappedMedia::needInfoDisplay() const { || _parent->isUnderCursor() || _parent->rightActionSize() || _parent->isLastAndSelfMessage() + || (_parent->delegate()->elementContext() == Context::ChatPreview) || (_parent->hasRightLayout() && _content->alwaysShowOutTimestamp()); } diff --git a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp index bb754dc0a..686004e73 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp @@ -77,9 +77,7 @@ Photo::Photo( , _storyId(realParent->media() ? realParent->media()->storyId() : FullStoryId()) -, _caption(st::minPhotoSize - st::msgPadding.left() - st::msgPadding.right()) , _spoiler(spoiler ? std::make_unique() : nullptr) { - _caption = createCaption(realParent); create(realParent->fullId()); } @@ -165,7 +163,6 @@ void Photo::unloadHeavyPart() { _spoiler->animation = nullptr; } _imageCache = QImage(); - _caption.unloadPersistentAnimation(); togglePollingStory(false); } @@ -188,15 +185,6 @@ QSize Photo::countOptimalSize() { if (_serviceWidth > 0) { return { int(_serviceWidth), int(_serviceWidth) }; } - - if (_parent->media() != this) { - _caption = Ui::Text::String(); - } else if (_caption.hasSkipBlock()) { - _caption.updateSkipBlock( - _parent->skipBlockWidth(), - _parent->skipBlockHeight()); - } - const auto dimensions = photoSize(); const auto scaled = CountDesiredMediaSize(dimensions); const auto minWidth = std::clamp( @@ -208,23 +196,10 @@ QSize Photo::countOptimalSize() { const auto maxActualWidth = qMax(scaled.width(), minWidth); auto maxWidth = qMax(maxActualWidth, scaled.height()); auto minHeight = qMax(scaled.height(), st::minPhotoSize); - if (_parent->hasBubble() && !_caption.isEmpty()) { - maxWidth = qMax( - maxWidth, - (st::msgPadding.left() - + _caption.maxWidth() - + st::msgPadding.right())); + if (_parent->hasBubble()) { minHeight = adjustHeightForLessCrop( dimensions, { maxWidth, minHeight }); - if (const auto botTop = _parent->Get()) { - accumulate_max(maxWidth, botTop->maxWidth); - minHeight += botTop->height; - } - minHeight += st::mediaCaptionSkip + _caption.minHeight(); - if (isBubbleBottom()) { - minHeight += st::msgPadding.bottom(); - } } return { maxWidth, minHeight }; } @@ -247,30 +222,20 @@ QSize Photo::countCurrentSize(int newWidth) { maxWidth()); newWidth = qMax(pix.width(), minWidth); auto newHeight = qMax(pix.height(), st::minPhotoSize); - auto imageHeight = newHeight; - if (_parent->hasBubble() && !_caption.isEmpty()) { - auto captionMaxWidth = st::msgPadding.left() - + _caption.maxWidth() - + st::msgPadding.right(); + if (_parent->hasBubble()) { + auto captionMaxWidth = _parent->textualMaxWidth(); const auto botTop = _parent->Get(); if (botTop) { accumulate_max(captionMaxWidth, botTop->maxWidth); } const auto maxWithCaption = qMin(st::msgMaxWidth, captionMaxWidth); newWidth = qMin(qMax(newWidth, maxWithCaption), thumbMaxWidth); - imageHeight = newHeight = adjustHeightForLessCrop( + newHeight = adjustHeightForLessCrop( dimensions, { newWidth, newHeight }); - const auto captionw = newWidth - - st::msgPadding.left() - - st::msgPadding.right(); - if (botTop) { - newHeight += botTop->height; - } - newHeight += st::mediaCaptionSkip + _caption.countHeight(captionw); - if (isBubbleBottom()) { - newHeight += st::msgPadding.bottom(); - } + } + if (newWidth >= maxWidth()) { + newHeight = qMin(newHeight, minHeight()); } const auto enlargeInner = st::historyPageEnlargeSize; const auto enlargeOuter = 2 * st::historyPageEnlargeSkip + enlargeInner; @@ -279,7 +244,7 @@ QSize Photo::countCurrentSize(int newWidth) { && _parent->data()->media()->webpage() && _parent->data()->media()->webpage()->suggestEnlargePhoto() && (newWidth >= enlargeOuter) - && (imageHeight >= enlargeOuter); + && (newHeight >= enlargeOuter); _showEnlarge = showEnlarge ? 1 : 0; return { newWidth, newHeight }; } @@ -305,7 +270,6 @@ void Photo::draw(Painter &p, const PaintContext &context) const { _dataMedia->automaticLoad(_realParent->fullId(), _parent->data()); const auto st = context.st; const auto sti = context.imageStyle(); - const auto stm = context.messageStyle(); auto loaded = _dataMedia->loaded(); auto displayLoading = _data->displayLoading(); @@ -313,8 +277,6 @@ void Photo::draw(Painter &p, const PaintContext &context) const { auto paintx = 0, painty = 0, paintw = width(), painth = height(); auto bubble = _parent->hasBubble(); - auto captionw = paintw - st::msgPadding.left() - st::msgPadding.right(); - if (displayLoading) { ensureAnimation(); if (!_animation->radial.animating()) { @@ -322,7 +284,6 @@ void Photo::draw(Painter &p, const PaintContext &context) const { } } const auto radial = isRadialAnimation(); - const auto botTop = _parent->Get(); auto rthumb = style::rtlrect(paintx, painty, paintw, painth, width()); if (_serviceWidth > 0) { @@ -330,19 +291,8 @@ void Photo::draw(Painter &p, const PaintContext &context) const { } else { const auto rounding = inWebPage ? std::optional() - : adjustedBubbleRoundingWithCaption(_caption); - if (bubble) { - if (!_caption.isEmpty()) { - painth -= st::mediaCaptionSkip + _caption.countHeight(captionw); - if (botTop) { - painth -= botTop->height; - } - if (isBubbleBottom()) { - painth -= st::msgPadding.bottom(); - } - rthumb = style::rtlrect(paintx, painty, paintw, painth, width()); - } - } else { + : adjustedBubbleRounding(); + if (!bubble) { Assert(rounding.has_value()); fillImageShadow(p, rthumb, *rounding, context); } @@ -418,35 +368,7 @@ void Photo::draw(Painter &p, const PaintContext &context) const { } // date - if (!_caption.isEmpty()) { - p.setPen(stm->historyTextFg); - _parent->prepareCustomEmojiPaint(p, context, _caption); - auto top = painty + painth + st::mediaCaptionSkip; - if (botTop) { - botTop->text.drawLeftElided( - p, - st::msgPadding.left(), - top, - captionw, - _parent->width()); - top += botTop->height; - } - auto highlightRequest = context.computeHighlightCache(); - _caption.draw(p, { - .position = QPoint(st::msgPadding.left(), top), - .availableWidth = captionw, - .palette = &stm->textPalette, - .pre = stm->preCache.get(), - .blockquote = context.quoteCache(parent()->contentColorIndex()), - .colors = context.st->highlightColors(), - .spoiler = Ui::Text::DefaultSpoilerCache(), - .now = context.now, - .pausedEmoji = context.paused || On(PowerSaving::kEmojiChat), - .pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler), - .selection = context.selection, - .highlight = highlightRequest ? &*highlightRequest : nullptr, - }); - } else if (!inWebPage) { + if (!inWebPage && (!bubble || isBubbleBottom())) { auto fullRight = paintx + paintw; auto fullBottom = painty + painth; if (needInfoDisplay()) { @@ -686,26 +608,6 @@ TextState Photo::textState(QPoint point, StateRequest request) const { auto paintx = 0, painty = 0, paintw = width(), painth = height(); auto bubble = _parent->hasBubble(); - if (bubble && !_caption.isEmpty()) { - const auto captionw = paintw - - st::msgPadding.left() - - st::msgPadding.right(); - painth -= _caption.countHeight(captionw); - if (isBubbleBottom()) { - painth -= st::msgPadding.bottom(); - } - if (QRect(st::msgPadding.left(), painth, captionw, height() - painth).contains(point)) { - result = TextState(_parent, _caption.getState( - point - QPoint(st::msgPadding.left(), painth), - captionw, - request.forText())); - return result; - } - if (const auto botTop = _parent->Get()) { - painth -= botTop->height; - } - painth -= st::mediaCaptionSkip; - } if (QRect(paintx, painty, paintw, painth).contains(point)) { ensureDataMediaCreated(); result.link = (_spoiler && !_spoiler->revealed) @@ -723,7 +625,7 @@ TextState Photo::textState(QPoint point, StateRequest request) const { result.cursor = CursorState::Enlarge; } } - if (_caption.isEmpty() && _parent->media() == this) { + if (_parent->media() == this && (!_parent->hasBubble() || isBubbleBottom())) { auto fullRight = paintx + paintw; auto fullBottom = painty + painth; const auto bottomInfoResult = _parent->bottomInfoTextState( @@ -750,13 +652,13 @@ TextState Photo::textState(QPoint point, StateRequest request) const { return result; } -QSize Photo::sizeForGroupingOptimal(int maxWidth) const { +QSize Photo::sizeForGroupingOptimal(int maxWidth, bool last) const { const auto size = photoSize(); return { std::max(size.width(), 1), std::max(size.height(), 1)}; } QSize Photo::sizeForGrouping(int width) const { - return sizeForGroupingOptimal(width); + return sizeForGroupingOptimal(width, false); } void Photo::drawGrouped( @@ -916,6 +818,7 @@ bool Photo::needInfoDisplay() const { return _parent->data()->isSending() || _parent->data()->hasFailed() || _parent->isUnderCursor() + || (_parent->delegate()->elementContext() == Context::ChatPreview) || _parent->isLastAndSelfMessage(); } @@ -1102,27 +1005,14 @@ bool Photo::videoAutoplayEnabled() const { _data); } -TextForMimeData Photo::selectedText(TextSelection selection) const { - return _caption.toTextForMimeData(selection); -} - -SelectedQuote Photo::selectedQuote(TextSelection selection) const { - return Element::FindSelectedQuote(_caption, selection, _realParent); -} - -TextSelection Photo::selectionFromQuote(const SelectedQuote "e) const { - return Element::FindSelectionFromQuote(_caption, quote); -} - void Photo::hideSpoilers() { - _caption.setSpoilerRevealed(false, anim::type::instant); if (_spoiler) { _spoiler->revealed = false; } } bool Photo::needsBubble() const { - if (_storyId || !_caption.isEmpty()) { + if (_storyId) { return true; } const auto item = _parent->data(); @@ -1130,6 +1020,7 @@ bool Photo::needsBubble() const { && (item->repliesAreComments() || item->externalReply() || item->viaBot() + || !item->emptyText() || _parent->displayReply() || _parent->displayForwardedFrom() || _parent->displayFromName() @@ -1147,13 +1038,6 @@ bool Photo::isReadyForOpen() const { return _dataMedia->loaded(); } -void Photo::parentTextUpdated() { - _caption = (_parent->media() == this) - ? createCaption(_parent->data()) - : Ui::Text::String(); - history()->owner().requestViewResize(_parent); -} - void Photo::showPhoto(FullMsgId id) { _parent->delegate()->elementOpenPhoto(_data, id); } diff --git a/Telegram/SourceFiles/history/view/media/history_view_photo.h b/Telegram/SourceFiles/history/view/media/history_view_photo.h index 1c99839ce..8b50f52ac 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_photo.h +++ b/Telegram/SourceFiles/history/view/media/history_view_photo.h @@ -41,26 +41,13 @@ public: int width); ~Photo(); + bool hideMessageText() const override { + return false; + } + void draw(Painter &p, const PaintContext &context) const override; TextState textState(QPoint point, StateRequest request) const override; - [[nodiscard]] TextSelection adjustSelection( - TextSelection selection, - TextSelectType type) const override { - return _caption.adjustSelection(selection, type); - } - uint16 fullSelectionLength() const override { - return _caption.length(); - } - bool hasTextForCopy() const override { - return !_caption.isEmpty(); - } - - TextForMimeData selectedText(TextSelection selection) const override; - SelectedQuote selectedQuote(TextSelection selection) const override; - TextSelection selectionFromQuote( - const SelectedQuote "e) const override; - PhotoData *getPhoto() const override { return _data; } @@ -71,7 +58,7 @@ public: QPoint photoPosition, bool markFrameShown) const; - QSize sizeForGroupingOptimal(int maxWidth) const override; + QSize sizeForGroupingOptimal(int maxWidth, bool last) const override; QSize sizeForGrouping(int width) const override; void drawGrouped( Painter &p, @@ -88,22 +75,17 @@ public: QPoint point, StateRequest request) const override; - TextWithEntities getCaption() const override { - return _caption.toTextWithEntities(); - } void hideSpoilers() override; bool needsBubble() const override; bool customInfoLayout() const override { - return _caption.isEmpty(); + return true; } QPoint resolveCustomInfoRightBottom() const override; bool skipBubbleTail() const override { - return isRoundedInBubbleBottom() && _caption.isEmpty(); + return isRoundedInBubbleBottom(); } bool isReadyForOpen() const override; - void parentTextUpdated() override; - bool hasHeavyPart() const override; void unloadHeavyPart() override; @@ -168,7 +150,6 @@ private: const not_null _data; const FullStoryId _storyId; - Ui::Text::String _caption; mutable std::shared_ptr _dataMedia; mutable std::unique_ptr _streamed; const std::unique_ptr _spoiler; diff --git a/Telegram/SourceFiles/history/view/media/history_view_sticker.cpp b/Telegram/SourceFiles/history/view/media/history_view_sticker.cpp index 0566167e9..e75a360c7 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_sticker.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_sticker.cpp @@ -46,6 +46,7 @@ constexpr auto kMaxSizeFixed = 512; constexpr auto kMaxEmojiSizeFixed = 256; constexpr auto kPremiumMultiplier = (1 + 0.245 * 2); constexpr auto kEmojiMultiplier = 3; +constexpr auto kMessageEffectMultiplier = 2; [[nodiscard]] QImage CacheDiceImage( const QString &emoji, @@ -213,6 +214,10 @@ QSize Sticker::EmojiEffectSize() { return EmojiSize() * kEmojiMultiplier; } +QSize Sticker::MessageEffectSize() { + return EmojiSize() * kMessageEffectMultiplier; +} + QSize Sticker::EmojiSize() { const auto side = std::min(st::maxAnimatedEmojiSize, kMaxEmojiSizeFixed); return { side, side }; diff --git a/Telegram/SourceFiles/history/view/media/history_view_sticker.h b/Telegram/SourceFiles/history/view/media/history_view_sticker.h index c6bbbd0da..91a69f7e2 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_sticker.h +++ b/Telegram/SourceFiles/history/view/media/history_view_sticker.h @@ -91,6 +91,7 @@ public: not_null document); [[nodiscard]] static QSize UsualPremiumEffectSize(); [[nodiscard]] static QSize EmojiEffectSize(); + [[nodiscard]] static QSize MessageEffectSize(); [[nodiscard]] static QSize EmojiSize(); [[nodiscard]] static ClickHandlerPtr ShowSetHandler( not_null document); 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 12307a215..4f1a209e4 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/media/history_view_web_page.h" #include "core/application.h" +#include "countries/countries_instance.h" #include "base/qt/qt_key_modifiers.h" #include "window/window_session_controller.h" #include "iv/iv_instance.h" @@ -19,13 +20,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_photo_media.h" #include "data/data_session.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_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_sticker.h" +#include "history/view/history_view_cursor_state.h" +#include "history/view/history_view_message.h" +#include "history/view/history_view_reply.h" +#include "history/view/history_view_sponsored_click_handler.h" +#include "history/history.h" +#include "history/history_item_components.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "menu/menu_sponsored.h" @@ -36,6 +38,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/text/format_values.h" #include "ui/text/text_options.h" #include "ui/text/text_utilities.h" +#include "ui/toast/toast.h" #include "styles/style_chat.h" // AyuGram includes @@ -46,7 +49,9 @@ namespace HistoryView { namespace { constexpr auto kMaxOriginalEntryLines = 8192; +constexpr auto kFactcheckCollapsedLines = 3; constexpr auto kStickerSetLines = 3; +constexpr auto kFactcheckAboutDuration = 5 * crl::time(1000); [[nodiscard]] int ArticleThumbWidth(not_null thumb, int height) { const auto size = thumb->location(Data::PhotoSize::Thumbnail); @@ -152,6 +157,51 @@ constexpr auto kStickerSetLines = 3; }); } +[[nodiscard]] QString LookupFactcheckCountryIso2( + not_null item) { + const auto info = item->Get(); + return info ? info->data.country : QString(); +} + +[[nodiscard]] QString LookupFactcheckCountryName(const QString &iso2) { + const auto name = Countries::Instance().countryNameByISO2(iso2); + return name.isEmpty() ? iso2 : name; +} + +[[nodiscard]] ClickHandlerPtr AboutFactcheckClickHandler(QString iso2) { + return std::make_shared([=](ClickContext context) { + const auto my = context.other.value(); + const auto controller = my.sessionWindow.get(); + const auto show = my.show + ? my.show + : controller + ? controller->uiShow() + : nullptr; + if (show) { + const auto country = LookupFactcheckCountryName(iso2); + show->showToast({ + .text = { + tr::lng_factcheck_about(tr::now, lt_country, country) + }, + .duration = kFactcheckAboutDuration, + }); + } + }); +} + +[[nodiscard]] ClickHandlerPtr ToggleFactcheckClickHandler( + not_null view) { + const auto weak = base::make_weak(view); + return std::make_shared([=](ClickContext context) { + if (const auto strong = weak.get()) { + if (const auto factcheck = strong->Get()) { + factcheck->expanded = factcheck->expanded ? 0 : 1; + strong->history()->owner().requestViewResize(strong); + } + } + }); +} + [[nodiscard]] TextWithEntities PageToPhrase(not_null page) { const auto type = page->type; const auto text = Ui::Text::Upper(page->iv @@ -236,34 +286,59 @@ WebPage::WebPage( not_null data, MediaWebPageFlags flags) : Media(parent) -, _st(st::historyPagePreview) +, _st(data->type == WebPageType::Factcheck + ? st::factcheckPage + : st::historyPagePreview) , _data(data) -, _sponsoredData([&]() -> std::optional { - if (!(flags & MediaWebPageFlag::Sponsored)) { - return std::nullopt; - } - const auto &session = _parent->data()->history()->session(); - const auto details = session.sponsoredMessages().lookupDetails( - _parent->data()->fullId()); - auto result = std::make_optional(); - result->buttonText = details.buttonText; - result->isLinkInternal = details.isLinkInternal; - result->backgroundEmojiId = details.backgroundEmojiId; - result->colorIndex = details.colorIndex; - result->canReport = details.canReport; - return result; -}()) +, _flags(flags) , _siteName(st::msgMinWidth - _st.padding.left() - _st.padding.right()) , _title(st::msgMinWidth - _st.padding.left() - _st.padding.right()) -, _description(st::msgMinWidth - _st.padding.left() - _st.padding.right()) -, _flags(flags) { +, _description(st::msgMinWidth - _st.padding.left() - _st.padding.right()) { history()->owner().registerWebPageView(_data, _parent); } +void WebPage::setupAdditionalData() { + if (_flags & MediaWebPageFlag::Sponsored) { + _additionalData = std::make_unique(SponsoredData()); + const auto raw = sponsoredData(); + const auto &session = _parent->data()->history()->session(); + const auto details = session.sponsoredMessages().lookupDetails( + _parent->data()->fullId()); + raw->buttonText = details.buttonText; + raw->isLinkInternal = details.isLinkInternal ? 1 : 0; + raw->backgroundEmojiId = details.backgroundEmojiId; + raw->colorIndex = details.colorIndex; + raw->canReport = details.canReport ? 1 : 0; + } else if (_data->stickerSet) { + _additionalData = std::make_unique(StickerSetData()); + const auto raw = stickerSetData(); + for (const auto &sticker : _data->stickerSet->items) { + if (!sticker->sticker()) { + continue; + } + raw->views.push_back( + std::make_unique(_parent, sticker, true)); + } + const auto side = std::ceil(std::sqrt(raw->views.size())); + const auto box = UnitedLineHeight() * kStickerSetLines; + const auto single = box / side; + for (const auto &view : raw->views) { + view->setWebpagePart(); + view->initSize(single); + } + } else if (_data->type == WebPageType::Factcheck) { + _additionalData = std::make_unique(FactcheckData()); + } +} + QSize WebPage::countOptimalSize() { if (_data->pendingTill || _data->failed) { return { 0, 0 }; } + setupAdditionalData(); + + const auto sponsored = sponsoredData(); + const auto factcheck = factcheckData(); // Detect _openButtonWidth before counting paddings. _openButton = Ui::Text::String(); @@ -278,12 +353,10 @@ QSize WebPage::countOptimalSize() { PageToPhrase(_data), kMarkupTextOptions, context); - } else if (_sponsoredData) { - if (!_sponsoredData->buttonText.isEmpty()) { - _openButton.setText( - st::semiboldTextStyle, - Ui::Text::Upper(_sponsoredData->buttonText)); - } + } else if (sponsored && !sponsored->buttonText.isEmpty()) { + _openButton.setText( + st::semiboldTextStyle, + Ui::Text::Upper(sponsored->buttonText)); } const auto padding = inBubblePadding() + innerMargin(); @@ -292,33 +365,27 @@ QSize WebPage::countOptimalSize() { _dataVersion = _data->version; _openl = nullptr; _attach = nullptr; - _collage = PrepareCollageMedia(_parent->data(), _data->collage); + const auto item = _parent->data(); + _collage = PrepareCollageMedia(item, _data->collage); 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); + if (factcheck) { + factcheck->footer = Ui::Text::String( + st::factcheckFooterStyle, + tr::lng_factcheck_bottom( + tr::now, + lt_country, + LookupFactcheckCountryName( + LookupFactcheckCountryIso2(item))), + kDefaultTextOptions, + min); + } } const auto lineHeight = UnitedLineHeight(); - if (_data->stickerSet && !_stickerSet) { - _stickerSet = std::make_unique(); - for (const auto &sticker : _data->stickerSet->items) { - if (!sticker->sticker()) { - continue; - } - _stickerSet->views.push_back( - std::make_unique(_parent, sticker, true)); - } - const auto side = std::ceil(std::sqrt(_stickerSet->views.size())); - const auto box = lineHeight * kStickerSetLines; - const auto single = box / side; - for (const auto &view : _stickerSet->views) { - view->setWebpagePart(); - view->initSize(single); - } - } - - if (!_openl && (!_data->url.isEmpty() || _sponsoredData)) { + if (!_openl && (!_data->url.isEmpty() || sponsored || factcheck)) { const auto original = _parent->data()->originalText(); const auto previewOfHiddenUrl = [&] { if (_data->type == WebPageType::BotApp) { @@ -356,27 +423,31 @@ QSize WebPage::countOptimalSize() { } return true; }(); - _openl = _data->iv - ? IvClickHandler(_data, original) - : (previewOfHiddenUrl || UrlClickHandler::IsSuspicious( - _data->url)) - ? std::make_shared(_data->url) - : std::make_shared(_data->url, true); - if (_data->document - && (_data->document->isWallPaper() - || _data->document->isTheme())) { - _openl = std::make_shared( - std::move(_openl), - _data->document, - _parent->data()->fullId()); - } - if (_sponsoredData) { - _openl = SponsoredLink( - _data->url, - _sponsoredData->isLinkInternal); - - if (_sponsoredData->canReport) { - _sponsoredData->hintLink = AboutSponsoredClickHandler(); + if (sponsored) { + _openl = SponsoredLink(_data->url, sponsored->isLinkInternal); + if (sponsored->canReport) { + sponsored->hint.link = AboutSponsoredClickHandler(); + } + } else if (factcheck) { + const auto item = _parent->data(); + const auto iso2 = LookupFactcheckCountryIso2(item); + if (!iso2.isEmpty()) { + factcheck->hint.link = AboutFactcheckClickHandler(iso2); + } + } else { + _openl = _data->iv + ? IvClickHandler(_data, original) + : (previewOfHiddenUrl || UrlClickHandler::IsSuspicious( + _data->url)) + ? std::make_shared(_data->url) + : std::make_shared(_data->url, true); + if (_data->document + && (_data->document->isWallPaper() + || _data->document->isTheme())) { + _openl = std::make_shared( + std::move(_openl), + _data->document, + _parent->data()->fullId()); } } } @@ -460,7 +531,12 @@ QSize WebPage::countOptimalSize() { const auto siteNameHeight = _siteName.isEmpty() ? 0 : lineHeight; const auto titleMinHeight = _title.isEmpty() ? 0 : lineHeight; - const auto descMaxLines = isLogEntryOriginal() + const auto factcheckMetrics = factcheck + ? computeFactcheckMetrics(_description.minHeight()) + : FactcheckMetrics(); + const auto descMaxLines = factcheck + ? factcheckMetrics.lines + : isLogEntryOriginal() ? kMaxOriginalEntryLines : (3 + (siteNameHeight ? 0 : 1) + (titleMinHeight ? 0 : 1)); const auto descriptionMinHeight = _description.isEmpty() @@ -490,6 +566,10 @@ QSize WebPage::countOptimalSize() { _description.maxWidth() + articlePhotoMaxWidth); minHeight += descriptionMinHeight; } + if (factcheck && factcheck->expanded) { + accumulate_max(maxWidth, factcheck->footer.maxWidth()); + minHeight += st::factcheckFooterSkip + factcheck->footer.minHeight(); + } if (_attach) { const auto attachAtTop = _siteName.isEmpty() && _title.isEmpty() @@ -521,15 +601,16 @@ QSize WebPage::countOptimalSize() { if (_asArticle) { minHeight = resizeGetHeight(maxWidth); } - if (_sponsoredData && _sponsoredData->canReport) { - _sponsoredData->widthBeforeHint - = st::webPageTitleStyle.font->width(siteName); + if (const auto hint = hintData()) { + hint->widthBefore = st::webPageTitleStyle.font->width(siteName); const auto &font = st::webPageSponsoredHintFont; - _sponsoredData->hintSize = QSize( - font->width(tr::lng_sponsored_message_revenue_button(tr::now)) - + font->height, + hint->text = sponsored + ? tr::lng_sponsored_message_revenue_button(tr::now) + : tr::lng_factcheck_whats_this(tr::now); + hint->size = QSize( + font->width(hint->text) + font->height, font->height); - maxWidth += _sponsoredData->hintSize.width(); + maxWidth += hint->size.width(); } return { maxWidth, minHeight }; } @@ -543,9 +624,23 @@ QSize WebPage::countCurrentSize(int newWidth) { const auto innerWidth = newWidth - rect::m::sum::h(padding); auto newHeight = 0; - const auto specialRightPix = (_sponsoredData || _stickerSet); + const auto stickerSet = stickerSetData(); + const auto factcheck = factcheckData(); + const auto specialRightPix = (sponsoredData() || stickerSet); const auto lineHeight = UnitedLineHeight(); - const auto linesMax = (specialRightPix || isLogEntryOriginal()) + const auto factcheckMetrics = factcheck + ? computeFactcheckMetrics(_description.countHeight(innerWidth)) + : FactcheckMetrics(); + if (factcheck) { + factcheck->expandable = factcheckMetrics.expandable ? 1 : 0; + factcheck->expanded = factcheckMetrics.expanded ? 1 : 0; + _openl = factcheck->expandable + ? ToggleFactcheckClickHandler(_parent) + : nullptr; + } + const auto linesMax = factcheck + ? (factcheckMetrics.lines + 1) + : (specialRightPix || isLogEntryOriginal()) ? kMaxOriginalEntryLines : 5; const auto siteNameHeight = _siteNameLines ? lineHeight : 0; @@ -554,7 +649,7 @@ QSize WebPage::countCurrentSize(int newWidth) { if (asArticle() || specialRightPix) { constexpr auto kSponsoredUserpicLines = 2; _pixh = lineHeight - * (_stickerSet + * (stickerSet ? kStickerSetLines : specialRightPix ? kSponsoredUserpicLines @@ -622,6 +717,11 @@ QSize WebPage::countCurrentSize(int newWidth) { newHeight += _descriptionLines * lineHeight; } } + if (factcheck && factcheck->expanded) { + factcheck->footerHeight = st::factcheckFooterSkip + + factcheck->footer.countHeight(innerWidth); + newHeight += factcheck->footerHeight; + } if (_attach) { const auto attachAtTop = !_siteNameLines @@ -677,8 +777,14 @@ void WebPage::ensurePhotoMediaCreated() const { } bool WebPage::hasHeavyPart() const { + if (const auto stickerSet = stickerSetData()) { + for (const auto &part : stickerSet->views) { + if (part->hasHeavyPart()) { + return true; + } + } + } return _photoMedia - || (_stickerSet) || (_attach ? _attach->hasHeavyPart() : false); } @@ -688,6 +794,11 @@ void WebPage::unloadHeavyPart() { } _description.unloadPersistentAnimation(); _photoMedia = nullptr; + if (const auto stickerSet = stickerSetData()) { + for (const auto &part : stickerSet->views) { + part->unloadHeavyPart(); + } + } } void WebPage::draw(Painter &p, const PaintContext &context) const { @@ -708,22 +819,26 @@ void WebPage::draw(Painter &p, const PaintContext &context) const { auto tshift = inner.top(); auto paintw = inner.width(); - const auto asSponsored = (!!_sponsoredData); + const auto sponsored = sponsoredData(); + const auto factcheck = factcheckData(); const auto selected = context.selected(); const auto view = parent(); const auto from = view->data()->contentColorsFrom(); - const auto colorIndex = (asSponsored && _sponsoredData->colorIndex) - ? _sponsoredData->colorIndex + const auto colorIndex = factcheck + ? 0 // red + : (sponsored && sponsored->colorIndex) + ? sponsored->colorIndex : from ? from->colorIndex() : view->colorIndex(); const auto cache = context.outbg ? stm->replyCache[st->colorPatternIndex(colorIndex)].get() : st->coloredReplyCache(selected, colorIndex).get(); - const auto backgroundEmojiId = (asSponsored - && _sponsoredData->backgroundEmojiId) - ? _sponsoredData->backgroundEmojiId + const auto backgroundEmojiId = factcheck + ? DocumentId() + : (sponsored && sponsored->backgroundEmojiId) + ? sponsored->backgroundEmojiId : from ? from->backgroundEmojiId() : DocumentId(); @@ -751,6 +866,16 @@ void WebPage::draw(Painter &p, const PaintContext &context) const { if (!backgroundEmojiCache->frames[0].isNull()) { FillBackgroundEmoji(p, outer, false, *backgroundEmojiCache); } + } else if (factcheck && factcheck->expandable) { + const auto &icon = factcheck->expanded ? _st.collapse : _st.expand; + const auto &position = factcheck->expanded + ? _st.collapsePosition + : _st.expandPosition; + icon.paint( + p, + outer.x() + outer.width() - icon.width() - position.x(), + outer.y() + outer.height() - icon.height() - position.y(), + width()); } if (_ripple) { @@ -761,8 +886,8 @@ void WebPage::draw(Painter &p, const PaintContext &context) const { } auto lineHeight = UnitedLineHeight(); - if (_stickerSet) { - const auto viewsCount = _stickerSet->views.size(); + if (const auto stickerSet = stickerSetData()) { + const auto viewsCount = stickerSet->views.size(); const auto box = _pixh; const auto topLeft = QPoint(inner.left() + paintw - box, tshift); const auto side = std::ceil(std::sqrt(viewsCount)); @@ -773,7 +898,7 @@ void WebPage::draw(Painter &p, const PaintContext &context) const { if (viewsCount <= index) { break; } - const auto &view = _stickerSet->views[index]; + const auto &view = stickerSet->views[index]; const auto size = view->countOptimalSize(); const auto offsetX = (single - size.width()) / 2.; const auto offsetY = (single - size.height()) / 2.; @@ -828,7 +953,7 @@ void WebPage::draw(Painter &p, const PaintContext &context) const { st->msgSelectOverlay(), st->msgSelectOverlayCorners(Ui::CachedCornerRadius::Small)); } - if (!asSponsored) { + if (!sponsored) { // Ignore photo width in sponsored messages, // as its width only affects the title. paintw -= pw + st::webPagePhotoDelta; @@ -856,35 +981,31 @@ void WebPage::draw(Painter &p, const PaintContext &context) const { endskip, false, context.selection); - if (asSponsored - && _sponsoredData->canReport - && (paintw > - _sponsoredData->widthBeforeHint - + _sponsoredData->hintSize.width())) { - if (_sponsoredData->hintRipple) { - _sponsoredData->hintRipple->paint( - p, - _sponsoredData->lastHintPos.x(), - _sponsoredData->lastHintPos.y(), - width(), - &cache->bg); - if (_sponsoredData->hintRipple->empty()) { - _sponsoredData->hintRipple = nullptr; - } - } - + const auto hint = hintData(); + if (hint && (paintw > hint->widthBefore + hint->size.width())) { auto color = cache->icon; color.setAlphaF(color.alphaF() * 0.15); const auto height = st::webPageSponsoredHintFont->height; const auto radius = height / 2; - _sponsoredData->lastHintPos = QPointF( - radius + inner.left() + _sponsoredData->widthBeforeHint, + hint->lastPosition = QPointF( + radius + inner.left() + hint->widthBefore, tshift + (_siteName.style()->font->height - height) / 2.); - const auto rect = QRectF( - _sponsoredData->lastHintPos, - _sponsoredData->hintSize); + + if (hint->ripple) { + hint->ripple->paint( + p, + hint->lastPosition.x(), + hint->lastPosition.y(), + width(), + &cache->bg); + if (hint->ripple->empty()) { + hint->ripple = nullptr; + } + } + + const auto rect = QRectF(hint->lastPosition, hint->size); auto hq = PainterHighQualityEnabler(p); p.setPen(Qt::NoPen); p.setBrush(color); @@ -893,10 +1014,7 @@ void WebPage::draw(Painter &p, const PaintContext &context) const { p.setPen(cache->icon); p.setBrush(Qt::NoBrush); p.setFont(st::webPageSponsoredHintFont); - p.drawText( - rect, - tr::lng_sponsored_message_revenue_button(tr::now), - style::al_center); + p.drawText(rect, hint->text, style::al_center); } tshift += lineHeight; @@ -907,7 +1025,7 @@ void WebPage::draw(Painter &p, const PaintContext &context) const { const auto endskip = _title.hasSkipBlock() ? _parent->skipBlockWidth() : 0; - const auto titleWidth = asSponsored + const auto titleWidth = sponsored ? (paintw - _pixh - st::webPagePhotoDelta) : paintw; _title.drawLeftElided( @@ -948,6 +1066,23 @@ void WebPage::draw(Painter &p, const PaintContext &context) const { ? (_descriptionLines * lineHeight) : _description.countHeight(paintw); } + if (factcheck && factcheck->expanded) { + const auto skip = st::factcheckFooterSkip; + const auto line = st::lineWidth; + const auto separatorTop = tshift + skip / 2; + + auto color = cache->icon; + color.setAlphaF(color.alphaF() * 0.3); + p.fillRect(inner.left(), separatorTop, paintw, line, color); + + p.setPen(cache->icon); + factcheck->footer.draw(p, { + .position = { inner.left(), tshift + skip }, + .outerWidth = width(), + .availableWidth = paintw, + }); + tshift += factcheck->footerHeight; + } if (_attach) { const auto attachAtTop = !_siteNameLines && !_titleLines @@ -1057,16 +1192,38 @@ bool WebPage::asArticle() const { return _asArticle && (_data->photo != nullptr); } +WebPage::StickerSetData *WebPage::stickerSetData() const { + return std::get_if(_additionalData.get()); +} + +WebPage::SponsoredData *WebPage::sponsoredData() const { + return std::get_if(_additionalData.get()); +} + +WebPage::FactcheckData *WebPage::factcheckData() const { + return std::get_if(_additionalData.get()); +} + +WebPage::HintData *WebPage::hintData() const { + if (const auto sponsored = sponsoredData()) { + return sponsored->hint.link ? &sponsored->hint : nullptr; + } else if (const auto factcheck = factcheckData()) { + return factcheck->hint.link ? &factcheck->hint : nullptr; + } + return nullptr; +} + TextState WebPage::textState(QPoint point, StateRequest request) const { auto result = TextState(_parent); if (width() < rect::m::sum::h(st::msgPadding) + 1) { return result; } + const auto sponsored = sponsoredData(); const auto bubble = _attach ? _attach->bubbleMargins() : QMargins(); const auto full = Rect(currentSize()); auto outer = full - inBubblePadding(); - if (_sponsoredData) { + if (sponsored) { outer.translate(0, st::msgDateFont->height); } const auto inner = outer - innerMargin(); @@ -1181,16 +1338,15 @@ TextState WebPage::textState(QPoint point, StateRequest request) const { } } } - if ((!result.link || _sponsoredData) && outer.contains(point)) { + if ((!result.link || sponsored) && outer.contains(point)) { result.link = _openl; } - if (_sponsoredData && _sponsoredData->canReport) { - const auto contains = QRectF( - _sponsoredData->lastHintPos, - _sponsoredData->hintSize).contains(point - - QPoint(0, st::msgDateFont->height)); - if (contains) { - result.link = _sponsoredData->hintLink; + if (const auto hint = hintData()) { + const auto check = point + - QPoint(0, sponsored ? st::msgDateFont->height : 0); + const auto hintRect = QRectF(hint->lastPosition, hint->size); + if (hintRect.contains(check)) { + result.link = hint->link; } } _lastPoint = point - outer.topLeft(); @@ -1262,25 +1418,25 @@ void WebPage::clickHandlerActiveChanged( void WebPage::clickHandlerPressedChanged( const ClickHandlerPtr &p, bool pressed) { - if (_sponsoredData && _sponsoredData->hintLink == p) { + const auto hint = hintData(); + if (hint && hint->link == p) { if (pressed) { - if (!_sponsoredData->hintRipple) { + if (!hint->ripple) { const auto owner = &parent()->history()->owner(); - auto ripple = std::make_unique( + hint->ripple = std::make_unique( st::defaultRippleAnimation, Ui::RippleAnimation::RoundRectMask( - _sponsoredData->hintSize, + hint->size, _st.radius), [=] { owner->requestViewRepaint(parent()); }); - _sponsoredData->hintRipple = std::move(ripple); } const auto full = Rect(currentSize()); const auto outer = full - inBubblePadding(); - _sponsoredData->hintRipple->add(_lastPoint + hint->ripple->add(_lastPoint + outer.topLeft() - - _sponsoredData->lastHintPos.toPoint()); - } else if (_sponsoredData->hintRipple) { - _sponsoredData->hintRipple->lastStop(); + - hint->lastPosition.toPoint()); + } else if (hint->ripple) { + hint->ripple->lastStop(); } return; } @@ -1393,6 +1549,22 @@ bool WebPage::isLogEntryOriginal() const { return _parent->data()->isAdminLogEntry() && _parent->media() != this; } +WebPage::FactcheckMetrics WebPage::computeFactcheckMetrics( + int fullHeight) const { + const auto possible = fullHeight / st::normalFont->height; + //const auto expandable = (possible > kFactcheckCollapsedLines + 1); + // Now always expandable because of the footer. + const auto expandable = true; + const auto check = _parent->Get(); + const auto expanded = check && check->expanded; + const auto allowExpanding = (expanded || !expandable); + return { + .lines = allowExpanding ? possible : kFactcheckCollapsedLines, + .expandable = expandable, + .expanded = expanded, + }; +} + int WebPage::bottomInfoPadding() const { if (!isBubbleBottom()) { return 0; diff --git a/Telegram/SourceFiles/history/view/media/history_view_web_page.h b/Telegram/SourceFiles/history/view/media/history_view_web_page.h index 86f7ec817..31bc076b0 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_web_page.h +++ b/Telegram/SourceFiles/history/view/media/history_view_web_page.h @@ -35,6 +35,9 @@ public: void draw(Painter &p, const PaintContext &context) const override; TextState textState(QPoint point, StateRequest request) const override; + bool aboveTextByDefault() const override { + return false; + } bool hideMessageText() const override { return false; } @@ -101,6 +104,44 @@ public: ~WebPage(); private: + struct FactcheckMetrics { + int lines = 0; + bool expandable = false; + bool expanded = false; + }; + struct HintData { + QSize size; + QPointF lastPosition; + QString text; + int widthBefore = 0; + std::unique_ptr ripple; + ClickHandlerPtr link; + }; + struct StickerSetData { + std::vector> views; + }; + struct SponsoredData { + QString buttonText; + + uint64 backgroundEmojiId = 0; + uint8 colorIndex : 6 = 0; + uint8 isLinkInternal : 1 = 0; + uint8 canReport : 1 = 0; + + HintData hint; + }; + struct FactcheckData { + HintData hint; + Ui::Text::String footer; + uint32 footerHeight : 30 = 0; + uint32 expandable : 1 = 0; + uint32 expanded : 1 = 0; + }; + using AdditionalData = std::variant< + StickerSetData, + SponsoredData, + FactcheckData>; + void playAnimation(bool autoplay) override; QSize countOptimalSize() override; QSize countCurrentSize(int newWidth) override; @@ -124,36 +165,26 @@ private: const ClickHandlerPtr &link) const; [[nodiscard]] bool asArticle() const; + [[nodiscard]] StickerSetData *stickerSetData() const; + [[nodiscard]] SponsoredData *sponsoredData() const; + [[nodiscard]] FactcheckData *factcheckData() const; + [[nodiscard]] HintData *hintData() const; + + [[nodiscard]] FactcheckMetrics computeFactcheckMetrics( + int fullHeight) const; + + void setupAdditionalData(); + const style::QuoteStyle &_st; const not_null _data; + const MediaWebPageFlags _flags; + std::vector> _collage; ClickHandlerPtr _openl; std::unique_ptr _attach; mutable std::shared_ptr _photoMedia; mutable std::unique_ptr _ripple; - struct StickerSet final { - std::vector> views; - }; - - std::unique_ptr _stickerSet; - - struct SponsoredData final { - QString buttonText; - bool isLinkInternal = false; - - uint64 backgroundEmojiId = 0; - uint8 colorIndex : 6 = 0; - - bool canReport = false; - QSize hintSize; - QPointF lastHintPos; - int widthBeforeHint = 0; - std::unique_ptr hintRipple; - ClickHandlerPtr hintLink; - }; - mutable std::optional _sponsoredData; - int _dataVersion = -1; int _siteNameLines = 0; int _descriptionLines = 0; @@ -172,7 +203,7 @@ private: int _pixw = 0; int _pixh = 0; - const MediaWebPageFlags _flags; + std::unique_ptr _additionalData; }; diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp b/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp index a249ae917..436416d43 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp @@ -186,7 +186,7 @@ InlineList::Button InlineList::prepareButtonWithId(const ReactionId &id) { customId, _customEmojiRepaint); } else { - _owner->preloadImageFor(id); + _owner->preloadReactionImageFor(id); } return result; } @@ -443,9 +443,7 @@ void InlineList::paint( } } if (!button.custom && button.image.isNull()) { - button.image = _owner->resolveImageFor( - button.id, - ::Data::Reactions::ImageSize::InlineList); + button.image = _owner->resolveReactionImageFor(button.id); } const auto textFg = !inbubble diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_button.h b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_button.h index 495f33847..4a1842d99 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_button.h +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_button.h @@ -138,7 +138,7 @@ public: Manager( QWidget *wheelEventsTarget, Fn buttonUpdate, - IconFactory iconFactory); + IconFactory iconFactory = nullptr); ~Manager(); using ReactionId = ::Data::ReactionId; 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 ab401b9fe..fe01819aa 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/scroll_area.h" #include "ui/widgets/popup_menu.h" #include "ui/widgets/shadow.h" +#include "ui/wrap/vertical_layout.h" #include "ui/text/text_custom_emoji.h" #include "ui/text/text_utilities.h" #include "ui/platform/ui_platform_utility.h" @@ -23,8 +24,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/stickers/data_custom_emoji.h" #include "lang/lang_keys.h" #include "main/main_session.h" +#include "menu/menu_send.h" #include "chat_helpers/emoji_list_widget.h" #include "chat_helpers/stickers_list_footer.h" +#include "chat_helpers/stickers_list_widget.h" #include "window/window_session_controller.h" #include "boxes/premium_preview_box.h" #include "mainwidget.h" @@ -128,15 +131,11 @@ UnifiedFactoryOwner::UnifiedFactoryOwner( const auto inStrip = _strip ? _strip->count() : 0; _unifiedIdsList.reserve(reactions.size()); for (const auto &reaction : reactions) { - if (const auto id = reaction.id.custom()) { - _unifiedIdsList.push_back(id); - } else { - _unifiedIdsList.push_back(reaction.selectAnimation->id); - } + _unifiedIdsList.push_back(reaction.selectAnimation->id); const auto unifiedId = _unifiedIdsList.back(); - if (!reaction.id.custom()) { - _defaultReactionIds.emplace(unifiedId, reaction.id.emoji()); + if (unifiedId != reaction.id.custom()) { + _defaultReactionIds.emplace(unifiedId, reaction.id); } if (index + 1 < inStrip) { _defaultReactionInStripMap.emplace(unifiedId, index++); @@ -170,7 +169,7 @@ Data::ReactionId UnifiedFactoryOwner::lookupReactionId( DocumentId unifiedId) const { const auto i = _defaultReactionIds.find(unifiedId); return (i != end(_defaultReactionIds)) - ? Data::ReactionId{ i->second } + ? i->second : Data::ReactionId{ unifiedId }; } @@ -179,21 +178,23 @@ UnifiedFactoryOwner::RecentFactory UnifiedFactoryOwner::factory() { -> std::unique_ptr { const auto tag = Data::CustomEmojiManager::SizeTag::Large; const auto sizeOverride = st::reactStripImage; - const auto isDefaultReaction = _defaultReactionIds.contains(id); + const auto i = _defaultReactionIds.find(id); + const auto isDefaultReaction = (i != end(_defaultReactionIds)) + && !i->second.custom(); const auto manager = &_session->data().customEmojiManager(); auto result = isDefaultReaction ? std::make_unique( manager->create(id, std::move(repaint), tag, sizeOverride), _defaultReactionShift) : manager->create(id, std::move(repaint), tag); - const auto i = _defaultReactionInStripMap.find(id); - if (i != end(_defaultReactionInStripMap)) { + const auto j = _defaultReactionInStripMap.find(id); + if (j != end(_defaultReactionInStripMap)) { Assert(_strip != nullptr); return std::make_unique( std::move(result), _strip, -_stripPaintOneShift, - i->second); + j->second); } return result; }; @@ -205,8 +206,9 @@ Selector::Selector( std::shared_ptr show, const Data::PossibleItemReactionsRef &reactions, TextWithEntities about, - IconFactory iconFactory, Fn close, + IconFactory iconFactory, + Fn paused, bool child) : Selector( parent, @@ -215,14 +217,18 @@ Selector::Selector( reactions, (reactions.customAllowed ? ChatHelpers::EmojiListMode::FullReactions - : ChatHelpers::EmojiListMode::RecentReactions), + : reactions.stickers.empty() + ? ChatHelpers::EmojiListMode::RecentReactions + : ChatHelpers::EmojiListMode::MessageEffects), {}, std::move(about), - iconFactory, - close, + std::move(iconFactory), + std::move(paused), + std::move(close), child) { } +#if 0 // not ready Selector::Selector( not_null parent, const style::EmojiPan &st, @@ -243,6 +249,7 @@ Selector::Selector( close, child) { } +#endif Selector::Selector( not_null parent, @@ -253,6 +260,7 @@ Selector::Selector( std::vector recent, TextWithEntities about, IconFactory iconFactory, + Fn paused, Fn close, bool child) : RpWidget(parent) @@ -261,19 +269,18 @@ Selector::Selector( , _reactions(reactions) , _recent(std::move(recent)) , _listMode(mode) +, _paused(std::move(paused)) , _jumpedToPremium([=] { close(false); }) , _cachedRound( QSize(2 * st::reactStripSkip + st::reactStripSize, st::reactStripHeight), st::reactionCornerShadow, st::reactStripHeight) -, _strip(iconFactory - ? std::make_unique( - _st, - QRect(0, 0, st::reactStripSize, st::reactStripSize), - st::reactStripImage, - crl::guard(this, [=] { update(_inner); }), - std::move(iconFactory)) - : nullptr) +, _strip(std::make_unique( + _st, + QRect(0, 0, st::reactStripSize, st::reactStripSize), + st::reactStripImage, + crl::guard(this, [=] { update(_inner); }), + std::move(iconFactory))) , _about(about.empty() ? nullptr : std::make_unique( @@ -381,6 +388,32 @@ int Selector::extendTopForCategoriesAndAbout(int width) const { return std::max(extendTopForCategories(), _aboutExtend); } +int Selector::opaqueExtendTopAbout(int width) const { + if (_about) { + const auto padding = _st.aboutPadding; + const auto available = width - padding.left() - padding.right(); + const auto countAboutHeight = [&](int width) { + _about->resizeToWidth(width); + return _about->height(); + }; + const auto desired = Ui::FindNiceTooltipWidth( + std::min(available, _st.about.minWidth * 2), + available, + countAboutHeight); + + _about->resizeToWidth(desired); + _aboutExtend = padding.top() + _about->height() + padding.bottom(); + } else { + _aboutExtend = 0; + } + return _aboutExtend; +} + +void Selector::setOpaqueHeightExpand(int expand, Fn apply) { + _opaqueHeightExpand = expand; + _opaqueApplyHeightExpand = std::move(apply); +} + int Selector::minimalHeight() const { return _skipy + std::min(_recentRows * _size, st::emojiPanMinHeight) @@ -403,17 +436,19 @@ void Selector::initGeometry(int innerTop) { const auto forAbout = width - margins.left() - margins.right(); _collapsedTopSkip = _useTransparency ? (extendTopForCategoriesAndAbout(forAbout) + _specialExpandTopSkip) - : 0; - _topAddOnExpand = extendTopForCategories() - - _aboutExtend - + _specialExpandTopSkip; + : opaqueExtendTopAbout(forAbout); + _topAddOnExpand = _collapsedTopSkip - _aboutExtend; const auto height = margins.top() + _aboutExtend + innerHeight + margins.bottom(); const auto left = style::RightToLeft() ? 0 : (parent.width() - width); - const auto top = innerTop - margins.top() - _collapsedTopSkip; - const auto add = _st.icons.stripBubble.height() - margins.bottom(); + const auto top = innerTop + - margins.top() + - (_useTransparency ? _collapsedTopSkip : 0); + const auto add = _useTransparency + ? (_st.icons.stripBubble.height() - margins.bottom()) + : 0; _outer = QRect(0, _collapsedTopSkip - _aboutExtend, width, height); _outerWithBubble = _outer.marginsAdded({ 0, 0, 0, add }); setGeometry(_outerWithBubble.marginsAdded( @@ -574,7 +609,7 @@ void Selector::paintCollapsed(QPainter &p) { } p.drawImage(_outer.topLeft(), _paintBuffer); } else { - p.fillRect(_inner, _st.bg); + p.fillRect(_outer.marginsRemoved(marginsForShadow()), _st.bg); } _strip->paint( p, @@ -653,6 +688,13 @@ Selector::ExpandingRects Selector::updateExpandingRects(float64 progress) { ? int(base::SafeRound( radius - sqrt(categories * (2 * radius - categories)))) : 0; + + if (!_useTransparency && _opaqueApplyHeightExpand) { + Ui::PostponeCall(this, [=] { + _opaqueApplyHeightExpand(y() + outer.y() + outer.height()); + }); + } + return { .categories = QRect(inner.x(), inner.y(), inner.width(), categories), .list = list, @@ -875,8 +917,9 @@ void Selector::expand() { const auto heightLimit = _reactions.customAllowed ? st::emojiPanMaxHeight : minimalHeight(); + const auto opaqueAdded = _useTransparency ? 0 : _opaqueHeightExpand; const auto willBeHeight = std::min( - parent.height() - y(), + parent.height() - y() + opaqueAdded, margins.top() + heightLimit + margins.bottom()); const auto additionalBottom = willBeHeight - height(); const auto additional = _specialExpandTopSkip + additionalBottom; @@ -901,7 +944,9 @@ void Selector::expand() { } _expanded = true; _paintBuffer = _cachedRound.PrepareImage(size()); - _expanding.start([=] { update(); }, 0., full, full); + _expanding.start([=] { + update(); + }, 0., full, full); }); } @@ -921,32 +966,80 @@ void Selector::createList() { &_show->session(), _strip ? _reactions.recent : std::vector(), _strip.get()); - _scroll = Ui::CreateChild(this, _reactions.customAllowed + _scroll = Ui::CreateChild(this, !_useTransparency + ? st::emojiScroll + : _reactions.customAllowed ? st::reactPanelScroll : st::reactPanelScrollRounded); _scroll->hide(); + const auto effects = !_reactions.stickers.empty(); const auto st = lifetime().make_state(_st); st->padding.setTop(_skipy); if (!_reactions.customAllowed) { st->bg = st::transparent; } - _list = _scroll->setOwnedWidget( - object_ptr(_scroll, EmojiListDescriptor{ + auto lists = _scroll->setOwnedWidget( + object_ptr(_scroll)); + auto recentList = _strip + ? _unifiedFactoryOwner->unifiedIdsList() + : _recent; + auto freeEffects = base::flat_set(); + if (effects) { + auto free = base::flat_set(); + free.reserve(_reactions.recent.size()); + for (const auto &reaction : _reactions.recent) { + if (!reaction.premium) { + free.emplace(reaction.id); + } + } + for (const auto &id : recentList) { + const auto reactionId = _strip + ? _unifiedFactoryOwner->lookupReactionId(id) + : Data::ReactionId{ id }; + if (free.contains(reactionId)) { + freeEffects.insert(id); + } + } + } + _list = lists->add( + object_ptr(lists, EmojiListDescriptor{ .show = _show, .mode = _listMode, - .paused = [] { return false; }, - .customRecentList = (_strip - ? _unifiedFactoryOwner->unifiedIdsList() - : _recent), + .paused = _paused ? _paused : [] { return false; }, + .customRecentList = std::move(recentList), .customRecentFactory = _unifiedFactoryOwner->factory(), + .freeEffects = std::move(freeEffects), .st = st, - }) - ).data(); + })); + if (!_reactions.stickers.empty()) { + auto descriptors = ranges::views::all( + _reactions.stickers + ) | ranges::view::transform([](const Data::Reaction &reaction) { + return ChatHelpers::StickerCustomRecentDescriptor{ + reaction.selectAnimation, + reaction.title + }; + }) | ranges::to_vector; + _stickers = lists->add( + object_ptr( + lists, + StickersListDescriptor{ + .show = _show, + .mode = StickersListMode::MessageEffects, + .paused = _paused ? _paused : [] { return false; }, + .customRecentList = std::move(descriptors), + .st = st, + })); + } _list->escapes() | rpl::start_to_stream(_escapes, _list->lifetime()); - _list->customChosen( + rpl::merge( + _list->customChosen(), + (_stickers + ? _stickers->chosen() + : rpl::never()) ) | rpl::start_with_next([=](ChatHelpers::FileChosen data) { _chosen.fire({ .id = _unifiedFactoryOwner->lookupReactionId(data.document->id), @@ -986,23 +1079,25 @@ void Selector::createList() { _shadow->show(); } const auto geometry = inner.marginsRemoved(_st.margin); - _list->move(0, 0); - _list->resizeToWidth(geometry.width()); + lists->move(0, 0); + lists->resizeToWidth(geometry.width()); _list->refreshEmoji(); - _list->show(); + lists->show(); const auto updateVisibleTopBottom = [=] { const auto scrollTop = _scroll->scrollTop(); const auto scrollBottom = scrollTop + _scroll->height(); - _list->setVisibleTopBottom(scrollTop, scrollBottom); + lists->setVisibleTopBottom(scrollTop, scrollBottom); }; _scroll->scrollTopChanges( - ) | rpl::start_with_next(updateVisibleTopBottom, _list->lifetime()); + ) | rpl::start_with_next(updateVisibleTopBottom, lists->lifetime()); _list->scrollToRequests( ) | rpl::start_with_next([=](int y) { _scroll->scrollToY(y); - _shadow->update(); + if (_shadow) { + _shadow->update(); + } }, _list->lifetime()); _scroll->setGeometry(inner.marginsRemoved({ @@ -1011,7 +1106,38 @@ void Selector::createList() { 0, 0, })); - _list->setMinimalHeight(geometry.width(), _scroll->height()); + if (_stickers) { + _list->setMinimalHeight(geometry.width(), 0); + _stickers->setMinimalHeight(geometry.width(), 0); + + _list->searchQueries( + ) | rpl::start_with_next([=](std::vector &&query) { + _stickers->applySearchQuery(std::move(query)); + updateVisibleTopBottom(); + }, _stickers->lifetime()); + + rpl::combine( + _list->recentShownCount(), + _stickers->recentShownCount() + ) | rpl::start_with_next([=](int emoji, int stickers) { + _showEmptySearch = !emoji && !stickers; + _scroll->update(); + }, _scroll->lifetime()); + + _scroll->paintRequest() | rpl::filter([=] { + return _showEmptySearch; + }) | rpl::start_with_next([=] { + auto p = QPainter(_scroll); + p.setPen(st::windowSubTextFg); + p.setFont(st::normalFont); + p.drawText( + _scroll->rect(), + tr::lng_effect_none(tr::now), + style::al_center); + }, _scroll->lifetime()); + } else { + _list->setMinimalHeight(geometry.width(), _scroll->height()); + } updateVisibleTopBottom(); } @@ -1036,7 +1162,7 @@ bool AdjustMenuGeometryForSelector( const auto margins = selector->marginsForShadow(); const auto categoriesAboutTop = selector->useTransparency() ? selector->extendTopForCategoriesAndAbout(width) - : 0; + : selector->opaqueExtendTopAbout(width); menu->setForceWidth(width - added); const auto height = menu->height(); const auto fullTop = margins.top() + categoriesAboutTop + extend.top(); @@ -1049,9 +1175,7 @@ bool AdjustMenuGeometryForSelector( const auto additionalPaddingBottom = (willBeHeightWithoutBottomPadding >= minimalHeight ? 0 - : useTransparency - ? (minimalHeight - willBeHeightWithoutBottomPadding) - : 0); + : (minimalHeight - willBeHeightWithoutBottomPadding)); menu->setAdditionalMenuPadding(QMargins( margins.left() + extend.left(), fullTop, @@ -1067,9 +1191,32 @@ bool AdjustMenuGeometryForSelector( return false; } const auto origin = menu->preparedOrigin(); - if (!additionalPaddingBottom - || origin == Ui::PanelAnimation::Origin::TopLeft - || origin == Ui::PanelAnimation::Origin::TopRight) { + const auto expandDown = (origin == Ui::PanelAnimation::Origin::TopLeft) + || (origin == Ui::PanelAnimation::Origin::TopRight); + if (!useTransparency) { + const auto expandBy = additionalPaddingBottom; + selector->setOpaqueHeightExpand(expandBy, [=](int bottom) { + const auto add = bottom - menu->height(); + if (add > 0) { + const auto updated = menu->geometry().marginsAdded({ + 0, expandDown ? 0 : add, 0, expandDown ? add : 0 }); + menu->setFixedSize(updated.size()); + menu->setGeometry(updated); + } + }); + menu->setAdditionalMenuPadding(QMargins( + margins.left() + extend.left(), + fullTop, + margins.right() + extend.right(), + 0 + ), QMargins( + margins.left(), + margins.top(), + margins.right(), + 0 + )); + return menu->prepareGeometryFor(desiredPosition); + } else if (!additionalPaddingBottom || expandDown) { return true; } menu->setAdditionalMenuPadding(QMargins( @@ -1216,7 +1363,8 @@ auto AttachSelectorToMenu( std::shared_ptr show, const Data::PossibleItemReactionsRef &reactions, TextWithEntities about, - IconFactory iconFactory) + IconFactory iconFactory, + Fn paused) -> base::expected, AttachSelectorResult> { const auto settings = &AyuSettings::getInstance(); if (!AyuUi::needToShowItem(settings->showReactionsPanelInContextMenu)) { @@ -1233,8 +1381,9 @@ auto AttachSelectorToMenu( std::move(show), std::move(reactions), std::move(about), - std::move(iconFactory), [=](bool fast) { menu->hideMenu(fast); }, + std::move(iconFactory), + std::move(paused), false); // child if (!AdjustMenuGeometryForSelector(menu, desiredPosition, selector)) { return base::make_unexpected(AttachSelectorResult::Failed); diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.h b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.h index 1d136c15b..56f5210b7 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.h +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.h @@ -24,6 +24,7 @@ namespace ChatHelpers { class Show; class TabbedPanel; class EmojiListWidget; +class StickersListWidget; class StickersListFooter; enum class EmojiListMode; } // namespace ChatHelpers @@ -66,7 +67,7 @@ private: Strip *_strip = nullptr; std::vector _unifiedIdsList; - base::flat_map _defaultReactionIds; + base::flat_map _defaultReactionIds; base::flat_map _defaultReactionInStripMap; QPoint _defaultReactionShift; @@ -82,9 +83,11 @@ public: std::shared_ptr show, const Data::PossibleItemReactionsRef &reactions, TextWithEntities about, - IconFactory iconFactory, Fn close, + IconFactory iconFactory = nullptr, + Fn paused = nullptr, bool child = false); +#if 0 // not ready Selector( not_null parent, const style::EmojiPan &st, @@ -93,6 +96,7 @@ public: std::vector recent, Fn close, bool child = false); +#endif ~Selector(); [[nodiscard]] bool useTransparency() const; @@ -101,12 +105,15 @@ public: [[nodiscard]] QMargins marginsForShadow() const; [[nodiscard]] int extendTopForCategories() const; [[nodiscard]] int extendTopForCategoriesAndAbout(int width) const; + [[nodiscard]] int opaqueExtendTopAbout(int width) const; [[nodiscard]] int minimalHeight() const; [[nodiscard]] int countAppearedWidth(float64 progress) const; void setSpecialExpandTopSkip(int skip); void initGeometry(int innerTop); void beforeDestroy(); + void setOpaqueHeightExpand(int expand, Fn apply); + [[nodiscard]] rpl::producer chosen() const { return _chosen.events(); } @@ -143,6 +150,7 @@ private: std::vector recent, TextWithEntities about, IconFactory iconFactory, + Fn paused, Fn close, bool child); @@ -181,6 +189,7 @@ private: const Data::PossibleItemReactions _reactions; const std::vector _recent; const ChatHelpers::EmojiListMode _listMode; + const Fn _paused; Fn _jumpedToPremium; Ui::RoundAreaWithShadow _cachedRound; std::unique_ptr _strip; @@ -193,11 +202,13 @@ private: Ui::ScrollArea *_scroll = nullptr; ChatHelpers::EmojiListWidget *_list = nullptr; + ChatHelpers::StickersListWidget *_stickers = nullptr; ChatHelpers::StickersListFooter *_footer = nullptr; std::unique_ptr _unifiedFactoryOwner; Ui::PlainShadow *_shadow = nullptr; rpl::variable _shadowTop = 0; rpl::variable _shadowSkip = 0; + bool _showEmptySearch = false; QImage _paintBuffer; Ui::Animations::Simple _expanding; @@ -212,6 +223,10 @@ private: int _specialExpandTopSkip = 0; int _collapsedTopSkip = 0; int _topAddOnExpand = 0; + + int _opaqueHeightExpand = 0; + Fn _opaqueApplyHeightExpand; + const int _size = 0; int _recentRows = 0; int _columns = 0; @@ -253,7 +268,7 @@ AttachSelectorResult AttachSelectorToMenu( not_null item, Fn chosen, TextWithEntities about, - IconFactory iconFactory); + IconFactory iconFactory = nullptr); [[nodiscard]] auto AttachSelectorToMenu( not_null menu, @@ -262,7 +277,8 @@ AttachSelectorResult AttachSelectorToMenu( std::shared_ptr show, const Data::PossibleItemReactionsRef &reactions, TextWithEntities about, - IconFactory iconFactory + IconFactory iconFactory = nullptr, + Fn paused = nullptr ) -> base::expected, AttachSelectorResult>; [[nodiscard]] TextWithEntities ItemReactionsAbout( diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_strip.cpp b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_strip.cpp index 7f36f6630..08524c180 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_strip.cpp +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_strip.cpp @@ -51,7 +51,9 @@ Strip::Strip( Fn update, IconFactory iconFactory) : _st(st) -, _iconFactory(std::move(iconFactory)) +, _iconFactory(iconFactory + ? std::move(iconFactory) + : DefaultCachingIconFactory) , _inner(inner) , _finalSize(size) , _update(std::move(update)) { @@ -558,4 +560,11 @@ std::shared_ptr DefaultIconFactory( return CreateIcon(media, size); } +std::shared_ptr DefaultCachingIconFactory( + not_null media, + int size) { + auto &factory = media->owner()->session().cachedReactionIconFactory(); + return factory.createMethod()(media, size); +} + } // namespace HistoryView::Reactions diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_strip.h b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_strip.h index f11ea7253..7ae2d0dd2 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_strip.h +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_strip.h @@ -53,7 +53,7 @@ public: QRect inner, int size, Fn update, - IconFactory iconFactory); + IconFactory iconFactory = nullptr); enum class AddedButton : uchar { None, @@ -173,4 +173,8 @@ private: not_null media, int size); +[[nodiscard]] std::shared_ptr DefaultCachingIconFactory( + not_null media, + int size); + } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_tabs.cpp b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_tabs.cpp index c3b5848cc..f3b23804f 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_tabs.cpp +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_tabs.cpp @@ -7,13 +7,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "history/view/reactions/history_view_reactions_tabs.h" -#include "ui/rp_widget.h" -#include "ui/abstract_button.h" -#include "ui/painter.h" -#include "ui/controls/who_reacted_context_action.h" #include "data/data_message_reaction_id.h" -#include "styles/style_widgets.h" +#include "lang/lang_tag.h" +#include "ui/abstract_button.h" +#include "ui/controls/who_reacted_context_action.h" +#include "ui/painter.h" +#include "ui/rp_widget.h" #include "styles/style_chat.h" +#include "styles/style_widgets.h" namespace HistoryView::Reactions { namespace { @@ -35,7 +36,7 @@ not_null CreateTab( bool selected = false; }; const auto stm = &st.item; - const auto text = QString("%L1").arg(count); + const auto text = Lang::FormatCountDecimal(count); const auto font = st::semiboldFont; const auto textWidth = font->width(text); const auto result = Ui::CreateChild(parent.get()); diff --git a/Telegram/SourceFiles/info/channel_statistics/boosts/create_giveaway_box.cpp b/Telegram/SourceFiles/info/channel_statistics/boosts/create_giveaway_box.cpp index 4734718b9..629d6949b 100644 --- a/Telegram/SourceFiles/info/channel_statistics/boosts/create_giveaway_box.cpp +++ b/Telegram/SourceFiles/info/channel_statistics/boosts/create_giveaway_box.cpp @@ -174,13 +174,15 @@ void AddPremiumTopBarWithDefaultTitleBar( const auto bar = Ui::CreateChild( box.get(), st::startGiveawayCover, - nullptr, - tr::lng_giveaway_new_title(), - (group - ? tr::lng_giveaway_new_about_group - : tr::lng_giveaway_new_about)(Ui::Text::RichLangValue), - true, - false); + Ui::Premium::TopBarDescriptor{ + .clickContextOther = nullptr, + .title = tr::lng_giveaway_new_title(), + .about = (group + ? tr::lng_giveaway_new_about_group + : tr::lng_giveaway_new_about)(Ui::Text::RichLangValue), + .light = true, + .optimizeMinistars = false, + }); bar->setAttribute(Qt::WA_TransparentForMouseEvents); box->addRow( diff --git a/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/giveaway.style b/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/giveaway.style index 2368a616f..2715dfe95 100644 --- a/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/giveaway.style +++ b/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/giveaway.style @@ -95,6 +95,10 @@ giveawayGiftCodeValue: FlatLabel(defaultFlatLabel) { linkUnderline: kLinkUnderlineNever; } } +giveawayGiftCodeValueMultiline: FlatLabel(giveawayGiftCodeValue) { + minWidth: 128px; + maxHeight: 100px; +} giveawayGiftCodeValueMargin: margins(13px, 9px, 13px, 9px); giveawayGiftCodePeerMargin: margins(11px, 6px, 11px, 4px); giveawayGiftCodeUserpic: UserpicButton(defaultUserpicButton) { diff --git a/Telegram/SourceFiles/info/channel_statistics/earn/info_earn_inner_widget.cpp b/Telegram/SourceFiles/info/channel_statistics/earn/info_earn_inner_widget.cpp index d3af5f14d..a151443e6 100644 --- a/Telegram/SourceFiles/info/channel_statistics/earn/info_earn_inner_widget.cpp +++ b/Telegram/SourceFiles/info/channel_statistics/earn/info_earn_inner_widget.cpp @@ -628,7 +628,7 @@ void InnerWidget::fill() { Ui::AddSkip(container); Ui::AddDivider(container); Ui::AddSkip(container); - if (channel) { + if (channel && data.availableBalance) { const auto value = data.availableBalance; AddHeader(container, tr::lng_channel_earn_balance_title); Ui::AddSkip(container); diff --git a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp index 12f41a836..3ceaeca49 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp @@ -175,12 +175,13 @@ base::options::toggle ShowPeerIdBelowAbout({ } using namespace Ui::Text; if (!value.empty()) { - value.append("\n"); + value.append("\n\n"); } value.append(Italic(u"id: "_q)); const auto raw = peer->id.value & PeerId::kChatTypeMask; - const auto id = QString::number(raw); - value.append(Link(Italic(id), "internal:copy:" + id)); + value.append(Link( + Italic(Lang::FormatCountDecimal(raw)), + "internal:copy:" + QString::number(raw))); return std::move(value); }); } diff --git a/Telegram/SourceFiles/info/profile/info_profile_cover.cpp b/Telegram/SourceFiles/info/profile/info_profile_cover.cpp index 1814870b8..44e415ff2 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_cover.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_cover.cpp @@ -215,7 +215,7 @@ void TopicIconView::setupImage(not_null topic) { ) | rpl::start_with_next([=] { _image = ForumTopicGeneralIconFrame( st::infoForumTopicIcon.size, - _generalIconFg); + _generalIconFg->c); _update(); }, _lifetime); return; @@ -235,12 +235,17 @@ TopicIconButton::TopicIconButton( QWidget *parent, not_null controller, not_null topic) +: TopicIconButton(parent, topic, [=] { + return controller->isGifPausedAtLeastFor(Window::GifPauseReason::Layer); +}) { +} + +TopicIconButton::TopicIconButton( + QWidget *parent, + not_null topic, + Fn paused) : AbstractButton(parent) -, _view( - topic, - [=] { return controller->isGifPausedAtLeastFor( - Window::GifPauseReason::Layer); }, - [=] { update(); }) { +, _view(topic, paused, [=] { update(); }) { resize(st::infoTopicCover.photo.size); paintRequest( ) | rpl::start_with_next([=] { diff --git a/Telegram/SourceFiles/info/profile/info_profile_cover.h b/Telegram/SourceFiles/info/profile/info_profile_cover.h index 040a00650..64d9152d8 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_cover.h +++ b/Telegram/SourceFiles/info/profile/info_profile_cover.h @@ -82,6 +82,10 @@ public: QWidget *parent, not_null controller, not_null topic); + TopicIconButton( + QWidget *parent, + not_null topic, + Fn paused); private: TopicIconView _view; 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 3a3176908..02f6f986e 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.cpp @@ -229,7 +229,7 @@ void EmojiStatusPanel::create(const Descriptor &descriptor) { _panel->selector()->contextMenuRequested( ) | rpl::start_with_next([=] { - _panel->selector()->showMenuWithType(SendMenu::Type::Scheduled); + _panel->selector()->showMenuWithDetails({}); }, _panel->lifetime()); auto statusChosen = _panel->selector()->customEmojiChosen( diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp b/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp index 1ed6d7884..2dd25c571 100644 --- a/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp +++ b/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp @@ -7,9 +7,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "info/statistics/info_statistics_list_controllers.h" +#include "api/api_credits.h" #include "api/api_statistics.h" #include "boxes/peer_list_controllers.h" #include "data/data_channel.h" +#include "data/data_credits.h" #include "data/data_session.h" #include "data/data_stories.h" #include "data/data_user.h" @@ -17,6 +19,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/channel_statistics/boosts/giveaway/boost_badge.h" #include "lang/lang_keys.h" #include "main/main_session.h" +#include "main/session/session_show.h" +#include "ui/effects/credits_graphics.h" #include "ui/effects/outline_segments.h" // Ui::UnreadStoryOutlineGradient. #include "ui/effects/toggle_arrow.h" #include "ui/painter.h" @@ -26,7 +30,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/popup_menu.h" #include "ui/wrap/slide_wrap.h" #include "ui/wrap/vertical_layout.h" +#include "styles/style_credits.h" #include "styles/style_dialogs.h" // dialogsStoriesFull. +#include "styles/style_layers.h" // boxRowPadding. #include "styles/style_menu_icons.h" #include "styles/style_settings.h" #include "styles/style_statistics.h" @@ -113,6 +119,15 @@ struct BoostsDescriptor final { not_null peer; }; +struct CreditsDescriptor final { + Data::CreditsStatusSlice firstSlice; + Fn entryClickedCallback; + not_null premiumBot; + not_null creditIcon; + bool in = false; + bool out = false; +}; + class PeerListRowWithFullId : public PeerListRow { public: PeerListRowWithFullId( @@ -692,6 +707,283 @@ rpl::producer BoostsController::totalBoostsValue() const { return _totalBoosts.value(); } +class CreditsRow final : public PeerListRow { +public: + struct Descriptor final { + Data::CreditsHistoryEntry entry; + not_null creditIcon; + int rowHeight = 0; + Fn)> updateCallback; + }; + + CreditsRow(not_null peer, const Descriptor &descriptor); + CreditsRow(const Descriptor &descriptor); + + [[nodiscard]] const Data::CreditsHistoryEntry &entry() const; + [[nodiscard]] QString generateName() override; + + [[nodiscard]] PaintRoundImageCallback generatePaintUserpicCallback( + bool forceRound) override; + + QSize rightActionSize() const override; + QMargins rightActionMargins() const override; + bool rightActionDisabled() const override; + void rightActionPaint( + Painter &p, + int x, + int y, + int outerWidth, + bool selected, + bool actionSelected) override; + +private: + void init(); + + const Data::CreditsHistoryEntry _entry; + not_null const _creditIcon; + const int _rowHeight; + + PaintRoundImageCallback _paintUserpicCallback; + QString _name; + + Ui::Text::String _rightText; +}; + +CreditsRow::CreditsRow(not_null peer, const Descriptor &descriptor) +: PeerListRow(peer, UniqueRowIdFromString(descriptor.entry.id)) +, _entry(descriptor.entry) +, _creditIcon(descriptor.creditIcon) +, _rowHeight(descriptor.rowHeight) { + const auto photo = _entry.photoId + ? peer->session().data().photo(_entry.photoId).get() + : nullptr; + if (photo) { + _paintUserpicCallback = Ui::GenerateCreditsPaintEntryCallback( + photo, + [this, update = descriptor.updateCallback] { update(this); }); + } + init(); +} + +CreditsRow::CreditsRow(const Descriptor &descriptor) +: PeerListRow(UniqueRowIdFromString(descriptor.entry.id)) +, _entry(descriptor.entry) +, _creditIcon(descriptor.creditIcon) +, _rowHeight(descriptor.rowHeight) { + init(); +} + +void CreditsRow::init() { + _name = !PeerListRow::special() + ? PeerListRow::generateName() + : Ui::GenerateEntryName(_entry).text; + const auto joiner = QString(QChar(' ')) + QChar(8212) + QChar(' '); + PeerListRow::setCustomStatus( + langDateTimeFull(_entry.date) + + (_entry.refunded + ? (joiner + tr::lng_channel_earn_history_return(tr::now)) + : QString()) + + (_entry.title.isEmpty() ? QString() : (joiner + _name))); + { + constexpr auto kMinus = QChar(0x2212); + _rightText.setText( + st::semiboldTextStyle, + ((!_entry.bareId || _entry.refunded) ? QChar('+') : kMinus) + + Lang::FormatCountDecimal(std::abs(int64(_entry.credits)))); + } + if (!_paintUserpicCallback) { + _paintUserpicCallback = !PeerListRow::special() + ? PeerListRow::generatePaintUserpicCallback(false) + : Ui::GenerateCreditsPaintUserpicCallback(_entry); + } +} + +const Data::CreditsHistoryEntry &CreditsRow::entry() const { + return _entry; +} + +QString CreditsRow::generateName() { + return _entry.title.isEmpty() ? _name : _entry.title; +} + +PaintRoundImageCallback CreditsRow::generatePaintUserpicCallback(bool force) { + return _paintUserpicCallback; +} + +QSize CreditsRow::rightActionSize() const { + return QSize( + _rightText.maxWidth() + + (_creditIcon->width() / style::DevicePixelRatio()) + + st::creditsHistoryRightSkip + + _rightText.style()->font->spacew * 2, + _rowHeight); +} + +QMargins CreditsRow::rightActionMargins() const { + return QMargins(0, 0, st::boxRowPadding.right(), 0); +} + +bool CreditsRow::rightActionDisabled() const { + return true; +} + +void CreditsRow::rightActionPaint( + Painter &p, + int x, + int y, + int outerWidth, + bool selected, + bool actionSelected) { + const auto &font = _rightText.style()->font; + y += _rowHeight / 2; + p.setPen((!_entry.bareId || _entry.refunded) + ? st::boxTextFgGood + : st::menuIconAttentionColor); + x += st::creditsHistoryRightSkip; + _rightText.draw(p, Ui::Text::PaintContext{ + .position = QPoint(x, y - font->height / 2), + .outerWidth = outerWidth, + .availableWidth = outerWidth, + }); + x += _rightText.maxWidth() + font->spacew * 2; + p.drawImage( + x, + y -(_creditIcon->height() / style::DevicePixelRatio()) / 2, + *_creditIcon); +} + +class CreditsController final : public PeerListController { +public: + explicit CreditsController(CreditsDescriptor d); + + Main::Session &session() const override; + void prepare() override; + void rowClicked(not_null row) override; + void loadMoreRows() override; + + base::unique_qptr rowContextMenu( + QWidget *parent, + not_null row) override; + + [[nodiscard]] bool skipRequest() const; + void requestNext(); + + [[nodiscard]] rpl::producer allLoadedValue() const; + +private: + void applySlice(const Data::CreditsStatusSlice &slice); + + const not_null _session; + const not_null _premiumBot; + Fn _entryClickedCallback; + not_null const _creditIcon; + + Api::CreditsHistory _api; + Data::CreditsStatusSlice _firstSlice; + Data::CreditsStatusSlice::OffsetToken _apiToken; + + rpl::variable _allLoaded = false; + bool _requesting = false; + +}; + +CreditsController::CreditsController(CreditsDescriptor d) +: _session(&d.premiumBot->session()) +, _premiumBot(d.premiumBot) +, _entryClickedCallback(std::move(d.entryClickedCallback)) +, _creditIcon(d.creditIcon) +, _api(d.premiumBot->session().user(), d.in, d.out) +, _firstSlice(std::move(d.firstSlice)) { + PeerListController::setStyleOverrides(&st::boostsListBox); +} + +Main::Session &CreditsController::session() const { + return *_session; +} + +bool CreditsController::skipRequest() const { + return _requesting || _allLoaded.current(); +} + +void CreditsController::requestNext() { + _requesting = true; + _api.request(_apiToken, [=](const Data::CreditsStatusSlice &s) { + _requesting = false; + applySlice(s); + }); +} + +void CreditsController::prepare() { + applySlice(base::take(_firstSlice)); + delegate()->peerListRefreshRows(); +} + +void CreditsController::loadMoreRows() { +} + +void CreditsController::applySlice(const Data::CreditsStatusSlice &slice) { + _allLoaded = slice.allLoaded; + _apiToken = slice.token; + + for (const auto &item : slice.list) { + auto row = [&] { + const auto descriptor = CreditsRow::Descriptor{ + .entry = item, + .creditIcon = _creditIcon, + .rowHeight = computeListSt().item.height, + .updateCallback = [=](not_null row) { + delegate()->peerListUpdateRow(row); + }, + }; + using Type = Data::CreditsHistoryEntry::PeerType; + if (item.bareId) { + const auto peer = session().data().peer(PeerId(item.bareId)); + return std::make_unique(peer, descriptor); + } else if (item.peerType == Type::PremiumBot) { + return std::make_unique(_premiumBot, descriptor); + } else { + return std::make_unique(descriptor); + } + }(); + delegate()->peerListAppendRow(std::move(row)); + } + delegate()->peerListRefreshRows(); +} + +void CreditsController::rowClicked(not_null row) { + if (_entryClickedCallback) { + _entryClickedCallback( + static_cast(row.get())->entry()); + } +} + +base::unique_qptr CreditsController::rowContextMenu( + QWidget *parent, + not_null row) { + const auto entry = static_cast(row.get())->entry(); + if (!entry.bareId) { + return nullptr; + } + auto menu = base::make_unique_q( + parent, + st::defaultPopupMenu); + const auto peer = row->peer(); + const auto callback = crl::guard(parent, [=, id = entry.id] { + const auto show = delegate()->peerListUiShow(); + Api::CreditsRefund( + peer, + id, + [=] { show->showToast(tr::lng_report_spam_done(tr::now)); }, + [=](const QString &error) { show->showToast(error); }); + }); + menu->addAction(tr::lng_channel_earn_history_return(tr::now), callback); + return menu; +} + +rpl::producer CreditsController::allLoadedValue() const { + return _allLoaded.value(); +} + } // namespace void AddPublicForwards( @@ -843,4 +1135,54 @@ void AddBoostsList( button->setClickedCallback(showMore); } +void AddCreditsHistoryList( + std::shared_ptr show, + const Data::CreditsStatusSlice &firstSlice, + not_null container, + Fn callback, + not_null bot, + not_null icon, + bool in, + bool out) { + struct State final { + State( + CreditsDescriptor d, + std::shared_ptr show) + : delegate(std::move(show)) + , controller(std::move(d)) { + } + PeerListContentDelegateShow delegate; + CreditsController controller; + }; + const auto state = container->lifetime().make_state( + CreditsDescriptor{ firstSlice, callback, bot, icon, in, out }, + show); + + state->delegate.setContent(container->add( + object_ptr(container, &state->controller))); + state->controller.setDelegate(&state->delegate); + + const auto wrap = container->add( + object_ptr>( + container, + object_ptr( + container, + tr::lng_stories_show_more(), + st::statisticsShowMoreButton)), + { 0, -st::settingsButton.padding.top(), 0, 0 }); + const auto button = wrap->entity(); + AddArrow(button); + + const auto showMore = [=] { + if (!state->controller.skipRequest()) { + state->controller.requestNext(); + container->resizeToWidth(container->width()); + } + }; + wrap->toggleOn( + state->controller.allLoadedValue() | rpl::map(!rpl::mappers::_1), + anim::type::instant); + button->setClickedCallback(showMore); +} + } // namespace Info::Statistics diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.h b/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.h index 2a2dfe93a..2d381cb7c 100644 --- a/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.h +++ b/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.h @@ -16,11 +16,17 @@ class VerticalLayout; namespace Data { struct Boost; struct BoostsListSlice; +struct CreditsHistoryEntry; +struct CreditsStatusSlice; struct PublicForwardsSlice; struct RecentPostId; struct SupergroupStatistics; } // namespace Data +namespace Main { +class SessionShow; +} // namespace Main + namespace Info::Statistics { void AddPublicForwards( @@ -44,4 +50,14 @@ void AddBoostsList( not_null peer, rpl::producer title); +void AddCreditsHistoryList( + std::shared_ptr show, + const Data::CreditsStatusSlice &firstSlice, + not_null container, + Fn entryClickedCallback, + not_null premiumBot, + not_null creditIcon, + bool in, + bool out); + } // namespace Info::Statistics diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp index 95e7630e4..68da29a3a 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp @@ -43,6 +43,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history.h" #include "history/history_item.h" #include "payments/payments_checkout_process.h" +#include "payments/payments_non_panel_process.h" #include "storage/storage_account.h" #include "boxes/peer_list_controllers.h" #include "lang/lang_keys.h" @@ -603,7 +604,15 @@ void AttachWebView::botHandleInvoice(QString slug) { } }; _panel->hideForPayment(); - Payments::CheckoutProcess::Start(&_bot->session(), slug, reactivate); + Payments::CheckoutProcess::Start( + &_bot->session(), + slug, + reactivate, + _context + ? Payments::ProcessNonPanelPaymentFormFactory( + _context->controller.get(), + reactivate) + : nullptr); } void AttachWebView::botHandleMenuButton(Ui::BotWebView::MenuButton button) { @@ -1703,7 +1712,7 @@ std::unique_ptr MakeAttachBotsMenu( flag, flag, source, - sendMenuType); + { sendMenuType }); }, &st::menuIconCreatePoll); } for (const auto &bot : bots->attachBots()) { diff --git a/Telegram/SourceFiles/inline_bots/inline_results_inner.cpp b/Telegram/SourceFiles/inline_bots/inline_results_inner.cpp index e9cf3f2d1..7aa324ad9 100644 --- a/Telegram/SourceFiles/inline_bots/inline_results_inner.cpp +++ b/Telegram/SourceFiles/inline_bots/inline_results_inner.cpp @@ -329,23 +329,26 @@ void Inner::contextMenuEvent(QContextMenuEvent *e) { if (_selected < 0 || _pressed >= 0) { return; } - const auto type = _sendMenuType - ? _sendMenuType() - : SendMenu::Type::Disabled; + auto details = _sendMenuDetails + ? _sendMenuDetails() + : SendMenu::Details(); + + // inline results don't have effects + details.effectAllowed = false; _menu = base::make_unique_q( this, st::popupMenuWithIcons); - const auto send = [=, selected = _selected](Api::SendOptions options) { + const auto selected = _selected; + const auto send = crl::guard(this, [=](Api::SendOptions options) { selectInlineResult(selected, options, false); - }; + }); SendMenu::FillSendMenu( _menu, - type, - SendMenu::DefaultSilentCallback(send), - SendMenu::DefaultScheduleCallback(_controller->uiShow(), type, send), - SendMenu::DefaultWhenOnlineCallback(send)); + _controller->uiShow(), + details, + SendMenu::DefaultCallback(_controller->uiShow(), send)); const auto item = _mosaic.itemAt(_selected); if (const auto previewDocument = item->getPreviewDocument()) { @@ -689,8 +692,8 @@ void Inner::switchPm() { } } -void Inner::setSendMenuType(Fn &&callback) { - _sendMenuType = std::move(callback); +void Inner::setSendMenuDetails(Fn &&callback) { + _sendMenuDetails = std::move(callback); } } // namespace Layout diff --git a/Telegram/SourceFiles/inline_bots/inline_results_inner.h b/Telegram/SourceFiles/inline_bots/inline_results_inner.h index 5aa22578a..4e6195e19 100644 --- a/Telegram/SourceFiles/inline_bots/inline_results_inner.h +++ b/Telegram/SourceFiles/inline_bots/inline_results_inner.h @@ -43,7 +43,7 @@ struct ResultSelected; } // namespace InlineBots namespace SendMenu { -enum class Type; +struct Details; } // namespace SendMenu namespace InlineBots { @@ -89,7 +89,7 @@ public: void setResultSelectedCallback(Fn callback) { _resultSelectedCallback = std::move(callback); } - void setSendMenuType(Fn &&callback); + void setSendMenuDetails(Fn &&callback); // Ui::AbstractTooltipShower interface. QString tooltipText() const override; @@ -179,7 +179,7 @@ private: bool _previewShown = false; Fn _resultSelectedCallback; - Fn _sendMenuType; + Fn _sendMenuDetails; }; diff --git a/Telegram/SourceFiles/inline_bots/inline_results_widget.cpp b/Telegram/SourceFiles/inline_bots/inline_results_widget.cpp index 8a7ed4e2d..384927e2d 100644 --- a/Telegram/SourceFiles/inline_bots/inline_results_widget.cpp +++ b/Telegram/SourceFiles/inline_bots/inline_results_widget.cpp @@ -265,8 +265,8 @@ void Widget::setResultSelectedCallback(Fn callback) { _inner->setResultSelectedCallback(std::move(callback)); } -void Widget::setSendMenuType(Fn &&callback) { - _inner->setSendMenuType(std::move(callback)); +void Widget::setSendMenuDetails(Fn &&callback) { + _inner->setSendMenuDetails(std::move(callback)); } void Widget::hideAnimated() { diff --git a/Telegram/SourceFiles/inline_bots/inline_results_widget.h b/Telegram/SourceFiles/inline_bots/inline_results_widget.h index 00b39e0d3..166287c58 100644 --- a/Telegram/SourceFiles/inline_bots/inline_results_widget.h +++ b/Telegram/SourceFiles/inline_bots/inline_results_widget.h @@ -45,7 +45,7 @@ struct ResultSelected; } // namespace InlineBots namespace SendMenu { -enum class Type; +struct Details; } // namespace SendMenu namespace InlineBots { @@ -75,7 +75,7 @@ public: void hideAnimated(); void setResultSelectedCallback(Fn callback); - void setSendMenuType(Fn &&callback); + void setSendMenuDetails(Fn &&callback); [[nodiscard]] rpl::producer requesting() const { return _requesting.events(); diff --git a/Telegram/SourceFiles/intro/intro_code.cpp b/Telegram/SourceFiles/intro/intro_code.cpp index e5d09140c..e48e8bb59 100644 --- a/Telegram/SourceFiles/intro/intro_code.cpp +++ b/Telegram/SourceFiles/intro/intro_code.cpp @@ -266,8 +266,10 @@ void CodeWidget::sendCall() { _callStatus = CallStatus::Calling; _callTimer.cancel(); _callRequestId = api().request(MTPauth_ResendCode( + MTP_flags(0), MTP_string(getData()->phone), - MTP_bytes(getData()->phoneHash) + MTP_bytes(getData()->phoneHash), + MTPstring() // reason )).done([=](const MTPauth_SentCode &result) { callDone(result); }).send(); @@ -376,8 +378,10 @@ void CodeWidget::noTelegramCode() { return; } _noTelegramCodeRequestId = api().request(MTPauth_ResendCode( + MTP_flags(0), MTP_string(getData()->phone), - MTP_bytes(getData()->phoneHash) + MTP_bytes(getData()->phoneHash), + MTPstring() // reason )).done([=](const MTPauth_SentCode &result) { noTelegramCodeDone(result); }).fail([=](const MTP::Error &error) { diff --git a/Telegram/SourceFiles/intro/intro_password_check.h b/Telegram/SourceFiles/intro/intro_password_check.h index ddd117551..9bfcbad3d 100644 --- a/Telegram/SourceFiles/intro/intro_password_check.h +++ b/Telegram/SourceFiles/intro/intro_password_check.h @@ -35,6 +35,10 @@ public: void submit() override; rpl::producer nextButtonText() const override; + bool hasBack() const override { + return true; + } + protected: void resizeEvent(QResizeEvent *e) override; diff --git a/Telegram/SourceFiles/intro/intro_widget.cpp b/Telegram/SourceFiles/intro/intro_widget.cpp index 13ba4837b..9592929e9 100644 --- a/Telegram/SourceFiles/intro/intro_widget.cpp +++ b/Telegram/SourceFiles/intro/intro_widget.cpp @@ -849,7 +849,7 @@ void Widget::backRequested() { Core::App().domain().activate(parent); } else { moveToStep( - new StartWidget(this, _account, getData()), + Ui::CreateChild(this, _account, getData()), StackAction::Replace, Animate::Back); } diff --git a/Telegram/SourceFiles/iv/iv_instance.cpp b/Telegram/SourceFiles/iv/iv_instance.cpp index ab2e21327..69df5f772 100644 --- a/Telegram/SourceFiles/iv/iv_instance.cpp +++ b/Telegram/SourceFiles/iv/iv_instance.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "iv/iv_instance.h" #include "apiwrap.h" +#include "base/platform/base_platform_info.h" #include "boxes/share_box.h" #include "core/application.h" #include "core/file_utilities.h" @@ -741,6 +742,11 @@ void Instance::show( not_null session, not_null data, QString hash) { + if (Platform::IsMac()) { + // Otherwise IV is not visible under the media viewer. + Core::App().hideMediaView(); + } + const auto guard = gsl::finally([&] { requestFull(session, data->id()); }); diff --git a/Telegram/SourceFiles/main/main_session.cpp b/Telegram/SourceFiles/main/main_session.cpp index 530c15207..e11cc2ebb 100644 --- a/Telegram/SourceFiles/main/main_session.cpp +++ b/Telegram/SourceFiles/main/main_session.cpp @@ -20,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "chat_helpers/stickers_emoji_pack.h" #include "chat_helpers/stickers_dice_pack.h" #include "chat_helpers/stickers_gift_box_pack.h" +#include "history/view/reactions/history_view_reactions_strip.h" #include "history/history.h" #include "history/history_item.h" #include "inline_bots/bot_attach_web_view.h" @@ -28,6 +29,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "storage/file_upload.h" #include "storage/storage_account.h" #include "storage/storage_facade.h" +#include "data/components/factchecks.h" #include "data/components/recent_peers.h" #include "data/components/scheduled_messages.h" #include "data/components/sponsored_messages.h" @@ -146,6 +148,8 @@ Session::Session( , _scheduledMessages(std::make_unique(this)) , _sponsoredMessages(std::make_unique(this)) , _topPeers(std::make_unique(this)) +, _factchecks(std::make_unique(this)) +, _cachedReactionIconFactory(std::make_unique()) , _supportHelper(Support::Helper::Create(this)) , _saveSettingsTimer([=] { saveSettings(); }) { Expects(_settings != nullptr); @@ -339,6 +343,14 @@ bool Session::premiumCanBuy() const { return _premiumPossible.current(); } +rpl::producer Session::creditsValue() const { + return _credits.value(); +} + +void Session::setCredits(uint64 credits) { + _credits = credits; +} + bool Session::isTestMode() const { return mtp().isTestMode(); } diff --git a/Telegram/SourceFiles/main/main_session.h b/Telegram/SourceFiles/main/main_session.h index daab53632..9581e7cd4 100644 --- a/Telegram/SourceFiles/main/main_session.h +++ b/Telegram/SourceFiles/main/main_session.h @@ -35,8 +35,13 @@ class RecentPeers; class ScheduledMessages; class SponsoredMessages; class TopPeers; +class Factchecks; } // namespace Data +namespace HistoryView::Reactions { +class CachedIconFactory; +} // namespace HistoryView::Reactions + namespace Storage { class DownloadManagerMtproto; class Uploader; @@ -96,6 +101,9 @@ public: [[nodiscard]] bool premiumBadgesShown() const; [[nodiscard]] bool premiumCanBuy() const; + [[nodiscard]] rpl::producer creditsValue() const; + void setCredits(uint64 credits); + [[nodiscard]] bool isTestMode() const; [[nodiscard]] uint64 uniqueId() const; // userId() with TestDC shift. [[nodiscard]] UserId userId() const; @@ -120,6 +128,9 @@ public: [[nodiscard]] Data::TopPeers &topPeers() const { return *_topPeers; } + [[nodiscard]] Data::Factchecks &factchecks() const { + return *_factchecks; + } [[nodiscard]] Api::Updates &updates() const { return *_updates; } @@ -156,6 +167,10 @@ public: [[nodiscard]] InlineBots::AttachWebView &attachWebView() const { return *_attachWebView; } + [[nodiscard]] auto cachedReactionIconFactory() const + -> HistoryView::Reactions::CachedIconFactory & { + return *_cachedReactionIconFactory; + } void saveSettings(); void saveSettingsDelayed(crl::time delay = kDefaultSaveDelay); @@ -217,8 +232,6 @@ public: private: static constexpr auto kDefaultSaveDelay = crl::time(1000); - void parseColorIndices(const MTPDhelp_peerColors &data); - const UserId _userId; const not_null _account; @@ -245,10 +258,15 @@ private: const std::unique_ptr _scheduledMessages; const std::unique_ptr _sponsoredMessages; const std::unique_ptr _topPeers; + const std::unique_ptr _factchecks; + + using ReactionIconFactory = HistoryView::Reactions::CachedIconFactory; + const std::unique_ptr _cachedReactionIconFactory; const std::unique_ptr _supportHelper; std::shared_ptr _selfUserpicView; + rpl::variable _credits = 0; rpl::variable _premiumPossible = false; rpl::event_stream _termsLockChanges; diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index 31ce8e064..ef7fb5abb 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -730,9 +730,18 @@ void MainWidget::hideSingleUseKeyboard(FullMsgId replyToId) { _history->hideSingleUseKeyboard(replyToId); } -void MainWidget::searchMessages(const QString &query, Dialogs::Key inChat, UserData *from) { +void MainWidget::searchMessages(const QString &query, Dialogs::Key inChat) { + auto tags = Data::SearchTagsFromQuery(query); if (controller()->isPrimary()) { - _dialogs->searchMessages(query, inChat, from); + auto state = Dialogs::SearchState{ + .inChat = ((tags.empty() || inChat.sublist()) + ? inChat + : session().data().history(session().user())), + .tags = tags, + .query = tags.empty() ? query : QString(), + }; + state.tab = state.defaultTabForMe(); + _dialogs->searchMessages(std::move(state)); if (isOneColumn()) { _controller->clearSectionStack(); } else { @@ -742,7 +751,7 @@ void MainWidget::searchMessages(const QString &query, Dialogs::Key inChat, UserD if (const auto sublist = inChat.sublist()) { controller()->showSection( std::make_shared(sublist)); - } else if (!Data::SearchTagsFromQuery(query).empty()) { + } else if (!tags.empty()) { inChat = controller()->session().data().history( controller()->session().user()); } @@ -1047,8 +1056,8 @@ void MainWidget::exportTopBarHeightUpdated() { } } -SendMenu::Type MainWidget::sendMenuType() const { - return _history->sendMenuType(); +SendMenu::Details MainWidget::sendMenuDetails() const { + return _history->sendMenuDetails(); } bool MainWidget::sendExistingDocument(not_null document) { @@ -1772,7 +1781,7 @@ void MainWidget::showNewSection( _thirdSection = std::move(newThirdSection); _thirdSection->removeRequests( ) | rpl::start_with_next([=] { - _thirdSection.destroy(); + destroyThirdSection(); _thirdShadow.destroy(); updateControlsGeometry(); }, _thirdSection->lifetime()); @@ -2289,7 +2298,7 @@ void MainWidget::updateControlsGeometry() { } } } else { - _thirdSection.destroy(); + destroyThirdSection(); _thirdShadow.destroy(); } const auto mainSectionTop = getMainSectionTop(); @@ -2408,6 +2417,15 @@ void MainWidget::updateControlsGeometry() { floatPlayerUpdatePositions(); } +void MainWidget::destroyThirdSection() { + if (const auto strong = _thirdSection.data()) { + if (Ui::InFocusChain(strong)) { + setFocus(); + } + } + _thirdSection.destroy(); +} + void MainWidget::refreshResizeAreas() { if (!isOneColumn() && _dialogs) { ensureFirstColumnResizeAreaCreated(); @@ -2555,7 +2573,7 @@ void MainWidget::updateThirdColumnToCurrentChat( if (saveThirdSectionToStackBack()) { _stack.back()->setThirdSectionMemento( _thirdSection->createMemento()); - _thirdSection.destroy(); + destroyThirdSection(); } }; auto &settings = Core::App().settings(); @@ -2601,7 +2619,7 @@ void MainWidget::updateThirdColumnToCurrentChat( settings.setTabbedReplacedWithInfo(false); if (!key) { if (_thirdSection) { - _thirdSection.destroy(); + destroyThirdSection(); _thirdShadow.destroy(); updateControlsGeometry(); } diff --git a/Telegram/SourceFiles/mainwidget.h b/Telegram/SourceFiles/mainwidget.h index be26770ce..4b0388707 100644 --- a/Telegram/SourceFiles/mainwidget.h +++ b/Telegram/SourceFiles/mainwidget.h @@ -29,7 +29,7 @@ struct SendOptions; } // namespace Api namespace SendMenu { -enum class Type; +struct Details; } // namespace SendMenu namespace Main { @@ -157,7 +157,7 @@ public: QPixmap grabForShowAnimation(const Window::SectionSlideParams ¶ms); void checkMainSectionToLayer(); - [[nodiscard]] SendMenu::Type sendMenuType() const; + [[nodiscard]] SendMenu::Details sendMenuDetails() const; bool sendExistingDocument(not_null document); bool sendExistingDocument( not_null document, @@ -279,6 +279,7 @@ private: void showNewSection( std::shared_ptr memento, const SectionShow ¶ms); + void destroyThirdSection(); Window::SectionSlideParams prepareThirdSectionAnimation(Window::SectionWidget *section); diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp index 40e69f965..8e36afbf7 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp @@ -679,11 +679,6 @@ auto Controller::stickerOrEmojiChosen() const return _delegate->storiesStickerOrEmojiChosen(); } -auto Controller::cachedReactionIconFactory() const --> HistoryView::Reactions::CachedIconFactory & { - return _delegate->storiesCachedReactionIconFactory(); -} - void Controller::rebuildFromContext( not_null peer, FullStoryId storyId) { diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h index b3d796882..776667f73 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.h +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h @@ -30,7 +30,6 @@ class DocumentMedia; } // namespace Data namespace HistoryView::Reactions { -class CachedIconFactory; struct ChosenReaction; enum class AttachSelectorResult; } // namespace HistoryView::Reactions @@ -137,8 +136,6 @@ public: [[nodiscard]] std::shared_ptr uiShow() const; [[nodiscard]] auto stickerOrEmojiChosen() const -> rpl::producer; - [[nodiscard]] auto cachedReactionIconFactory() const - -> HistoryView::Reactions::CachedIconFactory &; void show(not_null story, Data::StoriesContext context); void jumpTo(not_null story, Data::StoriesContext context); diff --git a/Telegram/SourceFiles/media/stories/media_stories_delegate.h b/Telegram/SourceFiles/media/stories/media_stories_delegate.h index d1a06a52e..c453177cb 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_delegate.h +++ b/Telegram/SourceFiles/media/stories/media_stories_delegate.h @@ -17,10 +17,6 @@ class Story; struct StoriesContext; } // namespace Data -namespace HistoryView::Reactions { -class CachedIconFactory; -} // namespace HistoryView::Reactions - namespace Main { class Session; } // namespace Main @@ -48,8 +44,6 @@ public: -> std::shared_ptr = 0; [[nodiscard]] virtual auto storiesStickerOrEmojiChosen() -> rpl::producer = 0; - [[nodiscard]] virtual auto storiesCachedReactionIconFactory() - -> HistoryView::Reactions::CachedIconFactory & = 0; virtual void storiesRedisplay(not_null story) = 0; virtual void storiesJumpTo( not_null session, diff --git a/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp b/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp index da07a1516..af45d9150 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp @@ -667,8 +667,10 @@ void Reactions::Panel::create() { TextWithEntities{ (mode == Mode::Message ? tr::lng_stories_reaction_as_message(tr::now) : QString()) }, - _controller->cachedReactionIconFactory().createMethod(), - [=](bool fast) { hide(mode); }); + [=](bool fast) { hide(mode); }, + nullptr, // iconFactory + nullptr, // paused + true); _selector->chosen( ) | rpl::start_with_next([=]( @@ -867,8 +869,7 @@ auto Reactions::attachToMenu( st::storiesReactionsPan, show, LookupPossibleReactions(&show->session()), - TextWithEntities(), - _controller->cachedReactionIconFactory().createMethod()); + TextWithEntities()); if (!result) { return result.error(); } diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp index b1a33d9c1..d93f49bf1 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp @@ -123,7 +123,7 @@ ReplyArea::ReplyArea(not_null controller) showPremiumToast(emoji); }, .mode = HistoryView::ComposeControlsMode::Normal, - .sendMenuType = SendMenu::Type::SilentOnly, + .sendMenuDetails = sendMenuDetails(), .stickerOrEmojiChosen = _controller->stickerOrEmojiChosen(), .customPlaceholder = PlaceholderText( _controller->uiShow(), @@ -473,6 +473,15 @@ void ReplyArea::chooseAttach( crl::guard(this, [=] { _choosingAttach = false; })); } +Fn ReplyArea::sendMenuDetails() const { + return crl::guard(this, [=] { + return SendMenu::Details{ + .type = SendMenu::Type::SilentOnly, + .effectAllowed = _data.peer && _data.peer->isUser(), + }; + }); +} + bool ReplyArea::confirmSendingFiles( not_null data, std::optional overrideSendImagesAsPhotos, @@ -528,7 +537,7 @@ bool ReplyArea::confirmSendingFiles( .limits = DefaultLimitsForPeer(_data.peer), .check = DefaultCheckForPeer(show, _data.peer), .sendType = Api::SendType::Normal, - .sendMenuType = SendMenu::Type::SilentOnly, + .sendMenuDetails = sendMenuDetails(), .stOverride = &st::storiesComposeControls, .confirmed = crl::guard(this, confirmed), .cancelled = _controls->restoreTextCallback(insertTextOnCancel), diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.h b/Telegram/SourceFiles/media/stories/media_stories_reply.h index 90214b421..3e4ff217b 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reply.h +++ b/Telegram/SourceFiles/media/stories/media_stories_reply.h @@ -38,6 +38,10 @@ namespace Main { class Session; } // namespace Main +namespace SendMenu { +struct Details; +} // namespace SendMenu + namespace Ui { struct PreparedList; class SendFilesWay; @@ -141,6 +145,8 @@ private: void sendVoice(VoiceToSend &&data); void chooseAttach(std::optional overrideSendImagesAsPhotos); + [[nodiscard]] Fn sendMenuDetails() const; + void showPremiumToast(not_null emoji); [[nodiscard]] bool showSlowmodeError(); diff --git a/Telegram/SourceFiles/media/stories/media_stories_share.cpp b/Telegram/SourceFiles/media/stories/media_stories_share.cpp index 76e353c00..773239e0b 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_share.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_share.cpp @@ -128,9 +128,7 @@ namespace Media::Stories { if (action.replyTo) { sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to; } - const auto silentPost = ShouldSendSilent( - threadPeer, - action.options); + const auto silentPost = ShouldSendSilent(threadPeer, options); if (silentPost) { sendFlags |= MTPmessages_SendMedia::Flag::f_silent; } @@ -140,6 +138,12 @@ namespace Media::Stories { if (options.shortcutId) { sendFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut; } + if (options.effectId) { + sendFlags |= MTPmessages_SendMedia::Flag::f_effect; + } + if (options.invertCaption) { + sendFlags |= MTPmessages_SendMedia::Flag::f_invert_media; + } const auto done = [=] { if (!--state->requests) { if (show->valid()) { @@ -161,14 +165,17 @@ namespace Media::Stories { MTP_long(randomId), MTPReplyMarkup(), MTPVector(), - MTP_int(action.options.scheduled), + MTP_int(options.scheduled), MTP_inputPeerEmpty(), - Data::ShortcutIdToMTP(session, action.options.shortcutId) + Data::ShortcutIdToMTP(session, options.shortcutId), + MTP_long(options.effectId) ), [=]( const MTPUpdates &result, const MTP::Response &response) { done(); - }, [=](const MTP::Error &error, const MTP::Response &response) { + }, [=]( + const MTP::Error &error, + const MTP::Response &response) { api->sendMessageFail(error, threadPeer, randomId); done(); }); diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style index 3a6a372cc..938df0865 100644 --- a/Telegram/SourceFiles/media/view/media_view.style +++ b/Telegram/SourceFiles/media/view/media_view.style @@ -636,6 +636,8 @@ storiesEmojiPan: EmojiPan(defaultEmojiPan) { menuWhenOnline: icon {{ "menu/send_when_online", storiesComposeWhiteText }}; menuSpoiler: icon {{ "menu/spoiler_on", storiesComposeWhiteText }}; menuSpoilerOff: icon {{ "menu/spoiler_off", storiesComposeWhiteText }}; + menuBelow: icon {{ "menu/link_below", storiesComposeWhiteText }}; + menuAbove: icon {{ "menu/link_above", storiesComposeWhiteText }}; stripBubble: icon{ { "chat/reactions_bubble_shadow", windowShadowFg }, diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index 83ff90465..6ee4937c6 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -352,8 +352,8 @@ public: rpl::producer adjustShadowLeft() const override { return rpl::single(false); } - SendMenu::Type sendMenuType() const override { - return SendMenu::Type::SilentOnly; + SendMenu::Details sendMenuDetails() const override { + return { SendMenu::Type::SilentOnly }; } bool showMediaPreview( @@ -419,7 +419,6 @@ OverlayWidget::OverlayWidget() , _widget(_surface->rpWidget()) , _fullscreen(Core::App().settings().mediaViewPosition().maximized == 2) , _windowed(Core::App().settings().mediaViewPosition().maximized == 0) -, _cachedReactionIconFactory(std::make_unique()) , _layerBg(std::make_unique(_body)) , _docDownload(_body, tr::lng_media_download(tr::now), st::mediaviewFileLink) , _docSaveAs(_body, tr::lng_mediaview_save_as(tr::now), st::mediaviewFileLink) @@ -4310,11 +4309,6 @@ auto OverlayWidget::storiesStickerOrEmojiChosen() return _storiesStickerOrEmojiChosen.events(); } -auto OverlayWidget::storiesCachedReactionIconFactory() --> HistoryView::Reactions::CachedIconFactory & { - return *_cachedReactionIconFactory; -} - void OverlayWidget::storiesJumpTo( not_null session, FullStoryId id, diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h index 94a7e7b2f..02bdca5fb 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h @@ -55,10 +55,6 @@ namespace Window::Theme { struct Preview; } // namespace Window::Theme -namespace HistoryView::Reactions { -class CachedIconFactory; -} // namespace HistoryView::Reactions - namespace Media::Player { struct TrackState; } // namespace Media::Player @@ -251,8 +247,6 @@ private: std::shared_ptr storiesShow() override; auto storiesStickerOrEmojiChosen() -> rpl::producer override; - auto storiesCachedReactionIconFactory() - -> HistoryView::Reactions::CachedIconFactory & override; void storiesRedisplay(not_null story) override; void storiesJumpTo( not_null session, @@ -629,8 +623,6 @@ private: bool _showAsPip = false; std::unique_ptr _stories; - using ReactionIconFactory = HistoryView::Reactions::CachedIconFactory; - std::unique_ptr _cachedReactionIconFactory; std::shared_ptr _cachedShow; rpl::event_stream<> _storiesChanged; Main::Session *_storiesSession = nullptr; diff --git a/Telegram/SourceFiles/menu/menu_send.cpp b/Telegram/SourceFiles/menu/menu_send.cpp index a31fcf103..58328da12 100644 --- a/Telegram/SourceFiles/menu/menu_send.cpp +++ b/Telegram/SourceFiles/menu/menu_send.cpp @@ -9,110 +9,745 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_common.h" #include "base/event_filter.h" +#include "base/unixtime.h" #include "boxes/abstract_box.h" +#include "chat_helpers/compose/compose_show.h" +#include "chat_helpers/stickers_emoji_pack.h" #include "core/shortcuts.h" +#include "history/admin_log/history_admin_log_item.h" +#include "history/view/media/history_view_sticker.h" +#include "history/view/reactions/history_view_reactions_selector.h" +#include "history/view/history_view_element.h" +#include "history/view/history_view_fake_items.h" #include "history/view/history_view_schedule_box.h" +#include "history/history.h" +#include "history/history_item.h" +#include "history/history_unread_things.h" #include "lang/lang_keys.h" +#include "lottie/lottie_single_player.h" +#include "ui/chat/chat_style.h" +#include "ui/chat/chat_theme.h" +#include "ui/effects/path_shift_gradient.h" +#include "ui/effects/radial_animation.h" +#include "ui/effects/ripple_animation.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/labels.h" #include "ui/widgets/popup_menu.h" +#include "ui/widgets/shadow.h" +#include "ui/wrap/padding_wrap.h" +#include "ui/painter.h" +#include "data/data_document.h" +#include "data/data_document_media.h" #include "data/data_peer.h" #include "data/data_forum.h" #include "data/data_forum_topic.h" +#include "data/data_message_reactions.h" #include "data/data_session.h" #include "main/main_session.h" -#include "history/history.h" -#include "history/history_unread_things.h" #include "apiwrap.h" +#include "settings/settings_premium.h" +#include "window/themes/window_theme.h" +#include "window/section_widget.h" +#include "styles/style_chat.h" #include "styles/style_chat_helpers.h" #include "styles/style_menu_icons.h" +#include "styles/style_window.h" #include namespace SendMenu { +namespace { -Fn DefaultSilentCallback(Fn send) { - return [=] { send({ .silent = true }); }; +constexpr auto kToggleDuration = crl::time(400); + +class Delegate final : public HistoryView::DefaultElementDelegate { +public: + Delegate(not_null pathGradient) + : _pathGradient(pathGradient) { + } + +private: + bool elementAnimationsPaused() override { + return false; + } + not_null elementPathShiftGradient() override { + return _pathGradient; + } + HistoryView::Context elementContext() override { + return HistoryView::Context::ContactPreview; + } + + const not_null _pathGradient; +}; + +class EffectPreview final : public Ui::RpWidget { +public: + EffectPreview( + not_null parent, + std::shared_ptr show, + Details details, + QPoint position, + const Data::Reaction &effect, + Fn action, + Fn done); + + void hideAnimated(); + +private: + void paintEvent(QPaintEvent *e) override; + void mousePressEvent(QMouseEvent *e) override; + + [[nodiscard]] bool canSend() const; + + void setupGeometry(QPoint position); + void setupBackground(); + void setupItem(); + void repaintBackground(); + void setupLottie(); + void setupSend(Details details); + void createLottie(); + + [[nodiscard]] bool ready() const; + void paintLoading(QPainter &p); + void paintLottie(QPainter &p); + bool checkIconBecameLoaded(); + [[nodiscard]] bool checkLoaded(); + void toggle(bool shown); + + const EffectId _effectId = 0; + const Data::Reaction _effect; + const std::shared_ptr _show; + const std::shared_ptr _theme; + const std::unique_ptr _chatStyle; + const std::unique_ptr _pathGradient; + const std::unique_ptr _delegate; + const not_null _history; + const AdminLog::OwnedItem _replyTo; + const AdminLog::OwnedItem _item; + const std::unique_ptr _send; + const std::unique_ptr> _premiumPromoLabel; + const not_null _bottom; + const Fn _close; + const Fn _actionWithEffect; + + QImage _icon; + std::shared_ptr _media; + QByteArray _bytes; + QString _filepath; + std::unique_ptr _lottie; + + QRect _inner; + QImage _bg; + QPoint _itemShift; + QRect _iconRect; + std::unique_ptr _loading; + + Ui::Animations::Simple _shownAnimation; + QPixmap _bottomCache; + bool _hiding = false; + + rpl::lifetime _readyCheckLifetime; + +}; + +class BottomRounded final : public Ui::FlatButton { +public: + using FlatButton::FlatButton; + +private: + QImage prepareRippleMask() const override; + void paintEvent(QPaintEvent *e) override; + +}; + +QImage BottomRounded::prepareRippleMask() const { + const auto fill = false; + return Ui::RippleAnimation::MaskByDrawer(size(), fill, [&](QPainter &p) { + const auto radius = st::previewMenu.radius; + const auto expanded = rect().marginsAdded({ 0, 2 * radius, 0, 0 }); + p.drawRoundedRect(expanded, radius, radius); + }); } -Fn DefaultScheduleCallback( - std::shared_ptr show, - Type type, - Fn send) { - return [=, weak = Ui::MakeWeak(show->toastParent())] { - show->showBox( - HistoryView::PrepareScheduleBox( - weak, - type, - [=](Api::SendOptions options) { send(options); }), - Ui::LayerOption::KeepOther); +void BottomRounded::paintEvent(QPaintEvent *e) { + auto p = QPainter(this); + auto hq = PainterHighQualityEnabler(p); + const auto radius = st::previewMenu.radius; + const auto expanded = rect().marginsAdded({ 0, 2 * radius, 0, 0 }); + p.setPen(Qt::NoPen); + const auto &st = st::previewMarkRead; + if (isOver()) { + p.setBrush(st.overBgColor); + } + p.drawRoundedRect(expanded, radius, radius); + p.end(); + + Ui::FlatButton::paintEvent(e); +} + +[[nodiscard]] Data::PossibleItemReactionsRef LookupPossibleEffects( + not_null session) { + auto result = Data::PossibleItemReactionsRef(); + const auto reactions = &session->data().reactions(); + const auto &effects = reactions->list(Data::Reactions::Type::Effects); + const auto premiumPossible = session->premiumPossible(); + auto added = base::flat_set(); + result.recent.reserve(effects.size()); + result.stickers.reserve(effects.size()); + for (const auto &reaction : effects) { + if (premiumPossible || !reaction.premium) { + if (added.emplace(reaction.id).second) { + if (reaction.aroundAnimation) { + result.recent.push_back(&reaction); + } else { + result.stickers.push_back(&reaction); + } + } + } + } + return result; +} + +[[nodiscard]] Fn ComposeActionWithEffect( + Fn sendAction, + EffectId id, + Fn done) { + return [=](Action action, Details details) { + action.options.effectId = id; + + const auto onstack = done; + sendAction(action, details); + if (onstack) { + onstack(); + } }; } -Fn DefaultWhenOnlineCallback(Fn send) { - return [=] { send(Api::DefaultSendWhenOnlineOptions()); }; +EffectPreview::EffectPreview( + not_null parent, + std::shared_ptr show, + Details details, + QPoint position, + const Data::Reaction &effect, + Fn action, + Fn done) +: RpWidget(parent) +, _effectId(effect.id.custom()) +, _effect(effect) +, _show(show) +, _theme(Window::Theme::DefaultChatThemeOn(lifetime())) +, _chatStyle( + std::make_unique( + _show->session().colorIndicesValue())) +, _pathGradient( + HistoryView::MakePathShiftGradient(_chatStyle.get(), [=] { update(); })) +, _delegate(std::make_unique(_pathGradient.get())) +, _history(show->session().data().history( + PeerData::kServiceNotificationsId)) +, _replyTo(HistoryView::GenerateItem( + _delegate.get(), + _history, + HistoryView::GenerateUser( + _history, + tr::lng_settings_chat_message_reply_from(tr::now)), + FullMsgId(), + tr::lng_settings_chat_message(tr::now))) +, _item(HistoryView::GenerateItem( + _delegate.get(), + _history, + _history->peer->id, + _replyTo->data()->fullId(), + tr::lng_settings_chat_message_reply(tr::now), + Data::Reactions::kFakeEffectId)) +, _send(canSend() + ? std::make_unique( + this, + tr::lng_effect_send(tr::now), + st::effectPreviewSend) + : nullptr) +, _premiumPromoLabel(canSend() + ? nullptr + : std::make_unique>( + this, + object_ptr( + this, + tr::lng_effect_premium( + lt_link, + tr::lng_effect_premium_link() | Ui::Text::ToLink(), + Ui::Text::WithEntities), + st::effectPreviewPromoLabel), + st::effectPreviewPromoPadding)) +, _bottom(_send ? ((Ui::RpWidget*)_send.get()) : _premiumPromoLabel.get()) +, _close(done) +, _actionWithEffect(ComposeActionWithEffect(action, _effectId, done)) { + _chatStyle->apply(_theme.get()); + + setupGeometry(position); + setupItem(); + setupBackground(); + setupLottie(); + setupSend(details); + + toggle(true); +} + +void EffectPreview::paintEvent(QPaintEvent *e) { + checkIconBecameLoaded(); + + const auto progress = _shownAnimation.value(_hiding ? 0. : 1.); + if (!progress) { + return; + } + + auto p = QPainter(this); + p.setOpacity(progress); + p.drawImage(0, 0, _bg); + + if (!_bottomCache.isNull()) { + p.drawPixmap(_bottom->pos(), _bottomCache); + } + + if (!ready()) { + paintLoading(p); + } else { + _loading = nullptr; + p.drawImage(_iconRect, _icon); + if (!_hiding) { + p.setOpacity(1.); + } + paintLottie(p); + } +} + +bool EffectPreview::ready() const { + return !_icon.isNull() && _lottie && _lottie->ready(); +} + +void EffectPreview::paintLoading(QPainter &p) { + if (!_loading) { + _loading = std::make_unique([=] { + update(); + }, st::effectPreviewLoading); + _loading->start(st::defaultInfiniteRadialAnimation.linearPeriod); + } + const auto loading = _iconRect.marginsRemoved( + { st::lineWidth, st::lineWidth, st::lineWidth, st::lineWidth }); + auto hq = PainterHighQualityEnabler(p); + Ui::InfiniteRadialAnimation::Draw( + p, + _loading->computeState(), + loading.topLeft(), + loading.size(), + width(), + _chatStyle->msgInDateFg(), + st::effectPreviewLoading.thickness); +} + +void EffectPreview::paintLottie(QPainter &p) { + const auto factor = style::DevicePixelRatio(); + auto request = Lottie::FrameRequest(); + request.box = _inner.size() * factor; + const auto rightAligned = _item->hasRightLayout(); + if (!rightAligned) { + request.mirrorHorizontal = true; + } + const auto frame = _lottie->frameInfo(request); + p.drawImage( + QRect(_inner.topLeft(), frame.image.size() / factor), + frame.image); + _lottie->markFrameShown(); +} + +void EffectPreview::hideAnimated() { + toggle(false); +} + +void EffectPreview::mousePressEvent(QMouseEvent *e) { + hideAnimated(); +} + +void EffectPreview::setupGeometry(QPoint position) { + const auto parent = parentWidget(); + const auto innerSize = HistoryView::Sticker::MessageEffectSize(); + const auto shadow = st::previewMenu.shadow; + const auto extend = shadow.extend; + _inner = QRect(QPoint(extend.left(), extend.top()), innerSize); + _bottom->resizeToWidth(_inner.width()); + const auto size = _inner.marginsAdded(extend).size() + + QSize(0, _bottom->height()); + const auto left = std::max( + std::min( + position.x() - size.width() / 2, + parent->width() - size.width()), + 0); + const auto topMin = std::min((parent->height() - size.height()) / 2, 0); + const auto top = std::max( + std::min( + position.y() - size.height() / 2, + parent->height() - size.height()), + topMin); + setGeometry(left, top, size.width(), size.height()); + _bottom->setGeometry( + _inner.x(), + _inner.y() + _inner.height(), + _inner.width(), + _bottom->height()); +} + +void EffectPreview::setupBackground() { + const auto ratio = style::DevicePixelRatio(); + _bg = QImage( + size() * ratio, + QImage::Format_ARGB32_Premultiplied); + _bg.setDevicePixelRatio(ratio); + repaintBackground(); + _theme->repaintBackgroundRequests() | rpl::start_with_next([=] { + repaintBackground(); + update(); + }, lifetime()); +} + +void EffectPreview::setupItem() { + _item->resizeGetHeight(st::windowMinWidth); + + const auto icon = _item->effectIconGeometry(); + Assert(!icon.isEmpty()); + + const auto size = _inner.size(); + const auto shift = _item->hasRightLayout() + ? (-size.width() / 3) + : (size.width() / 3); + const auto position = QPoint( + shift + icon.x() + (icon.width() - size.width()) / 2, + icon.y() + (icon.height() - size.height()) / 2); + _itemShift = _inner.topLeft() - position; + _iconRect = icon.translated(_itemShift); +} + +void EffectPreview::repaintBackground() { + const auto ratio = style::DevicePixelRatio(); + const auto inner = _inner.size() + QSize(0, _bottom->height()); + auto bg = QImage( + inner * ratio, + QImage::Format_ARGB32_Premultiplied); + bg.setDevicePixelRatio(ratio); + + { + auto p = Painter(&bg); + Window::SectionWidget::PaintBackground( + p, + _theme.get(), + QSize(inner.width(), inner.height() * 5), + QRect(QPoint(), inner)); + p.fillRect( + QRect(0, _inner.height(), _inner.width(), _bottom->height()), + st::previewMarkRead.bgColor); + + p.translate(_itemShift - _inner.topLeft()); + auto rect = QRect(0, 0, st::windowMinWidth, _inner.height()); + auto context = _theme->preparePaintContext( + _chatStyle.get(), + rect, + rect, + false); + context.outbg = _item->hasOutLayout(); + _item->draw(p, context); + p.translate(_inner.topLeft() - _itemShift); + + auto hq = PainterHighQualityEnabler(p); + p.setCompositionMode(QPainter::CompositionMode_DestinationIn); + auto roundRect = Ui::RoundRect(st::previewMenu.radius, st::menuBg); + roundRect.paint(p, QRect(QPoint(), inner), RectPart::AllCorners); + } + + _bg.fill(Qt::transparent); + auto p = QPainter(&_bg); + + const auto &shadow = st::previewMenu.animation.shadow; + const auto shadowed = QRect(_inner.topLeft(), inner); + Ui::Shadow::paint(p, shadowed, width(), shadow); + p.drawImage(_inner.topLeft(), bg); +} + +void EffectPreview::setupLottie() { + const auto reactions = &_show->session().data().reactions(); + reactions->preloadEffectImageFor(_effectId); + + if (const auto document = _effect.aroundAnimation) { + _media = document->createMediaView(); + } else { + _media = _effect.selectAnimation->createMediaView(); + } + rpl::single(rpl::empty) | rpl::then( + _show->session().downloaderTaskFinished() + ) | rpl::start_with_next([=] { + if (checkLoaded()) { + _readyCheckLifetime.destroy(); + createLottie(); + } + }, _readyCheckLifetime); +} + +void EffectPreview::createLottie() { + _lottie = _show->session().emojiStickersPack().effectPlayer( + _media->owner(), + _bytes, + _filepath, + Stickers::EffectType::MessageEffect); + const auto raw = _lottie.get(); + raw->updates( + ) | rpl::start_with_next([=](Lottie::Update update) { + v::match(update.data, [&](const Lottie::Information &information) { + }, [&](const Lottie::DisplayFrameRequest &request) { + this->update(); + }); + }, raw->lifetime()); +} + +bool EffectPreview::canSend() const { + return !_effect.premium || _show->session().premium(); +} + +void EffectPreview::setupSend(Details details) { + if (_send) { + _send->setClickedCallback([=] { + _actionWithEffect({}, details); + }); + const auto type = details.type; + SetupMenuAndShortcuts(_send.get(), _show, [=] { + return Details{ .type = type }; + }, _actionWithEffect); + } else { + _premiumPromoLabel->entity()->setClickHandlerFilter([=](auto&&...) { + const auto window = _show->resolveWindow( + ChatHelpers::WindowUsage::PremiumPromo); + if (window) { + if (const auto onstack = _close) { + onstack(); + } + Settings::ShowPremium(window, "message_effect"); + } + return false; + }); + } +} + +bool EffectPreview::checkIconBecameLoaded() { + if (!_icon.isNull()) { + return false; + } + const auto reactions = &_show->session().data().reactions(); + _icon = reactions->resolveEffectImageFor(_effect.id.custom()); + if (_icon.isNull()) { + return false; + } + repaintBackground(); + return true; +} + +bool EffectPreview::checkLoaded() { + if (checkIconBecameLoaded()) { + update(); + } + if (_effect.aroundAnimation) { + _bytes = _media->bytes(); + _filepath = _media->owner()->filepath(); + } else { + _bytes = _media->videoThumbnailContent(); + } + return !_icon.isNull() && (!_bytes.isEmpty() || !_filepath.isEmpty()); +} + +void EffectPreview::toggle(bool shown) { + if (!shown && _hiding) { + return; + } + _hiding = !shown; + if (_bottomCache.isNull()) { + _bottomCache = Ui::GrabWidget(_bottom); + _bottom->hide(); + } + _shownAnimation.start([=] { + update(); + if (!_shownAnimation.animating()) { + if (_hiding) { + delete this; + } else { + _bottomCache = QPixmap(); + _bottom->show(); + } + } + }, shown ? 0. : 1., shown ? 1. : 0., kToggleDuration, anim::easeOutCirc); + show(); +} + +} // namespace + +Fn DefaultCallback( + std::shared_ptr show, + Fn send) { + const auto guard = Ui::MakeWeak(show->toastParent()); + return [=](Action action, Details details) { + if (action.type == ActionType::Send) { + send(action.options); + return; + } + auto box = HistoryView::PrepareScheduleBox( + guard, + show, + details, + send, + action.options); + const auto weak = Ui::MakeWeak(box.data()); + show->showBox(std::move(box)); + if (const auto strong = weak.data()) { + strong->setCloseByOutsideClick(false); + } + }; } FillMenuResult FillSendMenu( not_null menu, - Type type, - Fn silent, - Fn schedule, - Fn whenOnline, - const style::ComposeIcons *iconsOverride) { - if (!silent && !schedule) { - return FillMenuResult::None; + std::shared_ptr showForEffect, + Details details, + Fn action, + const style::ComposeIcons *iconsOverride, + std::optional desiredPositionOverride) { + const auto type = details.type; + const auto sending = (type != Type::Disabled); + const auto empty = !sending + && (details.spoiler == SpoilerState::None) + && (details.caption == CaptionState::None); + if (empty || !action) { + return FillMenuResult::Skipped; } const auto &icons = iconsOverride ? *iconsOverride : st::defaultComposeIcons; - const auto now = type; - if (now == Type::Disabled - || (!silent && now == Type::SilentOnly)) { - return FillMenuResult::None; - } - if (silent && now != Type::Reminder) { + if (sending && type != Type::Reminder) { menu->addAction( tr::lng_send_silent_message(tr::now), - silent, + [=] { action({ Api::SendOptions{ .silent = true } }, details); }, &icons.menuMute); } - if (schedule && now != Type::SilentOnly) { + if (sending && type != Type::SilentOnly) { menu->addAction( - (now == Type::Reminder + (type == Type::Reminder ? tr::lng_reminder_message(tr::now) : tr::lng_schedule_message(tr::now)), - schedule, + [=] { action({ .type = ActionType::Schedule }, details); }, &icons.menuSchedule); } - if (whenOnline && now == Type::ScheduledToUser) { + if (sending && type == Type::ScheduledToUser) { menu->addAction( tr::lng_scheduled_send_until_online(tr::now), - whenOnline, + [=] { action( + { Api::DefaultSendWhenOnlineOptions() }, + details); }, &icons.menuWhenOnline); } - return FillMenuResult::Success; + + if ((type != Type::Disabled) + && ((details.spoiler != SpoilerState::None) + || (details.caption != CaptionState::None))) { + menu->addSeparator(&st::expandedMenuSeparator); + } + if (details.spoiler != SpoilerState::None) { + const auto spoilered = (details.spoiler == SpoilerState::Enabled); + menu->addAction( + (spoilered + ? tr::lng_context_disable_spoiler(tr::now) + : tr::lng_context_spoiler_effect(tr::now)), + [=] { action({ .type = spoilered + ? ActionType::SpoilerOff + : ActionType::SpoilerOn + }, details); }, + spoilered ? &icons.menuSpoilerOff : &icons.menuSpoiler); + } + if (details.caption != CaptionState::None) { + const auto above = (details.caption == CaptionState::Above); + menu->addAction( + (above + ? tr::lng_caption_move_down(tr::now) + : tr::lng_caption_move_up(tr::now)), + [=] { action({ .type = above + ? ActionType::CaptionDown + : ActionType::CaptionUp + }, details); }, + above ? &icons.menuBelow : &icons.menuAbove); + } + + using namespace HistoryView::Reactions; + const auto effect = std::make_shared>(); + const auto position = desiredPositionOverride.value_or(QCursor::pos()); + const auto selector = (showForEffect && details.effectAllowed) + ? AttachSelectorToMenu( + menu, + position, + st::reactPanelEmojiPan, + showForEffect, + LookupPossibleEffects(&showForEffect->session()), + { tr::lng_effect_add_title(tr::now) }, + nullptr, // iconFactory + [=] { return (*effect) != nullptr; }) // paused + : base::make_unexpected(AttachSelectorResult::Skipped); + if (!selector) { + if (selector.error() == AttachSelectorResult::Failed) { + return FillMenuResult::Failed; + } + menu->prepareGeometryFor(position); + return FillMenuResult::Prepared; + } + + (*selector)->chosen( + ) | rpl::start_with_next([=](ChosenReaction chosen) { + const auto &reactions = showForEffect->session().data().reactions(); + const auto &effects = reactions.list(Data::Reactions::Type::Effects); + const auto i = ranges::find(effects, chosen.id, &Data::Reaction::id); + if (i != end(effects)) { + if (const auto strong = effect->data()) { + strong->hideAnimated(); + } + const auto weak = Ui::MakeWeak(menu); + const auto done = [=] { + delete effect->data(); + if (const auto strong = weak.data()) { + strong->hideMenu(true); + } + }; + *effect = Ui::CreateChild( + menu, + showForEffect, + details, + menu->mapFromGlobal(chosen.globalGeometry.center()), + *i, + action, + crl::guard(menu, done)); + (*effect)->show(); + } + }, menu->lifetime()); + + return FillMenuResult::Prepared; } void SetupMenuAndShortcuts( not_null button, - Fn type, - Fn silent, - Fn schedule, - Fn whenOnline) { - if (!silent && !schedule && !whenOnline) { - return; - } + std::shared_ptr show, + Fn details, + Fn action) { const auto menu = std::make_shared>(); const auto showMenu = [=] { *menu = base::make_unique_q( button, st::popupMenuWithIcons); - const auto result = FillSendMenu(*menu, type(), silent, schedule, whenOnline); - const auto success = (result == FillMenuResult::Success); - if (success) { - (*menu)->popup(QCursor::pos()); + const auto result = FillSendMenu(*menu, show, details(), action); + if (result != FillMenuResult::Prepared) { + return false; } - return success; + (*menu)->popupPrepared(); + return true; }; base::install_event_filter(button, [=](not_null e) { if (e->type() == QEvent::ContextMenu && showMenu()) { @@ -127,24 +762,21 @@ void SetupMenuAndShortcuts( }) | rpl::start_with_next([=](not_null request) { using Command = Shortcuts::Command; - const auto now = type(); - if (now == Type::Disabled - || (!silent && now == Type::SilentOnly)) { + const auto now = details().type; + if (now == Type::Disabled) { return; } - (silent - && (now != Type::Reminder) + ((now != Type::Reminder) && request->check(Command::SendSilentMessage) && request->handle([=] { - silent(); + action({ Api::SendOptions{ .silent = true } }, details()); return true; })) || - (schedule - && (now != Type::SilentOnly) + ((now != Type::SilentOnly) && request->check(Command::ScheduleMessage) && request->handle([=] { - schedule(); + action({ .type = ActionType::Schedule }, details()); return true; })) || diff --git a/Telegram/SourceFiles/menu/menu_send.h b/Telegram/SourceFiles/menu/menu_send.h index 03b7f7ec5..2c6dbb9b7 100644 --- a/Telegram/SourceFiles/menu/menu_send.h +++ b/Telegram/SourceFiles/menu/menu_send.h @@ -7,13 +7,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "api/api_common.h" + namespace style { struct ComposeIcons; } // namespace style -namespace Api { -struct SendOptions; -} // namespace Api +namespace ChatHelpers { +class Show; +} // namespace ChatHelpers namespace Ui { class PopupMenu; @@ -27,7 +29,7 @@ class Thread; namespace SendMenu { -enum class Type { +enum class Type : uchar { Disabled, SilentOnly, Scheduled, @@ -35,32 +37,62 @@ enum class Type { Reminder, }; -enum class FillMenuResult { - Success, +enum class SpoilerState : uchar { None, + Enabled, + Possible, }; -Fn DefaultSilentCallback(Fn send); -Fn DefaultScheduleCallback( - std::shared_ptr show, - Type type, +enum class CaptionState : uchar { + None, + Below, + Above, +}; + +struct Details { + Type type = Type::Disabled; + SpoilerState spoiler = SpoilerState::None; + CaptionState caption = CaptionState::None; + bool effectAllowed = false; +}; + +enum class FillMenuResult : uchar { + Prepared, + Skipped, + Failed, +}; + +enum class ActionType : uchar { + Send, + Schedule, + SpoilerOn, + SpoilerOff, + CaptionUp, + CaptionDown, +}; +struct Action { + using Type = ActionType; + + Api::SendOptions options; + Type type = Type::Send; +}; +[[nodiscard]] Fn DefaultCallback( + std::shared_ptr show, Fn send); -Fn DefaultWhenOnlineCallback(Fn send); FillMenuResult FillSendMenu( not_null menu, - Type type, - Fn silent, - Fn schedule, - Fn whenOnline, - const style::ComposeIcons *iconsOverride = nullptr); + std::shared_ptr showForEffect, + Details details, + Fn action, + const style::ComposeIcons *iconsOverride = nullptr, + std::optional desiredPositionOverride = std::nullopt); void SetupMenuAndShortcuts( not_null button, - Fn type, - Fn silent, - Fn schedule, - Fn whenOnline); + std::shared_ptr show, + Fn details, + Fn action); void SetupUnreadMentionsMenu( not_null button, diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index 963785e0b..f62e07bf6 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -38,7 +38,7 @@ inputMediaVenue#c13d1c11 geo_point:InputGeoPoint title:string address:string pro inputMediaPhotoExternal#e5bbfe1a flags:# spoiler:flags.1?true url:string ttl_seconds:flags.0?int = InputMedia; inputMediaDocumentExternal#fb52dc99 flags:# spoiler:flags.1?true url:string ttl_seconds:flags.0?int = InputMedia; inputMediaGame#d33f43f3 id:InputGame = InputMedia; -inputMediaInvoice#8eb5a6d5 flags:# title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:string provider_data:DataJSON start_param:flags.1?string extended_media:flags.2?InputMedia = InputMedia; +inputMediaInvoice#405fef0d flags:# title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:flags.3?string provider_data:DataJSON start_param:flags.1?string extended_media:flags.2?InputMedia = InputMedia; inputMediaGeoLive#971fa843 flags:# stopped:flags.0?true geo_point:InputGeoPoint heading:flags.2?int period:flags.1?int proximity_notification_radius:flags.3?int = InputMedia; inputMediaPoll#f94e5f1 flags:# poll:Poll correct_answers:flags.0?Vector solution:flags.1?string solution_entities:flags.1?Vector = InputMedia; inputMediaDice#e66fbf7b emoticon:string = InputMedia; @@ -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#2357bf25 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 flags2:# offline:flags2.1?true id:int from_id:flags.8?Peer from_boosts_applied:flags.29?int peer_id:Peer saved_peer_id:flags.28?Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long via_business_bot_id:flags2.0?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 quick_reply_shortcut_id:flags.30?int = Message; +message#94345242 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 flags2:# offline:flags2.1?true id:int from_id:flags.8?Peer from_boosts_applied:flags.29?int peer_id:Peer saved_peer_id:flags.28?Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long via_business_bot_id:flags2.0?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 quick_reply_shortcut_id:flags.30?int effect:flags2.2?long factcheck:flags2.3?FactCheck = 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; @@ -413,6 +413,8 @@ updateBotNewBusinessMessage#9ddb347c flags:# connection_id:string message:Messag updateBotEditBusinessMessage#7df587c flags:# connection_id:string message:Message reply_to_message:flags.0?Message qts:int = Update; updateBotDeleteBusinessMessage#a02a982e connection_id:string peer:Peer messages:Vector qts:int = Update; updateNewStoryReaction#1824e40b story_id:int peer:Peer reaction:Reaction = Update; +updateBroadcastRevenueTransactions#dfd961f5 peer:Peer balances:BroadcastRevenueBalances = Update; +updateStarsBalance#fb85198 balance:long = Update; updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State; @@ -669,7 +671,7 @@ messageEntityStrike#bf0693d4 offset:int length:int = MessageEntity; messageEntityBankCard#761e6af4 offset:int length:int = MessageEntity; messageEntitySpoiler#32ca960f offset:int length:int = MessageEntity; messageEntityCustomEmoji#c8cf05f8 offset:int length:int document_id:long = MessageEntity; -messageEntityBlockquote#20df5d0 offset:int length:int = MessageEntity; +messageEntityBlockquote#f1ccaaac flags:# collapsed:flags.0?true offset:int length:int = MessageEntity; inputChannelEmpty#ee8c1e86 = InputChannel; inputChannel#f35aec28 channel_id:long access_hash:long = InputChannel; @@ -757,7 +759,7 @@ auth.sentCodeTypeMissedCall#82006484 prefix:string length:int = auth.SentCodeTyp auth.sentCodeTypeEmailCode#f450f59b flags:# apple_signin_allowed:flags.0?true google_signin_allowed:flags.1?true email_pattern:string length:int reset_available_period:flags.3?int reset_pending_date:flags.4?int = auth.SentCodeType; auth.sentCodeTypeSetUpEmailRequired#a5491dea flags:# apple_signin_allowed:flags.0?true google_signin_allowed:flags.1?true = auth.SentCodeType; auth.sentCodeTypeFragmentSms#d9565c39 url:string length:int = auth.SentCodeType; -auth.sentCodeTypeFirebaseSms#e57b1432 flags:# nonce:flags.0?bytes receipt:flags.1?string push_timeout:flags.1?int length:int = auth.SentCodeType; +auth.sentCodeTypeFirebaseSms#13c90f17 flags:# nonce:flags.0?bytes play_integrity_nonce:flags.2?bytes receipt:flags.1?string push_timeout:flags.1?int length:int = auth.SentCodeType; auth.sentCodeTypeSmsWord#a416ac81 flags:# beginning:flags.0?string = auth.SentCodeType; auth.sentCodeTypeSmsPhrase#b37794af flags:# beginning:flags.0?string = auth.SentCodeType; @@ -900,6 +902,7 @@ inputWebFileAudioAlbumThumbLocation#f46fe924 flags:# small:flags.2?true document upload.webFile#21e753bc size:int mime_type:string file_type:storage.FileType mtime:int bytes:bytes = upload.WebFile; payments.paymentForm#a0058751 flags:# can_save_credentials:flags.2?true password_missing:flags.3?true form_id:long bot_id:long title:string description:string photo:flags.5?WebDocument invoice:Invoice provider_id:long url:string native_provider:flags.4?string native_params:flags.4?DataJSON additional_methods:flags.6?Vector saved_info:flags.0?PaymentRequestedInfo saved_credentials:flags.1?Vector users:Vector = payments.PaymentForm; +payments.paymentFormStars#7bf6b15c flags:# form_id:long bot_id:long title:string description:string photo:flags.5?WebDocument invoice:Invoice users:Vector = payments.PaymentForm; payments.validatedRequestedInfo#d1451883 flags:# id:flags.0?string shipping_options:flags.1?Vector = payments.ValidatedRequestedInfo; @@ -907,6 +910,7 @@ payments.paymentResult#4e5f810d updates:Updates = payments.PaymentResult; payments.paymentVerificationNeeded#d8411139 url:string = payments.PaymentResult; payments.paymentReceipt#70c4fe03 flags:# date:int bot_id:long provider_id:long title:string description:string photo:flags.2?WebDocument invoice:Invoice info:flags.0?PaymentRequestedInfo shipping:flags.1?ShippingOption tip_amount:flags.3?long currency:string total_amount:long credentials_title:string users:Vector = payments.PaymentReceipt; +payments.paymentReceiptStars#dabbf83a flags:# date:int bot_id:long title:string description:string photo:flags.2?WebDocument invoice:Invoice currency:string total_amount:long transaction_id:string users:Vector = payments.PaymentReceipt; payments.savedInfo#fb8fe43c flags:# has_saved_credentials:flags.1?true saved_info:flags.0?PaymentRequestedInfo = payments.SavedInfo; @@ -1446,6 +1450,7 @@ attachMenuPeerTypeBroadcast#7bfbdefc = AttachMenuPeerType; inputInvoiceMessage#c5b56859 peer:InputPeer msg_id:int = InputInvoice; inputInvoiceSlug#c326caef slug:string = InputInvoice; inputInvoicePremiumGiftCode#98986c0d purpose:InputStorePaymentPurpose option:PremiumGiftCodeOption = InputInvoice; +inputInvoiceStars#1da33ad8 option:StarsTopupOption = InputInvoice; payments.exportedInvoice#aed0cbd9 url:string = payments.ExportedInvoice; @@ -1457,6 +1462,7 @@ inputStorePaymentPremiumSubscription#a6751e66 flags:# restore:flags.0?true upgra inputStorePaymentGiftPremium#616f7fe8 user_id:InputUser currency:string amount:long = InputStorePaymentPurpose; inputStorePaymentPremiumGiftCode#a3805f3f flags:# users:Vector boost_peer:flags.0?InputPeer 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; +inputStorePaymentStars#4f0ee8df flags:# stars:long currency:string amount:long = InputStorePaymentPurpose; premiumGiftOption#74c34319 flags:# months:int currency:string amount:long bot_url:string store_product:flags.0?string = PremiumGiftOption; @@ -1781,6 +1787,26 @@ reactionsNotifySettings#56e34970 flags:# messages_notify_from:flags.0?ReactionNo broadcastRevenueBalances#8438f1c6 current_balance:long available_balance:long overall_revenue:long = BroadcastRevenueBalances; +availableEffect#93c3e27e flags:# premium_required:flags.2?true id:long emoticon:string static_icon_id:flags.0?long effect_sticker_id:long effect_animation_id:flags.1?long = AvailableEffect; + +messages.availableEffectsNotModified#d1ed9a5b = messages.AvailableEffects; +messages.availableEffects#bddb616e hash:int effects:Vector documents:Vector = messages.AvailableEffects; + +factCheck#b89bfccf flags:# need_check:flags.0?true country:flags.1?string text:flags.1?TextWithEntities hash:long = FactCheck; + +starsTransactionPeerUnsupported#95f2bfe4 = StarsTransactionPeer; +starsTransactionPeerAppStore#b457b375 = StarsTransactionPeer; +starsTransactionPeerPlayMarket#7b560a0b = StarsTransactionPeer; +starsTransactionPeerPremiumBot#250dbaf8 = StarsTransactionPeer; +starsTransactionPeerFragment#e92fd902 = StarsTransactionPeer; +starsTransactionPeer#d80da15d peer:Peer = StarsTransactionPeer; + +starsTopupOption#bd915c0 flags:# extended:flags.1?true stars:long store_product:flags.0?string currency:string amount:long = StarsTopupOption; + +starsTransaction#cc7079b2 flags:# refund:flags.3?true id:string stars:long date:int peer:StarsTransactionPeer title:flags.0?string description:flags.1?string photo:flags.2?WebDocument = StarsTransaction; + +payments.starsStatus#8cf4ee60 flags:# balance:long history:Vector next_offset:flags.0?string chats:Vector users:Vector = payments.StarsStatus; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -1791,6 +1817,8 @@ invokeWithoutUpdates#bf9459b7 {X:Type} query:!X = X; invokeWithMessagesRange#365275f2 {X:Type} range:MessageRange query:!X = X; invokeWithTakeout#aca9fd2e {X:Type} takeout_id:long query:!X = X; invokeWithBusinessConnection#dd289f8e {X:Type} connection_id:string query:!X = X; +invokeWithGooglePlayIntegrity#1df92984 {X:Type} nonce:string token:string query:!X = X; +invokeWithApnsSecret#0dae54f8 {X:Type} nonce:string secret:string query:!X = X; auth.sendCode#a677244f phone_number:string api_id:int api_hash:string settings:CodeSettings = auth.SentCode; auth.signUp#aac7b717 flags:# no_joined_notifications:flags.0?true phone_number:string phone_code_hash:string first_name:string last_name:string = auth.Authorization; @@ -1804,7 +1832,7 @@ auth.importBotAuthorization#67a3ff2c flags:int api_id:int api_hash:string bot_au auth.checkPassword#d18b4d16 password:InputCheckPasswordSRP = auth.Authorization; auth.requestPasswordRecovery#d897bc66 = auth.PasswordRecovery; auth.recoverPassword#37096c70 flags:# code:string new_settings:flags.0?account.PasswordInputSettings = auth.Authorization; -auth.resendCode#3ef1a9bf phone_number:string phone_code_hash:string = auth.SentCode; +auth.resendCode#cae47523 flags:# phone_number:string phone_code_hash:string reason:flags.0?string = auth.SentCode; auth.cancelCode#1f040578 phone_number:string phone_code_hash:string = Bool; auth.dropTempAuthKeys#8e48a188 except_auth_keys:Vector = Bool; auth.exportLoginToken#b7e085fe api_id:int api_hash:string except_ids:Vector = auth.LoginToken; @@ -1812,7 +1840,7 @@ auth.importLoginToken#95ac5ce4 token:bytes = auth.LoginToken; auth.acceptLoginToken#e894ad4d token:bytes = Authorization; auth.checkRecoveryPassword#d36bf79 code:string = Bool; auth.importWebTokenAuthorization#2db873a9 api_id:int api_hash:string web_auth_token:string = auth.Authorization; -auth.requestFirebaseSms#89464b50 flags:# phone_number:string phone_code_hash:string safety_net_token:flags.0?string ios_push_secret:flags.1?string = Bool; +auth.requestFirebaseSms#8e39261e flags:# phone_number:string phone_code_hash:string safety_net_token:flags.0?string play_integrity_token:flags.2?string ios_push_secret:flags.1?string = Bool; auth.resetLoginEmail#7e960193 phone_number:string phone_code_hash:string = auth.SentCode; auth.reportMissingCode#cb9deff6 phone_number:string phone_code_hash:string mnc:string = Bool; @@ -1970,8 +1998,8 @@ messages.deleteHistory#b08f922a flags:# just_clear:flags.0?true revoke:flags.1?t messages.deleteMessages#e58e95d2 flags:# revoke:flags.0?true id:Vector = messages.AffectedMessages; messages.receivedMessages#5a954c0 max_id:int = Vector; messages.setTyping#58943ee2 flags:# peer:InputPeer top_msg_id:flags.0?int action:SendMessageAction = Bool; -messages.sendMessage#dff8042c flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true peer:InputPeer reply_to:flags.0?InputReplyTo message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut = Updates; -messages.sendMedia#7bd66041 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true peer:InputPeer reply_to:flags.0?InputReplyTo media:InputMedia message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut = Updates; +messages.sendMessage#983f9745 flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true peer:InputPeer reply_to:flags.0?InputReplyTo message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long = Updates; +messages.sendMedia#7852834e flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true peer:InputPeer reply_to:flags.0?InputReplyTo media:InputMedia message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long = Updates; messages.forwardMessages#d5039208 flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true drop_author:flags.11?true drop_media_captions:flags.12?true noforwards:flags.14?true from_peer:InputPeer id:Vector random_id:Vector to_peer:InputPeer top_msg_id:flags.9?int schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut = Updates; messages.reportSpam#cf1592db peer:InputPeer = Bool; messages.getPeerSettings#efd9a6a2 peer:InputPeer = messages.PeerSettings; @@ -2050,7 +2078,7 @@ messages.faveSticker#b9ffc55b id:InputDocument unfave:Bool = Bool; messages.getUnreadMentions#f107e790 flags:# peer:InputPeer top_msg_id:flags.0?int offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages; messages.readMentions#36e5bf4d flags:# peer:InputPeer top_msg_id:flags.0?int = messages.AffectedHistory; messages.getRecentLocations#702a40e0 peer:InputPeer limit:int hash:long = messages.Messages; -messages.sendMultiMedia#c964709 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true peer:InputPeer reply_to:flags.0?InputReplyTo multi_media:Vector schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut = Updates; +messages.sendMultiMedia#37b74355 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true peer:InputPeer reply_to:flags.0?InputReplyTo multi_media:Vector schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long = Updates; messages.uploadEncryptedFile#5057c497 peer:InputEncryptedChat file:InputEncryptedFile = EncryptedFile; messages.searchStickerSets#35705b8a flags:# exclude_featured:flags.0?true q:string hash:long = messages.FoundStickerSets; messages.getSplitRanges#1cff7e08 = Vector; @@ -2170,6 +2198,10 @@ messages.deleteQuickReplyMessages#e105e910 shortcut_id:int id:Vector = Upda messages.toggleDialogFilterTags#fd2dda49 enabled:Bool = Bool; messages.getMyStickers#d0b5e1fc offset_id:long limit:int = messages.MyStickers; messages.getEmojiStickerGroups#1dd840f5 hash:int = messages.EmojiGroups; +messages.getAvailableEffects#dea20a39 hash:int = messages.AvailableEffects; +messages.editFactCheck#589ee75 peer:InputPeer msg_id:int text:TextWithEntities = Updates; +messages.deleteFactCheck#d1da940c peer:InputPeer msg_id:int = Updates; +messages.getFactCheck#b9cdc5ee peer:InputPeer msg_id:Vector = Vector; 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; @@ -2280,6 +2312,7 @@ channels.setBoostsToUnblockRestrictions#ad399cee channel:InputChannel boosts:int channels.setEmojiStickers#3cd930b7 channel:InputChannel stickerset:InputStickerSet = Bool; channels.reportSponsoredMessage#af8ff6b9 channel:InputChannel random_id:bytes option:bytes = channels.SponsoredMessageReportResult; channels.restrictSponsoredMessages#9ae91519 channel:InputChannel restricted:Bool = Updates; +channels.searchPosts#d19f987b hashtag:string offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages; bots.sendCustomRequest#aa2769ed custom_method:string params:DataJSON = DataJSON; bots.answerWebhookJSONQuery#e6213f4d query_id:long data:DataJSON = Bool; @@ -2314,6 +2347,11 @@ payments.checkGiftCode#8e51b4c1 slug:string = payments.CheckedGiftCode; payments.applyGiftCode#f6e26854 slug:string = Updates; payments.getGiveawayInfo#f4239425 peer:InputPeer msg_id:int = payments.GiveawayInfo; payments.launchPrepaidGiveaway#5ff58f20 peer:InputPeer giveaway_id:long purpose:InputStorePaymentPurpose = Updates; +payments.getStarsTopupOptions#c00ec7d3 = Vector; +payments.getStarsStatus#104fcfa7 peer:InputPeer = payments.StarsStatus; +payments.getStarsTransactions#673ac2f9 flags:# inbound:flags.0?true outbound:flags.1?true peer:InputPeer offset:string = payments.StarsStatus; +payments.sendStarsForm#2bb731d flags:# form_id:long invoice:InputInvoice = payments.PaymentResult; +payments.refundStarsCharge#25ae8f4a user_id:InputUser charge_id:string = Updates; stickers.createStickerSet#9021ab67 flags:# masks:flags.0?true emojis:flags.5?true text_color:flags.6?true user_id:InputUser title:string short_name:string thumb:flags.2?InputDocument stickers:Vector software:flags.3?string = messages.StickerSet; stickers.removeStickerFromSet#f7760f51 sticker:InputDocument = messages.StickerSet; @@ -2432,4 +2470,4 @@ smsjobs.finishJob#4f1ebf24 flags:# job_id:string error:flags.0?string = Bool; fragment.getCollectibleInfo#be1e85ba collectible:InputCollectible = fragment.CollectibleInfo; -// LAYER 179 +// LAYER 181 diff --git a/Telegram/SourceFiles/overview/overview_layout.cpp b/Telegram/SourceFiles/overview/overview_layout.cpp index c56582bc4..d8c70f1df 100644 --- a/Telegram/SourceFiles/overview/overview_layout.cpp +++ b/Telegram/SourceFiles/overview/overview_layout.cpp @@ -1398,7 +1398,9 @@ void Document::drawCornerDownload(QPainter &p, bool selected, const PaintContext icon->paintInCenter(p, inner); if (_radial && _radial->animating()) { const auto rinner = inner.marginsRemoved(QMargins(st::historyAudioRadialLine, st::historyAudioRadialLine, st::historyAudioRadialLine, st::historyAudioRadialLine)); - auto fg = selected ? st::historyFileThumbRadialFgSelected : st::historyFileThumbRadialFg; + const auto &fg = selected + ? st::historyFileInIconFgSelected + : st::historyFileInIconFg; _radial->draw(p, rinner, st::historyAudioRadialLine, fg); } } diff --git a/Telegram/SourceFiles/passport/passport_form_controller.cpp b/Telegram/SourceFiles/passport/passport_form_controller.cpp index eafb62369..b7bc84207 100644 --- a/Telegram/SourceFiles/passport/passport_form_controller.cpp +++ b/Telegram/SourceFiles/passport/passport_form_controller.cpp @@ -2261,8 +2261,10 @@ void FormController::requestPhoneCall(not_null value) { value->verification.call->setStatus( { Ui::SentCodeCall::State::Calling, 0 }); _api.request(MTPauth_ResendCode( + MTP_flags(0), MTP_string(getPhoneFromValue(value)), - MTP_string(value->verification.phoneCodeHash) + MTP_string(value->verification.phoneCodeHash), + MTPstring() // reason )).done([=] { value->verification.call->callDone(); }).send(); diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.cpp b/Telegram/SourceFiles/payments/payments_checkout_process.cpp index e9fb12d26..cbb435389 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.cpp +++ b/Telegram/SourceFiles/payments/payments_checkout_process.cpp @@ -64,7 +64,9 @@ base::flat_map, SessionProcesses> Processes; void CheckoutProcess::Start( not_null item, Mode mode, - Fn reactivate) { + Fn reactivate, + Fn nonPanelPaymentFormProcess) { + const auto hasNonPanelPaymentFormProcess = !!nonPanelPaymentFormProcess; auto &processes = LookupSessionProcesses(&item->history()->session()); const auto media = item->media(); const auto invoice = media ? media->invoice() : nullptr; @@ -83,7 +85,11 @@ void CheckoutProcess::Start( const auto i = processes.byItem.find(id); if (i != end(processes.byItem)) { i->second->setReactivateCallback(std::move(reactivate)); - i->second->requestActivate(); + i->second->setNonPanelPaymentFormProcess( + std::move(nonPanelPaymentFormProcess)); + if (!hasNonPanelPaymentFormProcess) { + i->second->requestActivate(); + } return; } const auto j = processes.byItem.emplace( @@ -92,19 +98,28 @@ void CheckoutProcess::Start( InvoiceId{ InvoiceMessage{ item->history()->peer, id.msg } }, mode, std::move(reactivate), + std::move(nonPanelPaymentFormProcess), PrivateTag{})).first; - j->second->requestActivate(); + if (!hasNonPanelPaymentFormProcess) { + j->second->requestActivate(); + } } void CheckoutProcess::Start( not_null session, const QString &slug, - Fn reactivate) { + Fn reactivate, + Fn nonPanelPaymentFormProcess) { + const auto hasNonPanelPaymentFormProcess = !!nonPanelPaymentFormProcess; auto &processes = LookupSessionProcesses(session); const auto i = processes.bySlug.find(slug); if (i != end(processes.bySlug)) { i->second->setReactivateCallback(std::move(reactivate)); - i->second->requestActivate(); + i->second->setNonPanelPaymentFormProcess( + std::move(nonPanelPaymentFormProcess)); + if (!hasNonPanelPaymentFormProcess) { + i->second->requestActivate(); + } return; } const auto j = processes.bySlug.emplace( @@ -113,8 +128,11 @@ void CheckoutProcess::Start( InvoiceId{ InvoiceSlug{ session, slug } }, Mode::Payment, std::move(reactivate), + std::move(nonPanelPaymentFormProcess), PrivateTag{})).first; - j->second->requestActivate(); + if (!hasNonPanelPaymentFormProcess) { + j->second->requestActivate(); + } } void CheckoutProcess::Start( @@ -135,6 +153,30 @@ void CheckoutProcess::Start( std::move(id), Mode::Payment, std::move(reactivate), + nullptr, + PrivateTag{})).first; + j->second->requestActivate(); +} + +void CheckoutProcess::Start( + InvoiceCredits creditsInvoice, + Fn reactivate) { + const auto randomId = creditsInvoice.randomId; + auto id = InvoiceId{ std::move(creditsInvoice) }; + auto &processes = LookupSessionProcesses(SessionFromId(id)); + const auto i = processes.byRandomId.find(randomId); + if (i != end(processes.byRandomId)) { + i->second->setReactivateCallback(std::move(reactivate)); + i->second->requestActivate(); + return; + } + const auto j = processes.byRandomId.emplace( + randomId, + std::make_unique( + std::move(id), + Mode::Payment, + std::move(reactivate), + nullptr, PrivateTag{})).first; j->second->requestActivate(); } @@ -258,11 +300,13 @@ CheckoutProcess::CheckoutProcess( InvoiceId id, Mode mode, Fn reactivate, + Fn nonPanelPaymentFormProcess, PrivateTag) : _session(SessionFromId(id)) , _form(std::make_unique
(id, (mode == Mode::Receipt))) , _panel(std::make_unique(panelDelegate())) -, _reactivate(std::move(reactivate)) { +, _reactivate(std::move(reactivate)) +, _nonPanelPaymentFormProcess(std::move(nonPanelPaymentFormProcess)) { _form->updates( ) | rpl::start_with_next([=](const FormUpdate &update) { handleFormUpdate(update); @@ -277,7 +321,9 @@ CheckoutProcess::CheckoutProcess( ) | rpl::start_with_next([=] { panelCancelEdit(); }, _panel->lifetime()); - showForm(); + if (!_nonPanelPaymentFormProcess) { + showForm(); + } _panel->toggleProgress(true); if (mode == Mode::Payment) { @@ -296,6 +342,11 @@ void CheckoutProcess::setReactivateCallback( _reactivate = std::move(reactivate); } +void CheckoutProcess::setNonPanelPaymentFormProcess( + Fn callback) { + _nonPanelPaymentFormProcess = std::move(callback); +} + void CheckoutProcess::requestActivate() { _panel->requestActivate(); } @@ -359,6 +410,18 @@ void CheckoutProcess::handleFormUpdate(const FormUpdate &update) { if (weak) { closeAndReactivate(CheckoutResult::Paid); } + }, [&](const CreditsPaymentStarted &data) { + if (_nonPanelPaymentFormProcess) { + _nonPanelPaymentFormProcess( + std::make_shared(data.data)); + close(); + } + }, [&](const CreditsReceiptReady &data) { + if (_nonPanelPaymentFormProcess) { + _nonPanelPaymentFormProcess( + std::make_shared(data.data)); + close(); + } }, [&](const Error &error) { handleError(error); }); diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.h b/Telegram/SourceFiles/payments/payments_checkout_process.h index dd995324f..3c7f112ca 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.h +++ b/Telegram/SourceFiles/payments/payments_checkout_process.h @@ -37,8 +37,11 @@ namespace Payments { class Form; struct FormUpdate; struct Error; +struct InvoiceCredits; struct InvoiceId; struct InvoicePremiumGiftCode; +struct CreditsFormData; +struct CreditsReceiptData; enum class Mode { Payment, @@ -52,6 +55,12 @@ enum class CheckoutResult { Failed, }; +struct NonPanelPaymentForm : std::variant< + std::shared_ptr, + std::shared_ptr> { + using variant::variant; +}; + struct PaidInvoice { QString title; }; @@ -65,14 +74,19 @@ public: static void Start( not_null item, Mode mode, - Fn reactivate); + Fn reactivate, + Fn nonPanelPaymentFormProcess); static void Start( not_null session, const QString &slug, - Fn reactivate); + Fn reactivate, + Fn nonPanelPaymentFormProcess); static void Start( InvoicePremiumGiftCode giftCodeInvoice, Fn reactivate); + static void Start( + InvoiceCredits creditsInvoice, + Fn reactivate); [[nodiscard]] static std::optional InvoicePaid( not_null item); [[nodiscard]] static std::optional InvoicePaid( @@ -84,6 +98,7 @@ public: InvoiceId id, Mode mode, Fn reactivate, + Fn nonPanelPaymentFormProcess, PrivateTag); ~CheckoutProcess(); @@ -102,6 +117,7 @@ private: static void UnregisterPaymentStart(not_null process); void setReactivateCallback(Fn reactivate); + void setNonPanelPaymentFormProcess(Fn); void requestActivate(); void closeAndReactivate(CheckoutResult result); void close(); @@ -164,6 +180,7 @@ private: const std::unique_ptr _panel; QPointer _enterPasswordBox; Fn _reactivate; + Fn _nonPanelPaymentFormProcess; SubmitState _submitState = SubmitState::None; bool _initialSilentValidation = false; bool _sendFormPending = false; diff --git a/Telegram/SourceFiles/payments/payments_form.cpp b/Telegram/SourceFiles/payments/payments_form.cpp index d39b17737..8dac6d0c5 100644 --- a/Telegram/SourceFiles/payments/payments_form.cpp +++ b/Telegram/SourceFiles/payments/payments_form.cpp @@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "smartglocal/smartglocal_token.h" #include "storage/storage_account.h" #include "ui/image/image.h" +#include "ui/text/format_values.h" #include "ui/text/text_entity.h" #include "apiwrap.h" #include "core/core_cloud_password.h" @@ -117,6 +118,8 @@ not_null SessionFromId(const InvoiceId &id) { return &message->peer->session(); } else if (const auto slug = std::get_if(&id.value)) { return slug->session; + } else if (const auto slug = std::get_if(&id.value)) { + return slug->session; } const auto &giftCode = v::get(id.value); const auto users = std::get_if( @@ -314,6 +317,21 @@ MTPInputInvoice Form::inputInvoice() const { MTP_int(message->itemId.bare)); } else if (const auto slug = std::get_if(&_id.value)) { return MTP_inputInvoiceSlug(MTP_string(slug->slug)); + } else if (const auto credits = std::get_if(&_id.value)) { + using Flag = MTPDstarsTopupOption::Flag; + const auto emptyFlag = MTPDstarsTopupOption::Flags(0); + return MTP_inputInvoiceStars(MTP_starsTopupOption( + MTP_flags(emptyFlag + | (credits->product.isEmpty() + ? Flag::f_store_product + : emptyFlag) + | (credits->extended + ? Flag::f_extended + : emptyFlag)), + MTP_long(credits->credits), + MTP_string(credits->product), + MTP_string(credits->currency), + MTP_long(credits->amount))); } const auto &giftCode = v::get(_id.value); using Flag = MTPDpremiumGiftCodeOption::Flag; @@ -359,8 +377,41 @@ void Form::requestForm() { MTP_dataJSON(MTP_bytes(Window::Theme::WebViewParams().json)) )).done([=](const MTPpayments_PaymentForm &result) { hideProgress(); - result.match([&](const auto &data) { + result.match([&](const MTPDpayments_paymentForm &data) { processForm(data); + }, [&](const MTPDpayments_paymentFormStars &data) { + _session->data().processUsers(data.vusers()); + const auto currency = qs(data.vinvoice().data().vcurrency()); + const auto &tlPrices = data.vinvoice().data().vprices().v; + const auto amount = tlPrices.empty() + ? 0 + : tlPrices.front().data().vamount().v; + if (currency != ::Ui::kCreditsCurrency || !amount) { + using Type = Error::Type; + _updates.fire(Error{ Type::Form, u"Bad Stars Form."_q }); + return; + } + const auto invoice = InvoiceCredits{ + .session = _session, + .randomId = 0, + .credits = amount, + .currency = currency, + .amount = amount, + }; + const auto formData = CreditsFormData{ + .formId = data.vform_id().v, + .botId = data.vbot_id().v, + .title = qs(data.vtitle()), + .description = qs(data.vdescription()), + .photo = data.vphoto() + ? _session->data().photoFromWeb( + *data.vphoto(), + ImageLocation()) + : nullptr, + .invoice = invoice, + .inputInvoice = inputInvoice(), + }; + _updates.fire(CreditsPaymentStarted{ .data = formData }); }); }).fail([=](const MTP::Error &error) { hideProgress(); @@ -445,6 +496,25 @@ void Form::processReceipt(const MTPDpayments_paymentReceipt &data) { _updates.fire(FormReady{}); } +void Form::processReceipt(const MTPDpayments_paymentReceiptStars &data) { + _session->data().processUsers(data.vusers()); + + const auto receiptData = CreditsReceiptData{ + .id = qs(data.vtransaction_id()), + .title = qs(data.vtitle()), + .description = qs(data.vdescription()), + .photo = data.vphoto() + ? _session->data().photoFromWeb( + *data.vphoto(), + ImageLocation()) + : nullptr, + .peerId = peerFromUser(data.vbot_id().v), + .credits = data.vtotal_amount().v, + .date = data.vdate().v, + }; + _updates.fire(CreditsReceiptReady{ .data = receiptData }); +} + void Form::processInvoice(const MTPDinvoice &data) { const auto suggested = data.vsuggested_tip_amounts().value_or_empty(); _invoice = Ui::Invoice{ @@ -547,6 +617,37 @@ void Form::processDetails(const MTPDpayments_paymentReceipt &data) { } } +void Form::processDetails(const MTPDpayments_paymentReceiptStars &data) { + _invoice.receipt = Ui::Receipt{ + .date = data.vdate().v, + .totalAmount = ParsePriceAmount(data.vtotal_amount().v), + .currency = qs(data.vcurrency()), + .paid = true, + }; + _details = FormDetails{ + .botId = data.vbot_id().v, + }; + if (_invoice.cover.title.isEmpty() + && _invoice.cover.description.empty() + && _invoice.cover.thumbnail.isNull() + && !_thumbnailLoadProcess) { + _invoice.cover = Ui::Cover{ + .title = qs(data.vtitle()), + .description = { qs(data.vdescription()) }, + }; + if (const auto web = data.vphoto()) { + if (const auto photo = _session->data().photoFromWeb(*web, {})) { + loadThumbnail(photo); + } + } + } + if (_details.botId) { + if (const auto bot = _session->data().userLoaded(_details.botId)) { + _invoice.cover.seller = bot->name(); + } + } +} + void Form::processSavedInformation(const MTPDpaymentRequestedInfo &data) { const auto address = data.vshipping_address(); _savedInformation = _information = Ui::RequestedInformation{ diff --git a/Telegram/SourceFiles/payments/payments_form.h b/Telegram/SourceFiles/payments/payments_form.h index 414eb0b02..cf4cef890 100644 --- a/Telegram/SourceFiles/payments/payments_form.h +++ b/Telegram/SourceFiles/payments/payments_form.h @@ -120,63 +120,6 @@ struct PaymentMethod { Ui::PaymentMethodDetails ui; }; -struct ToggleProgress { - bool shown = true; -}; -struct FormReady {}; -struct ThumbnailUpdated { - QImage thumbnail; -}; -struct ValidateFinished {}; -struct PaymentMethodUpdate { - bool requestNewPassword = false; -}; -struct VerificationNeeded { - QString url; -}; -struct TmpPasswordRequired {}; -struct BotTrustRequired { - not_null bot; - not_null provider; -}; -struct PaymentFinished { - MTPUpdates updates; -}; -struct Error { - enum class Type { - None, - Form, - Validate, - Stripe, - SmartGlocal, - TmpPassword, - Send, - }; - Type type = Type::None; - QString id; - - [[nodiscard]] bool empty() const { - return (type == Type::None); - } - [[nodiscard]] explicit operator bool() const { - return !empty(); - } -}; - -struct FormUpdate : std::variant< - ToggleProgress, - FormReady, - ThumbnailUpdated, - ValidateFinished, - PaymentMethodUpdate, - VerificationNeeded, - TmpPasswordRequired, - BotTrustRequired, - PaymentFinished, - Error> { - using variant::variant; -}; - struct InvoiceMessage { not_null peer; MsgId itemId = 0; @@ -216,8 +159,107 @@ struct InvoicePremiumGiftCode { int months = 0; }; +struct InvoiceCredits { + not_null session; + uint64 randomId = 0; + uint64 credits = 0; + QString product; + QString currency; + uint64 amount = 0; + bool extended = false; +}; + struct InvoiceId { - std::variant value; + std::variant< + InvoiceMessage, + InvoiceSlug, + InvoicePremiumGiftCode, + InvoiceCredits> value; +}; + +struct CreditsFormData { + uint64 formId = 0; + uint64 botId = 0; + QString title; + QString description; + PhotoData *photo = nullptr; + InvoiceCredits invoice; + MTPInputInvoice inputInvoice; +}; + +struct CreditsReceiptData { + QString id; + QString title; + QString description; + PhotoData *photo = nullptr; + PeerId peerId = PeerId(0); + uint64 credits = 0; + TimeId date = 0; +}; + +struct ToggleProgress { + bool shown = true; +}; +struct FormReady {}; +struct ThumbnailUpdated { + QImage thumbnail; +}; +struct ValidateFinished {}; +struct PaymentMethodUpdate { + bool requestNewPassword = false; +}; +struct VerificationNeeded { + QString url; +}; +struct TmpPasswordRequired {}; +struct BotTrustRequired { + not_null bot; + not_null provider; +}; +struct PaymentFinished { + MTPUpdates updates; +}; +struct CreditsPaymentStarted { + CreditsFormData data; +}; +struct CreditsReceiptReady { + CreditsReceiptData data; +}; +struct Error { + enum class Type { + None, + Form, + Validate, + Stripe, + SmartGlocal, + TmpPassword, + Send, + }; + Type type = Type::None; + QString id; + + [[nodiscard]] bool empty() const { + return (type == Type::None); + } + [[nodiscard]] explicit operator bool() const { + return !empty(); + } +}; + +struct FormUpdate : std::variant< + ToggleProgress, + FormReady, + ThumbnailUpdated, + ValidateFinished, + PaymentMethodUpdate, + VerificationNeeded, + TmpPasswordRequired, + BotTrustRequired, + PaymentFinished, + CreditsPaymentStarted, + CreditsReceiptReady, + Error> { + using variant::variant; }; [[nodiscard]] not_null SessionFromId(const InvoiceId &id); @@ -287,9 +329,11 @@ private: void requestReceipt(); void processForm(const MTPDpayments_paymentForm &data); void processReceipt(const MTPDpayments_paymentReceipt &data); + void processReceipt(const MTPDpayments_paymentReceiptStars &data); void processInvoice(const MTPDinvoice &data); void processDetails(const MTPDpayments_paymentForm &data); void processDetails(const MTPDpayments_paymentReceipt &data); + void processDetails(const MTPDpayments_paymentReceiptStars &data); void processSavedInformation(const MTPDpaymentRequestedInfo &data); void processAdditionalPaymentMethods( const QVector &list); diff --git a/Telegram/SourceFiles/payments/payments_non_panel_process.cpp b/Telegram/SourceFiles/payments/payments_non_panel_process.cpp new file mode 100644 index 000000000..112cdd576 --- /dev/null +++ b/Telegram/SourceFiles/payments/payments_non_panel_process.cpp @@ -0,0 +1,131 @@ +/* +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 "payments/payments_non_panel_process.h" + +#include "api/api_credits.h" +#include "base/unixtime.h" +#include "boxes/send_credits_box.h" +#include "data/data_credits.h" +#include "data/data_photo.h" +#include "data/data_user.h" +#include "history/history_item.h" +#include "history/history_item_components.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "mainwidget.h" +#include "payments/payments_checkout_process.h" // NonPanelPaymentForm. +#include "payments/payments_form.h" +#include "settings/settings_credits_graphics.h" +#include "ui/boxes/boost_box.h" // Ui::StartFireworks. +#include "ui/layers/generic_box.h" +#include "ui/text/format_values.h" +#include "window/window_session_controller.h" + +namespace Payments { +namespace { + +bool IsCreditsInvoice(not_null item) { + if (const auto payment = item->Get()) { + return payment->isCreditsCurrency; + } + const auto media = item->media(); + const auto invoice = media ? media->invoice() : nullptr; + return invoice && (invoice->currency == Ui::kCreditsCurrency); +} + +} // namespace + +Fn ProcessNonPanelPaymentFormFactory( + not_null controller, + Fn maybeReturnToBot) { + return [=](NonPanelPaymentForm form) { + using CreditsFormDataPtr = std::shared_ptr; + using CreditsReceiptPtr = std::shared_ptr; + if (const auto creditsData = std::get_if(&form)) { + const auto form = *creditsData; + const auto lifetime = std::make_shared(); + const auto api = lifetime->make_state( + controller->session().user()); + const auto sendBox = [=, weak = base::make_weak(controller)] { + if (const auto strong = weak.get()) { + const auto unsuccessful = std::make_shared(true); + const auto box = controller->uiShow()->show(Box( + Ui::SendCreditsBox, + form, + crl::guard(strong, [=] { + *unsuccessful = false; + Ui::StartFireworks(strong->content()); + if (maybeReturnToBot) { + maybeReturnToBot(CheckoutResult::Paid); + } + }))); + box->boxClosing() | rpl::start_with_next([=] { + crl::on_main([=] { + if ((*unsuccessful) && maybeReturnToBot) { + maybeReturnToBot(CheckoutResult::Cancelled); + } + }); + }, box->lifetime()); + } + }; + const auto weak = base::make_weak(controller); + api->request({}, [=](Data::CreditsStatusSlice slice) { + if (const auto strong = weak.get()) { + strong->session().setCredits(slice.balance); + const auto creditsNeeded = int64(form->invoice.credits) + - int64(slice.balance); + if (creditsNeeded <= 0) { + sendBox(); + } else if (strong->session().premiumPossible()) { + strong->uiShow()->show(Box( + Settings::SmallBalanceBox, + strong, + creditsNeeded, + form->botId, + sendBox)); + } else { + strong->uiShow()->showToast( + tr::lng_credits_purchase_blocked(tr::now)); + if (maybeReturnToBot) { + maybeReturnToBot(CheckoutResult::Failed); + } + } + } + lifetime->destroy(); + }); + } + if (const auto r = std::get_if(&form)) { + const auto receipt = *r; + const auto entry = Data::CreditsHistoryEntry{ + .id = receipt->id, + .title = receipt->title, + .description = receipt->description, + .date = base::unixtime::parse(receipt->date), + .photoId = receipt->photo ? receipt->photo->id : 0, + .credits = receipt->credits, + .bareId = receipt->peerId.value, + .peerType = Data::CreditsHistoryEntry::PeerType::Peer, + }; + controller->uiShow()->show(Box( + Settings::ReceiptCreditsBox, + controller, + nullptr, + entry)); + } + }; +} + +Fn ProcessNonPanelPaymentFormFactory( + not_null controller, + not_null item) { + return IsCreditsInvoice(item) + ? ProcessNonPanelPaymentFormFactory(controller) + : nullptr; +} + +} // namespace Payments diff --git a/Telegram/SourceFiles/payments/payments_non_panel_process.h b/Telegram/SourceFiles/payments/payments_non_panel_process.h new file mode 100644 index 000000000..fb647a72a --- /dev/null +++ b/Telegram/SourceFiles/payments/payments_non_panel_process.h @@ -0,0 +1,29 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +class HistoryItem; + +namespace Window { +class SessionController; +} // namespace Window + +namespace Payments { + +enum class CheckoutResult; +struct NonPanelPaymentForm; + +Fn ProcessNonPanelPaymentFormFactory( + not_null controller, + Fn maybeReturnToBot = nullptr); + +Fn ProcessNonPanelPaymentFormFactory( + not_null controller, + not_null item); + +} // namespace Payments diff --git a/Telegram/SourceFiles/platform/linux/file_utilities_linux.cpp b/Telegram/SourceFiles/platform/linux/file_utilities_linux.cpp index d79e4778e..f314fb0af 100644 --- a/Telegram/SourceFiles/platform/linux/file_utilities_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/file_utilities_linux.cpp @@ -44,14 +44,12 @@ bool UnsafeShowOpenWith(const QString &filepath) { const auto fd = open( QFile::encodeName(filepath).constData(), - O_RDONLY); + O_RDONLY | O_CLOEXEC); if (fd == -1) { return false; } - const auto fdGuard = gsl::finally([&] { close(fd); }); - const auto handleToken = "tdesktop" + std::to_string(base::RandomValue()); @@ -73,6 +71,7 @@ bool UnsafeShowOpenWith(const QString &filepath) { nullptr)); if (!request) { + close(fd); return false; } @@ -107,7 +106,7 @@ bool UnsafeShowOpenWith(const QString &filepath) { GLib::Variant::new_variant( GLib::Variant::new_boolean(true))), }), - Gio::UnixFDList::new_from_array((std::array{ fd }).data(), 1), + Gio::UnixFDList::new_from_array(&fd, 1), nullptr); if (!result) { diff --git a/Telegram/SourceFiles/platform/linux/main_window_linux.cpp b/Telegram/SourceFiles/platform/linux/main_window_linux.cpp index 04cd9094b..440547151 100644 --- a/Telegram/SourceFiles/platform/linux/main_window_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/main_window_linux.cpp @@ -448,7 +448,7 @@ void MainWindow::updateGlobalMenuHook() { auto canSelectAll = false; const auto mimeData = QGuiApplication::clipboard()->mimeData(); const auto clipboardHasText = mimeData ? mimeData->hasText() : false; - auto markdownEnabled = false; + auto markdownState = Ui::MarkdownEnabledState(); if (const auto edit = qobject_cast(focused)) { canCut = canCopy = canDelete = edit->hasSelectedText(); canSelectAll = !edit->text().isEmpty(); @@ -464,7 +464,7 @@ void MainWindow::updateGlobalMenuHook() { if (canCopy) { if (const auto inputField = dynamic_cast( focused->parentWidget())) { - markdownEnabled = inputField->isMarkdownEnabled(); + markdownState = inputField->markdownEnabledState(); } } } else if (const auto list = dynamic_cast(focused)) { @@ -489,13 +489,19 @@ void MainWindow::updateGlobalMenuHook() { ForceDisabled(psNewGroup, inactive || support); ForceDisabled(psNewChannel, inactive || support); - ForceDisabled(psBold, !markdownEnabled); - ForceDisabled(psItalic, !markdownEnabled); - ForceDisabled(psUnderline, !markdownEnabled); - ForceDisabled(psStrikeOut, !markdownEnabled); - ForceDisabled(psBlockquote, !markdownEnabled); - ForceDisabled(psMonospace, !markdownEnabled); - ForceDisabled(psClearFormat, !markdownEnabled); + const auto diabled = [=](const QString &tag) { + return !markdownState.enabledForTag(tag); + }; + using Field = Ui::InputField; + ForceDisabled(psBold, diabled(Field::kTagBold)); + ForceDisabled(psItalic, diabled(Field::kTagItalic)); + ForceDisabled(psUnderline, diabled(Field::kTagUnderline)); + ForceDisabled(psStrikeOut, diabled(Field::kTagStrikeOut)); + ForceDisabled(psBlockquote, diabled(Field::kTagBlockquote)); + ForceDisabled( + psMonospace, + diabled(Field::kTagPre) || diabled(Field::kTagCode)); + ForceDisabled(psClearFormat, markdownState.disabled()); } bool MainWindow::eventFilter(QObject *obj, QEvent *evt) { diff --git a/Telegram/SourceFiles/platform/mac/main_window_mac.h b/Telegram/SourceFiles/platform/mac/main_window_mac.h index 6c2d15d07..63ed4009b 100644 --- a/Telegram/SourceFiles/platform/mac/main_window_mac.h +++ b/Telegram/SourceFiles/platform/mac/main_window_mac.h @@ -28,6 +28,10 @@ public: void updateWindowIcon() override; + rpl::producer globalForceClicks() override { + return _forceClicks.events(); + } + class Private; protected: @@ -60,8 +64,6 @@ private: base::Timer _hideAfterFullScreenTimer; - rpl::variable _canApplyMarkdown; - QMenuBar psMainMenu; QAction *psLogout = nullptr; QAction *psUndo = nullptr; @@ -85,7 +87,9 @@ private: QAction *psMonospace = nullptr; QAction *psClearFormat = nullptr; + rpl::event_stream _forceClicks; int _customTitleHeight = 0; + int _lastPressureStage = 0; }; diff --git a/Telegram/SourceFiles/platform/mac/main_window_mac.mm b/Telegram/SourceFiles/platform/mac/main_window_mac.mm index a6d9d0b0b..d0717426c 100644 --- a/Telegram/SourceFiles/platform/mac/main_window_mac.mm +++ b/Telegram/SourceFiles/platform/mac/main_window_mac.mm @@ -92,10 +92,11 @@ public: void setNativeWindow(NSWindow *window, NSView *view); void initTouchBar( NSWindow *window, - not_null controller, - rpl::producer canApplyMarkdown); + not_null controller); void setWindowBadge(const QString &str); + void setMarkdownEnabledState(Ui::MarkdownEnabledState state); + bool clipboardHasText(); ~Private(); @@ -103,6 +104,8 @@ private: not_null _public; friend class MainWindow; + rpl::variable _markdownState; + NSWindow * __weak _nativeWindow = nil; NSView * __weak _nativeView = nil; @@ -229,8 +232,7 @@ void MainWindow::Private::setNativeWindow(NSWindow *window, NSView *view) { void MainWindow::Private::initTouchBar( NSWindow *window, - not_null controller, - rpl::producer canApplyMarkdown) { + not_null controller) { if (!IsMac10_13OrGreater()) { return; } @@ -240,12 +242,17 @@ void MainWindow::Private::initTouchBar( [window performSelectorOnMainThread:@selector(setTouchBar:) withObject:[[[RootTouchBar alloc] - init:std::move(canApplyMarkdown) + init:_markdownState.value() controller:controller domain:(&Core::App().domain())] autorelease] waitUntilDone:true]; } +void MainWindow::Private::setMarkdownEnabledState( + Ui::MarkdownEnabledState state) { + _markdownState = state; +} + bool MainWindow::Private::clipboardHasText() { auto currentChangeCount = static_cast([_generalPasteboard changeCount]); if (_generalPasteboardChangeCount != currentChangeCount) { @@ -289,10 +296,7 @@ void MainWindow::initHook() { if (auto view = reinterpret_cast(winId())) { if (auto window = [view window]) { _private->setNativeWindow(window, view); - _private->initTouchBar( - window, - &controller(), - _canApplyMarkdown.changes()); + _private->initTouchBar(window, &controller()); } } } @@ -310,6 +314,16 @@ bool MainWindow::nativeEvent( Core::Sandbox::Instance().customEnterFromEventLoop([&] { imeCompositionStartReceived(); }); + } else if ([event type] == NSEventTypePressure) { + const auto stage = [event stage]; + if (_lastPressureStage != stage) { + _lastPressureStage = stage; + if (stage == 2) { + Core::Sandbox::Instance().customEnterFromEventLoop([&] { + _forceClicks.fire(QCursor::pos()); + }); + } + } } } return false; @@ -548,7 +562,7 @@ void MainWindow::updateGlobalMenuHook() { auto focused = QApplication::focusWidget(); bool canUndo = false, canRedo = false, canCut = false, canCopy = false, canPaste = false, canDelete = false, canSelectAll = false; auto clipboardHasText = _private->clipboardHasText(); - auto canApplyMarkdown = false; + auto markdownState = Ui::MarkdownEnabledState(); if (auto edit = qobject_cast(focused)) { canCut = canCopy = canDelete = edit->hasSelectedText(); canSelectAll = !edit->text().isEmpty(); @@ -564,7 +578,7 @@ void MainWindow::updateGlobalMenuHook() { if (canCopy) { if (const auto inputField = dynamic_cast( focused->parentWidget())) { - canApplyMarkdown = inputField->isMarkdownEnabled(); + markdownState = inputField->markdownEnabledState(); } } } else if (auto list = dynamic_cast(focused)) { @@ -572,7 +586,7 @@ void MainWindow::updateGlobalMenuHook() { canDelete = list->canDeleteSelected(); } - _canApplyMarkdown = canApplyMarkdown; + _private->setMarkdownEnabledState(markdownState); updateIsActive(); const auto logged = (sessionController() != nullptr); @@ -593,13 +607,19 @@ void MainWindow::updateGlobalMenuHook() { ForceDisabled(psNewChannel, inactive || support); ForceDisabled(psShowTelegram, isActive()); - ForceDisabled(psBold, !canApplyMarkdown); - ForceDisabled(psItalic, !canApplyMarkdown); - ForceDisabled(psUnderline, !canApplyMarkdown); - ForceDisabled(psStrikeOut, !canApplyMarkdown); - ForceDisabled(psBlockquote, !canApplyMarkdown); - ForceDisabled(psMonospace, !canApplyMarkdown); - ForceDisabled(psClearFormat, !canApplyMarkdown); + const auto diabled = [=](const QString &tag) { + return !markdownState.enabledForTag(tag); + }; + using Field = Ui::InputField; + ForceDisabled(psBold, diabled(Field::kTagBold)); + ForceDisabled(psItalic, diabled(Field::kTagItalic)); + ForceDisabled(psUnderline, diabled(Field::kTagUnderline)); + ForceDisabled(psStrikeOut, diabled(Field::kTagStrikeOut)); + ForceDisabled(psBlockquote, diabled(Field::kTagBlockquote)); + ForceDisabled( + psMonospace, + diabled(Field::kTagPre) || diabled(Field::kTagCode)); + ForceDisabled(psClearFormat, markdownState.disabled()); } bool MainWindow::eventFilter(QObject *obj, QEvent *evt) { diff --git a/Telegram/SourceFiles/platform/mac/touchbar/mac_touchbar_manager.h b/Telegram/SourceFiles/platform/mac/touchbar/mac_touchbar_manager.h index 464f87c9c..f34cf8190 100644 --- a/Telegram/SourceFiles/platform/mac/touchbar/mac_touchbar_manager.h +++ b/Telegram/SourceFiles/platform/mac/touchbar/mac_touchbar_manager.h @@ -17,9 +17,13 @@ namespace Window { class Controller; } // namespace Window +namespace Ui { +struct MarkdownEnabledState; +} // namespace Ui + API_AVAILABLE(macos(10.12.2)) @interface RootTouchBar : NSTouchBar -- (id)init:(rpl::producer)canApplyMarkdown +- (id)init:(rpl::producer)markdownState controller:(not_null)controller domain:(not_null)domain; @end diff --git a/Telegram/SourceFiles/platform/mac/touchbar/mac_touchbar_manager.mm b/Telegram/SourceFiles/platform/mac/touchbar/mac_touchbar_manager.mm index 5f314e4b5..ea273dab7 100644 --- a/Telegram/SourceFiles/platform/mac/touchbar/mac_touchbar_manager.mm +++ b/Telegram/SourceFiles/platform/mac/touchbar/mac_touchbar_manager.mm @@ -20,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "platform/mac/touchbar/mac_touchbar_audio.h" #include "platform/mac/touchbar/mac_touchbar_common.h" #include "platform/mac/touchbar/mac_touchbar_main.h" +#include "ui/widgets/fields/input_field.h" #include "window/window_controller.h" #include "window/window_session_controller.h" @@ -57,13 +58,12 @@ const auto kAudioItemIdentifier = @"touchbarAudio"; Main::Session *_session; Window::Controller *_controller; - bool _canApplyMarkdownLast; - rpl::event_stream _canApplyMarkdown; + rpl::variable _markdownState; rpl::event_stream<> _touchBarSwitches; rpl::lifetime _lifetime; } -- (id)init:(rpl::producer)canApplyMarkdown +- (id)init:(rpl::producer)markdownState controller:(not_null)controller domain:(not_null)domain { self = [super init]; @@ -75,10 +75,7 @@ const auto kAudioItemIdentifier = @"touchbarAudio"; self.defaultItemIdentifiers = @[]; }); _controller = controller; - _canApplyMarkdownLast = false; - std::move( - canApplyMarkdown - ) | rpl::start_to_stream(_canApplyMarkdown, _lifetime); + _markdownState = std::move(markdownState); auto sessionChanges = domain->activeSessionChanges( ) | rpl::map([=](Main::Session *session) { @@ -140,8 +137,7 @@ const auto kAudioItemIdentifier = @"touchbarAudio"; init:_controller touchBarSwitches:_touchBarSwitches.events()] autorelease]; rpl::combine( - _canApplyMarkdown.events_starting_with_copy( - _canApplyMarkdownLast), + _markdownState.value(), _controller->sessionController()->activeChatValue( ) | rpl::map([](Dialogs::Key k) { const auto topic = k.topic(); @@ -153,16 +149,15 @@ const auto kAudioItemIdentifier = @"touchbarAudio"; : (peer && Data::CanSendAnyOf(peer, rights)); }) | rpl::distinct_until_changed() ) | rpl::start_with_next([=]( - bool canApplyMarkdown, + Ui::MarkdownEnabledState state, bool hasActiveChat) { - _canApplyMarkdownLast = canApplyMarkdown; item.groupTouchBar.defaultItemIdentifiers = @[ kPinnedPanelItemIdentifier, - canApplyMarkdown + (!state.disabled() ? kPopoverInputItemIdentifier : hasActiveChat ? kPopoverPickerItemIdentifier - : @""]; + : @"")]; }, [item lifetime]); return [item autorelease]; diff --git a/Telegram/SourceFiles/settings/business/settings_chat_intro.cpp b/Telegram/SourceFiles/settings/business/settings_chat_intro.cpp index b8a7607f6..e9bc2f73e 100644 --- a/Telegram/SourceFiles/settings/business/settings_chat_intro.cpp +++ b/Telegram/SourceFiles/settings/business/settings_chat_intro.cpp @@ -21,7 +21,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/media/history_view_media_common.h" #include "history/view/media/history_view_sticker_player.h" #include "history/view/history_view_about_view.h" -#include "history/view/history_view_context_menu.h" #include "history/view/history_view_element.h" #include "history/history.h" #include "lang/lang_keys.h" @@ -164,11 +163,10 @@ private: current), st::settingsChatIntroFieldMargins); field->setMaxLength(limit); - AddLengthLimitLabel(field, limit); + Ui::AddLengthLimitLabel(field, limit); return field; } - rpl::producer> IconPlayerValue( not_null sticker, Fn update) { diff --git a/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp b/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp index 57e74cf25..941409f98 100644 --- a/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp +++ b/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp @@ -11,7 +11,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/application.h" #include "data/business/data_shortcut_messages.h" #include "data/data_session.h" -#include "history/view/history_view_context_menu.h" // AddLengthLimitLabel. #include "lang/lang_keys.h" #include "main/main_account.h" #include "main/main_session.h" @@ -214,7 +213,7 @@ void EditShortcutNameBox( field->selectAll(); field->setMaxLength(kShortcutLimit * 2); - HistoryView::AddLengthLimitLabel(field, kShortcutLimit); + Ui::AddLengthLimitLabel(field, kShortcutLimit); const auto callback = [=] { const auto name = field->getLastText().trimmed(); diff --git a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp index ca82da329..96c6cb841 100644 --- a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp +++ b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp @@ -72,7 +72,7 @@ using namespace HistoryView; class ShortcutMessages : public AbstractSection - , private ListDelegate + , private WindowListDelegate , private CornerButtonsDelegate { public: ShortcutMessages( @@ -164,6 +164,7 @@ private: History *listTranslateHistory() override; void listAddTranslatedItems( not_null tracker) override; + bool listIgnorePaintEvent(QWidget *w, QPaintEvent *e) override; // CornerButtonsDelegate delegate. void cornerButtonsShowAtPosition( @@ -239,9 +240,8 @@ private: not_null item, Api::SendOptions options, mtpRequestId *const saveEditMsgRequestId, - std::optional spoilerMediaOverride); + bool spoilered); void chooseAttach(std::optional overrideSendImagesAsPhotos); - [[nodiscard]] SendMenu::Type sendMenuType() const; [[nodiscard]] FullReplyTo replyTo() const; void doSetInnerFocus(); void showAtPosition( @@ -330,6 +330,7 @@ ShortcutMessages::ShortcutMessages( rpl::producer containerValue, BusinessShortcutId shortcutId) : AbstractSection(parent) +, WindowListDelegate(controller) , _controller(controller) , _session(&controller->session()) , _scroll(scroll) @@ -370,7 +371,7 @@ ShortcutMessages::ShortcutMessages( _inner = Ui::CreateChild( this, - controller, + &controller->session(), static_cast(this)); _inner->overrideIsChatWide(false); @@ -674,7 +675,7 @@ void ShortcutMessages::setupComposeControls() { ) | rpl::start_with_next([=](auto data) { if (const auto item = _session->data().message(data.fullId)) { if (item->isBusinessShortcut()) { - const auto spoiler = data.spoilerMediaOverride; + const auto spoiler = data.spoilered; edit(item, data.options, saveEditMsgRequestId, spoiler); } } @@ -790,7 +791,7 @@ QPointer ShortcutMessages::createPinnedToBottom( listShowPremiumToast(emoji); }, .mode = HistoryView::ComposeControlsMode::Normal, - .sendMenuType = SendMenu::Type::Disabled, + .sendMenuDetails = [] { return SendMenu::Details(); }, .regularWindow = _controller, .stickerOrEmojiChosen = _controller->stickerOrEmojiChosen(), .customPlaceholder = std::move(placeholder), @@ -1053,6 +1054,10 @@ void ShortcutMessages::listAddTranslatedItems( not_null tracker) { } +bool ShortcutMessages::listIgnorePaintEvent(QWidget *w, QPaintEvent *e) { + return false; +} + void ShortcutMessages::cornerButtonsShowAtPosition( Data::MessagePosition position) { showAtPosition(position); @@ -1216,7 +1221,7 @@ void ShortcutMessages::edit( not_null item, Api::SendOptions options, mtpRequestId *const saveEditMsgRequestId, - std::optional spoilerMediaOverride) { + bool spoilered) { if (*saveEditMsgRequestId) { return; } @@ -1285,7 +1290,7 @@ void ShortcutMessages::edit( options, crl::guard(this, done), crl::guard(this, fail), - spoilerMediaOverride); + spoilered); _composeControls->hidePanelsAnimated(); doSetInnerFocus(); @@ -1340,7 +1345,7 @@ bool ShortcutMessages::confirmSendingFiles( _composeControls->getTextWithAppliedMarkdown(), _history->peer, Api::SendType::Normal, - SendMenu::Type::Disabled); + SendMenu::Details()); box->setConfirmedCallback(crl::guard(this, [=]( Ui::PreparedList &&list, @@ -1537,12 +1542,6 @@ void ShortcutMessages::sendInlineResult( return; } sendInlineResult(result, bot, {}, std::nullopt); - //const auto callback = [=](Api::SendOptions options) { - // sendInlineResult(result, bot, options); - //}; - //Ui::show( - // PrepareScheduleBox(this, sendMenuType(), callback), - // Ui::LayerOption::KeepOther); } void ShortcutMessages::sendInlineResult( diff --git a/Telegram/SourceFiles/settings/settings_credits.cpp b/Telegram/SourceFiles/settings/settings_credits.cpp new file mode 100644 index 000000000..4cfeff843 --- /dev/null +++ b/Telegram/SourceFiles/settings/settings_credits.cpp @@ -0,0 +1,454 @@ +/* +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 "settings/settings_credits.h" + +#include "settings/settings_credits_graphics.h" +#include "api/api_credits.h" +#include "boxes/gift_premium_box.h" +#include "core/click_handler_types.h" +#include "data/data_file_origin.h" +#include "data/data_photo_media.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "info/settings/info_settings_widget.h" // SectionCustomTopBarData. +#include "info/statistics/info_statistics_list_controllers.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "settings/settings_common_session.h" +#include "statistics/widgets/chart_header_widget.h" +#include "ui/boxes/boost_box.h" // Ui::StartFireworks. +#include "ui/effects/premium_graphics.h" +#include "ui/effects/premium_top_bar.h" +#include "ui/layers/generic_box.h" +#include "ui/painter.h" +#include "ui/rect.h" +#include "ui/text/text_utilities.h" +#include "ui/vertical_list.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/discrete_sliders.h" +#include "ui/wrap/fade_wrap.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/wrap/vertical_layout.h" +#include "window/window_session_controller.h" +#include "styles/style_credits.h" +#include "styles/style_info.h" +#include "styles/style_layers.h" +#include "styles/style_premium.h" +#include "styles/style_settings.h" +#include "styles/style_statistics.h" + +namespace Settings { +namespace { + +class Credits : public Section { +public: + Credits( + QWidget *parent, + not_null controller); + + [[nodiscard]] rpl::producer title() override; + + [[nodiscard]] QPointer createPinnedToTop( + not_null parent) override; + + void showFinished() override; + + [[nodiscard]] bool hasFlexibleTopBar() const override; + + void setStepDataReference(std::any &data) override; + + [[nodiscard]] rpl::producer<> sectionShowBack() override final; + +private: + void setupContent(); + void setupOptions(not_null container); + void setupHistory(not_null container); + + const not_null _controller; + + QWidget *_parent = nullptr; + + QImage _star; + QImage _balanceStar; + + base::unique_qptr> _back; + base::unique_qptr _close; + rpl::variable _backToggles; + rpl::variable _wrap; + Fn _setPaused; + + rpl::event_stream<> _showBack; + rpl::event_stream<> _showFinished; + rpl::variable _buttonText; + +}; + +Credits::Credits( + QWidget *parent, + not_null controller) +: Section(parent) +, _controller(controller) +, _star(GenerateStars(st::creditsTopupButton.height, 1)) +, _balanceStar(GenerateStars(st::creditsBalanceStarHeight, 1)) { + setupContent(); +} + +rpl::producer Credits::title() { + return tr::lng_premium_summary_title(); +} + +bool Credits::hasFlexibleTopBar() const { + return true; +} + +rpl::producer<> Credits::sectionShowBack() { + return _showBack.events(); +} + +void Credits::setStepDataReference(std::any &data) { + using SectionCustomTopBarData = Info::Settings::SectionCustomTopBarData; + const auto my = std::any_cast(&data); + if (my) { + _backToggles = std::move( + my->backButtonEnables + ) | rpl::map_to(true); + _wrap = std::move(my->wrapValue); + } +} + +void Credits::setupHistory(not_null container) { + const auto history = container->add( + object_ptr>( + container, + object_ptr(container))); + const auto content = history->entity(); + + Ui::AddSkip(content, st::settingsPremiumOptionsPadding.top()); + + const auto fill = [=]( + not_null premiumBot, + const Data::CreditsStatusSlice &fullSlice, + const Data::CreditsStatusSlice &inSlice, + const Data::CreditsStatusSlice &outSlice) { + const auto inner = content; + if (fullSlice.list.empty()) { + return; + } + const auto hasOneTab = inSlice.list.empty() && outSlice.list.empty(); + const auto hasIn = !inSlice.list.empty(); + const auto hasOut = !outSlice.list.empty(); + const auto fullTabText = tr::lng_credits_summary_history_tab_full( + tr::now); + const auto inTabText = tr::lng_credits_summary_history_tab_in( + tr::now); + const auto outTabText = tr::lng_credits_summary_history_tab_out( + tr::now); + if (hasOneTab) { + Ui::AddSkip(inner); + const auto header = inner->add( + object_ptr(inner), + st::statisticsLayerMargins + + st::boostsChartHeaderPadding); + header->resizeToWidth(header->width()); + header->setTitle(fullTabText); + header->setSubTitle({}); + } + + class Slider final : public Ui::SettingsSlider { + public: + using Ui::SettingsSlider::SettingsSlider; + void setNaturalWidth(int w) { + _naturalWidth = w; + } + int naturalWidth() const override { + return _naturalWidth; + } + + private: + int _naturalWidth = 0; + + }; + + const auto slider = inner->add( + object_ptr>( + inner, + object_ptr(inner, st::defaultTabsSlider)), + st::boxRowPadding); + slider->toggle(!hasOneTab, anim::type::instant); + + slider->entity()->addSection(fullTabText); + if (hasIn) { + slider->entity()->addSection(inTabText); + } + if (hasOut) { + slider->entity()->addSection(outTabText); + } + + { + const auto &st = st::defaultTabsSlider; + slider->entity()->setNaturalWidth(0 + + st.labelStyle.font->width(fullTabText) + + (hasIn ? st.labelStyle.font->width(inTabText) : 0) + + (hasOut ? st.labelStyle.font->width(outTabText) : 0) + + rect::m::sum::h(st::boxRowPadding)); + } + + const auto fullWrap = inner->add( + object_ptr>( + inner, + object_ptr(inner))); + const auto inWrap = inner->add( + object_ptr>( + inner, + object_ptr(inner))); + const auto outWrap = inner->add( + object_ptr>( + inner, + object_ptr(inner))); + + rpl::single(0) | rpl::then( + slider->entity()->sectionActivated() + ) | rpl::start_with_next([=](int index) { + if (index == 0) { + fullWrap->toggle(true, anim::type::instant); + inWrap->toggle(false, anim::type::instant); + outWrap->toggle(false, anim::type::instant); + } else if (index == 1) { + inWrap->toggle(true, anim::type::instant); + fullWrap->toggle(false, anim::type::instant); + outWrap->toggle(false, anim::type::instant); + } else { + outWrap->toggle(true, anim::type::instant); + fullWrap->toggle(false, anim::type::instant); + inWrap->toggle(false, anim::type::instant); + } + }, inner->lifetime()); + + const auto controller = _controller->parentController(); + const auto entryClicked = [=](const Data::CreditsHistoryEntry &e) { + controller->uiShow()->show(Box( + ReceiptCreditsBox, + controller, + premiumBot.get(), + e)); + }; + + Info::Statistics::AddCreditsHistoryList( + controller->uiShow(), + fullSlice, + fullWrap->entity(), + entryClicked, + premiumBot, + &_star, + true, + true); + Info::Statistics::AddCreditsHistoryList( + controller->uiShow(), + inSlice, + inWrap->entity(), + entryClicked, + premiumBot, + &_star, + true, + false); + Info::Statistics::AddCreditsHistoryList( + controller->uiShow(), + outSlice, + outWrap->entity(), + std::move(entryClicked), + premiumBot, + &_star, + false, + true); + + Ui::AddSkip(inner); + Ui::AddSkip(inner); + + inner->resizeToWidth(container->width()); + }; + + const auto apiLifetime = content->lifetime().make_state(); + { + using Api = Api::CreditsHistory; + const auto self = _controller->session().user(); + const auto apiFull = apiLifetime->make_state(self, true, true); + const auto apiIn = apiLifetime->make_state(self, true, false); + const auto apiOut = apiLifetime->make_state(self, false, true); + apiFull->request({}, [=](Data::CreditsStatusSlice fullSlice) { + apiIn->request({}, [=](Data::CreditsStatusSlice inSlice) { + apiOut->request({}, [=](Data::CreditsStatusSlice outSlice) { + ::Api::PremiumPeerBot( + &_controller->session() + ) | rpl::start_with_next([=](not_null bot) { + fill(bot, fullSlice, inSlice, outSlice); + apiLifetime->destroy(); + }, *apiLifetime); + }); + }); + }); + } +} + +void Credits::setupContent() { + const auto content = Ui::CreateChild(this); + const auto paid = [=] { + if (_parent) { + Ui::StartFireworks(_parent); + } + }; + FillCreditOptions(_controller, content, 0, paid); + setupHistory(content); + + Ui::ResizeFitChild(this, content); +} + +QPointer Credits::createPinnedToTop( + not_null parent) { + _parent = parent; + + const auto content = [&]() -> Ui::Premium::TopBarAbstract* { + const auto weak = base::make_weak(_controller); + const auto clickContextOther = [=] { + return QVariant::fromValue(ClickHandlerContext{ + .sessionWindow = weak, + .botStartAutoSubmit = true, + }); + }; + return Ui::CreateChild( + parent.get(), + st::creditsPremiumCover, + Ui::Premium::TopBarDescriptor{ + .clickContextOther = clickContextOther, + .title = tr::lng_credits_summary_title(), + .about = tr::lng_credits_summary_about( + TextWithEntities::Simple), + .light = true, + .gradientStops = Ui::Premium::CreditsIconGradientStops(), + }); + }(); + _setPaused = [=](bool paused) { + content->setPaused(paused); + }; + + _wrap.value( + ) | rpl::start_with_next([=](Info::Wrap wrap) { + content->setRoundEdges(wrap == Info::Wrap::Layer); + }, content->lifetime()); + + content->setMaximumHeight(st::settingsPremiumTopHeight); + content->setMinimumHeight(st::infoLayerTopBarHeight); + + content->resize(content->width(), content->maximumHeight()); + content->additionalHeight( + ) | rpl::start_with_next([=](int additionalHeight) { + const auto wasMax = (content->height() == content->maximumHeight()); + content->setMaximumHeight(st::settingsPremiumTopHeight + + additionalHeight); + if (wasMax) { + content->resize(content->width(), content->maximumHeight()); + } + }, content->lifetime()); + + { + const auto balance = AddBalanceWidget( + content, + _controller->session().creditsValue(), + true); + const auto api = balance->lifetime().make_state( + _controller->session().user()); + api->request({}, [=](Data::CreditsStatusSlice slice) { + _controller->session().setCredits(slice.balance); + }); + rpl::combine( + balance->sizeValue(), + content->sizeValue() + ) | rpl::start_with_next([=](const QSize &, const QSize &) { + balance->moveToRight( + (_close + ? _close->width() + st::creditsHistoryRightSkip + : st::creditsHistoryRightSkip * 2), + st::creditsHistoryRightSkip); + balance->update(); + }, balance->lifetime()); + } + + _wrap.value( + ) | rpl::start_with_next([=](Info::Wrap wrap) { + const auto isLayer = (wrap == Info::Wrap::Layer); + _back = base::make_unique_q>( + content, + object_ptr( + content, + (isLayer ? st::infoTopBarBack : st::infoLayerTopBarBack)), + st::infoTopBarScale); + _back->setDuration(0); + _back->toggleOn(isLayer + ? _backToggles.value() | rpl::type_erased() + : rpl::single(true)); + _back->entity()->addClickHandler([=] { + _showBack.fire({}); + }); + _back->toggledValue( + ) | rpl::start_with_next([=](bool toggled) { + const auto &st = isLayer ? st::infoLayerTopBar : st::infoTopBar; + content->setTextPosition( + toggled ? st.back.width : st.titlePosition.x(), + st.titlePosition.y()); + }, _back->lifetime()); + + if (!isLayer) { + _close = nullptr; + } else { + _close = base::make_unique_q( + content, + st::infoTopBarClose); + _close->addClickHandler([=] { + _controller->parentController()->hideLayer(); + _controller->parentController()->hideSpecialLayer(); + }); + content->widthValue( + ) | rpl::start_with_next([=] { + _close->moveToRight(0, 0); + }, _close->lifetime()); + } + }, content->lifetime()); + + return Ui::MakeWeak(not_null{ content }); +} + +void Credits::showFinished() { + _showFinished.fire({}); +} + +} // namespace + +template <> +struct SectionFactory : AbstractSectionFactory { + object_ptr create( + not_null parent, + not_null controller, + not_null scroll, + rpl::producer containerValue + ) const final override { + return object_ptr(parent, controller); + } + bool hasCustomTopBar() const final override { + return true; + } + + [[nodiscard]] static const std::shared_ptr &Instance() { + static const auto result = std::make_shared(); + return result; + } +}; + +Type CreditsId() { + return Credits::Id(); +} + +} // namespace Settings diff --git a/Telegram/SourceFiles/ui/arc_angles.h b/Telegram/SourceFiles/settings/settings_credits.h similarity index 59% rename from Telegram/SourceFiles/ui/arc_angles.h rename to Telegram/SourceFiles/settings/settings_credits.h index cfaa59b94..c0e32e1be 100644 --- a/Telegram/SourceFiles/ui/arc_angles.h +++ b/Telegram/SourceFiles/settings/settings_credits.h @@ -7,10 +7,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once -namespace arc { +#include "settings/settings_type.h" -constexpr auto kFullLength = 360 * 16; -constexpr auto kQuarterLength = (kFullLength / 4); -constexpr auto kHalfLength = (kFullLength / 2); +namespace Settings { + +[[nodiscard]] Type CreditsId(); + +} // namespace Settings -} // namespace arc diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp new file mode 100644 index 000000000..b855121a4 --- /dev/null +++ b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp @@ -0,0 +1,666 @@ +/* +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 "settings/settings_credits_graphics.h" + +#include "api/api_credits.h" +#include "boxes/gift_premium_box.h" +#include "core/click_handler_types.h" +#include "data/data_file_origin.h" +#include "data/data_photo_media.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "info/settings/info_settings_widget.h" // SectionCustomTopBarData. +#include "info/statistics/info_statistics_list_controllers.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "payments/payments_checkout_process.h" +#include "payments/payments_form.h" +#include "settings/settings_common_session.h" +#include "settings/settings_credits_graphics.h" +#include "statistics/widgets/chart_header_widget.h" +#include "ui/controls/userpic_button.h" +#include "ui/effects/credits_graphics.h" +#include "ui/effects/premium_graphics.h" +#include "ui/effects/premium_top_bar.h" +#include "ui/image/image_prepare.h" +#include "ui/layers/generic_box.h" +#include "ui/painter.h" +#include "ui/rect.h" +#include "ui/text/format_values.h" +#include "ui/text/text_utilities.h" +#include "ui/vertical_list.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/discrete_sliders.h" +#include "ui/widgets/labels.h" +#include "ui/widgets/tooltip.h" +#include "ui/wrap/fade_wrap.h" +#include "ui/wrap/padding_wrap.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/wrap/vertical_layout.h" +#include "window/window_session_controller.h" +#include "styles/style_credits.h" +#include "styles/style_giveaway.h" +#include "styles/style_info.h" +#include "styles/style_layers.h" +#include "styles/style_premium.h" +#include "styles/style_settings.h" +#include "styles/style_statistics.h" + +#include // XXH64. + +#include + +namespace Settings { +namespace { + +[[nodiscard]] uint64 UniqueIdFromOption( + const Data::CreditTopupOption &d) { + const auto string = QString::number(d.credits) + + d.product + + d.currency + + QString::number(d.amount); + + return XXH64(string.data(), string.size() * sizeof(ushort), 0); +} + +class Balance final + : public Ui::RpWidget + , public Ui::AbstractTooltipShower { +public: + using Ui::RpWidget::RpWidget; + + void setBalance(uint64 balance) { + _balance = balance; + _tooltip = Lang::FormatCountDecimal(balance); + } + + void enterEventHook(QEnterEvent *e) override { + if (_balance >= 10'000) { + Ui::Tooltip::Show(1000, this); + } + } + + void leaveEventHook(QEvent *e) override { + Ui::Tooltip::Hide(); + } + + QString tooltipText() const override { + return _tooltip; + } + + QPoint tooltipPos() const override { + return QCursor::pos(); + } + + bool tooltipWindowActive() const override { + return Ui::AppInFocus() && Ui::InFocusChain(window()); + } + +private: + QString _tooltip; + uint64 _balance = 0; + +}; + +} // namespace + +QImage GenerateStars(int height, int count) { + constexpr auto kOutlineWidth = .6; + constexpr auto kStrokeWidth = 3; + constexpr auto kShift = 3; + + auto colorized = qs(Ui::Premium::ColorizedSvg( + Ui::Premium::CreditsIconGradientStops())); + colorized.replace( + u"stroke=\"none\""_q, + u"stroke=\"%1\""_q.arg(st::creditsStroke->c.name())); + colorized.replace( + u"stroke-width=\"1\""_q, + u"stroke-width=\"%1\""_q.arg(kStrokeWidth)); + auto svg = QSvgRenderer(colorized.toUtf8()); + svg.setViewBox(svg.viewBox() + Margins(kStrokeWidth)); + + const auto starSize = Size(height - kOutlineWidth * 2); + + auto frame = QImage( + QSize( + (height + kShift * (count - 1)) * style::DevicePixelRatio(), + height * style::DevicePixelRatio()), + QImage::Format_ARGB32_Premultiplied); + frame.setDevicePixelRatio(style::DevicePixelRatio()); + frame.fill(Qt::transparent); + const auto drawSingle = [&](QPainter &q) { + const auto s = kOutlineWidth; + q.save(); + q.translate(s, s); + q.setCompositionMode(QPainter::CompositionMode_Clear); + svg.render(&q, QRectF(QPointF(s, 0), starSize)); + svg.render(&q, QRectF(QPointF(s, s), starSize)); + svg.render(&q, QRectF(QPointF(0, s), starSize)); + svg.render(&q, QRectF(QPointF(-s, s), starSize)); + svg.render(&q, QRectF(QPointF(-s, 0), starSize)); + svg.render(&q, QRectF(QPointF(-s, -s), starSize)); + svg.render(&q, QRectF(QPointF(0, -s), starSize)); + svg.render(&q, QRectF(QPointF(s, -s), starSize)); + q.setCompositionMode(QPainter::CompositionMode_SourceOver); + svg.render(&q, Rect(starSize)); + q.restore(); + }; + { + auto q = QPainter(&frame); + q.translate(frame.width() / style::DevicePixelRatio() - height, 0); + for (auto i = count; i > 0; --i) { + drawSingle(q); + q.translate(-kShift, 0); + } + } + return frame; +} + +void FillCreditOptions( + not_null controller, + not_null container, + int minCredits, + Fn paid) { + const auto options = container->add( + object_ptr>( + container, + object_ptr(container))); + const auto content = options->entity(); + + Ui::AddSkip(content, st::settingsPremiumOptionsPadding.top()); + + const auto singleStarWidth = GenerateStars( + st::creditsTopupButton.height, + 1).width() / style::DevicePixelRatio(); + + const auto fill = [=](Data::CreditTopupOptions options) { + while (content->count()) { + delete content->widgetAt(0); + } + Ui::AddSubsectionTitle( + content, + tr::lng_credits_summary_options_subtitle()); + const auto &st = st::creditsTopupButton; + const auto diffBetweenTextAndStar = st.padding.left() + - st.iconLeft + - singleStarWidth; + const auto buttonHeight = st.height + rect::m::sum::v(st.padding); + for (auto i = 0; i < options.size(); i++) { + const auto &option = options[i]; + if (option.credits < minCredits) { + continue; + } + const auto button = content->add(object_ptr( + content, + rpl::never(), + st)); + const auto text = button->lifetime().make_state( + st.style, + tr::lng_credits_summary_options_credits( + tr::now, + lt_count_decimal, + option.credits)); + const auto price = Ui::CreateChild( + button, + Ui::FillAmountAndCurrency(option.amount, option.currency), + st::creditsTopupPrice); + const auto inner = Ui::CreateChild(button); + const auto stars = GenerateStars(st.height, (i + 1)); + inner->paintRequest( + ) | rpl::start_with_next([=](const QRect &rect) { + auto p = QPainter(inner); + p.drawImage( + 0, + (buttonHeight - stars.height()) / 2, + stars); + const auto textLeft = diffBetweenTextAndStar + + stars.width() / style::DevicePixelRatio(); + p.setPen(st.textFg); + text->draw(p, { + .position = QPoint(textLeft, 0), + .availableWidth = inner->width() - textLeft, + }); + }, inner->lifetime()); + button->sizeValue( + ) | rpl::start_with_next([=](const QSize &size) { + price->moveToRight(st.padding.right(), st.padding.top()); + inner->moveToLeft(st.iconLeft, st.padding.top()); + inner->resize( + size.width() + - rect::m::sum::h(st.padding) + - price->width(), + buttonHeight); + }, button->lifetime()); + button->setClickedCallback([=] { + const auto invoice = Payments::InvoiceCredits{ + .session = &controller->session(), + .randomId = UniqueIdFromOption(option), + .credits = option.credits, + .product = option.product, + .currency = option.currency, + .amount = option.amount, + .extended = option.extended, + }; + + const auto weak = Ui::MakeWeak(button); + const auto done = [=](Payments::CheckoutResult result) { + if (const auto strong = weak.data()) { + strong->window()->setFocus(); + if (result == Payments::CheckoutResult::Paid) { + if (paid) { + paid(); + } + } + } + }; + + Payments::CheckoutProcess::Start(std::move(invoice), done); + }); + Ui::ToggleChildrenVisibility(button, true); + } + + // Footer. + { + auto text = tr::lng_credits_summary_options_about( + lt_link, + tr::lng_credits_summary_options_about_link( + ) | rpl::map([](const QString &t) { + using namespace Ui::Text; + return Link(t, u"https://telegram.org/tos"_q); + }), + Ui::Text::RichLangValue); + Ui::AddSkip(content); + Ui::AddDividerText(content, std::move(text)); + } + + content->resizeToWidth(container->width()); + }; + + using ApiOptions = Api::CreditsTopupOptions; + const auto apiCredits = content->lifetime().make_state( + controller->session().user()); + + if (controller->session().premiumPossible()) { + apiCredits->request( + ) | rpl::start_with_error_done([=](const QString &error) { + controller->showToast(error); + }, [=] { + fill(apiCredits->options()); + }, content->lifetime()); + } + + controller->session().premiumPossibleValue( + ) | rpl::start_with_next([=](bool premiumPossible) { + if (!premiumPossible) { + fill({}); + } + }, content->lifetime()); +} + +not_null AddBalanceWidget( + not_null parent, + rpl::producer balanceValue, + bool rightAlign) { + const auto balance = Ui::CreateChild(parent); + const auto balanceStar = balance->lifetime().make_state( + GenerateStars(st::creditsBalanceStarHeight, 1)); + const auto starSize = balanceStar->size() / style::DevicePixelRatio(); + const auto label = balance->lifetime().make_state( + st::defaultTextStyle, + tr::lng_credits_summary_balance(tr::now)); + const auto count = balance->lifetime().make_state( + st::semiboldTextStyle, + tr::lng_contacts_loading(tr::now)); + const auto diffBetweenStarAndCount = count->style()->font->spacew; + const auto resize = [=] { + balance->resize( + std::max( + label->maxWidth(), + count->maxWidth() + + starSize.width() + + diffBetweenStarAndCount), + label->style()->font->height + starSize.height()); + }; + std::move(balanceValue) | rpl::start_with_next([=](uint64 value) { + count->setText( + st::semiboldTextStyle, + Lang::FormatCountToShort(value).string); + balance->setBalance(value); + resize(); + }, balance->lifetime()); + balance->paintRequest( + ) | rpl::start_with_next([=] { + auto p = QPainter(balance); + + p.setPen(st::boxTextFg); + + label->draw(p, { + .position = QPoint( + rightAlign ? (balance->width() - label->maxWidth()) : 0, + 0), + .availableWidth = balance->width(), + }); + count->draw(p, { + .position = QPoint( + balance->width() - count->maxWidth(), + label->minHeight() + + (starSize.height() - count->minHeight()) / 2), + .availableWidth = balance->width(), + }); + p.drawImage( + balance->width() + - count->maxWidth() + - starSize.width() + - diffBetweenStarAndCount, + label->minHeight(), + *balanceStar); + }, balance->lifetime()); + return balance; +} + +void ReceiptCreditsBox( + not_null box, + not_null controller, + PeerData *premiumBot, + const Data::CreditsHistoryEntry &e) { + box->setStyle(st::giveawayGiftCodeBox); + box->setNoContentMargin(true); + + const auto star = GenerateStars(st::creditsTopupButton.height, 1); + + const auto content = box->verticalLayout(); + Ui::AddSkip(content); + Ui::AddSkip(content); + Ui::AddSkip(content); + + using Type = Data::CreditsHistoryEntry::PeerType; + + const auto &stUser = st::boostReplaceUserpic; + const auto peer = (e.peerType == Type::PremiumBot) + ? premiumBot + : e.bareId + ? controller->session().data().peer(PeerId(e.bareId)).get() + : nullptr; + const auto photo = e.photoId + ? controller->session().data().photo(e.photoId).get() + : nullptr; + if (photo) { + content->add(object_ptr>( + content, + HistoryEntryPhoto(content, photo, stUser.photoSize))); + } else if (peer) { + content->add(object_ptr>( + content, + object_ptr(content, peer, stUser))); + } else { + const auto widget = content->add( + object_ptr>( + content, + object_ptr(content)))->entity(); + using Draw = Fn; + const auto draw = widget->lifetime().make_state( + Ui::GenerateCreditsPaintUserpicCallback(e)); + widget->resize(Size(stUser.photoSize)); + widget->paintRequest( + ) | rpl::start_with_next([=] { + auto p = Painter(widget); + (*draw)(p, 0, 0, stUser.photoSize, stUser.photoSize); + }, widget->lifetime()); + } + + Ui::AddSkip(content); + Ui::AddSkip(content); + + + box->addRow(object_ptr>( + box, + object_ptr( + box, + rpl::single( + !e.title.isEmpty() + ? e.title + : peer + ? peer->name() + : Ui::GenerateEntryName(e).text), + st::creditsBoxAboutTitle))); + + Ui::AddSkip(content); + + { + constexpr auto kMinus = QChar(0x2212); + auto &lifetime = content->lifetime(); + const auto text = lifetime.make_state( + st::semiboldTextStyle, + ((!e.bareId || e.refunded) ? QChar('+') : kMinus) + + Lang::FormatCountDecimal(std::abs(int64(e.credits)))); + const auto refundedText = tr::lng_channel_earn_history_return( + tr::now); + const auto refunded = e.refunded + ? lifetime.make_state( + st::defaultTextStyle, + refundedText) + : (Ui::Text::String*)(nullptr); + + const auto amount = content->add( + object_ptr( + content, + star.height() / style::DevicePixelRatio())); + const auto font = text->style()->font; + const auto refundedFont = st::defaultTextStyle.font; + const auto starWidth = star.width() + / style::DevicePixelRatio(); + const auto refundedSkip = refundedFont->spacew * 2; + const auto refundedWidth = refunded + ? refundedFont->width(refundedText) + + refundedSkip + + refundedFont->height + : 0; + const auto fullWidth = text->maxWidth() + + font->spacew * 1 + + starWidth + + refundedWidth; + amount->paintRequest( + ) | rpl::start_with_next([=] { + auto p = Painter(amount); + p.setPen((!e.bareId || e.refunded) + ? st::boxTextFgGood + : st::menuIconAttentionColor); + const auto x = (amount->width() - fullWidth) / 2; + text->draw(p, Ui::Text::PaintContext{ + .position = QPoint( + x, + (amount->height() - font->height) / 2), + .outerWidth = amount->width(), + .availableWidth = amount->width(), + }); + p.drawImage( + x + fullWidth - starWidth - refundedWidth, + 0, + star); + + if (refunded) { + const auto refundedLeft = fullWidth + + x + - refundedWidth + + refundedSkip; + const auto pen = p.pen(); + auto color = pen.color(); + color.setAlphaF(color.alphaF() * 0.15); + p.setPen(Qt::NoPen); + p.setBrush(color); + { + auto hq = PainterHighQualityEnabler(p); + p.drawRoundedRect( + refundedLeft, + (amount->height() - refundedFont->height) / 2, + refundedWidth - refundedSkip, + refundedFont->height, + refundedFont->height / 2, + refundedFont->height / 2); + } + p.setPen(pen); + refunded->draw(p, Ui::Text::PaintContext{ + .position = QPoint( + refundedLeft + refundedFont->height / 2, + (amount->height() - refundedFont->height) / 2), + .outerWidth = refundedWidth, + .availableWidth = refundedWidth, + }); + } + }, amount->lifetime()); + } + + if (!e.description.isEmpty()) { + Ui::AddSkip(content); + box->addRow(object_ptr>( + box, + object_ptr( + box, + rpl::single(e.description), + st::defaultFlatLabel))); + } + + Ui::AddSkip(content); + Ui::AddSkip(content); + + AddCreditsHistoryEntryTable( + controller, + box->verticalLayout(), + e); + + Ui::AddSkip(content); + + box->addRow(object_ptr>( + box, + object_ptr( + box, + tr::lng_credits_box_out_about( + lt_link, + tr::lng_payments_terms_link( + ) | rpl::map([](const QString &t) { + using namespace Ui::Text; + return Link(t, u"https://telegram.org/tos"_q); + }), + Ui::Text::WithEntities), + st::creditsBoxAboutDivider))); + + Ui::AddSkip(content); + + const auto button = box->addButton(tr::lng_box_ok(), [=] { + box->closeBox(); + }); + const auto buttonWidth = st::boxWidth + - rect::m::sum::h(st::giveawayGiftCodeBox.buttonPadding); + button->widthValue() | rpl::filter([=] { + return (button->widthNoMargins() != buttonWidth); + }) | rpl::start_with_next([=] { + button->resizeToWidth(buttonWidth); + }, button->lifetime()); +} + +object_ptr HistoryEntryPhoto( + not_null parent, + not_null photo, + int photoSize) { + auto owned = object_ptr(parent); + const auto widget = owned.data(); + widget->resize(Size(photoSize)); + + const auto draw = Ui::GenerateCreditsPaintEntryCallback( + photo, + [=] { widget->update(); }); + + widget->paintRequest( + ) | rpl::start_with_next([=] { + auto p = Painter(widget); + draw(p, 0, 0, photoSize, photoSize); + }, widget->lifetime()); + + return owned; +} + +void SmallBalanceBox( + not_null box, + not_null controller, + int creditsNeeded, + UserId botId, + Fn paid) { + box->setWidth(st::boxWideWidth); + box->addButton(tr::lng_close(), [=] { box->closeBox(); }); + const auto done = [=] { + box->closeBox(); + paid(); + }; + + const auto bot = controller->session().data().user(botId).get(); + + const auto content = [&]() -> Ui::Premium::TopBarAbstract* { + const auto weak = base::make_weak(controller); + const auto clickContextOther = [=] { + return QVariant::fromValue(ClickHandlerContext{ + .sessionWindow = weak, + .botStartAutoSubmit = true, + }); + }; + return box->setPinnedToTopContent(object_ptr( + box, + st::creditsLowBalancePremiumCover, + Ui::Premium::TopBarDescriptor{ + .clickContextOther = clickContextOther, + .title = tr::lng_credits_small_balance_title( + lt_count, + rpl::single(creditsNeeded) | tr::to_count()), + .about = tr::lng_credits_small_balance_about( + lt_bot, + rpl::single(TextWithEntities{ bot->name() }), + Ui::Text::RichLangValue), + .light = true, + .gradientStops = Ui::Premium::CreditsIconGradientStops(), + })); + }(); + + FillCreditOptions(controller, box->verticalLayout(), creditsNeeded, done); + + content->setMaximumHeight(st::creditsLowBalancePremiumCoverHeight); + content->setMinimumHeight(st::infoLayerTopBarHeight); + + content->resize(content->width(), content->maximumHeight()); + content->additionalHeight( + ) | rpl::start_with_next([=](int additionalHeight) { + const auto wasMax = (content->height() == content->maximumHeight()); + content->setMaximumHeight(st::creditsLowBalancePremiumCoverHeight + + additionalHeight); + if (wasMax) { + content->resize(content->width(), content->maximumHeight()); + } + }, content->lifetime()); + + { + const auto balance = AddBalanceWidget( + content, + controller->session().creditsValue(), + true); + const auto api = balance->lifetime().make_state( + controller->session().user()); + api->request({}, [=](Data::CreditsStatusSlice slice) { + controller->session().setCredits(slice.balance); + }); + rpl::combine( + balance->sizeValue(), + content->sizeValue() + ) | rpl::start_with_next([=](const QSize &, const QSize &) { + balance->moveToRight( + st::creditsHistoryRightSkip * 2, + st::creditsHistoryRightSkip); + balance->update(); + }, balance->lifetime()); + } +} + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.h b/Telegram/SourceFiles/settings/settings_credits_graphics.h new file mode 100644 index 000000000..cb6c27e7e --- /dev/null +++ b/Telegram/SourceFiles/settings/settings_credits_graphics.h @@ -0,0 +1,63 @@ +/* +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 + +template +class object_ptr; + +class PeerData; + +namespace Data { +struct CreditsHistoryEntry; +} // namespace Data + +namespace Window { +class SessionController; +} // namespace Window + +namespace Ui { +class GenericBox; +class RpWidget; +class VerticalLayout; +} // namespace Ui + +namespace Settings { + +[[nodiscard]] QImage GenerateStars(int height, int count); + +void FillCreditOptions( + not_null controller, + not_null container, + int minCredits, + Fn paid); + +[[nodiscard]] not_null AddBalanceWidget( + not_null parent, + rpl::producer balanceValue, + bool rightAlign); + +void ReceiptCreditsBox( + not_null box, + not_null controller, + PeerData *premiumBot, + const Data::CreditsHistoryEntry &e); + +[[nodiscard]] object_ptr HistoryEntryPhoto( + not_null parent, + not_null photo, + int photoSize); + +void SmallBalanceBox( + not_null box, + not_null controller, + int creditsNeeded, + UserId botId, + Fn paid); + +} // namespace Settings + diff --git a/Telegram/SourceFiles/settings/settings_main.cpp b/Telegram/SourceFiles/settings/settings_main.cpp index 879f2725f..690c3b913 100644 --- a/Telegram/SourceFiles/settings/settings_main.cpp +++ b/Telegram/SourceFiles/settings/settings_main.cpp @@ -7,19 +7,21 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "settings/settings_main.h" +#include "api/api_credits.h" #include "core/application.h" #include "core/click_handler_types.h" +#include "settings/settings_advanced.h" #include "settings/settings_business.h" -#include "settings/settings_codes.h" +#include "settings/settings_calls.h" #include "settings/settings_chat.h" +#include "settings/settings_codes.h" +#include "settings/settings_credits.h" +#include "settings/settings_folders.h" #include "settings/settings_information.h" #include "settings/settings_notifications.h" -#include "settings/settings_privacy_security.h" -#include "settings/settings_advanced.h" -#include "settings/settings_folders.h" -#include "settings/settings_calls.h" #include "settings/settings_power_saving.h" #include "settings/settings_premium.h" +#include "settings/settings_privacy_security.h" #include "settings/settings_scale_preview.h" #include "boxes/language_box.h" #include "boxes/username_box.h" @@ -27,12 +29,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/basic_click_handlers.h" #include "ui/boxes/confirm_box.h" #include "ui/controls/userpic_button.h" +#include "ui/effects/premium_graphics.h" +#include "ui/effects/premium_top_bar.h" // Ui::Premium::ColorizedSvg. #include "ui/wrap/slide_wrap.h" #include "ui/widgets/menu/menu_add_action_callback.h" #include "ui/widgets/continuous_sliders.h" #include "ui/text/text_utilities.h" #include "ui/toast/toast.h" #include "ui/new_badges.h" +#include "ui/rect.h" #include "ui/vertical_list.h" #include "info/profile/info_profile_badge.h" #include "info/profile/info_profile_emoji_status_panel.h" @@ -258,6 +263,77 @@ void Cover::refreshUsernameGeometry(int newWidth) { _username->moveToLeft(usernameLeft, usernameTop, newWidth); } +[[nodiscard]] not_null AddPremiumStar( + not_null button, + bool credits) { + const auto stops = credits + ? Ui::Premium::CreditsIconGradientStops() + : Ui::Premium::ButtonGradientStops(); + + const auto ministarsContainer = Ui::CreateChild(button); + const auto &buttonSt = button->st(); + const auto fullHeight = buttonSt.height + + rect::m::sum::v(buttonSt.padding); + using MiniStars = Ui::Premium::ColoredMiniStars; + const auto ministars = button->lifetime().make_state( + ministarsContainer, + false); + ministars->setColorOverride(stops); + + ministarsContainer->paintRequest( + ) | rpl::start_with_next([=] { + auto p = QPainter(ministarsContainer); + { + constexpr auto kScale = 0.35; + const auto r = ministarsContainer->rect(); + p.translate(r.center()); + p.scale(kScale, kScale); + p.translate(-r.center()); + } + ministars->paint(p); + }, ministarsContainer->lifetime()); + + const auto badge = Ui::CreateChild(button.get()); + + auto star = [&] { + const auto factor = style::DevicePixelRatio(); + const auto size = Size(st::settingsButtonNoIcon.style.font->ascent); + auto image = QImage( + size * factor, + QImage::Format_ARGB32_Premultiplied); + image.setDevicePixelRatio(factor); + image.fill(Qt::transparent); + { + auto p = QPainter(&image); + auto star = QSvgRenderer(Ui::Premium::ColorizedSvg(stops)); + star.render(&p, Rect(size)); + } + return image; + }(); + badge->resize(star.size() / style::DevicePixelRatio()); + badge->paintRequest( + ) | rpl::start_with_next([=] { + auto p = QPainter(badge); + p.drawImage(0, 0, star); + }, badge->lifetime()); + + button->sizeValue( + ) | rpl::start_with_next([=](const QSize &s) { + badge->moveToLeft( + button->st().iconLeft + + (st::menuIconShop.width() - badge->width()) / 2, + (s.height() - badge->height()) / 2); + ministarsContainer->moveToLeft( + badge->x() - (fullHeight - badge->height()) / 2, + 0); + }, badge->lifetime()); + + ministarsContainer->resize(fullHeight, fullHeight); + ministars->setCenter(ministarsContainer->rect()); + + return button; +} + } // namespace void SetupPowerSavingButton( @@ -419,15 +495,40 @@ void SetupPremium( Ui::AddDivider(container); Ui::AddSkip(container); - AddButtonWithIcon( - container, - tr::lng_premium_summary_title(), - st::settingsButton, - { .icon = &st::menuIconPremium } + AddPremiumStar( + AddButtonWithIcon( + container, + tr::lng_premium_summary_title(), + st::settingsButton), + false )->addClickHandler([=] { controller->setPremiumRef("settings"); showOther(PremiumId()); }); + { + const auto wrap = container->add( + object_ptr>( + container, + object_ptr(container))); + wrap->toggleOn( + controller->session().creditsValue( + ) | rpl::map(rpl::mappers::_1 > 0)); + wrap->finishAnimating(); + AddPremiumStar( + AddButtonWithLabel( + wrap->entity(), + tr::lng_credits_summary_title(), + controller->session().creditsValue( + ) | rpl::map([=](uint64 c) { + return c ? Lang::FormatCountToShort(c).string : QString{}; + }), + st::settingsButton), + true + )->addClickHandler([=] { + controller->setPremiumRef("settings"); + showOther(CreditsId()); + }); + } const auto button = AddButtonWithIcon( container, tr::lng_business_title(), @@ -438,6 +539,12 @@ void SetupPremium( }); Ui::NewBadge::AddToRight(button); + const auto api = button->lifetime().make_state( + controller->session().user()); + api->request({}, [=](Data::CreditsStatusSlice slice) { + controller->session().setCredits(slice.balance); + }); + if (controller->session().premiumCanBuy()) { const auto button = AddButtonWithIcon( container, diff --git a/Telegram/SourceFiles/settings/settings_power_saving.cpp b/Telegram/SourceFiles/settings/settings_power_saving.cpp index 4d523e07a..beaf46613 100644 --- a/Telegram/SourceFiles/settings/settings_power_saving.cpp +++ b/Telegram/SourceFiles/settings/settings_power_saving.cpp @@ -152,6 +152,7 @@ EditFlagsDescriptor PowerSavingLabels() { &st::menuIconChatBubble, }, { kChatSpoiler, tr::lng_settings_power_chat_spoiler(tr::now) }, + { kChatEffects, tr::lng_settings_power_chat_effects(tr::now) }, }; auto calls = std::vector