Compare commits
83 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
476e66d027 | ||
|
|
fc11d81673 | ||
|
|
629754a353 | ||
|
|
147dbee051 | ||
|
|
7204c3c25d | ||
|
|
b412241d25 | ||
|
|
f9883afd61 | ||
|
|
181f811f18 | ||
|
|
2f0bd3c085 | ||
|
|
a9d8332766 | ||
|
|
f5036171cf | ||
|
|
cdc8b8e473 | ||
|
|
ab404c5452 | ||
|
|
0585f9d667 | ||
|
|
19225c7dd3 | ||
|
|
8c1844b1c0 | ||
|
|
af63d86e24 | ||
|
|
1f2fd3ad96 | ||
|
|
f41fdcdb98 | ||
|
|
f78a9b4220 | ||
|
|
06b3ce58ed | ||
|
|
7c70d8b1c2 | ||
|
|
2b1e032a9b | ||
|
|
4f5d6a2fd5 | ||
|
|
c3b90aa492 | ||
|
|
bb2daac007 | ||
|
|
bc2449c3f9 | ||
|
|
0bf50de77a | ||
|
|
473bc32b71 | ||
|
|
e5e143dcf8 | ||
|
|
2de08746ac | ||
|
|
ae6833b4d5 | ||
|
|
f1fe5f6a71 | ||
|
|
5054d0615e | ||
|
|
4f5007ea64 | ||
|
|
233f6ed13b | ||
|
|
f15b883471 | ||
|
|
312d5f0121 | ||
|
|
75a1657c49 | ||
|
|
667d92100e | ||
|
|
8ff4bc8cff | ||
|
|
656b262648 | ||
|
|
c1769b9ba2 | ||
|
|
04c9d92b4a | ||
|
|
0babef5a09 | ||
|
|
cd52407723 | ||
|
|
68c0aa7fb9 | ||
|
|
f1622c40a4 | ||
|
|
688e7316eb | ||
|
|
ef5ec47797 | ||
|
|
147ad4a773 | ||
|
|
9752145b49 | ||
|
|
662b862d2f | ||
|
|
6d469509a4 | ||
|
|
0dfbb8a5ae | ||
|
|
1e12ecda70 | ||
|
|
035087987c | ||
|
|
18422c4193 | ||
|
|
f1f9fe27a9 | ||
|
|
10667e14e2 | ||
|
|
b04c7efdf4 | ||
|
|
0119731360 | ||
|
|
8f337684d5 | ||
|
|
dae93552f0 | ||
|
|
351bbb240f | ||
|
|
5716de2e6e | ||
|
|
14295d59d1 | ||
|
|
df0849473c | ||
|
|
9736706894 | ||
|
|
12343a5c31 | ||
|
|
a7f046a617 | ||
|
|
67bf796f1e | ||
|
|
22d632abc3 | ||
|
|
25094c1ee6 | ||
|
|
5b71ad0456 | ||
|
|
9146ba996f | ||
|
|
42900787e1 | ||
|
|
ba10c10a94 | ||
|
|
3f37e9ca6f | ||
|
|
c5ea86b474 | ||
|
|
76720092a5 | ||
|
|
7a75c80b27 | ||
|
|
b2dcbebb5b |
2
.github/workflows/mac.yml
vendored
@@ -40,7 +40,7 @@ jobs:
|
||||
|
||||
macos:
|
||||
name: MacOS
|
||||
runs-on: macos-15-intel
|
||||
runs-on: macos-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
|
||||
2
.github/workflows/snap.yml
vendored
@@ -61,7 +61,7 @@ jobs:
|
||||
sudo lxd waitready
|
||||
|
||||
- name: Free up some disk space.
|
||||
uses: endersonmenezes/free-disk-space@713d134e243b926eba4a5cce0cf608bfd1efb89a
|
||||
uses: endersonmenezes/free-disk-space@6c4664f43348c8c7011b53488d5ca65e9fc5cd1a
|
||||
with:
|
||||
remove_android: true
|
||||
remove_dotnet: true
|
||||
|
||||
@@ -440,6 +440,9 @@ div.toast_shown {
|
||||
.section.stories {
|
||||
background-image: url(../images/section_stories.png);
|
||||
}
|
||||
.section.music {
|
||||
background-image: url(../images/section_music.png);
|
||||
}
|
||||
.section.web {
|
||||
background-image: url(../images/section_web.png);
|
||||
}
|
||||
@@ -481,6 +484,16 @@ div.toast_shown {
|
||||
.media_video .fill {
|
||||
background-image: url(../images/media_video.png)
|
||||
}
|
||||
.audio_icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background-color: #4f9cd9;
|
||||
background-image: url(../images/media_music.png);
|
||||
background-repeat: no-repeat;
|
||||
background-position: 12px 12px;
|
||||
background-size: 24px 24px;
|
||||
}
|
||||
|
||||
@media only screen and (min--moz-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min-device-pixel-ratio: 2) {
|
||||
.section.calls {
|
||||
@@ -504,6 +517,9 @@ div.toast_shown {
|
||||
.section.stories {
|
||||
background-image: url(../images/section_stories@2x.png);
|
||||
}
|
||||
.section.music {
|
||||
background-image: url(../images/section_music@2x.png);
|
||||
}
|
||||
.section.web {
|
||||
background-image: url(../images/section_web@2x.png);
|
||||
}
|
||||
@@ -545,6 +561,9 @@ div.toast_shown {
|
||||
.media_video .fill {
|
||||
background-image: url(../images/media_video@2x.png)
|
||||
}
|
||||
.audio_icon {
|
||||
background-image: url(../images/media_music@2x.png);
|
||||
}
|
||||
}
|
||||
|
||||
.spoiler {
|
||||
|
||||
BIN
Telegram/Resources/export_html/images/section_music.png
Normal file
|
After Width: | Height: | Size: 446 B |
BIN
Telegram/Resources/export_html/images/section_music@2x.png
Normal file
|
After Width: | Height: | Size: 777 B |
BIN
Telegram/Resources/icons/chat/mini_gift_hidden.png
Normal file
|
After Width: | Height: | Size: 544 B |
BIN
Telegram/Resources/icons/chat/mini_gift_hidden@2x.png
Normal file
|
After Width: | Height: | Size: 938 B |
BIN
Telegram/Resources/icons/chat/mini_gift_hidden@3x.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
Telegram/Resources/icons/settings/button_auction.png
Normal file
|
After Width: | Height: | Size: 522 B |
BIN
Telegram/Resources/icons/settings/button_auction@2x.png
Normal file
|
After Width: | Height: | Size: 935 B |
BIN
Telegram/Resources/icons/settings/button_auction@3x.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
Telegram/Resources/icons/settings/toast_auction.png
Normal file
|
After Width: | Height: | Size: 605 B |
BIN
Telegram/Resources/icons/settings/toast_auction@2x.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
Telegram/Resources/icons/settings/toast_auction@3x.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
@@ -328,6 +328,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_proxy_box_password" = "Password";
|
||||
"lng_proxy_invalid" = "The proxy link is invalid.";
|
||||
"lng_proxy_unsupported" = "Your Telegram Desktop version doesn't support this proxy type or the proxy link is invalid. Please update Telegram Desktop to the latest version.";
|
||||
"lng_proxy_incorrect_secret" = "This proxy link uses invalid **secret** parameter. Please contact the proxy provider and ask him to update MTProxy source code and configure it with a correct **secret** value. Then let him provide a new link.";
|
||||
|
||||
"lng_edit_deleted" = "This message was deleted";
|
||||
"lng_edit_limit_reached#one" = "You've reached the message text limit. Please make the text shorter by {count} character.";
|
||||
@@ -1591,6 +1592,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_profile_administrators#one" = "{count} administrator";
|
||||
"lng_profile_administrators#other" = "{count} administrators";
|
||||
"lng_profile_manage" = "Channel settings";
|
||||
"lng_profile_topic_toast" = "This topic contains {name}";
|
||||
|
||||
"lng_invite_upgrade_title" = "Upgrade to Premium";
|
||||
"lng_invite_upgrade_group_invite#one" = "{users} only accepts invitations to groups from Contacts and **Premium** users.";
|
||||
@@ -1682,9 +1684,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_profile_suggest_photo" = "Suggest Profile Photo";
|
||||
"lng_profile_suggest_photo_from_clipboard" = "Suggest From Clipboard";
|
||||
"lng_profile_set_photo_for" = "Set Profile Photo";
|
||||
"lng_profile_set_photo_for_group" = "Set Group Photo";
|
||||
"lng_profile_set_photo_for_channel" = "Set Channel Photo";
|
||||
"lng_profile_set_photo_for_from_clipboard" = "Set From Clipboard";
|
||||
"lng_profile_set_photo_for_about" = "You can replace {user}'s photo with another photo that only you will see.";
|
||||
"lng_profile_photo_reset" = "Reset to Original";
|
||||
"lng_profile_photo_reset_button" = "Reset";
|
||||
"lng_profile_photo_reset_sure" = "Are you sure you want to reset {user}'s photo to the original?";
|
||||
"lng_profile_photo_from_clipboard" = "From clipboard";
|
||||
"lng_profile_suggest_sure" = "You can suggest {user} to set this photo for their Telegram profile.";
|
||||
@@ -2290,7 +2295,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_action_gift_sent_self_channel" = "You sent a gift to {name} for {cost}";
|
||||
"lng_action_gift_self_bought" = "You bought a gift for {cost}";
|
||||
"lng_action_gift_self_auction" = "You've successfully bought a gift in the auction for {cost}.";
|
||||
"lng_action_gift_auction" = "You've successfully bought a gift for {name} in the auction for {cost}.";
|
||||
"lng_action_gift_auction_won" = "You won the auction with a bid of {cost}.";
|
||||
"lng_action_gift_self_subtitle" = "Saved Gift";
|
||||
"lng_action_gift_self_about#one" = "Display this gift on your page or convert it to **{count}** Star.";
|
||||
"lng_action_gift_self_about#other" = "Display this gift on your page or convert it to **{count}** Stars.";
|
||||
@@ -3998,6 +4003,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_auction_about_top_bidders#other" = "top {count} bidders";
|
||||
"lng_auction_about_top_about#one" = "{count} gift is dropped in {rounds} to the {bidders} by bid amount.";
|
||||
"lng_auction_about_top_about#other" = "{count} gifts are dropped in {rounds} to the {bidders} by bid amount.";
|
||||
"lng_auction_about_top_short#one" = "{count} gift is dropped to the {bidders} by bid amount. {link}";
|
||||
"lng_auction_about_top_short#other" = "{count} gifts are dropped to the {bidders} by bid amount. {link}";
|
||||
"lng_auction_about_bid_title" = "Bid Carryover";
|
||||
"lng_auction_about_bid_about#one" = "If your bid leaves the top {count}, it will automatically join the next round.";
|
||||
"lng_auction_about_bid_about#other" = "If your bid leaves the top {count}, it will automatically join the next round.";
|
||||
@@ -4027,7 +4034,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_auction_menu_copy_link" = "Copy Link";
|
||||
"lng_auction_menu_share" = "Share";
|
||||
"lng_auction_bid_title" = "Place a Bid";
|
||||
"lng_auction_bid_subtitle#one" = "Top {count} bidder will win";
|
||||
"lng_auction_bid_subtitle#other" = "Top {count} bidders will win";
|
||||
"lng_auction_bid_your" = "your bid";
|
||||
"lng_auction_bid_custom" = "click to bid more";
|
||||
"lng_auction_bid_threshold#one" = "TOP {count}";
|
||||
"lng_auction_bid_threshold#other" = "TOP {count}";
|
||||
"lng_auction_bid_minimal#one" = "minimum bid";
|
||||
@@ -4045,6 +4055,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_auction_bid_increased_title" = "Your bid has been increased";
|
||||
"lng_auction_bid_done_text#one" = "If you fall below the **top {count}**, your bid will roll over to the next round.";
|
||||
"lng_auction_bid_done_text#other" = "If you fall below the **top {count}**, your bid will roll over to the next round.";
|
||||
"lng_auction_bid_custom_title" = "Custom Amount";
|
||||
"lng_auction_preview_left#one" = "{count} left";
|
||||
"lng_auction_preview_left#other" = "{count} left";
|
||||
"lng_auction_preview_join" = "Join";
|
||||
@@ -6293,6 +6304,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_export_option_contacts_about" = "If you allow access, contacts are continuously synced with Telegram. You can adjust this in Settings > Privacy & Security on mobile devices.";
|
||||
"lng_export_option_stories" = "Story archive";
|
||||
"lng_export_option_stories_about" = "All stories you posted from Telegram mobile apps.";
|
||||
"lng_export_option_profile_music" = "Music on Profiles";
|
||||
"lng_export_option_profile_music_about" = "All tracks you saved to your playlist.";
|
||||
"lng_export_option_sessions" = "Active sessions";
|
||||
"lng_export_option_sessions_about" = "We may store this to display your connected devices in Settings > Privacy & Security > Show all sessions.";
|
||||
"lng_export_header_other" = "Other";
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
<file alias="images/section_contacts@2x.png">../../export_html/images/section_contacts@2x.png</file>
|
||||
<file alias="images/section_frequent.png">../../export_html/images/section_frequent.png</file>
|
||||
<file alias="images/section_frequent@2x.png">../../export_html/images/section_frequent@2x.png</file>
|
||||
<file alias="images/section_music.png">../../export_html/images/section_music.png</file>
|
||||
<file alias="images/section_music@2x.png">../../export_html/images/section_music@2x.png</file>
|
||||
<file alias="images/section_other.png">../../export_html/images/section_other.png</file>
|
||||
<file alias="images/section_other@2x.png">../../export_html/images/section_other@2x.png</file>
|
||||
<file alias="images/section_photos.png">../../export_html/images/section_photos.png</file>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<Identity Name="TelegramMessengerLLP.TelegramDesktop"
|
||||
ProcessorArchitecture="ARCHITECTURE"
|
||||
Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
|
||||
Version="6.3.0.0" />
|
||||
Version="6.3.3.0" />
|
||||
<Properties>
|
||||
<DisplayName>Telegram Desktop</DisplayName>
|
||||
<PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName>
|
||||
|
||||
@@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico"
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 6,3,0,0
|
||||
PRODUCTVERSION 6,3,0,0
|
||||
FILEVERSION 6,3,3,0
|
||||
PRODUCTVERSION 6,3,3,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS 0x1L
|
||||
@@ -62,10 +62,10 @@ BEGIN
|
||||
BEGIN
|
||||
VALUE "CompanyName", "Telegram FZ-LLC"
|
||||
VALUE "FileDescription", "Telegram Desktop"
|
||||
VALUE "FileVersion", "6.3.0.0"
|
||||
VALUE "FileVersion", "6.3.3.0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2014-2025"
|
||||
VALUE "ProductName", "Telegram Desktop"
|
||||
VALUE "ProductVersion", "6.3.0.0"
|
||||
VALUE "ProductVersion", "6.3.3.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
@@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 6,3,0,0
|
||||
PRODUCTVERSION 6,3,0,0
|
||||
FILEVERSION 6,3,3,0
|
||||
PRODUCTVERSION 6,3,3,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS 0x1L
|
||||
@@ -53,10 +53,10 @@ BEGIN
|
||||
BEGIN
|
||||
VALUE "CompanyName", "Telegram FZ-LLC"
|
||||
VALUE "FileDescription", "Telegram Desktop Updater"
|
||||
VALUE "FileVersion", "6.3.0.0"
|
||||
VALUE "FileVersion", "6.3.3.0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2014-2025"
|
||||
VALUE "ProductName", "Telegram Desktop"
|
||||
VALUE "ProductVersion", "6.3.0.0"
|
||||
VALUE "ProductVersion", "6.3.3.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
@@ -30,6 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "data/notify/data_notify_settings.h"
|
||||
#include "data/stickers/data_stickers.h"
|
||||
#include "data/data_saved_messages.h"
|
||||
#include "data/data_saved_sublist.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_user.h"
|
||||
#include "data/data_chat.h"
|
||||
@@ -43,6 +44,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "data/data_histories.h"
|
||||
#include "data/data_folder.h"
|
||||
#include "data/data_forum.h"
|
||||
#include "data/data_forum_topic.h"
|
||||
#include "data/data_send_action.h"
|
||||
#include "data/data_stories.h"
|
||||
#include "data/data_message_reactions.h"
|
||||
@@ -1701,7 +1703,13 @@ void Updates::feedUpdate(const MTPUpdate &update) {
|
||||
local->history()->peer,
|
||||
local->date());
|
||||
}
|
||||
local->setRealId(d.vid().v);
|
||||
local->setRealId(newId);
|
||||
if (const auto topic = local->topic()) {
|
||||
topic->applyMaybeLast(local);
|
||||
}
|
||||
if (const auto sublist = local->savedSublist()) {
|
||||
sublist->applyMaybeLast(local);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -378,10 +378,8 @@ connectionPortInputField: InputField(defaultInputField) {
|
||||
width: 55px;
|
||||
}
|
||||
connectionUserInputField: InputField(defaultInputField) {
|
||||
width: 95px;
|
||||
}
|
||||
connectionPasswordInputField: InputField(defaultInputField) {
|
||||
width: 120px;
|
||||
}
|
||||
connectionIPv6Skip: 11px;
|
||||
|
||||
|
||||
@@ -115,6 +115,7 @@ void AddProxyFromClipboard(
|
||||
Success,
|
||||
Failed,
|
||||
Unsupported,
|
||||
IncorrectSecret,
|
||||
Invalid,
|
||||
};
|
||||
|
||||
@@ -154,8 +155,11 @@ void AddProxyFromClipboard(
|
||||
qthelp::UrlParamNameTransform::ToLower);
|
||||
const auto proxy = ProxyDataFromFields(type, fields);
|
||||
if (!proxy) {
|
||||
return (proxy.status() == ProxyData::Status::Unsupported)
|
||||
const auto status = proxy.status();
|
||||
return (status == ProxyData::Status::Unsupported)
|
||||
? Result::Unsupported
|
||||
: (status == ProxyData::Status::IncorrectSecret)
|
||||
? Result::IncorrectSecret
|
||||
: Result::Invalid;
|
||||
}
|
||||
const auto contains = controller->contains(proxy);
|
||||
@@ -189,9 +193,11 @@ void AddProxyFromClipboard(
|
||||
tr::lng_proxy_add_from_clipboard_failed_toast(tr::now));
|
||||
} else {
|
||||
show->showBox(Ui::MakeInformBox(
|
||||
(success == Result::Unsupported
|
||||
? tr::lng_proxy_unsupported(tr::now)
|
||||
: tr::lng_proxy_invalid(tr::now))));
|
||||
((success == Result::IncorrectSecret)
|
||||
? tr::lng_proxy_incorrect_secret(tr::now, tr::rich)
|
||||
: (success == Result::Unsupported)
|
||||
? tr::lng_proxy_unsupported(tr::now, tr::rich)
|
||||
: tr::lng_proxy_invalid(tr::now, tr::rich))));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1183,7 +1189,7 @@ void ProxyBox::setupSocketAddress(const ProxyData &data) {
|
||||
}
|
||||
|
||||
void ProxyBox::setupCredentials(const ProxyData &data) {
|
||||
_credentials = _content->add(
|
||||
_credentials = _content->add(
|
||||
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
|
||||
_content,
|
||||
object_ptr<Ui::VerticalLayout>(_content)));
|
||||
@@ -1316,10 +1322,13 @@ void ProxiesBoxController::ShowApplyConfirmation(
|
||||
const QMap<QString, QString> &fields) {
|
||||
const auto proxy = ProxyDataFromFields(type, fields);
|
||||
if (!proxy) {
|
||||
const auto status = proxy.status();
|
||||
auto box = Ui::MakeInformBox(
|
||||
(proxy.status() == ProxyData::Status::Unsupported
|
||||
? tr::lng_proxy_unsupported(tr::now)
|
||||
: tr::lng_proxy_invalid(tr::now)));
|
||||
((status == ProxyData::Status::Unsupported)
|
||||
? tr::lng_proxy_unsupported(tr::now, tr::rich)
|
||||
: (status == ProxyData::Status::IncorrectSecret)
|
||||
? tr::lng_proxy_incorrect_secret(tr::now, tr::rich)
|
||||
: tr::lng_proxy_invalid(tr::now, tr::rich)));
|
||||
if (controller) {
|
||||
controller->uiShow()->showBox(std::move(box));
|
||||
} else {
|
||||
|
||||
@@ -316,8 +316,17 @@ void AddUniqueGiftPropertyRows(
|
||||
const Data::CreditsHistoryEntry &entry,
|
||||
Fn<void()> convertToStars) {
|
||||
auto helper = Ui::Text::CustomEmojiHelper();
|
||||
const auto addUpgradeToValue = !entry.credits.ton()
|
||||
&& !entry.giftUpgradeGifted
|
||||
&& !entry.giftUpgradeSeparate
|
||||
&& entry.starsUpgradedBySender;
|
||||
const auto amount = addUpgradeToValue
|
||||
? CreditsAmount(
|
||||
entry.credits.whole() + entry.starsUpgradedBySender,
|
||||
entry.credits.nano())
|
||||
: entry.credits;
|
||||
const auto price = helper.paletteDependent(Ui::Earn::IconCreditsEmoji(
|
||||
)).append(' ').append(Lang::FormatCreditsAmountDecimal(entry.credits));
|
||||
)).append(' ').append(Lang::FormatCreditsAmountDecimal(amount));
|
||||
auto label = object_ptr<Ui::FlatLabel>(
|
||||
table,
|
||||
rpl::single(price),
|
||||
@@ -1328,6 +1337,15 @@ void AddStarGiftTable(
|
||||
PeerId(entry.bareEntryOwnerId)),
|
||||
st::giveawayGiftCodePeerMargin);
|
||||
}
|
||||
} else if (entry.auction && entry.bareGiftOwnerId) {
|
||||
AddTableRow(
|
||||
table,
|
||||
tr::lng_credits_box_history_entry_peer(),
|
||||
MakePeerTableValue(
|
||||
table,
|
||||
show,
|
||||
PeerId(entry.bareGiftOwnerId)),
|
||||
st::giveawayGiftCodePeerMargin);
|
||||
} else if (peerId && !giftToSelf) {
|
||||
const auto user = session->data().peer(peerId)->asUser();
|
||||
const auto withSendButton = entry.in && user && !user->isBot();
|
||||
|
||||
@@ -658,7 +658,7 @@ void Controller::setupPhotoButtons() {
|
||||
_window->session().api().peerPhoto().clearPersonal(_user);
|
||||
close();
|
||||
},
|
||||
.confirmText = tr::lng_profile_photo_reset(tr::now),
|
||||
.confirmText = tr::lng_profile_photo_reset_button(tr::now),
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
@@ -577,6 +577,7 @@ object_ptr<Ui::RpWidget> CreateUserpicsTransfer(
|
||||
rpl::producer<std::vector<not_null<PeerData*>>> from,
|
||||
not_null<PeerData*> to,
|
||||
UserpicsTransferType type) {
|
||||
using Type = UserpicsTransferType;
|
||||
struct State {
|
||||
std::vector<not_null<PeerData*>> from;
|
||||
std::vector<std::unique_ptr<Ui::UserpicButton>> buttons;
|
||||
@@ -676,27 +677,28 @@ object_ptr<Ui::RpWidget> CreateUserpicsTransfer(
|
||||
button->render(&q, position, QRegion(), QWidget::DrawChildren);
|
||||
}
|
||||
state->painting = false;
|
||||
const auto boosting = (type == UserpicsTransferType::BoostReplace);
|
||||
const auto last = state->buttons.back().get();
|
||||
const auto back = boosting ? last : right;
|
||||
const auto add = st::boostReplaceIconAdd;
|
||||
const auto &icon = boosting
|
||||
? st::boostReplaceIcon
|
||||
: st::starrefJoinIcon;
|
||||
const auto skip = boosting ? st::boostReplaceIconSkip : 0;
|
||||
const auto w = icon.width() + 2 * skip;
|
||||
const auto h = icon.height() + 2 * skip;
|
||||
const auto x = back->x() + back->width() - w + add.x();
|
||||
const auto y = back->y() + back->height() - h + add.y();
|
||||
|
||||
auto brush = QLinearGradient(QPointF(x + w, y + h), QPointF(x, y));
|
||||
brush.setStops(Ui::Premium::ButtonGradientStops());
|
||||
q.setBrush(brush);
|
||||
pen.setWidthF(stroke);
|
||||
q.setPen(pen);
|
||||
q.drawEllipse(x - half, y - half, w + stroke, h + stroke);
|
||||
icon.paint(q, x + skip, y + skip, outerw);
|
||||
if (type != Type::AuctionRecipient) {
|
||||
const auto boosting = (type == Type::BoostReplace);
|
||||
const auto back = boosting ? last : right;
|
||||
const auto add = st::boostReplaceIconAdd;
|
||||
const auto &icon = boosting
|
||||
? st::boostReplaceIcon
|
||||
: st::starrefJoinIcon;
|
||||
const auto skip = boosting ? st::boostReplaceIconSkip : 0;
|
||||
const auto w = icon.width() + 2 * skip;
|
||||
const auto h = icon.height() + 2 * skip;
|
||||
const auto x = back->x() + back->width() - w + add.x();
|
||||
const auto y = back->y() + back->height() - h + add.y();
|
||||
|
||||
auto brush = QLinearGradient(QPointF(x + w, y + h), QPointF(x, y));
|
||||
brush.setStops(Ui::Premium::ButtonGradientStops());
|
||||
q.setBrush(brush);
|
||||
pen.setWidthF(stroke);
|
||||
q.setPen(pen);
|
||||
q.drawEllipse(x - half, y - half, w + stroke, h + stroke);
|
||||
icon.paint(q, x + skip, y + skip, outerw);
|
||||
}
|
||||
const auto size = st::boostReplaceArrow.size();
|
||||
st::boostReplaceArrow.paint(
|
||||
q,
|
||||
@@ -705,7 +707,6 @@ object_ptr<Ui::RpWidget> CreateUserpicsTransfer(
|
||||
+ (st::boostReplaceUserpicsSkip - size.width()) / 2),
|
||||
(last->height() - size.height()) / 2,
|
||||
outerw);
|
||||
|
||||
q.end();
|
||||
|
||||
auto p = QPainter(overlay);
|
||||
|
||||
@@ -64,6 +64,7 @@ object_ptr<Ui::BoxContent> ReassignBoostsBox(
|
||||
enum class UserpicsTransferType {
|
||||
BoostReplace,
|
||||
StarRefJoin,
|
||||
AuctionRecipient,
|
||||
};
|
||||
[[nodiscard]] object_ptr<Ui::RpWidget> CreateUserpicsTransfer(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
|
||||
@@ -586,7 +586,7 @@ void ShareBox::showMenu(not_null<Ui::RpWidget*> parent) {
|
||||
uiShow()->showBox(
|
||||
HistoryView::PrepareScheduleBox(
|
||||
this,
|
||||
nullptr, // ChatHelpers::Show for effect attachment.
|
||||
_descriptor.session,
|
||||
sendMenuDetails(),
|
||||
[=](Api::SendOptions options) { submit(options); },
|
||||
action.options,
|
||||
|
||||
@@ -13,6 +13,7 @@ class Show;
|
||||
|
||||
namespace Data {
|
||||
struct GiftAuctionState;
|
||||
struct ActiveAuctions;
|
||||
} // namespace Data
|
||||
|
||||
namespace Info::PeerGifts {
|
||||
@@ -27,6 +28,7 @@ namespace Ui {
|
||||
|
||||
class BoxContent;
|
||||
class RoundButton;
|
||||
class GenericBox;
|
||||
|
||||
[[nodiscard]] rpl::lifetime ShowStarGiftAuction(
|
||||
not_null<Window::SessionController*> controller,
|
||||
@@ -53,4 +55,24 @@ void SetAuctionButtonCountdownText(
|
||||
AuctionButtonCountdownType type,
|
||||
rpl::producer<Data::GiftAuctionState> value);
|
||||
|
||||
void AuctionAboutBox(
|
||||
not_null<GenericBox*> box,
|
||||
int rounds,
|
||||
int giftsPerRound,
|
||||
Fn<void(Fn<void()> close)> understood);
|
||||
|
||||
[[nodiscard]] TextWithEntities ActiveAuctionsTitle(
|
||||
const Data::ActiveAuctions &auctions);
|
||||
struct ManyAuctionsState {
|
||||
TextWithEntities text;
|
||||
bool someOutbid = false;
|
||||
};
|
||||
[[nodiscard]] ManyAuctionsState ActiveAuctionsState(
|
||||
const Data::ActiveAuctions &auctions);
|
||||
[[nodiscard]] rpl::producer<TextWithEntities> ActiveAuctionsButton(
|
||||
const Data::ActiveAuctions &auctions);
|
||||
[[nodiscard]] Fn<void()> ActiveAuctionsCallback(
|
||||
not_null<Window::SessionController*> window,
|
||||
const Data::ActiveAuctions &auctions);
|
||||
|
||||
} // namespace Ui
|
||||
|
||||
@@ -1519,11 +1519,12 @@ void AddUpgradeButton(
|
||||
}
|
||||
|
||||
void AddSoldLeftSlider(
|
||||
not_null<RoundButton*> button,
|
||||
const GiftTypeStars &gift) {
|
||||
not_null<RpWidget*> above,
|
||||
const GiftTypeStars &gift,
|
||||
QMargins added = {}) {
|
||||
const auto still = gift.info.limitedLeft;
|
||||
const auto total = gift.info.limitedCount;
|
||||
const auto slider = CreateChild<RpWidget>(button->parentWidget());
|
||||
const auto slider = CreateChild<RpWidget>(above->parentWidget());
|
||||
struct State {
|
||||
Text::String still;
|
||||
Text::String sold;
|
||||
@@ -1540,13 +1541,13 @@ void AddSoldLeftSlider(
|
||||
state->height = st::giftLimitedPadding.top()
|
||||
+ st::semiboldFont->height
|
||||
+ st::giftLimitedPadding.bottom();
|
||||
button->geometryValue() | rpl::start_with_next([=](QRect geometry) {
|
||||
above->geometryValue() | rpl::start_with_next([=](QRect geometry) {
|
||||
const auto space = st::giftLimitedBox.buttonPadding.top();
|
||||
const auto skip = (space - state->height) / 2;
|
||||
slider->setGeometry(
|
||||
geometry.x(),
|
||||
geometry.x() + added.left(),
|
||||
geometry.y() - skip - state->height,
|
||||
geometry.width(),
|
||||
geometry.width() - added.left() - added.right(),
|
||||
state->height);
|
||||
}, slider->lifetime());
|
||||
slider->paintRequest() | rpl::start_with_next([=] {
|
||||
@@ -4455,6 +4456,7 @@ void SendGiftBox(
|
||||
const GiftDescriptor &descriptor,
|
||||
rpl::producer<Data::GiftAuctionState> auctionState) {
|
||||
const auto stars = std::get_if<GiftTypeStars>(&descriptor);
|
||||
const auto auction = !!auctionState;
|
||||
const auto limited = stars
|
||||
&& (stars->info.limitedCount > stars->info.limitedLeft)
|
||||
&& (stars->info.limitedLeft > 0);
|
||||
@@ -4465,7 +4467,7 @@ void SendGiftBox(
|
||||
: Api::DisallowedGiftTypes();
|
||||
const auto disallowLimited = !peer->isSelf()
|
||||
&& (disallowed & Api::DisallowedGiftType::Limited);
|
||||
box->setStyle(limited ? st::giftLimitedBox : st::giftBox);
|
||||
box->setStyle((limited && !auction) ? st::giftLimitedBox : st::giftBox);
|
||||
box->setWidth(st::boxWideWidth);
|
||||
box->setTitle(tr::lng_gift_send_title());
|
||||
box->addTopButton(st::boxTitleClose, [=] {
|
||||
@@ -4737,7 +4739,47 @@ void SendGiftBox(
|
||||
SendGift(window, peer, api, details, done);
|
||||
});
|
||||
if (limited) {
|
||||
AddSoldLeftSlider(button, *stars);
|
||||
if (auction) {
|
||||
const auto &now = state->auction.current();
|
||||
const auto rounds = now.totalRounds;
|
||||
const auto perRound = now.gift->auctionGiftsPerRound;
|
||||
auto owned = object_ptr<Ui::FlatLabel>(
|
||||
container,
|
||||
rpl::single(tr::lng_auction_about_top_short(
|
||||
tr::now,
|
||||
lt_count,
|
||||
perRound,
|
||||
lt_bidders,
|
||||
tr::lng_auction_about_top_bidders(
|
||||
tr::now,
|
||||
lt_count,
|
||||
perRound,
|
||||
tr::rich),
|
||||
lt_link,
|
||||
tr::lng_auction_text_link(
|
||||
tr::now,
|
||||
lt_arrow,
|
||||
Text::IconEmoji(&st::textMoreIconEmoji),
|
||||
tr::link),
|
||||
tr::rich)),
|
||||
st::defaultDividerLabel.label);
|
||||
const auto label = owned.data();
|
||||
const auto about = container->add(
|
||||
object_ptr<Ui::DividerLabel>(
|
||||
container,
|
||||
std::move(owned),
|
||||
st::defaultBoxDividerLabelPadding),
|
||||
{ 0, st::giftLimitedBox.buttonPadding.top(), 0, 0 });
|
||||
AddSoldLeftSlider(about, *stars, st::boxRowPadding);
|
||||
|
||||
const auto show = window->uiShow();
|
||||
label->setClickHandlerFilter([=](const auto &...) {
|
||||
show->show(Box(AuctionAboutBox, rounds, perRound, nullptr));
|
||||
return false;
|
||||
});
|
||||
} else {
|
||||
AddSoldLeftSlider(button, *stars);
|
||||
}
|
||||
}
|
||||
if (stars && stars->info.auction()) {
|
||||
SetAuctionButtonCountdownText(
|
||||
|
||||
@@ -1727,11 +1727,7 @@ groupCallTopReactorBadge: RoundCheckbox(defaultRoundCheckbox) {
|
||||
border: groupCallMembersBg;
|
||||
}
|
||||
|
||||
confcallLinkMenu: IconButton(boxTitleClose) {
|
||||
icon: icon {{ "title_menu_dots", boxTitleCloseFg }};
|
||||
iconOver: icon {{ "title_menu_dots", boxTitleCloseFgOver }};
|
||||
}
|
||||
groupCallLinkMenu: IconButton(confcallLinkMenu) {
|
||||
groupCallLinkMenu: IconButton(boxTitleMenu) {
|
||||
icon: icon {{ "title_menu_dots", groupCallMemberInactiveIcon }};
|
||||
iconOver: icon {{ "title_menu_dots", groupCallMemberInactiveIcon }};
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
|
||||
@@ -163,6 +163,7 @@ void Panel::Incoming::RendererGL::init(QOpenGLFunctions &f) {
|
||||
}
|
||||
|
||||
void Panel::Incoming::RendererGL::deinit(QOpenGLFunctions *f) {
|
||||
_controlsShadowImage.destroy(f);
|
||||
_textures.destroy(f);
|
||||
_imageProgram = std::nullopt;
|
||||
_texturedVertexShader = nullptr;
|
||||
|
||||
@@ -279,7 +279,7 @@ void ShowConferenceCallLinkBox(
|
||||
if (!args.initial && call->canManage()) {
|
||||
const auto toggle = Ui::CreateChild<Ui::IconButton>(
|
||||
close->parentWidget(),
|
||||
st.menuToggle ? *st.menuToggle : st::confcallLinkMenu);
|
||||
st.menuToggle ? *st.menuToggle : st::boxTitleMenu);
|
||||
const auto handler = [=] {
|
||||
if (state->resetting) {
|
||||
return;
|
||||
|
||||
@@ -164,6 +164,7 @@ void Messages::send(TextWithTags text, int stars) {
|
||||
_sendingIdByRandomId.emplace(randomId, localId);
|
||||
|
||||
const auto from = _call->messagesFrom();
|
||||
const auto creator = _real->creator();
|
||||
const auto skip = skipMessage(prepared, stars);
|
||||
if (skip) {
|
||||
_skippedIds.emplace(localId);
|
||||
@@ -173,7 +174,7 @@ void Messages::send(TextWithTags text, int stars) {
|
||||
.peer = from,
|
||||
.text = std::move(prepared),
|
||||
.stars = stars,
|
||||
.admin = (from == _call->peer()),
|
||||
.admin = (from == _call->peer()) || (creator && from->isSelf()),
|
||||
.mine = true,
|
||||
});
|
||||
}
|
||||
@@ -233,7 +234,8 @@ void Messages::received(const MTPDupdateGroupCallMessage &data) {
|
||||
fields.vfrom_id(),
|
||||
fields.vmessage(),
|
||||
fields.vdate().v,
|
||||
fields.vpaid_message_stars().value_or_empty());
|
||||
fields.vpaid_message_stars().value_or_empty(),
|
||||
fields.is_from_admin());
|
||||
}
|
||||
|
||||
void Messages::received(const MTPDupdateGroupCallEncryptedMessage &data) {
|
||||
@@ -268,6 +270,7 @@ void Messages::received(const MTPDupdateGroupCallEncryptedMessage &data) {
|
||||
deserialized->message,
|
||||
base::unixtime::now(), // date
|
||||
0, // stars
|
||||
false,
|
||||
true); // checkCustomEmoji
|
||||
}
|
||||
|
||||
@@ -332,6 +335,7 @@ void Messages::received(
|
||||
const MTPTextWithEntities &message,
|
||||
TimeId date,
|
||||
int stars,
|
||||
bool fromAdmin,
|
||||
bool checkCustomEmoji) {
|
||||
const auto peer = _call->peer();
|
||||
const auto i = ranges::find(_messages, id, &Message::id);
|
||||
@@ -381,7 +385,7 @@ void Messages::received(
|
||||
.peer = author,
|
||||
.text = std::move(text),
|
||||
.stars = stars,
|
||||
.admin = (author == _call->peer()),
|
||||
.admin = fromAdmin,
|
||||
.mine = mine,
|
||||
});
|
||||
ranges::sort(_messages, ranges::less(), &Message::id);
|
||||
|
||||
@@ -145,6 +145,7 @@ private:
|
||||
const MTPTextWithEntities &message,
|
||||
TimeId date,
|
||||
int stars,
|
||||
bool fromAdmin,
|
||||
bool checkCustomEmoji = false);
|
||||
void sent(uint64 randomId, const MTP::Response &response);
|
||||
void sent(uint64 randomId, MsgId realId);
|
||||
|
||||
@@ -551,7 +551,14 @@ void MessagesUi::updateMessageSize(MessageView &entry) {
|
||||
entry.left = _streamMode ? 0 : (_width - entry.width) / 2;
|
||||
entry.textLeft = leftSkip;
|
||||
entry.textTop = padding.top() + nameHeight;
|
||||
entry.nameWidth = std::min(entry.width - widthSkip, nameWidth);
|
||||
entry.nameWidth = std::min(
|
||||
nameWidth,
|
||||
(entry.width
|
||||
- widthSkip
|
||||
- space
|
||||
- _liveBadge.maxWidth()
|
||||
- space
|
||||
- _adminBadge.maxWidth()));
|
||||
updateReactionPosition(entry);
|
||||
|
||||
const auto contentHeight = entry.textTop + textHeight + padding.bottom();
|
||||
@@ -1275,6 +1282,7 @@ void MessagesUi::setupMessagesWidget() {
|
||||
},
|
||||
.availableWidth = entry.nameWidth,
|
||||
.palette = &st::groupCallMessagePalette,
|
||||
.elisionLines = 1,
|
||||
});
|
||||
const auto liveLeft = x + textLeft + entry.nameWidth + space;
|
||||
_liveBadge.draw(p, {
|
||||
|
||||
@@ -401,10 +401,12 @@ void Viewport::RendererGL::deinit(QOpenGLFunctions *f) {
|
||||
_noiseFramebuffer.destroy(f);
|
||||
for (auto &data : _tileData) {
|
||||
data.textures.destroy(f);
|
||||
data.framebuffers.destroy(f);
|
||||
}
|
||||
_tileData.clear();
|
||||
_tileDataIndices.clear();
|
||||
_buttons.destroy(f);
|
||||
_names.destroy(f);
|
||||
}
|
||||
|
||||
void Viewport::RendererGL::setDefaultViewport(QOpenGLFunctions &f) {
|
||||
|
||||
@@ -451,6 +451,8 @@ emojiPanSlideDuration: 200;
|
||||
emojiPanArea: size(34px, 32px);
|
||||
emojiPanRadius: 8px;
|
||||
emojiPanReactionsPreviewPadding: margins(10px, 20px, 10px, 20px);
|
||||
emojiPanEmojiPreviewMinHeight: 160px;
|
||||
emojiPanEmojiPreviewRadius: 8px + 8px + 4px;
|
||||
|
||||
defaultTabbedSearchCancel: CrossButton {
|
||||
width: 33px;
|
||||
|
||||
@@ -47,6 +47,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "emoji_suggestions_helper.h"
|
||||
#include "main/main_session.h"
|
||||
#include "main/main_session_settings.h"
|
||||
#include "mainwidget.h"
|
||||
#include "core/core_settings.h"
|
||||
#include "core/application.h"
|
||||
#include "settings/settings_premium.h"
|
||||
@@ -710,12 +711,22 @@ void EmojiListWidget::ensureMediaPreview() {
|
||||
? controller->sessionController()
|
||||
: nullptr;
|
||||
if (sessionController) {
|
||||
_mediaPreview.create(_mediaPreviewParent, sessionController);
|
||||
_mediaPreview->setCustomPadding(st::emojiPanReactionsPreviewPadding);
|
||||
_mediaPreview->setBackgroundMargins(_mediaPreviewMargins);
|
||||
_mediaPreview->setCornersSkip(st::emojiPanRadius - st::lineWidth);
|
||||
const auto tooSmall = _mediaPreviewParent->height()
|
||||
< st::emojiPanEmojiPreviewMinHeight;
|
||||
const auto parent = tooSmall
|
||||
? sessionController->content()
|
||||
: _mediaPreviewParent;
|
||||
_mediaPreview = base::make_unique_q<Window::MediaPreviewWidget>(
|
||||
parent,
|
||||
sessionController);
|
||||
if (!tooSmall) {
|
||||
_mediaPreview->setCustomPadding(
|
||||
st::emojiPanReactionsPreviewPadding);
|
||||
_mediaPreview->setBackgroundMargins(_mediaPreviewMargins);
|
||||
_mediaPreview->setCustomRadius(st::emojiPanEmojiPreviewRadius);
|
||||
}
|
||||
_mediaPreview->show();
|
||||
_mediaPreview->setGeometry(_mediaPreviewParent->geometry());
|
||||
_mediaPreview->setGeometry(parent->geometry());
|
||||
_mediaPreview->raise();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -487,7 +487,7 @@ private:
|
||||
bool _previewShown = false;
|
||||
|
||||
|
||||
object_ptr<Window::MediaPreviewWidget> _mediaPreview = { nullptr };
|
||||
base::unique_qptr<Window::MediaPreviewWidget> _mediaPreview;
|
||||
|
||||
rpl::event_stream<EmojiChosen> _chosen;
|
||||
rpl::event_stream<FileChosen> _customChosen;
|
||||
|
||||
@@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "platform/platform_specific.h"
|
||||
#include "core/application.h"
|
||||
#include "base/event_filter.h"
|
||||
#include "base/integration.h"
|
||||
#include "main/main_session.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_document.h"
|
||||
@@ -750,11 +751,11 @@ SuggestionsController::SuggestionsController(
|
||||
};
|
||||
_outerFilter.reset(base::install_event_filter(outer, outerCallback));
|
||||
|
||||
QObject::connect(
|
||||
_field,
|
||||
&QTextEdit::textChanged,
|
||||
_container,
|
||||
[=] { handleTextChange(); });
|
||||
QObject::connect(_field, &QTextEdit::textChanged, _container, [=] {
|
||||
base::Integration::Instance().enterFromEventLoop([&] {
|
||||
handleTextChange();
|
||||
});
|
||||
});
|
||||
QObject::connect(
|
||||
_field,
|
||||
&QTextEdit::cursorPositionChanged,
|
||||
|
||||
@@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D1ED}"_cs;
|
||||
constexpr auto AppNameOld = "Telegram Win (Unofficial)"_cs;
|
||||
constexpr auto AppName = "Telegram Desktop"_cs;
|
||||
constexpr auto AppFile = "Telegram"_cs;
|
||||
constexpr auto AppVersion = 6003000;
|
||||
constexpr auto AppVersionStr = "6.3";
|
||||
constexpr auto AppVersion = 6003003;
|
||||
constexpr auto AppVersionStr = "6.3.3";
|
||||
constexpr auto AppBetaVersion = false;
|
||||
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;
|
||||
|
||||
@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "data/components/gift_auctions.h"
|
||||
|
||||
#include "api/api_hash.h"
|
||||
#include "api/api_premium.h"
|
||||
#include "api/api_text_entities.h"
|
||||
#include "apiwrap.h"
|
||||
@@ -18,6 +19,16 @@ namespace Data {
|
||||
GiftAuctions::GiftAuctions(not_null<Main::Session*> session)
|
||||
: _session(session)
|
||||
, _timer([=] { checkSubscriptions(); }) {
|
||||
crl::on_main(_session, [=] {
|
||||
rpl::merge(
|
||||
_session->data().chatsListChanges(),
|
||||
_session->data().chatsListLoadedEvents()
|
||||
) | rpl::filter(
|
||||
!rpl::mappers::_1
|
||||
) | rpl::take(1) | rpl::start_with_next([=] {
|
||||
requestActive();
|
||||
}, _lifetime);
|
||||
});
|
||||
}
|
||||
|
||||
GiftAuctions::~GiftAuctions() = default;
|
||||
@@ -50,13 +61,27 @@ rpl::producer<GiftAuctionState> GiftAuctions::state(const QString &slug) {
|
||||
|
||||
void GiftAuctions::apply(const MTPDupdateStarGiftAuctionState &data) {
|
||||
if (const auto entry = find(data.vgift_id().v)) {
|
||||
const auto was = myStateKey(entry->state);
|
||||
apply(entry, data.vstate());
|
||||
entry->changes.fire({});
|
||||
if (was != myStateKey(entry->state)) {
|
||||
_activeChanged.fire({});
|
||||
}
|
||||
} else {
|
||||
requestActive();
|
||||
}
|
||||
}
|
||||
|
||||
void GiftAuctions::apply(const MTPDupdateStarGiftAuctionUserState &data) {
|
||||
if (const auto entry = find(data.vgift_id().v)) {
|
||||
const auto was = myStateKey(entry->state);
|
||||
apply(entry, data.vuser_state());
|
||||
entry->changes.fire({});
|
||||
if (was != myStateKey(entry->state)) {
|
||||
_activeChanged.fire({});
|
||||
}
|
||||
} else {
|
||||
requestActive();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,6 +129,37 @@ void GiftAuctions::requestAcquired(
|
||||
}).send();
|
||||
}
|
||||
|
||||
rpl::producer<ActiveAuctions> GiftAuctions::active() const {
|
||||
return _activeChanged.events_starting_with_copy(
|
||||
rpl::empty
|
||||
) | rpl::map([=] {
|
||||
return collectActive();
|
||||
});
|
||||
}
|
||||
|
||||
rpl::producer<bool> GiftAuctions::hasActiveChanges() const {
|
||||
const auto has = hasActive();
|
||||
return _activeChanged.events(
|
||||
) | rpl::map([=] {
|
||||
return hasActive();
|
||||
}) | rpl::combine_previous(
|
||||
has
|
||||
) | rpl::filter([=](bool previous, bool current) {
|
||||
return previous != current;
|
||||
}) | rpl::map([=](bool previous, bool current) {
|
||||
return current;
|
||||
});
|
||||
}
|
||||
|
||||
bool GiftAuctions::hasActive() const {
|
||||
for (const auto &[slug, entry] : _map) {
|
||||
if (myStateKey(entry->state)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void GiftAuctions::checkSubscriptions() {
|
||||
const auto now = crl::now();
|
||||
auto next = crl::time();
|
||||
@@ -126,6 +182,101 @@ void GiftAuctions::checkSubscriptions() {
|
||||
}
|
||||
}
|
||||
|
||||
auto GiftAuctions::myStateKey(const GiftAuctionState &state) const
|
||||
-> MyStateKey {
|
||||
if (!state.my.bid) {
|
||||
return {};
|
||||
}
|
||||
auto min = 0;
|
||||
for (const auto &level : state.bidLevels) {
|
||||
if (level.position > state.gift->auctionGiftsPerRound) {
|
||||
break;
|
||||
} else if (!min || min > level.amount) {
|
||||
min = level.amount;
|
||||
}
|
||||
}
|
||||
return {
|
||||
.bid = int(state.my.bid),
|
||||
.position = MyAuctionPosition(state),
|
||||
.version = state.version,
|
||||
};
|
||||
}
|
||||
|
||||
ActiveAuctions GiftAuctions::collectActive() const {
|
||||
auto result = ActiveAuctions();
|
||||
result.list.reserve(_map.size());
|
||||
for (const auto &[slug, entry] : _map) {
|
||||
const auto raw = &entry->state;
|
||||
if (raw->gift && raw->my.date) {
|
||||
result.list.push_back(raw);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
uint64 GiftAuctions::countActiveHash() const {
|
||||
auto result = Api::HashInit();
|
||||
for (const auto &active : collectActive().list) {
|
||||
Api::HashUpdate(result, active->version);
|
||||
Api::HashUpdate(result, active->my.date);
|
||||
}
|
||||
return Api::HashFinalize(result);
|
||||
}
|
||||
|
||||
void GiftAuctions::requestActive() {
|
||||
if (_activeRequestId) {
|
||||
return;
|
||||
}
|
||||
_activeRequestId = _session->api().request(
|
||||
MTPpayments_GetStarGiftActiveAuctions(MTP_long(countActiveHash()))
|
||||
).done([=](const MTPpayments_StarGiftActiveAuctions &result) {
|
||||
result.match([=](const MTPDpayments_starGiftActiveAuctions &data) {
|
||||
const auto owner = &_session->data();
|
||||
owner->processUsers(data.vusers());
|
||||
|
||||
auto giftsFound = base::flat_set<QString>();
|
||||
const auto &list = data.vauctions().v;
|
||||
giftsFound.reserve(list.size());
|
||||
for (const auto &auction : list) {
|
||||
const auto &data = auction.data();
|
||||
auto gift = Api::FromTL(_session, data.vgift());
|
||||
const auto slug = gift ? gift->auctionSlug : QString();
|
||||
if (slug.isEmpty()) {
|
||||
LOG(("Api Error: Bad auction gift."));
|
||||
continue;
|
||||
}
|
||||
auto &entry = _map[slug];
|
||||
if (!entry) {
|
||||
entry = std::make_unique<Entry>();
|
||||
}
|
||||
const auto raw = entry.get();
|
||||
if (!raw->state.gift) {
|
||||
raw->state.gift = std::move(gift);
|
||||
}
|
||||
apply(raw, data.vstate());
|
||||
apply(raw, data.vuser_state());
|
||||
giftsFound.emplace(slug);
|
||||
}
|
||||
for (const auto &[slug, entry] : _map) {
|
||||
const auto my = &entry->state.my;
|
||||
if (my->date && !giftsFound.contains(slug)) {
|
||||
my->to = nullptr;
|
||||
my->minBidAmount = 0;
|
||||
my->bid = 0;
|
||||
my->date = 0;
|
||||
my->returned = false;
|
||||
giftsFound.emplace(slug);
|
||||
}
|
||||
}
|
||||
for (const auto &slug : giftsFound) {
|
||||
_map[slug]->changes.fire({});
|
||||
}
|
||||
_activeChanged.fire({});
|
||||
}, [](const MTPDpayments_starGiftActiveAuctionsNotModified &) {
|
||||
});
|
||||
}).send();
|
||||
}
|
||||
|
||||
void GiftAuctions::request(const QString &slug) {
|
||||
auto &entry = _map[slug];
|
||||
Assert(entry != nullptr);
|
||||
@@ -142,6 +293,8 @@ void GiftAuctions::request(const QString &slug) {
|
||||
raw->requested = false;
|
||||
const auto &data = result.data();
|
||||
|
||||
_session->data().processUsers(data.vusers());
|
||||
|
||||
raw->state.gift = Api::FromTL(_session, data.vgift());
|
||||
if (!raw->state.gift) {
|
||||
return;
|
||||
@@ -150,8 +303,7 @@ void GiftAuctions::request(const QString &slug) {
|
||||
const auto ms = timeout * crl::time(1000);
|
||||
raw->state.subscribedTill = ms ? (crl::now() + ms) : -1;
|
||||
|
||||
_session->data().processUsers(data.vusers());
|
||||
|
||||
const auto was = myStateKey(raw->state);
|
||||
apply(raw, data.vstate());
|
||||
apply(raw, data.vuser_state());
|
||||
if (raw->changes.has_consumers()) {
|
||||
@@ -160,6 +312,9 @@ void GiftAuctions::request(const QString &slug) {
|
||||
_timer.callOnce(ms);
|
||||
}
|
||||
}
|
||||
if (was != myStateKey(raw->state)) {
|
||||
_activeChanged.fire({});
|
||||
}
|
||||
}).send();
|
||||
}
|
||||
|
||||
@@ -175,49 +330,54 @@ GiftAuctions::Entry *GiftAuctions::find(uint64 giftId) const {
|
||||
void GiftAuctions::apply(
|
||||
not_null<Entry*> entry,
|
||||
const MTPStarGiftAuctionState &state) {
|
||||
Expects(entry->state.gift.has_value());
|
||||
apply(&entry->state, state);
|
||||
}
|
||||
|
||||
void GiftAuctions::apply(
|
||||
not_null<GiftAuctionState*> entry,
|
||||
const MTPStarGiftAuctionState &state) {
|
||||
Expects(entry->gift.has_value());
|
||||
|
||||
const auto raw = &entry->state;
|
||||
state.match([&](const MTPDstarGiftAuctionState &data) {
|
||||
const auto version = data.vversion().v;
|
||||
if (raw->version >= version) {
|
||||
if (entry->version >= version) {
|
||||
return;
|
||||
}
|
||||
const auto owner = &_session->data();
|
||||
raw->startDate = data.vstart_date().v;
|
||||
raw->endDate = data.vend_date().v;
|
||||
raw->minBidAmount = data.vmin_bid_amount().v;
|
||||
entry->startDate = data.vstart_date().v;
|
||||
entry->endDate = data.vend_date().v;
|
||||
entry->minBidAmount = data.vmin_bid_amount().v;
|
||||
const auto &levels = data.vbid_levels().v;
|
||||
raw->bidLevels.clear();
|
||||
raw->bidLevels.reserve(levels.size());
|
||||
entry->bidLevels.clear();
|
||||
entry->bidLevels.reserve(levels.size());
|
||||
for (const auto &level : levels) {
|
||||
auto &entry = raw->bidLevels.emplace_back();
|
||||
auto &bid = entry->bidLevels.emplace_back();
|
||||
const auto &data = level.data();
|
||||
entry.amount = data.vamount().v;
|
||||
entry.position = data.vpos().v;
|
||||
entry.date = data.vdate().v;
|
||||
bid.amount = data.vamount().v;
|
||||
bid.position = data.vpos().v;
|
||||
bid.date = data.vdate().v;
|
||||
}
|
||||
const auto &top = data.vtop_bidders().v;
|
||||
raw->topBidders.clear();
|
||||
raw->topBidders.reserve(top.size());
|
||||
entry->topBidders.clear();
|
||||
entry->topBidders.reserve(top.size());
|
||||
for (const auto &user : top) {
|
||||
raw->topBidders.push_back(owner->user(UserId(user.v)));
|
||||
entry->topBidders.push_back(owner->user(UserId(user.v)));
|
||||
}
|
||||
raw->nextRoundAt = data.vnext_round_at().v;
|
||||
raw->giftsLeft = data.vgifts_left().v;
|
||||
raw->currentRound = data.vcurrent_round().v;
|
||||
raw->totalRounds = data.vtotal_rounds().v;
|
||||
raw->averagePrice = 0;
|
||||
entry->nextRoundAt = data.vnext_round_at().v;
|
||||
entry->giftsLeft = data.vgifts_left().v;
|
||||
entry->currentRound = data.vcurrent_round().v;
|
||||
entry->totalRounds = data.vtotal_rounds().v;
|
||||
entry->averagePrice = 0;
|
||||
}, [&](const MTPDstarGiftAuctionStateFinished &data) {
|
||||
raw->averagePrice = data.vaverage_price().v;
|
||||
raw->startDate = data.vstart_date().v;
|
||||
raw->endDate = data.vend_date().v;
|
||||
raw->minBidAmount = 0;
|
||||
raw->nextRoundAt
|
||||
= raw->currentRound
|
||||
= raw->totalRounds
|
||||
= raw->giftsLeft
|
||||
= raw->version
|
||||
entry->averagePrice = data.vaverage_price().v;
|
||||
entry->startDate = data.vstart_date().v;
|
||||
entry->endDate = data.vend_date().v;
|
||||
entry->minBidAmount = 0;
|
||||
entry->nextRoundAt
|
||||
= entry->currentRound
|
||||
= entry->totalRounds
|
||||
= entry->giftsLeft
|
||||
= entry->version
|
||||
= 0;
|
||||
}, [&](const MTPDstarGiftAuctionStateNotModified &data) {
|
||||
});
|
||||
@@ -226,16 +386,32 @@ void GiftAuctions::apply(
|
||||
void GiftAuctions::apply(
|
||||
not_null<Entry*> entry,
|
||||
const MTPStarGiftAuctionUserState &state) {
|
||||
apply(&entry->state.my, state);
|
||||
}
|
||||
|
||||
void GiftAuctions::apply(
|
||||
not_null<StarGiftAuctionMyState*> entry,
|
||||
const MTPStarGiftAuctionUserState &state) {
|
||||
const auto &data = state.data();
|
||||
const auto raw = &entry->state.my;
|
||||
raw->to = data.vbid_peer()
|
||||
entry->to = data.vbid_peer()
|
||||
? _session->data().peer(peerFromMTP(*data.vbid_peer())).get()
|
||||
: nullptr;
|
||||
raw->minBidAmount = data.vmin_bid_amount().value_or(0);
|
||||
raw->bid = data.vbid_amount().value_or(0);
|
||||
raw->date = data.vbid_date().value_or(0);
|
||||
raw->gotCount = data.vacquired_count().v;
|
||||
raw->returned = data.is_returned();
|
||||
entry->minBidAmount = data.vmin_bid_amount().value_or(0);
|
||||
entry->bid = data.vbid_amount().value_or(0);
|
||||
entry->date = data.vbid_date().value_or(0);
|
||||
entry->gotCount = data.vacquired_count().v;
|
||||
entry->returned = data.is_returned();
|
||||
}
|
||||
|
||||
int MyAuctionPosition(const GiftAuctionState &state) {
|
||||
const auto &levels = state.bidLevels;
|
||||
for (auto i = begin(levels), e = end(levels); i != e; ++i) {
|
||||
if (i->amount < state.my.bid
|
||||
|| (i->amount == state.my.bid && i->date > state.my.date)) {
|
||||
return i->position;
|
||||
}
|
||||
}
|
||||
return (levels.empty() ? 0 : levels.back().position) + 1;
|
||||
}
|
||||
|
||||
} // namespace Data
|
||||
|
||||
@@ -62,6 +62,10 @@ struct GiftAcquired {
|
||||
bool nameHidden = false;
|
||||
};
|
||||
|
||||
struct ActiveAuctions {
|
||||
std::vector<not_null<GiftAuctionState*>> list;
|
||||
};
|
||||
|
||||
class GiftAuctions final {
|
||||
public:
|
||||
explicit GiftAuctions(not_null<Main::Session*> session);
|
||||
@@ -73,31 +77,63 @@ public:
|
||||
void apply(const MTPDupdateStarGiftAuctionUserState &data);
|
||||
|
||||
void requestAcquired(
|
||||
uint64 giftId,
|
||||
uint64 giftId,
|
||||
Fn<void(std::vector<Data::GiftAcquired>)> done);
|
||||
|
||||
[[nodiscard]] rpl::producer<ActiveAuctions> active() const;
|
||||
[[nodiscard]] rpl::producer<bool> hasActiveChanges() const;
|
||||
[[nodiscard]] bool hasActive() const;
|
||||
|
||||
private:
|
||||
struct Entry {
|
||||
GiftAuctionState state;
|
||||
rpl::event_stream<> changes;
|
||||
bool requested = false;
|
||||
};
|
||||
struct MyStateKey {
|
||||
int bid = 0;
|
||||
int position = 0;
|
||||
int version = 0;
|
||||
|
||||
explicit operator bool() const {
|
||||
return bid != 0;
|
||||
}
|
||||
friend inline bool operator==(MyStateKey, MyStateKey) = default;
|
||||
};
|
||||
|
||||
void request(const QString &slug);
|
||||
Entry *find(uint64 giftId) const;
|
||||
void apply(
|
||||
not_null<Entry*> entry,
|
||||
const MTPStarGiftAuctionState &state);
|
||||
void apply(
|
||||
not_null<GiftAuctionState*> entry,
|
||||
const MTPStarGiftAuctionState &state);
|
||||
void apply(
|
||||
not_null<Entry*> entry,
|
||||
const MTPStarGiftAuctionUserState &state);
|
||||
void apply(
|
||||
not_null<StarGiftAuctionMyState*> entry,
|
||||
const MTPStarGiftAuctionUserState &state);
|
||||
void checkSubscriptions();
|
||||
|
||||
[[nodiscard]] MyStateKey myStateKey(const GiftAuctionState &state) const;
|
||||
[[nodiscard]] ActiveAuctions collectActive() const;
|
||||
[[nodiscard]] uint64 countActiveHash() const;
|
||||
void requestActive();
|
||||
|
||||
const not_null<Main::Session*> _session;
|
||||
|
||||
base::Timer _timer;
|
||||
base::flat_map<QString, std::unique_ptr<Entry>> _map;
|
||||
|
||||
rpl::event_stream<> _activeChanged;
|
||||
mtpRequestId _activeRequestId = 0;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] int MyAuctionPosition(const GiftAuctionState &state);
|
||||
|
||||
} // namespace Data
|
||||
|
||||
@@ -104,11 +104,13 @@ struct CreditsHistoryEntry final {
|
||||
bool converted : 1 = false;
|
||||
bool anonymous : 1 = false;
|
||||
bool stargift : 1 = false;
|
||||
bool auction : 1 = false;
|
||||
bool postsSearch : 1 = false;
|
||||
bool giftTransferred : 1 = false;
|
||||
bool giftRefunded : 1 = false;
|
||||
bool giftUpgraded : 1 = false;
|
||||
bool giftUpgradeSeparate : 1 = false;
|
||||
bool giftUpgradeGifted : 1 = false;
|
||||
bool giftResale : 1 = false;
|
||||
bool giftResaleForceTon : 1 = false;
|
||||
bool giftPinned : 1 = false;
|
||||
|
||||
@@ -836,6 +836,16 @@ void ForumTopic::applyColorId(int32 colorId) {
|
||||
}
|
||||
}
|
||||
|
||||
void ForumTopic::applyMaybeLast(not_null<HistoryItem*> item) {
|
||||
if (!_lastServerMessage.value_or(nullptr)
|
||||
|| (*_lastServerMessage)->id < item->id) {
|
||||
setLastServerMessage(item);
|
||||
resolveChatListMessageGroup();
|
||||
} else {
|
||||
growLastKnownServerMessageId(item->id);
|
||||
}
|
||||
}
|
||||
|
||||
void ForumTopic::applyItemAdded(not_null<HistoryItem*> item) {
|
||||
if (item->isRegular()) {
|
||||
setLastServerMessage(item);
|
||||
|
||||
@@ -160,6 +160,7 @@ public:
|
||||
void applyCreator(PeerId creatorId);
|
||||
void applyCreationDate(TimeId date);
|
||||
void applyIsMy(bool my);
|
||||
void applyMaybeLast(not_null<HistoryItem*> item);
|
||||
void applyItemAdded(not_null<HistoryItem*> item);
|
||||
void applyItemRemoved(MsgId id);
|
||||
void maybeSetLastMessage(not_null<HistoryItem*> item);
|
||||
|
||||
@@ -147,6 +147,10 @@ GroupCallOrigin GroupCall::origin() const {
|
||||
: GroupCallOrigin::Group;
|
||||
}
|
||||
|
||||
bool GroupCall::creator() const {
|
||||
return _creator;
|
||||
}
|
||||
|
||||
bool GroupCall::canManage() const {
|
||||
return _conference ? _creator : _peer->canManageGroupCall();
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@ public:
|
||||
[[nodiscard]] rpl::producer<bool> loadedValue() const;
|
||||
[[nodiscard]] bool rtmp() const;
|
||||
[[nodiscard]] GroupCallOrigin origin() const;
|
||||
[[nodiscard]] bool creator() const;
|
||||
[[nodiscard]] bool canManage() const;
|
||||
[[nodiscard]] bool listenersHidden() const;
|
||||
[[nodiscard]] bool blockchainMayBeEmpty() const;
|
||||
|
||||
@@ -2616,7 +2616,9 @@ std::unique_ptr<HistoryView::Media> MediaGiftBox::createView(
|
||||
HistoryView::GenerateUniqueGiftMedia(message, replacing, unique),
|
||||
HistoryView::MediaGenericDescriptor{
|
||||
.maxWidth = st::msgServiceGiftBoxSize.width(),
|
||||
.paintBg = HistoryView::UniqueGiftBg(message, unique),
|
||||
.paintBgFactory = [=] {
|
||||
return HistoryView::UniqueGiftBg(message, unique);
|
||||
},
|
||||
.service = true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ struct GiveawayResults {
|
||||
};
|
||||
|
||||
enum class GiftType : uchar {
|
||||
Premium, // count - months
|
||||
Premium, // count - days
|
||||
Credits, // count - credits
|
||||
Ton, // count - nano tons
|
||||
StarGift, // count - stars
|
||||
@@ -149,6 +149,7 @@ struct GiftCode {
|
||||
PeerData *stargiftReleasedBy = nullptr;
|
||||
std::shared_ptr<UniqueGift> unique;
|
||||
TextWithEntities message;
|
||||
PeerData *auctionTo = nullptr;
|
||||
ChannelData *channel = nullptr;
|
||||
PeerData *channelFrom = nullptr;
|
||||
uint64 channelSavedId = 0;
|
||||
@@ -159,6 +160,7 @@ struct GiftCode {
|
||||
int starsToUpgrade = 0;
|
||||
int starsUpgradedBySender = 0;
|
||||
int starsForDetailsRemove = 0;
|
||||
int starsBid = 0;
|
||||
int limitedCount = 0;
|
||||
int limitedLeft = 0;
|
||||
int64 count = 0;
|
||||
@@ -166,6 +168,7 @@ struct GiftCode {
|
||||
bool viaGiveaway : 1 = false;
|
||||
bool transferred : 1 = false;
|
||||
bool upgradeSeparate : 1 = false;
|
||||
bool upgradeGifted : 1 = false;
|
||||
bool upgradable : 1 = false;
|
||||
bool unclaimed : 1 = false;
|
||||
bool anonymous : 1 = false;
|
||||
|
||||
@@ -157,6 +157,38 @@ void SavedMusic::remove(not_null<DocumentData*> document) {
|
||||
_changed.fire_copy(peerId);
|
||||
}
|
||||
|
||||
void SavedMusic::reorder(int oldPosition, int newPosition) {
|
||||
const auto peerId = _owner->session().userPeerId();
|
||||
auto &entry = _entries[peerId];
|
||||
if (oldPosition < 0 || newPosition < 0
|
||||
|| oldPosition >= entry.list.size()
|
||||
|| newPosition >= entry.list.size()
|
||||
|| oldPosition == newPosition) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto item = entry.list[oldPosition];
|
||||
const auto document = ItemDocument(item);
|
||||
|
||||
base::reorder(entry.list, oldPosition, newPosition);
|
||||
|
||||
const auto afterDocument = (newPosition > 0)
|
||||
? ItemDocument(entry.list[newPosition - 1]).get()
|
||||
: nullptr;
|
||||
|
||||
_owner->session().api().request(MTPaccount_SaveMusic(
|
||||
MTP_flags(afterDocument
|
||||
? MTPaccount_SaveMusic::Flag::f_after_id
|
||||
: MTPaccount_SaveMusic::Flags(0)),
|
||||
document->mtpInput(),
|
||||
afterDocument ? afterDocument->mtpInput() : MTPInputDocument()
|
||||
)).done([=] {
|
||||
}).fail([=](const MTP::Error &error) {
|
||||
}).send();
|
||||
|
||||
_changed.fire_copy(peerId);
|
||||
}
|
||||
|
||||
void SavedMusic::apply(not_null<UserData*> user, const MTPDocument *last) {
|
||||
const auto peerId = user->id;
|
||||
auto &entry = _entries[peerId];
|
||||
|
||||
@@ -37,6 +37,7 @@ public:
|
||||
[[nodiscard]] bool has(not_null<DocumentData*> document) const;
|
||||
void save(not_null<DocumentData*> document, FileOrigin origin);
|
||||
void remove(not_null<DocumentData*> document);
|
||||
void reorder(int oldPosition, int newPosition);
|
||||
|
||||
void apply(not_null<UserData*> user, const MTPDocument *last);
|
||||
|
||||
|
||||
@@ -187,12 +187,13 @@ rpl::producer<> SavedSublist::destroyed() const {
|
||||
) | rpl::to_empty);
|
||||
}
|
||||
|
||||
void SavedSublist::applyMaybeLast(not_null<HistoryItem*> item, bool added) {
|
||||
growLastKnownServerMessageId(item->id);
|
||||
void SavedSublist::applyMaybeLast(not_null<HistoryItem*> item) {
|
||||
if (!_lastServerMessage.value_or(nullptr)
|
||||
|| (*_lastServerMessage)->id < item->id) {
|
||||
setLastServerMessage(item);
|
||||
resolveChatListMessageGroup();
|
||||
} else {
|
||||
growLastKnownServerMessageId(item->id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -52,8 +52,7 @@ public:
|
||||
[[nodiscard]] bool isHiddenAuthor() const;
|
||||
[[nodiscard]] rpl::producer<> destroyed() const;
|
||||
|
||||
void growLastKnownServerMessageId(MsgId id);
|
||||
void applyMaybeLast(not_null<HistoryItem*> item, bool added = false);
|
||||
void applyMaybeLast(not_null<HistoryItem*> item);
|
||||
void applyItemAdded(not_null<HistoryItem*> item);
|
||||
void applyItemRemoved(MsgId id);
|
||||
|
||||
@@ -143,6 +142,7 @@ private:
|
||||
void setChatListMessage(HistoryItem *item);
|
||||
void allowChatListMessageResolve();
|
||||
void resolveChatListMessageGroup();
|
||||
void growLastKnownServerMessageId(MsgId id);
|
||||
|
||||
void changeUnreadCountByMessage(MsgId id, int delta);
|
||||
void setUnreadCount(std::optional<int> count);
|
||||
|
||||
@@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "core/mime_type.h" // Core::IsMimeSticker
|
||||
#include "ui/image/image_location_factory.h" // Images::FromPhotoSize
|
||||
#include "ui/text/format_values.h" // Ui::FormatPhone
|
||||
#include "ui/color_int_conversion.h"
|
||||
#include "export/export_manager.h"
|
||||
#include "export/view/export_view_panel_controller.h"
|
||||
#include "mtproto/mtproto_config.h"
|
||||
@@ -1877,6 +1878,14 @@ rpl::producer<GiftsUpdate> Session::giftsUpdates() const {
|
||||
return _giftsUpdates.events();
|
||||
}
|
||||
|
||||
void Session::notifyGiftAuctionGot(GiftAuctionGot &&update) {
|
||||
_giftAuctionGots.fire(std::move(update));
|
||||
}
|
||||
|
||||
rpl::producer<GiftAuctionGot> Session::giftAuctionGots() const {
|
||||
return _giftAuctionGots.events();
|
||||
}
|
||||
|
||||
HistoryItem *Session::changeMessageId(PeerId peerId, MsgId wasId, MsgId nowId) {
|
||||
const auto list = messagesListForInsert(peerId);
|
||||
const auto i = list->find(wasId);
|
||||
@@ -3782,6 +3791,7 @@ not_null<WebPageData*> Session::processWebpage(
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
0,
|
||||
QString(),
|
||||
false,
|
||||
@@ -3852,6 +3862,7 @@ not_null<WebPageData*> Session::webpage(
|
||||
std::move(iv),
|
||||
std::move(stickerSet),
|
||||
std::move(uniqueGift),
|
||||
nullptr,
|
||||
duration,
|
||||
author,
|
||||
hasLargeMedia,
|
||||
@@ -3959,6 +3970,35 @@ void Session::webpageApplyFields(
|
||||
return nullptr;
|
||||
};
|
||||
|
||||
using WebPageAuctionPtr = std::unique_ptr<WebPageAuction>;
|
||||
const auto lookupAuction = [&]() -> WebPageAuctionPtr {
|
||||
const auto toUint = [](const MTPint &c) {
|
||||
return (uint32(1) << 24) | uint32(c.v);
|
||||
};
|
||||
if (const auto attributes = data.vattributes()) {
|
||||
for (const auto &attribute : attributes->v) {
|
||||
return attribute.match([&](
|
||||
const MTPDwebPageAttributeStarGiftAuction &data) {
|
||||
const auto gift = Api::FromTL(_session, data.vgift());
|
||||
if (!gift) {
|
||||
return WebPageAuctionPtr(nullptr);
|
||||
}
|
||||
auto auction = std::make_unique<WebPageAuction>();
|
||||
auction->auctionGift = std::make_shared<StarGift>(*gift);
|
||||
auction->endDate = data.vend_date().v;
|
||||
auction->centerColor = Ui::ColorFromSerialized(
|
||||
toUint(data.vcenter_color()));
|
||||
auction->edgeColor = Ui::ColorFromSerialized(
|
||||
toUint(data.vedge_color()));
|
||||
auction->textColor = Ui::ColorFromSerialized(
|
||||
toUint(data.vtext_color()));
|
||||
return auction;
|
||||
}, [](const auto &) -> WebPageAuctionPtr { return nullptr; });
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
};
|
||||
|
||||
auto story = (Data::Story*)nullptr;
|
||||
auto storyId = FullStoryId();
|
||||
if (const auto attributes = data.vattributes()) {
|
||||
@@ -4052,6 +4092,7 @@ void Session::webpageApplyFields(
|
||||
std::move(iv),
|
||||
lookupStickerSet(),
|
||||
lookupUniqueGift(),
|
||||
lookupAuction(),
|
||||
data.vduration().value_or_empty(),
|
||||
qs(data.vauthor().value_or_empty()),
|
||||
data.is_has_large_media(),
|
||||
@@ -4074,6 +4115,7 @@ void Session::webpageApplyFields(
|
||||
std::unique_ptr<Iv::Data> iv,
|
||||
std::unique_ptr<WebPageStickerSet> stickerSet,
|
||||
std::shared_ptr<UniqueGift> uniqueGift,
|
||||
std::unique_ptr<WebPageAuction> auction,
|
||||
int duration,
|
||||
const QString &author,
|
||||
bool hasLargeMedia,
|
||||
@@ -4094,6 +4136,7 @@ void Session::webpageApplyFields(
|
||||
std::move(iv),
|
||||
std::move(stickerSet),
|
||||
std::move(uniqueGift),
|
||||
std::move(auction),
|
||||
duration,
|
||||
author,
|
||||
hasLargeMedia,
|
||||
|
||||
@@ -19,6 +19,7 @@ class Image;
|
||||
class HistoryItem;
|
||||
struct WebPageCollage;
|
||||
struct WebPageStickerSet;
|
||||
struct WebPageAuction;
|
||||
enum class WebPageType : uint8;
|
||||
enum class NewMessageType;
|
||||
|
||||
@@ -115,6 +116,10 @@ struct GiftsUpdate {
|
||||
std::vector<Data::SavedStarGiftId> added;
|
||||
std::vector<Data::SavedStarGiftId> removed;
|
||||
};
|
||||
struct GiftAuctionGot {
|
||||
uint64 giftId = 0;
|
||||
not_null<PeerData*> to;
|
||||
};
|
||||
|
||||
struct SentToScheduled {
|
||||
not_null<History*> history;
|
||||
@@ -361,6 +366,8 @@ public:
|
||||
[[nodiscard]] rpl::producer<GiftUpdate> giftUpdates() const;
|
||||
void notifyGiftsUpdate(GiftsUpdate &&update);
|
||||
[[nodiscard]] rpl::producer<GiftsUpdate> giftsUpdates() const;
|
||||
void notifyGiftAuctionGot(GiftAuctionGot &&update);
|
||||
[[nodiscard]] rpl::producer<GiftAuctionGot> giftAuctionGots() const;
|
||||
void requestItemRepaint(not_null<const HistoryItem*> item);
|
||||
[[nodiscard]] rpl::producer<not_null<const HistoryItem*>> itemRepaintRequest() const;
|
||||
void requestViewRepaint(not_null<const ViewElement*> view);
|
||||
@@ -1016,6 +1023,7 @@ private:
|
||||
std::unique_ptr<Iv::Data> iv,
|
||||
std::unique_ptr<WebPageStickerSet> stickerSet,
|
||||
std::shared_ptr<UniqueGift> uniqueGift,
|
||||
std::unique_ptr<WebPageAuction> auction,
|
||||
int duration,
|
||||
const QString &author,
|
||||
bool hasLargeMedia,
|
||||
@@ -1075,6 +1083,7 @@ private:
|
||||
rpl::event_stream<not_null<HistoryItem*>> _newItemAdded;
|
||||
rpl::event_stream<GiftUpdate> _giftUpdates;
|
||||
rpl::event_stream<GiftsUpdate> _giftsUpdates;
|
||||
rpl::event_stream<GiftAuctionGot> _giftAuctionGots;
|
||||
rpl::event_stream<not_null<const HistoryItem*>> _itemRepaintRequest;
|
||||
rpl::event_stream<not_null<const ViewElement*>> _viewRepaintRequest;
|
||||
rpl::event_stream<not_null<const HistoryItem*>> _itemResizeRequest;
|
||||
|
||||
@@ -2006,6 +2006,58 @@ void Stories::albumDelete(not_null<PeerData*> peer, int id) {
|
||||
}
|
||||
}
|
||||
|
||||
void Stories::albumReorderStories(
|
||||
not_null<PeerData*> peer,
|
||||
int albumId,
|
||||
int oldPosition,
|
||||
int newPosition,
|
||||
Fn<void()> done,
|
||||
Fn<void()> fail) {
|
||||
const auto ids = albumIds(peer->id, albumId);
|
||||
const auto list = RespectingPinned(ids);
|
||||
|
||||
if (oldPosition < 0 || newPosition < 0
|
||||
|| oldPosition >= list.size() || newPosition >= list.size()) {
|
||||
fail();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_reorderStoriesRequestId) {
|
||||
_owner->session().api().request(
|
||||
base::take(_reorderStoriesRequestId)).cancel();
|
||||
}
|
||||
|
||||
auto reorderedList = list;
|
||||
base::reorder(reorderedList, oldPosition, newPosition);
|
||||
|
||||
auto order = QVector<MTPint>();
|
||||
order.reserve(reorderedList.size());
|
||||
for (const auto id : reorderedList) {
|
||||
order.push_back(MTP_int(id));
|
||||
}
|
||||
|
||||
_reorderStoriesRequestId = _owner->session().api().request(
|
||||
MTPstories_UpdateAlbum(
|
||||
MTP_flags(MTPstories_UpdateAlbum::Flag::f_order),
|
||||
peer->input,
|
||||
MTP_int(albumId),
|
||||
MTPstring(),
|
||||
MTPVector<MTPint>(),
|
||||
MTPVector<MTPint>(),
|
||||
MTP_vector<MTPint>(order)
|
||||
)).done([=](const MTPStoryAlbum &result) {
|
||||
_reorderStoriesRequestId = 0;
|
||||
if (const auto set = albumIdsSet(peer->id, albumId)) {
|
||||
set->ids.list = reorderedList;
|
||||
_albumIdsChanged.fire({ peer->id, albumId });
|
||||
}
|
||||
done();
|
||||
}).fail([=] {
|
||||
_reorderStoriesRequestId = 0;
|
||||
fail();
|
||||
}).send();
|
||||
}
|
||||
|
||||
void Stories::notifyAlbumUpdate(StoryAlbumUpdate &&update) {
|
||||
const auto peerId = update.peer->id;
|
||||
const auto i = _albums.find(peerId);
|
||||
|
||||
@@ -242,6 +242,13 @@ public:
|
||||
Fn<void(StoryAlbum)> done,
|
||||
Fn<void(QString)> fail);
|
||||
void albumDelete(not_null<PeerData*> peer, int id);
|
||||
void albumReorderStories(
|
||||
not_null<PeerData*> peer,
|
||||
int albumId,
|
||||
int oldPosition,
|
||||
int newPosition,
|
||||
Fn<void()> done,
|
||||
Fn<void()> fail);
|
||||
void notifyAlbumUpdate(StoryAlbumUpdate &&update);
|
||||
[[nodiscard]] rpl::producer<StoryAlbumUpdate> albumUpdates() const;
|
||||
|
||||
@@ -477,6 +484,8 @@ private:
|
||||
base::Timer _pollingTimer;
|
||||
base::Timer _pollingViewsTimer;
|
||||
|
||||
mtpRequestId _reorderStoriesRequestId = 0;
|
||||
|
||||
rpl::variable<StealthMode> _stealthMode;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
@@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "data/data_photo.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_document.h"
|
||||
#include "data/data_star_gift.h"
|
||||
#include "core/local_url_handlers.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "iv/iv_data.h"
|
||||
@@ -177,6 +178,8 @@ WebPageType ParseWebPageType(
|
||||
return WebPageType::StoryAlbum;
|
||||
} else if (type == u"telegram_collection"_q) {
|
||||
return WebPageType::GiftCollection;
|
||||
} else if (type == u"telegram_auction"_q) {
|
||||
return WebPageType::Auction;
|
||||
} else if (hasIV) {
|
||||
return WebPageType::ArticleWithIV;
|
||||
} else {
|
||||
@@ -232,6 +235,7 @@ bool WebPageData::applyChanges(
|
||||
std::unique_ptr<Iv::Data> newIv,
|
||||
std::unique_ptr<WebPageStickerSet> newStickerSet,
|
||||
std::shared_ptr<Data::UniqueGift> newUniqueGift,
|
||||
std::unique_ptr<WebPageAuction> newAuction,
|
||||
int newDuration,
|
||||
const QString &newAuthor,
|
||||
bool newHasLargeMedia,
|
||||
@@ -291,6 +295,7 @@ bool WebPageData::applyChanges(
|
||||
&& (!iv || iv->partial() == newIv->partial())
|
||||
&& (!stickerSet == !newStickerSet)
|
||||
&& (!uniqueGift == !newUniqueGift)
|
||||
&& (!auction == !newAuction)
|
||||
&& duration == newDuration
|
||||
&& author == resultAuthor
|
||||
&& hasLargeMedia == (newHasLargeMedia ? 1 : 0)
|
||||
@@ -316,6 +321,7 @@ bool WebPageData::applyChanges(
|
||||
iv = std::move(newIv);
|
||||
stickerSet = std::move(newStickerSet);
|
||||
uniqueGift = std::move(newUniqueGift);
|
||||
auction = std::move(newAuction);
|
||||
duration = newDuration;
|
||||
author = resultAuthor;
|
||||
pendingTill = newPendingTill;
|
||||
|
||||
@@ -16,6 +16,7 @@ class ChannelData;
|
||||
namespace Data {
|
||||
class Session;
|
||||
struct UniqueGift;
|
||||
struct StarGift;
|
||||
} // namespace Data
|
||||
|
||||
namespace Iv {
|
||||
@@ -51,6 +52,7 @@ enum class WebPageType : uint8 {
|
||||
StickerSet,
|
||||
StoryAlbum,
|
||||
GiftCollection,
|
||||
Auction,
|
||||
|
||||
Article,
|
||||
ArticleWithIV,
|
||||
@@ -85,6 +87,14 @@ struct WebPageStickerSet {
|
||||
|
||||
};
|
||||
|
||||
struct WebPageAuction {
|
||||
std::shared_ptr<Data::StarGift> auctionGift;
|
||||
TimeId endDate = 0;
|
||||
QColor centerColor;
|
||||
QColor edgeColor;
|
||||
QColor textColor;
|
||||
};
|
||||
|
||||
struct WebPageData {
|
||||
WebPageData(not_null<Data::Session*> owner, const WebPageId &id);
|
||||
~WebPageData();
|
||||
@@ -106,6 +116,7 @@ struct WebPageData {
|
||||
std::unique_ptr<Iv::Data> newIv,
|
||||
std::unique_ptr<WebPageStickerSet> newStickerSet,
|
||||
std::shared_ptr<Data::UniqueGift> newUniqueGift,
|
||||
std::unique_ptr<WebPageAuction> newAuction,
|
||||
int newDuration,
|
||||
const QString &newAuthor,
|
||||
bool newHasLargeMedia,
|
||||
@@ -137,6 +148,7 @@ struct WebPageData {
|
||||
std::unique_ptr<Iv::Data> iv;
|
||||
std::unique_ptr<WebPageStickerSet> stickerSet;
|
||||
std::shared_ptr<Data::UniqueGift> uniqueGift;
|
||||
std::unique_ptr<WebPageAuction> auction;
|
||||
int duration = 0;
|
||||
TimeId pendingTill = 0;
|
||||
uint32 version : 29 = 0;
|
||||
|
||||
@@ -879,15 +879,15 @@ void CustomEmojiManager::repaintLater(
|
||||
not_null<Ui::CustomEmoji::Instance*> instance,
|
||||
Ui::CustomEmoji::RepaintRequest request) {
|
||||
auto &bunch = _repaints[request.duration];
|
||||
if (bunch.when < request.when) {
|
||||
if (bunch.when > 0) {
|
||||
for (const auto &already : bunch.instances) {
|
||||
if (already.get() == instance) {
|
||||
// Still waiting for full bunch repaint, don't bump.
|
||||
return;
|
||||
}
|
||||
if (bunch.when > 0) {
|
||||
for (const auto &already : bunch.instances) {
|
||||
if (already.get() == instance) {
|
||||
// Still waiting for full bunch repaint, don't bump.
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (bunch.when < request.when) {
|
||||
bunch.when = request.when;
|
||||
#if 0 // inject-to-on_main
|
||||
_repaintsLastAdded = request.when;
|
||||
|
||||
@@ -129,6 +129,11 @@ dialogRowOpenBot: DialogRightButton {
|
||||
dialogRowOpenBotRecent: DialogRightButton(dialogRowOpenBot) {
|
||||
margin: margins(0px, 32px, 16px, 0px);
|
||||
}
|
||||
dialogsTopBarRightButton: RoundButton(defaultActiveButton) {
|
||||
width: -16px;
|
||||
height: 22px;
|
||||
textTop: 2px;
|
||||
}
|
||||
|
||||
forumDialogJumpArrow: icon{{ "dialogs/dialogs_topic_arrow", dialogsTextFg }};
|
||||
forumDialogJumpArrowOver: icon{{ "dialogs/dialogs_topic_arrow", dialogsTextFgOver }};
|
||||
|
||||
@@ -14,9 +14,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "apiwrap.h"
|
||||
#include "base/call_delayed.h"
|
||||
#include "boxes/star_gift_box.h" // ShowStarGiftBox.
|
||||
#include "boxes/star_gift_auction_box.h"
|
||||
#include "core/application.h"
|
||||
#include "core/click_handler_types.h"
|
||||
#include "core/ui_integration.h"
|
||||
#include "data/components/gift_auctions.h"
|
||||
#include "data/components/promo_suggestions.h"
|
||||
#include "data/data_birthday.h"
|
||||
#include "data/data_changes.h"
|
||||
@@ -184,6 +186,7 @@ rpl::producer<Ui::SlideWrap<Ui::RpWidget>*> TopBarSuggestionValue(
|
||||
rpl::lifetime userpicLifetime;
|
||||
rpl::lifetime giftsLifetime;
|
||||
rpl::lifetime creditsLifetime;
|
||||
rpl::lifetime auctionsLifetime;
|
||||
std::unique_ptr<Api::CreditsHistory> creditsHistory;
|
||||
};
|
||||
|
||||
@@ -193,8 +196,11 @@ rpl::producer<Ui::SlideWrap<Ui::RpWidget>*> TopBarSuggestionValue(
|
||||
rpl::single(st::dialogsTopBarLeftPadding));
|
||||
const auto ensureContent = [=] {
|
||||
if (!state->content) {
|
||||
const auto window = FindSessionController(parent);
|
||||
state->content = Ui::CreateChild<TopBarSuggestionContent>(
|
||||
parent);
|
||||
parent,
|
||||
[=] { return window->isGifPausedAtLeastFor(
|
||||
Window::GifPauseReason::Layer); });
|
||||
rpl::combine(
|
||||
parent->widthValue(),
|
||||
state->content->desiredHeightValue()
|
||||
@@ -229,6 +235,7 @@ rpl::producer<Ui::SlideWrap<Ui::RpWidget>*> TopBarSuggestionValue(
|
||||
state->userpicLifetime.destroy();
|
||||
state->giftsLifetime.destroy();
|
||||
state->creditsLifetime.destroy();
|
||||
state->auctionsLifetime.destroy();
|
||||
|
||||
if (!session->api().authorizations().unreviewed().empty()) {
|
||||
state->content = nullptr;
|
||||
@@ -273,7 +280,50 @@ rpl::producer<Ui::SlideWrap<Ui::RpWidget>*> TopBarSuggestionValue(
|
||||
const auto wrap = state->wrap.get();
|
||||
using RightIcon = TopBarSuggestionContent::RightIcon;
|
||||
const auto promo = &session->promoSuggestions();
|
||||
if (const auto custom = promo->custom()) {
|
||||
const auto auctions = &session->giftAuctions();
|
||||
if (auctions->hasActive()) {
|
||||
using namespace Data;
|
||||
struct Button {
|
||||
rpl::variable<TextWithEntities> text;
|
||||
Fn<void()> callback;
|
||||
base::has_weak_ptr guard;
|
||||
};
|
||||
auto &lifetime = state->auctionsLifetime;
|
||||
const auto button = lifetime.template make_state<Button>();
|
||||
const auto window = FindSessionController(parent);
|
||||
auctions->active(
|
||||
) | rpl::start_with_next([=](ActiveAuctions &&active) {
|
||||
const auto empty = active.list.empty();
|
||||
state->desiredWrapToggle.force_assign(
|
||||
Toggle{ !empty, anim::type::normal });
|
||||
if (empty) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto text = Ui::ActiveAuctionsState(active);
|
||||
const auto textColorOverride = text.someOutbid
|
||||
? st::attentionButtonFg->c
|
||||
: std::optional<QColor>();
|
||||
content->setContent(
|
||||
Ui::ActiveAuctionsTitle(active),
|
||||
std::move(text.text),
|
||||
Core::TextContext({ .session = session }),
|
||||
textColorOverride);
|
||||
button->text = Ui::ActiveAuctionsButton(active);
|
||||
button->callback = Ui::ActiveAuctionsCallback(
|
||||
window,
|
||||
active);
|
||||
}, state->auctionsLifetime);
|
||||
const auto callback = crl::guard(&button->guard, [=] {
|
||||
button->callback();
|
||||
});
|
||||
content->setRightButton(button->text.value(), callback);
|
||||
content->setClickedCallback(callback);
|
||||
content->setLeftPadding(state->leftPadding.value());
|
||||
state->desiredWrapToggle.force_assign(
|
||||
Toggle{ true, anim::type::normal });
|
||||
return;
|
||||
} else if (const auto custom = promo->custom()) {
|
||||
content->setRightIcon(RightIcon::Close);
|
||||
content->setLeftPadding(state->leftPadding.value());
|
||||
content->setClickedCallback([=] {
|
||||
@@ -733,12 +783,14 @@ rpl::producer<Ui::SlideWrap<Ui::RpWidget>*> TopBarSuggestionValue(
|
||||
rpl::merge(
|
||||
session->promoSuggestions().value(),
|
||||
session->api().authorizations().unreviewedChanges(),
|
||||
Data::AmPremiumValue(session) | rpl::skip(1) | rpl::to_empty
|
||||
Data::AmPremiumValue(session) | rpl::skip(1) | rpl::to_empty,
|
||||
session->giftAuctions().hasActiveChanges() | rpl::to_empty
|
||||
) | rpl::start_with_next([=] {
|
||||
const auto was = state->wrap.get();
|
||||
const auto weak = base::make_weak(was);
|
||||
processCurrentSuggestion(processCurrentSuggestion);
|
||||
if (was != state->wrap) {
|
||||
consumer.put_next_copy(state->wrap);
|
||||
if (was != state->wrap || (was && !weak)) {
|
||||
consumer.put_next_copy(state->wrap.get());
|
||||
}
|
||||
}, lifetime);
|
||||
|
||||
|
||||
@@ -156,15 +156,19 @@ not_null<Ui::SlideWrap<Ui::VerticalLayout>*> CreateUnconfirmedAuthContent(
|
||||
return wrap;
|
||||
}
|
||||
|
||||
TopBarSuggestionContent::TopBarSuggestionContent(not_null<Ui::RpWidget*> p)
|
||||
: Ui::RippleButton(p, st::defaultRippleAnimationBgOver)
|
||||
TopBarSuggestionContent::TopBarSuggestionContent(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
Fn<bool()> emojiPaused)
|
||||
: Ui::RippleButton(parent, st::defaultRippleAnimationBgOver)
|
||||
, _titleSt(st::semiboldTextStyle)
|
||||
, _contentTitleSt(st::dialogsTopBarSuggestionTitleStyle)
|
||||
, _contentTextSt(st::dialogsTopBarSuggestionAboutStyle) {
|
||||
, _contentTextSt(st::dialogsTopBarSuggestionAboutStyle)
|
||||
, _emojiPaused(std::move(emojiPaused)) {
|
||||
setRightIcon(RightIcon::Close);
|
||||
}
|
||||
|
||||
void TopBarSuggestionContent::setRightIcon(RightIcon icon) {
|
||||
_rightButton = nullptr;
|
||||
if (icon == _rightIcon) {
|
||||
return;
|
||||
}
|
||||
@@ -201,6 +205,35 @@ void TopBarSuggestionContent::setRightIcon(RightIcon icon) {
|
||||
}
|
||||
}
|
||||
|
||||
void TopBarSuggestionContent::setRightButton(
|
||||
rpl::producer<TextWithEntities> text,
|
||||
Fn<void()> callback) {
|
||||
_rightHide = nullptr;
|
||||
_rightArrow = nullptr;
|
||||
_rightIcon = RightIcon::None;
|
||||
if (!text) {
|
||||
_rightButton = nullptr;
|
||||
return;
|
||||
}
|
||||
using namespace Ui;
|
||||
_rightButton = base::make_unique_q<RoundButton>(
|
||||
this,
|
||||
rpl::single(QString()),
|
||||
st::dialogsTopBarRightButton);
|
||||
_rightButton->setText(std::move(text));
|
||||
rpl::combine(
|
||||
sizeValue(),
|
||||
_rightButton->sizeValue()
|
||||
) | rpl::start_with_next([=](QSize outer, QSize inner) {
|
||||
const auto top = (outer.height() - inner.height()) / 2;
|
||||
_rightButton->moveToRight(top, top, outer.width());
|
||||
}, _rightButton->lifetime());
|
||||
_rightButton->setFullRadius(true);
|
||||
_rightButton->setTextTransform(RoundButton::TextTransform::NoTransform);
|
||||
_rightButton->setClickedCallback(std::move(callback));
|
||||
_rightButton->show();
|
||||
}
|
||||
|
||||
void TopBarSuggestionContent::draw(QPainter &p) {
|
||||
const auto kLinesForPhoto = 3;
|
||||
|
||||
@@ -226,6 +259,8 @@ void TopBarSuggestionContent::draw(QPainter &p) {
|
||||
- (_rightHide ? _rightHide->width() : 0);
|
||||
const auto titleRight = leftPadding;
|
||||
const auto hasSecondLineTitle = availableWidth < _contentTitle.maxWidth();
|
||||
const auto paused = On(PowerSaving::kEmojiChat)
|
||||
|| (_emojiPaused && _emojiPaused());
|
||||
p.setPen(st::windowActiveTextFg);
|
||||
p.setPen(st::windowFg);
|
||||
{
|
||||
@@ -237,7 +272,7 @@ void TopBarSuggestionContent::draw(QPainter &p) {
|
||||
? availableWidth
|
||||
: (availableWidth - titleRight),
|
||||
.availableWidth = availableWidth,
|
||||
.pausedEmoji = On(PowerSaving::kEmojiChat),
|
||||
.pausedEmoji = paused,
|
||||
.elisionLines = hasSecondLineTitle ? 2 : 1,
|
||||
});
|
||||
}
|
||||
@@ -270,7 +305,7 @@ void TopBarSuggestionContent::draw(QPainter &p) {
|
||||
: availableWidth,
|
||||
};
|
||||
};
|
||||
p.setPen(st::windowSubTextFg);
|
||||
p.setPen(_descriptionColorOverride.value_or(st::windowSubTextFg->c));
|
||||
_contentText.draw(p, {
|
||||
.position = QPoint(left, top),
|
||||
.outerWidth = availableWidth,
|
||||
@@ -278,7 +313,7 @@ void TopBarSuggestionContent::draw(QPainter &p) {
|
||||
.geometry = Ui::Text::GeometryDescriptor{
|
||||
.layout = std::move(lineLayout),
|
||||
},
|
||||
.pausedEmoji = On(PowerSaving::kEmojiChat),
|
||||
.pausedEmoji = paused,
|
||||
});
|
||||
_lastPaintedContentTop = top;
|
||||
_lastPaintedContentLineAmount = lastContentLineAmount;
|
||||
@@ -288,7 +323,9 @@ void TopBarSuggestionContent::draw(QPainter &p) {
|
||||
void TopBarSuggestionContent::setContent(
|
||||
TextWithEntities title,
|
||||
TextWithEntities description,
|
||||
std::optional<Ui::Text::MarkedContext> context) {
|
||||
std::optional<Ui::Text::MarkedContext> context,
|
||||
std::optional<QColor> descriptionColorOverride) {
|
||||
_descriptionColorOverride = descriptionColorOverride;
|
||||
if (context) {
|
||||
context->repaint = [=] { update(); };
|
||||
_contentTitle.setMarkedText(
|
||||
@@ -305,6 +342,7 @@ void TopBarSuggestionContent::setContent(
|
||||
_contentTitle.setMarkedText(_contentTitleSt, std::move(title));
|
||||
_contentText.setMarkedText(_contentTextSt, std::move(description));
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
void TopBarSuggestionContent::paintEvent(QPaintEvent *) {
|
||||
|
||||
@@ -40,17 +40,23 @@ public:
|
||||
Arrow,
|
||||
};
|
||||
|
||||
TopBarSuggestionContent(not_null<Ui::RpWidget*>);
|
||||
TopBarSuggestionContent(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
Fn<bool()> emojiPaused = nullptr);
|
||||
|
||||
void setContent(
|
||||
TextWithEntities title,
|
||||
TextWithEntities description,
|
||||
std::optional<Ui::Text::MarkedContext> context = std::nullopt);
|
||||
std::optional<Ui::Text::MarkedContext> context = std::nullopt,
|
||||
std::optional<QColor> descriptionColorOverride = std::nullopt);
|
||||
|
||||
[[nodiscard]] rpl::producer<int> desiredHeightValue() const override;
|
||||
|
||||
void setHideCallback(Fn<void()>);
|
||||
void setRightIcon(RightIcon);
|
||||
void setRightButton(
|
||||
rpl::producer<TextWithEntities> text,
|
||||
Fn<void()> callback);
|
||||
void setLeftPadding(rpl::producer<int>);
|
||||
|
||||
[[nodiscard]] const style::TextStyle &contentTitleSt() const;
|
||||
@@ -69,10 +75,13 @@ private:
|
||||
Ui::Text::String _contentText;
|
||||
rpl::variable<int> _lastPaintedContentLineAmount = 0;
|
||||
rpl::variable<int> _lastPaintedContentTop = 0;
|
||||
std::optional<QColor> _descriptionColorOverride;
|
||||
|
||||
base::unique_qptr<Ui::IconButton> _rightHide;
|
||||
base::unique_qptr<Ui::IconButton> _rightArrow;
|
||||
base::unique_qptr<Ui::RoundButton> _rightButton;
|
||||
Fn<void()> _hideCallback;
|
||||
Fn<bool()> _emojiPaused;
|
||||
|
||||
int _leftPadding = 0;
|
||||
|
||||
|
||||
@@ -89,6 +89,10 @@ struct StoriesInfo {
|
||||
int count = 0;
|
||||
};
|
||||
|
||||
struct ProfileMusicInfo {
|
||||
int count = 0;
|
||||
};
|
||||
|
||||
struct FileLocation {
|
||||
int dcId = 0;
|
||||
MTPInputFileLocation data;
|
||||
@@ -929,6 +933,11 @@ StoriesSlice ParseStoriesSlice(
|
||||
const MTPVector<MTPStoryItem> &data,
|
||||
int baseIndex);
|
||||
|
||||
struct ProfileMusicSlice {
|
||||
std::vector<Message> list;
|
||||
int skipped = 0;
|
||||
};
|
||||
|
||||
Message ParseMessage(
|
||||
ParseMediaContext &context,
|
||||
const MTPMessage &data,
|
||||
|
||||
@@ -32,6 +32,7 @@ constexpr auto kFileMaxSize = 4000 * int64(1024 * 1024);
|
||||
constexpr auto kLocationCacheSize = 100'000;
|
||||
constexpr auto kMaxEmojiPerRequest = 100;
|
||||
constexpr auto kStoriesSliceLimit = 100;
|
||||
constexpr auto kProfileMusicSliceLimit = 100;
|
||||
|
||||
struct LocationKey {
|
||||
uint64 type;
|
||||
@@ -112,6 +113,7 @@ struct ApiWrap::StartProcess {
|
||||
enum class Step {
|
||||
UserpicsCount,
|
||||
StoriesCount,
|
||||
ProfileMusicCount,
|
||||
SplitRanges,
|
||||
DialogsCount,
|
||||
LeftChannelsCount,
|
||||
@@ -155,6 +157,19 @@ struct ApiWrap::StoriesProcess {
|
||||
int fileIndex = 0;
|
||||
};
|
||||
|
||||
struct ApiWrap::ProfileMusicProcess {
|
||||
FnMut<bool(Data::ProfileMusicInfo&&)> start;
|
||||
Fn<bool(DownloadProgress)> fileProgress;
|
||||
Fn<bool(Data::ProfileMusicSlice&&)> handleSlice;
|
||||
FnMut<void()> finish;
|
||||
|
||||
int processed = 0;
|
||||
std::optional<Data::ProfileMusicSlice> slice;
|
||||
int offsetId = 0;
|
||||
bool lastSlice = false;
|
||||
int fileIndex = 0;
|
||||
};
|
||||
|
||||
struct ApiWrap::OtherDataProcess {
|
||||
Data::File file;
|
||||
FnMut<void(Data::File&&)> done;
|
||||
@@ -438,6 +453,9 @@ void ApiWrap::startExport(
|
||||
if (_settings->types & Settings::Type::Stories) {
|
||||
_startProcess->steps.push_back(Step::StoriesCount);
|
||||
}
|
||||
if (_settings->types & Settings::Type::ProfileMusic) {
|
||||
_startProcess->steps.push_back(Step::ProfileMusicCount);
|
||||
}
|
||||
if (_settings->types & Settings::Type::AnyChatsMask) {
|
||||
_startProcess->steps.push_back(Step::SplitRanges);
|
||||
_startProcess->steps.push_back(Step::DialogsCount);
|
||||
@@ -468,6 +486,8 @@ void ApiWrap::sendNextStartRequest() {
|
||||
return requestUserpicsCount();
|
||||
case Step::StoriesCount:
|
||||
return requestStoriesCount();
|
||||
case Step::ProfileMusicCount:
|
||||
return requestProfileMusicCount();
|
||||
case Step::SplitRanges:
|
||||
return requestSplitRanges();
|
||||
case Step::DialogsCount:
|
||||
@@ -518,6 +538,34 @@ void ApiWrap::requestStoriesCount() {
|
||||
}).send();
|
||||
}
|
||||
|
||||
void ApiWrap::requestProfileMusicCount() {
|
||||
Expects(_startProcess != nullptr);
|
||||
|
||||
mainRequest(MTPusers_GetSavedMusic(
|
||||
_user,
|
||||
MTP_int(0), // offset
|
||||
MTP_int(0), // limit
|
||||
MTP_long(0) // hash
|
||||
)).done([=](const MTPusers_SavedMusic &result) {
|
||||
Expects(_settings != nullptr);
|
||||
Expects(_startProcess != nullptr);
|
||||
|
||||
const auto count = result.match(
|
||||
[](const MTPDusers_savedMusic &data) {
|
||||
return data.vcount().v;
|
||||
}, [](const MTPDusers_savedMusicNotModified &data) {
|
||||
return -1;
|
||||
});
|
||||
if (count < 0) {
|
||||
error("Unexpected messagesNotModified received.");
|
||||
return;
|
||||
}
|
||||
_startProcess->info.profileMusicCount = count;
|
||||
|
||||
sendNextStartRequest();
|
||||
}).send();
|
||||
}
|
||||
|
||||
void ApiWrap::requestSplitRanges() {
|
||||
Expects(_startProcess != nullptr);
|
||||
|
||||
@@ -1062,6 +1110,215 @@ void ApiWrap::finishStories() {
|
||||
base::take(_storiesProcess)->finish();
|
||||
}
|
||||
|
||||
void ApiWrap::requestProfileMusic(
|
||||
FnMut<bool(Data::ProfileMusicInfo&&)> start,
|
||||
Fn<bool(DownloadProgress)> progress,
|
||||
Fn<bool(Data::ProfileMusicSlice&&)> slice,
|
||||
FnMut<void()> finish) {
|
||||
Expects(_profileMusicProcess == nullptr);
|
||||
|
||||
_profileMusicProcess = std::make_unique<ProfileMusicProcess>();
|
||||
_profileMusicProcess->start = std::move(start);
|
||||
_profileMusicProcess->fileProgress = std::move(progress);
|
||||
_profileMusicProcess->handleSlice = std::move(slice);
|
||||
_profileMusicProcess->finish = std::move(finish);
|
||||
|
||||
mainRequest(MTPusers_GetSavedMusic(
|
||||
_user,
|
||||
MTP_int(0), // offset
|
||||
MTP_int(kProfileMusicSliceLimit), // limit
|
||||
MTP_long(0) // hash
|
||||
)).done([=](const MTPusers_SavedMusic &result) mutable {
|
||||
Expects(_profileMusicProcess != nullptr);
|
||||
|
||||
auto startInfo = result.match(
|
||||
[](const MTPDusers_savedMusic &data) {
|
||||
return Data::ProfileMusicInfo{ data.vcount().v };
|
||||
}, [](const MTPDusers_savedMusicNotModified &data) {
|
||||
return Data::ProfileMusicInfo{ 0 };
|
||||
});
|
||||
if (!_profileMusicProcess->start(std::move(startInfo))) {
|
||||
return;
|
||||
}
|
||||
|
||||
handleProfileMusicSlice(result);
|
||||
}).send();
|
||||
}
|
||||
|
||||
void ApiWrap::handleProfileMusicSlice(const MTPusers_SavedMusic &result) {
|
||||
Expects(_profileMusicProcess != nullptr);
|
||||
Expects(_selfId.has_value());
|
||||
|
||||
auto context = Data::ParseMediaContext();
|
||||
context.selfPeerId = peerFromUser(*_selfId);
|
||||
|
||||
auto slice = result.match([&](const MTPDusers_savedMusic &data) {
|
||||
if (data.vdocuments().v.size() < kProfileMusicSliceLimit) {
|
||||
_profileMusicProcess->lastSlice = true;
|
||||
}
|
||||
auto result = Data::MessagesSlice();
|
||||
for (const auto &doc : data.vdocuments().v) {
|
||||
auto message = Data::Message();
|
||||
message.id = ++_profileMusicProcess->processed;
|
||||
message.date = 0;
|
||||
message.media.content = Data::ParseDocument(
|
||||
context,
|
||||
doc,
|
||||
"profile_music/",
|
||||
0);
|
||||
result.list.push_back(std::move(message));
|
||||
}
|
||||
return result;
|
||||
}, [&](const MTPDusers_savedMusicNotModified &) {
|
||||
_profileMusicProcess->lastSlice = true;
|
||||
return Data::MessagesSlice();
|
||||
});
|
||||
|
||||
auto profileSlice = Data::ProfileMusicSlice();
|
||||
profileSlice.list.reserve(slice.list.size());
|
||||
for (auto &message : slice.list) {
|
||||
if (v::is<Data::Document>(message.media.content)) {
|
||||
const auto &doc = v::get<Data::Document>(message.media.content);
|
||||
if (doc.isAudioFile) {
|
||||
profileSlice.list.push_back(std::move(message));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadProfileMusicFiles(std::move(profileSlice));
|
||||
}
|
||||
|
||||
void ApiWrap::loadProfileMusicFiles(Data::ProfileMusicSlice &&slice) {
|
||||
Expects(_profileMusicProcess != nullptr);
|
||||
Expects(!_profileMusicProcess->slice.has_value());
|
||||
|
||||
if (slice.list.empty()) {
|
||||
_profileMusicProcess->lastSlice = true;
|
||||
}
|
||||
_profileMusicProcess->slice = std::move(slice);
|
||||
_profileMusicProcess->fileIndex = 0;
|
||||
loadNextProfileMusic();
|
||||
}
|
||||
|
||||
void ApiWrap::loadNextProfileMusic() {
|
||||
Expects(_profileMusicProcess != nullptr);
|
||||
Expects(_profileMusicProcess->slice.has_value());
|
||||
|
||||
for (auto &list = _profileMusicProcess->slice->list
|
||||
; _profileMusicProcess->fileIndex < list.size()
|
||||
; ++_profileMusicProcess->fileIndex) {
|
||||
auto &message = list[_profileMusicProcess->fileIndex];
|
||||
const auto origin = Data::FileOrigin{ .messageId = message.id };
|
||||
const auto ready = processFileLoad(
|
||||
message.file(),
|
||||
origin,
|
||||
[=](FileProgress value) { return loadProfileMusicProgress(value); },
|
||||
[=](const QString &path) { loadProfileMusicDone(path); },
|
||||
&message);
|
||||
if (!ready) {
|
||||
return;
|
||||
}
|
||||
const auto thumbProgress = [=](FileProgress value) {
|
||||
return loadProfileMusicThumbProgress(value);
|
||||
};
|
||||
const auto thumbReady = processFileLoad(
|
||||
message.thumb().file,
|
||||
origin,
|
||||
thumbProgress,
|
||||
[=](const QString &path) { loadProfileMusicThumbDone(path); },
|
||||
&message);
|
||||
if (!thumbReady) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
finishProfileMusicSlice();
|
||||
}
|
||||
|
||||
void ApiWrap::finishProfileMusicSlice() {
|
||||
Expects(_profileMusicProcess != nullptr);
|
||||
Expects(_profileMusicProcess->slice.has_value());
|
||||
|
||||
auto slice = *base::take(_profileMusicProcess->slice);
|
||||
if (!slice.list.empty()) {
|
||||
_profileMusicProcess->processed += slice.list.size();
|
||||
_profileMusicProcess->offsetId = slice.list.back().id;
|
||||
if (!_profileMusicProcess->handleSlice(std::move(slice))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (_profileMusicProcess->lastSlice) {
|
||||
finishProfileMusic();
|
||||
return;
|
||||
}
|
||||
|
||||
mainRequest(MTPusers_GetSavedMusic(
|
||||
_user,
|
||||
MTP_int(_profileMusicProcess->offsetId),
|
||||
MTP_int(kProfileMusicSliceLimit),
|
||||
MTP_long(0)
|
||||
)).done([=](const MTPusers_SavedMusic &result) {
|
||||
handleProfileMusicSlice(result);
|
||||
}).send();
|
||||
}
|
||||
|
||||
bool ApiWrap::loadProfileMusicProgress(FileProgress progress) {
|
||||
Expects(_fileProcess != nullptr);
|
||||
Expects(_profileMusicProcess != nullptr);
|
||||
Expects(_profileMusicProcess->slice.has_value());
|
||||
Expects((_profileMusicProcess->fileIndex >= 0)
|
||||
&& (_profileMusicProcess->fileIndex
|
||||
< _profileMusicProcess->slice->list.size()));
|
||||
|
||||
return _profileMusicProcess->fileProgress(DownloadProgress{
|
||||
_fileProcess->randomId,
|
||||
_fileProcess->relativePath,
|
||||
_profileMusicProcess->fileIndex,
|
||||
progress.ready,
|
||||
progress.total });
|
||||
}
|
||||
|
||||
void ApiWrap::loadProfileMusicDone(const QString &relativePath) {
|
||||
Expects(_profileMusicProcess != nullptr);
|
||||
Expects(_profileMusicProcess->slice.has_value());
|
||||
Expects((_profileMusicProcess->fileIndex >= 0)
|
||||
&& (_profileMusicProcess->fileIndex
|
||||
< _profileMusicProcess->slice->list.size()));
|
||||
|
||||
const auto index = _profileMusicProcess->fileIndex;
|
||||
auto &file = _profileMusicProcess->slice->list[index].file();
|
||||
file.relativePath = relativePath;
|
||||
if (relativePath.isEmpty()) {
|
||||
file.skipReason = Data::File::SkipReason::Unavailable;
|
||||
}
|
||||
loadNextProfileMusic();
|
||||
}
|
||||
|
||||
bool ApiWrap::loadProfileMusicThumbProgress(FileProgress progress) {
|
||||
return loadProfileMusicProgress(progress);
|
||||
}
|
||||
|
||||
void ApiWrap::loadProfileMusicThumbDone(const QString &relativePath) {
|
||||
Expects(_profileMusicProcess != nullptr);
|
||||
Expects(_profileMusicProcess->slice.has_value());
|
||||
Expects((_profileMusicProcess->fileIndex >= 0)
|
||||
&& (_profileMusicProcess->fileIndex
|
||||
< _profileMusicProcess->slice->list.size()));
|
||||
|
||||
const auto index = _profileMusicProcess->fileIndex;
|
||||
auto &file = _profileMusicProcess->slice->list[index].thumb().file;
|
||||
file.relativePath = relativePath;
|
||||
if (relativePath.isEmpty()) {
|
||||
file.skipReason = Data::File::SkipReason::Unavailable;
|
||||
}
|
||||
loadNextProfileMusic();
|
||||
}
|
||||
|
||||
void ApiWrap::finishProfileMusic() {
|
||||
Expects(_profileMusicProcess != nullptr);
|
||||
|
||||
base::take(_profileMusicProcess)->finish();
|
||||
}
|
||||
|
||||
void ApiWrap::requestContacts(FnMut<void(Data::ContactsList&&)> done) {
|
||||
Expects(_contactsProcess == nullptr);
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ struct UserpicsInfo;
|
||||
struct UserpicsSlice;
|
||||
struct StoriesInfo;
|
||||
struct StoriesSlice;
|
||||
struct ProfileMusicInfo;
|
||||
struct ProfileMusicSlice;
|
||||
struct ContactsList;
|
||||
struct SessionsList;
|
||||
struct DialogsInfo;
|
||||
@@ -50,6 +52,7 @@ public:
|
||||
struct StartInfo {
|
||||
int userpicsCount = 0;
|
||||
int storiesCount = 0;
|
||||
int profileMusicCount = 0;
|
||||
int dialogsCount = 0;
|
||||
};
|
||||
void startExport(
|
||||
@@ -86,6 +89,12 @@ public:
|
||||
Fn<bool(Data::StoriesSlice&&)> slice,
|
||||
FnMut<void()> finish);
|
||||
|
||||
void requestProfileMusic(
|
||||
FnMut<bool(Data::ProfileMusicInfo&&)> start,
|
||||
Fn<bool(DownloadProgress)> progress,
|
||||
Fn<bool(Data::ProfileMusicSlice&&)> slice,
|
||||
FnMut<void()> finish);
|
||||
|
||||
void requestContacts(FnMut<void(Data::ContactsList&&)> done);
|
||||
|
||||
void requestSessions(FnMut<void(Data::SessionsList&&)> done);
|
||||
@@ -109,6 +118,7 @@ private:
|
||||
struct ContactsProcess;
|
||||
struct UserpicsProcess;
|
||||
struct StoriesProcess;
|
||||
struct ProfileMusicProcess;
|
||||
struct OtherDataProcess;
|
||||
struct FileProcess;
|
||||
struct FileProgress;
|
||||
@@ -121,6 +131,7 @@ private:
|
||||
void sendNextStartRequest();
|
||||
void requestUserpicsCount();
|
||||
void requestStoriesCount();
|
||||
void requestProfileMusicCount();
|
||||
void requestSplitRanges();
|
||||
void requestDialogsCount();
|
||||
void requestLeftChannelsCount();
|
||||
@@ -146,6 +157,16 @@ private:
|
||||
void finishStoriesSlice();
|
||||
void finishStories();
|
||||
|
||||
void handleProfileMusicSlice(const MTPusers_SavedMusic &result);
|
||||
void loadProfileMusicFiles(Data::ProfileMusicSlice &&slice);
|
||||
void loadNextProfileMusic();
|
||||
bool loadProfileMusicProgress(FileProgress value);
|
||||
void loadProfileMusicDone(const QString &relativePath);
|
||||
bool loadProfileMusicThumbProgress(FileProgress value);
|
||||
void loadProfileMusicThumbDone(const QString &relativePath);
|
||||
void finishProfileMusicSlice();
|
||||
void finishProfileMusic();
|
||||
|
||||
void otherDataDone(const QString &relativePath);
|
||||
|
||||
bool useOnlyLastSplit() const;
|
||||
@@ -258,6 +279,7 @@ private:
|
||||
std::unique_ptr<ContactsProcess> _contactsProcess;
|
||||
std::unique_ptr<UserpicsProcess> _userpicsProcess;
|
||||
std::unique_ptr<StoriesProcess> _storiesProcess;
|
||||
std::unique_ptr<ProfileMusicProcess> _profileMusicProcess;
|
||||
std::unique_ptr<OtherDataProcess> _otherDataProcess;
|
||||
std::unique_ptr<FileProcess> _fileProcess;
|
||||
std::unique_ptr<LeftChannelsProcess> _leftChannelsProcess;
|
||||
|
||||
@@ -76,6 +76,7 @@ private:
|
||||
void exportPersonalInfo();
|
||||
void exportUserpics();
|
||||
void exportStories();
|
||||
void exportProfileMusic();
|
||||
void exportContacts();
|
||||
void exportSessions();
|
||||
void exportOtherData();
|
||||
@@ -91,6 +92,7 @@ private:
|
||||
ProcessingState statePersonalInfo() const;
|
||||
ProcessingState stateUserpics(const DownloadProgress &progress) const;
|
||||
ProcessingState stateStories(const DownloadProgress &progress) const;
|
||||
ProcessingState stateProfileMusic(const DownloadProgress &progress) const;
|
||||
ProcessingState stateContacts() const;
|
||||
ProcessingState stateSessions() const;
|
||||
ProcessingState stateOtherData() const;
|
||||
@@ -119,6 +121,9 @@ private:
|
||||
int _storiesWritten = 0;
|
||||
int _storiesCount = 0;
|
||||
|
||||
int _profileMusicWritten = 0;
|
||||
int _profileMusicCount = 0;
|
||||
|
||||
// rpl::variable<State> fails to compile in MSVC :(
|
||||
State _state;
|
||||
rpl::event_stream<State> _stateChanges;
|
||||
@@ -281,6 +286,9 @@ void ControllerObject::fillExportSteps() {
|
||||
if (_settings.types & Type::Stories) {
|
||||
_steps.push_back(Step::Stories);
|
||||
}
|
||||
if (_settings.types & Type::ProfileMusic) {
|
||||
_steps.push_back(Step::ProfileMusic);
|
||||
}
|
||||
if (_settings.types & Type::Contacts) {
|
||||
_steps.push_back(Step::Contacts);
|
||||
}
|
||||
@@ -317,6 +325,9 @@ void ControllerObject::fillSubstepsInSteps(const ApiWrap::StartInfo &info) {
|
||||
if (_settings.types & Settings::Type::Stories) {
|
||||
push(Step::Stories, 1);
|
||||
}
|
||||
if (_settings.types & Settings::Type::ProfileMusic) {
|
||||
push(Step::ProfileMusic, 1);
|
||||
}
|
||||
if (_settings.types & Settings::Type::Contacts) {
|
||||
push(Step::Contacts, 1);
|
||||
}
|
||||
@@ -356,6 +367,7 @@ void ControllerObject::exportNext() {
|
||||
case Step::PersonalInfo: return exportPersonalInfo();
|
||||
case Step::Userpics: return exportUserpics();
|
||||
case Step::Stories: return exportStories();
|
||||
case Step::ProfileMusic: return exportProfileMusic();
|
||||
case Step::Contacts: return exportContacts();
|
||||
case Step::Sessions: return exportSessions();
|
||||
case Step::OtherData: return exportOtherData();
|
||||
@@ -454,6 +466,32 @@ void ControllerObject::exportStories() {
|
||||
});
|
||||
}
|
||||
|
||||
void ControllerObject::exportProfileMusic() {
|
||||
_api.requestProfileMusic([=](Data::ProfileMusicInfo &&start) {
|
||||
if (ioCatchError(_writer->writeProfileMusicStart(start))) {
|
||||
return false;
|
||||
}
|
||||
_profileMusicWritten = 0;
|
||||
_profileMusicCount = start.count;
|
||||
return true;
|
||||
}, [=](DownloadProgress progress) {
|
||||
setState(stateProfileMusic(progress));
|
||||
return true;
|
||||
}, [=](Data::ProfileMusicSlice &&slice) {
|
||||
if (ioCatchError(_writer->writeProfileMusicSlice(slice))) {
|
||||
return false;
|
||||
}
|
||||
_profileMusicWritten += slice.list.size();
|
||||
setState(stateProfileMusic(DownloadProgress()));
|
||||
return true;
|
||||
}, [=] {
|
||||
if (ioCatchError(_writer->writeProfileMusicEnd())) {
|
||||
return;
|
||||
}
|
||||
exportNext();
|
||||
});
|
||||
}
|
||||
|
||||
void ControllerObject::exportContacts() {
|
||||
setState(stateContacts());
|
||||
_api.requestContacts([=](Data::ContactsList &&result) {
|
||||
@@ -596,6 +634,21 @@ ProcessingState ControllerObject::stateStories(
|
||||
});
|
||||
}
|
||||
|
||||
ProcessingState ControllerObject::stateProfileMusic(
|
||||
const DownloadProgress &progress) const {
|
||||
return prepareState(Step::ProfileMusic, [&](ProcessingState &result) {
|
||||
result.entityIndex = _profileMusicWritten + progress.itemIndex;
|
||||
result.entityCount = std::max(_profileMusicCount, result.entityIndex);
|
||||
result.bytesRandomId = progress.randomId;
|
||||
if (!progress.path.isEmpty()) {
|
||||
const auto last = progress.path.lastIndexOf('/');
|
||||
result.bytesName = progress.path.mid(last + 1);
|
||||
}
|
||||
result.bytesLoaded = progress.ready;
|
||||
result.bytesCount = progress.total;
|
||||
});
|
||||
}
|
||||
|
||||
ProcessingState ControllerObject::stateContacts() const {
|
||||
return prepareState(Step::Contacts);
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ struct ProcessingState {
|
||||
PersonalInfo,
|
||||
Userpics,
|
||||
Stories,
|
||||
ProfileMusic,
|
||||
Contacts,
|
||||
Sessions,
|
||||
OtherData,
|
||||
|
||||
@@ -58,6 +58,7 @@ struct Settings {
|
||||
PrivateChannels = 0x200,
|
||||
PublicChannels = 0x400,
|
||||
Stories = 0x800,
|
||||
ProfileMusic = 0x1000,
|
||||
|
||||
GroupsMask = PrivateGroups | PublicGroups,
|
||||
ChannelsMask = PrivateChannels | PublicChannels,
|
||||
@@ -68,6 +69,7 @@ struct Settings {
|
||||
| Userpics
|
||||
| Contacts
|
||||
| Stories
|
||||
| ProfileMusic
|
||||
| Sessions),
|
||||
AllMask = NonChatsMask | OtherData | AnyChatsMask,
|
||||
};
|
||||
@@ -97,6 +99,7 @@ struct Settings {
|
||||
| Type::Userpics
|
||||
| Type::Contacts
|
||||
| Type::Stories
|
||||
| Type::ProfileMusic
|
||||
| Type::PersonalChats
|
||||
| Type::PrivateGroups;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ struct UserpicsInfo;
|
||||
struct UserpicsSlice;
|
||||
struct StoriesInfo;
|
||||
struct StoriesSlice;
|
||||
struct ProfileMusicInfo;
|
||||
struct ProfileMusicSlice;
|
||||
struct ContactsList;
|
||||
struct SessionsList;
|
||||
struct DialogsInfo;
|
||||
@@ -64,6 +66,12 @@ public:
|
||||
const Data::StoriesSlice &data) = 0;
|
||||
[[nodiscard]] virtual Result writeStoriesEnd() = 0;
|
||||
|
||||
[[nodiscard]] virtual Result writeProfileMusicStart(
|
||||
const Data::ProfileMusicInfo &data) = 0;
|
||||
[[nodiscard]] virtual Result writeProfileMusicSlice(
|
||||
const Data::ProfileMusicSlice &data) = 0;
|
||||
[[nodiscard]] virtual Result writeProfileMusicEnd() = 0;
|
||||
|
||||
[[nodiscard]] virtual Result writeContactsList(
|
||||
const Data::ContactsList &data) = 0;
|
||||
|
||||
|
||||
@@ -44,9 +44,10 @@ constexpr auto kContactsPriority = 2;
|
||||
constexpr auto kFrequentContactsPriority = 3;
|
||||
constexpr auto kUserpicsPriority = 4;
|
||||
constexpr auto kStoriesPriority = 5;
|
||||
constexpr auto kSessionsPriority = 6;
|
||||
constexpr auto kWebSessionsPriority = 7;
|
||||
constexpr auto kOtherPriority = 8;
|
||||
constexpr auto kProfileMusicPriority = 6;
|
||||
constexpr auto kSessionsPriority = 7;
|
||||
constexpr auto kWebSessionsPriority = 8;
|
||||
constexpr auto kOtherPriority = 9;
|
||||
|
||||
const auto kLineBreak = QByteArrayLiteral("<br>");
|
||||
|
||||
@@ -538,6 +539,12 @@ public:
|
||||
const std::vector<Data::TextPart> &caption,
|
||||
const QString &internalLinksDomain,
|
||||
const QString &link = QString());
|
||||
[[nodiscard]] QByteArray pushAudioEntry(
|
||||
const QByteArray &name,
|
||||
const QByteArray &info,
|
||||
const QByteArrayList &details,
|
||||
const QByteArray &duration,
|
||||
const QString &link = QString());
|
||||
[[nodiscard]] QByteArray pushSessionListEntry(
|
||||
int apiId,
|
||||
const QByteArray &name,
|
||||
@@ -877,6 +884,54 @@ QByteArray HtmlWriter::Wrap::pushStoriesListEntry(
|
||||
return result;
|
||||
}
|
||||
|
||||
QByteArray HtmlWriter::Wrap::pushAudioEntry(
|
||||
const QByteArray &name,
|
||||
const QByteArray &info,
|
||||
const QByteArrayList &details,
|
||||
const QByteArray &duration,
|
||||
const QString &link) {
|
||||
auto result = pushDiv("entry clearfix");
|
||||
if (!link.isEmpty()) {
|
||||
result.append(pushTag("a", {
|
||||
{ "class", "pull_left userpic_wrap" },
|
||||
{ "href", relativePath(link).toUtf8() + "#allow_back" },
|
||||
}));
|
||||
} else {
|
||||
result.append(pushDiv("pull_left userpic_wrap"));
|
||||
}
|
||||
result.append(pushDiv("userpic audio_icon"));
|
||||
result.append(popTag());
|
||||
result.append(popTag());
|
||||
result.append(pushDiv("body"));
|
||||
if (!duration.isEmpty()) {
|
||||
result.append(pushDiv("pull_right info details"));
|
||||
result.append(SerializeString(duration));
|
||||
result.append(popTag());
|
||||
}
|
||||
if (!info.isEmpty()) {
|
||||
if (!link.isEmpty()) {
|
||||
result.append(pushTag("a", {
|
||||
{ "class", "block_link expanded" },
|
||||
{ "href", relativePath(link).toUtf8() + "#allow_back" },
|
||||
}));
|
||||
}
|
||||
result.append(pushDiv("name bold"));
|
||||
result.append(SerializeString(info));
|
||||
result.append(popTag());
|
||||
if (!link.isEmpty()) {
|
||||
result.append(popTag());
|
||||
}
|
||||
}
|
||||
for (const auto &detail : details) {
|
||||
result.append(pushDiv("details_entry details"));
|
||||
result.append(SerializeString(detail));
|
||||
result.append(popTag());
|
||||
}
|
||||
result.append(popTag());
|
||||
result.append(popTag());
|
||||
return result;
|
||||
}
|
||||
|
||||
QByteArray HtmlWriter::Wrap::pushSessionListEntry(
|
||||
int apiId,
|
||||
const QByteArray &name,
|
||||
@@ -2706,6 +2761,7 @@ Result HtmlWriter::start(
|
||||
"images/section_chats.png",
|
||||
"images/section_contacts.png",
|
||||
"images/section_frequent.png",
|
||||
"images/section_music.png",
|
||||
"images/section_other.png",
|
||||
"images/section_photos.png",
|
||||
"images/section_sessions.png",
|
||||
@@ -3001,6 +3057,93 @@ Result HtmlWriter::writeStoriesEnd() {
|
||||
return Result::Success();
|
||||
}
|
||||
|
||||
Result HtmlWriter::writeProfileMusicStart(const Data::ProfileMusicInfo &data) {
|
||||
Expects(_summary != nullptr);
|
||||
Expects(_profileMusic == nullptr);
|
||||
|
||||
_profileMusicCount = data.count;
|
||||
if (!_profileMusicCount) {
|
||||
return Result::Success();
|
||||
}
|
||||
_profileMusic = fileWithRelativePath(profileMusicFilePath());
|
||||
|
||||
auto block = _profileMusic->pushHeader(
|
||||
"Profile Music",
|
||||
mainFileRelativePath());
|
||||
block.append(_profileMusic->pushDiv("page_body list_page"));
|
||||
block.append(_profileMusic->pushDiv("entry_list"));
|
||||
if (const auto result = _profileMusic->writeBlock(block); !result) {
|
||||
return result;
|
||||
}
|
||||
return Result::Success();
|
||||
}
|
||||
|
||||
Result HtmlWriter::writeProfileMusicSlice(const Data::ProfileMusicSlice &data) {
|
||||
Expects(_profileMusic != nullptr);
|
||||
|
||||
_profileMusicCount -= data.skipped;
|
||||
if (data.list.empty()) {
|
||||
return Result::Success();
|
||||
}
|
||||
auto block = QByteArray();
|
||||
for (const auto &message : data.list) {
|
||||
if (!v::is<Data::Document>(message.media.content)) {
|
||||
continue;
|
||||
}
|
||||
const auto &doc = v::get<Data::Document>(message.media.content);
|
||||
if (!doc.isAudioFile) {
|
||||
continue;
|
||||
}
|
||||
using SkipReason = Data::File::SkipReason;
|
||||
const auto &file = doc.file;
|
||||
Assert(!file.relativePath.isEmpty()
|
||||
|| file.skipReason != SkipReason::None);
|
||||
auto status = QByteArrayList();
|
||||
status.append([&]() -> Data::Utf8String {
|
||||
switch (file.skipReason) {
|
||||
case SkipReason::Unavailable:
|
||||
return "(File unavailable, please try again later)";
|
||||
case SkipReason::FileSize:
|
||||
return "(File exceeds maximum size. "
|
||||
"Change data exporting settings to download.)";
|
||||
case SkipReason::FileType:
|
||||
return "(File not included. "
|
||||
"Change data exporting settings to download.)";
|
||||
case SkipReason::None: return Data::FormatFileSize(file.size);
|
||||
}
|
||||
Unexpected("Skip reason while writing profile music path.");
|
||||
}());
|
||||
const auto &path = file.relativePath;
|
||||
const auto title = !doc.songTitle.isEmpty()
|
||||
? doc.songTitle
|
||||
: !doc.name.isEmpty()
|
||||
? doc.name
|
||||
: Data::Utf8String("Unknown Track");
|
||||
const auto performer = !doc.songPerformer.isEmpty()
|
||||
? doc.songPerformer
|
||||
: Data::Utf8String("Unknown Artist");
|
||||
const auto info = performer + " - " + title;
|
||||
const auto duration = doc.duration > 0
|
||||
? Data::FormatDuration(doc.duration)
|
||||
: QByteArray();
|
||||
block.append(_profileMusic->pushAudioEntry(
|
||||
(path.isEmpty() ? QString("File unavailable") : path).toUtf8(),
|
||||
info,
|
||||
status,
|
||||
duration,
|
||||
path));
|
||||
}
|
||||
return _profileMusic->writeBlock(block);
|
||||
}
|
||||
|
||||
Result HtmlWriter::writeProfileMusicEnd() {
|
||||
pushProfileMusicSection();
|
||||
if (_profileMusic) {
|
||||
return base::take(_profileMusic)->close();
|
||||
}
|
||||
return Result::Success();
|
||||
}
|
||||
|
||||
QString HtmlWriter::storiesFilePath() const {
|
||||
return "lists/stories.html";
|
||||
}
|
||||
@@ -3014,6 +3157,19 @@ void HtmlWriter::pushStoriesSection() {
|
||||
storiesFilePath());
|
||||
}
|
||||
|
||||
QString HtmlWriter::profileMusicFilePath() const {
|
||||
return "lists/profile_music.html";
|
||||
}
|
||||
|
||||
void HtmlWriter::pushProfileMusicSection() {
|
||||
pushSection(
|
||||
kProfileMusicPriority,
|
||||
"Profile Music",
|
||||
"music",
|
||||
_profileMusicCount,
|
||||
profileMusicFilePath());
|
||||
}
|
||||
|
||||
Result HtmlWriter::writeContactsList(const Data::ContactsList &data) {
|
||||
Expects(_summary != nullptr);
|
||||
|
||||
|
||||
@@ -64,6 +64,10 @@ public:
|
||||
Result writeStoriesSlice(const Data::StoriesSlice &data) override;
|
||||
Result writeStoriesEnd() override;
|
||||
|
||||
Result writeProfileMusicStart(const Data::ProfileMusicInfo &data) override;
|
||||
Result writeProfileMusicSlice(const Data::ProfileMusicSlice &data) override;
|
||||
Result writeProfileMusicEnd() override;
|
||||
|
||||
Result writeContactsList(const Data::ContactsList &data) override;
|
||||
|
||||
Result writeSessionsList(const Data::SessionsList &data) override;
|
||||
@@ -131,9 +135,11 @@ private:
|
||||
const QString &userpicPath);
|
||||
void pushUserpicsSection();
|
||||
void pushStoriesSection();
|
||||
void pushProfileMusicSection();
|
||||
|
||||
[[nodiscard]] QString userpicsFilePath() const;
|
||||
[[nodiscard]] QString storiesFilePath() const;
|
||||
[[nodiscard]] QString profileMusicFilePath() const;
|
||||
|
||||
[[nodiscard]] QByteArray wrapMessageLink(
|
||||
int messageId,
|
||||
@@ -159,6 +165,9 @@ private:
|
||||
int _storiesCount = 0;
|
||||
std::unique_ptr<Wrap> _stories;
|
||||
|
||||
int _profileMusicCount = 0;
|
||||
std::unique_ptr<Wrap> _profileMusic;
|
||||
|
||||
QString _dialogsRelativePath;
|
||||
Data::DialogInfo _dialog;
|
||||
DialogsMode _dialogsMode = DialogsMode::None;
|
||||
|
||||
@@ -73,6 +73,24 @@ Result HtmlAndJsonWriter::writeStoriesEnd() {
|
||||
});
|
||||
}
|
||||
|
||||
Result HtmlAndJsonWriter::writeProfileMusicStart(const Data::ProfileMusicInfo &data) {
|
||||
return invoke([&](WriterPtr w) {
|
||||
return w->writeProfileMusicStart(data);
|
||||
});
|
||||
}
|
||||
|
||||
Result HtmlAndJsonWriter::writeProfileMusicSlice(const Data::ProfileMusicSlice &data) {
|
||||
return invoke([&](WriterPtr w) {
|
||||
return w->writeProfileMusicSlice(data);
|
||||
});
|
||||
}
|
||||
|
||||
Result HtmlAndJsonWriter::writeProfileMusicEnd() {
|
||||
return invoke([&](WriterPtr w) {
|
||||
return w->writeProfileMusicEnd();
|
||||
});
|
||||
}
|
||||
|
||||
Result HtmlAndJsonWriter::writeContactsList(const Data::ContactsList &data) {
|
||||
return invoke([&](WriterPtr w) {
|
||||
return w->writeContactsList(data);
|
||||
|
||||
@@ -36,6 +36,10 @@ public:
|
||||
Result writeStoriesSlice(const Data::StoriesSlice &data) override;
|
||||
Result writeStoriesEnd() override;
|
||||
|
||||
Result writeProfileMusicStart(const Data::ProfileMusicInfo &data) override;
|
||||
Result writeProfileMusicSlice(const Data::ProfileMusicSlice &data) override;
|
||||
Result writeProfileMusicEnd() override;
|
||||
|
||||
Result writeContactsList(const Data::ContactsList &data) override;
|
||||
|
||||
Result writeSessionsList(const Data::SessionsList &data) override;
|
||||
|
||||
@@ -1318,6 +1318,34 @@ Result JsonWriter::writeStoriesEnd() {
|
||||
return _output->writeBlock(popNesting());
|
||||
}
|
||||
|
||||
Result JsonWriter::writeProfileMusicStart(const Data::ProfileMusicInfo &data) {
|
||||
Expects(_output != nullptr);
|
||||
|
||||
auto block = prepareObjectItemStart("profile_music");
|
||||
return _output->writeBlock(block + pushNesting(Context::kArray));
|
||||
}
|
||||
|
||||
Result JsonWriter::writeProfileMusicSlice(const Data::ProfileMusicSlice &data) {
|
||||
Expects(_output != nullptr);
|
||||
|
||||
if (data.list.empty()) {
|
||||
return Result::Success();
|
||||
}
|
||||
|
||||
auto block = QByteArray();
|
||||
for (const auto &message : data.list) {
|
||||
block.append(prepareArrayItemStart());
|
||||
block.append(SerializeMessage(_context, message, {}, QString()));
|
||||
}
|
||||
return _output->writeBlock(block);
|
||||
}
|
||||
|
||||
Result JsonWriter::writeProfileMusicEnd() {
|
||||
Expects(_output != nullptr);
|
||||
|
||||
return _output->writeBlock(popNesting());
|
||||
}
|
||||
|
||||
Result JsonWriter::writeContactsList(const Data::ContactsList &data) {
|
||||
Expects(_output != nullptr);
|
||||
|
||||
|
||||
@@ -48,6 +48,10 @@ public:
|
||||
Result writeStoriesSlice(const Data::StoriesSlice &data) override;
|
||||
Result writeStoriesEnd() override;
|
||||
|
||||
Result writeProfileMusicStart(const Data::ProfileMusicInfo &data) override;
|
||||
Result writeProfileMusicSlice(const Data::ProfileMusicSlice &data) override;
|
||||
Result writeProfileMusicEnd() override;
|
||||
|
||||
Result writeContactsList(const Data::ContactsList &data) override;
|
||||
|
||||
Result writeSessionsList(const Data::SessionsList &data) override;
|
||||
|
||||
@@ -96,6 +96,13 @@ Content ContentFromState(
|
||||
state.bytesName,
|
||||
state.bytesRandomId);
|
||||
break;
|
||||
case Step::ProfileMusic:
|
||||
pushMain(tr::lng_export_option_profile_music(tr::now));
|
||||
pushBytes(
|
||||
"music" + QString::number(state.entityIndex),
|
||||
state.bytesName,
|
||||
state.bytesRandomId);
|
||||
break;
|
||||
case Step::Sessions:
|
||||
pushMain(tr::lng_export_option_sessions(tr::now));
|
||||
break;
|
||||
|
||||
@@ -183,6 +183,11 @@ void SettingsWidget::setupFullExportOptions(
|
||||
tr::lng_export_option_stories(tr::now),
|
||||
Type::Stories,
|
||||
tr::lng_export_option_stories_about(tr::now));
|
||||
addOptionWithAbout(
|
||||
container,
|
||||
tr::lng_export_option_profile_music(tr::now),
|
||||
Type::ProfileMusic,
|
||||
tr::lng_export_option_profile_music_about(tr::now));
|
||||
addHeader(container, tr::lng_export_header_chats(tr::now));
|
||||
addOption(
|
||||
container,
|
||||
@@ -233,7 +238,8 @@ void SettingsWidget::setupMediaOptions(
|
||||
| Type::PrivateGroups
|
||||
| Type::PrivateChannels
|
||||
| Type::PublicGroups
|
||||
| Type::PublicChannels)) != 0, anim::type::normal);
|
||||
| Type::PublicChannels
|
||||
| Type::ProfileMusic)) != 0, anim::type::normal);
|
||||
}, mediaWrap->lifetime());
|
||||
|
||||
widthValue(
|
||||
|
||||
@@ -752,7 +752,7 @@ not_null<HistoryItem*> History::addNewItem(
|
||||
}
|
||||
|
||||
if (const auto sublist = item->savedSublist()) {
|
||||
sublist->applyMaybeLast(item, unread);
|
||||
sublist->applyMaybeLast(item);
|
||||
}
|
||||
|
||||
return item;
|
||||
@@ -1373,6 +1373,13 @@ void History::applyServiceChanges(
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [&](const MTPDmessageActionStarGift &data) {
|
||||
if (data.is_auction_acquired() && data.vto_id()) {
|
||||
const auto to = peer->owner().peer(peerFromMTP(*data.vto_id()));
|
||||
data.vgift().match([&](const MTPDstarGift &data) {
|
||||
peer->owner().notifyGiftAuctionGot({ data.vid().v, to });
|
||||
}, [](const auto &) {});
|
||||
}
|
||||
}, [](const auto &) {
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6065,29 +6065,15 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) {
|
||||
}
|
||||
}
|
||||
} else if (anonymous || _history->peer->isSelf()) {
|
||||
const auto to = (action.is_auction_acquired() && action.vto_id())
|
||||
? peer->owner().peer(peerFromMTP(*action.vto_id())).get()
|
||||
: nullptr;
|
||||
result.text = to
|
||||
? tr::lng_action_gift_auction(
|
||||
result.text = (action.is_auction_acquired()
|
||||
? tr::lng_action_gift_auction_won
|
||||
: anonymous
|
||||
? tr::lng_action_gift_received_anonymous
|
||||
: tr::lng_action_gift_self_bought)(
|
||||
tr::now,
|
||||
lt_name,
|
||||
Ui::Text::Link(to->shortName(), 1),
|
||||
lt_cost,
|
||||
cost,
|
||||
Ui::Text::WithEntities)
|
||||
: (action.is_auction_acquired()
|
||||
? tr::lng_action_gift_self_auction
|
||||
: anonymous
|
||||
? tr::lng_action_gift_received_anonymous
|
||||
: tr::lng_action_gift_self_bought)(
|
||||
tr::now,
|
||||
lt_cost,
|
||||
cost,
|
||||
Ui::Text::WithEntities);
|
||||
if (to) {
|
||||
result.links.push_back(to->createOpenLink());
|
||||
}
|
||||
tr::marked);
|
||||
} else if (upgradeGifted) {
|
||||
// Who sent the gift.
|
||||
const auto fromId = action.vfrom_id()
|
||||
@@ -6636,6 +6622,14 @@ void HistoryItem::applyAction(const MTPMessageAction &action) {
|
||||
: PeerId();
|
||||
const auto upgradeMsgId = data.vupgrade_msg_id().value_or_empty();
|
||||
const auto realGiftMsgId = data.vgift_msg_id().value_or_empty();
|
||||
const auto bid = data.vgift().match([&](const MTPDstarGift &gift) {
|
||||
return data.is_auction_acquired()
|
||||
? (int(gift.vstars().v)
|
||||
+ int(gift.vupgrade_stars().value_or_empty()))
|
||||
: 0;
|
||||
}, [](const MTPDstarGiftUnique &) {
|
||||
return 0;
|
||||
});
|
||||
using Fields = Data::GiftCode;
|
||||
auto fields = Fields{
|
||||
.message = (data.vmessage()
|
||||
@@ -6643,6 +6637,12 @@ void HistoryItem::applyAction(const MTPMessageAction &action) {
|
||||
&history()->session(),
|
||||
*data.vmessage())
|
||||
: TextWithEntities()),
|
||||
.auctionTo = (service
|
||||
&& data.is_auction_acquired()
|
||||
&& data.vto_id())
|
||||
? history()->owner().peer(
|
||||
peerFromMTP(*data.vto_id())).get()
|
||||
: nullptr,
|
||||
.channel = ((service && peerIsChannel(to))
|
||||
? history()->owner().channel(peerToChannel(to)).get()
|
||||
: nullptr),
|
||||
@@ -6656,8 +6656,10 @@ void HistoryItem::applyAction(const MTPMessageAction &action) {
|
||||
.starsConverted = int(data.vconvert_stars().value_or_empty()),
|
||||
.starsUpgradedBySender = int(
|
||||
data.vupgrade_stars().value_or_empty()),
|
||||
.starsBid = bid,
|
||||
.type = Data::GiftType::StarGift,
|
||||
.upgradeSeparate = data.is_upgrade_separate(),
|
||||
.upgradeGifted = data.is_prepaid_upgrade(),
|
||||
.upgradable = data.is_can_upgrade(),
|
||||
.anonymous = data.is_name_hidden(),
|
||||
.converted = data.is_converted(),
|
||||
|
||||
@@ -30,6 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "data/notify/data_notify_settings.h"
|
||||
#include "data/data_changes.h"
|
||||
#include "data/data_drafts.h"
|
||||
#include "data/data_group_call.h"
|
||||
#include "data/data_messages.h"
|
||||
#include "data/data_message_reactions.h"
|
||||
#include "data/data_saved_sublist.h"
|
||||
@@ -1090,9 +1091,12 @@ void ComposeControls::initLikeButton() {
|
||||
}
|
||||
|
||||
void ComposeControls::initEditStarsButton() {
|
||||
if (!_features.editMessageStars) {
|
||||
if (!editStarsButtonShown()) {
|
||||
delete base::take(_editStars);
|
||||
_chosenStarsCount = std::nullopt;
|
||||
if (_chosenStarsCount) {
|
||||
_chosenStarsCount = std::nullopt;
|
||||
updateSendButtonType();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (_chosenStarsCount.value_or(0) < _minStarsCount.current()) {
|
||||
@@ -1128,22 +1132,27 @@ void ComposeControls::editStarsFrom(int selected) {
|
||||
}));
|
||||
}
|
||||
|
||||
void ComposeControls::updateLikeParent() {
|
||||
if (_like) {
|
||||
using namespace Controls;
|
||||
const auto hidden = _like->isHidden();
|
||||
const auto &restriction = _writeRestriction.current();
|
||||
if (_writeRestricted
|
||||
&& restriction.type == WriteRestrictionType::PremiumRequired) {
|
||||
_like->setParent(_writeRestricted.get());
|
||||
void ComposeControls::updateControlsParents() {
|
||||
const auto toggle = [&](auto &&control, bool inRestriction) {
|
||||
if (!control) {
|
||||
return;
|
||||
}
|
||||
const auto hidden = control->isHidden();
|
||||
if (_writeRestricted && inRestriction) {
|
||||
control->setParent(_writeRestricted.get());
|
||||
} else {
|
||||
_like->setParent(_wrap.get());
|
||||
control->setParent(_wrap.get());
|
||||
}
|
||||
if (!hidden) {
|
||||
_like->show();
|
||||
control->show();
|
||||
updateControlsGeometry(_wrap->size());
|
||||
}
|
||||
}
|
||||
};
|
||||
using Type = Controls::WriteRestrictionType;
|
||||
const auto &restriction = _writeRestriction.current();
|
||||
toggle(_like, restriction.type == Type::PremiumRequired);
|
||||
toggle(_commentsShown, restriction.type != Type::None);
|
||||
toggle(_starsReaction, restriction.type != Type::None);
|
||||
}
|
||||
|
||||
void ComposeControls::updateFeatures(ChatHelpers::ComposeFeatures features) {
|
||||
@@ -1156,7 +1165,7 @@ void ComposeControls::updateFeatures(ChatHelpers::ComposeFeatures features) {
|
||||
} else {
|
||||
_like = Ui::CreateChild<Ui::IconButton>(_wrap.get(), _st.like);
|
||||
initLikeButton();
|
||||
updateLikeParent();
|
||||
updateControlsParents();
|
||||
if (updateLikeShown()) {
|
||||
updateControlsVisibility();
|
||||
}
|
||||
@@ -1285,19 +1294,26 @@ void ComposeControls::setToggleCommentsButton(
|
||||
_commentsShown->setClickedCallback([=] {
|
||||
_commentsShownToggles.fire({});
|
||||
});
|
||||
updateControlsParents();
|
||||
_commentsShownHidden.value(
|
||||
) | rpl::start_with_next([=](bool hidden) {
|
||||
if (_commentsShown->isHidden() != hidden) {
|
||||
if (hidden) {
|
||||
_commentsShown->hide();
|
||||
} else {
|
||||
_commentsShown->show();
|
||||
updateControlsGeometry(_wrap->size());
|
||||
}
|
||||
}
|
||||
}, _commentsShown->lifetime());
|
||||
std::move(
|
||||
state
|
||||
) | rpl::start_with_next([=](ToggleCommentsState value) {
|
||||
if (value == ToggleCommentsState::Empty) {
|
||||
if (!_commentsShown->isHidden()) {
|
||||
_commentsShown->hide();
|
||||
updateControlsGeometry(_wrap->size());
|
||||
}
|
||||
_commentsShownHidden = true;
|
||||
return;
|
||||
} else if (_commentsShown->isHidden()) {
|
||||
_commentsShown->show();
|
||||
updateControlsGeometry(_wrap->size());
|
||||
}
|
||||
_commentsShownHidden = false;
|
||||
const auto icon = (value == ToggleCommentsState::Shown)
|
||||
? &_st.commentsShown
|
||||
: nullptr;
|
||||
@@ -1328,6 +1344,7 @@ void ComposeControls::setStarsReactionCounter(
|
||||
_st.attach,
|
||||
_st.starsReactionCounter,
|
||||
std::move(count));
|
||||
updateControlsParents();
|
||||
updateControlsVisibility();
|
||||
|
||||
_starsReaction->widthValue(
|
||||
@@ -1895,7 +1912,7 @@ bool ComposeControls::showRecordButton() const {
|
||||
}
|
||||
|
||||
bool ComposeControls::showEditStarsButton() const {
|
||||
return _features.editMessageStars
|
||||
return editStarsButtonShown()
|
||||
&& !HasSendText(_field)
|
||||
&& !readyToForward()
|
||||
&& !isEditingMessage()
|
||||
@@ -1912,7 +1929,7 @@ void ComposeControls::clearListenState() {
|
||||
}
|
||||
|
||||
void ComposeControls::clearChosenStarsForMessage() {
|
||||
const auto empty = _features.editMessageStars
|
||||
const auto empty = editStarsButtonShown()
|
||||
? _minStarsCount.current()
|
||||
: std::optional<int>();
|
||||
if (_chosenStarsCount != empty) {
|
||||
@@ -1921,6 +1938,10 @@ void ComposeControls::clearChosenStarsForMessage() {
|
||||
}
|
||||
}
|
||||
|
||||
bool ComposeControls::editStarsButtonShown() const {
|
||||
return _features.editMessageStars && !_videoStreamAdmin.current();
|
||||
}
|
||||
|
||||
int ComposeControls::chosenStarsForMessage() const {
|
||||
return _chosenStarsCount.value_or(0);
|
||||
}
|
||||
@@ -2817,7 +2838,7 @@ void ComposeControls::initWriteRestriction() {
|
||||
}, _writeRestricted->lifetime());
|
||||
_writeRestricted->resize(
|
||||
_writeRestricted->width(),
|
||||
st::historyUnblock.height);
|
||||
_st.send.inner.height);
|
||||
const auto background = [=](QPainter &p, QRect clip) {
|
||||
paintBackground(p, _writeRestricted->rect(), clip);
|
||||
};
|
||||
@@ -2949,7 +2970,7 @@ void ComposeControls::updateWrappingVisibility() {
|
||||
_writeRestricted->setVisible(!hidden && restricted);
|
||||
}
|
||||
_wrap->setVisible(!hidden && !restricted);
|
||||
updateLikeParent();
|
||||
updateControlsParents();
|
||||
if (!hidden && !restricted) {
|
||||
updateControlsGeometry(_wrap->size());
|
||||
_wrap->raise();
|
||||
@@ -3159,6 +3180,9 @@ void ComposeControls::updateControlsVisibility() {
|
||||
if (_scheduled) {
|
||||
_scheduled->setVisible(!isEditingMessage());
|
||||
}
|
||||
if (_commentsShown) {
|
||||
_commentsShown->setVisible(!_commentsShownHidden.current());
|
||||
}
|
||||
if (_starsReaction) {
|
||||
_starsReaction->show();
|
||||
}
|
||||
@@ -3238,6 +3262,7 @@ bool ComposeControls::updateSendAsButton(
|
||||
if (!_sendAs) {
|
||||
return false;
|
||||
}
|
||||
_videoStreamAdmin = false;
|
||||
_sendAs = nullptr;
|
||||
return true;
|
||||
} else if (_sendAs) {
|
||||
@@ -3247,8 +3272,18 @@ bool ComposeControls::updateSendAsButton(
|
||||
_sendAs = std::make_unique<Ui::SendAsButton>(_wrap.get(), st.button);
|
||||
if (videoStream) {
|
||||
Ui::SetupSendAsButton(_sendAs.get(), st, videoStream, _show);
|
||||
_videoStreamAdmin = videoStream->sendAsValue(
|
||||
) | rpl::map([=](not_null<PeerData*> peer) {
|
||||
return (videoStream->peer() == peer)
|
||||
|| (videoStream->creator() && peer->isSelf());
|
||||
}) | rpl::distinct_until_changed(
|
||||
) | rpl::after_next([=](bool admin) {
|
||||
initEditStarsButton();
|
||||
updateControlsGeometry(_wrap->size());
|
||||
});
|
||||
} else {
|
||||
Ui::SetupSendAsButton(_sendAs.get(), st, rpl::single(peer), _show);
|
||||
_videoStreamAdmin = false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -3286,15 +3321,13 @@ void ComposeControls::paintBackground(QPainter &p, QRect full, QRect clip) {
|
||||
p.setBrush(_st.bg);
|
||||
p.setPen(Qt::NoPen);
|
||||
const auto r = _st.radius;
|
||||
if (_commentsShown
|
||||
&& !_commentsShown->isHidden()
|
||||
&& !_wrap->isHidden()) {
|
||||
if (_commentsShown && !_commentsShown->isHidden()) {
|
||||
p.drawRoundedRect(_commentsShown->geometry(), r, r);
|
||||
full.setLeft(full.left()
|
||||
+ _commentsShown->width()
|
||||
+ _st.commentsSkip);
|
||||
}
|
||||
if (_starsReaction && !_wrap->isHidden()) {
|
||||
if (_starsReaction) {
|
||||
full.setWidth(full.width()
|
||||
- _starsReaction->width()
|
||||
- _st.starsSkip);
|
||||
|
||||
@@ -308,7 +308,7 @@ private:
|
||||
void initKeyHandler();
|
||||
void initLikeButton();
|
||||
void initEditStarsButton();
|
||||
void updateLikeParent();
|
||||
void updateControlsParents();
|
||||
void updateSubmitSettings();
|
||||
void updateSendButtonType();
|
||||
void updateMessagesTTLShown();
|
||||
@@ -352,7 +352,8 @@ private:
|
||||
void clearInlineBot();
|
||||
void inlineBotChanged();
|
||||
|
||||
bool hasSilentBroadcastToggle() const;
|
||||
[[nodiscard]] bool hasSilentBroadcastToggle() const;
|
||||
[[nodiscard]] bool editStarsButtonShown() const;
|
||||
|
||||
// Look in the _field for the inline bot and query string.
|
||||
void updateInlineBotQuery();
|
||||
@@ -423,6 +424,7 @@ private:
|
||||
rpl::variable<int> _minStarsCount;
|
||||
std::optional<int> _chosenStarsCount;
|
||||
Ui::IconButton *_commentsShown = nullptr;
|
||||
rpl::variable<bool> _commentsShownHidden;
|
||||
Ui::RpWidget *_commentsShownNewDot = nullptr;
|
||||
Ui::IconButton *_attachToggle = nullptr;
|
||||
Ui::AbstractButton *_starsReaction = nullptr;
|
||||
@@ -432,6 +434,7 @@ private:
|
||||
const not_null<Ui::InputField*> _field;
|
||||
Ui::IconButton * const _botCommandStart = nullptr;
|
||||
std::unique_ptr<Ui::SendAsButton> _sendAs;
|
||||
rpl::variable<bool> _videoStreamAdmin;
|
||||
std::unique_ptr<Ui::SilentToggle> _silent;
|
||||
std::unique_ptr<Controls::TTLButton> _ttlInfo;
|
||||
base::unique_qptr<Controls::CharactersLimitLabel> _charsLimitation;
|
||||
|
||||
@@ -73,6 +73,19 @@ namespace {
|
||||
}));
|
||||
}
|
||||
|
||||
[[nodiscard]] not_null<Ui::RpWidget*> AddMoneyInputIcon(
|
||||
not_null<QWidget*> parent,
|
||||
Ui::Text::PaletteDependentEmoji emoji) {
|
||||
auto helper = Ui::Text::CustomEmojiHelper();
|
||||
auto text = helper.paletteDependent(std::move(emoji));
|
||||
return Ui::CreateChild<Ui::FlatLabel>(
|
||||
parent,
|
||||
rpl::single(std::move(text)),
|
||||
st::defaultFlatLabel,
|
||||
st::defaultPopupMenu,
|
||||
helper.context());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ChooseSuggestTimeBox(
|
||||
@@ -138,6 +151,60 @@ void AddApproximateUsd(
|
||||
usd->widthValue() | rpl::start_with_next(move, usd->lifetime());
|
||||
}
|
||||
|
||||
not_null<Ui::NumberInput*> AddStarsInputField(
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
StarsInputFieldArgs &&args) {
|
||||
const auto wrap = container->add(
|
||||
object_ptr<Ui::FixedHeightWidget>(
|
||||
container,
|
||||
st::editTagField.heightMin),
|
||||
st::boxRowPadding);
|
||||
const auto result = Ui::CreateChild<Ui::NumberInput>(
|
||||
wrap,
|
||||
st::editTagField,
|
||||
rpl::single(u"0"_q),
|
||||
args.value ? QString::number(*args.value) : QString(),
|
||||
args.max ? args.max : std::numeric_limits<int>::max());
|
||||
const auto icon = AddMoneyInputIcon(
|
||||
result,
|
||||
Ui::Earn::IconCreditsEmoji());
|
||||
|
||||
wrap->widthValue() | rpl::start_with_next([=](int width) {
|
||||
icon->move(st::starsFieldIconPosition);
|
||||
result->move(0, 0);
|
||||
result->resize(width, result->height());
|
||||
wrap->resize(width, result->height());
|
||||
}, wrap->lifetime());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
not_null<Ui::InputField*> AddTonInputField(
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
TonInputFieldArgs &&args) {
|
||||
const auto wrap = container->add(
|
||||
object_ptr<Ui::FixedHeightWidget>(
|
||||
container,
|
||||
st::editTagField.heightMin),
|
||||
st::boxRowPadding);
|
||||
const auto result = Ui::CreateTonAmountInput(
|
||||
wrap,
|
||||
rpl::single('0' + Ui::TonAmountSeparator() + '0'),
|
||||
args.value);
|
||||
const auto icon = AddMoneyInputIcon(
|
||||
result,
|
||||
Ui::Earn::IconCurrencyEmoji());
|
||||
|
||||
wrap->widthValue() | rpl::start_with_next([=](int width) {
|
||||
icon->move(st::tonFieldIconPosition);
|
||||
result->move(0, 0);
|
||||
result->resize(width, result->height());
|
||||
wrap->resize(width, result->height());
|
||||
}, wrap->lifetime());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
StarsTonPriceInput AddStarsTonPriceInput(
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
StarsTonPriceArgs &&args) {
|
||||
@@ -153,18 +220,6 @@ StarsTonPriceInput AddStarsTonPriceInput(
|
||||
|
||||
const auto session = args.session;
|
||||
const auto added = st::boxRowPadding - st::defaultSubsectionTitlePadding;
|
||||
auto helper = Ui::Text::CustomEmojiHelper();
|
||||
const auto makeIcon = [&](
|
||||
not_null<QWidget*> parent,
|
||||
Ui::Text::PaletteDependentEmoji emoji) {
|
||||
auto text = helper.paletteDependent(std::move(emoji));
|
||||
return Ui::CreateChild<Ui::FlatLabel>(
|
||||
parent,
|
||||
rpl::single(std::move(text)),
|
||||
st::defaultFlatLabel,
|
||||
st::defaultPopupMenu,
|
||||
helper.context());
|
||||
};
|
||||
|
||||
const auto starsWrap = container->add(
|
||||
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
|
||||
@@ -181,30 +236,11 @@ StarsTonPriceInput AddStarsTonPriceInput(
|
||||
added.right(),
|
||||
-st::defaultSubsectionTitlePadding.bottom()));
|
||||
|
||||
const auto starsFieldWrap = starsInner->add(
|
||||
object_ptr<Ui::FixedHeightWidget>(
|
||||
starsInner,
|
||||
st::editTagField.heightMin),
|
||||
st::boxRowPadding);
|
||||
auto ownedStarsField = object_ptr<Ui::NumberInput>(
|
||||
starsFieldWrap,
|
||||
st::editTagField,
|
||||
rpl::single(u"0"_q),
|
||||
((args.price && args.price.stars())
|
||||
? QString::number(args.price.whole())
|
||||
: QString()),
|
||||
args.starsMax);
|
||||
const auto starsField = ownedStarsField.data();
|
||||
const auto starsIcon = makeIcon(
|
||||
starsField,
|
||||
Ui::Earn::IconCreditsEmoji());
|
||||
|
||||
starsFieldWrap->widthValue() | rpl::start_with_next([=](int width) {
|
||||
starsIcon->move(st::starsFieldIconPosition);
|
||||
starsField->move(0, 0);
|
||||
starsField->resize(width, starsField->height());
|
||||
starsFieldWrap->resize(width, starsField->height());
|
||||
}, starsFieldWrap->lifetime());
|
||||
const auto starsField = AddStarsInputField(starsInner, {
|
||||
.value = ((args.price && args.price.stars())
|
||||
? args.price.whole()
|
||||
: std::optional<int64>()),
|
||||
});
|
||||
|
||||
AddApproximateUsd(
|
||||
starsField,
|
||||
@@ -232,27 +268,11 @@ StarsTonPriceInput AddStarsTonPriceInput(
|
||||
added.right(),
|
||||
-st::defaultSubsectionTitlePadding.bottom()));
|
||||
|
||||
const auto tonFieldWrap = tonInner->add(
|
||||
object_ptr<Ui::FixedHeightWidget>(
|
||||
tonInner,
|
||||
st::editTagField.heightMin),
|
||||
st::boxRowPadding);
|
||||
auto ownedTonField = object_ptr<Ui::InputField>::fromRaw(
|
||||
Ui::CreateTonAmountInput(
|
||||
tonFieldWrap,
|
||||
rpl::single('0' + Ui::TonAmountSeparator() + '0'),
|
||||
((args.price && args.price.ton())
|
||||
? (args.price.whole() * Ui::kNanosInOne + args.price.nano())
|
||||
: 0)));
|
||||
const auto tonField = ownedTonField.data();
|
||||
const auto tonIcon = makeIcon(tonField, Ui::Earn::IconCurrencyEmoji());
|
||||
|
||||
tonFieldWrap->widthValue() | rpl::start_with_next([=](int width) {
|
||||
tonIcon->move(st::tonFieldIconPosition);
|
||||
tonField->move(0, 0);
|
||||
tonField->resize(width, tonField->height());
|
||||
tonFieldWrap->resize(width, tonField->height());
|
||||
}, tonFieldWrap->lifetime());
|
||||
const auto tonField = AddTonInputField(tonInner, {
|
||||
.value = (args.price && args.price.ton())
|
||||
? (args.price.whole() * Ui::kNanosInOne + args.price.nano())
|
||||
: 0,
|
||||
});
|
||||
|
||||
AddApproximateUsd(
|
||||
tonField,
|
||||
|
||||
@@ -16,6 +16,8 @@ class Show;
|
||||
namespace Ui {
|
||||
class GenericBox;
|
||||
class VerticalLayout;
|
||||
class NumberInput;
|
||||
class InputField;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Main {
|
||||
@@ -44,6 +46,21 @@ void ChooseSuggestTimeBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
SuggestTimeBoxArgs &&args);
|
||||
|
||||
struct StarsInputFieldArgs {
|
||||
std::optional<int64> value;
|
||||
int64 max = 0;
|
||||
};
|
||||
[[nodiscard]] not_null<Ui::NumberInput*> AddStarsInputField(
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
StarsInputFieldArgs &&args);
|
||||
|
||||
struct TonInputFieldArgs {
|
||||
int64 value = 0;
|
||||
};
|
||||
[[nodiscard]] not_null<Ui::InputField*> AddTonInputField(
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
TonInputFieldArgs &&args);
|
||||
|
||||
struct StarsTonPriceInput {
|
||||
Fn<void()> focusCallback;
|
||||
Fn<std::optional<CreditsAmount>()> computeResult;
|
||||
|
||||
@@ -30,6 +30,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_menu_icons.h"
|
||||
|
||||
namespace HistoryView::details {
|
||||
|
||||
not_null<Main::Session*> SessionFromShow(
|
||||
const std::shared_ptr<ChatHelpers::Show> &show) {
|
||||
return &show->session();
|
||||
}
|
||||
|
||||
} // namespace HistoryView::details
|
||||
|
||||
namespace HistoryView {
|
||||
namespace {
|
||||
|
||||
@@ -75,7 +84,8 @@ bool CanScheduleUntilOnline(not_null<PeerData*> peer) {
|
||||
|
||||
void ScheduleBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
std::shared_ptr<ChatHelpers::Show> show,
|
||||
not_null<Main::Session*> session,
|
||||
std::shared_ptr<ChatHelpers::Show> maybeShow,
|
||||
const Api::SendOptions &initialOptions,
|
||||
const SendMenu::Details &details,
|
||||
Fn<void(Api::SendOptions)> done,
|
||||
@@ -114,12 +124,14 @@ void ScheduleBox(
|
||||
});
|
||||
|
||||
if (repeat) {
|
||||
const auto boxShow = box->uiShow();
|
||||
const auto showPremiumPromo = [=] {
|
||||
if (show->session().premium()) {
|
||||
if (session->premium()) {
|
||||
return false;
|
||||
}
|
||||
Settings::ShowPremiumPromoToast(
|
||||
show,
|
||||
Main::MakeSessionShow(boxShow, session),
|
||||
ChatHelpers::ResolveWindowDefault(),
|
||||
tr::lng_schedule_repeat_promo(
|
||||
tr::now,
|
||||
lt_link,
|
||||
@@ -131,16 +143,16 @@ void ScheduleBox(
|
||||
return true;
|
||||
};
|
||||
auto locked = Data::AmPremiumValue(
|
||||
&show->session()
|
||||
session
|
||||
) | rpl::map([=](bool premium) {
|
||||
return !premium;
|
||||
});
|
||||
const auto row = box->addRow(Ui::ChooseRepeatPeriod(box, {
|
||||
.value = show->session().premium() ? *repeat : TimeId(),
|
||||
.value = session->premium() ? *repeat : TimeId(),
|
||||
.locked = std::move(locked),
|
||||
.filter = showPremiumPromo,
|
||||
.changed = [=](TimeId value) { *repeat = value; },
|
||||
.test = show->session().isTestMode(),
|
||||
.test = session->isTestMode(),
|
||||
}), style::al_top);
|
||||
std::move(descriptor.width) | rpl::start_with_next([=](int width) {
|
||||
row->setNaturalWidth(width);
|
||||
@@ -169,7 +181,7 @@ void ScheduleBox(
|
||||
});
|
||||
SetupMenuAndShortcuts(
|
||||
descriptor.submit.data(),
|
||||
show,
|
||||
maybeShow,
|
||||
[=] { return childDetails; },
|
||||
sendAction);
|
||||
|
||||
|
||||
@@ -23,6 +23,13 @@ namespace SendMenu {
|
||||
struct Details;
|
||||
} // namespace SendMenu
|
||||
|
||||
namespace HistoryView::details {
|
||||
|
||||
[[nodiscard]] not_null<Main::Session*> SessionFromShow(
|
||||
const std::shared_ptr<ChatHelpers::Show> &show);
|
||||
|
||||
} // namespace HistoryView::details
|
||||
|
||||
namespace HistoryView {
|
||||
|
||||
[[nodiscard]] TimeId DefaultScheduleTime();
|
||||
@@ -37,7 +44,8 @@ struct ScheduleBoxStyleArgs {
|
||||
|
||||
void ScheduleBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
std::shared_ptr<ChatHelpers::Show> show,
|
||||
not_null<Main::Session*> session,
|
||||
std::shared_ptr<ChatHelpers::Show> maybeShow,
|
||||
const Api::SendOptions &initialOptions,
|
||||
const SendMenu::Details &details,
|
||||
Fn<void(Api::SendOptions)> done,
|
||||
@@ -53,8 +61,10 @@ template <typename Guard, typename Submit>
|
||||
const Api::SendOptions &initialOptions = {},
|
||||
TimeId scheduleTime = DefaultScheduleTime(),
|
||||
ScheduleBoxStyleArgs style = ScheduleBoxStyleArgs()) {
|
||||
const auto session = details::SessionFromShow(show);
|
||||
return Box(
|
||||
ScheduleBox,
|
||||
session,
|
||||
std::move(show),
|
||||
initialOptions,
|
||||
details,
|
||||
@@ -63,4 +73,24 @@ template <typename Guard, typename Submit>
|
||||
std::move(style));
|
||||
}
|
||||
|
||||
template <typename Guard, typename Submit>
|
||||
[[nodiscard]] object_ptr<Ui::GenericBox> PrepareScheduleBox(
|
||||
Guard &&guard,
|
||||
not_null<Main::Session*> session,
|
||||
const SendMenu::Details &details,
|
||||
Submit &&submit,
|
||||
const Api::SendOptions &initialOptions = {},
|
||||
TimeId scheduleTime = DefaultScheduleTime(),
|
||||
ScheduleBoxStyleArgs style = ScheduleBoxStyleArgs()) {
|
||||
return Box(
|
||||
ScheduleBox,
|
||||
session,
|
||||
nullptr,
|
||||
initialOptions,
|
||||
details,
|
||||
crl::guard(std::forward<Guard>(guard), std::forward<Submit>(submit)),
|
||||
scheduleTime,
|
||||
std::move(style));
|
||||
}
|
||||
|
||||
} // namespace HistoryView
|
||||
|
||||
@@ -415,7 +415,7 @@ void SelfForwardsTagger::setupToastTimer(
|
||||
return base::EventFilterResult::Continue;
|
||||
}
|
||||
return base::EventFilterResult::Continue;
|
||||
}, state->timerLifetime);
|
||||
}, widget->lifetime());
|
||||
|
||||
restartTimer(kInitTimer);
|
||||
}
|
||||
|
||||
@@ -62,7 +62,8 @@ MediaGeneric::MediaGeneric(
|
||||
Fn<void(std::unique_ptr<Part>)>)> generate,
|
||||
MediaGenericDescriptor &&descriptor)
|
||||
: Media(parent)
|
||||
, _paintBg(std::move(descriptor.paintBg))
|
||||
, _paintBgFactory(std::move(descriptor.paintBgFactory))
|
||||
, _paintBg(_paintBgFactory ? _paintBgFactory() : nullptr)
|
||||
, _fullAreaLink(descriptor.fullAreaLink)
|
||||
, _maxWidthCap(descriptor.maxWidth)
|
||||
, _service(descriptor.service)
|
||||
@@ -110,7 +111,11 @@ void MediaGeneric::draw(Painter &p, const PaintContext &context) const {
|
||||
const auto outer = width();
|
||||
if (outer < st::msgPadding.left() + st::msgPadding.right() + 1) {
|
||||
return;
|
||||
} else if (_paintBg) {
|
||||
}
|
||||
if (!_paintBg && _paintBgFactory) {
|
||||
_paintBg = _paintBgFactory();
|
||||
}
|
||||
if (_paintBg) {
|
||||
_paintBg(p, context, this);
|
||||
} else if (_service) {
|
||||
PainterHighQualityEnabler hq(p);
|
||||
@@ -208,6 +213,7 @@ bool MediaGeneric::hasHeavyPart() const {
|
||||
}
|
||||
|
||||
void MediaGeneric::unloadHeavyPart() {
|
||||
_paintBg = nullptr;
|
||||
for (const auto &entry : _entries) {
|
||||
entry.object->unloadHeavyPart();
|
||||
}
|
||||
|
||||
@@ -29,6 +29,12 @@ class MediaGeneric;
|
||||
|
||||
class MediaGenericPart : public Object {
|
||||
public:
|
||||
using PaintBg = Fn<void(
|
||||
Painter&,
|
||||
const PaintContext&,
|
||||
not_null<const MediaGeneric*>)>;
|
||||
using PaintBgFactory = Fn<PaintBg()>;
|
||||
|
||||
virtual ~MediaGenericPart() = default;
|
||||
|
||||
virtual void draw(
|
||||
@@ -53,10 +59,7 @@ public:
|
||||
|
||||
struct MediaGenericDescriptor {
|
||||
int maxWidth = 0;
|
||||
Fn<void(
|
||||
Painter&,
|
||||
const PaintContext&,
|
||||
not_null<const MediaGeneric*>)> paintBg;
|
||||
MediaGenericPart::PaintBgFactory paintBgFactory;
|
||||
ClickHandlerPtr fullAreaLink;
|
||||
bool service = false;
|
||||
bool hideServiceText = false;
|
||||
@@ -124,10 +127,8 @@ private:
|
||||
[[nodiscard]] QMargins inBubblePadding() const;
|
||||
|
||||
std::vector<Entry> _entries;
|
||||
Fn<void(
|
||||
Painter&,
|
||||
const PaintContext&,
|
||||
not_null<const MediaGeneric*>)> _paintBg;
|
||||
Part::PaintBgFactory _paintBgFactory;
|
||||
mutable Part::PaintBg _paintBg;
|
||||
ClickHandlerPtr _fullAreaLink;
|
||||
int _maxWidthCap = 0;
|
||||
int _marginTop = 0;
|
||||
|
||||
@@ -69,111 +69,114 @@ QSize PremiumGift::size() {
|
||||
}
|
||||
|
||||
TextWithEntities PremiumGift::title() {
|
||||
using namespace Ui::Text;
|
||||
if (tonGift()) {
|
||||
return tr::lng_gift_ton_amount(
|
||||
tr::now,
|
||||
lt_count_decimal,
|
||||
CreditsAmount(0, _data.count, CreditsType::Ton).value(),
|
||||
Ui::Text::WithEntities);
|
||||
tr::marked);
|
||||
} else if (starGift()) {
|
||||
const auto peer = _parent->history()->peer;
|
||||
const auto to = _data.auctionTo ? _data.auctionTo : peer.get();
|
||||
return peer->isSelf()
|
||||
? tr::lng_action_gift_self_subtitle(tr::now, WithEntities)
|
||||
? tr::lng_action_gift_self_subtitle(tr::now, tr::marked)
|
||||
: (peer->isServiceUser() && _data.channelFrom)
|
||||
? tr::lng_action_gift_got_subtitle(
|
||||
tr::now,
|
||||
lt_user,
|
||||
WithEntities({})
|
||||
.append(SingleCustomEmoji(
|
||||
tr::marked()
|
||||
.append(Ui::Text::SingleCustomEmoji(
|
||||
peer->owner().customEmojiManager(
|
||||
).peerUserpicEmojiData(_data.channelFrom)))
|
||||
).peerUserpicEmojiData(_data.channelFrom)))
|
||||
.append(' ')
|
||||
.append(_data.channelFrom->shortName()),
|
||||
WithEntities)
|
||||
: peer->isServiceUser()
|
||||
? tr::lng_gift_link_label_gift(tr::now, WithEntities)
|
||||
tr::marked)
|
||||
: (!_data.auctionTo && peer->isServiceUser())
|
||||
? tr::lng_gift_link_label_gift(tr::now, tr::marked)
|
||||
: (outgoingGift()
|
||||
? tr::lng_action_gift_sent_subtitle
|
||||
: tr::lng_action_gift_got_subtitle)(
|
||||
tr::now,
|
||||
lt_user,
|
||||
WithEntities({})
|
||||
.append(SingleCustomEmoji(
|
||||
peer->owner().customEmojiManager(
|
||||
).peerUserpicEmojiData(peer)))
|
||||
tr::marked()
|
||||
.append(Ui::Text::SingleCustomEmoji(
|
||||
to->owner().customEmojiManager(
|
||||
).peerUserpicEmojiData(to)))
|
||||
.append(' ')
|
||||
.append(peer->shortName()),
|
||||
WithEntities);
|
||||
.append(to->shortName()),
|
||||
tr::marked);
|
||||
} else if (creditsPrize()) {
|
||||
return tr::lng_prize_title(tr::now, WithEntities);
|
||||
} else if (const auto c = credits()) {
|
||||
return tr::lng_gift_stars_title(tr::now, lt_count, c, WithEntities);
|
||||
return tr::lng_prize_title(tr::now, tr::marked);
|
||||
} else if (const auto stars = credits()) {
|
||||
return tr::lng_gift_stars_title(tr::now, lt_count_decimal, stars, tr::marked);
|
||||
}
|
||||
return gift()
|
||||
? tr::lng_action_gift_premium_months(
|
||||
tr::now,
|
||||
lt_count,
|
||||
_data.count,
|
||||
WithEntities)
|
||||
premiumMonths(),
|
||||
tr::marked)
|
||||
: _data.unclaimed
|
||||
? tr::lng_prize_unclaimed_title(tr::now, WithEntities)
|
||||
: tr::lng_prize_title(tr::now, WithEntities);
|
||||
? tr::lng_prize_unclaimed_title(tr::now, tr::marked)
|
||||
: tr::lng_prize_title(tr::now, tr::marked);
|
||||
}
|
||||
|
||||
TextWithEntities PremiumGift::author() {
|
||||
using namespace Ui::Text;
|
||||
if (!_data.stargiftReleasedBy) {
|
||||
return {};
|
||||
}
|
||||
return tr::lng_gift_released_by(
|
||||
tr::now,
|
||||
lt_name,
|
||||
Ui::Text::Link('@' + _data.stargiftReleasedBy->username()),
|
||||
Ui::Text::WithEntities);
|
||||
tr::link('@' + _data.stargiftReleasedBy->username()),
|
||||
tr::marked);
|
||||
}
|
||||
|
||||
TextWithEntities PremiumGift::subtitle() {
|
||||
if (tonGift()) {
|
||||
return tr::lng_action_gift_got_ton(tr::now, Ui::Text::WithEntities);
|
||||
return tr::lng_action_gift_got_ton(tr::now, tr::marked);
|
||||
} else if (starGift()) {
|
||||
const auto toChannel = _data.channel
|
||||
&& _parent->history()->peer->isServiceUser();
|
||||
return !_data.message.empty()
|
||||
? _data.message
|
||||
: _data.refunded
|
||||
? tr::lng_action_gift_refunded(tr::now, Ui::Text::RichLangValue)
|
||||
? tr::lng_action_gift_refunded(tr::now, tr::rich)
|
||||
: outgoingGift()
|
||||
? (_data.starsUpgradedBySender
|
||||
? (_data.auctionTo
|
||||
? tr::lng_action_gift_self_auction(
|
||||
tr::now,
|
||||
lt_cost,
|
||||
tr::lng_action_gift_for_stars(
|
||||
tr::now,
|
||||
lt_count_decimal,
|
||||
_data.starsBid,
|
||||
tr::marked),
|
||||
tr::rich)
|
||||
: _data.starsUpgradedBySender
|
||||
? tr::lng_action_gift_sent_upgradable(
|
||||
tr::now,
|
||||
lt_user,
|
||||
Ui::Text::Bold(_parent->history()->peer->shortName()),
|
||||
Ui::Text::RichLangValue)
|
||||
tr::bold(_parent->history()->peer->shortName()),
|
||||
tr::rich)
|
||||
: tr::lng_action_gift_sent_text(
|
||||
tr::now,
|
||||
lt_count,
|
||||
lt_count_decimal,
|
||||
_data.starsConverted,
|
||||
lt_user,
|
||||
Ui::Text::Bold(_parent->history()->peer->shortName()),
|
||||
Ui::Text::RichLangValue))
|
||||
tr::bold(_parent->history()->peer->shortName()),
|
||||
tr::rich))
|
||||
: _data.starsUpgradedBySender
|
||||
? tr::lng_action_gift_got_upgradable_text(
|
||||
tr::now,
|
||||
Ui::Text::RichLangValue)
|
||||
? tr::lng_action_gift_got_upgradable_text(tr::now, tr::rich)
|
||||
: (_data.starsToUpgrade
|
||||
&& !_data.converted
|
||||
&& _parent->history()->peer->isSelf())
|
||||
? tr::lng_action_gift_self_about_unique(
|
||||
tr::now,
|
||||
Ui::Text::RichLangValue)
|
||||
? tr::lng_action_gift_self_about_unique(tr::now, tr::rich)
|
||||
: (_data.starsToUpgrade
|
||||
&& !_data.converted
|
||||
&& _parent->history()->peer->isServiceUser()
|
||||
&& _data.channel)
|
||||
? tr::lng_action_gift_channel_about_unique(
|
||||
tr::now,
|
||||
Ui::Text::RichLangValue)
|
||||
? tr::lng_action_gift_channel_about_unique(tr::now, tr::rich)
|
||||
: (!_data.converted && !_data.starsConverted)
|
||||
? (_data.saved
|
||||
? (toChannel
|
||||
@@ -181,9 +184,7 @@ TextWithEntities PremiumGift::subtitle() {
|
||||
: tr::lng_action_gift_can_remove_text)
|
||||
: (toChannel
|
||||
? tr::lng_action_gift_got_gift_channel
|
||||
: tr::lng_action_gift_got_gift_text))(
|
||||
tr::now,
|
||||
Ui::Text::RichLangValue)
|
||||
: tr::lng_action_gift_got_gift_text))(tr::now, tr::rich)
|
||||
: (_data.converted
|
||||
? (toChannel
|
||||
? tr::lng_gift_channel_got
|
||||
@@ -196,7 +197,7 @@ TextWithEntities PremiumGift::subtitle() {
|
||||
tr::now,
|
||||
lt_count,
|
||||
_data.starsConverted,
|
||||
Ui::Text::RichLangValue);
|
||||
tr::rich);
|
||||
}
|
||||
const auto isCreditsPrize = creditsPrize();
|
||||
if (const auto count = credits(); count && !isCreditsPrize) {
|
||||
@@ -204,15 +205,13 @@ TextWithEntities PremiumGift::subtitle() {
|
||||
? tr::lng_gift_stars_outgoing(
|
||||
tr::now,
|
||||
lt_user,
|
||||
Ui::Text::Bold(_parent->history()->peer->shortName()),
|
||||
Ui::Text::RichLangValue)
|
||||
: tr::lng_gift_stars_incoming(tr::now, Ui::Text::WithEntities);
|
||||
tr::bold(_parent->history()->peer->shortName()),
|
||||
tr::rich)
|
||||
: tr::lng_gift_stars_incoming(tr::now, tr::marked);
|
||||
} else if (gift()) {
|
||||
return !_data.message.empty()
|
||||
? _data.message
|
||||
: tr::lng_action_gift_premium_about(
|
||||
tr::now,
|
||||
Ui::Text::RichLangValue);
|
||||
: tr::lng_action_gift_premium_about(tr::now, tr::rich);
|
||||
}
|
||||
const auto name = _data.channel ? _data.channel->name() : "channel";
|
||||
auto result = (_data.unclaimed
|
||||
@@ -222,8 +221,8 @@ TextWithEntities PremiumGift::subtitle() {
|
||||
: tr::lng_prize_gift_about)(
|
||||
tr::now,
|
||||
lt_channel,
|
||||
Ui::Text::Bold(name),
|
||||
Ui::Text::RichLangValue);
|
||||
tr::bold(name),
|
||||
tr::rich);
|
||||
result.append("\n\n");
|
||||
result.append(isCreditsPrize
|
||||
? tr::lng_prize_credits(
|
||||
@@ -231,10 +230,10 @@ TextWithEntities PremiumGift::subtitle() {
|
||||
lt_amount,
|
||||
tr::lng_prize_credits_amount(
|
||||
tr::now,
|
||||
lt_count,
|
||||
lt_count_decimal,
|
||||
credits(),
|
||||
Ui::Text::RichLangValue),
|
||||
Ui::Text::RichLangValue)
|
||||
tr::marked),
|
||||
tr::rich)
|
||||
: (_data.unclaimed
|
||||
? tr::lng_prize_unclaimed_duration
|
||||
: _data.viaGiveaway
|
||||
@@ -242,8 +241,8 @@ TextWithEntities PremiumGift::subtitle() {
|
||||
: tr::lng_prize_gift_duration)(
|
||||
tr::now,
|
||||
lt_duration,
|
||||
Ui::Text::Bold(GiftDuration(_data.count)),
|
||||
Ui::Text::RichLangValue));
|
||||
tr::bold(GiftDuration(premiumDays())),
|
||||
tr::rich));
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -423,12 +422,12 @@ void PremiumGift::unloadHeavyPart() {
|
||||
|
||||
bool PremiumGift::incomingGift() const {
|
||||
const auto out = _parent->data()->out();
|
||||
return gift() && (starGiftUpgrade() ? out : !out);
|
||||
return gift() && !_data.auctionTo && (starGiftUpgrade() ? out : !out);
|
||||
}
|
||||
|
||||
bool PremiumGift::outgoingGift() const {
|
||||
const auto out = _parent->data()->out();
|
||||
return gift() && (starGiftUpgrade() ? !out : out);
|
||||
return gift() && (_data.auctionTo || (starGiftUpgrade() ? !out : out));
|
||||
}
|
||||
|
||||
bool PremiumGift::gift() const {
|
||||
@@ -457,6 +456,14 @@ int PremiumGift::credits() const {
|
||||
return (_data.type == Data::GiftType::Credits) ? _data.count : 0;
|
||||
}
|
||||
|
||||
int PremiumGift::premiumDays() const {
|
||||
return (_data.type == Data::GiftType::Premium) ? _data.count : 0;
|
||||
}
|
||||
|
||||
int PremiumGift::premiumMonths() const {
|
||||
return premiumDays() / 30;
|
||||
}
|
||||
|
||||
void PremiumGift::ensureStickerCreated() const {
|
||||
if (_sticker) {
|
||||
return;
|
||||
@@ -485,7 +492,9 @@ void PremiumGift::ensureStickerCreated() const {
|
||||
const auto &session = _parent->history()->session();
|
||||
auto &packs = session.giftBoxStickersPacks();
|
||||
const auto count = credits();
|
||||
const auto months = count ? packs.monthsForStars(count) : _data.count;
|
||||
const auto months = count
|
||||
? packs.monthsForStars(count)
|
||||
: premiumMonths();
|
||||
if (const auto document = packs.lookup(months)) {
|
||||
if (document->sticker()) {
|
||||
const auto skipPremiumEffect = false;
|
||||
|
||||
@@ -60,6 +60,8 @@ private:
|
||||
[[nodiscard]] bool gift() const;
|
||||
[[nodiscard]] bool creditsPrize() const;
|
||||
[[nodiscard]] int credits() const;
|
||||
[[nodiscard]] int premiumDays() const;
|
||||
[[nodiscard]] int premiumMonths() const;
|
||||
void ensureStickerCreated() const;
|
||||
|
||||
const not_null<Element*> _parent;
|
||||
|
||||
@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "history/view/media/history_view_unique_gift.h"
|
||||
|
||||
#include "base/unixtime.h"
|
||||
#include "boxes/star_gift_box.h"
|
||||
#include "chat_helpers/stickers_lottie.h"
|
||||
#include "core/click_handler_types.h"
|
||||
@@ -15,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "data/data_media_types.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_star_gift.h"
|
||||
#include "data/data_web_page.h"
|
||||
#include "history/view/media/history_view_media_generic.h"
|
||||
#include "history/view/media/history_view_premium_gift.h"
|
||||
#include "history/view/history_view_cursor_state.h"
|
||||
@@ -26,6 +28,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "main/main_session.h"
|
||||
#include "settings/settings_credits_graphics.h"
|
||||
#include "ui/chat/chat_style.h"
|
||||
#include "ui/effects/ministar_particles.h"
|
||||
#include "ui/effects/premium_stars_colored.h"
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
@@ -536,6 +539,187 @@ auto GenerateUniqueGiftPreview(
|
||||
};
|
||||
}
|
||||
|
||||
auto GenerateAuctionPreview(
|
||||
not_null<Element*> parent,
|
||||
Element *replacing,
|
||||
std::shared_ptr<Data::StarGift> gift,
|
||||
Data::UniqueGiftBackdrop backdrop,
|
||||
TimeId endDate)
|
||||
-> Fn<void(
|
||||
not_null<MediaGeneric*>,
|
||||
Fn<void(std::unique_ptr<MediaGenericPart>)>)> {
|
||||
return [=](
|
||||
not_null<MediaGeneric*> media,
|
||||
Fn<void(std::unique_ptr<MediaGenericPart>)> push) {
|
||||
const auto sticker = [=] {
|
||||
using Tag = ChatHelpers::StickerLottieSize;
|
||||
return StickerInBubblePart::Data{
|
||||
.sticker = gift->document,
|
||||
.size = st::chatIntroStickerSize,
|
||||
.cacheTag = Tag::ChatIntroHelloSticker,
|
||||
};
|
||||
};
|
||||
push(std::make_unique<StickerInBubblePart>(
|
||||
parent,
|
||||
replacing,
|
||||
sticker,
|
||||
st::webPageAuctionPreviewPadding));
|
||||
const auto name = gift->unique
|
||||
? Data::UniqueGiftName(*gift->unique)
|
||||
: gift->resellTitle;
|
||||
if (!name.isEmpty()) {
|
||||
push(std::make_unique<TextPartColored>(
|
||||
Ui::Text::Bold(name),
|
||||
QMargins(0, 0, 0, st::defaultVerticalListSkip),
|
||||
[c = backdrop.textColor](const auto&) { return c; },
|
||||
st::chatUniqueTitle));
|
||||
}
|
||||
if (const auto all = gift->limitedCount) {
|
||||
push(std::make_unique<TextPartColored>(
|
||||
tr::lng_boosts_list_tab_gifts(
|
||||
tr::now,
|
||||
lt_count,
|
||||
all,
|
||||
Ui::Text::WithEntities),
|
||||
QMargins(0, 0, 0, st::webPageAuctionPreviewPadding.top()),
|
||||
[c = backdrop.textColor](const auto&) { return c; },
|
||||
st::chatUniqueTextStyle));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
auto AuctionBg(
|
||||
not_null<Element*> view,
|
||||
Data::UniqueGiftBackdrop backdrop,
|
||||
std::shared_ptr<Data::StarGift> gift,
|
||||
TimeId endDate)
|
||||
-> Fn<void(
|
||||
Painter&,
|
||||
const Ui::ChatPaintContext&,
|
||||
not_null<const MediaGeneric*>)> {
|
||||
struct State {
|
||||
std::unique_ptr<Ui::Text::CustomEmoji> pattern;
|
||||
base::flat_map<float64, QImage> cache;
|
||||
std::optional<Ui::StarParticles> particles;
|
||||
std::unique_ptr<base::Timer> timer;
|
||||
crl::time pausedAt = 0;
|
||||
crl::time pauseOffset = 0;
|
||||
};
|
||||
const auto state = std::make_shared<State>();
|
||||
if (gift->unique && gift->unique->pattern.document) {
|
||||
state->pattern = view->history()->owner().customEmojiManager().create(
|
||||
gift->unique->pattern.document,
|
||||
[=] { view->repaint(); },
|
||||
Data::CustomEmojiSizeTag::Large);
|
||||
}
|
||||
state->particles.emplace(
|
||||
Ui::StarParticles::Type::RadialInside,
|
||||
25,
|
||||
st::lineWidth * 8);
|
||||
state->particles->setSpeed(0.05);
|
||||
state->particles->setColor(backdrop.textColor);
|
||||
|
||||
return [=](
|
||||
Painter &p,
|
||||
const Ui::ChatPaintContext &context,
|
||||
not_null<const MediaGeneric*> media) {
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.setPen(Qt::NoPen);
|
||||
const auto webpreview = (media.get() != view->media());
|
||||
const auto radius = webpreview
|
||||
? st::roundRadiusLarge
|
||||
: st::msgServiceGiftBoxRadius;
|
||||
const auto full = QRect(0, 0, media->width(), media->height());
|
||||
auto gradient = QRadialGradient(full.center(), full.height() / 2);
|
||||
gradient.setStops({
|
||||
{ 0., backdrop.centerColor },
|
||||
{ 1., backdrop.edgeColor },
|
||||
});
|
||||
p.setBrush(gradient);
|
||||
p.drawRoundedRect(full, radius, radius);
|
||||
|
||||
/*if (state->pattern) {
|
||||
const auto width = media->width();
|
||||
const auto shift = width / 12;
|
||||
const auto doubled = width + 2 * shift;
|
||||
const auto top = (webpreview ? 2 : 1) * (-shift);
|
||||
const auto outer = QRect(-shift, top, doubled, doubled);
|
||||
p.setClipRect(full);
|
||||
if (gift->unique) {
|
||||
Ui::PaintBgPoints(
|
||||
p,
|
||||
Ui::PatternBgPoints(),
|
||||
state->cache,
|
||||
state->pattern.get(),
|
||||
*gift->unique,
|
||||
outer);
|
||||
}
|
||||
p.setClipping(false);
|
||||
}*/
|
||||
|
||||
if (state->particles) {
|
||||
p.setClipRect(full);
|
||||
if (context.paused) {
|
||||
if (!state->pausedAt) {
|
||||
state->pausedAt = crl::now();
|
||||
}
|
||||
const auto diff = state->pausedAt - state->pauseOffset;
|
||||
state->particles->paint(p, full, diff);
|
||||
} else {
|
||||
if (state->pausedAt) {
|
||||
state->pauseOffset += crl::now() - state->pausedAt;
|
||||
state->pausedAt = 0;
|
||||
}
|
||||
const auto diff = context.now - state->pauseOffset;
|
||||
state->particles->paint(p, full, diff);
|
||||
}
|
||||
p.setClipping(false);
|
||||
}
|
||||
|
||||
const auto now = base::unixtime::now();
|
||||
const auto left = std::max(endDate - now, 0);
|
||||
if (left > 0) {
|
||||
if (!state->timer) {
|
||||
state->timer = std::make_unique<base::Timer>([=] {
|
||||
view->repaint();
|
||||
});
|
||||
}
|
||||
state->timer->callOnce(1000);
|
||||
} else if (left <= 0 && state->timer) {
|
||||
state->timer = nullptr;
|
||||
}
|
||||
const auto text = left > 0
|
||||
? QString("%1:%2:%3")
|
||||
.arg(left / 3600, 2, 10, QChar('0'))
|
||||
.arg((left % 3600) / 60, 2, 10, QChar('0'))
|
||||
.arg(left % 60, 2, 10, QChar('0'))
|
||||
: tr::lng_auctino_preview_finished(tr::now);
|
||||
|
||||
const auto &font = st::webPageAuctionTimeFont;
|
||||
const auto textWidth = font->width(text);
|
||||
const auto padding = st::webPageAuctionTimerPadding;
|
||||
const auto timerWidth = textWidth + rect::m::sum::h(padding);
|
||||
const auto timerHeight = font->height + rect::m::sum::v(padding);
|
||||
const auto timerRadius = timerHeight / 2.;
|
||||
const auto timerRect = QRectF(
|
||||
padding.top(),
|
||||
padding.top(),
|
||||
timerWidth,
|
||||
timerHeight);
|
||||
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(st::slideFadeOutBg);
|
||||
p.drawRoundedRect(timerRect, timerRadius, timerRadius);
|
||||
|
||||
p.setPen(backdrop.textColor);
|
||||
p.setFont(font);
|
||||
p.drawText(
|
||||
timerRect.x() + padding.left(),
|
||||
timerRect.y() + padding.top() + font->ascent,
|
||||
text);
|
||||
};
|
||||
}
|
||||
|
||||
std::unique_ptr<MediaGenericPart> MakeGenericButtonPart(
|
||||
const QString &text,
|
||||
QMargins margins,
|
||||
|
||||
@@ -14,6 +14,8 @@ class Painter;
|
||||
namespace Data {
|
||||
class MediaGiftBox;
|
||||
struct UniqueGift;
|
||||
struct UniqueGiftBackdrop;
|
||||
struct StarGift;
|
||||
class Birthday;
|
||||
} // namespace Data
|
||||
|
||||
@@ -51,6 +53,26 @@ class MediaGenericPart;
|
||||
not_null<MediaGeneric*>,
|
||||
Fn<void(std::unique_ptr<MediaGenericPart>)>)>;
|
||||
|
||||
[[nodiscard]] auto GenerateAuctionPreview(
|
||||
not_null<Element*> parent,
|
||||
Element *replacing,
|
||||
std::shared_ptr<Data::StarGift> gift,
|
||||
Data::UniqueGiftBackdrop backdrop,
|
||||
TimeId endDate)
|
||||
-> Fn<void(
|
||||
not_null<MediaGeneric*>,
|
||||
Fn<void(std::unique_ptr<MediaGenericPart>)>)>;
|
||||
|
||||
[[nodiscard]] auto AuctionBg(
|
||||
not_null<Element*> view,
|
||||
Data::UniqueGiftBackdrop backdrop,
|
||||
std::shared_ptr<Data::StarGift> gift,
|
||||
TimeId endDate)
|
||||
-> Fn<void(
|
||||
Painter&,
|
||||
const Ui::ChatPaintContext&,
|
||||
not_null<const MediaGeneric*>)>;
|
||||
|
||||
[[nodiscard]] std::unique_ptr<MediaGenericPart> MakeGenericButtonPart(
|
||||
const QString &text,
|
||||
QMargins margins,
|
||||
|
||||
@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "history/view/media/history_view_web_page.h"
|
||||
|
||||
#include "base/unixtime.h"
|
||||
#include "core/application.h"
|
||||
#include "countries/countries_instance.h"
|
||||
#include "base/qt/qt_key_modifiers.h"
|
||||
@@ -236,6 +237,11 @@ constexpr auto kSponsoredUserpicLines = 2;
|
||||
? tr::lng_view_button_storyalbum(tr::now)
|
||||
: (type == WebPageType::GiftCollection)
|
||||
? tr::lng_view_button_collection(tr::now)
|
||||
: (type == WebPageType::Auction)
|
||||
? (page->auction && page->auction->endDate
|
||||
&& page->auction->endDate <= base::unixtime::now())
|
||||
? tr::lng_auction_preview_view_results(tr::now)
|
||||
: tr::lng_auction_preview_join(tr::now)
|
||||
: QString());
|
||||
if (page->iv) {
|
||||
return Ui::Text::IconEmoji(&st::historyIvIcon).append(text);
|
||||
@@ -271,7 +277,8 @@ constexpr auto kSponsoredUserpicLines = 2;
|
||||
&& webpage->document->isWallPaper())
|
||||
|| (type == WebPageType::StickerSet)
|
||||
|| (type == WebPageType::StoryAlbum)
|
||||
|| (type == WebPageType::GiftCollection);
|
||||
|| (type == WebPageType::GiftCollection)
|
||||
|| (type == WebPageType::Auction);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -517,8 +524,36 @@ QSize WebPage::countOptimalSize() {
|
||||
_data->uniqueGift),
|
||||
MediaGenericDescriptor{
|
||||
.maxWidth = st::msgServiceGiftPreview,
|
||||
.paintBg = UniqueGiftBg(_parent, _data->uniqueGift),
|
||||
.paintBgFactory = [=] {
|
||||
return UniqueGiftBg(_parent, _data->uniqueGift);
|
||||
},
|
||||
});
|
||||
} else if (!_attach && _data->auction) {
|
||||
const auto &gift = _data->auction->auctionGift;
|
||||
const auto backdrop = Data::UniqueGiftBackdrop{
|
||||
.centerColor = _data->auction->centerColor,
|
||||
.edgeColor = _data->auction->edgeColor,
|
||||
.patternColor = _data->auction->edgeColor,
|
||||
.textColor = _data->auction->textColor,
|
||||
};
|
||||
_attach = std::make_unique<MediaGeneric>(
|
||||
_parent,
|
||||
GenerateAuctionPreview(
|
||||
_parent,
|
||||
nullptr,
|
||||
gift,
|
||||
backdrop,
|
||||
_data->auction->endDate),
|
||||
MediaGenericDescriptor{
|
||||
.maxWidth = st::msgServiceGiftPreview,
|
||||
.paintBgFactory = [=] {
|
||||
return AuctionBg(
|
||||
_parent,
|
||||
backdrop,
|
||||
gift,
|
||||
_data->auction->endDate);
|
||||
},
|
||||
});
|
||||
} else if (!_attach && !_asArticle) {
|
||||
_attach = CreateAttach(
|
||||
_parent,
|
||||
@@ -533,7 +568,8 @@ QSize WebPage::countOptimalSize() {
|
||||
// init strings
|
||||
if (_description.isEmpty()
|
||||
&& !_data->description.text.isEmpty()
|
||||
&& !_data->uniqueGift) {
|
||||
&& !_data->uniqueGift
|
||||
&& !_data->auction) {
|
||||
const auto &text = _data->description;
|
||||
using Type = Core::TextContextDetails::HashtagMentionType;
|
||||
auto context = Core::TextContext({
|
||||
|
||||
@@ -40,6 +40,36 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include <QtCore/QCoreApplication>
|
||||
|
||||
namespace Info {
|
||||
namespace {
|
||||
|
||||
class FlexibleFiller final : public Ui::RpWidget {
|
||||
public:
|
||||
using RpWidget::RpWidget;
|
||||
|
||||
void setTargetWidget(base::unique_qptr<RpWidget> widget);
|
||||
|
||||
private:
|
||||
void visibleTopBottomUpdated(int visibleTop, int visibleBottom) override;
|
||||
|
||||
base::unique_qptr<RpWidget> _target;
|
||||
|
||||
};
|
||||
|
||||
void FlexibleFiller::setTargetWidget(base::unique_qptr<RpWidget> widget) {
|
||||
Expects(!_target);
|
||||
|
||||
_target = std::move(widget);
|
||||
}
|
||||
|
||||
void FlexibleFiller::visibleTopBottomUpdated(
|
||||
int visibleTop,
|
||||
int visibleBottom) {
|
||||
if (const auto raw = _target.get()) {
|
||||
raw->setVisibleTopBottom(visibleTop, visibleBottom);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ContentWidget::ContentWidget(
|
||||
QWidget *parent,
|
||||
@@ -203,6 +233,36 @@ Ui::RpWidget *ContentWidget::doSetInnerWidget(
|
||||
return _innerWrap->entity();
|
||||
}
|
||||
|
||||
Ui::RpWidget *ContentWidget::doSetupFlexibleInnerWidget(
|
||||
object_ptr<Ui::RpWidget> inner,
|
||||
FlexibleScrollData &flexibleScroll,
|
||||
Fn<void(Ui::RpWidget*)> customSetup) {
|
||||
const auto filler = setInnerWidget(object_ptr<FlexibleFiller>(this));
|
||||
filler->resize(1, 1);
|
||||
|
||||
flexibleScroll.contentHeightValue.events(
|
||||
) | rpl::start_with_next([=](int h) {
|
||||
filler->resize(filler->width(), h);
|
||||
}, filler->lifetime());
|
||||
|
||||
filler->widthValue(
|
||||
) | rpl::start_to_stream(
|
||||
flexibleScroll.fillerWidthValue,
|
||||
filler->lifetime());
|
||||
|
||||
if (customSetup) {
|
||||
customSetup(filler);
|
||||
}
|
||||
|
||||
// ScrollArea -> PaddingWrap -> RpWidget.
|
||||
const auto result = inner.release();
|
||||
result->setParent(filler->parentWidget()->parentWidget());
|
||||
result->raise();
|
||||
filler->setTargetWidget(base::unique_qptr<Ui::RpWidget>(result));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
int ContentWidget::scrollTillBottom(int forHeight) const {
|
||||
const auto scrollHeight = forHeight
|
||||
- _scrollTopSkip.current()
|
||||
@@ -384,7 +444,7 @@ void ContentWidget::refreshSearchField(bool shown) {
|
||||
view->show();
|
||||
_searchField->setFocus();
|
||||
setScrollTopSkip(view->heightNoMargins() - st::lineWidth);
|
||||
} else {
|
||||
} else if (_searchWrap) {
|
||||
if (Ui::InFocusChain(this)) {
|
||||
setFocus();
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ namespace Info {
|
||||
|
||||
class ContentMemento;
|
||||
class Controller;
|
||||
struct FlexibleScrollData;
|
||||
|
||||
class ContentWidget : public Ui::RpWidget {
|
||||
public:
|
||||
@@ -155,40 +156,18 @@ protected:
|
||||
doSetInnerWidget(std::move(inner)));
|
||||
}
|
||||
|
||||
template <typename Widget, typename FlexibleData>
|
||||
template <typename Widget>
|
||||
Widget *setupFlexibleInnerWidget(
|
||||
object_ptr<Widget> inner,
|
||||
FlexibleData &flexibleScroll,
|
||||
FlexibleScrollData &flexibleScroll,
|
||||
Fn<void(Ui::RpWidget*)> customSetup = nullptr) {
|
||||
if (inner->hasFlexibleTopBar()) {
|
||||
auto filler = setInnerWidget(object_ptr<Ui::RpWidget>(this));
|
||||
filler->resize(1, 1);
|
||||
|
||||
flexibleScroll.contentHeightValue.events(
|
||||
) | rpl::start_with_next([=](int h) {
|
||||
filler->resize(filler->width(), h);
|
||||
}, filler->lifetime());
|
||||
|
||||
filler->widthValue(
|
||||
) | rpl::start_to_stream(
|
||||
flexibleScroll.fillerWidthValue,
|
||||
lifetime());
|
||||
|
||||
if (customSetup) {
|
||||
customSetup(filler);
|
||||
}
|
||||
|
||||
// ScrollArea -> PaddingWrap -> RpWidget.
|
||||
inner->setParent(filler->parentWidget()->parentWidget());
|
||||
inner->raise();
|
||||
|
||||
using InnerPtr = base::unique_qptr<Widget>;
|
||||
auto owner = filler->lifetime().make_state<InnerPtr>(
|
||||
std::move(inner.release()));
|
||||
return owner->get();
|
||||
} else {
|
||||
if (!inner->hasFlexibleTopBar()) {
|
||||
return setInnerWidget(std::move(inner));
|
||||
}
|
||||
return static_cast<Widget*>(doSetupFlexibleInnerWidget(
|
||||
std::move(inner),
|
||||
flexibleScroll,
|
||||
std::move(customSetup)));
|
||||
}
|
||||
|
||||
[[nodiscard]] not_null<Controller*> controller() const {
|
||||
@@ -214,7 +193,12 @@ protected:
|
||||
void setViewport(rpl::producer<not_null<QEvent*>> &&events) const;
|
||||
|
||||
private:
|
||||
RpWidget *doSetInnerWidget(object_ptr<RpWidget> inner);
|
||||
Ui::RpWidget *doSetInnerWidget(object_ptr<Ui::RpWidget> inner);
|
||||
Ui::RpWidget *doSetupFlexibleInnerWidget(
|
||||
object_ptr<Ui::RpWidget> inner,
|
||||
FlexibleScrollData &flexibleScroll,
|
||||
Fn<void(Ui::RpWidget*)> customSetup);
|
||||
|
||||
void updateControlsGeometry();
|
||||
void refreshSearchField(bool shown);
|
||||
void setupSwipeHandler(not_null<Ui::RpWidget*> widget);
|
||||
|
||||
@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "info/info_flexible_scroll.h"
|
||||
|
||||
#include "ui/effects/animation_value.h"
|
||||
#include "ui/widgets/scroll_area.h"
|
||||
#include "base/event_filter.h"
|
||||
#include "base/options.h"
|
||||
@@ -65,7 +66,7 @@ void FlexibleScrollHelper::setupScrollAnimation() {
|
||||
_scrollTopFrom,
|
||||
_scrollTopTo,
|
||||
std::clamp(eased, 0., 1.));
|
||||
_scroll->scrollToY(scrollCurrent);
|
||||
scrollToY(scrollCurrent);
|
||||
_lastScrollApplied = scrollCurrent;
|
||||
if (progress >= 1) {
|
||||
clearScrollState();
|
||||
@@ -121,7 +122,7 @@ void FlexibleScrollHelper::setupScrollHandling() {
|
||||
: -1);
|
||||
{
|
||||
_applyingFakeScrollState = true;
|
||||
_scroll->scrollToY(previousValue);
|
||||
scrollToY(previousValue);
|
||||
_applyingFakeScrollState = false;
|
||||
}
|
||||
if (_scrollAnimation.animating()
|
||||
@@ -209,16 +210,21 @@ void FlexibleScrollHelper::setupScrollHandling() {
|
||||
}
|
||||
|
||||
void FlexibleScrollHelper::setupScrollHandlingWithFilter() {
|
||||
const auto heightDiff = [=] {
|
||||
return _pinnedToTop->maximumHeight()
|
||||
- _pinnedToTop->minimumHeight();
|
||||
};
|
||||
|
||||
rpl::combine(
|
||||
_pinnedToTop->heightValue(),
|
||||
_inner->heightValue()
|
||||
) | rpl::start_with_next([=](int, int h) {
|
||||
_data.contentHeightValue.fire(h + heightDiff());
|
||||
const auto max = _pinnedToTop->maximumHeight();
|
||||
const auto min = _pinnedToTop->minimumHeight();
|
||||
const auto diff = max - min;
|
||||
const auto progress = (diff > 0)
|
||||
? std::clamp(
|
||||
(_pinnedToTop->height() - min) / float64(diff),
|
||||
0.,
|
||||
1.)
|
||||
: 1.;
|
||||
_data.contentHeightValue.fire(h
|
||||
+ anim::interpolate(diff, 0, progress));
|
||||
}, _pinnedToTop->lifetime());
|
||||
|
||||
const auto singleStep = _scroll->verticalScrollBar()->singleStep()
|
||||
@@ -237,7 +243,8 @@ void FlexibleScrollHelper::setupScrollHandlingWithFilter() {
|
||||
const auto wheel = static_cast<QWheelEvent*>(e.get());
|
||||
const auto delta = wheel->angleDelta().y();
|
||||
if (std::abs(delta) != 120) {
|
||||
return base::EventFilterResult::Continue;
|
||||
scrollToY(_scroll->scrollTop() - delta);
|
||||
return base::EventFilterResult::Cancel;
|
||||
}
|
||||
const auto actualTop = _scroll->scrollTop();
|
||||
const auto animationActive = _scrollAnimation.animating()
|
||||
@@ -318,13 +325,8 @@ void FlexibleScrollHelper::setupScrollHandlingWithFilter() {
|
||||
return base::EventFilterResult::Cancel;
|
||||
}, _filterLifetime);
|
||||
|
||||
_scroll->scrollTopValue(
|
||||
) | rpl::start_with_next([=](int top) {
|
||||
const auto current = heightDiff() - top;
|
||||
_inner->moveToLeft(0, std::min(0, current));
|
||||
_pinnedToTop->resize(
|
||||
_pinnedToTop->width(),
|
||||
std::max(current + _pinnedToTop->minimumHeight(), 0));
|
||||
_scroll->scrollTopValue() | rpl::start_with_next([=](int top) {
|
||||
applyScrollToPinnedLayout(top);
|
||||
}, _inner->lifetime());
|
||||
|
||||
_data.fillerWidthValue.events(
|
||||
@@ -339,4 +341,21 @@ void FlexibleScrollHelper::setupScrollHandlingWithFilter() {
|
||||
}));
|
||||
}
|
||||
|
||||
} // namespace Info
|
||||
void FlexibleScrollHelper::scrollToY(int scrollCurrent) {
|
||||
applyScrollToPinnedLayout(scrollCurrent);
|
||||
_scroll->scrollToY(scrollCurrent);
|
||||
}
|
||||
|
||||
void FlexibleScrollHelper::applyScrollToPinnedLayout(int scrollCurrent) {
|
||||
const auto top = std::min(scrollCurrent, _scroll->scrollTopMax());
|
||||
const auto minimumHeight = _pinnedToTop->minimumHeight();
|
||||
const auto current = _pinnedToTop->maximumHeight()
|
||||
- minimumHeight
|
||||
- top;
|
||||
_inner->moveToLeft(0, std::min(0, current));
|
||||
_pinnedToTop->resize(
|
||||
_pinnedToTop->width(),
|
||||
std::max(current + minimumHeight, 0));
|
||||
}
|
||||
|
||||
} // namespace Info
|
||||
|
||||