mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-04-14 13:17:08 +02:00
Merge tag 'v5.1.2' into dev
# Conflicts: # Telegram/Resources/winrc/Telegram.rc # Telegram/Resources/winrc/Updater.rc # Telegram/SourceFiles/boxes/send_files_box.cpp # Telegram/SourceFiles/core/version.h # Telegram/SourceFiles/data/data_search_controller.cpp # Telegram/SourceFiles/dialogs/dialogs_widget.cpp # Telegram/SourceFiles/dialogs/dialogs_widget.h # Telegram/SourceFiles/history/view/media/history_view_gif.cpp # Telegram/SourceFiles/mainwidget.cpp # Telegram/lib_ui
This commit is contained in:
commit
01b8602437
304 changed files with 13815 additions and 3704 deletions
|
@ -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
|
||||
|
|
BIN
Telegram/Resources/icons/menu/factcheck.png
Normal file
BIN
Telegram/Resources/icons/menu/factcheck.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 588 B |
BIN
Telegram/Resources/icons/menu/factcheck@2x.png
Normal file
BIN
Telegram/Resources/icons/menu/factcheck@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
BIN
Telegram/Resources/icons/menu/factcheck@3x.png
Normal file
BIN
Telegram/Resources/icons/menu/factcheck@3x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.2 KiB |
|
@ -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...";
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
<Identity Name="TelegramMessengerLLP.TelegramDesktop"
|
||||
ProcessorArchitecture="ARCHITECTURE"
|
||||
Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
|
||||
Version="5.0.2.0" />
|
||||
Version="5.1.2.0" />
|
||||
<Properties>
|
||||
<DisplayName>Telegram Desktop</DisplayName>
|
||||
<PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
|
|
222
Telegram/SourceFiles/api/api_credits.cpp
Normal file
222
Telegram/SourceFiles/api/api_credits.cpp
Normal file
|
@ -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<PeerData*> 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<PeerData*> 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<PeerData*> peer)
|
||||
: _peer(peer)
|
||||
, _api(&peer->session().api().instance()) {
|
||||
}
|
||||
|
||||
rpl::producer<rpl::no_value, QString> CreditsTopupOptions::request() {
|
||||
return [=](auto consumer) {
|
||||
auto lifetime = rpl::lifetime();
|
||||
|
||||
using TLOption = MTPStarsTopupOption;
|
||||
_api.request(MTPpayments_GetStarsTopupOptions(
|
||||
)).done([=](const MTPVector<TLOption> &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<PeerData*> peer)
|
||||
: _peer(peer)
|
||||
, _api(&peer->session().api().instance()) {
|
||||
}
|
||||
|
||||
void CreditsStatus::request(
|
||||
const Data::CreditsStatusSlice::OffsetToken &token,
|
||||
Fn<void(Data::CreditsStatusSlice)> 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<PeerData*> 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<void(Data::CreditsStatusSlice)> 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<not_null<PeerData*>> PremiumPeerBot(
|
||||
not_null<Main::Session*> session) {
|
||||
const auto username = session->appConfig().get<QString>(
|
||||
u"premium_bot_username"_q,
|
||||
QString());
|
||||
if (username.isEmpty()) {
|
||||
return rpl::never<not_null<PeerData*>>();
|
||||
}
|
||||
if (const auto p = session->data().peerByUsername(username)) {
|
||||
return rpl::single<not_null<PeerData*>>(p);
|
||||
}
|
||||
return [=](auto consumer) {
|
||||
auto lifetime = rpl::lifetime();
|
||||
|
||||
const auto api = lifetime.make_state<MTP::Sender>(&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<PeerData*> peer,
|
||||
const QString &entryId,
|
||||
Fn<void()> done,
|
||||
Fn<void(QString)> 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
|
80
Telegram/SourceFiles/api/api_credits.h
Normal file
80
Telegram/SourceFiles/api/api_credits.h
Normal file
|
@ -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<PeerData*> peer);
|
||||
|
||||
[[nodiscard]] rpl::producer<rpl::no_value, QString> request();
|
||||
[[nodiscard]] Data::CreditTopupOptions options() const;
|
||||
|
||||
private:
|
||||
const not_null<PeerData*> _peer;
|
||||
|
||||
Data::CreditTopupOptions _options;
|
||||
|
||||
MTP::Sender _api;
|
||||
|
||||
};
|
||||
|
||||
class CreditsStatus final {
|
||||
public:
|
||||
CreditsStatus(not_null<PeerData*> peer);
|
||||
|
||||
void request(
|
||||
const Data::CreditsStatusSlice::OffsetToken &token,
|
||||
Fn<void(Data::CreditsStatusSlice)> done);
|
||||
|
||||
private:
|
||||
const not_null<PeerData*> _peer;
|
||||
|
||||
mtpRequestId _requestId = 0;
|
||||
|
||||
MTP::Sender _api;
|
||||
|
||||
};
|
||||
|
||||
class CreditsHistory final {
|
||||
public:
|
||||
CreditsHistory(not_null<PeerData*> peer, bool in, bool out);
|
||||
|
||||
void request(
|
||||
const Data::CreditsStatusSlice::OffsetToken &token,
|
||||
Fn<void(Data::CreditsStatusSlice)> done);
|
||||
|
||||
private:
|
||||
using HistoryTL = MTPpayments_GetStarsTransactions;
|
||||
const not_null<PeerData*> _peer;
|
||||
const HistoryTL::Flags _flags;
|
||||
|
||||
mtpRequestId _requestId = 0;
|
||||
|
||||
MTP::Sender _api;
|
||||
|
||||
};
|
||||
|
||||
void CreditsRefund(
|
||||
not_null<PeerData*> peer,
|
||||
const QString &entryId,
|
||||
Fn<void()> done,
|
||||
Fn<void(QString)> fail);
|
||||
|
||||
[[nodiscard]] rpl::producer<not_null<PeerData*>> PremiumPeerBot(
|
||||
not_null<Main::Session*> session);
|
||||
|
||||
} // namespace Api
|
|
@ -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<HistoryItem*> item,
|
||||
SendOptions options) {
|
||||
const auto empty = [] {};
|
||||
options.invertCaption = item->invertMedia();
|
||||
EditMessage(item, options, empty, empty);
|
||||
}
|
||||
|
||||
|
@ -254,85 +257,85 @@ mtpRequestId EditTextMessage(
|
|||
SendOptions options,
|
||||
Fn<void(mtpRequestId requestId)> done,
|
||||
Fn<void(const QString &error, mtpRequestId requestId)> fail,
|
||||
std::optional<bool> spoilerMediaOverride) {
|
||||
if (spoilerMediaOverride) {
|
||||
const auto spoiler = *spoilerMediaOverride;
|
||||
if (const auto media = item->media()) {
|
||||
auto takeInputMedia = Fn<std::optional<MTPInputMedia>()>(nullptr);
|
||||
auto takeFileReference = Fn<QByteArray()>(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<void()> 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<std::optional<MTPInputMedia>()>(nullptr);
|
||||
auto takeFileReference = Fn<QByteArray()>(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<void()> 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<void()> applyUpdates, mtpRequestId id) {
|
||||
|
|
|
@ -56,6 +56,6 @@ mtpRequestId EditTextMessage(
|
|||
SendOptions options,
|
||||
Fn<void(mtpRequestId requestId)> done,
|
||||
Fn<void(const QString &error, mtpRequestId requestId)> fail,
|
||||
std::optional<bool> spoilerMediaOverride);
|
||||
bool spoilered);
|
||||
|
||||
} // namespace Api
|
||||
|
|
|
@ -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<MTPMessageEntity>(),
|
||||
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(
|
||||
|
|
|
@ -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<MTPMessageEntity>(),
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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<MTPMessageEntity> 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<MTPMessageEntity> 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));
|
||||
|
|
|
@ -1145,7 +1145,9 @@ void Updates::applyUpdatesNoPtsCheck(const MTPUpdates &updates) {
|
|||
MTPMessageReactions(),
|
||||
MTPVector<MTPRestrictionReason>(),
|
||||
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<MTPRestrictionReason>(),
|
||||
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;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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*> 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<void()> 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<PeerData*> 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<SendingAlbum*> 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<SendingAlbum*> album) {
|
|||
MTP_vector<MTPInputSingleMedia>(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);
|
||||
|
||||
|
|
|
@ -274,6 +274,10 @@ public:
|
|||
Fn<void(not_null<PeerData*>, MsgId)> callback);
|
||||
|
||||
using SliceType = Data::LoadDirection;
|
||||
void requestHistory(
|
||||
not_null<History*> history,
|
||||
MsgId messageId,
|
||||
SliceType slice);
|
||||
void requestSharedMedia(
|
||||
not_null<PeerData*> peer,
|
||||
MsgId topicRootId,
|
||||
|
@ -511,7 +515,8 @@ private:
|
|||
not_null<PeerData*> peer,
|
||||
bool justClear,
|
||||
bool revoke);
|
||||
void applyAffectedMessages(const MTPmessages_AffectedMessages &result) const;
|
||||
void applyAffectedMessages(
|
||||
const MTPmessages_AffectedMessages &result) const;
|
||||
|
||||
void deleteAllFromParticipantSend(
|
||||
not_null<ChannelData*> channel,
|
||||
|
@ -645,6 +650,17 @@ private:
|
|||
};
|
||||
base::flat_set<SharedMediaRequest> _sharedMediaRequests;
|
||||
|
||||
struct HistoryRequest {
|
||||
not_null<PeerData*> peer;
|
||||
MsgId aroundId = 0;
|
||||
SliceType sliceType = {};
|
||||
|
||||
friend inline auto operator<=>(
|
||||
const HistoryRequest&,
|
||||
const HistoryRequest&) = default;
|
||||
};
|
||||
base::flat_set<HistoryRequest> _historyRequests;
|
||||
|
||||
std::unique_ptr<DialogsLoadState> _dialogsLoadState;
|
||||
TimeId _dialogsLoadTill = 0;
|
||||
rpl::variable<bool> _dialogsLoadMayBlockByDate = false;
|
||||
|
|
|
@ -192,20 +192,27 @@ void ShowAddParticipantsError(
|
|||
&& channel
|
||||
&& !channel->isMegagroup()
|
||||
&& channel->canAddAdmins()) {
|
||||
const auto makeAdmin = [=] {
|
||||
const auto makeAdmin = [=](Fn<void()> close) {
|
||||
const auto user = forbidden.users.front();
|
||||
const auto weak = std::make_shared<QPointer<EditAdminBox>>();
|
||||
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<EditAdminBox>(
|
||||
channel,
|
||||
user,
|
||||
|
@ -214,6 +221,7 @@ void ShowAddParticipantsError(
|
|||
box->setSaveCallback(saveCallback);
|
||||
*weak = box.data();
|
||||
show->showBox(std::move(box));
|
||||
close();
|
||||
};
|
||||
show->showBox(
|
||||
Ui::MakeConfirmBox({
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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<QString, QString> &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<base::unique_qptr<Ui::PopupMenu>>();
|
||||
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<Ui::PopupMenu>(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<Ui::VerticalLayout>(this));
|
||||
|
||||
|
@ -1094,70 +1187,84 @@ ProxiesBoxController::ProxiesBoxController(not_null<Main::Account*> account)
|
|||
}
|
||||
|
||||
void ProxiesBoxController::ShowApplyConfirmation(
|
||||
Window::SessionController *controller,
|
||||
Type type,
|
||||
const QMap<QString, QString> &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<void()> &&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<Ui::GenericBox*> box) {
|
||||
box->setTitle(tr::lng_proxy_box_title());
|
||||
if (type == Type::Mtproto) {
|
||||
box->addRow(object_ptr<Ui::FlatLabel>(
|
||||
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<Ui::FlatLabel>(box, s, stL));
|
||||
box->addRow(object_ptr<Ui::FlatLabel>(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<Ui::BoxContent> 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);
|
||||
|
|
|
@ -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<Main::Account*> account);
|
||||
|
||||
static void ShowApplyConfirmation(
|
||||
Window::SessionController *controller,
|
||||
Type type,
|
||||
const QMap<QString, QString> &fields);
|
||||
|
||||
|
@ -77,6 +82,9 @@ public:
|
|||
void setTryIPv6(bool enabled);
|
||||
rpl::producer<ProxyData::Settings> proxySettingsValue() const;
|
||||
|
||||
[[nodiscard]] bool contains(const ProxyData &proxy) const;
|
||||
void addNewItem(const ProxyData &proxy);
|
||||
|
||||
rpl::producer<ItemView> views() const;
|
||||
|
||||
~ProxiesBoxController();
|
||||
|
@ -109,7 +117,6 @@ private:
|
|||
void replaceItemValue(
|
||||
std::vector<Item>::iterator which,
|
||||
const ProxyData &proxy);
|
||||
void addNewItem(const ProxyData &proxy);
|
||||
|
||||
const not_null<Main::Account*> _account;
|
||||
Core::SettingsProxy &_settings;
|
||||
|
|
|
@ -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::Result> CreatePollBox::submitRequests() const {
|
||||
|
@ -1044,7 +1044,7 @@ not_null<Ui::InputField*> 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<Ui::RpWidget> 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<QWidget*> widget) {
|
||||
|
@ -1313,24 +1303,25 @@ object_ptr<Ui::RpWidget> 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;
|
||||
|
|
|
@ -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<Result> 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<SendMenu::Details()> _sendMenuDetails;
|
||||
base::unique_qptr<ChatHelpers::TabbedPanel> _emojiPanel;
|
||||
Fn<void()> _setInnerFocus;
|
||||
Fn<rpl::producer<bool>()> _dataIsValidValue;
|
||||
|
|
|
@ -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<Window::SessionController*> controller,
|
||||
std::shared_ptr<Data::PhotoMedia> media,
|
||||
bool wasSpoiler,
|
||||
bool spoilered,
|
||||
Fn<void(Ui::PreparedList)> 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<ImageInfo>(&file.information->media);
|
||||
|
||||
image->modifications = mods;
|
||||
|
@ -225,25 +226,18 @@ void EditPhotoImage(
|
|||
|
||||
} // namespace
|
||||
|
||||
EditCaptionBox::EditCaptionBox(
|
||||
QWidget*,
|
||||
not_null<Window::SessionController*> controller,
|
||||
not_null<HistoryItem*> item)
|
||||
: EditCaptionBox({}, controller, item, PrepareEditText(item), {}, {}) {
|
||||
}
|
||||
|
||||
EditCaptionBox::EditCaptionBox(
|
||||
QWidget*,
|
||||
not_null<Window::SessionController*> controller,
|
||||
not_null<HistoryItem*> item,
|
||||
TextWithTags &&text,
|
||||
bool spoilered,
|
||||
bool invertCaption,
|
||||
Ui::PreparedList &&list,
|
||||
Fn<void()> 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<Ui::VerticalLayout>(this))
|
||||
, _scroll(base::make_unique_q<Ui::ScrollArea>(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<Window::SessionController*> controller,
|
||||
FullMsgId itemId,
|
||||
TextWithTags text,
|
||||
bool spoilered,
|
||||
bool invertCaption,
|
||||
Fn<void()> 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<void()> 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<Data::PhotoMedia> media,
|
||||
FullMsgId itemId,
|
||||
TextWithTags text,
|
||||
bool spoilered,
|
||||
bool invertCaption,
|
||||
Fn<void()> 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<Ui::SingleFilePreview>(
|
||||
|
@ -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)
|
||||
|
|
|
@ -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<Window::SessionController*> controller,
|
||||
not_null<HistoryItem*> item);
|
||||
EditCaptionBox(
|
||||
QWidget*,
|
||||
not_null<Window::SessionController*> controller,
|
||||
not_null<HistoryItem*> item,
|
||||
TextWithTags &&text,
|
||||
bool spoilered,
|
||||
bool invertCaption,
|
||||
Ui::PreparedList &&list,
|
||||
Fn<void()> saved);
|
||||
~EditCaptionBox();
|
||||
|
@ -49,18 +48,24 @@ public:
|
|||
not_null<Window::SessionController*> controller,
|
||||
FullMsgId itemId,
|
||||
TextWithTags text,
|
||||
bool spoilered,
|
||||
bool invertCaption,
|
||||
Fn<void()> saved);
|
||||
static void StartMediaReplace(
|
||||
not_null<Window::SessionController*> controller,
|
||||
FullMsgId itemId,
|
||||
Ui::PreparedList &&list,
|
||||
TextWithTags text,
|
||||
bool spoilered,
|
||||
bool invertCaption,
|
||||
Fn<void()> saved);
|
||||
static void StartPhotoEdit(
|
||||
not_null<Window::SessionController*> controller,
|
||||
std::shared_ptr<Data::PhotoMedia> media,
|
||||
FullMsgId itemId,
|
||||
TextWithTags text,
|
||||
bool spoilered,
|
||||
bool invertCaption,
|
||||
Fn<void()> saved);
|
||||
|
||||
protected:
|
||||
|
@ -111,7 +116,6 @@ private:
|
|||
const base::unique_qptr<Ui::EmojiButton> _emojiToggle;
|
||||
|
||||
base::unique_qptr<Ui::AbstractSinglePreview> _content;
|
||||
Fn<bool()> _previewHasSpoiler;
|
||||
base::unique_qptr<ChatHelpers::TabbedPanel> _emojiPanel;
|
||||
base::unique_qptr<QObject> _emojiFilter;
|
||||
|
||||
|
@ -122,6 +126,7 @@ private:
|
|||
std::shared_ptr<Data::PhotoMedia> _photoMedia;
|
||||
|
||||
Ui::PreparedList _preparedList;
|
||||
HistoryView::MediaEditManager _mediaEditManager;
|
||||
|
||||
mtpRequestId _saveRequestId = 0;
|
||||
|
||||
|
|
|
@ -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<Ui::Premium::TopBar>(
|
||||
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<Ui::Premium::TopBar>(
|
||||
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<Window::SessionNavigation*> controller,
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
const Data::CreditsHistoryEntry &entry) {
|
||||
auto table = container->add(
|
||||
object_ptr<Ui::TableLayout>(
|
||||
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<Ui::FlatLabel>(
|
||||
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))));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Data::GiveawayStart> start,
|
||||
std::optional<Data::GiveawayResults> results);
|
||||
|
||||
void AddCreditsHistoryEntryTable(
|
||||
not_null<Window::SessionNavigation*> controller,
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
const Data::CreditsHistoryEntry &entry);
|
||||
|
|
|
@ -36,6 +36,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "styles/style_widgets.h"
|
||||
|
||||
#include <xxhash.h> // XXH64.
|
||||
#include <QtWidgets/QApplication>
|
||||
|
||||
[[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);
|
||||
|
||||
|
|
|
@ -348,6 +348,9 @@ public:
|
|||
virtual int peerListPartitionRows(Fn<bool(const PeerListRow &a)> border) = 0;
|
||||
virtual std::shared_ptr<Main::SessionShow> peerListUiShow() = 0;
|
||||
|
||||
virtual void peerListPressLeftToContextMenu(bool shown) = 0;
|
||||
virtual bool peerListTrackRowPressFromGlobal(QPoint globalPosition) = 0;
|
||||
|
||||
template <typename PeerDataRange>
|
||||
void peerListAddSelectedPeers(PeerDataRange &&range) {
|
||||
for (const auto peer : range) {
|
||||
|
@ -478,6 +481,15 @@ public:
|
|||
}
|
||||
}
|
||||
|
||||
virtual bool rowTrackPress(not_null<PeerListRow*> row) {
|
||||
return false;
|
||||
}
|
||||
virtual void rowTrackPressCancel() {
|
||||
}
|
||||
virtual bool rowTrackPressSkipMouseSelection() {
|
||||
return false;
|
||||
}
|
||||
|
||||
virtual void loadMoreRows() {
|
||||
}
|
||||
virtual void itemDeselectedHook(not_null<PeerData*> 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<QPoint> _lastMousePosition;
|
||||
Qt::MouseButton _pressButton = Qt::LeftButton;
|
||||
std::optional<QPoint> _trackPressStart;
|
||||
|
||||
rpl::event_stream<Ui::ScrollToRequest> _scrollToRequests;
|
||||
|
||||
|
@ -992,6 +1007,13 @@ public:
|
|||
bool highlightRow,
|
||||
Fn<void(not_null<Ui::PopupMenu*>)> destroyed = nullptr) override;
|
||||
|
||||
void peerListPressLeftToContextMenu(bool shown) override {
|
||||
_content->pressLeftToContextMenu(shown);
|
||||
}
|
||||
bool peerListTrackRowPressFromGlobal(QPoint globalPosition) override {
|
||||
return _content->trackRowPressFromGlobal(globalPosition);
|
||||
}
|
||||
|
||||
protected:
|
||||
not_null<PeerListContent*> content() const {
|
||||
return _content;
|
||||
|
|
|
@ -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<DefaultIcon> value,
|
||||
Fn<void()> repaint);
|
||||
Fn<void()> 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<DefaultIcon> value,
|
||||
Fn<void()> repaint) {
|
||||
rpl::producer<DefaultIcon> value,
|
||||
Fn<void()> 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<DefaultIconEmoji>(
|
||||
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<Ui::Text::CustomEmoji> MakeTopicIconEmoji(
|
||||
Data::TopicIconDescriptor descriptor,
|
||||
Fn<void()> repaint,
|
||||
Data::CustomEmojiSizeTag tag) {
|
||||
return std::make_unique<DefaultIconEmoji>(
|
||||
rpl::single(descriptor),
|
||||
std::move(repaint),
|
||||
tag);
|
||||
}
|
||||
|
|
|
@ -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<Window::SessionController*> controller,
|
||||
not_null<History*> forum,
|
||||
MsgId rootId);
|
||||
|
||||
[[nodiscard]] std::unique_ptr<Ui::Text::CustomEmoji> MakeTopicIconEmoji(
|
||||
Data::TopicIconDescriptor descriptor,
|
||||
Fn<void()> repaint,
|
||||
Data::CustomEmojiSizeTag tag);
|
||||
|
|
|
@ -415,7 +415,12 @@ private:
|
|||
std::deque<FnMut<void()>> _saveStagesQueue;
|
||||
Saving _savingData;
|
||||
|
||||
const rpl::event_stream<Privacy> _privacyTypeUpdates;
|
||||
struct PrivacyAndForwards {
|
||||
Privacy privacy;
|
||||
bool noForwards = false;
|
||||
};
|
||||
|
||||
const rpl::event_stream<PrivacyAndForwards> _privacyTypeUpdates;
|
||||
const rpl::event_stream<ChannelData*> _linkedChatUpdates;
|
||||
mtpRequestId _linkedChatsRequestId = 0;
|
||||
|
||||
|
@ -761,7 +766,7 @@ void Controller::refreshHistoryVisibility() {
|
|||
void Controller::showEditPeerTypeBox(
|
||||
std::optional<rpl::producer<QString>> 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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -286,7 +286,7 @@ void PreloadSticker(const std::shared_ptr<Data::DocumentMedia> &media) {
|
|||
document,
|
||||
media->videoThumbnailContent(),
|
||||
QString(),
|
||||
true);
|
||||
Stickers::EffectType::PremiumSticker);
|
||||
|
||||
const auto update = [=] {
|
||||
if (!state->readyInvoked
|
||||
|
|
|
@ -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*> 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<MTPRestrictionReason>(), // restrictions
|
||||
MTPstring(), // bot placeholder
|
||||
MTPstring(), // lang code
|
||||
MTPEmojiStatus(),
|
||||
MTPVector<MTPUsername>(),
|
||||
MTPint(), // stories_max_id
|
||||
MTPPeerColor(), // color
|
||||
MTPPeerColor())); // profile_color
|
||||
return peerId;
|
||||
}
|
||||
|
||||
AdminLog::OwnedItem GenerateItem(
|
||||
not_null<HistoryView::ElementDelegate*> delegate,
|
||||
not_null<History*> 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<Ui::VerticalLayout*> container,
|
||||
not_null<Window::SessionController*> 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,
|
||||
|
|
250
Telegram/SourceFiles/boxes/send_credits_box.cpp
Normal file
250
Telegram/SourceFiles/boxes/send_credits_box.cpp
Normal file
|
@ -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<Ui::GenericBox*> box,
|
||||
std::shared_ptr<Payments::CreditsFormData> form,
|
||||
Fn<void()> sent) {
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
struct State {
|
||||
rpl::variable<bool> confirmButtonBusy = false;
|
||||
};
|
||||
const auto state = box->lifetime().make_state<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<Ui::RpWidget>(box);
|
||||
const auto fullHeight = photoSize * 2;
|
||||
using MiniStars = Ui::Premium::ColoredMiniStars;
|
||||
const auto ministars = box->lifetime().make_state<MiniStars>(
|
||||
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<Ui::CenterWrap<>>(
|
||||
content,
|
||||
Settings::HistoryEntryPhoto(content, form->photo, photoSize)));
|
||||
} else {
|
||||
const auto widget = box->addRow(
|
||||
object_ptr<Ui::CenterWrap<>>(
|
||||
content,
|
||||
object_ptr<Ui::UserpicButton>(
|
||||
content,
|
||||
bot,
|
||||
st::defaultUserpicButton)));
|
||||
widget->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
}
|
||||
|
||||
Ui::AddSkip(content);
|
||||
box->addRow(object_ptr<Ui::CenterWrap<>>(
|
||||
box,
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box,
|
||||
tr::lng_credits_box_out_title(),
|
||||
st::settingsPremiumUserTitle)));
|
||||
Ui::AddSkip(content);
|
||||
box->addRow(object_ptr<Ui::CenterWrap<>>(
|
||||
box,
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
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<Ui::FlatLabel>(
|
||||
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<Ui::IconButton>(
|
||||
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<Api::CreditsStatus>(
|
||||
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
|
25
Telegram/SourceFiles/boxes/send_credits_box.h
Normal file
25
Telegram/SourceFiles/boxes/send_credits_box.h
Normal file
|
@ -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<Ui::GenericBox*> box,
|
||||
std::shared_ptr<Payments::CreditsFormData> data,
|
||||
Fn<void()> sent);
|
||||
|
||||
} // namespace Ui
|
|
@ -336,7 +336,7 @@ SendFilesBox::SendFilesBox(
|
|||
const TextWithTags &caption,
|
||||
not_null<PeerData*> 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<SendMenu::Details()> 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<void(MenuAction, MenuDetails)> {
|
||||
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<Ui::PopupMenu>(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<ImageInfo>(&_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<void(Api::SendOptions)> SendFilesBox::sendCallback() {
|
||||
return crl::guard(this, [=](Api::SendOptions options) {
|
||||
send(options, false);
|
||||
});
|
||||
}
|
||||
|
||||
SendFilesBox::~SendFilesBox() = default;
|
||||
|
|
|
@ -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<SendMenu::Details()> sendMenuDetails = nullptr;
|
||||
const style::ComposeControls *stOverride = nullptr;
|
||||
SendFilesConfirmed confirmed;
|
||||
Fn<void()> cancelled;
|
||||
|
@ -115,7 +116,7 @@ public:
|
|||
const TextWithTags &caption,
|
||||
not_null<PeerData*> 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<void(Api::SendOptions)> sendCallback();
|
||||
void captionResized();
|
||||
void saveSendWaySettings();
|
||||
|
||||
|
@ -227,6 +229,11 @@ private:
|
|||
|
||||
void checkCharsLimitation();
|
||||
|
||||
[[nodiscard]] Fn<MenuDetails()> prepareSendMenuDetails(
|
||||
const SendFilesBoxDescriptor &descriptor);
|
||||
[[nodiscard]] auto prepareSendMenuCallback()
|
||||
-> Fn<void(MenuAction, MenuDetails)>;
|
||||
|
||||
const std::shared_ptr<ChatHelpers::Show> _show;
|
||||
const style::ComposeControls &_st;
|
||||
const Api::SendType _sendType = Api::SendType();
|
||||
|
@ -238,12 +245,14 @@ private:
|
|||
std::optional<int> _removingIndex;
|
||||
|
||||
SendFilesLimits _limits = {};
|
||||
SendMenu::Type _sendMenuType = {};
|
||||
Fn<MenuDetails()> _sendMenuDetails;
|
||||
Fn<void(MenuAction, MenuDetails)> _sendMenuCallback;
|
||||
PeerData *_captionToPeer = nullptr;
|
||||
SendFilesCheck _check;
|
||||
SendFilesConfirmed _confirmedCallback;
|
||||
Fn<void()> _cancelledCallback;
|
||||
bool _confirmed = false;
|
||||
bool _invertCaption = false;
|
||||
|
||||
object_ptr<Ui::InputField> _caption = { nullptr };
|
||||
TextWithTags _prefilledCaptionText;
|
||||
|
|
|
@ -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<Ui::RpWidget*> parent) {
|
||||
|
@ -523,15 +526,32 @@ void ShareBox::showMenu(not_null<Ui::RpWidget*> 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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -79,7 +79,9 @@ using Data::StickersSet;
|
|||
using Data::StickersPack;
|
||||
using SetFlag = Data::StickersSetFlag;
|
||||
|
||||
[[nodiscard]] std::optional<QColor> ComputeImageColor(const QImage &frame) {
|
||||
[[nodiscard]] std::optional<QColor> 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<Main::Session*> session) {
|
||||
StickerPremiumMark::StickerPremiumMark(
|
||||
not_null<Main::Session*> 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<Ui::PopupMenu>(
|
||||
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 = [=] {
|
||||
|
|
|
@ -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<Main::Session*> session);
|
||||
StickerPremiumMark(
|
||||
not_null<Main::Session*> 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;
|
||||
|
|
|
@ -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 }};
|
||||
|
|
|
@ -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<bool> adjustShadowLeft() const;
|
||||
[[nodiscard]] virtual SendMenu::Type sendMenuType() const = 0;
|
||||
[[nodiscard]] virtual SendMenu::Details sendMenuDetails() const = 0;
|
||||
|
||||
virtual bool showMediaPreview(
|
||||
Data::FileOrigin origin,
|
||||
|
|
|
@ -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<LocalStickersManager>(&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<StickerPremiumMark>(
|
||||
&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<std::vector<QString>> EmojiListWidget::searchQueries() const {
|
||||
return _searchQueries.events();
|
||||
}
|
||||
|
||||
rpl::producer<int> 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<TabbedSelector::InnerFooter> 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<DocumentId> &list) {
|
|||
}
|
||||
|
||||
base::unique_qptr<Ui::PopupMenu> 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<RecentEmojiDocument>(recent.id.data)
|
||||
&& !_freeEffects.contains(
|
||||
v::get<RecentEmojiDocument>(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<EmojiPtr>(&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);
|
||||
|
|
|
@ -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<std::unique_ptr<Ui::Text::CustomEmoji>(
|
||||
DocumentId,
|
||||
Fn<void()>)> customRecentFactory;
|
||||
base::flat_set<DocumentId> freeEffects;
|
||||
const style::EmojiPan *st = nullptr;
|
||||
ComposeFeatures features;
|
||||
};
|
||||
|
@ -144,7 +148,10 @@ public:
|
|||
RectPart origin);
|
||||
|
||||
base::unique_qptr<Ui::PopupMenu> fillContextMenu(
|
||||
SendMenu::Type type) override;
|
||||
const SendMenu::Details &details) override;
|
||||
|
||||
[[nodiscard]] rpl::producer<std::vector<QString>> searchQueries() const;
|
||||
[[nodiscard]] rpl::producer<int> recentShownCount() const;
|
||||
|
||||
protected:
|
||||
void visibleTopBottomUpdated(
|
||||
|
@ -397,10 +404,13 @@ private:
|
|||
int _counts[kEmojiSectionCount];
|
||||
std::vector<RecentOne> _recent;
|
||||
base::flat_set<DocumentId> _recentCustomIds;
|
||||
base::flat_set<DocumentId> _freeEffects;
|
||||
base::flat_set<uint64> _repaintsScheduled;
|
||||
rpl::variable<int> _recentShownCount;
|
||||
std::unique_ptr<Ui::Text::CustomEmojiPaintContext> _emojiPaintContext;
|
||||
bool _recentPainted = false;
|
||||
bool _grabbingChosen = false;
|
||||
bool _paintAsPremium = false;
|
||||
QVector<EmojiPtr> _emoji[kEmojiSectionCount];
|
||||
std::vector<CustomSet> _custom;
|
||||
base::flat_set<DocumentId> _restrictedCustomList;
|
||||
|
@ -414,10 +424,13 @@ private:
|
|||
Ui::RoundRect _overBg;
|
||||
QImage _searchExpandCache;
|
||||
|
||||
std::unique_ptr<StickerPremiumMark> _premiumMark;
|
||||
QImage _premiumMarkFrameCache;
|
||||
mutable std::unique_ptr<Ui::RippleAnimation> _colorAllRipple;
|
||||
bool _colorAllRippleForced = false;
|
||||
rpl::lifetime _colorAllRippleForcedLifetime;
|
||||
|
||||
rpl::event_stream<std::vector<QString>> _searchQueries;
|
||||
std::vector<QString> _nextSearchQuery;
|
||||
std::vector<QString> _searchQuery;
|
||||
base::flat_set<EmojiPtr> _searchEmoji;
|
||||
|
|
|
@ -87,7 +87,7 @@ public:
|
|||
Api::SendOptions options = {}) const;
|
||||
|
||||
void setRecentInlineBotsInRows(int32 bots);
|
||||
void setSendMenuType(Fn<SendMenu::Type()> &&callback);
|
||||
void setSendMenuDetails(Fn<SendMenu::Details()> &&callback);
|
||||
void rowsUpdated();
|
||||
|
||||
rpl::producer<FieldAutocomplete::MentionChosen> mentionChosen() const;
|
||||
|
@ -155,7 +155,7 @@ private:
|
|||
const std::unique_ptr<Ui::PathShiftGradient> _pathGradient;
|
||||
StickerPremiumMark _premiumMark;
|
||||
|
||||
Fn<SendMenu::Type()> _sendMenuType;
|
||||
Fn<SendMenu::Details()> _sendMenuDetails;
|
||||
|
||||
rpl::event_stream<FieldAutocomplete::MentionChosen> _mentionChosen;
|
||||
rpl::event_stream<FieldAutocomplete::HashtagChosen> _hashtagChosen;
|
||||
|
@ -835,8 +835,9 @@ bool FieldAutocomplete::chooseSelected(ChooseMethod method) const {
|
|||
return _inner->chooseSelected(method);
|
||||
}
|
||||
|
||||
void FieldAutocomplete::setSendMenuType(Fn<SendMenu::Type()> &&callback) {
|
||||
_inner->setSendMenuType(std::move(callback));
|
||||
void FieldAutocomplete::setSendMenuDetails(
|
||||
Fn<SendMenu::Details()> &&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<Ui::PopupMenu>(
|
||||
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<SendMenu::Type()> &&callback) {
|
||||
_sendMenuType = std::move(callback);
|
||||
void FieldAutocomplete::Inner::setSendMenuDetails(
|
||||
Fn<SendMenu::Details()> &&callback) {
|
||||
_sendMenuDetails = std::move(callback);
|
||||
}
|
||||
|
||||
auto FieldAutocomplete::Inner::mentionChosen() const
|
||||
|
|
|
@ -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<bool(int)> callback) {
|
||||
_moderateKeyActivateCallback = std::move(callback);
|
||||
}
|
||||
void setSendMenuType(Fn<SendMenu::Type()> &&callback);
|
||||
void setSendMenuDetails(Fn<SendMenu::Details()> &&callback);
|
||||
|
||||
void hideFast();
|
||||
void showAnimated();
|
||||
|
|
|
@ -387,22 +387,31 @@ void GifsListWidget::mousePressEvent(QMouseEvent *e) {
|
|||
}
|
||||
|
||||
base::unique_qptr<Ui::PopupMenu> GifsListWidget::fillContextMenu(
|
||||
SendMenu::Type type) {
|
||||
const SendMenu::Details &details) {
|
||||
if (_selected < 0 || _pressed >= 0) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto menu = base::make_unique_q<Ui::PopupMenu>(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)) {
|
||||
|
|
|
@ -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<Ui::PopupMenu> fillContextMenu(
|
||||
SendMenu::Type type) override;
|
||||
const SendMenu::Details &details) override;
|
||||
|
||||
~GifsListWidget();
|
||||
|
||||
|
|
|
@ -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<Main::Session*> _session,
|
||||
Fn<bool(not_null<DocumentData*>)> allowPremiumEmoji);
|
||||
|
||||
QString operator()(QStringView mimeTag);
|
||||
|
||||
private:
|
||||
const not_null<Main::Session*> _session;
|
||||
const Fn<bool(not_null<DocumentData*>)> _allowPremiumEmoji;
|
||||
|
||||
};
|
||||
|
||||
FieldTagMimeProcessor::FieldTagMimeProcessor(
|
||||
not_null<Main::Session*> session,
|
||||
Fn<bool(not_null<DocumentData*>)> 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<QString(QStringView)> FieldTagMimeProcessor(
|
||||
not_null<Main::Session*> session,
|
||||
Fn<bool(not_null<DocumentData*>)> 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<void(QString, QString)> callback,
|
||||
const style::InputField *fieldStyle) {
|
||||
const style::InputField *fieldStyle,
|
||||
Fn<QString(QString)> 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 @@ Fn<bool(
|
|||
text,
|
||||
link,
|
||||
std::move(callback),
|
||||
fieldStyle));
|
||||
fieldStyle,
|
||||
qthelp::validate_url));
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
@ -352,7 +338,7 @@ void InitMessageFieldHandlers(
|
|||
field->setInstantReplaces(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<bool(
|
||||
Ui::InputField::EditLinkSelection selection,
|
||||
QString text,
|
||||
QString link,
|
||||
EditLinkAction action)> FactcheckEditLinkCallback(
|
||||
std::shared_ptr<Main::SessionShow> show,
|
||||
not_null<Ui::InputField*> 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<void(not_null<Ui::InputField*>)> FactcheckFieldIniter(
|
||||
std::shared_ptr<Main::SessionShow> show) {
|
||||
Expects(show != nullptr);
|
||||
|
||||
return [=](not_null<Ui::InputField*> 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<Window::SessionController*> controller,
|
||||
not_null<Ui::InputField*> field,
|
||||
|
@ -431,10 +499,7 @@ bool HasSendText(not_null<const Ui::InputField*> 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();
|
||||
|
|
|
@ -77,6 +77,9 @@ void InitSpellchecker(
|
|||
not_null<Ui::InputField*> field,
|
||||
bool skipDictionariesManager = false);
|
||||
|
||||
[[nodiscard]] Fn<void(not_null<Ui::InputField*>)> FactcheckFieldIniter(
|
||||
std::shared_ptr<Main::SessionShow> show);
|
||||
|
||||
bool HasSendText(not_null<const Ui::InputField*> field);
|
||||
|
||||
void InitMessageFieldFade(
|
||||
|
|
|
@ -269,10 +269,10 @@ std::unique_ptr<Lottie::SinglePlayer> EmojiPack::effectPlayer(
|
|||
not_null<DocumentData*> 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<Lottie::SinglePlayer> 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),
|
||||
|
|
|
@ -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<DocumentData*> document,
|
||||
QByteArray data,
|
||||
QString filepath,
|
||||
bool premium);
|
||||
EffectType type);
|
||||
|
||||
private:
|
||||
class ImageLoader;
|
||||
|
||||
struct ProviderKey {
|
||||
not_null<DocumentData*> 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<DocumentData*>,
|
||||
ProviderKey,
|
||||
std::weak_ptr<Lottie::FrameProvider>> _sharedProviders;
|
||||
|
||||
rpl::event_stream<> _refreshed;
|
||||
|
|
|
@ -195,8 +195,10 @@ StickersListWidget::StickersListWidget(
|
|||
, _overBg(st::roundRadiusLarge, st().overBg)
|
||||
, _api(&session().mtp())
|
||||
, _localSetsManager(std::make_unique<LocalStickersManager>(&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<StickerPremiumMark>(&session()))
|
||||
, _premiumMark(std::make_unique<StickerPremiumMark>(
|
||||
&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<FileChosen> 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<uint64>());
|
||||
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<int> 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<StickersSet*> set) {
|
|||
std::move(elements));
|
||||
}
|
||||
|
||||
void StickersListWidget::toggleSearchLoading(bool loading) {
|
||||
if (_search) {
|
||||
_search->setLoading(loading);
|
||||
}
|
||||
}
|
||||
|
||||
void StickersListWidget::takeHeavyData(
|
||||
std::vector<Set> &to,
|
||||
std::vector<Set> &from) {
|
||||
|
@ -831,7 +860,7 @@ auto StickersListWidget::shownSets() -> std::vector<Set> & {
|
|||
|
||||
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<DocumentData*> document) {
|
|||
}
|
||||
|
||||
base::unique_qptr<Ui::PopupMenu> 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<Ui::PopupMenu> StickersListWidget::fillContextMenu(
|
|||
auto menu = base::make_unique_q<Ui::PopupMenu>(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<Ui::PopupMenu> 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<Sticker> {
|
||||
_custom.clear();
|
||||
_cornerEmoji.clear();
|
||||
auto result = std::vector<Sticker>();
|
||||
|
||||
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<Sticker> {
|
||||
if (_isEffects) {
|
||||
return collectCustomRecents();
|
||||
}
|
||||
_custom.clear();
|
||||
auto result = std::vector<Sticker>();
|
||||
|
||||
|
@ -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<QString> &&query) {
|
||||
auto set = base::flat_set<EmojiPtr>();
|
||||
auto text = ranges::accumulate(query, QString(), [](
|
||||
applySearchQuery(std::move(query));
|
||||
}, session, type);
|
||||
}
|
||||
|
||||
void StickersListWidget::applySearchQuery(std::vector<QString> &&query) {
|
||||
auto set = base::flat_set<EmojiPtr>();
|
||||
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<EmojiPtr> &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<EmojiPtr>();
|
||||
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<Ui::BoxContent> MakeConfirmRemoveSetBox(
|
||||
|
|
|
@ -66,12 +66,19 @@ enum class StickersListMode {
|
|||
Masks,
|
||||
UserpicBuilder,
|
||||
ChatIntro,
|
||||
MessageEffects,
|
||||
};
|
||||
|
||||
struct StickerCustomRecentDescriptor {
|
||||
not_null<DocumentData*> document;
|
||||
QString cornerEmoji;
|
||||
};
|
||||
|
||||
struct StickersListDescriptor {
|
||||
std::shared_ptr<Show> show;
|
||||
StickersListMode mode = StickersListMode::Full;
|
||||
Fn<bool()> paused;
|
||||
std::vector<StickerCustomRecentDescriptor> customRecentList;
|
||||
const style::EmojiPan *st = nullptr;
|
||||
ComposeFeatures features;
|
||||
};
|
||||
|
@ -116,10 +123,13 @@ public:
|
|||
std::shared_ptr<Lottie::FrameRenderer> getLottieRenderer();
|
||||
|
||||
base::unique_qptr<Ui::PopupMenu> fillContextMenu(
|
||||
SendMenu::Type type) override;
|
||||
const SendMenu::Details &details) override;
|
||||
|
||||
bool mySetsEmpty() const;
|
||||
|
||||
void applySearchQuery(std::vector<QString> &&query);
|
||||
[[nodiscard]] rpl::producer<int> 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<Sticker> collectRecentStickers();
|
||||
[[nodiscard]] std::vector<Sticker> collectRecentStickers();
|
||||
[[nodiscard]] std::vector<Sticker> 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<Ui::RippleAnimation> createButtonRipple(int section);
|
||||
QPoint buttonRippleTopLeft(int section) const;
|
||||
[[nodiscard]] std::unique_ptr<Ui::RippleAnimation> createButtonRipple(
|
||||
int section);
|
||||
[[nodiscard]] QPoint buttonRippleTopLeft(int section) const;
|
||||
|
||||
std::vector<Set> &shownSets();
|
||||
const std::vector<Set> &shownSets() const;
|
||||
int featuredRowHeight() const;
|
||||
[[nodiscard]] std::vector<Set> &shownSets();
|
||||
[[nodiscard]] const std::vector<Set> &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<EmojiPtr> &emoji);
|
||||
|
||||
enum class AppendSkip {
|
||||
None,
|
||||
|
@ -347,6 +361,7 @@ private:
|
|||
void fillLocalSearchRows(const QString &query);
|
||||
void fillCloudSearchRows(const std::vector<uint64> &cloudSets);
|
||||
void addSearchRow(not_null<Data::StickersSet*> set);
|
||||
void toggleSearchLoading(bool loading);
|
||||
|
||||
void showPreview();
|
||||
|
||||
|
@ -364,14 +379,17 @@ private:
|
|||
std::unique_ptr<LocalStickersManager> _localSetsManager;
|
||||
ChannelData *_megagroupSet = nullptr;
|
||||
uint64 _megagroupSetIdRequested = 0;
|
||||
std::vector<StickerCustomRecentDescriptor> _customRecentIds;
|
||||
std::vector<Set> _mySets;
|
||||
std::vector<Set> _officialSets;
|
||||
std::vector<Set> _searchSets;
|
||||
int _featuredSetsCount = 0;
|
||||
std::vector<bool> _custom;
|
||||
std::vector<EmojiPtr> _cornerEmoji;
|
||||
base::flat_set<not_null<DocumentData*>> _favedStickersMap;
|
||||
std::weak_ptr<Lottie::FrameRenderer> _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<StickerPremiumMark> _premiumMark;
|
||||
|
||||
std::vector<not_null<DocumentData*>> _filteredStickers;
|
||||
std::vector<EmojiPtr> _filterStickersCornerEmoji;
|
||||
rpl::variable<int> _recentShownCount;
|
||||
std::map<QString, std::vector<uint64>> _searchCache;
|
||||
std::vector<std::pair<uint64, QStringList>> _searchIndex;
|
||||
base::Timer _searchRequestTimer;
|
||||
|
|
|
@ -28,6 +28,9 @@ TabbedSection::TabbedSection(
|
|||
not_null<Window::SessionController*> controller)
|
||||
: Window::SectionWidget(parent, controller)
|
||||
, _selector(controller->tabbedSelector()) {
|
||||
if (Ui::InFocusChain(_selector)) {
|
||||
parent->window()->setFocus();
|
||||
}
|
||||
_selector->setParent(this);
|
||||
_selector->setRoundRadius(0);
|
||||
_selector->setGeometry(rect());
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<Ui::PopupMenu> 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<int> _minimalHeight;
|
||||
|
||||
rpl::event_stream<int> _scrollToRequests;
|
||||
rpl::event_stream<bool> _disableScrollRequests;
|
||||
|
|
|
@ -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::Controller*> window) {
|
|||
|
||||
void Application::showOpenGLCrashNotification() {
|
||||
const auto enable = [=] {
|
||||
Ui::GL::ForceDisable(false);
|
||||
Ui::GL::CrashCheckFinish();
|
||||
settings().setDisableOpenGL(false);
|
||||
Local::writeSettings();
|
||||
Restart();
|
||||
};
|
||||
const auto keepDisabled = [=](Fn<void()> 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([=] {
|
||||
|
|
|
@ -52,6 +52,8 @@ struct ClickHandlerContext {
|
|||
};
|
||||
Q_DECLARE_METATYPE(ClickHandlerContext);
|
||||
|
||||
class PhoneClickHandler;
|
||||
|
||||
class HiddenUrlClickHandler : public UrlClickHandler {
|
||||
public:
|
||||
HiddenUrlClickHandler(QString url) : UrlClickHandler(url, false) {
|
||||
|
|
|
@ -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>(workMode);
|
||||
switch (uncheckedWorkMode) {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
335
Telegram/SourceFiles/core/phone_click_handler.cpp
Normal file
335
Telegram/SourceFiles/core/phone_click_handler.cpp
Normal file
|
@ -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<Ui::RpWidget*> parent,
|
||||
const style::Menu &st,
|
||||
const QString &phone,
|
||||
not_null<Window::SessionController*> controller);
|
||||
|
||||
bool isEnabled() const override;
|
||||
not_null<QAction*> action() const override;
|
||||
|
||||
void handleKeyPress(not_null<QKeyEvent*> 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<QAction*> _dummyAction;
|
||||
const style::Menu &_st;
|
||||
rpl::variable<PeerData*> _peer;
|
||||
rpl::variable<bool> _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<Ui::RpWidget*> parent,
|
||||
const style::Menu &st,
|
||||
const QString &phone,
|
||||
not_null<Window::SessionController*> 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<QAction*> 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<QKeyEvent*> 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<Main::Session*> 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<ClickHandlerContext>();
|
||||
const auto controller = my.sessionWindow.get();
|
||||
const auto pos = QCursor::pos();
|
||||
if (!controller) {
|
||||
return;
|
||||
}
|
||||
const auto menu = Ui::CreateChild<Ui::PopupMenu>(
|
||||
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<ResolvePhoneAction>(
|
||||
menu,
|
||||
menu->st().menu,
|
||||
phone,
|
||||
controller));
|
||||
|
||||
menu->popup(pos);
|
||||
}
|
||||
|
||||
auto PhoneClickHandler::getTextEntity() const -> TextEntity {
|
||||
return { EntityType::Phone };
|
||||
}
|
||||
|
||||
QString PhoneClickHandler::tooltip() const {
|
||||
return _text;
|
||||
}
|
30
Telegram/SourceFiles/core/phone_click_handler.h
Normal file
30
Telegram/SourceFiles/core/phone_click_handler.h
Normal file
|
@ -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<Main::Session*> session, QString text);
|
||||
|
||||
void onClick(ClickContext context) const override;
|
||||
|
||||
TextEntity getTextEntity() const override;
|
||||
|
||||
QString tooltip() const override;
|
||||
|
||||
private:
|
||||
const not_null<Main::Session*> _session;
|
||||
QString _text;
|
||||
|
||||
};
|
|
@ -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<ClickHandler> UiIntegration::createLinkHandler(
|
|||
return std::make_shared<MonospaceClickHandler>(data.text, data.type);
|
||||
case EntityType::Pre:
|
||||
return std::make_shared<MonospaceClickHandler>(data.text, data.type);
|
||||
case EntityType::Phone:
|
||||
return std::make_shared<PhoneClickHandler>(my->session, data.text);
|
||||
}
|
||||
return Integration::createLinkHandler(data, context);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -87,7 +87,9 @@ constexpr auto kRequestTimeLimit = 60 * crl::time(1000);
|
|||
MTPMessageReactions(),
|
||||
MTPVector<MTPRestrictionReason>(),
|
||||
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()));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
218
Telegram/SourceFiles/data/components/factchecks.cpp
Normal file
218
Telegram/SourceFiles/data/components/factchecks.cpp
Normal file
|
@ -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<Main::Session*> session)
|
||||
: _session(session)
|
||||
, _requestTimer([=] { request(); }) {
|
||||
}
|
||||
|
||||
void Factchecks::requestFor(not_null<HistoryItem*> item) {
|
||||
subscribeIfNotYet();
|
||||
|
||||
if (const auto factcheck = item->Get<HistoryMessageFactcheck>()) {
|
||||
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<const HistoryItem*> 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<MTPint>();
|
||||
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<MTPint>(std::move(ids))
|
||||
)).done([=](const MTPVector<MTPFactCheck> &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<HistoryView::WebPage> Factchecks::makeMedia(
|
||||
not_null<HistoryView::Message*> view,
|
||||
not_null<HistoryMessageFactcheck*> factcheck) {
|
||||
if (!factcheck->page) {
|
||||
factcheck->page = view->history()->owner().webpage(
|
||||
base::RandomValue<WebPageId>(),
|
||||
tr::lng_factcheck_title(tr::now),
|
||||
factcheck->data.text);
|
||||
factcheck->page->type = WebPageType::Factcheck;
|
||||
}
|
||||
return std::make_unique<HistoryView::WebPage>(
|
||||
view,
|
||||
factcheck->page,
|
||||
MediaWebPageFlags());
|
||||
}
|
||||
|
||||
bool Factchecks::canEdit(not_null<HistoryItem*> 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<bool>(u"can_edit_factcheck"_q, false);
|
||||
}
|
||||
|
||||
int Factchecks::lengthLimit() const {
|
||||
return _session->appConfig().get<int>(u"factcheck_length_limit"_q, 1024);
|
||||
}
|
||||
|
||||
void Factchecks::save(
|
||||
FullMsgId itemId,
|
||||
TextWithEntities text,
|
||||
Fn<void(QString)> 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<Ui::Show> 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
|
70
Telegram/SourceFiles/data/components/factchecks.h
Normal file
70
Telegram/SourceFiles/data/components/factchecks.h
Normal file
|
@ -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<Main::Session*> session);
|
||||
|
||||
void requestFor(not_null<HistoryItem*> item);
|
||||
[[nodiscard]] std::unique_ptr<HistoryView::WebPage> makeMedia(
|
||||
not_null<HistoryView::Message*> view,
|
||||
not_null<HistoryMessageFactcheck*> factcheck);
|
||||
|
||||
[[nodiscard]] bool canEdit(not_null<HistoryItem*> item) const;
|
||||
[[nodiscard]] int lengthLimit() const;
|
||||
|
||||
void save(
|
||||
FullMsgId itemId,
|
||||
TextWithEntities text,
|
||||
Fn<void(QString)> done);
|
||||
void save(
|
||||
FullMsgId itemId,
|
||||
const TextWithEntities &was,
|
||||
TextWithEntities text,
|
||||
std::shared_ptr<Ui::Show> show);
|
||||
|
||||
private:
|
||||
[[nodiscard]] bool canEdit() const;
|
||||
|
||||
void subscribeIfNotYet();
|
||||
void request();
|
||||
|
||||
const not_null<Main::Session*> _session;
|
||||
|
||||
base::Timer _requestTimer;
|
||||
base::flat_set<not_null<HistoryItem*>> _pending;
|
||||
std::vector<HistoryItem*> _requested;
|
||||
mtpRequestId _requestId = 0;
|
||||
bool _subscribed = false;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Data
|
|
@ -91,7 +91,9 @@ constexpr auto kRequestTimeLimit = 60 * crl::time(1000);
|
|||
MTPMessageReactions(),
|
||||
MTPVector<MTPRestrictionReason>(),
|
||||
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<MTPRestrictionReason>(),
|
||||
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);
|
||||
|
||||
|
|
51
Telegram/SourceFiles/data/data_credits.h
Normal file
51
Telegram/SourceFiles/data/data_credits.h
Normal file
|
@ -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<CreditTopupOption>;
|
||||
|
||||
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<CreditsHistoryEntry> list;
|
||||
uint64 balance = 0;
|
||||
bool allLoaded = false;
|
||||
OffsetToken token;
|
||||
};
|
||||
|
||||
} // namespace Data
|
|
@ -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*> 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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
219
Telegram/SourceFiles/data/data_history_messages.cpp
Normal file
219
Telegram/SourceFiles/data/data_history_messages.cpp
Normal file
|
@ -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<MsgId> &&messageIds,
|
||||
MsgRange noSkipRange,
|
||||
std::optional<int> 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<Storage::SparseIdsSliceUpdate> {
|
||||
return _chat.sliceUpdated();
|
||||
}
|
||||
|
||||
rpl::producer<MsgId> HistoryMessages::oneRemoved() const {
|
||||
return _oneRemoved.events();
|
||||
}
|
||||
|
||||
rpl::producer<> HistoryMessages::allRemoved() const {
|
||||
return _allRemoved.events();
|
||||
}
|
||||
|
||||
rpl::producer<> HistoryMessages::bottomInvalidated() const {
|
||||
return _bottomInvalidated.events();
|
||||
}
|
||||
|
||||
rpl::producer<SparseIdsSlice> HistoryViewer(
|
||||
not_null<History*> 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<SparseIdsSliceBuilder>(
|
||||
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<SparseIdsMergedSlice> HistoryMergedViewer(
|
||||
not_null<History*> 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<MessagesSlice> HistoryMessagesViewer(
|
||||
not_null<History*> 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
|
67
Telegram/SourceFiles/data/data_history_messages.h
Normal file
67
Telegram/SourceFiles/data/data_history_messages.h
Normal file
|
@ -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<MsgId> &&messageIds,
|
||||
MsgRange noSkipRange,
|
||||
std::optional<int> count);
|
||||
void removeOne(MsgId messageId);
|
||||
void removeAll();
|
||||
void invalidateBottom();
|
||||
|
||||
[[nodiscard]] Storage::SparseIdsListResult snapshot(
|
||||
const Storage::SparseIdsListQuery &query) const;
|
||||
[[nodiscard]] auto sliceUpdated() const
|
||||
-> rpl::producer<Storage::SparseIdsSliceUpdate>;
|
||||
[[nodiscard]] rpl::producer<MsgId> oneRemoved() const;
|
||||
[[nodiscard]] rpl::producer<> allRemoved() const;
|
||||
[[nodiscard]] rpl::producer<> bottomInvalidated() const;
|
||||
|
||||
private:
|
||||
Storage::SparseIdsList _chat;
|
||||
rpl::event_stream<MsgId> _oneRemoved;
|
||||
rpl::event_stream<> _allRemoved;
|
||||
rpl::event_stream<> _bottomInvalidated;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] rpl::producer<SparseIdsSlice> HistoryViewer(
|
||||
not_null<History*> history,
|
||||
MsgId aroundId,
|
||||
int limitBefore,
|
||||
int limitAfter);
|
||||
|
||||
[[nodiscard]] rpl::producer<SparseIdsMergedSlice> HistoryMergedViewer(
|
||||
not_null<History*> history,
|
||||
/*Universal*/MsgId universalAroundId,
|
||||
int limitBefore,
|
||||
int limitAfter);
|
||||
|
||||
[[nodiscard]] rpl::producer<MessagesSlice> HistoryMessagesViewer(
|
||||
not_null<History*> history,
|
||||
MessagePosition aroundId,
|
||||
int limitBefore,
|
||||
int limitAfter);
|
||||
|
||||
} // namespace Data
|
|
@ -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<Media> MediaContact::clone(not_null<HistoryItem*> 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<HistoryView::Media> MediaContact::createView(
|
|||
not_null<HistoryView::Element*> message,
|
||||
not_null<HistoryItem*> realParent,
|
||||
HistoryView::Element *replacing) {
|
||||
return std::make_unique<HistoryView::Contact>(
|
||||
message,
|
||||
_contact.userId,
|
||||
_contact.firstName,
|
||||
_contact.lastName,
|
||||
_contact.phoneNumber);
|
||||
return std::make_unique<HistoryView::Contact>(message, _contact);
|
||||
}
|
||||
|
||||
MediaLocation::MediaLocation(
|
||||
|
|
|
@ -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<VcardItemType, QString>;
|
||||
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<Media> clone(not_null<HistoryItem*> parent) override;
|
||||
|
|
|
@ -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<Session*> 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<Reaction> &Reactions::list(Type type) const {
|
||||
switch (type) {
|
||||
case Type::Active: return _active;
|
||||
|
@ -357,6 +368,7 @@ const std::vector<Reaction> &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<ReactionId> 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<DocumentData*> 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<CustomEmojiManager::Listener*> Reactions::resolveListener() {
|
||||
return static_cast<CustomEmojiManager::Listener*>(this);
|
||||
}
|
||||
|
@ -1116,35 +1273,74 @@ void Reactions::resolve(const ReactionId &id) {
|
|||
}
|
||||
|
||||
std::optional<Reaction> 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<Reaction> 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(),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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<not_null<const Reaction*>> recent;
|
||||
std::vector<not_null<const Reaction*>> stickers;
|
||||
bool customAllowed = false;
|
||||
bool tags = false;
|
||||
};
|
||||
|
@ -50,6 +53,7 @@ struct PossibleItemReactions {
|
|||
explicit PossibleItemReactions(const PossibleItemReactionsRef &other);
|
||||
|
||||
std::vector<Reaction> recent;
|
||||
std::vector<Reaction> 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<Reaction> &list(Type type) const;
|
||||
[[nodiscard]] const std::vector<MyTagInfo> &myTagsInfo() const;
|
||||
|
@ -108,16 +114,19 @@ public:
|
|||
[[nodiscard]] rpl::producer<> myTagsUpdates() const;
|
||||
[[nodiscard]] rpl::producer<> tagsUpdates() const;
|
||||
[[nodiscard]] rpl::producer<ReactionId> 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<HistoryItem*> item, bool addToRecent);
|
||||
[[nodiscard]] bool sending(not_null<HistoryItem*> item) const;
|
||||
|
@ -139,11 +148,11 @@ public:
|
|||
|
||||
private:
|
||||
struct ImageSet {
|
||||
QImage bottomInfo;
|
||||
QImage inlineList;
|
||||
QImage image;
|
||||
std::shared_ptr<DocumentMedia> media;
|
||||
std::unique_ptr<Ui::AnimatedIcon> 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<Reaction> resolveById(const ReactionId &id);
|
||||
[[nodiscard]] std::vector<Reaction> resolveByIds(
|
||||
|
@ -203,13 +215,20 @@ private:
|
|||
|
||||
[[nodiscard]] std::optional<Reaction> parse(
|
||||
const MTPAvailableReaction &entry);
|
||||
[[nodiscard]] std::optional<Reaction> 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<DocumentData*> 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<ReactionId> _topIds;
|
||||
base::flat_set<ReactionId> _unresolvedTop;
|
||||
std::vector<not_null<DocumentData*>> _genericAnimations;
|
||||
std::vector<Reaction> _effects;
|
||||
ReactionId _favoriteId;
|
||||
ReactionId _unresolvedFavoriteId;
|
||||
std::optional<Reaction> _favorite;
|
||||
|
@ -249,6 +269,7 @@ private:
|
|||
rpl::event_stream<SavedSublist*> _myTagsUpdated;
|
||||
rpl::event_stream<> _tagsUpdated;
|
||||
rpl::event_stream<ReactionId> _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<ReactionId, ImageSet> _images;
|
||||
rpl::lifetime _imagesLoadLifetime;
|
||||
bool _waitingForList = false;
|
||||
bool _waitingForReactions = false;
|
||||
bool _waitingForEffects = false;
|
||||
|
||||
base::flat_map<FullMsgId, mtpRequestId> _sentRequests;
|
||||
|
||||
|
|
|
@ -78,10 +78,6 @@ private:
|
|||
[[nodiscard]] Histories &histories();
|
||||
|
||||
void subscribeToUpdates();
|
||||
[[nodiscard]] rpl::producer<MessagesSlice> sourceFromServer(
|
||||
MessagePosition aroundId,
|
||||
int limitBefore,
|
||||
int limitAfter);
|
||||
void appendClientSideMessages(MessagesSlice &slice);
|
||||
|
||||
[[nodiscard]] bool buildFromData(not_null<Viewer*> viewer);
|
||||
|
|
|
@ -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<PeerData*> 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<PeerData*> peer,
|
||||
MsgId messageId,
|
||||
Data::LoadDirection direction,
|
||||
const HistoryRequestResult &data) {
|
||||
return ParseSearchResult(
|
||||
peer,
|
||||
Storage::SharedMediaType::kCount,
|
||||
messageId,
|
||||
direction,
|
||||
data);
|
||||
}
|
||||
|
||||
SearchController::CacheEntry::CacheEntry(
|
||||
not_null<Main::Session*> session,
|
||||
const Query &query)
|
||||
|
|
|
@ -32,7 +32,11 @@ struct SearchResult {
|
|||
using SearchRequest = MTPmessages_Search;
|
||||
using SearchRequestResult = MTPmessages_Messages;
|
||||
|
||||
std::optional<SearchRequest> PrepareSearchRequest(
|
||||
using HistoryResult = SearchResult;
|
||||
using HistoryRequest = MTPmessages_GetHistory;
|
||||
using HistoryRequestResult = MTPmessages_Messages;
|
||||
|
||||
[[nodiscard]] std::optional<SearchRequest> PrepareSearchRequest(
|
||||
not_null<PeerData*> peer,
|
||||
MsgId topicRootId,
|
||||
Storage::SharedMediaType type,
|
||||
|
@ -40,13 +44,24 @@ std::optional<SearchRequest> PrepareSearchRequest(
|
|||
MsgId messageId,
|
||||
Data::LoadDirection direction);
|
||||
|
||||
SearchResult ParseSearchResult(
|
||||
[[nodiscard]] SearchResult ParseSearchResult(
|
||||
not_null<PeerData*> peer,
|
||||
Storage::SharedMediaType type,
|
||||
MsgId messageId,
|
||||
Data::LoadDirection direction,
|
||||
const SearchRequestResult &data);
|
||||
|
||||
[[nodiscard]] HistoryRequest PrepareHistoryRequest(
|
||||
not_null<PeerData*> peer,
|
||||
MsgId messageId,
|
||||
Data::LoadDirection direction);
|
||||
|
||||
[[nodiscard]] HistoryResult ParseHistoryResult(
|
||||
not_null<PeerData*> peer,
|
||||
MsgId messageId,
|
||||
Data::LoadDirection direction,
|
||||
const HistoryRequestResult &data);
|
||||
|
||||
class SearchController final {
|
||||
public:
|
||||
using IdsList = Storage::SparseIdsList;
|
||||
|
|
|
@ -1830,11 +1830,16 @@ rpl::producer<not_null<HistoryItem*>> Session::itemDataChanges() const {
|
|||
}
|
||||
|
||||
void Session::requestItemTextRefresh(not_null<HistoryItem*> item) {
|
||||
if (const auto i = _views.find(item); i != _views.end()) {
|
||||
for (const auto &view : i->second) {
|
||||
const auto call = [&](not_null<HistoryItem*> item) {
|
||||
enumerateItemViews(item, [&](not_null<ViewElement*> 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<PollData*> poll) {
|
|||
}
|
||||
|
||||
void Session::sendWebPageGamePollNotifications() {
|
||||
auto resize = std::vector<not_null<ViewElement*>>();
|
||||
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<not_null<WebPageData*>> Session::webPageUpdates() const {
|
||||
|
@ -4636,7 +4639,9 @@ void Session::insertCheckedServiceNotification(
|
|||
MTPMessageReactions(),
|
||||
MTPVector<MTPRestrictionReason>(),
|
||||
MTPint(), // ttl_period
|
||||
MTPint()), // quick_reply_shortcut_id
|
||||
MTPint(), // quick_reply_shortcut_id
|
||||
MTPlong(), // effect
|
||||
MTPFactCheck()),
|
||||
localFlags,
|
||||
NewMessageType::Unread);
|
||||
}
|
||||
|
|
|
@ -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<MessageFlag>;
|
||||
|
|
|
@ -54,6 +54,8 @@ enum class WebPageType : uint8 {
|
|||
|
||||
VoiceChat,
|
||||
Livestream,
|
||||
|
||||
Factcheck,
|
||||
};
|
||||
[[nodiscard]] WebPageType ParseWebPageType(const MTPDwebPage &type);
|
||||
[[nodiscard]] bool IgnoreIv(WebPageType type);
|
||||
|
|
|
@ -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<Ui::Text::CustomEmoji> 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<Ui::Text::CustomEmoji> CustomEmojiManager::userpic(
|
|||
if (v.size() != 5 && v.size() != 1) {
|
||||
return nullptr;
|
||||
}
|
||||
const auto id = PeerId(v[0].toULongLong());
|
||||
auto image = std::shared_ptr<Ui::DynamicImage>();
|
||||
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<Ui::CustomEmoji::DynamicImageEmoji>(
|
||||
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<PeerData*> 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) {
|
||||
|
|
|
@ -94,7 +94,8 @@ public:
|
|||
|
||||
[[nodiscard]] QString peerUserpicEmojiData(
|
||||
not_null<PeerData*> peer,
|
||||
QMargins padding = {});
|
||||
QMargins padding = {},
|
||||
bool respectSavedRepliesEtc = false);
|
||||
|
||||
[[nodiscard]] uint64 coloredSetId() const;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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<const style::DialogRow*> st() const {
|
||||
return _st;
|
||||
}
|
||||
[[nodiscard]] bool waitingForSearch() const {
|
||||
return _waitingForSearch;
|
||||
}
|
||||
[[nodiscard]] bool hasFilteredResults() const;
|
||||
|
||||
void searchInChat(
|
||||
Key key,
|
||||
PeerData *from,
|
||||
std::vector<Data::ReactionId> tags);
|
||||
void applySearchState(SearchState state);
|
||||
[[nodiscard]] auto searchTagsChanges() const
|
||||
-> rpl::producer<std::vector<Data::ReactionId>>;
|
||||
|
||||
void applyFilterUpdate(QString newFilter, bool force = false);
|
||||
void onHashtagFilterUpdate(QStringView newFilter);
|
||||
void appendToFiltered(Key key);
|
||||
|
||||
|
@ -156,6 +157,7 @@ public:
|
|||
void setLoadMoreFilteredCallback(Fn<void()> callback);
|
||||
[[nodiscard]] rpl::producer<> listBottomReached() const;
|
||||
[[nodiscard]] rpl::producer<> cancelSearchFromUserRequests() const;
|
||||
[[nodiscard]] rpl::producer<> cancelSearchRequests() const;
|
||||
[[nodiscard]] rpl::producer<ChosenRow> chosenRow() const;
|
||||
[[nodiscard]] rpl::producer<> updated() const;
|
||||
|
||||
|
@ -163,7 +165,6 @@ public:
|
|||
[[nodiscard]] rpl::producer<Ui::ScrollToRequest> mustScrollTo() const;
|
||||
[[nodiscard]] rpl::producer<Ui::ScrollToRequest> dialogMoved() const;
|
||||
[[nodiscard]] rpl::producer<> searchMessages() const;
|
||||
[[nodiscard]] rpl::producer<> cancelSearchInChatRequests() const;
|
||||
[[nodiscard]] rpl::producer<QString> completeHashtagRequests() const;
|
||||
[[nodiscard]] rpl::producer<> refreshHashtagsRequests() const;
|
||||
|
||||
|
@ -174,6 +175,11 @@ public:
|
|||
|
||||
void parentGeometryChanged();
|
||||
|
||||
bool processTouchEvent(not_null<QTouchEvent*> 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<const PeerSearchResult*> 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*> history);
|
||||
void trackSearchResultsForum(Data::Forum *forum);
|
||||
|
||||
[[nodiscard]] QBrush currentBg() const;
|
||||
[[nodiscard]] RowDescriptor computeChatPreviewRow() const;
|
||||
|
||||
[[nodiscard]] const std::vector<Key> &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<Ui::RpWidget> _loadingAnimation = { nullptr };
|
||||
object_ptr<SearchEmpty> _searchEmpty = { nullptr };
|
||||
SearchState _searchEmptyState;
|
||||
object_ptr<Ui::FlatLabel> _empty = { nullptr };
|
||||
object_ptr<Ui::IconButton> _cancelSearchInChat;
|
||||
object_ptr<Ui::IconButton> _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> _searchTags;
|
||||
std::vector<Data::ReactionId> _searchTagsSelected;
|
||||
int _searchTagsLeft = 0;
|
||||
RowDescriptor _menuRow;
|
||||
|
||||
|
@ -514,11 +526,20 @@ private:
|
|||
rpl::event_stream<QString> _completeHashtagRequests;
|
||||
rpl::event_stream<> _refreshHashtagsRequests;
|
||||
|
||||
RowDescriptor _chatPreviewRow;
|
||||
bool _chatPreviewScheduled = false;
|
||||
std::optional<QPoint> _chatPreviewTouchGlobal;
|
||||
base::Timer _touchDragPinnedTimer;
|
||||
std::optional<QPoint> _touchDragStartGlobal;
|
||||
std::optional<QPoint> _touchDragNowGlobal;
|
||||
rpl::event_stream<> _touchCancelRequests;
|
||||
|
||||
rpl::variable<ChildListShown> _childListShown;
|
||||
float64 _narrowRatio = 0.;
|
||||
bool _geometryInited = false;
|
||||
|
||||
bool _savedSublists = false;
|
||||
bool _searchLoading = false;
|
||||
|
||||
base::unique_qptr<Ui::PopupMenu> _menu;
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<Data::ReactionId> 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
|
||||
|
|
|
@ -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(
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue