Merge tag 'v5.6.3' into dev

This commit is contained in:
AlexeyZavar 2024-10-16 13:41:32 +03:00
commit 7321b654bf
270 changed files with 9084 additions and 3098 deletions

View file

@ -402,6 +402,8 @@ PRIVATE
boxes/sessions_box.h
boxes/share_box.cpp
boxes/share_box.h
boxes/star_gift_box.cpp
boxes/star_gift_box.h
boxes/sticker_set_box.cpp
boxes/sticker_set_box.h
boxes/stickers_box.cpp
@ -649,6 +651,8 @@ PRIVATE
data/data_lastseen_status.h
data/data_location.cpp
data/data_location.h
data/data_media_preload.cpp
data/data_media_preload.h
data/data_media_rotation.cpp
data/data_media_rotation.h
data/data_media_types.cpp
@ -684,6 +688,7 @@ PRIVATE
data/data_replies_list.h
data/data_reply_preview.cpp
data/data_reply_preview.h
data/data_report.h
data/data_saved_messages.cpp
data/data_saved_messages.h
data/data_saved_sublist.cpp
@ -1027,6 +1032,10 @@ PRIVATE
info/media/info_media_widget.h
info/members/info_members_widget.cpp
info/members/info_members_widget.h
info/peer_gifts/info_peer_gifts_common.cpp
info/peer_gifts/info_peer_gifts_common.h
info/peer_gifts/info_peer_gifts_widget.cpp
info/peer_gifts/info_peer_gifts_widget.h
info/polls/info_polls_results_inner_widget.cpp
info/polls/info_polls_results_inner_widget.h
info/polls/info_polls_results_widget.cpp

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 MiB

After

Width:  |  Height:  |  Size: 1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 832 KiB

After

Width:  |  Height:  |  Size: 935 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 234 KiB

View file

@ -582,3 +582,55 @@ div.toast_shown {
.bot_button_column_separator {
width: 2px
}
.reactions {
margin: 5px 0;
}
.reactions .reaction {
display: inline-flex;
height: 20px;
border-radius: 15px;
background-color: #e8f5fc;
color: #168acd;
font-weight: bold;
margin-bottom: 5px;
}
.reactions .reaction.active {
background-color: #40a6e2;
color: #fff;
}
.reactions .reaction.paid {
background-color: #fdf6e1;
color: #c58523;
}
.reactions .reaction.active.paid {
background-color: #ecae0a;
color: #fdf6e1;
}
.reactions .reaction .emoji {
line-height: 20px;
margin: 0 5px;
font-size: 15px;
}
.reactions .reaction .userpic:not(:first-child) {
margin-left: -8px;
}
.reactions .reaction .userpic {
display: inline-block;
}
.reactions .reaction .userpic .initials {
font-size: 8px;
}
.reactions .reaction .count {
margin-right: 8px;
line-height: 20px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -12,6 +12,7 @@ body {
margin: 0;
background-color: var(--td-window-bg);
color: var(--td-window-fg);
zoom: var(--td-zoom-percentage);
}
html.custom_scroll ::-webkit-scrollbar {

View file

@ -453,6 +453,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_username_app_not_found" = "Bot application not found.";
"lng_username_link" = "This link opens a chat with you:";
"lng_username_copied" = "Link copied to clipboard.";
"lng_username_text_copied" = "Username copied to clipboard.";
"lng_usernames_edit" = "click to edit";
"lng_usernames_active" = "active";
@ -487,6 +488,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_collectible_phone_info" = "This phone number was bought on **Fragment** on {date} for {price}";
"lng_collectible_phone_copy" = "Copy Phone Number";
"lng_collectible_learn_more" = "Learn More";
"lng_collectible_phone_copied" = "Phone number copied to clipboard.";
"lng_settings_section_info" = "Info";
@ -508,6 +510,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_settings_alert_linux" = "Draw attention to the window";
"lng_settings_badge_title" = "Badge counter";
"lng_settings_include_muted" = "Include muted chats in unread count";
"lng_settings_include_muted_folders" = "Include muted chats in folder counters";
"lng_settings_count_unread" = "Count unread messages";
"lng_settings_events_title" = "Events";
"lng_settings_events_joined" = "Contact joined Telegram";
@ -1322,6 +1325,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_profile_similar_channels#other" = "{count} similar channels";
"lng_profile_saved_messages#one" = "{count} saved message";
"lng_profile_saved_messages#other" = "{count} saved messages";
"lng_profile_peer_gifts#one" = "{count} gift";
"lng_profile_peer_gifts#other" = "{count} gifts";
"lng_profile_participants_section" = "Members";
"lng_profile_subscribers_section" = "Subscribers";
"lng_profile_add_contact" = "Add Contact";
@ -1376,6 +1381,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_profile_copy_fullname" = "Copy Name";
"lng_profile_photo_by_you" = "photo set by you";
"lng_profile_public_photo" = "public photo";
"lng_profile_administrators#one" = "{count} administrator";
"lng_profile_administrators#other" = "{count} administrators";
"lng_profile_manage" = "Channel settings";
"lng_invite_upgrade_title" = "Upgrade to Premium";
"lng_invite_upgrade_group_invite#one" = "{users} only accepts invitations to groups from Contacts and **Premium** users.";
@ -1659,6 +1667,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_report_and_ban_button" = "Ban user";
"lng_report_details_about" = "Please enter any additional details relevant to your report.";
"lng_report_details" = "Additional Details";
"lng_report_details_optional" = "Add Comment (Optional)";
"lng_report_details_non_optional" = "Add Comment";
"lng_report_details_message_about" = "Please help us by telling what is wrong with the message you have selected";
"lng_report_reason_spam" = "Spam";
"lng_report_reason_fake" = "Fake Account";
"lng_report_reason_violence" = "Violence";
@ -1852,8 +1863,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_action_proximity_distance_km#other" = "{count} km";
"lng_action_webview_data_done" = "Data from the \"{text}\" button was transferred to the bot.";
"lng_action_gift_received" = "{user} sent you a gift for {cost}";
"lng_action_gift_received_me" = "You sent to {user} a gift for {cost}";
"lng_action_gift_sent" = "You sent a gift for {cost}";
"lng_action_gift_received_anonymous" = "Unknown user sent you a gift for {cost}";
"lng_action_gift_for_stars#one" = "{count} Star";
"lng_action_gift_for_stars#other" = "{count} Stars";
"lng_action_gift_got_subtitle" = "Gift from {user}";
"lng_action_gift_got_stars_text#one" = "Display this gift on your page or convert it to **{count}** Star.";
"lng_action_gift_got_stars_text#other" = "Display this gift on your page or convert it to **{count}** Stars.";
"lng_action_gift_sent_subtitle" = "Gift for {user}";
"lng_action_gift_sent_text#one" = "{user} can display this gift on their page or convert it to {count} Star.";
"lng_action_gift_sent_text#other" = "{user} can display this gift on their page or convert it to {count} Stars.";
"lng_action_gift_premium_months#one" = "{count} Month Premium";
"lng_action_gift_premium_months#other" = "{count} Months Premium";
"lng_action_gift_premium_about" = "Subscription for exclusive Telegram features.";
"lng_action_suggested_photo_me" = "You suggested this photo for {user}'s Telegram profile.";
"lng_action_suggested_photo" = "{user} suggests this photo for your Telegram profile.";
"lng_action_suggested_photo_button" = "View Photo";
@ -1915,6 +1937,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_similar_channels_premium_all_link" = "Telegram Premium";
"lng_similar_channels_show_more" = "Show more channels";
"lng_peer_gifts_title" = "Gifts";
"lng_peer_gifts_about" = "These gifts were sent to {user} by other users.";
"lng_peer_gifts_about_mine" = "These gifts were sent to you by other users. Click on a gift to convert it to Stars or change its privacy settings.";
"lng_premium_gift_duration_months#one" = "for {count} month";
"lng_premium_gift_duration_months#other" = "for {count} months";
"lng_premium_gift_duration_years#one" = "for {count} year";
@ -2415,6 +2441,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_credits_box_history_entry_gift_name" = "Received Gift";
"lng_credits_box_history_entry_giveaway_name" = "Received Prize";
"lng_credits_box_history_entry_gift_sent" = "Sent Gift";
"lng_credits_box_history_entry_gift_converted" = "Converted Gift";
"lng_credits_box_history_entry_gift_out_about" = "With Stars, **{user}** will be able to unlock content and services on Telegram.\n{link}";
"lng_credits_box_history_entry_gift_in_about" = "Use Stars to unlock content and services on Telegram. {link}";
"lng_credits_box_history_entry_gift_about_link" = "See Examples {emoji}";
@ -2461,6 +2488,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_credits_small_balance_about" = "Buy **Stars** and use them on **{bot}** and other miniapps.";
"lng_credits_small_balance_reaction" = "Buy **Stars** and send them to {channel} to support their posts.";
"lng_credits_small_balance_subscribe" = "Buy **Stars** and subscribe to **{channel}** and other channels.";
"lng_credits_small_balance_star_gift" = "Buy **Stars** to send gifts to {user} and other contacts.";
"lng_credits_small_balance_fallback" = "Buy **Stars** to unlock content and services on Telegram.";
"lng_credits_purchase_blocked" = "Sorry, you can't purchase this item with Telegram Stars.";
"lng_credits_enough" = "You have enough stars at the moment. {link}";
@ -2980,6 +3008,53 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_gift_stars_incoming" = "Use Stars to unlock content and services on Telegram.";
"lng_gift_until" = "Until";
"lng_gift_premium_or_stars" = "Gift Premium or Stars";
"lng_gift_premium_subtitle" = "Gift Premium";
"lng_gift_premium_about" = "Give {name} access to exclusive features with Telegram Premium. {features}";
"lng_gift_premium_features" = "See Features >";
"lng_gift_premium_label" = "Premium";
"lng_gift_stars_subtitle" = "Gift Stars";
"lng_gift_stars_about" = "Give {name} gifts that can be kept on your profile or converted to Stars. {link}";
"lng_gift_stars_link" = "What are Stars >";
"lng_gift_stars_limited" = "limited";
"lng_gift_stars_sold_out" = "sold out";
"lng_gift_stars_tabs_all" = "All Gifts";
"lng_gift_stars_tabs_limited" = "Limited";
"lng_gift_send_title" = "Send a Gift";
"lng_gift_send_message" = "Enter Message";
"lng_gift_send_anonymous" = "Hide My Name";
"lng_gift_send_anonymous_about" = "You can hide your name and message from visitors to {user}'s profile. {recipient} will still see your name and message.";
"lng_gift_send_premium_about" = "Only {user} will see your message.";
"lng_gift_send_button" = "Send a Gift for {cost}";
"lng_gift_sent_title" = "Gift Sent!";
"lng_gift_sent_about#one" = "You spent **{count}** Star from your balance.";
"lng_gift_sent_about#other" = "You spent **{count}** Stars from your balance.";
"lng_gift_limited_of_one" = "unique";
"lng_gift_limited_of_count" = "1 of {amount}";
"lng_gift_anonymous_hint" = "Only you can see the sender's name.";
"lng_gift_hidden_hint" = "This gift is hidden. Only you can see it.";
"lng_gift_visible_hint" = "This gift is visible to visitors of your page.";
"lng_gift_availability" = "Availability";
"lng_gift_from_hidden" = "Hidden User";
"lng_gift_availability_left#one" = "{count} of {amount} left";
"lng_gift_availability_left#other" = "{count} of {amount} left";
"lng_gift_availability_none" = "None of {amount} left";
"lng_gift_display_on_page" = "Display on my Page";
"lng_gift_display_on_page_hide" = "Hide from my Page";
"lng_gift_convert_to_stars#one" = "Convert to {count} Star";
"lng_gift_convert_to_stars#other" = "Convert to {count} Stars";
"lng_gift_convert_sure_title" = "Convert Gift to Stars";
"lng_gift_convert_sure_text#one" = "Do you want to convert this gift from {user} to **{count} Star**?\n\nThis action cannot be undone.";
"lng_gift_convert_sure_text#other" = "Do you want to convert this gift from {user} to **{count} Stars**?\n\nThis action cannot be undone.";
"lng_gift_convert_sure" = "Convert";
"lng_gift_display_done" = "The gift is now shown on your profile page.";
"lng_gift_display_done_hide" = "The gift is now hidden from your profile page.";
"lng_gift_got_stars#one" = "You got **{count} Star** for this gift.";
"lng_gift_got_stars#other" = "You got **{count} Stars** for this gift.";
"lng_gift_sold_out_title" = "Sold Out!";
"lng_gift_sold_out_text#one" = "All {count} gift was already sold.";
"lng_gift_sold_out_text#other" = "All {count} gifts were already sold.";
"lng_accounts_limit_title" = "Limit Reached";
"lng_accounts_limit1#one" = "You have reached the limit of **{count}** connected account.";
"lng_accounts_limit1#other" = "You have reached the limit of **{count}** connected accounts.";
@ -3222,6 +3297,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_replies_discussion_started" = "Discussion started";
"lng_replies_no_comments" = "No comments here yet...";
"lng_verification_codes" = "Verification Codes";
"lng_archived_name" = "Archived chats";
"lng_archived_add" = "Archive";
"lng_archived_remove" = "Unarchive";
@ -3246,7 +3323,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_open_link" = "Open";
"lng_allow_bot_pass" = "Allow {bot_name} to pass your Telegram name and ID (not your phone number) to the web pages you open via this bot?";
"lng_allow_bot" = "Allow";
"lng_allow_bot_webview" = "{bot_name} would like to open its web app to proceed.\n\nIt will be able to access your **IP address** and basic device info.";
"lng_allow_bot_webview_details" = "More about this bot {emoji}";
"lng_allow_bot_webview_details_about" = "To launch this web app, you will connect to its website.\n\nIt will be able to access your **IP address** and basic device info.";
"lng_url_auth_open_confirm" = "Do you want to open {link}?";
"lng_url_auth_login_option" = "Log in to {domain} as {user}";
"lng_url_auth_allow_messages" = "Allow {bot} to send me messages";
@ -3498,6 +3576,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_context_animated_reactions_many#one" = "Reactions contain emoji from **{count} pack**.";
"lng_context_animated_reactions_many#other" = "Reactions contain emoji from **{count} packs**.";
"lng_context_noforwards_info_channel" = "Copying and forwarding is not allowed in this channel.";
"lng_context_noforwards_info_group" = "Copying and forwarding is not allowed in this group.";
"lng_context_noforwards_info_bot" = "Copying and forwarding is not allowed from this bot.";
"lng_context_spoiler_effect" = "Hide with Spoiler";
"lng_context_disable_spoiler" = "Remove Spoiler";
"lng_context_make_paid" = "Make This Content Paid";
@ -3608,6 +3690,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_reply_header_short" = "Reply";
"lng_reply_quote_selected" = "Quote Selected";
"lng_reply_from_private_chat" = "This reply is from a private chat.";
"lng_reply_quote_long_title" = "Quote too long!";
"lng_reply_quote_long_text" = "The selected text is too long to quote.";
"lng_link_options_header" = "Link Preview Settings";
"lng_link_header_short" = "Link";
"lng_link_move_up" = "Move Up";
@ -4233,6 +4317,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_rights_restriction_for_all" = "This option is disabled for all members in Group Permissions.";
"lng_rights_permission_for_all" = "This option is enabled for all members in Group Permissions.";
"lng_rights_permission_unavailable" = "This permission is not available in public groups.";
"lng_rights_permission_in_discuss" = "This permission is not available in discussion groups.";
"lng_rights_permission_cant_edit" = "You cannot change this permission.";
"lng_rights_user_restrictions" = "User permissions";
"lng_rights_user_restrictions_header" = "What can this member do?";
@ -5550,6 +5635,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_qr_box_quality1" = "Normal";
"lng_qr_box_quality2" = "High";
"lng_qr_box_quality3" = "Very High";
"lng_qr_box_transparent_background" = "Transparent Background";
"lng_qr_box_font_size" = "Font size";
// Wnd specific

View file

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

View file

@ -16,6 +16,8 @@
</compatibility>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitor</dpiAwareness>
<activeCodePage xmlns="http://schemas.microsoft.com/SMI/2019/WindowsSettings">UTF-8</activeCodePage>
</windowsSettings>
</application>

View file

@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico"
//
VS_VERSION_INFO VERSIONINFO
FILEVERSION 5,5,5,0
PRODUCTVERSION 5,5,5,0
FILEVERSION 5,6,3,0
PRODUCTVERSION 5,6,3,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
@ -62,10 +62,10 @@ BEGIN
BEGIN
VALUE "CompanyName", "Radolyn Labs"
VALUE "FileDescription", "AyuGram Desktop"
VALUE "FileVersion", "5.5.5.0"
VALUE "FileVersion", "5.6.3.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2024"
VALUE "ProductName", "AyuGram Desktop"
VALUE "ProductVersion", "5.5.5.0"
VALUE "ProductVersion", "5.6.3.0"
END
END
BLOCK "VarFileInfo"

View file

@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
//
VS_VERSION_INFO VERSIONINFO
FILEVERSION 5,5,5,0
PRODUCTVERSION 5,5,5,0
FILEVERSION 5,6,3,0
PRODUCTVERSION 5,6,3,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.5.5.0"
VALUE "FileVersion", "5.6.3.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2024"
VALUE "ProductName", "AyuGram Desktop"
VALUE "ProductVersion", "5.5.5.0"
VALUE "ProductVersion", "5.6.3.0"
END
END
BLOCK "VarFileInfo"

View file

@ -39,6 +39,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/layers/generic_box.h"
#include "ui/text/text_utilities.h"
#include <QtGui/QGuiApplication>
#include <QtGui/QClipboard>
namespace Api {
namespace {
@ -503,11 +506,19 @@ void ActivateBotCommand(ClickHandlerContext context, int row, int column) {
bot->session().attachWebView().open({
.bot = bot,
.context = { .controller = controller },
.button = {.text = button->text, .url = button->data },
.button = { .text = button->text, .url = button->data },
.source = InlineBots::WebViewSourceButton{ .simple = true },
});
}
} break;
case ButtonType::CopyText: {
const auto text = QString::fromUtf8(button->data);
if (!text.isEmpty()) {
QGuiApplication::clipboard()->setText(text);
controller->showToast(tr::lng_text_copied(tr::now));
}
} break;
}
}

View file

@ -275,7 +275,6 @@ void ConfirmSubscriptionBox(
: 0;
state->api->request(
MTPpayments_SendStarsForm(
MTP_flags(0),
MTP_long(formId),
MTP_inputInvoiceChatInviteSubscription(MTP_string(hash)))
).done([=](const MTPpayments_PaymentResult &result) {

View file

@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_updates.h"
#include "apiwrap.h"
#include "base/unixtime.h"
#include "data/components/credits.h"
#include "data/data_channel.h"
#include "data/data_document.h"
#include "data/data_peer.h"
@ -69,10 +70,12 @@ constexpr auto kTransactionsLimit = 100;
}, [](const auto &) {
return PeerId(0);
}).value;
const auto stargift = tl.data().vstargift();
const auto incoming = (int64(tl.data().vstars().v) >= 0);
return Data::CreditsHistoryEntry{
.id = qs(tl.data().vid()),
.title = qs(tl.data().vtitle().value_or_empty()),
.description = qs(tl.data().vdescription().value_or_empty()),
.description = { qs(tl.data().vdescription().value_or_empty()) },
.date = base::unixtime::parse(tl.data().vdate().v),
.photoId = photo ? photo->id : 0,
.extended = std::move(extended),
@ -81,6 +84,9 @@ constexpr auto kTransactionsLimit = 100;
.barePeerId = barePeerId,
.bareGiveawayMsgId = uint64(
tl.data().vgiveaway_post_id().value_or_empty()),
.bareGiftStickerId = (stargift
? owner->processDocument(stargift->data().vsticker())->id
: 0),
.peerType = tl.data().vpeer().match([](const HistoryPeerTL &) {
return Data::CreditsHistoryEntry::PeerType::Peer;
}, [](const MTPDstarsTransactionPeerPlayMarket &) {
@ -104,12 +110,16 @@ constexpr auto kTransactionsLimit = 100;
? base::unixtime::parse(tl.data().vtransaction_date()->v)
: QDateTime(),
.successLink = qs(tl.data().vtransaction_url().value_or_empty()),
.convertStars = int(stargift
? stargift->data().vconvert_stars().v
: 0),
.converted = stargift && incoming,
.reaction = tl.data().is_reaction(),
.refunded = tl.data().is_refund(),
.pending = tl.data().is_pending(),
.failed = tl.data().is_failed(),
.in = (int64(tl.data().vstars().v) >= 0),
.gift = tl.data().is_gift(),
.in = incoming,
.gift = tl.data().is_gift() || stargift.has_value(),
};
}
@ -239,6 +249,8 @@ void CreditsStatus::request(
_peer->isSelf() ? MTP_inputPeerSelf() : _peer->input
)).done([=](const TLResult &result) {
_requestId = 0;
const auto balance = result.data().vbalance().v;
_peer->session().credits().apply(_peer->id, balance);
if (const auto onstack = done) {
onstack(StatusFromTL(result, _peer));
}

View file

@ -37,7 +37,8 @@ MTPVector<MTPDocumentAttribute> ComposeSendingDocumentAttributes(
MTP_int(dimensions.width()),
MTP_int(dimensions.height()),
MTPint(), // preload_prefix_size
MTPdouble())); // video_start_ts
MTPdouble(), // video_start_ts
MTPstring())); // video_codec
} else {
attributes.push_back(MTP_documentAttributeImageSize(
MTP_int(dimensions.width()),

View file

@ -550,6 +550,24 @@ Payments::InvoicePremiumGiftCode PremiumGiftCodeOptions::invoice(
};
}
std::vector<GiftOptionData> PremiumGiftCodeOptions::optionsForPeer() const {
auto result = std::vector<GiftOptionData>();
if (!_optionsForOnePerson.currency.isEmpty()) {
const auto count = int(_optionsForOnePerson.months.size());
result.reserve(count);
for (auto i = 0; i != count; ++i) {
Assert(i < _optionsForOnePerson.totalCosts.size());
result.push_back({
.cost = _optionsForOnePerson.totalCosts[i],
.currency = _optionsForOnePerson.currency,
.months = _optionsForOnePerson.months[i],
});
}
}
return result;
}
Data::PremiumSubscriptionOptions PremiumGiftCodeOptions::options(int amount) {
const auto it = _subscriptionOptions.find(amount);
if (it != end(_subscriptionOptions)) {
@ -571,6 +589,41 @@ Data::PremiumSubscriptionOptions PremiumGiftCodeOptions::options(int amount) {
}
}
auto PremiumGiftCodeOptions::requestStarGifts()
-> rpl::producer<rpl::no_value, QString> {
return [=](auto consumer) {
auto lifetime = rpl::lifetime();
_api.request(MTPpayments_GetStarGifts(
MTP_int(0)
)).done([=](const MTPpayments_StarGifts &result) {
result.match([&](const MTPDpayments_starGifts &data) {
_giftsHash = data.vhash().v;
const auto &list = data.vgifts().v;
const auto session = &_peer->session();
auto gifts = std::vector<StarGift>();
gifts.reserve(list.size());
for (const auto &gift : list) {
if (auto parsed = FromTL(session, gift)) {
gifts.push_back(std::move(*parsed));
}
}
_gifts = std::move(gifts);
}, [&](const MTPDpayments_starGiftsNotModified &) {
});
consumer.put_done();
}).fail([=](const MTP::Error &error) {
consumer.put_error_copy(error.type());
}).send();
return lifetime;
};
}
const std::vector<StarGift> &PremiumGiftCodeOptions::starGifts() const {
return _gifts;
}
int PremiumGiftCodeOptions::giveawayBoostsPerPremium() const {
constexpr auto kFallbackCount = 4;
return _peer->session().appConfig().get<int>(
@ -705,4 +758,56 @@ rpl::producer<DocumentData*> RandomHelloStickerValue(
}) | rpl::take(1) | rpl::map(random));
}
std::optional<StarGift> FromTL(
not_null<Main::Session*> session,
const MTPstarGift &gift) {
const auto &data = gift.data();
const auto document = session->data().processDocument(
data.vsticker());
const auto remaining = data.vavailability_remains();
const auto total = data.vavailability_total();
if (!document->sticker()) {
return {};
}
return StarGift{
.id = uint64(data.vid().v),
.stars = int64(data.vstars().v),
.convertStars = int64(data.vconvert_stars().v),
.document = document,
.limitedLeft = remaining.value_or_empty(),
.limitedCount = total.value_or_empty(),
};
}
std::optional<UserStarGift> FromTL(
not_null<UserData*> to,
const MTPuserStarGift &gift) {
const auto session = &to->session();
const auto &data = gift.data();
auto parsed = FromTL(session, data.vgift());
if (!parsed) {
return {};
}
return UserStarGift{
.gift = std::move(*parsed),
.message = (data.vmessage()
? TextWithEntities{
.text = qs(data.vmessage()->data().vtext()),
.entities = Api::EntitiesFromMTP(
session,
data.vmessage()->data().ventities().v),
}
: TextWithEntities()),
.convertStars = int64(data.vconvert_stars().value_or_empty()),
.fromId = (data.vfrom_id()
? peerFromUser(data.vfrom_id()->v)
: PeerId()),
.messageId = data.vmsg_id().value_or_empty(),
.date = data.vdate().v,
.anonymous = data.is_name_hidden(),
.hidden = data.is_unsaved(),
.mine = to->isSelf(),
};
}
} // namespace Api

View file

@ -67,6 +67,33 @@ struct GiveawayInfo {
}
};
struct GiftOptionData {
int64 cost = 0;
QString currency;
int months = 0;
};
struct StarGift {
uint64 id = 0;
int64 stars = 0;
int64 convertStars = 0;
not_null<DocumentData*> document;
int limitedLeft = 0;
int limitedCount = 0;
};
struct UserStarGift {
StarGift gift;
TextWithEntities message;
int64 convertStars = 0;
PeerId fromId = 0;
MsgId messageId = 0;
TimeId date = 0;
bool anonymous = false;
bool hidden = false;
bool mine = false;
};
class Premium final {
public:
explicit Premium(not_null<ApiWrap*> api);
@ -171,6 +198,7 @@ public:
PremiumGiftCodeOptions(not_null<PeerData*> peer);
[[nodiscard]] rpl::producer<rpl::no_value, QString> request();
[[nodiscard]] std::vector<GiftOptionData> optionsForPeer() const;
[[nodiscard]] Data::PremiumSubscriptionOptions options(int amount);
[[nodiscard]] const std::vector<int> &availablePresets() const;
[[nodiscard]] int monthsFromPreset(int monthsIndex);
@ -187,6 +215,9 @@ public:
[[nodiscard]] int giveawayPeriodMax() const;
[[nodiscard]] bool giveawayGiftsPurchaseAvailable() const;
[[nodiscard]] rpl::producer<rpl::no_value, QString> requestStarGifts();
[[nodiscard]] const std::vector<StarGift> &starGifts() const;
private:
struct Token final {
int users = 0;
@ -206,7 +237,7 @@ private:
base::flat_map<Amount, PremiumSubscriptionOptions> _subscriptionOptions;
struct {
std::vector<int> months;
std::vector<float64> totalCosts;
std::vector<int64> totalCosts;
QString currency;
} _optionsForOnePerson;
@ -214,6 +245,9 @@ private:
base::flat_map<Token, Store> _stores;
int32 _giftsHash = 0;
std::vector<StarGift> _gifts;
MTP::Sender _api;
};
@ -242,4 +276,11 @@ enum class RequirePremiumState {
[[nodiscard]] rpl::producer<DocumentData*> RandomHelloStickerValue(
not_null<Main::Session*> session);
[[nodiscard]] std::optional<StarGift> FromTL(
not_null<Main::Session*> session,
const MTPstarGift &gift);
[[nodiscard]] std::optional<UserStarGift> FromTL(
not_null<UserData*> to,
const MTPuserStarGift &gift);
} // namespace Api

View file

@ -10,10 +10,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "apiwrap.h"
#include "data/data_peer.h"
#include "data/data_photo.h"
#include "data/data_report.h"
#include "data/data_user.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "ui/boxes/report_box.h"
#include "ui/boxes/report_box_graphics.h"
#include "ui/layers/show.h"
namespace Api {
@ -40,15 +41,11 @@ MTPreportReason ReasonToTL(const Ui::ReportReason &reason) {
} // namespace
void SendReport(
std::shared_ptr<Ui::Show> show,
not_null<PeerData*> peer,
Ui::ReportReason reason,
const QString &comment,
std::variant<
v::null_t,
MessageIdsList,
not_null<PhotoData*>,
StoryId> data) {
std::shared_ptr<Ui::Show> show,
not_null<PeerData*> peer,
Ui::ReportReason reason,
const QString &comment,
std::variant<v::null_t, not_null<PhotoData*>> data) {
auto done = [=] {
show->showToast(tr::lng_report_thanks(tr::now));
};
@ -58,18 +55,6 @@ void SendReport(
ReasonToTL(reason),
MTP_string(comment)
)).done(std::move(done)).send();
}, [&](const MessageIdsList &ids) {
auto apiIds = QVector<MTPint>();
apiIds.reserve(ids.size());
for (const auto &fullId : ids) {
apiIds.push_back(MTP_int(fullId.msg));
}
peer->session().api().request(MTPmessages_Report(
peer->input,
MTP_vector<MTPint>(apiIds),
ReasonToTL(reason),
MTP_string(comment)
)).done(std::move(done)).send();
}, [&](not_null<PhotoData*> photo) {
peer->session().api().request(MTPaccount_ReportProfilePhoto(
peer->input,
@ -77,14 +62,93 @@ void SendReport(
ReasonToTL(reason),
MTP_string(comment)
)).done(std::move(done)).send();
}, [&](StoryId id) {
peer->session().api().request(MTPstories_Report(
peer->input,
MTP_vector<MTPint>(1, MTP_int(id)),
ReasonToTL(reason),
MTP_string(comment)
)).done(std::move(done)).send();
});
}
auto CreateReportMessagesOrStoriesCallback(
std::shared_ptr<Ui::Show> show,
not_null<PeerData*> peer)
-> Fn<void(Data::ReportInput, Fn<void(ReportResult)>)> {
using TLChoose = MTPDreportResultChooseOption;
using TLAddComment = MTPDreportResultAddComment;
using TLReported = MTPDreportResultReported;
using Result = ReportResult;
struct State final {
#ifdef _DEBUG
~State() {
qDebug() << "Messages or Stories Report ~State().";
}
#endif
mtpRequestId requestId = 0;
};
const auto state = std::make_shared<State>();
return [=](
Data::ReportInput reportInput,
Fn<void(Result)> done) {
auto apiIds = QVector<MTPint>();
apiIds.reserve(reportInput.ids.size() + reportInput.stories.size());
for (const auto &id : reportInput.ids) {
apiIds.push_back(MTP_int(id));
}
for (const auto &story : reportInput.stories) {
apiIds.push_back(MTP_int(story));
}
const auto received = [=](
const MTPReportResult &result,
mtpRequestId requestId) {
if (state->requestId != requestId) {
return;
}
state->requestId = 0;
done(result.match([&](const TLChoose &data) {
const auto t = qs(data.vtitle());
auto list = Result::Options();
list.reserve(data.voptions().v.size());
for (const auto &tl : data.voptions().v) {
list.emplace_back(Result::Option{
.id = tl.data().voption().v,
.text = qs(tl.data().vtext()),
});
}
return Result{ .options = std::move(list), .title = t };
}, [&](const TLAddComment &data) -> Result {
return {
.commentOption = ReportResult::CommentOption{
.optional = data.is_optional(),
.id = data.voption().v,
}
};
}, [&](const TLReported &data) -> Result {
return { .successful = true };
}));
};
const auto fail = [=](const MTP::Error &error) {
state->requestId = 0;
done({ .error = error.type() });
};
if (!reportInput.stories.empty()) {
state->requestId = peer->session().api().request(
MTPstories_Report(
peer->input,
MTP_vector<MTPint>(apiIds),
MTP_bytes(reportInput.optionId),
MTP_string(reportInput.comment))
).done(received).fail(fail).send();
} else {
state->requestId = peer->session().api().request(
MTPmessages_Report(
peer->input,
MTP_vector<MTPint>(apiIds),
MTP_bytes(reportInput.optionId),
MTP_string(reportInput.comment))
).done(received).fail(fail).send();
}
};
}
} // namespace Api

View file

@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
class HistoryItem;
class PeerData;
class PhotoData;
@ -15,17 +16,41 @@ class Show;
enum class ReportReason;
} // namespace Ui
namespace Data {
struct ReportInput;
} // namespace Data
namespace Api {
struct ReportResult final {
using Id = QByteArray;
struct Option final {
Id id = 0;
QString text;
};
using Options = std::vector<Option>;
Options options;
QString title;
QString error;
QString comment;
struct CommentOption {
bool optional = false;
Id id = 0;
};
std::optional<CommentOption> commentOption;
bool successful = false;
};
void SendReport(
std::shared_ptr<Ui::Show> show,
not_null<PeerData*> peer,
Ui::ReportReason reason,
const QString &comment,
std::variant<
v::null_t,
MessageIdsList,
not_null<PhotoData*>,
StoryId> data);
std::variant<v::null_t, not_null<PhotoData*>> data);
[[nodiscard]] auto CreateReportMessagesOrStoriesCallback(
std::shared_ptr<Ui::Show> show,
not_null<PeerData*> peer)
-> Fn<void(Data::ReportInput, Fn<void(ReportResult)>)>;
} // namespace Api

View file

@ -547,7 +547,7 @@ void SendConfirmedFile(
MTP_flags(Flag::f_document
| (file->spoiler ? Flag::f_spoiler : Flag())),
file->document,
MTPDocument(), // alt_document
MTPVector<MTPDocument>(), // alt_documents
MTPint());
} else if (file->type == SendMediaType::Audio) {
const auto ttlSeconds = file->to.options.ttlSeconds;
@ -572,7 +572,7 @@ void SendConfirmedFile(
| (isVoice ? Flag::f_voice : Flag())
| (ttlSeconds ? Flag::f_ttl_seconds : Flag())),
file->document,
MTPDocument(), // alt_document
MTPVector<MTPDocument>(), // alt_documents
MTP_int(ttlSeconds));
} else {
Unexpected("Type in sendFilesConfirmed.");

View file

@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/premium_limits_box.h"
#include "boxes/premium_preview_box.h"
#include "chat_helpers/emoji_suggestions_widget.h"
#include "chat_helpers/field_autocomplete.h"
#include "chat_helpers/message_field.h"
#include "chat_helpers/tabbed_panel.h"
#include "chat_helpers/tabbed_selector.h"
@ -371,6 +372,14 @@ void EditCaptionBox::StartPhotoEdit(
});
}
void EditCaptionBox::showFinished() {
if (const auto raw = _autocomplete.get()) {
InvokeQueued(raw, [=] {
raw->raise();
});
}
}
void EditCaptionBox::prepare() {
const auto button = addButton(tr::lng_settings_save(), [=] { save(); });
addButton(tr::lng_cancel(), [=] { closeBox(); });
@ -525,6 +534,7 @@ void EditCaptionBox::setupField() {
_field.get(),
Window::GifPauseReason::Layer,
allow);
setupFieldAutocomplete();
Ui::Emoji::SuggestionsController::Init(
getDelegate()->outerContainer(),
_field,
@ -562,6 +572,55 @@ void EditCaptionBox::setupField() {
});
}
void EditCaptionBox::setupFieldAutocomplete() {
const auto parent = getDelegate()->outerContainer();
ChatHelpers::InitFieldAutocomplete(_autocomplete, {
.parent = parent,
.show = _controller->uiShow(),
.field = _field.get(),
.peer = _historyItem->history()->peer,
.features = [=] {
auto result = ChatHelpers::ComposeFeatures();
result.autocompleteCommands = false;
result.suggestStickersByEmoji = false;
return result;
},
});
const auto raw = _autocomplete.get();
const auto scheduled = std::make_shared<bool>();
const auto recountPostponed = [=] {
if (*scheduled) {
return;
}
*scheduled = true;
Ui::PostponeCall(raw, [=] {
*scheduled = false;
auto field = Ui::MapFrom(parent, this, _field->geometry());
_autocomplete->setBoundings(QRect(
field.x() - _field->x(),
st::defaultBox.margin.top(),
width(),
(field.y()
+ st::defaultComposeFiles.caption.textMargins.top()
+ st::defaultComposeFiles.caption.placeholderShift
+ st::defaultComposeFiles.caption.placeholderFont->height
- st::defaultBox.margin.top())));
});
};
for (auto w = (QWidget*)_field.get(); w; w = w->parentWidget()) {
base::install_event_filter(raw, w, [=](not_null<QEvent*> e) {
if (e->type() == QEvent::Move || e->type() == QEvent::Resize) {
recountPostponed();
}
return base::EventFilterResult::Continue;
});
if (w == parent) {
break;
}
}
}
void EditCaptionBox::setInitialText() {
_field->setTextWithTags(
_initialText,

View file

@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace ChatHelpers {
class TabbedPanel;
class FieldAutocomplete;
} // namespace ChatHelpers
namespace Window {
@ -68,6 +69,8 @@ public:
bool invertCaption,
Fn<void()> saved);
void showFinished() override;
protected:
void prepare() override;
void setInnerFocus() override;
@ -81,6 +84,7 @@ private:
void setupEditEventHandler();
void setupPhotoEditorEventHandler();
void setupField();
void setupFieldAutocomplete();
void setupControls();
void setInitialText();
@ -115,6 +119,8 @@ private:
const base::unique_qptr<Ui::InputField> _field;
const base::unique_qptr<Ui::EmojiButton> _emojiToggle;
std::unique_ptr<ChatHelpers::FieldAutocomplete> _autocomplete;
base::unique_qptr<Ui::AbstractSinglePreview> _content;
base::unique_qptr<ChatHelpers::TabbedPanel> _emojiPanel;
base::unique_qptr<QObject> _emojiFilter;

View file

@ -356,11 +356,12 @@ void PrivacyExceptionsBoxController::rowClicked(not_null<PeerListRow*> row) {
auto PrivacyExceptionsBoxController::createRow(not_null<History*> history)
-> std::unique_ptr<Row> {
if (history->peer->isSelf() || history->peer->isRepliesChat()) {
const auto peer = history->peer;
if (peer->isSelf() || peer->isRepliesChat() || peer->isVerifyCodes()) {
return nullptr;
} else if (!history->peer->isUser()
&& !history->peer->isChat()
&& !history->peer->isMegagroup()) {
} else if (!peer->isUser()
&& !peer->isChat()
&& !peer->isMegagroup()) {
return nullptr;
}
auto result = std::make_unique<Row>(history);

View file

@ -131,10 +131,13 @@ ExceptionRow::ExceptionRow(not_null<History*> history) : Row(history) {
}
QString ExceptionRow::generateName() {
return peer()->isSelf()
const auto peer = this->peer();
return peer->isSelf()
? tr::lng_saved_messages(tr::now)
: peer()->isRepliesChat()
: peer->isRepliesChat()
? tr::lng_replies_messages(tr::now)
: peer->isVerifyCodes()
? tr::lng_verification_codes(tr::now)
: Row::generateName();
}
@ -152,10 +155,11 @@ PaintRoundImageCallback ExceptionRow::generatePaintUserpicCallback(
return ForceRoundUserpicCallback(peer);
}
return [=](Painter &p, int x, int y, int outerWidth, int size) mutable {
using namespace Ui;
if (saved) {
Ui::EmptyUserpic::PaintSavedMessages(p, x, y, outerWidth, size);
EmptyUserpic::PaintSavedMessages(p, x, y, outerWidth, size);
} else if (replies) {
Ui::EmptyUserpic::PaintRepliesMessages(p, x, y, outerWidth, size);
EmptyUserpic::PaintRepliesMessages(p, x, y, outerWidth, size);
} else {
peer->paintUserpicLeft(p, userpic, x, y, outerWidth, size);
}

View file

@ -122,9 +122,11 @@ void FilterChatsPreview::paintEvent(QPaintEvent *e) {
top += st.height;
}
for (auto &[history, userpic, name, button] : _removePeer) {
const auto savedMessages = history->peer->isSelf();
const auto repliesMessages = history->peer->isRepliesChat();
if (savedMessages || repliesMessages) {
const auto peer = history->peer;
const auto savedMessages = peer->isSelf();
const auto repliesMessages = peer->isRepliesChat();
const auto verifyCodes = peer->isVerifyCodes();
if (savedMessages || repliesMessages || verifyCodes) {
if (savedMessages) {
Ui::EmptyUserpic::PaintSavedMessages(
p,
@ -132,13 +134,21 @@ void FilterChatsPreview::paintEvent(QPaintEvent *e) {
top + iconTop,
width(),
st.photoSize);
} else {
} else if (repliesMessages) {
Ui::EmptyUserpic::PaintRepliesMessages(
p,
iconLeft,
top + iconTop,
width(),
st.photoSize);
} else {
history->peer->paintUserpicLeft(
p,
userpic,
iconLeft,
top + iconTop,
width(),
st.photoSize);
}
p.setPen(st::contactsNameFg);
p.drawTextLeft(
@ -147,7 +157,9 @@ void FilterChatsPreview::paintEvent(QPaintEvent *e) {
width(),
(savedMessages
? tr::lng_saved_messages(tr::now)
: tr::lng_replies_messages(tr::now)));
: repliesMessages
? tr::lng_replies_messages(tr::now)
: tr::lng_verification_codes(tr::now)));
} else {
history->peer->paintUserpicLeft(
p,

View file

@ -337,12 +337,13 @@ PaintRoundImageCallback ChatRow::generatePaintUserpicCallback(
int y,
int outerWidth,
int size) mutable {
using namespace Ui;
if (forceRound && peer->isForum()) {
ForceRoundUserpicCallback(peer)(p, x, y, outerWidth, size);
} else if (saved) {
Ui::EmptyUserpic::PaintSavedMessages(p, x, y, outerWidth, size);
EmptyUserpic::PaintSavedMessages(p, x, y, outerWidth, size);
} else if (replies) {
Ui::EmptyUserpic::PaintRepliesMessages(p, x, y, outerWidth, size);
EmptyUserpic::PaintRepliesMessages(p, x, y, outerWidth, size);
} else {
peer->paintUserpicLeft(p, userpic, x, y, outerWidth, size);
}

View file

@ -64,11 +64,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace {
constexpr auto kUserpicsMax = size_t(3);
using GiftOption = Data::PremiumSubscriptionOption;
using GiftOptions = Data::PremiumSubscriptionOptions;
[[nodiscard]] QString CreateMessageLink(
not_null<Main::Session*> session,
PeerId peerId,
@ -87,638 +82,6 @@ using GiftOptions = Data::PremiumSubscriptionOptions;
return QString();
};
GiftOptions GiftOptionFromTL(const MTPDuserFull &data) {
auto result = GiftOptions();
const auto gifts = data.vpremium_gifts();
if (!gifts) {
return result;
}
result = Api::PremiumSubscriptionOptionsFromTL(gifts->v);
for (auto &option : result) {
option.costPerMonth = tr::lng_premium_gift_per(
tr::now,
lt_cost,
option.costPerMonth);
}
return result;
}
[[nodiscard]] Fn<TextWithEntities(TextWithEntities)> BoostsForGiftText(
const std::vector<not_null<UserData*>> users) {
Expects(!users.empty());
const auto session = &users.front()->session();
const auto emoji = Ui::Text::SingleCustomEmoji(
session->data().customEmojiManager().registerInternalEmoji(
st::premiumGiftsBoostIcon,
QMargins(0, st::premiumGiftsUserpicBadgeInner, 0, 0),
false));
return [=, count = users.size()](TextWithEntities text) {
text.append('\n');
text.append('\n');
text.append(tr::lng_premium_gifts_about_reward(
tr::now,
lt_count,
count * BoostsForGift(session),
lt_emoji,
emoji,
Ui::Text::RichLangValue));
return text;
};
}
using TagUser1 = lngtag_user;
using TagUser2 = lngtag_second_user;
using TagUser3 = lngtag_name;
[[nodiscard]] rpl::producer<TextWithEntities> ComplexAboutLabel(
const std::vector<not_null<UserData*>> &users,
tr::phrase<TagUser1> phrase1,
tr::phrase<TagUser1, TagUser2> phrase2,
tr::phrase<TagUser1, TagUser2, TagUser3> phrase3,
tr::phrase<lngtag_count, TagUser1, TagUser2, TagUser3> phraseMore) {
Expects(!users.empty());
const auto count = users.size();
const auto nameValue = [&](not_null<UserData*> user) {
return user->session().changes().peerFlagsValue(
user,
Data::PeerUpdate::Flag::Name
) | rpl::map([=] { return TextWithEntities{ user->firstName }; });
};
if (count == 1) {
return phrase1(
lt_user,
nameValue(users.front()),
Ui::Text::RichLangValue);
} else if (count == 2) {
return phrase2(
lt_user,
nameValue(users.front()),
lt_second_user,
nameValue(users[1]),
Ui::Text::RichLangValue);
} else if (count == 3) {
return phrase3(
lt_user,
nameValue(users.front()),
lt_second_user,
nameValue(users[1]),
lt_name,
nameValue(users[2]),
Ui::Text::RichLangValue);
} else {
return phraseMore(
lt_count,
rpl::single(count - kUserpicsMax) | tr::to_count(),
lt_user,
nameValue(users.front()),
lt_second_user,
nameValue(users[1]),
lt_name,
nameValue(users[2]),
Ui::Text::RichLangValue);
}
}
[[nodiscard]] not_null<Ui::RpWidget*> CircleBadge(
not_null<Ui::RpWidget*> parent,
const QString &text) {
const auto widget = Ui::CreateChild<Ui::RpWidget>(parent.get());
const auto full = Rect(st::premiumGiftsUserpicBadgeSize);
const auto inner = full - Margins(st::premiumGiftsUserpicBadgeInner);
auto gradient = QLinearGradient(
QPointF(0, full.height()),
QPointF(full.width(), 0));
gradient.setStops(Ui::Premium::GiftGradientStops());
widget->paintRequest(
) | rpl::start_with_next([=] {
auto p = QPainter(widget);
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(st::boxBg);
p.drawEllipse(full);
p.setPen(Qt::NoPen);
p.setBrush(gradient);
p.drawEllipse(inner);
p.setFont(st::premiumGiftsUserpicBadgeFont);
p.setPen(st::premiumButtonFg);
p.drawText(full, text, style::al_center);
}, widget->lifetime());
widget->resize(full.size());
return widget;
}
[[nodiscard]] not_null<Ui::RpWidget*> UserpicsContainer(
not_null<Ui::RpWidget*> parent,
std::vector<not_null<UserData*>> users) {
Expects(!users.empty());
if (users.size() == 1) {
const auto userpic = Ui::CreateChild<Ui::UserpicButton>(
parent.get(),
users.front(),
st::defaultUserpicButton);
userpic->setAttribute(Qt::WA_TransparentForMouseEvents);
return userpic;
}
const auto &singleSize = st::defaultUserpicButton.size;
const auto container = Ui::CreateChild<Ui::RpWidget>(parent.get());
const auto single = singleSize.width();
const auto shift = single - st::boostReplaceUserpicsShift;
const auto maxWidth = users.size() * (single - shift) + shift;
container->resize(maxWidth, singleSize.height());
container->setAttribute(Qt::WA_TransparentForMouseEvents);
const auto diff = (single - st::premiumGiftsUserpicButton.size.width())
/ 2;
for (auto i = 0; i < users.size(); i++) {
const auto bg = Ui::CreateChild<Ui::RpWidget>(container);
bg->resize(singleSize);
bg->paintRequest(
) | rpl::start_with_next([=] {
auto p = QPainter(bg);
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(st::boxBg);
p.drawEllipse(bg->rect());
}, bg->lifetime());
bg->moveToLeft(std::max(0, i * (single - shift)), 0);
const auto userpic = Ui::CreateChild<Ui::UserpicButton>(
bg,
users[i],
st::premiumGiftsUserpicButton);
userpic->moveToLeft(diff, diff);
}
return container;
}
void GiftBox(
not_null<Ui::GenericBox*> box,
not_null<Window::SessionController*> controller,
not_null<UserData*> user,
GiftOptions options) {
const auto boxWidth = st::boxWideWidth;
box->setWidth(boxWidth);
box->setNoContentMargin(true);
const auto buttonsParent = box->verticalLayout().get();
struct State {
rpl::event_stream<QString> buttonText;
};
const auto state = box->lifetime().make_state<State>();
const auto userpicPadding = st::premiumGiftUserpicPadding;
const auto top = box->addRow(object_ptr<Ui::FixedHeightWidget>(
buttonsParent,
userpicPadding.top()
+ userpicPadding.bottom()
+ st::defaultUserpicButton.size.height()));
using ColoredMiniStars = Ui::Premium::ColoredMiniStars;
const auto stars = box->lifetime().make_state<ColoredMiniStars>(
top,
true);
const auto userpic = UserpicsContainer(top, { user });
userpic->setAttribute(Qt::WA_TransparentForMouseEvents);
top->widthValue(
) | rpl::start_with_next([=](int width) {
userpic->moveToLeft(
(width - userpic->width()) / 2,
userpicPadding.top());
const auto center = top->rect().center();
const auto size = QSize(
userpic->width() * Ui::Premium::MiniStars::kSizeFactor,
userpic->height());
const auto ministarsRect = QRect(
QPoint(center.x() - size.width(), center.y() - size.height()),
QPoint(center.x() + size.width(), center.y() + size.height()));
stars->setPosition(ministarsRect.topLeft());
stars->setSize(ministarsRect.size());
}, userpic->lifetime());
top->paintRequest(
) | rpl::start_with_next([=](const QRect &r) {
auto p = QPainter(top);
p.fillRect(r, Qt::transparent);
stars->paint(p);
}, top->lifetime());
const auto close = Ui::CreateChild<Ui::IconButton>(
buttonsParent,
st::infoTopBarClose);
close->setClickedCallback([=] { box->closeBox(); });
buttonsParent->widthValue(
) | rpl::start_with_next([=](int width) {
close->moveToRight(0, 0, width);
}, close->lifetime());
// Header.
const auto &padding = st::premiumGiftAboutPadding;
const auto available = boxWidth - padding.left() - padding.right();
const auto &stTitle = st::premiumPreviewAboutTitle;
auto titleLabel = object_ptr<Ui::FlatLabel>(
box,
tr::lng_premium_gift_title(),
stTitle);
titleLabel->resizeToWidth(available);
box->addRow(
object_ptr<Ui::CenterWrap<Ui::FlatLabel>>(
box,
std::move(titleLabel)),
st::premiumGiftTitlePadding);
auto textLabel = Ui::CreateLabelWithCustomEmoji(
box,
tr::lng_premium_gift_about(
lt_user,
user->session().changes().peerFlagsValue(
user,
Data::PeerUpdate::Flag::Name
) | rpl::map([=] { return TextWithEntities{ user->firstName }; }),
Ui::Text::RichLangValue
) | rpl::map(
BoostsForGiftText({ user })
),
{ .session = &user->session() },
st::premiumPreviewAbout);
textLabel->setTextColorOverride(stTitle.textFg->c);
textLabel->resizeToWidth(available);
box->addRow(
object_ptr<Ui::CenterWrap<Ui::FlatLabel>>(box, std::move(textLabel)),
padding);
// List.
const auto group = std::make_shared<Ui::RadiobuttonGroup>();
const auto groupValueChangedCallback = [=](int value) {
Expects(value < options.size() && value >= 0);
auto text = tr::lng_premium_gift_button(
tr::now,
lt_cost,
options[value].costTotal);
state->buttonText.fire(std::move(text));
};
group->setChangedCallback(groupValueChangedCallback);
Ui::Premium::AddGiftOptions(
buttonsParent,
group,
options,
st::premiumGiftOption);
// Footer.
auto terms = object_ptr<Ui::FlatLabel>(
box,
tr::lng_premium_gift_terms(
lt_link,
tr::lng_premium_gift_terms_link(
) | rpl::map([=](const QString &t) {
return Ui::Text::Link(t, 1);
}),
Ui::Text::WithEntities),
st::premiumGiftTerms);
terms->setLink(1, std::make_shared<LambdaClickHandler>([=] {
box->closeBox();
Settings::ShowPremium(&user->session(), QString());
}));
terms->resizeToWidth(available);
box->addRow(
object_ptr<Ui::CenterWrap<Ui::FlatLabel>>(box, std::move(terms)),
st::premiumGiftTermsPadding);
// Button.
const auto &stButton = st::premiumGiftBox;
box->setStyle(stButton);
auto raw = Settings::CreateSubscribeButton({
controller,
box,
[] { return u"gift"_q; },
state->buttonText.events(),
Ui::Premium::GiftGradientStops(),
[=] {
const auto value = group->current();
return (value < options.size() && value >= 0)
? options[value].botUrl
: QString();
},
});
auto button = object_ptr<Ui::GradientButton>::fromRaw(raw);
button->resizeToWidth(boxWidth - rect::m::sum::h(stButton.buttonPadding));
box->setShowFinishedCallback([raw = button.data()] {
raw->startGlareAnimation();
});
box->addButton(std::move(button));
groupValueChangedCallback(0);
Data::PeerPremiumValue(
user
) | rpl::skip(1) | rpl::start_with_next([=] {
box->closeBox();
}, box->lifetime());
}
void GiftsBox(
not_null<Ui::GenericBox*> box,
not_null<Window::SessionController*> controller,
std::vector<not_null<UserData*>> users,
not_null<Api::PremiumGiftCodeOptions*> api,
const QString &ref) {
Expects(!users.empty());
const auto boxWidth = st::boxWideWidth;
box->setWidth(boxWidth);
box->setNoContentMargin(true);
const auto buttonsParent = box->verticalLayout().get();
const auto session = &users.front()->session();
struct State {
rpl::event_stream<QString> buttonText;
rpl::variable<bool> confirmButtonBusy = false;
rpl::variable<bool> isPaymentComplete = false;
};
const auto state = box->lifetime().make_state<State>();
const auto userpicPadding = st::premiumGiftUserpicPadding;
const auto top = box->addRow(object_ptr<Ui::FixedHeightWidget>(
buttonsParent,
userpicPadding.top()
+ userpicPadding.bottom()
+ st::defaultUserpicButton.size.height()));
using ColoredMiniStars = Ui::Premium::ColoredMiniStars;
const auto stars = box->lifetime().make_state<ColoredMiniStars>(
top,
true);
const auto maxWithUserpic = std::min(users.size(), kUserpicsMax);
const auto userpics = UserpicsContainer(
top,
{ users.begin(), users.begin() + maxWithUserpic });
top->widthValue(
) | rpl::start_with_next([=](int width) {
userpics->moveToLeft(
(width - userpics->width()) / 2,
userpicPadding.top());
const auto center = top->rect().center();
const auto size = QSize(
userpics->width() * Ui::Premium::MiniStars::kSizeFactor,
userpics->height());
const auto ministarsRect = QRect(
QPoint(center.x() - size.width(), center.y() - size.height()),
QPoint(center.x() + size.width(), center.y() + size.height()));
stars->setPosition(ministarsRect.topLeft());
stars->setSize(ministarsRect.size());
}, userpics->lifetime());
if (const auto rest = users.size() - maxWithUserpic; rest > 0) {
const auto badge = CircleBadge(
userpics,
QChar('+') + QString::number(rest));
badge->moveToRight(0, userpics->height() - badge->height());
}
top->paintRequest(
) | rpl::start_with_next([=](const QRect &r) {
auto p = QPainter(top);
p.fillRect(r, Qt::transparent);
stars->paint(p);
}, top->lifetime());
const auto close = Ui::CreateChild<Ui::IconButton>(
buttonsParent,
st::infoTopBarClose);
close->setClickedCallback([=] { box->closeBox(); });
buttonsParent->widthValue(
) | rpl::start_with_next([=](int width) {
close->moveToRight(0, 0, width);
}, close->lifetime());
// Header.
const auto &padding = st::premiumGiftAboutPadding;
const auto available = boxWidth - padding.left() - padding.right();
const auto &stTitle = st::premiumPreviewAboutTitle;
auto titleLabel = object_ptr<Ui::FlatLabel>(
box,
rpl::conditional(
state->isPaymentComplete.value(),
tr::lng_premium_gifts_about_paid_title(),
tr::lng_premium_gift_title()),
stTitle);
titleLabel->resizeToWidth(available);
box->addRow(
object_ptr<Ui::CenterWrap<Ui::FlatLabel>>(
box,
std::move(titleLabel)),
st::premiumGiftTitlePadding);
// About.
{
auto text = rpl::conditional(
state->isPaymentComplete.value(),
ComplexAboutLabel(
users,
tr::lng_premium_gifts_about_paid1,
tr::lng_premium_gifts_about_paid2,
tr::lng_premium_gifts_about_paid3,
tr::lng_premium_gifts_about_paid_more
) | rpl::map([count = users.size()](TextWithEntities text) {
text.append('\n');
text.append('\n');
text.append(tr::lng_premium_gifts_about_paid_below(
tr::now,
lt_count,
float64(count),
Ui::Text::RichLangValue));
return text;
}),
ComplexAboutLabel(
users,
tr::lng_premium_gifts_about_user1,
tr::lng_premium_gifts_about_user2,
tr::lng_premium_gifts_about_user3,
tr::lng_premium_gifts_about_user_more
) | rpl::map(BoostsForGiftText(users))
);
const auto label = box->addRow(
object_ptr<Ui::CenterWrap<Ui::FlatLabel>>(
box,
Ui::CreateLabelWithCustomEmoji(
box,
std::move(text),
{ .session = session },
st::premiumPreviewAbout)),
padding)->entity();
label->setTextColorOverride(stTitle.textFg->c);
label->resizeToWidth(available);
}
// List.
const auto optionsContainer = buttonsParent->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
buttonsParent,
object_ptr<Ui::VerticalLayout>(buttonsParent)));
const auto options = api->options(users.size());
const auto group = std::make_shared<Ui::RadiobuttonGroup>();
const auto groupValueChangedCallback = [=](int value) {
Expects(value < options.size() && value >= 0);
auto text = tr::lng_premium_gift_button(
tr::now,
lt_cost,
options[value].costTotal);
state->buttonText.fire(std::move(text));
};
group->setChangedCallback(groupValueChangedCallback);
Ui::Premium::AddGiftOptions(
optionsContainer->entity(),
group,
options,
st::premiumGiftOption);
optionsContainer->toggleOn(
state->isPaymentComplete.value() | rpl::map(!rpl::mappers::_1),
anim::type::instant);
// Summary.
{
{
// Will be hidden after payment.
const auto content = optionsContainer->entity();
Ui::AddSkip(content);
Ui::AddDivider(content);
Ui::AddSkip(content);
Ui::AddSubsectionTitle(
content,
tr::lng_premium_gifts_summary_subtitle());
}
const auto content = box->addRow(
object_ptr<Ui::VerticalLayout>(box),
{});
auto buttonCallback = [=](PremiumFeature section) {
stars->setPaused(true);
const auto previewBoxShown = [=](
not_null<Ui::BoxContent*> previewBox) {
previewBox->boxClosing(
) | rpl::start_with_next(crl::guard(box, [=] {
stars->setPaused(false);
}), previewBox->lifetime());
};
ShowPremiumPreviewBox(
controller->uiShow(),
section,
previewBoxShown,
true);
};
Settings::AddSummaryPremium(
content,
controller,
ref,
std::move(buttonCallback));
}
// Footer.
{
box->addRow(
object_ptr<Ui::DividerLabel>(
box,
object_ptr<Ui::FlatLabel>(
box,
tr::lng_premium_gifts_terms(
lt_link,
tr::lng_payments_terms_link(
) | rpl::map([](const QString &t) {
using namespace Ui::Text;
return Link(t, u"https://telegram.org/tos"_q);
}),
lt_policy,
tr::lng_premium_gifts_terms_policy(
) | rpl::map([](const QString &t) {
using namespace Ui::Text;
return Link(t, u"https://telegram.org/privacy"_q);
}),
Ui::Text::RichLangValue),
st::premiumGiftTerms),
st::defaultBoxDividerLabelPadding),
{});
}
// Button.
const auto &stButton = st::premiumGiftBox;
box->setStyle(stButton);
auto raw = Settings::CreateSubscribeButton({
controller,
box,
[=] { return ref; },
rpl::combine(
state->buttonText.events(),
state->confirmButtonBusy.value(),
state->isPaymentComplete.value()
) | rpl::map([](const QString &text, bool busy, bool paid) {
return busy
? QString()
: paid
? tr::lng_close(tr::now)
: text;
}),
Ui::Premium::GiftGradientStops(),
});
raw->setClickedCallback([=] {
if (state->confirmButtonBusy.current()) {
return;
}
if (state->isPaymentComplete.current()) {
return box->closeBox();
}
auto invoice = api->invoice(
users.size(),
api->monthsFromPreset(group->current()));
invoice.purpose = Payments::InvoicePremiumGiftCodeUsers{ users };
state->confirmButtonBusy = true;
const auto show = box->uiShow();
const auto weak = Ui::MakeWeak(box.get());
const auto done = [=](Payments::CheckoutResult result) {
if (const auto strong = weak.data()) {
strong->window()->setFocus();
state->confirmButtonBusy = false;
if (result == Payments::CheckoutResult::Paid) {
state->isPaymentComplete = true;
Ui::StartFireworks(box->parentWidget());
}
}
};
Payments::CheckoutProcess::Start(std::move(invoice), done);
});
{
using namespace Info::Statistics;
const auto loadingAnimation = InfiniteRadialAnimationWidget(
raw,
raw->height() / 2);
AddChildToWidgetCenter(raw, loadingAnimation);
loadingAnimation->showOn(state->confirmButtonBusy.value());
}
auto button = object_ptr<Ui::GradientButton>::fromRaw(raw);
button->resizeToWidth(boxWidth - rect::m::sum::h(stButton.buttonPadding));
box->setShowFinishedCallback([raw = button.data()] {
raw->startGlareAnimation();
});
box->addButton(std::move(button));
groupValueChangedCallback(0);
}
[[nodiscard]] Data::GiftCodeLink MakeGiftCodeLink(
not_null<Main::Session*> session,
const QString &slug) {
@ -793,16 +156,55 @@ void GiftsBox(
return result;
}
[[nodiscard]] object_ptr<Ui::RpWidget> MakeHiddenPeerTableValue(
not_null<QWidget*> parent,
not_null<Window::SessionNavigation*> controller) {
auto result = object_ptr<Ui::RpWidget>(parent);
const auto raw = result.data();
const auto &st = st::giveawayGiftCodeUserpic;
raw->resize(raw->width(), st.photoSize);
const auto userpic = Ui::CreateChild<Ui::RpWidget>(raw);
const auto usize = st.photoSize;
userpic->resize(usize, usize);
userpic->paintRequest() | rpl::start_with_next([=] {
auto p = QPainter(userpic);
Ui::EmptyUserpic::PaintHiddenAuthor(p, 0, 0, usize, usize);
}, userpic->lifetime());
const auto label = Ui::CreateChild<Ui::FlatLabel>(
raw,
tr::lng_gift_from_hidden(),
st::giveawayGiftCodeValue);
raw->widthValue(
) | rpl::start_with_next([=](int width) {
const auto position = st::giveawayGiftCodeNamePosition;
label->resizeToNaturalWidth(width - position.x());
label->moveToLeft(position.x(), position.y(), width);
const auto top = (raw->height() - userpic->height()) / 2;
userpic->moveToLeft(0, top, width);
}, label->lifetime());
userpic->setAttribute(Qt::WA_TransparentForMouseEvents);
label->setAttribute(Qt::WA_TransparentForMouseEvents);
label->setTextColorOverride(st::windowFg->c);
return result;
}
void AddTableRow(
not_null<Ui::TableLayout*> table,
rpl::producer<QString> label,
object_ptr<Ui::RpWidget> value,
style::margins valueMargins) {
table->addRow(
object_ptr<Ui::FlatLabel>(
table,
std::move(label),
st::giveawayGiftCodeLabel),
(label
? object_ptr<Ui::FlatLabel>(
table,
std::move(label),
st::giveawayGiftCodeLabel)
: object_ptr<Ui::FlatLabel>(nullptr)),
std::move(value),
st::giveawayGiftCodeLabelMargin,
valueMargins);
@ -957,180 +359,6 @@ void ShowAlreadyPremiumToast(
} // namespace
GiftPremiumValidator::GiftPremiumValidator(
not_null<Window::SessionController*> controller)
: _controller(controller)
, _api(&_controller->session().mtp()) {
}
void GiftPremiumValidator::cancel() {
_requestId = 0;
}
void GiftPremiumValidator::showChoosePeerBox(const QString &ref) {
if (_manyGiftsLifetime) {
return;
}
using namespace Api;
const auto api = _manyGiftsLifetime.make_state<PremiumGiftCodeOptions>(
_controller->session().user());
const auto show = _controller->uiShow();
api->request(
) | rpl::start_with_error_done([=](const QString &error) {
show->showToast(error);
}, [=] {
const auto maxAmount = *ranges::max_element(api->availablePresets());
class Controller final : public ContactsBoxController {
public:
Controller(
not_null<Main::Session*> session,
Fn<bool(int)> checkErrorCallback)
: ContactsBoxController(session)
, _checkErrorCallback(std::move(checkErrorCallback)) {
}
protected:
std::unique_ptr<PeerListRow> createRow(
not_null<UserData*> user) override {
if (user->isSelf()
|| user->isBot()
|| user->isServiceUser()
|| user->isInaccessible()) {
return nullptr;
}
return ContactsBoxController::createRow(user);
}
void rowClicked(not_null<PeerListRow*> row) override {
const auto checked = !row->checked();
if (checked
&& _checkErrorCallback
&& _checkErrorCallback(
delegate()->peerListSelectedRowsCount())) {
return;
}
delegate()->peerListSetRowChecked(row, checked);
}
private:
const Fn<bool(int)> _checkErrorCallback;
};
auto initBox = [=](not_null<PeerListBox*> peersBox) {
const auto ignoreClose = peersBox->lifetime().make_state<bool>(0);
auto process = [=] {
const auto selected = peersBox->collectSelectedRows();
const auto users = ranges::views::all(
selected
) | ranges::views::transform([](not_null<PeerData*> p) {
return p->asUser();
}) | ranges::views::filter([](UserData *u) -> bool {
return u;
}) | ranges::to<std::vector<not_null<UserData*>>>();
if (users.empty()) {
show->showToast(
tr::lng_settings_gift_premium_choose(tr::now));
return;
}
const auto giftBox = show->show(
Box(GiftsBox, _controller, users, api, ref));
giftBox->boxClosing(
) | rpl::start_with_next([=] {
_manyGiftsLifetime.destroy();
}, giftBox->lifetime());
(*ignoreClose) = true;
peersBox->closeBox();
};
peersBox->setTitle(tr::lng_premium_gift_title());
peersBox->addButton(
tr::lng_settings_gift_premium_users_confirm(),
std::move(process));
peersBox->addButton(tr::lng_cancel(), [=] {
peersBox->closeBox();
});
peersBox->boxClosing(
) | rpl::start_with_next([=] {
if (!(*ignoreClose)) {
_manyGiftsLifetime.destroy();
}
}, peersBox->lifetime());
};
auto listController = std::make_unique<Controller>(
&_controller->session(),
[=](int count) {
if (count <= maxAmount) {
return false;
}
show->showToast(tr::lng_settings_gift_premium_users_error(
tr::now,
lt_count,
maxAmount));
return true;
});
show->showBox(
Box<PeerListBox>(
std::move(listController),
std::move(initBox)),
Ui::LayerOption::KeepOther);
}, _manyGiftsLifetime);
}
void GiftPremiumValidator::showChosenPeerBox(
not_null<UserData*> user,
const QString &ref) {
if (_manyGiftsLifetime) {
return;
}
using namespace Api;
const auto api = _manyGiftsLifetime.make_state<PremiumGiftCodeOptions>(
_controller->session().user());
const auto show = _controller->uiShow();
api->request(
) | rpl::start_with_error_done([=](const QString &error) {
show->showToast(error);
}, [=] {
const auto users = std::vector<not_null<UserData*>>{ user };
const auto giftBox = show->show(
Box(GiftsBox, _controller, users, api, ref));
giftBox->boxClosing(
) | rpl::start_with_next([=] {
_manyGiftsLifetime.destroy();
}, giftBox->lifetime());
}, _manyGiftsLifetime);
}
void GiftPremiumValidator::showBox(not_null<UserData*> user) {
if (_requestId) {
return;
}
_requestId = _api.request(MTPusers_GetFullUser(
user->inputUser
)).done([=](const MTPusers_UserFull &result) {
if (!_requestId) {
// Canceled.
return;
}
_requestId = 0;
// _controller->api().processFullPeer(peer, result);
_controller->session().data().processUsers(result.data().vusers());
_controller->session().data().processChats(result.data().vchats());
const auto &fullUser = result.data().vfull_user().data();
auto options = GiftOptionFromTL(fullUser);
if (!options.empty()) {
_controller->show(
Box(GiftBox, _controller, user, std::move(options)));
}
}).fail([=] {
_requestId = 0;
}).send();
}
rpl::producer<QString> GiftDurationValue(int months) {
return GiftDurationPhrase(months)(
lt_count,
@ -1708,6 +936,77 @@ void ResolveGiveawayInfo(
crl::guard(controller, show));
}
void AddStarGiftTable(
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);
const auto peerId = PeerId(entry.barePeerId);
if (peerId) {
AddTableRow(
table,
tr::lng_credits_box_history_entry_peer_in(),
controller,
peerId);
} else {
AddTableRow(
table,
tr::lng_credits_box_history_entry_peer_in(),
MakeHiddenPeerTableValue(table, controller),
st::giveawayGiftCodePeerMargin);
}
if (!entry.date.isNull()) {
AddTableRow(
table,
tr::lng_gift_link_label_date(),
rpl::single(Ui::Text::WithEntities(langDateTime(entry.date))));
}
if (entry.limitedCount > 0) {
auto amount = rpl::single(TextWithEntities{
QString::number(entry.limitedCount)
});
AddTableRow(
table,
tr::lng_gift_availability(),
((entry.limitedLeft > 0)
? tr::lng_gift_availability_left(
lt_count,
rpl::single(entry.limitedLeft * 1.),
lt_amount,
std::move(amount),
Ui::Text::WithEntities)
: tr::lng_gift_availability_none(
lt_amount,
std::move(amount),
Ui::Text::WithEntities)));
}
if (!entry.description.empty()) {
const auto session = &controller->session();
const auto makeContext = [=](Fn<void()> update) {
return Core::MarkedTextContext{
.session = session,
.customEmojiRepaint = std::move(update),
};
};
auto label = object_ptr<Ui::FlatLabel>(
table,
rpl::single(entry.description),
st::giveawayGiftMessage,
st::defaultPopupMenu,
makeContext);
label->setSelectable(true);
table->addRow(
nullptr,
std::move(label),
st::giveawayGiftCodeLabelMargin,
st::giveawayGiftCodeValueMargin);
}
}
void AddCreditsHistoryEntryTable(
not_null<Window::SessionNavigation*> controller,
not_null<Ui::VerticalLayout*> container,

View file

@ -9,8 +9,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "mtproto/sender.h"
class UserData;
namespace Api {
struct GiftCode;
} // namespace Api
@ -29,29 +27,9 @@ class VerticalLayout;
} // namespace Ui
namespace Window {
class SessionController;
class SessionNavigation;
} // namespace Window
class GiftPremiumValidator final {
public:
GiftPremiumValidator(not_null<Window::SessionController*> controller);
void showBox(not_null<UserData*> user);
void showChoosePeerBox(const QString &ref);
void showChosenPeerBox(not_null<UserData*> user, const QString &ref);
void cancel();
private:
const not_null<Window::SessionController*> _controller;
MTP::Sender _api;
mtpRequestId _requestId = 0;
rpl::lifetime _manyGiftsLifetime;
};
[[nodiscard]] rpl::producer<QString> GiftDurationValue(int months);
[[nodiscard]] QString GiftDuration(int months);
@ -76,6 +54,10 @@ void ResolveGiveawayInfo(
std::optional<Data::GiveawayStart> start,
std::optional<Data::GiveawayResults> results);
void AddStarGiftTable(
not_null<Window::SessionNavigation*> controller,
not_null<Ui::VerticalLayout*> container,
const Data::CreditsHistoryEntry &entry);
void AddCreditsHistoryEntryTable(
not_null<Window::SessionNavigation*> controller,
not_null<Ui::VerticalLayout*> container,

View file

@ -447,6 +447,8 @@ void PeerListBox::addSelectItem(
? tr::lng_saved_short(tr::now)
: (respect && peer->isRepliesChat())
? tr::lng_replies_messages(tr::now)
: (respect && peer->isVerifyCodes())
? tr::lng_verification_codes(tr::now)
: peer->shortName();
addSelectItem(
peer->id.value,
@ -625,6 +627,8 @@ void PeerListRow::refreshName(const style::PeerListItem &st) {
? tr::lng_saved_messages(tr::now)
: _isRepliesMessagesChat
? tr::lng_replies_messages(tr::now)
: _isVerifyCodesChat
? tr::lng_verification_codes(tr::now)
: generateName();
_name.setText(st.nameStyle, text, Ui::NameTextOptions());
}
@ -695,6 +699,8 @@ QString PeerListRow::generateShortName() {
? tr::lng_saved_short(tr::now)
: _isRepliesMessagesChat
? tr::lng_replies_messages(tr::now)
: _isVerifyCodesChat
? tr::lng_verification_codes(tr::now)
: peer()->shortName();
}
@ -715,10 +721,11 @@ PaintRoundImageCallback PeerListRow::generatePaintUserpicCallback(
return ForceRoundUserpicCallback(peer);
}
return [=](Painter &p, int x, int y, int outerWidth, int size) mutable {
using namespace Ui;
if (saved) {
Ui::EmptyUserpic::PaintSavedMessages(p, x, y, outerWidth, size);
EmptyUserpic::PaintSavedMessages(p, x, y, outerWidth, size);
} else if (replies) {
Ui::EmptyUserpic::PaintRepliesMessages(p, x, y, outerWidth, size);
EmptyUserpic::PaintRepliesMessages(p, x, y, outerWidth, size);
} else {
peer->paintUserpicLeft(p, userpic, x, y, outerWidth, size);
}
@ -757,12 +764,14 @@ int PeerListRow::paintNameIconGetWidth(
int availableWidth,
int outerWidth,
bool selected) {
if (special()
if (_skipPeerBadge
|| special()
|| !_savedMessagesStatus.isEmpty()
|| _isRepliesMessagesChat) {
|| _isRepliesMessagesChat
|| _isVerifyCodesChat) {
return 0;
}
return _bagde.drawGetWidth(
return _badge.drawGetWidth(
p,
QRect(
nameLeft,
@ -874,12 +883,13 @@ void PeerListRow::paintDisabledCheckUserpic(
auto iconBorderPen = st.checkbox.check.border->p;
iconBorderPen.setWidth(st.checkbox.selectWidth);
const auto size = userpicRadius * 2;
if (!_savedMessagesStatus.isEmpty()) {
Ui::EmptyUserpic::PaintSavedMessages(p, userpicLeft, userpicTop, outerWidth, userpicRadius * 2);
Ui::EmptyUserpic::PaintSavedMessages(p, userpicLeft, userpicTop, outerWidth, size);
} else if (_isRepliesMessagesChat) {
Ui::EmptyUserpic::PaintRepliesMessages(p, userpicLeft, userpicTop, outerWidth, userpicRadius * 2);
Ui::EmptyUserpic::PaintRepliesMessages(p, userpicLeft, userpicTop, outerWidth, size);
} else {
peer()->paintUserpicLeft(p, _userpic, userpicLeft, userpicTop, outerWidth, userpicRadius * 2);
peer()->paintUserpicLeft(p, _userpic, userpicLeft, userpicTop, outerWidth, size);
}
{
@ -1069,10 +1079,13 @@ void PeerListContent::setRowHidden(not_null<PeerListRow*> row, bool hidden) {
void PeerListContent::addRowEntry(not_null<PeerListRow*> row) {
const auto savedMessagesStatus = _controller->savedMessagesChatStatus();
if (!savedMessagesStatus.isEmpty() && !row->special()) {
if (row->peer()->isSelf()) {
const auto peer = row->peer();
if (peer->isSelf()) {
row->setSavedMessagesChatStatus(savedMessagesStatus);
} else if (row->peer()->isRepliesChat()) {
} else if (peer->isRepliesChat()) {
row->setIsRepliesMessagesChat(true);
} else if (peer->isVerifyCodes()) {
row->setIsVerifyCodesChat(true);
}
}
_rowsById.emplace(row->id(), row);

View file

@ -200,6 +200,9 @@ public:
void setIsRepliesMessagesChat(bool isRepliesMessagesChat) {
_isRepliesMessagesChat = isRepliesMessagesChat;
}
void setIsVerifyCodesChat(bool isVerifyCodesChat) {
_isVerifyCodesChat = isVerifyCodesChat;
}
template <typename UpdateCallback>
void setChecked(
@ -251,6 +254,10 @@ public:
return _nameFirstLetters;
}
void setSkipPeerBadge(bool skip) {
_skipPeerBadge = skip;
}
virtual void lazyInitialize(const style::PeerListItem &st);
virtual void paintStatusText(
Painter &p,
@ -288,7 +295,7 @@ private:
std::unique_ptr<Ui::RoundImageCheckbox> _checkbox;
Ui::Text::String _name;
Ui::Text::String _status;
Ui::PeerBadge _bagde;
Ui::PeerBadge _badge;
StatusType _statusType = StatusType::Online;
crl::time _statusValidTill = 0;
base::flat_set<QChar> _nameFirstLetters;
@ -299,6 +306,8 @@ private:
bool _initialized : 1 = false;
bool _isSearchResult : 1 = false;
bool _isRepliesMessagesChat : 1 = false;
bool _isVerifyCodesChat : 1 = false;
bool _skipPeerBadge : 1 = false;
};

View file

@ -842,6 +842,7 @@ auto ChooseRecipientBoxController::createRow(
? !_filter(history)
: ((peer->isBroadcast() && !Data::CanSendAnything(peer))
|| peer->isRepliesChat()
|| peer->isVerifyCodes()
|| (peer->isUser() && (_premiumRequiredError
? !peer->asUser()->canSendIgnoreRequirePremium()
: !Data::CanSendAnything(peer))));

View file

@ -269,7 +269,8 @@ PaintRoundImageCallback ForbiddenRow::generatePaintUserpicCallback(
const auto peer = this->peer();
const auto saved = peer->isSelf();
const auto replies = peer->isRepliesChat();
auto userpic = (saved || replies)
const auto verifyCodes = peer->isVerifyCodes();
auto userpic = (saved || replies || verifyCodes)
? Ui::PeerUserpicView()
: ensureUserpicView();
auto paint = [=](
@ -302,6 +303,7 @@ PaintRoundImageCallback ForbiddenRow::generatePaintUserpicCallback(
repaint = (_paletteVersion != style::PaletteVersion())
|| (!saved
&& !replies
&& !verifyCodes
&& (_userpicKey != peer->userpicUniqueKey(userpic)));
}
if (repaint) {

View file

@ -33,6 +33,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "chat_helpers/tabbed_selector.h"
#include "core/application.h"
#include "core/core_settings.h"
#include "data/components/credits.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_peer.h"
@ -1591,6 +1592,9 @@ void Controller::fillBotBalanceButton() {
auto &lifetime = _controls.buttonsLayout->lifetime();
const auto state = lifetime.make_state<State>();
if (const auto balance = _peer->session().credits().balance(_peer->id)) {
state->balance = QString::number(balance);
}
const auto wrap = _controls.buttonsLayout->add(
object_ptr<Ui::SlideWrap<Ui::SettingsButton>>(
@ -1604,7 +1608,7 @@ void Controller::fillBotBalanceButton() {
},
st::manageGroupButton,
{})));
wrap->toggle(false, anim::type::instant);
wrap->toggle(!state->balance.current().isEmpty(), anim::type::instant);
const auto button = wrap->entity();
{

View file

@ -1131,11 +1131,14 @@ void ShowEditPeerPermissionsBox(
disabledByAdminRights,
tr::lng_rights_permission_cant_edit(tr::now));
if (const auto channel = peer->asChannel()) {
if (channel->isPublic()
|| (channel->isMegagroup() && channel->linkedChat())) {
if (channel->isPublic()) {
result.emplace(
Flag::ChangeInfo | Flag::PinMessages,
tr::lng_rights_permission_unavailable(tr::now));
} else if (channel->isMegagroup() && channel->linkedChat()) {
result.emplace(
Flag::ChangeInfo | Flag::PinMessages,
tr::lng_rights_permission_in_discuss(tr::now));
}
}
return result;

View file

@ -8,23 +8,28 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/report_messages_box.h"
#include "api/api_report.h"
#include "core/application.h"
#include "data/data_peer.h"
#include "data/data_photo.h"
#include "lang/lang_keys.h"
#include "ui/boxes/report_box.h"
#include "ui/boxes/report_box_graphics.h"
#include "ui/layers/generic_box.h"
#include "ui/rect.h"
#include "ui/vertical_list.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/fields/input_field.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
#include "styles/style_boxes.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_layers.h"
#include "styles/style_settings.h"
namespace {
[[nodiscard]] object_ptr<Ui::BoxContent> Report(
not_null<PeerData*> peer,
std::variant<
v::null_t,
MessageIdsList,
not_null<PhotoData*>,
StoryId> data,
std::variant<v::null_t, not_null<PhotoData*>> data,
const style::ReportBox *stOverride) {
const auto source = v::match(data, [](const MessageIdsList &ids) {
return Ui::ReportSource::Message;
@ -62,64 +67,151 @@ namespace {
} // namespace
object_ptr<Ui::BoxContent> ReportItemsBox(
not_null<PeerData*> peer,
MessageIdsList ids) {
return Report(peer, ids, nullptr);
}
object_ptr<Ui::BoxContent> ReportProfilePhotoBox(
not_null<PeerData*> peer,
not_null<PhotoData*> photo) {
return Report(peer, photo, nullptr);
}
void ShowReportPeerBox(
not_null<Window::SessionController*> window,
not_null<PeerData*> peer) {
struct State {
QPointer<Ui::BoxContent> reasonBox;
QPointer<Ui::BoxContent> detailsBox;
MessageIdsList ids;
};
const auto state = std::make_shared<State>();
const auto chosen = [=](Ui::ReportReason reason) {
const auto send = [=](const QString &text) {
window->clearChooseReportMessages();
Api::SendReport(
window->uiShow(),
peer,
reason,
text,
std::move(state->ids));
if (const auto strong = state->reasonBox.data()) {
strong->closeBox();
void ShowReportMessageBox(
std::shared_ptr<Ui::Show> show,
not_null<PeerData*> peer,
const std::vector<MsgId> &ids,
const std::vector<StoryId> &stories,
const style::ReportBox *stOverride) {
const auto report = Api::CreateReportMessagesOrStoriesCallback(
show,
peer);
auto performRequest = [=](
const auto &repeatRequest,
Data::ReportInput reportInput) -> void {
constexpr auto kToastDuration = crl::time(4000);
report(reportInput, [=](const Api::ReportResult &result) {
if (!result.error.isEmpty()) {
if (result.error == u"MESSAGE_ID_REQUIRED"_q) {
const auto widget = show->toastParent();
const auto window = Core::App().findWindow(widget);
const auto controller = window
? window->sessionController()
: nullptr;
if (controller) {
const auto callback = [=](std::vector<MsgId> ids) {
auto copy = reportInput;
copy.ids = std::move(ids);
repeatRequest(repeatRequest, std::move(copy));
};
controller->showChooseReportMessages(
peer,
reportInput,
std::move(callback));
}
} else {
show->showToast(result.error);
}
return;
}
if (const auto strong = state->detailsBox.data()) {
strong->closeBox();
if (!result.options.empty() || result.commentOption) {
show->show(Box([=](not_null<Ui::GenericBox*> box) {
box->setTitle(
rpl::single(
result.title.isEmpty()
? reportInput.optionText
: result.title));
for (const auto &option : result.options) {
const auto button = Ui::AddReportOptionButton(
box->verticalLayout(),
option.text,
stOverride);
button->setClickedCallback([=] {
auto copy = reportInput;
copy.optionId = option.id;
copy.optionText = option.text;
repeatRequest(repeatRequest, std::move(copy));
});
}
if (const auto commentOption = result.commentOption) {
constexpr auto kReportReasonLengthMax = 512;
const auto &st = stOverride
? stOverride
: &st::defaultReportBox;
Ui::AddReportDetailsIconButton(box);
Ui::AddSkip(box->verticalLayout());
Ui::AddSkip(box->verticalLayout());
const auto details = box->addRow(
object_ptr<Ui::InputField>(
box,
st->field,
Ui::InputField::Mode::MultiLine,
commentOption->optional
? tr::lng_report_details_optional()
: tr::lng_report_details_non_optional(),
QString()));
Ui::AddSkip(box->verticalLayout());
Ui::AddSkip(box->verticalLayout());
{
const auto container = box->verticalLayout();
auto label = object_ptr<Ui::FlatLabel>(
container,
tr::lng_report_details_message_about(),
st::boxDividerLabel);
label->setTextColorOverride(st->dividerFg->c);
using namespace Ui;
const auto widget = container->add(
object_ptr<PaddingWrap<>>(
container,
std::move(label),
st::defaultBoxDividerLabelPadding));
const auto background
= CreateChild<BoxContentDivider>(
widget,
st::boxDividerHeight,
st->dividerBg,
RectPart::Top | RectPart::Bottom);
background->lower();
widget->sizeValue(
) | rpl::start_with_next([=](const QSize &s) {
background->resize(s);
}, background->lifetime());
}
details->setMaxLength(kReportReasonLengthMax);
box->setFocusCallback([=] {
details->setFocusFast();
});
const auto submit = [=] {
if (!commentOption->optional
&& details->empty()) {
details->showError();
details->setFocus();
return;
}
auto copy = reportInput;
copy.optionId = commentOption->id;
copy.comment = details->getLastText();
repeatRequest(repeatRequest, std::move(copy));
};
details->submits(
) | rpl::start_with_next(submit, details->lifetime());
box->addButton(tr::lng_report_button(), submit);
} else {
box->addButton(
tr::lng_close(),
[=] { show->hideLayer(); });
}
if (!reportInput.optionId.isNull()) {
box->addLeftButton(
tr::lng_create_group_back(),
[=] { box->closeBox(); });
}
}));
} else if (result.successful) {
show->showToast(
tr::lng_report_thanks(tr::now),
kToastDuration);
show->hideLayer();
}
};
if (reason == Ui::ReportReason::Fake
|| reason == Ui::ReportReason::Other) {
state->ids = {};
state->detailsBox = window->show(
Box(Ui::ReportDetailsBox, st::defaultReportBox, send));
return;
}
window->showChooseReportMessages(peer, reason, [=](
MessageIdsList ids) {
state->ids = std::move(ids);
state->detailsBox = window->show(
Box(Ui::ReportDetailsBox, st::defaultReportBox, send));
});
};
state->reasonBox = window->show(Box(
Ui::ReportReasonBox,
st::defaultReportBox,
(peer->isBroadcast()
? Ui::ReportSource::Channel
: peer->isUser()
? Ui::ReportSource::Bot
: Ui::ReportSource::Group),
chosen));
performRequest(performRequest, { .ids = ids, .stories = stories });
}

View file

@ -12,20 +12,22 @@ class object_ptr;
namespace Ui {
class BoxContent;
class Show;
} // namespace Ui
namespace Window {
class SessionController;
} // namespace Main
namespace style {
struct ReportBox;
} // namespace style
class PeerData;
[[nodiscard]] object_ptr<Ui::BoxContent> ReportItemsBox(
not_null<PeerData*> peer,
MessageIdsList ids);
[[nodiscard]] object_ptr<Ui::BoxContent> ReportProfilePhotoBox(
not_null<PeerData*> peer,
not_null<PhotoData*> photo);
void ShowReportPeerBox(
not_null<Window::SessionController*> window,
not_null<PeerData*> peer);
void ShowReportMessageBox(
std::shared_ptr<Ui::Show> show,
not_null<PeerData*> peer,
const std::vector<MsgId> &ids,
const std::vector<StoryId> &stories,
const style::ReportBox *stOverride = nullptr);

View file

@ -272,10 +272,13 @@ void SendCreditsBox(
state->confirmButtonBusy = true;
session->api().request(
MTPpayments_SendStarsForm(
MTP_flags(0),
MTP_long(form->formId),
form->inputInvoice)
).done([=](auto result) {
).done([=](const MTPpayments_PaymentResult &result) {
result.match([&](const MTPDpayments_paymentResult &data) {
session->api().applyUpdates(data.vupdates());
}, [](const MTPDpayments_paymentVerificationNeeded &data) {
});
if (weak) {
state->confirmButtonBusy = false;
box->closeBox();
@ -311,41 +314,22 @@ void SendCreditsBox(
AddChildToWidgetCenter(button.data(), loadingAnimation);
loadingAnimation->showOn(state->confirmButtonBusy.value());
}
{
auto buttonText = tr::lng_credits_box_out_confirm(
lt_count,
rpl::single(form->invoice.amount) | tr::to_count(),
lt_emoji,
rpl::single(CreditsEmojiSmall(session)),
Ui::Text::RichLangValue);
const auto buttonLabel = Ui::CreateChild<Ui::FlatLabel>(
button,
rpl::single(QString()),
st::creditsBoxButtonLabel);
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());
}
SetButtonMarkedLabel(
button,
rpl::combine(
tr::lng_credits_box_out_confirm(
lt_count,
rpl::single(form->invoice.amount) | tr::to_count(),
lt_emoji,
rpl::single(CreditsEmojiSmall(session)),
Ui::Text::RichLangValue),
state->confirmButtonBusy.value()
) | rpl::map([](TextWithEntities &&text, bool busy) {
return busy ? TextWithEntities() : std::move(text);
}),
session,
st::creditsBoxButtonLabel,
box->getDelegate()->style().button.textFg->c);
const auto buttonWidth = st::boxWidth
- rect::m::sum::h(st::giveawayGiftCodeBox.buttonPadding);
@ -405,4 +389,73 @@ TextWithEntities CreditsEmojiSmall(not_null<Main::Session*> session) {
QString(QChar(0x2B50)));
}
not_null<FlatLabel*> SetButtonMarkedLabel(
not_null<RpWidget*> button,
rpl::producer<TextWithEntities> text,
Fn<std::any(Fn<void()> update)> context,
const style::FlatLabel &st,
std::optional<QColor> textFg) {
const auto buttonLabel = Ui::CreateChild<Ui::FlatLabel>(
button,
rpl::single(QString()),
st);
rpl::duplicate(
text
) | rpl::filter([=](const TextWithEntities &text) {
return !text.text.isEmpty();
}) | rpl::start_with_next([=](const TextWithEntities &text) {
buttonLabel->setMarkedText(
text,
context([=] { buttonLabel->update(); }));
}, buttonLabel->lifetime());
if (textFg) {
buttonLabel->setTextColorOverride(textFg);
}
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);
buttonLabel->showOn(std::move(
text
) | rpl::map([=](const TextWithEntities &text) {
return !text.text.isEmpty();
}));
return buttonLabel;
}
not_null<FlatLabel*> SetButtonMarkedLabel(
not_null<RpWidget*> button,
rpl::producer<TextWithEntities> text,
not_null<Main::Session*> session,
const style::FlatLabel &st,
std::optional<QColor> textFg) {
return SetButtonMarkedLabel(button, text, [=](Fn<void()> update) {
return Core::MarkedTextContext{
.session = session,
.customEmojiRepaint = update,
};
}, st, textFg);
}
void SendStarGift(
not_null<Main::Session*> session,
std::shared_ptr<Payments::CreditsFormData> data,
Fn<void(std::optional<QString>)> done) {
session->api().request(MTPpayments_SendStarsForm(
MTP_long(data->formId),
data->inputInvoice
)).done([=](const MTPpayments_PaymentResult &result) {
result.match([&](const MTPDpayments_paymentResult &data) {
session->api().applyUpdates(data.vupdates());
}, [](const MTPDpayments_paymentVerificationNeeded &data) {
});
done(std::nullopt);
}).fail([=](const MTP::Error &error) {
done(error.type());
}).send();
}
} // namespace Ui

View file

@ -9,6 +9,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
class HistoryItem;
namespace style {
struct FlatLabel;
} // namespace style
namespace Main {
class Session;
} // namespace Main
@ -19,7 +23,9 @@ struct CreditsFormData;
namespace Ui {
class RpWidget;
class GenericBox;
class FlatLabel;
void SendCreditsBox(
not_null<Ui::GenericBox*> box,
@ -32,4 +38,23 @@ void SendCreditsBox(
[[nodiscard]] TextWithEntities CreditsEmojiSmall(
not_null<Main::Session*> session);
not_null<FlatLabel*> SetButtonMarkedLabel(
not_null<RpWidget*> button,
rpl::producer<TextWithEntities> text,
Fn<std::any(Fn<void()> update)> context,
const style::FlatLabel &st,
std::optional<QColor> textFg = {});
not_null<FlatLabel*> SetButtonMarkedLabel(
not_null<RpWidget*> button,
rpl::producer<TextWithEntities> text,
not_null<Main::Session*> session,
const style::FlatLabel &st,
std::optional<QColor> textFg = {});
void SendStarGift(
not_null<Main::Session*> session,
std::shared_ptr<Payments::CreditsFormData> data,
Fn<void(std::optional<QString>)> done);
} // namespace Ui

View file

@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "chat_helpers/message_field.h"
#include "menu/menu_send.h"
#include "chat_helpers/emoji_suggestions_widget.h"
#include "chat_helpers/field_autocomplete.h"
#include "chat_helpers/tabbed_panel.h"
#include "chat_helpers/tabbed_selector.h"
#include "editor/photo_editor_layer_widget.h"
@ -1304,13 +1305,17 @@ void SendFilesBox::setupCaption() {
: (_limits & SendFilesAllow::EmojiWithoutPremium);
};
const auto show = _show;
InitMessageFieldHandlers(
&show->session(),
show,
_caption.data(),
[=] { return show->paused(Window::GifPauseReason::Layer); },
allow,
&_st.files.caption);
InitMessageFieldHandlers({
.session = &show->session(),
.show = show,
.field = _caption.data(),
.customEmojiPaused = [=] {
return show->paused(Window::GifPauseReason::Layer);
},
.allowPremiumEmoji = allow,
.fieldStyle = &_st.files.caption,
});
setupCaptionAutocomplete();
Ui::Emoji::SuggestionsController::Init(
getDelegate()->outerContainer(),
_caption,
@ -1370,6 +1375,59 @@ void SendFilesBox::setupCaption() {
}, _caption->lifetime());
}
void SendFilesBox::setupCaptionAutocomplete() {
if (!_captionToPeer || !_caption) {
return;
}
const auto parent = getDelegate()->outerContainer();
ChatHelpers::InitFieldAutocomplete(_autocomplete, {
.parent = parent,
.show = _show,
.field = _caption.data(),
.peer = _captionToPeer,
.features = [=] {
auto result = ChatHelpers::ComposeFeatures();
result.autocompleteCommands = false;
result.suggestStickersByEmoji = false;
return result;
},
.sendMenuDetails = _sendMenuDetails,
});
const auto raw = _autocomplete.get();
const auto scheduled = std::make_shared<bool>();
const auto recountPostponed = [=] {
if (*scheduled) {
return;
}
*scheduled = true;
Ui::PostponeCall(raw, [=] {
*scheduled = false;
auto field = Ui::MapFrom(parent, this, _caption->geometry());
_autocomplete->setBoundings(QRect(
field.x() - _caption->x(),
st::defaultBox.margin.top(),
width(),
(field.y()
+ _st.files.caption.textMargins.top()
+ _st.files.caption.placeholderShift
+ _st.files.caption.placeholderFont->height
- st::defaultBox.margin.top())));
});
};
for (auto w = (QWidget*)_caption.data(); w; w = w->parentWidget()) {
base::install_event_filter(raw, w, [=](not_null<QEvent*> e) {
if (e->type() == QEvent::Move || e->type() == QEvent::Resize) {
recountPostponed();
}
return base::EventFilterResult::Continue;
});
if (w == parent) {
break;
}
}
}
void SendFilesBox::checkCharsLimitation() {
const auto limits = Data::PremiumLimits(&_show->session());
const auto caption = (_caption && !_caption->isHidden())
@ -1685,6 +1743,14 @@ void SendFilesBox::updateControlsGeometry() {
_scroll->move(0, _titleHeight.current());
}
void SendFilesBox::showFinished() {
if (const auto raw = _autocomplete.get()) {
InvokeQueued(raw, [=] {
raw->raise();
});
}
}
void SendFilesBox::setInnerFocus() {
if (_caption && !_caption->isHidden()) {
_caption->setFocusFast();

View file

@ -28,6 +28,7 @@ enum class SendType;
namespace ChatHelpers {
class TabbedPanel;
class Show;
class FieldAutocomplete;
} // namespace ChatHelpers
namespace Ui {
@ -126,6 +127,8 @@ public:
_cancelledCallback = std::move(callback);
}
void showFinished() override;
~SendFilesBox();
protected:
@ -206,6 +209,7 @@ private:
void refreshControls(bool initial = false);
void setupSendWayControls();
void setupCaption();
void setupCaptionAutocomplete();
void setupEmojiPanel();
void updateSendWayControls();
@ -257,6 +261,7 @@ private:
SendFilesLimits _limits = {};
Fn<MenuDetails()> _sendMenuDetails;
Fn<void(MenuAction, MenuDetails)> _sendMenuCallback;
PeerData *_captionToPeer = nullptr;
SendFilesCheck _check;
SendFilesConfirmed _confirmedCallback;
@ -268,6 +273,7 @@ private:
bool _invertCaption = false;
object_ptr<Ui::InputField> _caption = { nullptr };
std::unique_ptr<ChatHelpers::FieldAutocomplete> _autocomplete;
TextWithTags _prefilledCaptionText;
object_ptr<Ui::EmojiButton> _emojiToggle = { nullptr };
base::unique_qptr<ChatHelpers::TabbedPanel> _emojiPanel;

View file

@ -7,7 +7,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "boxes/send_gif_with_caption_box.h"
#include "base/event_filter.h"
#include "boxes/premium_preview_box.h"
#include "chat_helpers/field_autocomplete.h"
#include "chat_helpers/message_field.h"
#include "chat_helpers/tabbed_panel.h"
#include "chat_helpers/tabbed_selector.h"
@ -30,9 +32,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/controls/emoji_button.h"
#include "ui/controls/emoji_button_factory.h"
#include "ui/layers/generic_box.h"
#include "ui/rect.h"
#include "ui/vertical_list.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/rect.h"
#include "ui/ui_utility.h"
#include "ui/vertical_list.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
#include "styles/style_boxes.h"
@ -226,6 +229,7 @@ namespace {
void SendGifWithCaptionBox(
not_null<Ui::GenericBox*> box,
not_null<DocumentData*> document,
not_null<PeerData*> peer,
const SendMenu::Details &details,
Fn<void(Api::SendOptions, TextWithTags)> done) {
const auto window = Core::App().findWindow(box);
@ -255,6 +259,61 @@ void SendGifWithCaptionBox(
return true;
});
const auto sendMenuDetails = [=] { return details; };
struct Autocomplete {
std::unique_ptr<ChatHelpers::FieldAutocomplete> dropdown;
bool geometryUpdateScheduled = false;
};
const auto autocomplete = box->lifetime().make_state<Autocomplete>();
const auto outer = box->getDelegate()->outerContainer();
ChatHelpers::InitFieldAutocomplete(autocomplete->dropdown, {
.parent = outer,
.show = controller->uiShow(),
.field = input,
.peer = peer,
.features = [=] {
auto result = ChatHelpers::ComposeFeatures();
result.autocompleteCommands = false;
result.suggestStickersByEmoji = false;
return result;
},
.sendMenuDetails = sendMenuDetails,
});
const auto raw = autocomplete->dropdown.get();
const auto recountPostponed = [=] {
if (autocomplete->geometryUpdateScheduled) {
return;
}
autocomplete->geometryUpdateScheduled = true;
Ui::PostponeCall(raw, [=] {
autocomplete->geometryUpdateScheduled = false;
const auto from = input->parentWidget();
auto field = Ui::MapFrom(outer, from, input->geometry());
const auto &st = st::defaultComposeFiles;
autocomplete->dropdown->setBoundings(QRect(
field.x() - input->x(),
st::defaultBox.margin.top(),
input->width(),
(field.y()
+ st.caption.textMargins.top()
+ st.caption.placeholderShift
+ st.caption.placeholderFont->height
- st::defaultBox.margin.top())));
});
};
for (auto w = (QWidget*)input; w; w = w->parentWidget()) {
base::install_event_filter(raw, w, [=](not_null<QEvent*> e) {
if (e->type() == QEvent::Move || e->type() == QEvent::Resize) {
recountPostponed();
}
return base::EventFilterResult::Continue;
});
if (w == outer) {
break;
}
}
const auto send = [=](Api::SendOptions options) {
done(std::move(options), input->getTextWithTags());
};
@ -264,8 +323,15 @@ void SendGifWithCaptionBox(
SendMenu::SetupMenuAndShortcuts(
confirm,
controller->uiShow(),
[=] { return details; },
sendMenuDetails,
SendMenu::DefaultCallback(controller->uiShow(), send));
box->setShowFinishedCallback([=] {
if (const auto raw = autocomplete->dropdown.get()) {
InvokeQueued(raw, [=] {
raw->raise();
});
}
});
box->addButton(tr::lng_cancel(), [=] {
box->closeBox();
});

View file

@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
class PeerData;
class DocumentData;
namespace Api {
@ -24,6 +25,7 @@ class GenericBox;
void SendGifWithCaptionBox(
not_null<Ui::GenericBox*> box,
not_null<DocumentData*> document,
not_null<PeerData*> peer,
const SendMenu::Details &details,
Fn<void(Api::SendOptions, TextWithTags)> done);

View file

@ -245,13 +245,12 @@ void ShareBox::prepareCommentField() {
}, field->lifetime());
if (const auto show = uiShow(); show->valid()) {
InitMessageFieldHandlers(
_descriptor.session,
Main::MakeSessionShow(show, _descriptor.session),
field,
nullptr,
nullptr,
_descriptor.stLabel);
InitMessageFieldHandlers({
.session = _descriptor.session,
.show = Main::MakeSessionShow(show, _descriptor.session),
.field = field,
.fieldStyle = _descriptor.stLabel,
});
}
field->setSubmitSettings(Core::App().settings().sendSubmitWay());
@ -843,6 +842,8 @@ void ShareBox::Inner::updateChatName(not_null<Chat*> chat) {
? tr::lng_saved_messages(tr::now)
: peer->isRepliesChat()
? tr::lng_replies_messages(tr::now)
: peer->isVerifyCodes()
? tr::lng_verification_codes(tr::now)
: peer->name();
chat->name.setText(_st.item.nameStyle, text, Ui::NameTextOptions());
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,23 @@
/*
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 Window {
class SessionController;
} // namespace Window
namespace Ui {
void ChooseStarGiftRecipient(
not_null<Window::SessionController*> controller);
void ShowStarGiftBox(
not_null<Window::SessionController*> controller,
not_null<PeerData*> peer);
} // namespace Ui

View file

@ -89,7 +89,8 @@ using TLStickerSet = MTPmessages_StickerSet;
[[nodiscard]] std::optional<QColor> ComputeImageColor(
const style::icon &lockIcon,
const QImage &frame) {
const QImage &frame,
RectPart part) {
if (frame.isNull()
|| frame.format() != QImage::Format_ARGB32_Premultiplied) {
return {};
@ -98,13 +99,29 @@ using TLStickerSet = MTPmessages_StickerSet;
auto sg = int64();
auto sb = int64();
auto sa = int64();
const auto factor = frame.devicePixelRatio();
const auto factor = style::DevicePixelRatio();
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;
const auto radius = st::roundRadiusSmall;
const auto skipy = std::max(frame.height() - height - radius, 0);
const auto skipx = (part == RectPart::TopLeft
|| part == RectPart::Left
|| part == RectPart::BottomLeft)
? 0
: (part == RectPart::Top
|| part == RectPart::Center
|| part == RectPart::Bottom)
? (frame.width() - width) / 2
: std::max(frame.width() - width - radius, 0);
const auto skipy = (part == RectPart::TopLeft
|| part == RectPart::Top
|| part == RectPart::TopRight)
? 0
: (part == RectPart::Left
|| part == RectPart::Center
|| part == RectPart::Right)
? (frame.height() - height) / 2
: std::max(frame.height() - height - radius, 0);
const auto perline = frame.bytesPerLine();
const auto addperline = perline - (width * 4);
auto bits = static_cast<const uchar*>(frame.bits())
@ -128,17 +145,20 @@ using TLStickerSet = MTPmessages_StickerSet;
[[nodiscard]] QColor ComputeLockColor(
const style::icon &lockIcon,
const QImage &frame) {
const QImage &frame,
RectPart part) {
return ComputeImageColor(
lockIcon,
frame
frame,
part
).value_or(st::windowSubTextFg->c);
}
void ValidatePremiumLockBg(
const style::icon &lockIcon,
QImage &image,
const QImage &frame) {
const QImage &frame,
RectPart part) {
if (!image.isNull()) {
return;
}
@ -149,7 +169,7 @@ void ValidatePremiumLockBg(
QImage::Format_ARGB32_Premultiplied);
image.setDevicePixelRatio(factor);
auto p = QPainter(&image);
const auto color = ComputeLockColor(lockIcon, frame);
const auto color = ComputeLockColor(lockIcon, frame, part);
p.fillRect(
QRect(QPoint(), size),
anim::color(color, st::windowSubTextFg, kGrayLockOpacity));
@ -202,8 +222,10 @@ void ValidatePremiumStarFg(const style::icon &lockIcon, QImage &image) {
StickerPremiumMark::StickerPremiumMark(
not_null<Main::Session*> session,
const style::icon &lockIcon)
: _lockIcon(lockIcon) {
const style::icon &lockIcon,
RectPart part)
: _lockIcon(lockIcon)
, _part(part) {
style::PaletteChanged(
) | rpl::start_with_next([=] {
_lockGray = QImage();
@ -228,11 +250,15 @@ void StickerPremiumMark::paint(
const auto &bg = frame.isNull() ? _lockGray : backCache;
const auto factor = style::DevicePixelRatio();
const auto radius = st::roundRadiusSmall;
const auto point = position + QPoint(
(singleSize.width() - (bg.width() / factor) - radius),
singleSize.height() - (bg.height() / factor) - radius);
const auto shiftx = (_part == RectPart::Center)
? (singleSize.width() - (bg.width() / factor)) / 2
: (singleSize.width() - (bg.width() / factor) - radius);
const auto shifty = (_part == RectPart::Center)
? (singleSize.height() - (bg.height() / factor)) / 2
: (singleSize.height() - (bg.height() / factor) - radius);
const auto point = position + QPoint(shiftx, shifty);
p.drawImage(point, bg);
if (_premium) {
if (_premium && _part != RectPart::Center) {
validateStar();
p.drawImage(point, _star);
} else {
@ -244,7 +270,7 @@ void StickerPremiumMark::validateLock(
const QImage &frame,
QImage &backCache) {
auto &image = frame.isNull() ? _lockGray : backCache;
ValidatePremiumLockBg(_lockIcon, image, frame);
ValidatePremiumLockBg(_lockIcon, image, frame, _part);
}
void StickerPremiumMark::validateStar() {
@ -1447,9 +1473,13 @@ void StickerSetBox::Inner::contextMenuEvent(QContextMenuEvent *e) {
const auto send = crl::guard(this, [=](Api::SendOptions options) {
chosen(index, document, options);
});
// In case we're adding items after FillSendMenu we have
// to pass nullptr for showForEffect and attach selector later.
// Otherwise added items widths won't be respected in menu geometry.
SendMenu::FillSendMenu(
_menu.get(),
_show,
nullptr, // showForEffect
details,
SendMenu::DefaultCallback(_show, send));
@ -1483,6 +1513,12 @@ void StickerSetBox::Inner::contextMenuEvent(QContextMenuEvent *e) {
.isAttention = true,
});
}
SendMenu::AttachSendMenuEffect(
_menu.get(),
_show,
details,
SendMenu::DefaultCallback(_show, send));
}
if (_menu->empty()) {
_menu = nullptr;

View file

@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/layers/box_content.h"
#include "base/timer.h"
#include "data/stickers/data_stickers.h"
#include "ui/rect_part.h"
namespace Window {
class SessionController;
@ -32,7 +33,8 @@ class StickerPremiumMark final {
public:
StickerPremiumMark(
not_null<Main::Session*> session,
const style::icon &lockIcon);
const style::icon &lockIcon,
RectPart part = RectPart::Bottom);
void paint(
QPainter &p,
@ -49,6 +51,7 @@ private:
const style::icon &_lockIcon;
QImage _lockGray;
QImage _star;
RectPart _part = RectPart::Bottom;
bool _premium = false;
rpl::lifetime _lifetime;

View file

@ -1139,6 +1139,7 @@ groupCallTitle: WindowTitle(defaultWindowTitle) {
bgActive: groupCallBg;
fg: transparent;
fgActive: transparent;
oneSideControls: true;
minimize: IconButton(groupCallTitleButton) {
icon: groupCallTitleMinimizeIcon;
iconOver: groupCallTitleMinimizeIconOver;

View file

@ -198,7 +198,9 @@ void Panel::initWindow() {
return Flag::None | Flag(0);
}
#ifndef Q_OS_MAC
if (_controls->controls.geometry().contains(widgetPoint)) {
using Result = Ui::Platform::HitTestResult;
const auto windowPoint = widget()->mapTo(window(), widgetPoint);
if (_controls->controls.hitTest(windowPoint) != Result::None) {
return Flag::None | Flag(0);
}
#endif // !Q_OS_MAC

View file

@ -23,7 +23,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include <tgcalls/desktop_capturer/DesktopCaptureSourceManager.h>
#include <tgcalls/desktop_capturer/DesktopCaptureSourceHelper.h>
#include <QtGui/QGuiApplication>
#include <QtGui/QWindow>
namespace Calls::Group::Ui::DesktopCapture {
@ -585,13 +584,7 @@ void ChooseSourceProcess::setupSourcesGeometry() {
void ChooseSourceProcess::setupGeometryWithParent(
not_null<QWidget*> parent) {
const auto parentScreen = [&] {
if (const auto screen = QGuiApplication::screenAt(
parent->geometry().center())) {
return screen;
}
return parent->screen();
}();
const auto parentScreen = parent->screen();
const auto myScreen = _window->screen();
if (parentScreen && myScreen != parentScreen) {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)

View file

@ -218,8 +218,11 @@ ComposeControls {
ReportBox {
button: SettingsButton;
noIconButton: SettingsButton;
label: FlatLabel;
field: InputField;
dividerBg: color;
dividerFg: color;
spam: icon;
fake: icon;
violence: icon;
@ -1360,8 +1363,13 @@ reportReasonButton: SettingsButton(defaultSettingsButton) {
defaultReportBox: ReportBox {
button: reportReasonButton;
noIconButton: SettingsButton(reportReasonButton) {
padding: margins(22px, 7px, 8px, 7px);
}
label: boxLabel;
field: newGroupDescription;
dividerBg: boxDividerBg;
dividerFg: windowSubTextFg;
spam: menuIconDelete;
fake: menuIconFake;
violence: menuIconViolence;

View file

@ -10,20 +10,21 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace ChatHelpers {
struct ComposeFeatures {
bool likes = false;
bool sendAs = true;
bool ttlInfo = true;
bool botCommandSend = true;
bool silentBroadcastToggle = true;
bool attachBotsMenu = true;
bool inlineBots = true;
bool megagroupSet = true;
bool stickersSettings = true;
bool openStickerSets = true;
bool autocompleteHashtags = true;
bool autocompleteMentions = true;
bool autocompleteCommands = true;
bool commonTabbedPanel = true;
bool likes : 1 = false;
bool sendAs : 1 = true;
bool ttlInfo : 1 = true;
bool botCommandSend : 1 = true;
bool silentBroadcastToggle : 1 = true;
bool attachBotsMenu : 1 = true;
bool inlineBots : 1 = true;
bool megagroupSet : 1 = true;
bool stickersSettings : 1 = true;
bool openStickerSets : 1 = true;
bool autocompleteHashtags : 1 = true;
bool autocompleteMentions : 1 = true;
bool autocompleteCommands : 1 = true;
bool suggestStickersByEmoji : 1 = true;
bool commonTabbedPanel : 1 = true;
};
} // namespace ChatHelpers

View file

@ -41,9 +41,9 @@ inline auto PreviewPath(int i) {
const auto kSets = {
Set{ { 0, 0, 0, "Mac" }, PreviewPath(0) },
Set{ { 1, 1804, 8'115'639, "Android" }, PreviewPath(1) },
Set{ { 2, 1805, 5'481'197, "Twemoji" }, PreviewPath(2) },
Set{ { 3, 1806, 7'047'594, "JoyPixels" }, PreviewPath(3) },
Set{ { 1, 2290, 8'306'943, "Android" }, PreviewPath(1) },
Set{ { 2, 2291, 5'694'303, "Twemoji" }, PreviewPath(2) },
Set{ { 3, 2292, 7'261'223, "JoyPixels" }, PreviewPath(3) },
};
using Loading = MTP::DedicatedLoader::Progress;

View file

@ -713,11 +713,13 @@ void SuggestionsWidget::leaveEventHook(QEvent *e) {
}
SuggestionsController::SuggestionsController(
not_null<QWidget*> parent,
not_null<QWidget*> outer,
not_null<QTextEdit*> field,
not_null<Main::Session*> session,
const Options &options)
: _st(options.st ? *options.st : st::defaultEmojiSuggestions)
: QObject(parent)
, _st(options.st ? *options.st : st::defaultEmojiSuggestions)
, _field(field)
, _session(session)
, _showExactTimer([=] { showWithQuery(getEmojiQuery()); })

View file

@ -37,7 +37,7 @@ class SuggestionsWidget;
using SuggestionsQuery = std::variant<QString, EmojiPtr>;
class SuggestionsController {
class SuggestionsController final : public QObject {
public:
struct Options {
bool suggestExactFirstWord = true;
@ -47,6 +47,7 @@ public:
};
SuggestionsController(
not_null<QWidget*> parent,
not_null<QWidget*> outer,
not_null<QTextEdit*> field,
not_null<Main::Session*> session,

View file

@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/business/data_shortcut_messages.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
#include "data/data_changes.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_user.h"
@ -53,6 +54,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include <QtWidgets/QApplication>
namespace ChatHelpers {
namespace {
[[nodiscard]] QString PrimaryUsername(not_null<UserData*> user) {
@ -60,6 +62,18 @@ namespace {
return usernames.empty() ? user->username() : usernames.front();
}
template <typename T, typename U>
inline int indexOfInFirstN(const T &v, const U &elem, int last) {
for (auto b = v.cbegin(), i = b, e = b + std::max(int(v.size()), last)
; i != e
; ++i) {
if (i->user == elem) {
return (i - b);
}
}
return -1;
}
} // namespace
class FieldAutocomplete::Inner final : public Ui::RpWidget {
@ -70,7 +84,7 @@ public:
};
Inner(
std::shared_ptr<ChatHelpers::Show> show,
std::shared_ptr<Show> show,
const style::EmojiPan &st,
not_null<FieldAutocomplete*> parent,
not_null<MentionRows*> mrows,
@ -127,7 +141,7 @@ private:
Media::Clip::Notification notification,
not_null<DocumentData*> document);
const std::shared_ptr<ChatHelpers::Show> _show;
const std::shared_ptr<Show> _show;
const not_null<Main::Session*> _session;
const style::EmojiPan &_st;
const not_null<FieldAutocomplete*> _parent;
@ -191,13 +205,7 @@ struct FieldAutocomplete::BotCommandRow {
FieldAutocomplete::FieldAutocomplete(
QWidget *parent,
not_null<Window::SessionController*> controller)
: FieldAutocomplete(parent, controller->uiShow()) {
}
FieldAutocomplete::FieldAutocomplete(
QWidget *parent,
std::shared_ptr<ChatHelpers::Show> show,
std::shared_ptr<Show> show,
const style::EmojiPan *stOverride)
: RpWidget(parent)
, _show(std::move(show))
@ -235,10 +243,26 @@ FieldAutocomplete::FieldAutocomplete(
}), lifetime());
}
std::shared_ptr<ChatHelpers::Show> FieldAutocomplete::uiShow() const {
std::shared_ptr<Show> FieldAutocomplete::uiShow() const {
return _show;
}
void FieldAutocomplete::requestRefresh() {
_refreshRequests.fire({});
}
rpl::producer<> FieldAutocomplete::refreshRequests() const {
return _refreshRequests.events();
}
void FieldAutocomplete::requestStickersUpdate() {
_stickersUpdateRequests.fire({});
}
rpl::producer<> FieldAutocomplete::stickersUpdateRequests() const {
return _stickersUpdateRequests.events();
}
auto FieldAutocomplete::mentionChosen() const
-> rpl::producer<FieldAutocomplete::MentionChosen> {
return _inner->mentionChosen();
@ -365,6 +389,10 @@ void FieldAutocomplete::showStickers(EmojiPtr emoji) {
updateFiltered(resetScroll);
}
EmojiPtr FieldAutocomplete::stickersEmoji() const {
return _emoji;
}
bool FieldAutocomplete::clearFilteredBotCommands() {
if (_brows.empty()) {
return false;
@ -373,18 +401,6 @@ bool FieldAutocomplete::clearFilteredBotCommands() {
return true;
}
namespace {
template <typename T, typename U>
inline int indexOfInFirstN(const T &v, const U &elem, int last) {
for (auto b = v.cbegin(), i = b, e = b + std::max(int(v.size()), last); i != e; ++i) {
if (i->user == elem) {
return (i - b);
}
}
return -1;
}
}
FieldAutocomplete::StickerRows FieldAutocomplete::getStickerSuggestions() {
const auto data = &_session->data().stickers();
const auto list = data->getListByEmoji({ _emoji }, _stickersSeed);
@ -871,7 +887,7 @@ bool FieldAutocomplete::eventFilter(QObject *obj, QEvent *e) {
}
FieldAutocomplete::Inner::Inner(
std::shared_ptr<ChatHelpers::Show> show,
std::shared_ptr<Show> show,
const style::EmojiPan &st,
not_null<FieldAutocomplete*> parent,
not_null<MentionRows*> mrows,
@ -963,8 +979,8 @@ void FieldAutocomplete::Inner::paintEvent(QPaintEvent *e) {
media->checkStickerSmall();
const auto paused = _show->paused(
ChatHelpers::PauseReason::TabbedPanel);
const auto size = ChatHelpers::ComputeStickerSize(
PauseReason::TabbedPanel);
const auto size = ComputeStickerSize(
document,
stickerBoundingBox());
const auto ppos = pos + QPoint(
@ -989,7 +1005,7 @@ void FieldAutocomplete::Inner::paintEvent(QPaintEvent *e) {
} else if (const auto image = media->getStickerSmall()) {
p.drawPixmapLeft(ppos, width(), image->pix(size));
} else {
ChatHelpers::PaintStickerThumbnailPath(
PaintStickerThumbnailPath(
p,
media.get(),
QRect(ppos, size),
@ -1250,7 +1266,7 @@ bool FieldAutocomplete::Inner::chooseAtIndex(
const auto bounding = selectedRect(index);
auto contentRect = QRect(
QPoint(),
ChatHelpers::ComputeStickerSize(
ComputeStickerSize(
document,
stickerBoundingBox()));
contentRect.moveCenter(bounding.center());
@ -1464,9 +1480,9 @@ auto FieldAutocomplete::Inner::getLottieRenderer()
void FieldAutocomplete::Inner::setupLottie(StickerSuggestion &suggestion) {
const auto document = suggestion.document;
suggestion.lottie = ChatHelpers::LottiePlayerFromDocument(
suggestion.lottie = LottiePlayerFromDocument(
suggestion.documentMedia.get(),
ChatHelpers::StickerLottieSize::InlineResults,
StickerLottieSize::InlineResults,
stickerBoundingBox() * style::DevicePixelRatio(),
Lottie::Quality::Default,
getLottieRenderer());
@ -1534,7 +1550,7 @@ void FieldAutocomplete::Inner::clipCallback(
} else if (i->webm->state() == State::Error) {
i->webm.setBad();
} else if (i->webm->ready() && !i->webm->started()) {
const auto size = ChatHelpers::ComputeStickerSize(
const auto size = ComputeStickerSize(
i->document,
stickerBoundingBox());
i->webm->start({ .frame = size, .keepAlpha = true });
@ -1632,3 +1648,171 @@ auto FieldAutocomplete::Inner::scrollToRequested() const
-> rpl::producer<ScrollTo> {
return _scrollToRequested.events();
}
void InitFieldAutocomplete(
std::unique_ptr<FieldAutocomplete> &autocomplete,
FieldAutocompleteDescriptor &&descriptor) {
Expects(!autocomplete);
autocomplete = std::make_unique<FieldAutocomplete>(
descriptor.parent,
descriptor.show,
descriptor.stOverride);
const auto raw = autocomplete.get();
const auto field = descriptor.field;
field->rawTextEdit()->installEventFilter(raw);
field->customTab(true);
raw->mentionChosen(
) | rpl::start_with_next([=](FieldAutocomplete::MentionChosen data) {
const auto user = data.user;
if (data.mention.isEmpty()) {
field->insertTag(
user->firstName.isEmpty() ? user->name() : user->firstName,
PrepareMentionTag(user));
} else {
field->insertTag('@' + data.mention);
}
}, raw->lifetime());
const auto sendCommand = descriptor.sendBotCommand;
const auto setText = descriptor.setText;
raw->hashtagChosen(
) | rpl::start_with_next([=](FieldAutocomplete::HashtagChosen data) {
field->insertTag(data.hashtag);
}, raw->lifetime());
const auto peer = descriptor.peer;
const auto features = descriptor.features;
const auto processShortcut = descriptor.processShortcut;
const auto shortcutMessages = (processShortcut != nullptr)
? &peer->owner().shortcutMessages()
: nullptr;
raw->botCommandChosen(
) | rpl::start_with_next([=](FieldAutocomplete::BotCommandChosen data) {
if (!features().autocompleteCommands) {
return;
}
using Method = FieldAutocompleteChooseMethod;
const auto byTab = (data.method == Method::ByTab);
const auto shortcut = data.user->isSelf();
// Send bot command at once, if it was not inserted by pressing Tab.
if (byTab && data.command.size() > 1) {
field->insertTag(data.command);
} else if (!shortcut) {
sendCommand(data.command);
setText(
field->getTextWithTagsPart(field->textCursor().position()));
} else if (processShortcut) {
processShortcut(data.command.mid(1));
}
}, raw->lifetime());
raw->setModerateKeyActivateCallback(std::move(descriptor.moderateKeyActivateCallback));
if (const auto stickerChoosing = descriptor.stickerChoosing) {
raw->choosingProcesses(
) | rpl::start_with_next([=](FieldAutocomplete::Type type) {
if (type == FieldAutocomplete::Type::Stickers) {
stickerChoosing();
}
}, raw->lifetime());
}
if (const auto chosen = descriptor.stickerChosen) {
raw->stickerChosen(
) | rpl::start_with_next(chosen, raw->lifetime());
}
field->tabbed(
) | rpl::start_with_next([=] {
if (!raw->isHidden()) {
raw->chooseSelected(FieldAutocomplete::ChooseMethod::ByTab);
}
}, raw->lifetime());
const auto check = [=] {
auto parsed = ParseMentionHashtagBotCommandQuery(field, features());
if (parsed.query.isEmpty()) {
} else if (parsed.query[0] == '#'
&& cRecentWriteHashtags().isEmpty()
&& cRecentSearchHashtags().isEmpty()) {
peer->session().local().readRecentHashtagsAndBots();
} else if (parsed.query[0] == '@'
&& cRecentInlineBots().isEmpty()) {
peer->session().local().readRecentHashtagsAndBots();
} else if (parsed.query[0] == '/'
&& peer->isUser()
&& !peer->asUser()->isBot()
&& (!shortcutMessages
|| shortcutMessages->shortcuts().list.empty())) {
parsed = {};
}
raw->showFiltered(peer, parsed.query, parsed.fromStart);
};
const auto updateStickersByEmoji = [=] {
const auto errorForStickers = Data::RestrictionError(
peer,
ChatRestriction::SendStickers);
if (features().suggestStickersByEmoji && !errorForStickers) {
const auto &text = field->getTextWithTags().text;
auto length = 0;
if (const auto emoji = Ui::Emoji::Find(text, &length)) {
if (text.size() <= length) {
raw->showStickers(emoji);
return;
}
}
}
raw->showStickers(nullptr);
};
raw->refreshRequests(
) | rpl::start_with_next(check, raw->lifetime());
raw->stickersUpdateRequests(
) | rpl::start_with_next(updateStickersByEmoji, raw->lifetime());
peer->owner().botCommandsChanges(
) | rpl::filter([=](not_null<PeerData*> changed) {
return (peer == changed);
}) | rpl::start_with_next([=] {
if (raw->clearFilteredBotCommands()) {
check();
}
}, raw->lifetime());
peer->owner().stickers().updated(
Data::StickersType::Stickers
) | rpl::start_with_next(updateStickersByEmoji, raw->lifetime());
QObject::connect(
field->rawTextEdit(),
&QTextEdit::cursorPositionChanged,
raw,
check,
Qt::QueuedConnection);
field->changes() | rpl::start_with_next(
updateStickersByEmoji,
raw->lifetime());
peer->session().changes().peerUpdates(
Data::PeerUpdate::Flag::Rights
) | rpl::filter([=](const Data::PeerUpdate &update) {
return (update.peer == peer);
}) | rpl::start_with_next(updateStickersByEmoji, raw->lifetime());
if (shortcutMessages) {
shortcutMessages->shortcutsChanged(
) | rpl::start_with_next(check, raw->lifetime());
}
raw->setSendMenuDetails(std::move(descriptor.sendMenuDetails));
raw->hideFast();
}
} // namespace ChatHelpers

View file

@ -46,46 +46,49 @@ struct Details;
} // namespace SendMenu
namespace ChatHelpers {
struct ComposeFeatures;
struct FileChosen;
class Show;
} // namespace ChatHelpers
enum class FieldAutocompleteChooseMethod {
ByEnter,
ByTab,
ByClick,
};
class FieldAutocomplete final : public Ui::RpWidget {
public:
FieldAutocomplete(
QWidget *parent,
not_null<Window::SessionController*> controller);
FieldAutocomplete(
QWidget *parent,
std::shared_ptr<ChatHelpers::Show> show,
std::shared_ptr<Show> show,
const style::EmojiPan *stOverride = nullptr);
~FieldAutocomplete();
[[nodiscard]] std::shared_ptr<ChatHelpers::Show> uiShow() const;
[[nodiscard]] std::shared_ptr<Show> uiShow() const;
bool clearFilteredBotCommands();
void showFiltered(
not_null<PeerData*> peer,
QString query,
bool addInlineBots);
void showStickers(EmojiPtr emoji);
[[nodiscard]] EmojiPtr stickersEmoji() const;
void setBoundings(QRect boundings);
const QString &filter() const;
ChatData *chat() const;
ChannelData *channel() const;
UserData *user() const;
[[nodiscard]] const QString &filter() const;
[[nodiscard]] ChatData *chat() const;
[[nodiscard]] ChannelData *channel() const;
[[nodiscard]] UserData *user() const;
int32 innerTop();
int32 innerBottom();
[[nodiscard]] int32 innerTop();
[[nodiscard]] int32 innerBottom();
bool eventFilter(QObject *obj, QEvent *e) override;
enum class ChooseMethod {
ByEnter,
ByTab,
ByClick,
};
using ChooseMethod = FieldAutocompleteChooseMethod;
struct MentionChosen {
not_null<UserData*> user;
QString mention;
@ -100,7 +103,7 @@ public:
QString command;
ChooseMethod method = ChooseMethod::ByEnter;
};
using StickerChosen = ChatHelpers::FileChosen;
using StickerChosen = FileChosen;
enum class Type {
Mentions,
Hashtags,
@ -110,13 +113,14 @@ public:
bool chooseSelected(ChooseMethod method) const;
bool stickersShown() const {
[[nodiscard]] bool stickersShown() const {
return !_srows.empty();
}
bool overlaps(const QRect &globalRect) {
if (isHidden() || !testAttribute(Qt::WA_OpaquePaintEvent)) return false;
[[nodiscard]] bool overlaps(const QRect &globalRect) {
if (isHidden() || !testAttribute(Qt::WA_OpaquePaintEvent)) {
return false;
}
return rect().contains(QRect(mapFromGlobal(globalRect.topLeft()), globalRect.size()));
}
@ -129,11 +133,16 @@ public:
void showAnimated();
void hideAnimated();
rpl::producer<MentionChosen> mentionChosen() const;
rpl::producer<HashtagChosen> hashtagChosen() const;
rpl::producer<BotCommandChosen> botCommandChosen() const;
rpl::producer<StickerChosen> stickerChosen() const;
rpl::producer<Type> choosingProcesses() const;
void requestRefresh();
[[nodiscard]] rpl::producer<> refreshRequests() const;
void requestStickersUpdate();
[[nodiscard]] rpl::producer<> stickersUpdateRequests() const;
[[nodiscard]] rpl::producer<MentionChosen> mentionChosen() const;
[[nodiscard]] rpl::producer<HashtagChosen> hashtagChosen() const;
[[nodiscard]] rpl::producer<BotCommandChosen> botCommandChosen() const;
[[nodiscard]] rpl::producer<StickerChosen> stickerChosen() const;
[[nodiscard]] rpl::producer<Type> choosingProcesses() const;
protected:
void paintEvent(QPaintEvent *e) override;
@ -157,7 +166,7 @@ private:
void recount(bool resetScroll = false);
StickerRows getStickerSuggestions();
const std::shared_ptr<ChatHelpers::Show> _show;
const std::shared_ptr<Show> _show;
const not_null<Main::Session*> _session;
const style::EmojiPan &_st;
QPixmap _cache;
@ -189,7 +198,30 @@ private:
bool _hiding = false;
Ui::Animations::Simple _a_opacity;
rpl::event_stream<> _refreshRequests;
rpl::event_stream<> _stickersUpdateRequests;
Fn<bool(int)> _moderateKeyActivateCallback;
};
struct FieldAutocompleteDescriptor {
not_null<QWidget*> parent;
std::shared_ptr<Show> show;
not_null<Ui::InputField*> field;
const style::EmojiPan *stOverride = nullptr;
not_null<PeerData*> peer;
Fn<ComposeFeatures()> features;
Fn<SendMenu::Details()> sendMenuDetails;
Fn<void()> stickerChoosing;
Fn<void(FileChosen&&)> stickerChosen;
Fn<void(TextWithTags)> setText;
Fn<void(QString)> sendBotCommand;
Fn<void(QString)> processShortcut;
Fn<bool(int)> moderateKeyActivateCallback;
};
void InitFieldAutocomplete(
std::unique_ptr<FieldAutocomplete> &autocomplete,
FieldAutocompleteDescriptor &&descriptor);
} // namespace ChatHelpers

View file

@ -409,24 +409,30 @@ base::unique_qptr<Ui::PopupMenu> GifsListWidget::fillContextMenu(
// inline results don't have effects
copyDetails.effectAllowed = false;
}
// In case we're adding items after FillSendMenu we have
// to pass nullptr for showForEffect and attach selector later.
// Otherwise added items widths won't be respected in menu geometry.
SendMenu::FillSendMenu(
menu,
_show,
nullptr, // showForMenu
copyDetails,
SendMenu::DefaultCallback(_show, send),
icons);
if (!isInlineResult) {
if (!isInlineResult && _inlineQueryPeer) {
auto done = crl::guard(this, [=](
Api::SendOptions options,
TextWithTags text) {
selectInlineResult(selected, options, true, std::move(text));
});
const auto show = _show;
const auto peer = _inlineQueryPeer;
menu->addAction(tr::lng_send_gif_with_caption(tr::now), [=] {
show->show(Box(
Ui::SendGifWithCaptionBox,
item->getDocument(),
peer,
copyDetails,
std::move(done)));
}, &st::menuIconEdit);
@ -446,6 +452,13 @@ base::unique_qptr<Ui::PopupMenu> GifsListWidget::fillContextMenu(
AddGifAction(std::move(callback), _show, document, icons);
}
}
SendMenu::AttachSendMenuEffect(
menu,
_show,
copyDetails,
SendMenu::DefaultCallback(_show, send));
return menu;
}

View file

@ -63,6 +63,12 @@ constexpr auto kParseLinksTimeout = crl::time(500);
constexpr auto kTypesDuration = 4 * crl::time(1000);
constexpr auto kCodeLanguageLimit = 32;
constexpr auto kLinkProtocols = {
"http://",
"https://",
"tonsite://"
};
// For mention / custom emoji tags save and validate selfId,
// ignore tags for different users.
[[nodiscard]] Fn<QString(QStringView)> FieldTagMimeProcessor(
@ -152,11 +158,10 @@ void EditLinkBox(
return startLink.trimmed();
}
const auto clipboard = QGuiApplication::clipboard()->text().trimmed();
if (clipboard.startsWith("http://")
|| clipboard.startsWith("https://")) {
return clipboard;
}
return QString();
const auto starts = [&](const auto &protocol) {
return clipboard.startsWith(protocol);
};
return std::ranges::any_of(kLinkProtocols, starts) ? clipboard : QString();
}();
const auto url = Ui::AttachParentChild(
content,
@ -220,6 +225,9 @@ void EditLinkBox(
if (startText.isEmpty()) {
text->setFocusFast();
} else {
if (!url->empty()) {
url->selectAll();
}
url->setFocusFast();
}
});
@ -227,12 +235,31 @@ void EditLinkBox(
url->customTab(true);
text->customTab(true);
const auto clearFullSelection = [=](not_null<Ui::InputField*> input) {
if (input->empty()) {
return;
}
auto cursor = input->rawTextEdit()->textCursor();
const auto hasFull = (!cursor.selectionStart()
&& (cursor.selectionEnd()
== (input->rawTextEdit()->document()->characterCount() - 1)));
if (hasFull) {
cursor.clearSelection();
input->setTextCursor(cursor);
}
};
url->tabbed(
) | rpl::start_with_next([=] {
clearFullSelection(url);
text->setFocus();
}, url->lifetime());
text->tabbed(
) | rpl::start_with_next([=] {
if (!url->empty()) {
url->selectAll();
}
clearFullSelection(text);
url->setFocus();
}, text->lifetime());
}
@ -396,18 +423,14 @@ Fn<void(QString now, Fn<void(QString)> save)> DefaultEditLanguageCallback(
};
}
void InitMessageFieldHandlers(
not_null<Main::Session*> session,
std::shared_ptr<Main::SessionShow> show,
not_null<Ui::InputField*> field,
Fn<bool()> customEmojiPaused,
Fn<bool(not_null<DocumentData*>)> allowPremiumEmoji,
const style::InputField *fieldStyle) {
const auto paused = [customEmojiPaused] {
return customEmojiPaused && customEmojiPaused();
void InitMessageFieldHandlers(MessageFieldHandlersArgs &&args) {
const auto paused = [passed = args.customEmojiPaused] {
return passed && passed();
};
const auto field = args.field;
const auto session = args.session;
field->setTagMimeProcessor(
FieldTagMimeProcessor(session, allowPremiumEmoji));
FieldTagMimeProcessor(session, args.allowPremiumEmoji));
field->setCustomTextContext([=](Fn<void()> repaint) {
return std::any(Core::MarkedTextContext{
.session = session,
@ -421,12 +444,14 @@ void InitMessageFieldHandlers(
field->setInstantReplaces(Ui::InstantReplaces::Default());
field->setInstantReplacesEnabled(
Core::App().settings().replaceEmojiValue());
field->setMarkdownReplacesEnabled(true);
if (show) {
field->setMarkdownReplacesEnabled(rpl::single(Ui::MarkdownEnabledState{
Ui::MarkdownEnabled{ std::move(args.allowMarkdownTags) }
}));
if (const auto &show = args.show) {
field->setEditLinkCallback(
DefaultEditLinkCallback(show, field, fieldStyle));
DefaultEditLinkCallback(show, field, args.fieldStyle));
field->setEditLanguageCallback(DefaultEditLanguageCallback(show));
InitSpellchecker(show, field, fieldStyle != nullptr);
InitSpellchecker(show, field, args.fieldStyle != nullptr);
}
const auto style = field->lifetime().make_state<Ui::ChatStyle>(
session->colorIndicesValue());
@ -526,12 +551,15 @@ void InitMessageFieldHandlers(
not_null<Ui::InputField*> field,
ChatHelpers::PauseReason pauseReasonLevel,
Fn<bool(not_null<DocumentData*>)> allowPremiumEmoji) {
InitMessageFieldHandlers(
&controller->session(),
controller->uiShow(),
field,
[=] { return controller->isGifPausedAtLeastFor(pauseReasonLevel); },
allowPremiumEmoji);
InitMessageFieldHandlers({
.session = &controller->session(),
.show = controller->uiShow(),
.field = field,
.customEmojiPaused = [=] {
return controller->isGifPausedAtLeastFor(pauseReasonLevel);
},
.allowPremiumEmoji = std::move(allowPremiumEmoji),
});
}
void InitMessageFieldGeometry(not_null<Ui::InputField*> field) {
@ -547,14 +575,16 @@ void InitMessageField(
std::shared_ptr<ChatHelpers::Show> show,
not_null<Ui::InputField*> field,
Fn<bool(not_null<DocumentData*>)> allowPremiumEmoji) {
InitMessageFieldHandlers(
&show->session(),
show,
field,
[=] { return show->paused(ChatHelpers::PauseReason::Any); },
std::move(allowPremiumEmoji));
InitMessageFieldHandlers({
.session = &show->session(),
.show = show,
.field = field,
.customEmojiPaused = [=] {
return show->paused(ChatHelpers::PauseReason::Any);
},
.allowPremiumEmoji = std::move(allowPremiumEmoji),
});
InitMessageFieldGeometry(field);
field->customTab(true);
}
void InitMessageField(

View file

@ -54,13 +54,18 @@ Fn<bool(
const style::InputField *fieldStyle = nullptr);
Fn<void(QString now, Fn<void(QString)> save)> DefaultEditLanguageCallback(
std::shared_ptr<Ui::Show> show);
void InitMessageFieldHandlers(
not_null<Main::Session*> session,
std::shared_ptr<Main::SessionShow> show, // may be null
not_null<Ui::InputField*> field,
Fn<bool()> customEmojiPaused,
Fn<bool(not_null<DocumentData*>)> allowPremiumEmoji = nullptr,
const style::InputField *fieldStyle = nullptr);
struct MessageFieldHandlersArgs {
not_null<Main::Session*> session;
std::shared_ptr<Main::SessionShow> show; // may be null
not_null<Ui::InputField*> field;
Fn<bool()> customEmojiPaused;
Fn<bool(not_null<DocumentData*>)> allowPremiumEmoji;
const style::InputField *fieldStyle = nullptr;
base::flat_set<QString> allowMarkdownTags;
};
void InitMessageFieldHandlers(MessageFieldHandlersArgs &&args);
void InitMessageFieldHandlers(
not_null<Window::SessionController*> controller,
not_null<Ui::InputField*> field,

View file

@ -22,6 +22,10 @@ GiftBoxPack::GiftBoxPack(not_null<Main::Session*> session)
GiftBoxPack::~GiftBoxPack() = default;
rpl::producer<> GiftBoxPack::updated() const {
return _updated.events();
}
int GiftBoxPack::monthsForStars(int stars) const {
if (stars <= 1000) {
return 3;
@ -112,6 +116,7 @@ void GiftBoxPack::applySet(const MTPDmessages_stickerSet &data) {
}
});
}
_updated.fire({});
}
} // namespace Stickers

View file

@ -28,6 +28,7 @@ public:
[[nodiscard]] int monthsForStars(int stars) const;
[[nodiscard]] DocumentData *lookup(int months) const;
[[nodiscard]] Data::FileOrigin origin() const;
[[nodiscard]] rpl::producer<> updated() const;
private:
using SetId = uint64;
@ -36,6 +37,7 @@ private:
const not_null<Main::Session*> _session;
const std::vector<int> _localMonths;
rpl::event_stream<> _updated;
std::vector<DocumentData*> _documents;
SetId _setId = 0;
uint64 _accessHash = 0;

View file

@ -1800,9 +1800,13 @@ base::unique_qptr<Ui::PopupMenu> StickersListWidget::fillContextMenu(
});
});
const auto icons = &st().icons;
// In case we're adding items after FillSendMenu we have
// to pass nullptr for showForEffect and attach selector later.
// Otherwise added items widths won't be respected in menu geometry.
SendMenu::FillSendMenu(
menu,
_show,
nullptr, // showForEffect
details,
SendMenu::DefaultCallback(_show, send),
icons);
@ -1836,6 +1840,13 @@ base::unique_qptr<Ui::PopupMenu> StickersListWidget::fillContextMenu(
false);
}, &icons->menuRecentRemove);
}
SendMenu::AttachSendMenuEffect(
menu,
_show,
details,
SendMenu::DefaultCallback(_show, send));
return menu;
}

View file

@ -1170,7 +1170,14 @@ bool Application::openCustomUrl(
|| passcodeLocked()) {
return false;
}
const auto command = base::StringViewMid(urlTrimmed, protocol.size(), 8192);
static const auto kTagExp = QRegularExpression(
u"^\\~[a-zA-Z0-9_\\-]+\\~:"_q);
auto skip = protocol.size();
const auto match = kTagExp.match(base::StringViewMid(urlTrimmed, skip));
if (match.hasMatch()) {
skip += match.capturedLength();
}
const auto command = base::StringViewMid(urlTrimmed, skip, 8192);
const auto my = context.value<ClickHandlerContext>();
const auto controller = my.sessionWindow.get()
? my.sessionWindow.get()

View file

@ -367,17 +367,6 @@ void MonospaceClickHandler::onClick(ClickContext context) const {
}
const auto my = context.other.value<ClickHandlerContext>();
if (const auto controller = my.sessionWindow.get()) {
auto &data = controller->session().data();
const auto item = data.message(my.itemId);
const auto hasCopyRestriction = item
&& (!item->history()->peer->allowsForwarding()
|| item->forbidsForward());
if (hasCopyRestriction) {
controller->showToast(item->history()->peer->isBroadcast()
? tr::lng_error_nocopy_channel(tr::now)
: tr::lng_error_nocopy_group(tr::now));
return;
}
controller->showToast(tr::lng_text_copied(tr::now));
}
TextUtilities::SetClipboardText(TextForMimeData::Simple(_text.trimmed()));

View file

@ -225,7 +225,8 @@ QByteArray Settings::serialize() const {
+ Serialize::stringSize(noWarningExtensions)
+ Serialize::stringSize(_customFontFamily)
+ sizeof(qint32) * 3
+ Serialize::bytearraySize(_tonsiteStorageToken);
+ Serialize::bytearraySize(_tonsiteStorageToken)
+ sizeof(qint32) * 2;
auto result = QByteArray();
result.reserve(size);
@ -379,7 +380,9 @@ QByteArray Settings::serialize() const {
1000000))
<< qint32(_systemUnlockEnabled ? 1 : 0)
<< qint32(!_weatherInCelsius ? 0 : *_weatherInCelsius ? 1 : 2)
<< _tonsiteStorageToken;
<< _tonsiteStorageToken
<< qint32(_includeMutedCounterFolders ? 1 : 0)
<< qint32(_ivZoom.current());
}
Ensures(result.size() == size);
@ -429,6 +432,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
qint32 sendFilesWay = _sendFilesWay.serialize();
qint32 sendSubmitWay = static_cast<qint32>(_sendSubmitWay.current());
qint32 includeMutedCounter = _includeMutedCounter ? 1 : 0;
qint32 includeMutedCounterFolders = _includeMutedCounterFolders ? 1 : 0;
qint32 countUnreadMessages = _countUnreadMessages ? 1 : 0;
std::optional<QString> noWarningExtensions;
qint32 legacyExeLaunchWarning = 1;
@ -504,6 +508,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
qint32 systemUnlockEnabled = _systemUnlockEnabled ? 1 : 0;
qint32 weatherInCelsius = !_weatherInCelsius ? 0 : *_weatherInCelsius ? 1 : 2;
QByteArray tonsiteStorageToken = _tonsiteStorageToken;
qint32 ivZoom = _ivZoom.current();
stream >> themesAccentColors;
if (!stream.atEnd()) {
@ -810,6 +815,12 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
if (!stream.atEnd()) {
stream >> tonsiteStorageToken;
}
if (!stream.atEnd()) {
stream >> includeMutedCounterFolders;
}
if (!stream.atEnd()) {
stream >> ivZoom;
}
if (stream.status() != QDataStream::Ok) {
LOG(("App Error: "
"Bad data for Core::Settings::constructFromSerialized()"));
@ -851,6 +862,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
case ScreenCorner::TopCenter: _notificationsCorner = uncheckedNotificationsCorner; break;
}
_includeMutedCounter = (includeMutedCounter == 1);
_includeMutedCounterFolders = (includeMutedCounterFolders == 1);
_countUnreadMessages = (countUnreadMessages == 1);
_notifyAboutPinned = (notifyAboutPinned == 1);
_autoLock = autoLock;
@ -871,8 +883,6 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
case Ui::InputSubmitSettings::Enter:
case Ui::InputSubmitSettings::CtrlEnter: _sendSubmitWay = uncheckedSendSubmitWay; break;
}
_includeMutedCounter = (includeMutedCounter == 1);
_countUnreadMessages = (countUnreadMessages == 1);
if (noWarningExtensions) {
const auto list = noWarningExtensions->mid(0, 10240)
.split(' ', Qt::SkipEmptyParts)
@ -1023,6 +1033,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
? std::optional<bool>()
: (weatherInCelsius == 1);
_tonsiteStorageToken = tonsiteStorageToken;
_ivZoom = ivZoom;
}
QString Settings::getSoundPath(const QString &key) const {
@ -1352,6 +1363,7 @@ void Settings::resetOnLastLogout() {
//_notificationsCount = 3;
//_notificationsCorner = ScreenCorner::BottomRight;
_includeMutedCounter = true;
_includeMutedCounterFolders = true;
_countUnreadMessages = true;
_notifyAboutPinned = true;
//_autoLock = 3600;
@ -1409,6 +1421,7 @@ void Settings::resetOnLastLogout() {
_hiddenGroupCallTooltips = 0;
_storiesClickTooltipHidden = false;
_ttlVoiceClickTooltipHidden = false;
_ivZoom = 100;
_recentEmojiPreload.clear();
_recentEmoji.clear();
@ -1548,4 +1561,16 @@ bool Settings::rememberedDeleteMessageOnlyForYou() const {
return _rememberedDeleteMessageOnlyForYou;
}
int Settings::ivZoom() const {
return _ivZoom.current();
}
rpl::producer<int> Settings::ivZoomValue() const {
return _ivZoom.value();
}
void Settings::setIvZoom(int value) {
constexpr auto kMin = 30;
constexpr auto kMax = 200;
_ivZoom = std::clamp(value, kMin, kMax);
}
} // namespace Core

View file

@ -244,6 +244,12 @@ public:
void setIncludeMutedCounter(bool value) {
_includeMutedCounter = value;
}
[[nodiscard]] bool includeMutedCounterFolders() const {
return _includeMutedCounterFolders;
}
void setIncludeMutedCounterFolders(bool value) {
_includeMutedCounterFolders = value;
}
[[nodiscard]] bool countUnreadMessages() const {
return _countUnreadMessages;
}
@ -915,6 +921,10 @@ public:
_tonsiteStorageToken = value;
}
[[nodiscard]] int ivZoom() const;
[[nodiscard]] rpl::producer<int> ivZoomValue() const;
void setIvZoom(int value);
[[nodiscard]] static bool ThirdColumnByDefault();
[[nodiscard]] static float64 DefaultDialogsWidthRatio();
@ -957,6 +967,7 @@ private:
int _notificationsCount = 3;
ScreenCorner _notificationsCorner = ScreenCorner::BottomRight;
bool _includeMutedCounter = true;
bool _includeMutedCounterFolders = true;
bool _countUnreadMessages = true;
rpl::variable<bool> _notifyAboutPinned = true;
int _autoLock = 3600;
@ -1049,6 +1060,7 @@ private:
bool _systemUnlockEnabled = false;
std::optional<bool> _weatherInCelsius;
QByteArray _tonsiteStorageToken;
rpl::variable<int> _ivZoom = 100;
bool _tabbedReplacedWithInfo = false; // per-window
rpl::event_stream<bool> _tabbedReplacedWithInfoValue; // per-window

View file

@ -27,10 +27,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "payments/payments_non_panel_process.h"
#include "boxes/share_box.h"
#include "boxes/connection_box.h"
#include "boxes/gift_premium_box.h"
#include "boxes/edit_privacy_box.h"
#include "boxes/premium_preview_box.h"
#include "boxes/sticker_set_box.h"
#include "boxes/sessions_box.h"
#include "boxes/star_gift_box.h"
#include "boxes/language_box.h"
#include "passport/passport_form_controller.h"
#include "ui/text/text_utilities.h"
@ -787,9 +789,9 @@ bool CopyPeerId(
Window::SessionController *controller,
const Match &match,
const QVariant &context) {
TextUtilities::SetClipboardText(TextForMimeData{ match->captured(1) });
TextUtilities::SetClipboardText({ match->captured(1) });
if (controller) {
controller->showToast(tr::lng_text_copied(tr::now));
controller->showToast(u"ID copied to clipboard."_q);
}
return true;
}
@ -928,6 +930,34 @@ bool ShowCollectibleUsername(
return true;
}
bool CopyUsernameLink(
Window::SessionController *controller,
const Match &match,
const QVariant &context) {
if (!controller) {
return false;
}
const auto username = match->captured(1);
TextUtilities::SetClipboardText({
controller->session().createInternalLinkFull(username)
});
controller->showToast(tr::lng_username_copied(tr::now));
return true;
}
bool CopyUsername(
Window::SessionController *controller,
const Match &match,
const QVariant &context) {
if (!controller) {
return false;
}
const auto username = match->captured(1);
TextUtilities::SetClipboardText({ '@' + username });
controller->showToast(tr::lng_username_text_copied(tr::now));
return true;
}
bool ShowStarsExamples(
Window::SessionController *controller,
const Match &match,
@ -1135,10 +1165,7 @@ bool ResolvePremiumMultigift(
if (!controller) {
return false;
}
const auto params = url_parse_params(
match->captured(1).mid(1),
qthelp::UrlParamNameTransform::ToLower);
controller->showGiftPremiumsBox(params.value(u"ref"_q, u"gift_url"_q));
Ui::ChooseStarGiftRecipient(controller);
controller->window().activate();
return true;
}
@ -1404,6 +1431,14 @@ const std::vector<LocalUrlHandler> &InternalUrlHandlers() {
u"^collectible_username/([a-zA-Z0-9\\-\\_\\.]+)@([0-9]+)$"_q,
ShowCollectibleUsername,
},
{
u"^username_link/([a-zA-Z0-9\\-\\_\\.]+)@([0-9]+)$"_q,
CopyUsernameLink,
},
{
u"^username_regular/([a-zA-Z0-9\\-\\_\\.]+)@([0-9]+)$"_q,
CopyUsername,
},
{
u"^stars_examples$"_q,
ShowStarsExamples,

View file

@ -13,7 +13,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "core/application.h"
#include "core/sandbox.h"
#include "core/click_handler_types.h"
#include "data/components/sponsored_messages.h"
#include "data/stickers/data_custom_emoji.h"
#include "data/data_session.h"
#include "iv/iv_instance.h"
@ -304,16 +303,6 @@ Fn<void()> UiIntegration::createSpoilerRepaint(const std::any &context) {
return my ? my->customEmojiRepaint : nullptr;
}
bool UiIntegration::allowClickHandlerActivation(
const std::shared_ptr<ClickHandler> &handler,
const ClickContext &context) {
const auto my = context.other.value<ClickHandlerContext>();
if (const auto window = my.sessionWindow.get()) {
window->session().sponsoredMessages().clicked(my.itemId);
}
return true;
}
rpl::producer<> UiIntegration::forcePopupMenuHideRequests() {
return Core::App().passcodeLockChanges() | rpl::to_empty;
}

View file

@ -61,9 +61,6 @@ public:
QStringView data,
const std::any &context) override;
Fn<void()> createSpoilerRepaint(const std::any &context) override;
bool allowClickHandlerActivation(
const std::shared_ptr<ClickHandler> &handler,
const ClickContext &context) override;
QString phraseContextCopyText() override;
QString phraseContextCopyEmail() override;

View file

@ -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 = 5005005;
constexpr auto AppVersionStr = "5.5.5";
constexpr auto AppVersion = 5006003;
constexpr auto AppVersionStr = "5.6.3";
constexpr auto AppBetaVersion = false;
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;

View file

@ -69,6 +69,11 @@ uint64 Credits::balance() const {
return _nonLockedBalance.current();
}
uint64 Credits::balance(PeerId peerId) const {
const auto it = _cachedPeerBalances.find(peerId);
return (it != _cachedPeerBalances.end()) ? it->second : 0;
}
rpl::producer<uint64> Credits::balanceValue() const {
return _nonLockedBalance.value();
}
@ -119,4 +124,8 @@ void Credits::apply(uint64 balance) {
}
}
void Credits::apply(PeerId peerId, uint64 balance) {
_cachedPeerBalances[peerId] = balance;
}
} // namespace Data

View file

@ -24,11 +24,13 @@ public:
void load(bool force = false);
void apply(uint64 balance);
void apply(PeerId peerId, uint64 balance);
[[nodiscard]] bool loaded() const;
[[nodiscard]] rpl::producer<bool> loadedValue() const;
[[nodiscard]] uint64 balance() const;
[[nodiscard]] uint64 balance(PeerId peerId) const;
[[nodiscard]] rpl::producer<uint64> balanceValue() const;
[[nodiscard]] rpl::producer<float64> rateValue(
not_null<PeerData*> ownedBotOrChannel);
@ -47,6 +49,8 @@ private:
std::unique_ptr<Api::CreditsStatus> _loader;
base::flat_map<PeerId, uint64> _cachedPeerBalances;
uint64 _balance = 0;
uint64 _locked = 0;
rpl::variable<uint64> _nonLockedBalance;

View file

@ -95,6 +95,7 @@ QByteArray RecentPeers::serialize() const {
void RecentPeers::applyLocal(QByteArray serialized) {
_list.clear();
if (serialized.isEmpty()) {
DEBUG_LOG(("Suggestions: Bad RecentPeers local, empty."));
return;
}
auto stream = Serialize::ByteArrayReader(serialized);
@ -102,8 +103,13 @@ void RecentPeers::applyLocal(QByteArray serialized) {
auto count = quint32();
stream >> streamAppVersion >> count;
if (!stream.ok()) {
DEBUG_LOG(("Suggestions: Bad RecentPeers local, not ok."));
return;
}
DEBUG_LOG(("Suggestions: "
"Start RecentPeers read, count: %1, version: %2."
).arg(count
).arg(streamAppVersion));
_list.reserve(count);
for (auto i = 0; i != int(count); ++i) {
const auto peer = Serialize::readPeer(
@ -114,9 +120,15 @@ void RecentPeers::applyLocal(QByteArray serialized) {
_list.push_back(peer);
} else {
_list.clear();
DEBUG_LOG(("Suggestions: Failed RecentPeers reading %1 / %2."
).arg(i + 1
).arg(count));
_list.clear();
return;
}
}
DEBUG_LOG(
("Suggestions: RecentPeers read OK, count: %1").arg(_list.size()));
}
} // namespace Data

View file

@ -12,6 +12,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "core/click_handler_types.h"
#include "data/data_channel.h"
#include "data/data_document.h"
#include "data/data_file_origin.h"
#include "data/data_media_preload.h"
#include "data/data_photo.h"
#include "data/data_session.h"
#include "data/data_user.h"
@ -73,16 +75,17 @@ void SponsoredMessages::clearOldRequests() {
}
}
bool SponsoredMessages::append(not_null<History*> history) {
SponsoredMessages::AppendResult SponsoredMessages::append(
not_null<History*> history) {
const auto it = _data.find(history);
if (it == end(_data)) {
return false;
return SponsoredMessages::AppendResult::None;
}
auto &list = it->second;
if (list.showedAll
|| !TooEarlyForRequest(list.received)
|| list.postsBetween) {
return false;
return SponsoredMessages::AppendResult::None;
}
const auto entryIt = ranges::find_if(list.entries, [](const Entry &e) {
@ -90,19 +93,16 @@ bool SponsoredMessages::append(not_null<History*> history) {
});
if (entryIt == end(list.entries)) {
list.showedAll = true;
return false;
return SponsoredMessages::AppendResult::None;
} else if (entryIt->preload) {
return SponsoredMessages::AppendResult::MediaLoading;
}
// SponsoredMessages::Details can be requested within
// the constructor of HistoryItem, so itemFullId is used as a key.
entryIt->itemFullId = FullMsgId(
history->peer->id,
_session->data().nextLocalMessageId());
entryIt->item.reset(history->addSponsoredMessage(
entryIt->itemFullId.msg,
entryIt->sponsored.from,
entryIt->sponsored.textWithEntities));
return true;
return SponsoredMessages::AppendResult::Appended;
}
void SponsoredMessages::inject(
@ -227,8 +227,7 @@ void SponsoredMessages::request(not_null<History*> history, Fn<void()> done) {
const auto channel = history->peer->asChannel();
Assert(channel != nullptr);
request.requestId = _session->api().request(
MTPchannels_GetSponsoredMessages(
channel->inputChannel)
MTPchannels_GetSponsoredMessages(channel->inputChannel)
).done([=](const MTPmessages_sponsoredMessages &result) {
parse(history, result);
if (done) {
@ -276,15 +275,14 @@ void SponsoredMessages::append(
const MTPSponsoredMessage &message) {
const auto &data = message.data();
const auto randomId = data.vrandom_id().v;
auto mediaPhotoId = PhotoId(0);
auto mediaDocumentId = DocumentId(0);
auto mediaPhoto = (PhotoData*)nullptr;
auto mediaDocument = (DocumentData*)nullptr;
{
if (data.vmedia()) {
data.vmedia()->match([&](const MTPDmessageMediaPhoto &media) {
if (const auto tlPhoto = media.vphoto()) {
tlPhoto->match([&](const MTPDphoto &data) {
const auto p = history->owner().processPhoto(data);
mediaPhotoId = p->id;
mediaPhoto = history->owner().processPhoto(data);
}, [](const MTPDphotoEmpty &) {
});
}
@ -296,7 +294,7 @@ void SponsoredMessages::append(
|| d->isSilentVideo()
|| d->isAnimation()
|| d->isGifv()) {
mediaDocumentId = d->id;
mediaDocument = d;
}
}, [](const MTPDdocumentEmpty &) {
});
@ -312,8 +310,8 @@ void SponsoredMessages::append(
.photoId = data.vphoto()
? history->session().data().processPhoto(*data.vphoto())->id
: PhotoId(0),
.mediaPhotoId = mediaPhotoId,
.mediaDocumentId = mediaDocumentId,
.mediaPhotoId = (mediaPhoto ? mediaPhoto->id : 0),
.mediaDocumentId = (mediaDocument ? mediaDocument->id : 0),
.backgroundEmojiId = data.vcolor().has_value()
? data.vcolor()->data().vbackground_emoji_id().value_or_empty()
: uint64(0),
@ -347,7 +345,56 @@ void SponsoredMessages::append(
.sponsorInfo = std::move(sponsorInfo),
.additionalInfo = std::move(additionalInfo),
};
list.entries.push_back({ nullptr, {}, std::move(sharedMessage) });
list.entries.push_back({
.sponsored = std::move(sharedMessage),
});
auto &entry = list.entries.back();
const auto itemId = entry.itemFullId = FullMsgId(
history->peer->id,
_session->data().nextLocalMessageId());
const auto fileOrigin = FileOrigin(); // No way to refresh in ads.
static const auto kFlaggedPreload = ((MediaPreload*)quintptr(0x01));
const auto preloaded = [=] {
const auto i = _data.find(history);
if (i == end(_data)) {
return;
}
auto &entries = i->second.entries;
const auto j = ranges::find(entries, itemId, &Entry::itemFullId);
if (j == end(entries)) {
return;
}
auto &entry = *j;
if (entry.preload.get() == kFlaggedPreload) {
entry.preload.release();
} else {
entry.preload = nullptr;
}
};
auto preload = std::unique_ptr<MediaPreload>();
entry.preload.reset(kFlaggedPreload);
if (mediaPhoto) {
preload = std::make_unique<PhotoPreload>(
mediaPhoto,
fileOrigin,
preloaded);
} else if (mediaDocument && VideoPreload::Can(mediaDocument)) {
preload = std::make_unique<VideoPreload>(
mediaDocument,
fileOrigin,
preloaded);
}
// Preload constructor may have called preloaded(), which zero-ed
// entry.preload, that way we're ready and don't need to save it.
// Otherwise we're preloading and need to save the task.
if (entry.preload.get() == kFlaggedPreload) {
entry.preload.release();
if (preload) {
entry.preload = std::move(preload);
}
}
}
void SponsoredMessages::clearItems(not_null<History*> history) {
@ -439,7 +486,10 @@ SponsoredMessages::Details SponsoredMessages::lookupDetails(
};
}
void SponsoredMessages::clicked(const FullMsgId &fullId) {
void SponsoredMessages::clicked(
const FullMsgId &fullId,
bool isMedia,
bool isFullscreen) {
const auto entryPtr = find(fullId);
if (!entryPtr) {
return;
@ -447,7 +497,11 @@ void SponsoredMessages::clicked(const FullMsgId &fullId) {
const auto randomId = entryPtr->sponsored.randomId;
const auto channel = entryPtr->item->history()->peer->asChannel();
Assert(channel != nullptr);
using Flag = MTPchannels_ClickSponsoredMessage::Flag;
_session->api().request(MTPchannels_ClickSponsoredMessage(
MTP_flags(Flag(0)
| (isMedia ? Flag::f_media : Flag(0))
| (isFullscreen ? Flag::f_fullscreen : Flag(0))),
channel->inputChannel,
MTP_bytes(randomId)
)).send();

View file

@ -20,6 +20,8 @@ class Session;
namespace Data {
class MediaPreload;
struct SponsoredReportResult final {
using Id = QByteArray;
struct Option final {
@ -65,6 +67,11 @@ struct SponsoredMessage {
class SponsoredMessages final {
public:
enum class AppendResult {
None,
Appended,
MediaLoading,
};
enum class State {
None,
AppendToEnd,
@ -90,9 +97,9 @@ public:
void request(not_null<History*> history, Fn<void()> done);
void clearItems(not_null<History*> history);
[[nodiscard]] Details lookupDetails(const FullMsgId &fullId) const;
void clicked(const FullMsgId &fullId);
void clicked(const FullMsgId &fullId, bool isMedia, bool isFullscreen);
[[nodiscard]] bool append(not_null<History*> history);
[[nodiscard]] AppendResult append(not_null<History*> history);
void inject(
not_null<History*> history,
MsgId injectAfterMsgId,
@ -114,6 +121,7 @@ private:
OwnedItem item;
FullMsgId itemFullId;
SponsoredMessage sponsored;
std::unique_ptr<MediaPreload> preload;
};
struct List {
std::vector<Entry> entries;

View file

@ -274,11 +274,13 @@ QByteArray TopPeers::serialize() const {
void TopPeers::applyLocal(QByteArray serialized) {
if (_lastReceived) {
DEBUG_LOG(("Suggestions: Skipping TopPeers local, got already."));
return;
}
_list.clear();
_disabled = false;
if (serialized.isEmpty()) {
DEBUG_LOG(("Suggestions: Bad TopPeers local, empty."));
return;
}
auto stream = Serialize::ByteArrayReader(serialized);
@ -287,8 +289,14 @@ void TopPeers::applyLocal(QByteArray serialized) {
auto count = quint32();
stream >> streamAppVersion >> disabled >> count;
if (!stream.ok()) {
DEBUG_LOG(("Suggestions: Bad TopPeers local, not ok."));
return;
}
DEBUG_LOG(("Suggestions: "
"Start TopPeers read, count: %1, version: %2, disabled: %3."
).arg(count
).arg(streamAppVersion
).arg(disabled));
_list.reserve(count);
for (auto i = 0; i != int(count); ++i) {
auto rating = quint64();
@ -303,11 +311,15 @@ void TopPeers::applyLocal(QByteArray serialized) {
.rating = DeserializeRating(rating),
});
} else {
DEBUG_LOG(("Suggestions: "
"Failed TopPeers reading %1 / %2.").arg(i + 1).arg(count));
_list.clear();
return;
}
}
_disabled = (disabled == 1);
DEBUG_LOG(
("Suggestions: TopPeers read OK, count: %1").arg(_list.size()));
}
} // namespace Data

View file

@ -84,34 +84,35 @@ struct PeerUpdate {
BotCanBeInvited = (1ULL << 22),
BotStartToken = (1ULL << 23),
CommonChats = (1ULL << 24),
HasCalls = (1ULL << 25),
SupportInfo = (1ULL << 26),
IsBot = (1ULL << 27),
EmojiStatus = (1ULL << 28),
BusinessDetails = (1ULL << 29),
Birthday = (1ULL << 30),
PersonalChannel = (1ULL << 31),
PeerGifts = (1ULL << 25),
HasCalls = (1ULL << 26),
SupportInfo = (1ULL << 27),
IsBot = (1ULL << 28),
EmojiStatus = (1ULL << 29),
BusinessDetails = (1ULL << 30),
Birthday = (1ULL << 31),
PersonalChannel = (1ULL << 32),
// For chats and channels
InviteLinks = (1ULL << 32),
Members = (1ULL << 33),
Admins = (1ULL << 34),
BannedUsers = (1ULL << 35),
Rights = (1ULL << 36),
PendingRequests = (1ULL << 37),
Reactions = (1ULL << 38),
InviteLinks = (1ULL << 33),
Members = (1ULL << 34),
Admins = (1ULL << 35),
BannedUsers = (1ULL << 36),
Rights = (1ULL << 37),
PendingRequests = (1ULL << 38),
Reactions = (1ULL << 39),
// For channels
ChannelAmIn = (1ULL << 39),
StickersSet = (1ULL << 40),
EmojiSet = (1ULL << 41),
ChannelLinkedChat = (1ULL << 42),
ChannelLocation = (1ULL << 43),
Slowmode = (1ULL << 44),
GroupCall = (1ULL << 45),
ChannelAmIn = (1ULL << 40),
StickersSet = (1ULL << 41),
EmojiSet = (1ULL << 42),
ChannelLinkedChat = (1ULL << 43),
ChannelLocation = (1ULL << 44),
Slowmode = (1ULL << 45),
GroupCall = (1ULL << 46),
// For iteration
LastUsedBit = (1ULL << 45),
LastUsedBit = (1ULL << 46),
};
using Flags = base::flags<Flag>;
friend inline constexpr auto is_flag_type(Flag) { return true; }

View file

@ -112,7 +112,9 @@ bool CanSendAnyOf(
ChatRestrictions rights,
bool forbidInForums) {
if (const auto user = peer->asUser()) {
if (user->isInaccessible() || user->isRepliesChat()) {
if (user->isInaccessible()
|| user->isRepliesChat()
|| user->isVerifyCodes()) {
return false;
} else if (user->meRequiresPremiumToWrite()
&& !user->session().premium()) {

View file

@ -50,7 +50,7 @@ struct CreditsHistoryEntry final {
QString id;
QString title;
QString description;
TextWithEntities description;
QDateTime date;
PhotoId photoId = 0;
std::vector<CreditsHistoryMedia> extended;
@ -58,10 +58,18 @@ struct CreditsHistoryEntry final {
uint64 bareMsgId = 0;
uint64 barePeerId = 0;
uint64 bareGiveawayMsgId = 0;
uint64 bareGiftStickerId = 0;
PeerType peerType;
QDateTime subscriptionUntil;
QDateTime successDate;
QString successLink;
int limitedCount = 0;
int limitedLeft = 0;
int convertStars = 0;
bool converted = false;
bool anonymous = false;
bool savedToProfile = false;
bool fromGiftsList = false;
bool reaction = false;
bool refunded = false;
bool pending = false;

View file

@ -936,14 +936,14 @@ void DocumentData::setFileName(const QString &remoteFileName) {
// in filenames, because they introduce a security issue, when
// an executable "Fil[x]gepj.exe" may look like "Filexe.jpeg".
QChar controls[] = {
0x200E, // LTR Mark
0x200F, // RTL Mark
0x202A, // LTR Embedding
0x202B, // RTL Embedding
0x202D, // LTR Override
0x202E, // RTL Override
0x2066, // LTR Isolate
0x2067, // RTL Isolate
QChar(0x200E), // LTR Mark
QChar(0x200F), // RTL Mark
QChar(0x202A), // LTR Embedding
QChar(0x202B), // RTL Embedding
QChar(0x202D), // LTR Override
QChar(0x202E), // RTL Override
QChar(0x2066), // LTR Isolate
QChar(0x2067), // RTL Isolate
};
for (const auto &ch : controls) {
_filename = std::move(_filename).replace(ch, "_");

View file

@ -42,6 +42,8 @@ namespace {
base::options::toggle OptionExternalVideoPlayer({
.id = kOptionExternalVideoPlayer,
.name = "External video player",
.description = "Use system video player instead of the internal one. "
"This disabes video playback in messages.",
});
void ConfirmDontWarnBox(

View file

@ -0,0 +1,209 @@
/*
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_media_preload.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
#include "data/data_file_origin.h"
#include "data/data_photo.h"
#include "data/data_photo_media.h"
#include "data/data_session.h"
#include "main/main_session.h"
#include "main/main_session_settings.h"
#include "media/streaming/media_streaming_reader.h"
#include "storage/file_download.h" // kMaxFileInMemory.
namespace Data {
namespace {
constexpr auto kDefaultPreloadPrefix = 4 * 1024 * 1024;
[[nodiscard]] int64 ChoosePreloadPrefix(not_null<DocumentData*> video) {
const auto result = video->videoPreloadPrefix();
return result
? result
: std::min(int64(kDefaultPreloadPrefix), video->size);
}
} // namespace
MediaPreload::MediaPreload(Fn<void()> done)
: _done(std::move(done)) {
}
void MediaPreload::callDone() {
if (const auto onstack = _done) {
onstack();
}
}
PhotoPreload::PhotoPreload(
not_null<PhotoData*> photo,
FileOrigin origin,
Fn<void()> done)
: MediaPreload(std::move(done))
, _photo(photo->createMediaView()) {
start(origin);
}
PhotoPreload::~PhotoPreload() {
if (_photo) {
base::take(_photo)->owner()->cancel();
}
}
bool PhotoPreload::Should(
not_null<PhotoData*> photo,
not_null<PeerData*> context) {
return !photo->cancelled()
&& AutoDownload::Should(
photo->session().settings().autoDownload(),
context,
photo);
}
void PhotoPreload::start(FileOrigin origin) {
if (_photo->loaded()) {
callDone();
} else {
_photo->owner()->load(origin, LoadFromCloudOrLocal, true);
_photo->owner()->session().downloaderTaskFinished(
) | rpl::filter([=] {
return _photo->loaded();
}) | rpl::start_with_next([=] { callDone(); }, _lifetime);
}
}
VideoPreload::VideoPreload(
not_null<DocumentData*> video,
FileOrigin origin,
Fn<void()> done)
: MediaPreload(std::move(done))
, DownloadMtprotoTask(
&video->session().downloader(),
video->videoPreloadLocation(),
origin)
, _video(video)
, _full(video->size) {
if (Can(video)) {
check();
} else {
callDone();
}
}
void VideoPreload::check() {
const auto key = _video->bigFileBaseCacheKey();
const auto weak = base::make_weak(static_cast<has_weak_ptr*>(this));
_video->owner().cacheBigFile().get(key, [weak](
const QByteArray &result) {
if (!result.isEmpty()) {
crl::on_main([weak] {
if (const auto strong = weak.get()) {
static_cast<VideoPreload*>(strong)->callDone();
}
});
} else {
crl::on_main([weak] {
if (const auto strong = weak.get()) {
static_cast<VideoPreload*>(strong)->load();
}
});
}
});
}
void VideoPreload::load() {
if (!Can(_video)) {
callDone();
return;
}
const auto prefix = ChoosePreloadPrefix(_video);
Assert(prefix > 0 && prefix <= _video->size);
const auto part = Storage::kDownloadPartSize;
const auto parts = (prefix + part - 1) / part;
for (auto i = 0; i != parts; ++i) {
_parts.emplace(i * part, QByteArray());
}
addToQueue();
}
void VideoPreload::done(QByteArray result) {
const auto key = _video->bigFileBaseCacheKey();
if (!result.isEmpty() && key) {
Assert(result.size() < Storage::kMaxFileInMemory);
_video->owner().cacheBigFile().putIfEmpty(
key,
Storage::Cache::Database::TaggedValue(std::move(result), 0));
}
callDone();
}
VideoPreload::~VideoPreload() {
if (!_finished && !_failed) {
cancelAllRequests();
}
}
bool VideoPreload::Can(not_null<DocumentData*> video) {
return video->canBeStreamed(nullptr)
&& video->videoPreloadLocation().valid()
&& video->bigFileBaseCacheKey();
}
bool VideoPreload::readyToRequest() const {
const auto part = Storage::kDownloadPartSize;
return !_failed && (_nextRequestOffset < _parts.size() * part);
}
int64 VideoPreload::takeNextRequestOffset() {
Expects(readyToRequest());
_requestedOffsets.emplace(_nextRequestOffset);
_nextRequestOffset += Storage::kDownloadPartSize;
return _requestedOffsets.back();
}
bool VideoPreload::feedPart(
int64 offset,
const QByteArray &bytes) {
Expects(offset < _parts.size() * Storage::kDownloadPartSize);
Expects(_requestedOffsets.contains(int(offset)));
Expects(bytes.size() <= Storage::kDownloadPartSize);
const auto part = Storage::kDownloadPartSize;
_requestedOffsets.remove(int(offset));
_parts[offset] = bytes;
if ((_nextRequestOffset + part >= _parts.size() * part)
&& _requestedOffsets.empty()) {
_finished = true;
removeFromQueue();
auto result = ::Media::Streaming::SerializeComplexPartsMap(_parts);
if (result.size() == _full) {
// Make sure it is parsed as a complex map.
result.push_back(char(0));
}
done(std::move(result));
}
return true;
}
void VideoPreload::cancelOnFail() {
_failed = true;
cancelAllRequests();
done({});
}
bool VideoPreload::setWebFileSizeHook(int64 size) {
_failed = true;
cancelAllRequests();
done({});
return false;
}
} // namespace Data

View file

@ -0,0 +1,83 @@
/*
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/download_manager_mtproto.h"
namespace Data {
class PhotoMedia;
struct FileOrigin;
class MediaPreload {
public:
explicit MediaPreload(Fn<void()> done);
virtual ~MediaPreload() = default;
protected:
void callDone();
private:
Fn<void()> _done;
};
class PhotoPreload final : public MediaPreload {
public:
[[nodiscard]] static bool Should(
not_null<PhotoData*> photo,
not_null<PeerData*> context);
PhotoPreload(
not_null<PhotoData*> data,
FileOrigin origin,
Fn<void()> done);
~PhotoPreload();
private:
void start(FileOrigin origin);
std::shared_ptr<PhotoMedia> _photo;
rpl::lifetime _lifetime;
};
class VideoPreload final
: public MediaPreload
, private Storage::DownloadMtprotoTask {
public:
[[nodiscard]] static bool Can(not_null<DocumentData*> video);
VideoPreload(
not_null<DocumentData*> video,
FileOrigin origin,
Fn<void()> done);
~VideoPreload();
private:
void check();
void load();
void done(QByteArray result);
bool readyToRequest() const override;
int64 takeNextRequestOffset() override;
bool feedPart(int64 offset, const QByteArray &bytes) override;
void cancelOnFail() override;
bool setWebFileSizeHook(int64 size) override;
const not_null<DocumentData*> _video;
base::flat_map<uint32, QByteArray> _parts;
base::flat_set<int> _requestedOffsets;
int64 _full = 0;
int _nextRequestOffset = 0;
bool _finished = false;
bool _failed = false;
};
} // namespace Data

View file

@ -579,6 +579,10 @@ const Invoice *Media::invoice() const {
return nullptr;
}
const GiftCode *Media::gift() const {
return nullptr;
}
CloudImage *Media::location() const {
return nullptr;
}
@ -2331,8 +2335,8 @@ not_null<PeerData*> MediaGiftBox::from() const {
return _from;
}
const GiftCode &MediaGiftBox::data() const {
return _data;
const GiftCode *MediaGiftBox::gift() const {
return &_data;
}
TextWithEntities MediaGiftBox::notificationText() const {

View file

@ -130,16 +130,25 @@ struct GiveawayResults {
enum class GiftType : uchar {
Premium, // count - months
Credits, // count - credits
StarGift, // count - stars
};
struct GiftCode {
QString slug;
DocumentData *document = nullptr;
TextWithEntities message;
ChannelData *channel = nullptr;
MsgId giveawayMsgId = 0;
int convertStars = 0;
int limitedCount = 0;
int limitedLeft = 0;
int count = 0;
int giveawayMsgId = 0;
GiftType type = GiftType::Premium;
bool viaGiveaway = false;
bool unclaimed = false;
bool viaGiveaway : 1 = false;
bool unclaimed : 1 = false;
bool anonymous : 1 = false;
bool converted : 1 = false;
bool saved : 1 = false;
};
class Media {
@ -163,6 +172,7 @@ public:
virtual const Call *call() const;
virtual GameData *game() const;
virtual const Invoice *invoice() const;
virtual const GiftCode *gift() const;
virtual CloudImage *location() const;
virtual PollData *poll() const;
virtual const WallPaper *paper() const;
@ -612,7 +622,7 @@ public:
std::unique_ptr<Media> clone(not_null<HistoryItem*> parent) override;
[[nodiscard]] not_null<PeerData*> from() const;
[[nodiscard]] const GiftCode &data() const;
[[nodiscard]] const GiftCode *gift() const override;
TextWithEntities notificationText() const override;
QString pinnedTextSubstring() const override;

View file

@ -394,7 +394,8 @@ void PeerData::paintUserpic(
Ui::PeerUserpicView &view,
int x,
int y,
int size) const {
int size,
bool forceCircle) const {
const auto cloud = userpicCloudImage(view);
const auto ratio = style::DevicePixelRatio();
Ui::ValidateUserpicCache(
@ -402,7 +403,7 @@ void PeerData::paintUserpic(
cloud,
cloud ? nullptr : ensureEmptyUserpic().get(),
size * ratio,
isForum());
!forceCircle && isForum());
p.drawImage(QRect(x, y, size, size), view.cached);
}
@ -627,7 +628,8 @@ bool PeerData::canCreatePolls() const {
if (const auto user = asUser()) {
return user->isBot()
&& !user->isSupport()
&& !user->isRepliesChat();
&& !user->isRepliesChat()
&& !user->isVerifyCodes();
}
return Data::CanSend(this, ChatRestriction::SendPolls);
}
@ -663,7 +665,7 @@ bool PeerData::canEditMessagesIndefinitely() const {
}
bool PeerData::canExportChatHistory() const {
if (isRepliesChat() || !allowsForwarding()) {
if (isRepliesChat() || isVerifyCodes() || !allowsForwarding()) {
return false;
} else if (const auto channel = asChannel()) {
if (!channel->amIn() && channel->invitePeekExpires()) {
@ -864,6 +866,13 @@ void PeerData::fillNames() {
if (localized != english) {
appendToIndex(localized);
}
} else if (isVerifyCodes()) {
const auto english = u"Verification Codes"_q;
const auto localized = tr::lng_verification_codes(tr::now);
appendToIndex(english);
if (localized != english) {
appendToIndex(localized);
}
}
} else if (const auto channel = asChannel()) {
appendToIndex(channel->username());
@ -1201,6 +1210,11 @@ bool PeerData::isRepliesChat() const {
: kTestId) == id;
}
bool PeerData::isVerifyCodes() const {
constexpr auto kVerifyCodesId = peerFromUser(489000);
return (id == kVerifyCodesId);
}
bool PeerData::sharedMediaInfo() const {
return isSelf() || isRepliesChat();
}

View file

@ -227,6 +227,7 @@ public:
[[nodiscard]] bool isForum() const;
[[nodiscard]] bool isGigagroup() const;
[[nodiscard]] bool isRepliesChat() const;
[[nodiscard]] bool isVerifyCodes() const;
[[nodiscard]] bool sharedMediaInfo() const;
[[nodiscard]] bool savedSublistsInfo() const;
[[nodiscard]] bool hasStoriesHidden() const;
@ -317,15 +318,23 @@ public:
Ui::PeerUserpicView &view,
int x,
int y,
int size) const;
int size,
bool forceCircle = false) const;
void paintUserpicLeft(
Painter &p,
Ui::PeerUserpicView &view,
int x,
int y,
int w,
int size) const {
paintUserpic(p, view, rtl() ? (w - x - size) : x, y, size);
int size,
bool forceCircle = false) const {
paintUserpic(
p,
view,
rtl() ? (w - x - size) : x,
y,
size,
forceCircle);
}
void loadUserpic();
[[nodiscard]] bool hasUserpic() const;

View file

@ -220,7 +220,7 @@ inline auto DefaultRestrictionValue(
ChatRestrictions rights,
bool forbidInForums) {
if (const auto user = peer->asUser()) {
if (user->isRepliesChat()) {
if (user->isRepliesChat() || user->isVerifyCodes()) {
return rpl::single(false);
}
using namespace rpl::mappers;

View file

@ -0,0 +1,24 @@
/*
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 ReportInput final {
QByteArray optionId;
QString optionText;
QString comment;
std::vector<MsgId> ids;
std::vector<StoryId> stories;
inline bool operator==(const ReportInput &other) const {
return optionId == other.optionId && comment == other.comment;
}
};
} // namespace Data

View file

@ -1668,6 +1668,14 @@ rpl::producer<not_null<HistoryItem*>> Session::newItemAdded() const {
return _newItemAdded.events();
}
void Session::notifyGiftUpdate(GiftUpdate &&update) {
_giftUpdates.fire(std::move(update));
}
rpl::producer<GiftUpdate> Session::giftUpdates() const {
return _giftUpdates.events();
}
HistoryItem *Session::changeMessageId(PeerId peerId, MsgId wasId, MsgId nowId) {
const auto list = messagesListForInsert(peerId);
const auto i = list->find(wasId);

View file

@ -77,6 +77,18 @@ struct RepliesReadTillUpdate {
bool out = false;
};
struct GiftUpdate {
enum class Action : uchar {
Save,
Unsave,
Convert,
Delete,
};
FullMsgId itemId;
Action action = {};
};
class Session final {
public:
using ViewElement = HistoryView::Element;
@ -281,6 +293,8 @@ public:
[[nodiscard]] rpl::producer<not_null<const ViewElement*>> viewLayoutChanged() const;
void notifyNewItemAdded(not_null<HistoryItem*> item);
[[nodiscard]] rpl::producer<not_null<HistoryItem*>> newItemAdded() const;
void notifyGiftUpdate(GiftUpdate &&update);
[[nodiscard]] rpl::producer<GiftUpdate> giftUpdates() const;
void requestItemRepaint(not_null<const HistoryItem*> item);
[[nodiscard]] rpl::producer<not_null<const HistoryItem*>> itemRepaintRequest() const;
void requestViewRepaint(not_null<const ViewElement*> view);
@ -924,6 +938,7 @@ private:
rpl::event_stream<not_null<const HistoryItem*>> _itemLayoutChanges;
rpl::event_stream<not_null<const ViewElement*>> _viewLayoutChanges;
rpl::event_stream<not_null<HistoryItem*>> _newItemAdded;
rpl::event_stream<GiftUpdate> _giftUpdates;
rpl::event_stream<not_null<const HistoryItem*>> _itemRepaintRequest;
rpl::event_stream<not_null<const ViewElement*>> _viewRepaintRequest;
rpl::event_stream<not_null<const HistoryItem*>> _itemResizeRequest;

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