Compare commits
79 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
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
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
BIN
Telegram/Resources/export_html/images/section_music.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 446 B |
BIN
Telegram/Resources/export_html/images/section_music@2x.png
Normal file
BIN
Telegram/Resources/export_html/images/section_music@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 777 B |
BIN
Telegram/Resources/icons/chat/mini_gift_hidden.png
Normal file
BIN
Telegram/Resources/icons/chat/mini_gift_hidden.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 544 B |
BIN
Telegram/Resources/icons/chat/mini_gift_hidden@2x.png
Normal file
BIN
Telegram/Resources/icons/chat/mini_gift_hidden@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 938 B |
BIN
Telegram/Resources/icons/chat/mini_gift_hidden@3x.png
Normal file
BIN
Telegram/Resources/icons/chat/mini_gift_hidden@3x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 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.2.0" />
|
||||
<Properties>
|
||||
<DisplayName>Telegram Desktop</DisplayName>
|
||||
<PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName>
|
||||
|
||||
@@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico"
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 6,3,0,0
|
||||
PRODUCTVERSION 6,3,0,0
|
||||
FILEVERSION 6,3,2,0
|
||||
PRODUCTVERSION 6,3,2,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.2.0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2014-2025"
|
||||
VALUE "ProductName", "Telegram Desktop"
|
||||
VALUE "ProductVersion", "6.3.0.0"
|
||||
VALUE "ProductVersion", "6.3.2.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,2,0
|
||||
PRODUCTVERSION 6,3,2,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.2.0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2014-2025"
|
||||
VALUE "ProductName", "Telegram Desktop"
|
||||
VALUE "ProductVersion", "6.3.0.0"
|
||||
VALUE "ProductVersion", "6.3.2.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,
|
||||
|
||||
@@ -9,7 +9,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
|
||||
#include "api/api_text_entities.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "boxes/peers/replace_boost_box.h"
|
||||
#include "boxes/send_credits_box.h" // CreditsEmojiSmall
|
||||
#include "boxes/share_box.h"
|
||||
#include "boxes/star_gift_box.h"
|
||||
#include "calls/group/calls_group_common.h"
|
||||
#include "core/credits_amount.h"
|
||||
@@ -17,7 +19,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "data/components/credits.h"
|
||||
#include "data/components/gift_auctions.h"
|
||||
#include "data/data_message_reactions.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_user.h"
|
||||
#include "history/view/controls/history_view_suggest_options.h"
|
||||
#include "info/channel_statistics/earn/earn_icons.h"
|
||||
#include "info/peer_gifts/info_peer_gifts_common.h"
|
||||
#include "lang/lang_keys.h"
|
||||
@@ -39,7 +43,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "ui/text/format_values.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/toast/toast.h"
|
||||
#include "ui/widgets/fields/number_input.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "ui/wrap/table_layout.h"
|
||||
#include "ui/color_int_conversion.h"
|
||||
#include "ui/dynamic_thumbnails.h"
|
||||
@@ -47,12 +53,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "window/window_session_controller.h"
|
||||
#include "styles/style_calls.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
#include "styles/style_credits.h"
|
||||
#include "styles/style_giveaway.h"
|
||||
#include "styles/style_info.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_menu_icons.h"
|
||||
#include "styles/style_premium.h"
|
||||
#include "styles/style_settings.h"
|
||||
|
||||
#include <QtWidgets/QApplication>
|
||||
#include <QtGui/QClipboard>
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
@@ -60,6 +71,7 @@ namespace {
|
||||
constexpr auto kAuctionAboutShownPref = "gift_auction_about_shown"_cs;
|
||||
constexpr auto kBidPlacedToastDuration = 5 * crl::time(1000);
|
||||
constexpr auto kMaxShownBid = 30'000;
|
||||
constexpr auto kShowTopPlaces = 3;
|
||||
|
||||
enum class BidType {
|
||||
Setting,
|
||||
@@ -69,6 +81,8 @@ enum class BidType {
|
||||
struct BidRowData {
|
||||
UserData *user = nullptr;
|
||||
int stars = 0;
|
||||
int position = 0;
|
||||
int winners = 0;
|
||||
QString place;
|
||||
BidType type = BidType::Setting;
|
||||
|
||||
@@ -77,13 +91,26 @@ struct BidRowData {
|
||||
const BidRowData &) = default;
|
||||
};
|
||||
|
||||
[[nodiscard]] std::optional<QColor> BidColorOverride(BidType type) {
|
||||
switch (type) {
|
||||
case BidType::Setting: return {};
|
||||
case BidType::Winning: return st::boxTextFgGood->c;
|
||||
case BidType::Loosing: return st::attentionButtonFg->c;
|
||||
}
|
||||
Unexpected("Type in BidType.");
|
||||
struct BidSliderValues {
|
||||
int min = 0;
|
||||
int explicitlyAllowed = 0;
|
||||
int max = 0;
|
||||
|
||||
friend inline bool operator==(
|
||||
const BidSliderValues &,
|
||||
const BidSliderValues &) = default;
|
||||
};
|
||||
|
||||
[[nodiscard]] std::optional<QColor> BidColorOverride(int position, int per) {
|
||||
return (position <= per)
|
||||
? st::boxTextFgGood->c
|
||||
: st::attentionButtonFg->c;
|
||||
//switch (type) {
|
||||
//case BidType::Setting: return {};
|
||||
//case BidType::Winning: return st::boxTextFgGood->c;
|
||||
//case BidType::Loosing: return st::attentionButtonFg->c;
|
||||
//}
|
||||
//Unexpected("Type in BidType.");
|
||||
}
|
||||
|
||||
[[nodiscard]] rpl::producer<int> MinutesLeftTillValue(TimeId endDate) {
|
||||
@@ -186,7 +213,7 @@ struct BidRowData {
|
||||
}));
|
||||
const auto star = helper.paletteDependent(Ui::Earn::IconCreditsEmoji());
|
||||
auto stars = rpl::duplicate(data) | rpl::map([=](const BidRowData &bid) {
|
||||
return TextWithEntities{ star }.append(' ').append(
|
||||
return tr::marked(star).append(' ').append(
|
||||
Lang::FormatCountDecimal(bid.stars));
|
||||
});
|
||||
state->stars = std::make_unique<FlatLabel>(
|
||||
@@ -200,7 +227,8 @@ struct BidRowData {
|
||||
const auto userpicLeft = st::auctionBidPlace.style.font->width(kHuge);
|
||||
|
||||
std::move(data) | rpl::start_with_next([=](BidRowData bid) {
|
||||
state->place->setTextColorOverride(BidColorOverride(bid.type));
|
||||
state->place->setTextColorOverride(
|
||||
BidColorOverride(bid.position, bid.winners));
|
||||
if (state->user != bid.user) {
|
||||
state->user = bid.user;
|
||||
if (state->user) {
|
||||
@@ -243,6 +271,37 @@ struct BidRowData {
|
||||
return result;
|
||||
}
|
||||
|
||||
Fn<void()> MakeAuctionMenuCallback(
|
||||
not_null<QWidget*> parent,
|
||||
std::shared_ptr<ChatHelpers::Show> show,
|
||||
const Data::GiftAuctionState &state) {
|
||||
const auto url = show->session().createInternalLinkFull(
|
||||
u"auction/"_q + state.gift->auctionSlug);
|
||||
const auto rounds = state.totalRounds;
|
||||
const auto perRound = state.gift->auctionGiftsPerRound;;
|
||||
const auto menu = std::make_shared<base::unique_qptr<PopupMenu>>();
|
||||
return [=] {
|
||||
*menu = base::make_unique_q<Ui::PopupMenu>(
|
||||
parent,
|
||||
st::popupMenuWithIcons);
|
||||
|
||||
(*menu)->addAction(tr::lng_auction_menu_about(tr::now), [=] {
|
||||
show->show(Box(AuctionAboutBox, rounds, perRound, nullptr));
|
||||
}, &st::menuIconInfo);
|
||||
|
||||
(*menu)->addAction(tr::lng_auction_menu_copy_link(tr::now), [=] {
|
||||
QApplication::clipboard()->setText(url);
|
||||
show->showToast(tr::lng_username_copied(tr::now));
|
||||
}, &st::menuIconLink);
|
||||
|
||||
(*menu)->addAction(tr::lng_auction_menu_share(tr::now), [=] {
|
||||
FastShareLink(show, url);
|
||||
}, &st::menuIconShare);
|
||||
|
||||
(*menu)->popup(QCursor::pos());
|
||||
};
|
||||
}
|
||||
|
||||
void PlaceAuctionBid(
|
||||
std::shared_ptr<ChatHelpers::Show> show,
|
||||
not_null<PeerData*> to,
|
||||
@@ -291,7 +350,8 @@ void PlaceAuctionBid(
|
||||
object_ptr<RpWidget> MakeAuctionInfoBlocks(
|
||||
not_null<RpWidget*> box,
|
||||
not_null<Main::Session*> session,
|
||||
rpl::producer<Data::GiftAuctionState> stateValue) {
|
||||
rpl::producer<Data::GiftAuctionState> stateValue,
|
||||
Fn<void()> setMinimal) {
|
||||
auto helper = Text::CustomEmojiHelper(Core::TextContext({
|
||||
.session = session,
|
||||
}));
|
||||
@@ -300,14 +360,22 @@ object_ptr<RpWidget> MakeAuctionInfoBlocks(
|
||||
auto bidTitle = rpl::duplicate(
|
||||
stateValue
|
||||
) | rpl::map([=](const Data::GiftAuctionState &state) {
|
||||
return TextWithEntities{
|
||||
star
|
||||
}.append(' ').append(Lang::FormatCountDecimal(state.minBidAmount));
|
||||
const auto count = int(state.my.minBidAmount
|
||||
? state.my.minBidAmount
|
||||
: state.minBidAmount);
|
||||
const auto text = (count >= 10'000'000)
|
||||
? Lang::FormatCountToShort(count).string
|
||||
: (count >= 1000'000)
|
||||
? Lang::FormatCountToShort(count, true).string
|
||||
: Lang::FormatCountDecimal(count);
|
||||
return tr::marked(star).append(' ').append(text);
|
||||
});
|
||||
auto minimal = rpl::duplicate(
|
||||
stateValue
|
||||
) | rpl::map([=](const Data::GiftAuctionState &state) {
|
||||
return state.minBidAmount;
|
||||
return state.my.minBidAmount
|
||||
? state.my.minBidAmount
|
||||
: state.minBidAmount;
|
||||
}) | tr::to_count();
|
||||
auto untilTitle = rpl::duplicate(
|
||||
stateValue
|
||||
@@ -341,6 +409,7 @@ object_ptr<RpWidget> MakeAuctionInfoBlocks(
|
||||
.subtext = tr::lng_auction_bid_minimal(
|
||||
lt_count,
|
||||
std::move(minimal)),
|
||||
.click = setMinimal,
|
||||
},
|
||||
{
|
||||
.title = std::move(untilTitle),
|
||||
@@ -368,6 +437,7 @@ void AddBidPlaces(
|
||||
rpl::variable<My> my;
|
||||
rpl::variable<std::vector<BidRowData>> top;
|
||||
std::vector<Ui::PeerUserpicView> cache;
|
||||
int winners = 0;
|
||||
};
|
||||
const auto state = box->lifetime().make_state<State>();
|
||||
|
||||
@@ -379,6 +449,7 @@ void AddBidPlaces(
|
||||
for (const auto &user : value.topBidders) {
|
||||
cache.push_back(user->createUserpicView());
|
||||
}
|
||||
state->winners = value.gift->auctionGiftsPerRound;
|
||||
state->cache = std::move(cache);
|
||||
}, box->lifetime());
|
||||
|
||||
@@ -390,14 +461,16 @@ void AddBidPlaces(
|
||||
const auto &levels = value.bidLevels;
|
||||
|
||||
auto top = std::vector<BidRowData>();
|
||||
top.reserve(3);
|
||||
top.reserve(kShowTopPlaces);
|
||||
const auto pushTop = [&](auto i) {
|
||||
const auto index = int(i - begin(levels));
|
||||
if (top.size() < 3 && index < value.topBidders.size()) {
|
||||
if (top.size() >= kShowTopPlaces
|
||||
|| index >= value.topBidders.size()) {
|
||||
return false;
|
||||
} else if (!value.topBidders[index]->isSelf()) {
|
||||
top.push_back({ value.topBidders[index], int(i->amount) });
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
const auto setting = (chosen > my);
|
||||
@@ -426,6 +499,7 @@ void AddBidPlaces(
|
||||
}
|
||||
pushTop(i);
|
||||
}
|
||||
top.push_back({ show->session().user(), chosen });
|
||||
return finishWith((levels.empty() ? 0 : levels.back().position) + 1);
|
||||
});
|
||||
auto myLabelText = state->my.value() | rpl::map([](My my) {
|
||||
@@ -440,15 +514,18 @@ void AddBidPlaces(
|
||||
box->verticalLayout(),
|
||||
std::move(myLabelText));
|
||||
state->my.value() | rpl::start_with_next([=](My my) {
|
||||
myLabel->setTextColorOverride(BidColorOverride(my.type));
|
||||
myLabel->setTextColorOverride(
|
||||
BidColorOverride(my.position, state->winners));
|
||||
}, myLabel->lifetime());
|
||||
|
||||
auto bid = rpl::combine(
|
||||
state->my.value(),
|
||||
rpl::duplicate(chosen)
|
||||
) | rpl::map([=, user = show->session().user()](My my, int stars) {
|
||||
const auto place = QString::number(my.position);
|
||||
return BidRowData{ user, stars, place, my.type };
|
||||
const auto position = my.position;
|
||||
const auto winners = state->winners;
|
||||
const auto place = QString::number(position);
|
||||
return BidRowData{ user, stars, position, winners, place, my.type };
|
||||
});
|
||||
box->addRow(MakeBidRow(box, show, std::move(bid)));
|
||||
|
||||
@@ -456,7 +533,7 @@ void AddBidPlaces(
|
||||
box->verticalLayout(),
|
||||
tr::lng_auction_bid_winners_title(),
|
||||
{ 0, st::paidReactTitleSkip / 2, 0, 0 });
|
||||
for (auto i = 0; i != 3; ++i) {
|
||||
for (auto i = 0; i != kShowTopPlaces; ++i) {
|
||||
auto icon = QString::fromUtf8("\xf0\x9f\xa5\x87");
|
||||
icon.back().unicode() += i;
|
||||
|
||||
@@ -470,9 +547,44 @@ void AddBidPlaces(
|
||||
}
|
||||
}
|
||||
|
||||
void AuctionBidBox(not_null<GenericBox*> box, AuctionBidBoxArgs &&args) {
|
||||
const auto weak = base::make_weak(box);
|
||||
void EditCustomBid(
|
||||
not_null<GenericBox*> box,
|
||||
std::shared_ptr<ChatHelpers::Show> show,
|
||||
Fn<void(int)> save,
|
||||
rpl::producer<int> minBid,
|
||||
int current) {
|
||||
box->setTitle(tr::lng_auction_bid_custom_title());
|
||||
|
||||
const auto container = box->verticalLayout();
|
||||
|
||||
box->addTopButton(st::boxTitleClose, [=] { box->closeBox(); });
|
||||
|
||||
const auto starsField = HistoryView::AddStarsInputField(container, {
|
||||
.value = current,
|
||||
});
|
||||
|
||||
const auto min = box->lifetime().make_state<rpl::variable<int>>(
|
||||
std::move(minBid));
|
||||
|
||||
box->setFocusCallback([=] {
|
||||
starsField->setFocusFast();
|
||||
});
|
||||
|
||||
box->addButton(tr::lng_settings_save(), [=] {
|
||||
const auto value = starsField->getLastText().toLongLong();
|
||||
if (value <= min->current() || value > 1'000'000'000) {
|
||||
starsField->showError();
|
||||
return;
|
||||
}
|
||||
save(value);
|
||||
box->closeBox();
|
||||
});
|
||||
box->addButton(tr::lng_cancel(), [=] {
|
||||
box->closeBox();
|
||||
});
|
||||
}
|
||||
|
||||
void AuctionBidBox(not_null<GenericBox*> box, AuctionBidBoxArgs &&args) {
|
||||
using namespace Info::PeerGifts;
|
||||
struct State {
|
||||
State(rpl::producer<Data::GiftAuctionState> value)
|
||||
@@ -480,61 +592,75 @@ void AuctionBidBox(not_null<GenericBox*> box, AuctionBidBoxArgs &&args) {
|
||||
}
|
||||
|
||||
rpl::variable<Data::GiftAuctionState> value;
|
||||
rpl::variable<BidSliderValues> sliderValues;
|
||||
rpl::variable<int> chosen;
|
||||
rpl::variable<QString> subtext;
|
||||
bool placing = false;
|
||||
};
|
||||
const auto state = box->lifetime().make_state<State>(
|
||||
std::move(args.state));
|
||||
const auto &now = state->value.current();
|
||||
const auto mine = int(now.my.bid);
|
||||
const auto min = std::max(
|
||||
int(mine ? now.my.minBidAmount : now.minBidAmount),
|
||||
1);
|
||||
const auto last = now.bidLevels.empty()
|
||||
? 0
|
||||
: now.bidLevels.front().amount;
|
||||
const auto max = std::max({
|
||||
min + 1,
|
||||
kMaxShownBid,
|
||||
int(base::SafeRound(mine * 1.2)),
|
||||
int(base::SafeRound(last * 1.2)),
|
||||
state->sliderValues = state->value.value(
|
||||
) | rpl::map([=](const Data::GiftAuctionState &value) {
|
||||
const auto mine = int(value.my.bid);
|
||||
const auto min = std::max(1, int(value.my.minBidAmount
|
||||
? value.my.minBidAmount
|
||||
: value.minBidAmount));
|
||||
const auto last = value.bidLevels.empty()
|
||||
? 0
|
||||
: value.bidLevels.front().amount;
|
||||
auto max = std::max({
|
||||
min + 1,
|
||||
kMaxShownBid,
|
||||
int(base::SafeRound(mine * 1.2)),
|
||||
});
|
||||
if (max < last * 1.05) {
|
||||
max = int(base::SafeRound(last * 1.2));
|
||||
}
|
||||
return BidSliderValues{
|
||||
.min = min,
|
||||
.explicitlyAllowed = mine,
|
||||
.max = max,
|
||||
};
|
||||
});
|
||||
const auto chosen = mine ? mine : std::clamp(mine, min, max);
|
||||
state->chosen = chosen;
|
||||
|
||||
const auto show = args.show;
|
||||
const auto giftId = state->value.current().gift->id;
|
||||
const auto &sliderValues = state->sliderValues.current();
|
||||
state->chosen = sliderValues.explicitlyAllowed
|
||||
? sliderValues.explicitlyAllowed
|
||||
: sliderValues.min;
|
||||
|
||||
state->subtext = rpl::combine(
|
||||
state->value.value(),
|
||||
state->chosen.value()
|
||||
) | rpl::map([=](
|
||||
const Data::GiftAuctionState &value,
|
||||
int chosen) {
|
||||
if (value.my.bid == chosen) {
|
||||
return tr::lng_auction_bid_your(tr::now);
|
||||
} else if (chosen == state->sliderValues.current().max) {
|
||||
return tr::lng_auction_bid_custom(tr::now);
|
||||
} else if (value.my.bid && chosen > value.my.bid) {
|
||||
const auto delta = chosen - value.my.bid;
|
||||
return '+' + Lang::FormatCountDecimal(delta);
|
||||
}
|
||||
return QString();
|
||||
});
|
||||
|
||||
args.peer->owner().giftAuctionGots(
|
||||
) | rpl::start_with_next([=](const Data::GiftAuctionGot &update) {
|
||||
if (update.giftId == giftId) {
|
||||
box->closeBox();
|
||||
|
||||
if (const auto window = show->resolveWindow()) {
|
||||
window->showPeer(update.to, ShowAtTheEndMsgId);
|
||||
}
|
||||
}
|
||||
}, box->lifetime());
|
||||
|
||||
const auto details = args.details
|
||||
? *args.details
|
||||
: std::optional<GiftSendDetails>();
|
||||
const auto save = [=, peer = args.peer](int amount) {
|
||||
const auto ¤t = state->value.current();
|
||||
if (amount > current.my.bid) {
|
||||
const auto was = (current.my.bid > 0);
|
||||
const auto perRound = current.gift->auctionGiftsPerRound;
|
||||
const auto done = [=](Payments::CheckoutResult result) {
|
||||
if (result == Payments::CheckoutResult::Paid) {
|
||||
show->showToast({
|
||||
.title = (was
|
||||
? tr::lng_auction_bid_increased_title
|
||||
: tr::lng_auction_bid_placed_title)(
|
||||
tr::now),
|
||||
.text = tr::lng_auction_bid_done_text(
|
||||
tr::now,
|
||||
lt_count,
|
||||
perRound,
|
||||
tr::rich),
|
||||
.duration = kBidPlacedToastDuration,
|
||||
});
|
||||
}
|
||||
};
|
||||
auto owned = details
|
||||
? std::make_unique<GiftSendDetails>(*details)
|
||||
: nullptr;
|
||||
PlaceAuctionBid(show, peer, amount, now, std::move(owned), done);
|
||||
}
|
||||
if (const auto strong = weak.get()) {
|
||||
strong->closeBox();
|
||||
}
|
||||
};
|
||||
const auto colorings = show->session().appConfig().groupCallColorings();
|
||||
|
||||
box->setWidth(st::boxWideWidth);
|
||||
@@ -550,21 +676,63 @@ void AuctionBidBox(not_null<GenericBox*> box, AuctionBidBoxArgs &&args) {
|
||||
count);
|
||||
return ColorFromSerialized(coloring.bgLight);
|
||||
};
|
||||
AddStarSelectBubble(box, state->chosen.value(), max, activeFgOverride);
|
||||
const auto sliderWrap = content->add(
|
||||
object_ptr<VerticalLayout>(content));
|
||||
state->sliderValues.value(
|
||||
) | rpl::start_with_next([=](const BidSliderValues &values) {
|
||||
const auto initial = !sliderWrap->count();
|
||||
if (!initial) {
|
||||
while (sliderWrap->count()) {
|
||||
delete sliderWrap->widgetAt(0);
|
||||
}
|
||||
while (!sliderWrap->children().isEmpty()) {
|
||||
delete sliderWrap->children().front();
|
||||
}
|
||||
}
|
||||
|
||||
PaidReactionSlider(
|
||||
content,
|
||||
st::paidReactSlider,
|
||||
min,
|
||||
mine,
|
||||
chosen,
|
||||
max,
|
||||
[=](int count) { state->chosen = count; },
|
||||
activeFgOverride);
|
||||
const auto bubble = AddStarSelectBubble(
|
||||
sliderWrap,
|
||||
initial ? BoxShowFinishes(box) : nullptr,
|
||||
state->chosen.value(),
|
||||
values.max,
|
||||
activeFgOverride);
|
||||
bubble->setAttribute(Qt::WA_TransparentForMouseEvents, false);
|
||||
bubble->setClickedCallback([=] {
|
||||
auto min = state->value.value(
|
||||
) | rpl::map([=](const Data::GiftAuctionState &state) {
|
||||
return std::max(1, int(state.my.minBidAmount
|
||||
? state.my.minBidAmount
|
||||
: state.minBidAmount));
|
||||
});
|
||||
show->show(Box(EditCustomBid, show, crl::guard(box, [=](int v) {
|
||||
state->chosen = v;
|
||||
}), std::move(min), state->chosen.current()));
|
||||
});
|
||||
state->subtext.value() | rpl::start_with_next([=](QString &&text) {
|
||||
bubble->setSubtext(std::move(text));
|
||||
}, bubble->lifetime());
|
||||
|
||||
PaidReactionSlider(
|
||||
sliderWrap,
|
||||
st::paidReactSlider,
|
||||
values.min,
|
||||
values.explicitlyAllowed,
|
||||
state->chosen.value(),
|
||||
values.max,
|
||||
[=](int count) { state->chosen = count; },
|
||||
activeFgOverride);
|
||||
|
||||
sliderWrap->resizeToWidth(st::boxWideWidth);
|
||||
}, sliderWrap->lifetime());
|
||||
|
||||
box->addTopButton(
|
||||
st::boxTitleClose,
|
||||
[=] { box->closeBox(); });
|
||||
if (const auto now = state->value.current(); !now.finished()) {
|
||||
box->addTopButton(
|
||||
st::boxTitleMenu,
|
||||
MakeAuctionMenuCallback(box, show, now));
|
||||
}
|
||||
|
||||
const auto skip = st::paidReactTitleSkip;
|
||||
box->addRow(
|
||||
@@ -572,11 +740,34 @@ void AuctionBidBox(not_null<GenericBox*> box, AuctionBidBoxArgs &&args) {
|
||||
box,
|
||||
tr::lng_auction_bid_title(),
|
||||
st::boostCenteredTitle),
|
||||
st::boxRowPadding + QMargins(0, skip, 0, 0),
|
||||
st::boxRowPadding + QMargins(0, skip / 2, 0, 0),
|
||||
style::al_top);
|
||||
|
||||
auto subtitle = tr::lng_auction_bid_subtitle(
|
||||
lt_count,
|
||||
state->value.value(
|
||||
) | rpl::map([=](const Data::GiftAuctionState &state) {
|
||||
return state.gift->auctionGiftsPerRound * 1.;
|
||||
}));
|
||||
box->addRow(
|
||||
MakeAuctionInfoBlocks(box, &show->session(), state->value.value()),
|
||||
object_ptr<FlatLabel>(
|
||||
box,
|
||||
std::move(subtitle),
|
||||
st::auctionCenteredSubtitle),
|
||||
style::al_top);
|
||||
|
||||
const auto setMinimal = [=] {
|
||||
const auto &now = state->value.current();
|
||||
state->chosen = int(now.my.minBidAmount
|
||||
? now.my.minBidAmount
|
||||
: now.minBidAmount);
|
||||
};
|
||||
box->addRow(
|
||||
MakeAuctionInfoBlocks(
|
||||
box,
|
||||
&show->session(),
|
||||
state->value.value(),
|
||||
setMinimal),
|
||||
st::boxRowPadding + QMargins(0, skip / 2, 0, skip));
|
||||
|
||||
AddBidPlaces(box, show, state->value.value(), state->chosen.value());
|
||||
@@ -584,8 +775,40 @@ void AuctionBidBox(not_null<GenericBox*> box, AuctionBidBoxArgs &&args) {
|
||||
AddSkip(content);
|
||||
AddSkip(content);
|
||||
|
||||
const auto peer = args.peer;
|
||||
const auto button = box->addButton(rpl::single(QString()), [=] {
|
||||
save(state->chosen.current());
|
||||
const auto ¤t = state->value.current();
|
||||
const auto amount = state->chosen.current();
|
||||
if (amount <= current.my.bid) {
|
||||
box->closeBox();
|
||||
return;
|
||||
} else if (state->placing) {
|
||||
return;
|
||||
}
|
||||
state->placing = true;
|
||||
const auto was = (current.my.bid > 0);
|
||||
const auto perRound = current.gift->auctionGiftsPerRound;
|
||||
const auto done = [=](Payments::CheckoutResult result) {
|
||||
state->placing = false;
|
||||
if (result == Payments::CheckoutResult::Paid) {
|
||||
show->showToast({
|
||||
.title = (was
|
||||
? tr::lng_auction_bid_increased_title
|
||||
: tr::lng_auction_bid_placed_title)(
|
||||
tr::now),
|
||||
.text = tr::lng_auction_bid_done_text(
|
||||
tr::now,
|
||||
lt_count,
|
||||
perRound,
|
||||
tr::rich),
|
||||
.duration = kBidPlacedToastDuration,
|
||||
});
|
||||
}
|
||||
};
|
||||
auto owned = details
|
||||
? std::make_unique<GiftSendDetails>(*details)
|
||||
: nullptr;
|
||||
PlaceAuctionBid(show, peer, amount, current, std::move(owned), done);
|
||||
});
|
||||
|
||||
button->setText(rpl::combine(
|
||||
@@ -607,6 +830,7 @@ void AuctionBidBox(not_null<GenericBox*> box, AuctionBidBoxArgs &&args) {
|
||||
tr::marked);
|
||||
}) | rpl::flatten_latest());
|
||||
|
||||
show->session().credits().load(true);
|
||||
AddStarSelectBalance(
|
||||
box,
|
||||
&show->session(),
|
||||
@@ -738,97 +962,6 @@ void AuctionBidBox(not_null<GenericBox*> box, AuctionBidBoxArgs &&args) {
|
||||
return result;
|
||||
}
|
||||
|
||||
void AuctionAboutBox(
|
||||
not_null<GenericBox*> box,
|
||||
int rounds,
|
||||
int giftsPerRound,
|
||||
Fn<void(Fn<void()> close)> understood) {
|
||||
box->setStyle(st::confcallJoinBox);
|
||||
box->setWidth(st::boxWideWidth);
|
||||
box->setNoContentMargin(true);
|
||||
box->addTopButton(st::boxTitleClose, [=] {
|
||||
box->closeBox();
|
||||
});
|
||||
box->addRow(
|
||||
Calls::Group::MakeRoundActiveLogo(
|
||||
box,
|
||||
st::auctionAboutLogo,
|
||||
st::auctionAboutLogoPadding),
|
||||
st::boxRowPadding + st::confcallLinkHeaderIconPadding);
|
||||
|
||||
box->addRow(
|
||||
object_ptr<FlatLabel>(
|
||||
box,
|
||||
tr::lng_auction_about_title(),
|
||||
st::boxTitle),
|
||||
st::boxRowPadding + st::confcallLinkTitlePadding,
|
||||
style::al_top);
|
||||
box->addRow(
|
||||
object_ptr<FlatLabel>(
|
||||
box,
|
||||
tr::lng_auction_about_subtitle(tr::rich),
|
||||
st::confcallLinkCenteredText),
|
||||
st::boxRowPadding,
|
||||
style::al_top
|
||||
)->setTryMakeSimilarLines(true);
|
||||
|
||||
const auto features = std::vector<FeatureListEntry>{
|
||||
{
|
||||
st::menuIconAuctionDrop,
|
||||
tr::lng_auction_about_top_title(
|
||||
tr::now,
|
||||
lt_count,
|
||||
giftsPerRound),
|
||||
tr::lng_auction_about_top_about(
|
||||
tr::now,
|
||||
lt_count,
|
||||
giftsPerRound,
|
||||
lt_rounds,
|
||||
tr::lng_auction_about_top_rounds(
|
||||
tr::now,
|
||||
lt_count,
|
||||
rounds,
|
||||
tr::rich),
|
||||
lt_bidders,
|
||||
tr::lng_auction_about_top_bidders(
|
||||
tr::now,
|
||||
lt_count,
|
||||
giftsPerRound,
|
||||
tr::rich),
|
||||
tr::rich),
|
||||
},
|
||||
{
|
||||
st::menuIconStarsCarryover,
|
||||
tr::lng_auction_about_bid_title(tr::now),
|
||||
tr::lng_auction_about_bid_about(
|
||||
tr::now,
|
||||
lt_count,
|
||||
giftsPerRound,
|
||||
tr::rich),
|
||||
},
|
||||
{
|
||||
st::menuIconStarsRefund,
|
||||
tr::lng_auction_about_missed_title(tr::now),
|
||||
tr::lng_auction_about_missed_about(tr::now, tr::rich),
|
||||
},
|
||||
};
|
||||
for (const auto &feature : features) {
|
||||
box->addRow(MakeFeatureListEntry(box, feature));
|
||||
}
|
||||
|
||||
const auto close = Fn<void()>([weak = base::make_weak(box)] {
|
||||
if (const auto strong = weak.get()) {
|
||||
strong->closeBox();
|
||||
}
|
||||
});
|
||||
box->addButton(
|
||||
rpl::single(QString()),
|
||||
understood ? [=] { understood(close); } : close
|
||||
)->setText(rpl::single(Text::IconEmoji(
|
||||
&st::infoStarsUnderstood
|
||||
).append(' ').append(tr::lng_auction_about_understood(tr::now))));
|
||||
}
|
||||
|
||||
void AuctionGotGiftsBox(
|
||||
not_null<GenericBox*> box,
|
||||
std::shared_ptr<ChatHelpers::Show> show,
|
||||
@@ -841,6 +974,8 @@ void AuctionGotGiftsBox(
|
||||
tr::lng_auction_bought_title(lt_count, rpl::single(count * 1.)));
|
||||
box->setWidth(st::boxWideWidth);
|
||||
box->setMaxHeight(st::boxWideWidth * 2);
|
||||
box->addButton(tr::lng_box_ok(), [=] { box->closeBox(); });
|
||||
box->addTopButton(st::boxTitleClose, [=] { box->closeBox(); });
|
||||
|
||||
auto helper = Text::CustomEmojiHelper(Core::TextContext({
|
||||
.session = &show->session(),
|
||||
@@ -931,16 +1066,19 @@ void AuctionInfoBox(
|
||||
|
||||
std::vector<Data::GiftAcquired> acquired;
|
||||
bool acquiredRequested = false;
|
||||
|
||||
base::unique_qptr<PopupMenu> menu;
|
||||
};
|
||||
const auto show = window->uiShow();
|
||||
const auto state = box->lifetime().make_state<State>(&show->session());
|
||||
state->value = std::move(value);
|
||||
state->minutesLeft = MinutesLeftTillValue(
|
||||
state->value.current().endDate);
|
||||
const auto &now = state->value.current();
|
||||
|
||||
state->minutesLeft = MinutesLeftTillValue(now.endDate);
|
||||
|
||||
box->setStyle(st::giftBox);
|
||||
|
||||
const auto name = state->value.current().gift->resellTitle;
|
||||
const auto name = now.gift->resellTitle;
|
||||
const auto extend = st::defaultDropdownMenu.wrap.shadow.extend;
|
||||
const auto side = st::giftBoxGiftSmall;
|
||||
const auto size = QSize(side, side).grownBy(extend);
|
||||
@@ -950,7 +1088,7 @@ void AuctionInfoBox(
|
||||
const auto gift = CreateChild<GiftButton>(preview, &state->delegate);
|
||||
gift->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
gift->setDescriptor(GiftTypeStars{
|
||||
.info = *state->value.current().gift,
|
||||
.info = *now.gift,
|
||||
}, GiftButtonMode::Minimal);
|
||||
|
||||
preview->widthValue() | rpl::start_with_next([=](int width) {
|
||||
@@ -1085,25 +1223,39 @@ void AuctionInfoBox(
|
||||
AuctionButtonCountdownType::Join,
|
||||
state->value.value());
|
||||
|
||||
box->setNoContentMargin(true);
|
||||
const auto close = CreateChild<IconButton>(
|
||||
box->verticalLayout(),
|
||||
st::boxTitleClose);
|
||||
close->setClickedCallback([=] { box->closeBox(); });
|
||||
|
||||
const auto menu = CreateChild<IconButton>(
|
||||
box->verticalLayout(),
|
||||
st::boxTitleMenu);
|
||||
menu->setClickedCallback(MakeAuctionMenuCallback(menu, show, now));
|
||||
const auto weakMenu = base::make_weak(menu);
|
||||
|
||||
box->verticalLayout()->widthValue() | rpl::start_with_next([=](int) {
|
||||
close->moveToRight(0, 0);
|
||||
if (const auto strong = weakMenu.get()) {
|
||||
strong->moveToRight(close->width(), 0);
|
||||
}
|
||||
}, close->lifetime());
|
||||
|
||||
rpl::combine(
|
||||
state->value.value(),
|
||||
state->minutesLeft.value()
|
||||
) | rpl::start_with_next([=](
|
||||
const Data::GiftAuctionState &state,
|
||||
int minutes) {
|
||||
about->setTextColorOverride((state.finished() || minutes <= 0)
|
||||
const auto finished = state.finished() || (minutes <= 0);
|
||||
about->setTextColorOverride(finished
|
||||
? st::attentionButtonFg->c
|
||||
: std::optional<QColor>());
|
||||
if (const auto strong = finished ? weakMenu.get() : nullptr) {
|
||||
delete strong;
|
||||
}
|
||||
}, box->lifetime());
|
||||
|
||||
box->setNoContentMargin(true);
|
||||
const auto close = CreateChild<IconButton>(
|
||||
box->verticalLayout(),
|
||||
st::boxTitleClose);
|
||||
close->setClickedCallback([=] { box->closeBox(); });
|
||||
box->verticalLayout()->widthValue() | rpl::start_with_next([=](int) {
|
||||
close->moveToRight(0, 0);
|
||||
}, close->lifetime());
|
||||
}
|
||||
|
||||
base::weak_qptr<BoxContent> ChooseAndShowAuctionBox(
|
||||
@@ -1143,12 +1295,13 @@ base::weak_qptr<BoxContent> ChooseAndShowAuctionBox(
|
||||
sendBox->boxClosing(
|
||||
) | rpl::start_with_next(close, sendBox->lifetime());
|
||||
};
|
||||
const auto text = (now.my.to->isSelf()
|
||||
const auto from = now.my.to;
|
||||
const auto text = (from->isSelf()
|
||||
? tr::lng_auction_change_already_me(tr::now, tr::rich)
|
||||
: tr::lng_auction_change_already(
|
||||
tr::now,
|
||||
lt_name,
|
||||
tr::bold(now.my.to->name()),
|
||||
tr::bold(from->name()),
|
||||
tr::rich)).append(' ').append(peer->isSelf()
|
||||
? tr::lng_auction_change_to_me(tr::now, tr::rich)
|
||||
: tr::lng_auction_change_to(
|
||||
@@ -1156,11 +1309,22 @@ base::weak_qptr<BoxContent> ChooseAndShowAuctionBox(
|
||||
lt_name,
|
||||
tr::bold(peer->name()),
|
||||
tr::rich));
|
||||
box = window->show(MakeConfirmBox({
|
||||
.text = text,
|
||||
.confirmed = change,
|
||||
.confirmText = tr::lng_auction_change_button(),
|
||||
.title = tr::lng_auction_change_title(),
|
||||
box = window->show(Box([=](not_null<GenericBox*> box) {
|
||||
box->addRow(
|
||||
CreateUserpicsTransfer(
|
||||
box,
|
||||
rpl::single(std::vector{ not_null<PeerData*>(from) }),
|
||||
peer,
|
||||
UserpicsTransferType::AuctionRecipient),
|
||||
st::boxRowPadding + st::auctionChangeRecipientPadding
|
||||
)->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
|
||||
ConfirmBox(box, {
|
||||
.text = text,
|
||||
.confirmed = change,
|
||||
.confirmText = tr::lng_auction_change_button(),
|
||||
.title = tr::lng_auction_change_title(),
|
||||
});
|
||||
}));
|
||||
} else if (showInfoBox) {
|
||||
box = window->show(Box(
|
||||
@@ -1306,4 +1470,95 @@ void SetAuctionButtonCountdownText(
|
||||
st::resaleButtonSubtitle);
|
||||
}
|
||||
|
||||
void AuctionAboutBox(
|
||||
not_null<GenericBox*> box,
|
||||
int rounds,
|
||||
int giftsPerRound,
|
||||
Fn<void(Fn<void()> close)> understood) {
|
||||
box->setStyle(st::confcallJoinBox);
|
||||
box->setWidth(st::boxWideWidth);
|
||||
box->setNoContentMargin(true);
|
||||
box->addTopButton(st::boxTitleClose, [=] {
|
||||
box->closeBox();
|
||||
});
|
||||
box->addRow(
|
||||
Calls::Group::MakeRoundActiveLogo(
|
||||
box,
|
||||
st::auctionAboutLogo,
|
||||
st::auctionAboutLogoPadding),
|
||||
st::boxRowPadding + st::confcallLinkHeaderIconPadding);
|
||||
|
||||
box->addRow(
|
||||
object_ptr<FlatLabel>(
|
||||
box,
|
||||
tr::lng_auction_about_title(),
|
||||
st::boxTitle),
|
||||
st::boxRowPadding + st::confcallLinkTitlePadding,
|
||||
style::al_top);
|
||||
box->addRow(
|
||||
object_ptr<FlatLabel>(
|
||||
box,
|
||||
tr::lng_auction_about_subtitle(tr::rich),
|
||||
st::confcallLinkCenteredText),
|
||||
st::boxRowPadding,
|
||||
style::al_top
|
||||
)->setTryMakeSimilarLines(true);
|
||||
|
||||
const auto features = std::vector<FeatureListEntry>{
|
||||
{
|
||||
st::menuIconAuctionDrop,
|
||||
tr::lng_auction_about_top_title(
|
||||
tr::now,
|
||||
lt_count,
|
||||
giftsPerRound),
|
||||
tr::lng_auction_about_top_about(
|
||||
tr::now,
|
||||
lt_count,
|
||||
giftsPerRound,
|
||||
lt_rounds,
|
||||
tr::lng_auction_about_top_rounds(
|
||||
tr::now,
|
||||
lt_count,
|
||||
rounds,
|
||||
tr::rich),
|
||||
lt_bidders,
|
||||
tr::lng_auction_about_top_bidders(
|
||||
tr::now,
|
||||
lt_count,
|
||||
giftsPerRound,
|
||||
tr::rich),
|
||||
tr::rich),
|
||||
},
|
||||
{
|
||||
st::menuIconStarsCarryover,
|
||||
tr::lng_auction_about_bid_title(tr::now),
|
||||
tr::lng_auction_about_bid_about(
|
||||
tr::now,
|
||||
lt_count,
|
||||
giftsPerRound,
|
||||
tr::rich),
|
||||
},
|
||||
{
|
||||
st::menuIconStarsRefund,
|
||||
tr::lng_auction_about_missed_title(tr::now),
|
||||
tr::lng_auction_about_missed_about(tr::now, tr::rich),
|
||||
},
|
||||
};
|
||||
for (const auto &feature : features) {
|
||||
box->addRow(MakeFeatureListEntry(box, feature));
|
||||
}
|
||||
|
||||
const auto close = Fn<void()>([weak = base::make_weak(box)] {
|
||||
if (const auto strong = weak.get()) {
|
||||
strong->closeBox();
|
||||
}
|
||||
});
|
||||
box->addButton(
|
||||
rpl::single(QString()),
|
||||
understood ? [=] { understood(close); } : close
|
||||
)->setText(rpl::single(Text::IconEmoji(
|
||||
&st::infoStarsUnderstood
|
||||
).append(' ').append(tr::lng_auction_about_understood(tr::now))));
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
|
||||
@@ -27,6 +27,7 @@ namespace Ui {
|
||||
|
||||
class BoxContent;
|
||||
class RoundButton;
|
||||
class GenericBox;
|
||||
|
||||
[[nodiscard]] rpl::lifetime ShowStarGiftAuction(
|
||||
not_null<Window::SessionController*> controller,
|
||||
@@ -53,4 +54,10 @@ 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);
|
||||
|
||||
} // 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 = 6003002;
|
||||
constexpr auto AppVersionStr = "6.3.2";
|
||||
constexpr auto AppBetaVersion = false;
|
||||
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;
|
||||
|
||||
@@ -51,12 +51,14 @@ rpl::producer<GiftAuctionState> GiftAuctions::state(const QString &slug) {
|
||||
void GiftAuctions::apply(const MTPDupdateStarGiftAuctionState &data) {
|
||||
if (const auto entry = find(data.vgift_id().v)) {
|
||||
apply(entry, data.vstate());
|
||||
entry->changes.fire({});
|
||||
}
|
||||
}
|
||||
|
||||
void GiftAuctions::apply(const MTPDupdateStarGiftAuctionUserState &data) {
|
||||
if (const auto entry = find(data.vgift_id().v)) {
|
||||
apply(entry, data.vuser_state());
|
||||
entry->changes.fire({});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -18,6 +18,7 @@ extern const char kAlternativeScrollProcessing[];
|
||||
struct FlexibleScrollData {
|
||||
rpl::event_stream<int> contentHeightValue;
|
||||
rpl::event_stream<int> fillerWidthValue;
|
||||
rpl::event_stream<> backButtonEnables;
|
||||
};
|
||||
|
||||
class FlexibleScrollHelper final {
|
||||
@@ -34,6 +35,8 @@ private:
|
||||
void setupScrollAnimation();
|
||||
void setupScrollHandling();
|
||||
void setupScrollHandlingWithFilter();
|
||||
void scrollToY(int value);
|
||||
void applyScrollToPinnedLayout(int scrollCurrent);
|
||||
|
||||
const not_null<Ui::ScrollArea*> _scroll;
|
||||
const not_null<Ui::RpWidget*> _inner;
|
||||
|
||||
@@ -946,7 +946,11 @@ void WrapWidget::showNewContent(
|
||||
void WrapWidget::showNewContent(not_null<ContentMemento*> memento) {
|
||||
// Validates contentGeometry().
|
||||
setupTop();
|
||||
showContent(createContent(memento, _controller.get()));
|
||||
auto newContent = createContent(memento, _controller.get());
|
||||
if (!_topBar && hasBackButton()) {
|
||||
newContent->enableBackButton();
|
||||
}
|
||||
showContent(std::move(newContent));
|
||||
}
|
||||
|
||||
void WrapWidget::resizeEvent(QResizeEvent *e) {
|
||||
|
||||
@@ -55,6 +55,7 @@ struct ListContext {
|
||||
not_null<ListSelectedMap*> selected;
|
||||
not_null<ListSelectedMap*> dragSelected;
|
||||
ListDragSelectAction dragSelectAction = ListDragSelectAction::None;
|
||||
BaseLayout *draggedItem = nullptr;
|
||||
};
|
||||
|
||||
struct ListScrollTopState {
|
||||
@@ -69,6 +70,11 @@ struct ListFoundItem {
|
||||
bool exact = false;
|
||||
};
|
||||
|
||||
struct ListFoundItemWithSection {
|
||||
ListFoundItem item;
|
||||
not_null<const ListSection*> section;
|
||||
};
|
||||
|
||||
struct CachedItem {
|
||||
CachedItem(std::unique_ptr<BaseLayout> item) : item(std::move(item)) {
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
|
||||
#include "storage/storage_shared_media.h"
|
||||
#include "layout/layout_selection.h"
|
||||
#include "ui/rect.h"
|
||||
#include "ui/painter.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
#include "styles/style_info.h"
|
||||
@@ -45,6 +46,10 @@ int ListSection::top() const {
|
||||
return _top;
|
||||
}
|
||||
|
||||
void ListSection::setCanReorder(bool value) {
|
||||
_canReorder = value;
|
||||
}
|
||||
|
||||
int ListSection::height() const {
|
||||
return _height;
|
||||
}
|
||||
@@ -53,6 +58,10 @@ int ListSection::bottom() const {
|
||||
return top() + height();
|
||||
}
|
||||
|
||||
bool ListSection::isOneColumn() const {
|
||||
return _itemsInRow == 1;
|
||||
}
|
||||
|
||||
bool ListSection::addItem(not_null<BaseLayout*> item) {
|
||||
if (_items.empty() || belongsHere(item)) {
|
||||
if (_items.empty()) {
|
||||
@@ -103,15 +112,20 @@ bool ListSection::removeItem(not_null<const HistoryItem*> item) {
|
||||
return false;
|
||||
}
|
||||
|
||||
void ListSection::reorderItems(int oldPosition, int newPosition) {
|
||||
base::reorder(_items, oldPosition, newPosition);
|
||||
refreshHeight();
|
||||
}
|
||||
|
||||
QRect ListSection::findItemRect(
|
||||
not_null<const BaseLayout*> item) const {
|
||||
auto position = item->position();
|
||||
const auto position = item->position();
|
||||
if (!_mosaic.empty()) {
|
||||
return _mosaic.findRect(position);
|
||||
}
|
||||
auto top = position / _itemsInRow;
|
||||
auto indexInRow = position % _itemsInRow;
|
||||
auto left = _itemsLeft
|
||||
const auto top = position / _itemsInRow;
|
||||
const auto indexInRow = position % _itemsInRow;
|
||||
const auto left = _itemsLeft
|
||||
+ indexInRow * (_itemWidth + st::infoMediaSkip);
|
||||
return QRect(left, top, _itemWidth, item->height());
|
||||
}
|
||||
@@ -222,7 +236,7 @@ void ListSection::paint(
|
||||
const ListContext &context,
|
||||
QRect clip,
|
||||
int outerWidth) const {
|
||||
auto header = headerHeight();
|
||||
const auto header = headerHeight();
|
||||
if (QRect(0, 0, outerWidth, header).intersects(clip)) {
|
||||
p.setPen(st::infoMediaHeaderFg);
|
||||
_header.drawLeftElided(
|
||||
@@ -234,7 +248,7 @@ void ListSection::paint(
|
||||
}
|
||||
auto localContext = context.layoutContext;
|
||||
if (!_mosaic.empty()) {
|
||||
auto paintItem = [&](not_null<BaseLayout*> item, QPoint point) {
|
||||
const auto paintItem = [&](not_null<BaseLayout*> item, QPoint point) {
|
||||
p.translate(point.x(), point.y());
|
||||
item->paint(
|
||||
p,
|
||||
@@ -247,13 +261,17 @@ void ListSection::paint(
|
||||
return;
|
||||
}
|
||||
|
||||
auto fromIt = findItemAfterTop(clip.y());
|
||||
auto tillIt = findItemAfterBottom(
|
||||
const auto fromIt = findItemAfterTop(clip.y());
|
||||
const auto tillIt = findItemAfterBottom(
|
||||
fromIt,
|
||||
clip.y() + clip.height());
|
||||
for (auto it = fromIt; it != tillIt; ++it) {
|
||||
auto item = *it;
|
||||
const auto item = *it;
|
||||
if (item == context.draggedItem) {
|
||||
continue;
|
||||
}
|
||||
auto rect = findItemRect(item);
|
||||
rect.translate(item->shift());
|
||||
localContext.skipBorder = (rect.y() <= header + _itemsTop);
|
||||
if (rect.intersects(clip)) {
|
||||
p.translate(rect.topLeft());
|
||||
@@ -263,6 +281,15 @@ void ListSection::paint(
|
||||
itemSelection(item, context),
|
||||
&localContext);
|
||||
p.translate(-rect.topLeft());
|
||||
|
||||
if (_canReorder && isOneColumn()) {
|
||||
st::stickersReorderIcon.paint(
|
||||
p,
|
||||
rect::right(rect) - oneColumnRightPadding(),
|
||||
(rect.height() - st::stickersReorderIcon.height()) / 2
|
||||
+ rect.y(),
|
||||
outerWidth);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -302,16 +329,16 @@ TextSelection ListSection::itemSelection(
|
||||
not_null<const BaseLayout*> item,
|
||||
const ListContext &context) const {
|
||||
const auto parent = item->getItem();
|
||||
auto dragSelectAction = context.dragSelectAction;
|
||||
const auto dragSelectAction = context.dragSelectAction;
|
||||
if (dragSelectAction != ListDragSelectAction::None) {
|
||||
auto i = context.dragSelected->find(parent);
|
||||
const auto i = context.dragSelected->find(parent);
|
||||
if (i != context.dragSelected->end()) {
|
||||
return (dragSelectAction == ListDragSelectAction::Selecting)
|
||||
? FullSelection
|
||||
: TextSelection();
|
||||
}
|
||||
}
|
||||
auto i = context.selected->find(parent);
|
||||
const auto i = context.selected->find(parent);
|
||||
return (i == context.selected->cend())
|
||||
? TextSelection()
|
||||
: i->second.text;
|
||||
@@ -321,19 +348,28 @@ int ListSection::headerHeight() const {
|
||||
return _header.isEmpty() ? 0 : st::infoMediaHeaderHeight;
|
||||
}
|
||||
|
||||
int ListSection::oneColumnRightPadding() const {
|
||||
return !isOneColumn()
|
||||
? 0
|
||||
: _canReorder
|
||||
? st::stickersReorderIcon.width() + st::infoMediaLeft
|
||||
: 0;
|
||||
}
|
||||
|
||||
void ListSection::resizeToWidth(int newWidth) {
|
||||
auto minWidth = st::infoMediaMinGridSize + st::infoMediaSkip * 2;
|
||||
const auto minWidth = st::infoMediaMinGridSize + st::infoMediaSkip * 2;
|
||||
if (newWidth < minWidth) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto resizeOneColumn = [&](int itemsLeft, int itemWidth) {
|
||||
const auto resizeOneColumn = [&](int itemsLeft, int itemWidth) {
|
||||
const auto rightPadding = oneColumnRightPadding();
|
||||
_itemsLeft = itemsLeft;
|
||||
_itemsTop = 0;
|
||||
_itemsInRow = 1;
|
||||
_itemWidth = itemWidth;
|
||||
_itemWidth = itemWidth - rightPadding;
|
||||
for (auto &item : _items) {
|
||||
item->resizeGetHeight(_itemWidth);
|
||||
item->resizeGetHeight(_itemWidth - rightPadding);
|
||||
}
|
||||
};
|
||||
switch (_type) {
|
||||
@@ -365,8 +401,8 @@ void ListSection::resizeToWidth(int newWidth) {
|
||||
break;
|
||||
case Type::File:
|
||||
case Type::Link: {
|
||||
auto itemsLeft = st::infoMediaHeaderPosition.x();
|
||||
auto itemWidth = newWidth - 2 * itemsLeft;
|
||||
const auto itemsLeft = st::infoMediaHeaderPosition.x();
|
||||
const auto itemWidth = newWidth - 2 * itemsLeft;
|
||||
resizeOneColumn(itemsLeft, itemWidth);
|
||||
} break;
|
||||
}
|
||||
@@ -382,7 +418,7 @@ int ListSection::recountHeight() {
|
||||
case Type::Video:
|
||||
case Type::PhotoVideo:
|
||||
case Type::RoundFile: {
|
||||
auto itemHeight = _itemHeight + st::infoMediaSkip;
|
||||
const auto itemHeight = _itemHeight + st::infoMediaSkip;
|
||||
auto index = 0;
|
||||
result += _itemsTop;
|
||||
for (auto &item : _items) {
|
||||
|
||||
@@ -26,12 +26,16 @@ public:
|
||||
|
||||
void setTop(int top);
|
||||
[[nodiscard]] int top() const;
|
||||
void setCanReorder(bool);
|
||||
void resizeToWidth(int newWidth);
|
||||
[[nodiscard]] int height() const;
|
||||
|
||||
[[nodiscard]] int bottom() const;
|
||||
[[nodiscard]] bool isOneColumn() const;
|
||||
[[nodiscard]] int oneColumnRightPadding() const;
|
||||
|
||||
bool removeItem(not_null<const HistoryItem*> item);
|
||||
void reorderItems(int oldPosition, int newPosition);
|
||||
[[nodiscard]] std::optional<ListFoundItem> findItemByItem(
|
||||
not_null<const HistoryItem*> item) const;
|
||||
[[nodiscard]] ListFoundItem findItemDetails(
|
||||
@@ -87,6 +91,7 @@ private:
|
||||
mutable int _rowsCount = 0;
|
||||
int _top = 0;
|
||||
int _height = 0;
|
||||
bool _canReorder = false;
|
||||
|
||||
Mosaic::Layout::MosaicLayout<BaseLayout> _mosaic;
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "ui/chat/chat_style.h"
|
||||
#include "ui/cached_round_corners.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/rect.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "ui/inactive_press.h"
|
||||
#include "lang/lang_keys.h"
|
||||
@@ -72,6 +73,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "styles/style_menu_icons.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_credits.h" // giftBoxHiddenMark
|
||||
#include "styles/style_chat_helpers.h"
|
||||
|
||||
#include <QtWidgets/QApplication>
|
||||
#include <QtGui/QClipboard>
|
||||
@@ -302,7 +304,8 @@ void ListWidget::setupStoriesTrackIds() {
|
||||
}
|
||||
}
|
||||
if (_storiesInAlbum.size() > ids.size()) {
|
||||
for (auto i = begin(_storiesInAlbum); i != end(_storiesInAlbum);) {
|
||||
const auto endIt = end(_storiesInAlbum);
|
||||
for (auto i = begin(_storiesInAlbum); i != endIt;) {
|
||||
if (ids.contains(*i)) {
|
||||
++i;
|
||||
} else {
|
||||
@@ -365,6 +368,13 @@ void ListWidget::selectionAction(SelectionAction action) {
|
||||
}
|
||||
}
|
||||
|
||||
void ListWidget::setReorderDescriptor(ReorderDescriptor descriptor) {
|
||||
_reorderDescriptor = std::move(descriptor);
|
||||
if (!_reorderDescriptor.save) {
|
||||
cancelReorder();
|
||||
}
|
||||
}
|
||||
|
||||
QRect ListWidget::getCurrentSongGeometry() {
|
||||
const auto type = AudioMsgId::Type::Song;
|
||||
const auto current = ::Media::Player::instance()->current(type);
|
||||
@@ -387,6 +397,8 @@ void ListWidget::restart() {
|
||||
_heavyLayouts.clear();
|
||||
|
||||
_provider->restart();
|
||||
|
||||
_reorderState = {};
|
||||
}
|
||||
|
||||
void ListWidget::itemRemoved(not_null<const HistoryItem*> item) {
|
||||
@@ -398,8 +410,12 @@ void ListWidget::itemRemoved(not_null<const HistoryItem*> item) {
|
||||
_contextItem = nullptr;
|
||||
}
|
||||
|
||||
if (_reorderState.item && _reorderState.item->getItem() == item) {
|
||||
_reorderState = {};
|
||||
}
|
||||
|
||||
auto needHeightRefresh = false;
|
||||
auto sectionIt = findSectionByItem(item);
|
||||
const auto sectionIt = findSectionByItem(item);
|
||||
if (sectionIt != _sections.end()) {
|
||||
if (sectionIt->removeItem(item)) {
|
||||
if (sectionIt->empty()) {
|
||||
@@ -433,7 +449,7 @@ void ListWidget::itemRemoved(not_null<const HistoryItem*> item) {
|
||||
}
|
||||
|
||||
auto ListWidget::collectSelectedItems() const -> SelectedItems {
|
||||
auto convert = [&](
|
||||
const auto convert = [&](
|
||||
not_null<const HistoryItem*> item,
|
||||
const SelectionData &selection) {
|
||||
auto result = SelectedItem(item->globalId());
|
||||
@@ -444,7 +460,7 @@ auto ListWidget::collectSelectedItems() const -> SelectedItems {
|
||||
result.storyInProfile = selection.storyInProfile;
|
||||
return result;
|
||||
};
|
||||
auto transformation = [&](const auto &item) {
|
||||
const auto transformation = [&](const auto &item) {
|
||||
return convert(item.first, item.second);
|
||||
};
|
||||
auto items = SelectedItems(_provider->type());
|
||||
@@ -665,6 +681,7 @@ void ListWidget::markStoryMsgsSelected() {
|
||||
void ListWidget::refreshRows() {
|
||||
saveScrollState();
|
||||
|
||||
_reorderState = {};
|
||||
_sections.clear();
|
||||
_sections = _provider->fillSections(this);
|
||||
|
||||
@@ -706,23 +723,38 @@ void ListWidget::restoreState(not_null<Memento*> memento) {
|
||||
int ListWidget::resizeGetHeight(int newWidth) {
|
||||
if (newWidth > 0) {
|
||||
for (auto §ion : _sections) {
|
||||
section.setCanReorder(canReorder());
|
||||
section.resizeToWidth(newWidth);
|
||||
}
|
||||
}
|
||||
return recountHeight();
|
||||
}
|
||||
|
||||
auto ListWidget::findItemByPoint(QPoint point) const -> FoundItem {
|
||||
auto ListWidget::findSectionAndItem(QPoint point) const
|
||||
-> std::pair<std::vector<Section>::const_iterator, FoundItem> {
|
||||
Expects(!_sections.empty());
|
||||
|
||||
auto sectionIt = findSectionAfterTop(point.y());
|
||||
if (sectionIt == _sections.end()) {
|
||||
--sectionIt;
|
||||
}
|
||||
auto shift = QPoint(0, sectionIt->top());
|
||||
return foundItemInSection(
|
||||
sectionIt->findItemByPoint(point - shift),
|
||||
*sectionIt);
|
||||
const auto shift = QPoint(0, sectionIt->top());
|
||||
return {
|
||||
sectionIt,
|
||||
foundItemInSection(
|
||||
sectionIt->findItemByPoint(point - shift),
|
||||
*sectionIt)
|
||||
};
|
||||
}
|
||||
|
||||
auto ListWidget::findItemByPoint(QPoint point) const -> FoundItem {
|
||||
return findSectionAndItem(point).second;
|
||||
}
|
||||
|
||||
auto ListWidget::findItemByPointWithSection(QPoint point) const
|
||||
-> ListFoundItemWithSection {
|
||||
const auto [sectionIt, item] = findSectionAndItem(point);
|
||||
return { item, &(*sectionIt) };
|
||||
}
|
||||
|
||||
auto ListWidget::findItemByItem(const HistoryItem *item)
|
||||
@@ -730,7 +762,7 @@ auto ListWidget::findItemByItem(const HistoryItem *item)
|
||||
if (!item || !_provider->isPossiblyMyItem(item)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
auto sectionIt = findSectionByItem(item);
|
||||
const auto sectionIt = findSectionByItem(item);
|
||||
if (sectionIt != _sections.end()) {
|
||||
if (const auto found = sectionIt->findItemByItem(item)) {
|
||||
return foundItemInSection(*found, *sectionIt);
|
||||
@@ -820,7 +852,7 @@ void ListWidget::toggleScrollDateShown() {
|
||||
}
|
||||
|
||||
void ListWidget::checkMoveToOtherViewer() {
|
||||
auto visibleHeight = (_visibleBottom - _visibleTop);
|
||||
const auto visibleHeight = (_visibleBottom - _visibleTop);
|
||||
if (width() <= 0
|
||||
|| visibleHeight <= 0
|
||||
|| _sections.empty()
|
||||
@@ -828,12 +860,12 @@ void ListWidget::checkMoveToOtherViewer() {
|
||||
return;
|
||||
}
|
||||
|
||||
auto topItem = findItemByPoint({ st::infoMediaSkip, _visibleTop });
|
||||
auto bottomItem = findItemByPoint({ st::infoMediaSkip, _visibleBottom });
|
||||
const auto topItem = findItemByPoint({ st::infoMediaSkip, _visibleTop });
|
||||
const auto bottomItem = findItemByPoint({ st::infoMediaSkip, _visibleBottom });
|
||||
|
||||
auto preloadBefore = kPreloadIfLessThanScreens * visibleHeight;
|
||||
auto preloadTop = (_visibleTop < preloadBefore);
|
||||
auto preloadBottom = (height() - _visibleBottom < preloadBefore);
|
||||
const auto preloadBefore = kPreloadIfLessThanScreens * visibleHeight;
|
||||
const auto preloadTop = (_visibleTop < preloadBefore);
|
||||
const auto preloadBottom = (height() - _visibleBottom < preloadBefore);
|
||||
|
||||
_provider->checkPreload(
|
||||
{ width(), visibleHeight },
|
||||
@@ -904,8 +936,8 @@ void ListWidget::restoreScrollState() {
|
||||
if (!found) {
|
||||
return;
|
||||
}
|
||||
auto item = foundItemInSection(*found, *sectionIt);
|
||||
auto newVisibleTop = item.geometry.y() + _scrollTopState.shift;
|
||||
const auto item = foundItemInSection(*found, *sectionIt);
|
||||
const auto newVisibleTop = item.geometry.y() + _scrollTopState.shift;
|
||||
if (_visibleTop != newVisibleTop) {
|
||||
_scrollToRequests.fire_copy(newVisibleTop);
|
||||
}
|
||||
@@ -929,25 +961,29 @@ QMargins ListWidget::padding() const {
|
||||
void ListWidget::paintEvent(QPaintEvent *e) {
|
||||
Painter p(this);
|
||||
|
||||
auto outerWidth = width();
|
||||
auto clip = e->rect();
|
||||
auto ms = crl::now();
|
||||
auto fromSectionIt = findSectionAfterTop(clip.y());
|
||||
auto tillSectionIt = findSectionAfterBottom(
|
||||
const auto outerWidth = width();
|
||||
const auto clip = e->rect();
|
||||
const auto ms = crl::now();
|
||||
const auto fromSectionIt = findSectionAfterTop(clip.y());
|
||||
const auto tillSectionIt = findSectionAfterBottom(
|
||||
fromSectionIt,
|
||||
clip.y() + clip.height());
|
||||
const auto window = _controller->parentController();
|
||||
const auto paused = window->isGifPausedAtLeastFor(
|
||||
Window::GifPauseReason::Layer);
|
||||
const auto selecting = hasSelectedItems() || _storiesAddToAlbumId;
|
||||
const auto paintContext = Overview::Layout::PaintContext(ms, selecting, paused);
|
||||
auto context = ListContext{
|
||||
Overview::Layout::PaintContext(ms, selecting, paused),
|
||||
paintContext,
|
||||
&_selected,
|
||||
&_dragSelected,
|
||||
_dragSelectAction
|
||||
};
|
||||
if (_mouseAction == MouseAction::Reordering && _reorderState.item) {
|
||||
context.draggedItem = _reorderState.item;
|
||||
}
|
||||
for (auto it = fromSectionIt; it != tillSectionIt; ++it) {
|
||||
auto top = it->top();
|
||||
const auto top = it->top();
|
||||
p.translate(0, top);
|
||||
it->paint(p, context, clip.translated(0, -top), outerWidth);
|
||||
p.translate(0, -top);
|
||||
@@ -956,6 +992,33 @@ void ListWidget::paintEvent(QPaintEvent *e) {
|
||||
fromSectionIt->paintFloatingHeader(p, _visibleTop, outerWidth);
|
||||
}
|
||||
|
||||
if (_mouseAction == MouseAction::Reordering && _reorderState.item) {
|
||||
const auto o = ScopedPainterOpacity(p, 0.8);
|
||||
p.translate(_reorderState.currentPos);
|
||||
const auto isOneColumn = _reorderState.section
|
||||
&& _reorderState.section->isOneColumn();
|
||||
_reorderState.item->paint(
|
||||
p,
|
||||
QRect(
|
||||
0,
|
||||
0,
|
||||
_reorderState.item->maxWidth(),
|
||||
_reorderState.item->minHeight()),
|
||||
isOneColumn ? TextSelection{} : FullSelection,
|
||||
&context.layoutContext);
|
||||
|
||||
if (isOneColumn) {
|
||||
st::stickersReorderIcon.paint(
|
||||
p,
|
||||
width()
|
||||
- _reorderState.section->oneColumnRightPadding() * 2,
|
||||
(_reorderState.item->minHeight()
|
||||
- st::stickersReorderIcon.height()) / 2,
|
||||
outerWidth);
|
||||
}
|
||||
p.translate(-_reorderState.currentPos);
|
||||
}
|
||||
|
||||
if (_dateBadge->goodType && clip.intersects(_dateBadge->rect)) {
|
||||
const auto scrollDateOpacity
|
||||
= _dateBadge->opacity.value(_dateBadge->shown ? 1. : 0.);
|
||||
@@ -989,7 +1052,7 @@ void ListWidget::mousePressEvent(QMouseEvent *e) {
|
||||
}
|
||||
|
||||
void ListWidget::mouseMoveEvent(QMouseEvent *e) {
|
||||
auto buttonsPressed = (e->buttons() & (Qt::LeftButton | Qt::MiddleButton));
|
||||
const auto buttonsPressed = (e->buttons() & (Qt::LeftButton | Qt::MiddleButton));
|
||||
if (!buttonsPressed && _mouseAction != MouseAction::None) {
|
||||
mouseReleaseEvent(e);
|
||||
}
|
||||
@@ -1080,7 +1143,7 @@ void ListWidget::showContextMenu(
|
||||
});
|
||||
};
|
||||
|
||||
auto link = ClickHandler::getActive();
|
||||
const auto link = ClickHandler::getActive();
|
||||
|
||||
_contextMenu = base::make_unique_q<Ui::PopupMenu>(
|
||||
this,
|
||||
@@ -1119,7 +1182,7 @@ void ListWidget::showContextMenu(
|
||||
item,
|
||||
lnkDocument);
|
||||
if (!filepath.isEmpty()) {
|
||||
auto handler = base::fn_delayed(
|
||||
const auto handler = base::fn_delayed(
|
||||
st::defaultDropdownMenu.menu.ripple.hideDuration,
|
||||
this,
|
||||
[filepath] {
|
||||
@@ -1132,7 +1195,7 @@ void ListWidget::showContextMenu(
|
||||
std::move(handler),
|
||||
&st::menuIconShowInFolder);
|
||||
}
|
||||
auto handler = base::fn_delayed(
|
||||
const auto handler = base::fn_delayed(
|
||||
st::defaultDropdownMenu.menu.ripple.hideDuration,
|
||||
this,
|
||||
[=] {
|
||||
@@ -1343,7 +1406,7 @@ void ListWidget::forwardItems(MessageIdsList &&items) {
|
||||
{ id.peer, StoryIdFromMsgId(id.msg) }));
|
||||
}
|
||||
} else {
|
||||
auto callback = [weak = base::make_weak(this)] {
|
||||
const auto callback = [weak = base::make_weak(this)] {
|
||||
if (const auto strong = weak.get()) {
|
||||
strong->clearSelected();
|
||||
}
|
||||
@@ -1565,11 +1628,11 @@ void ListWidget::setActionBoxWeak(base::weak_qptr<Ui::BoxContent> box) {
|
||||
}
|
||||
|
||||
void ListWidget::trySwitchToWordSelection() {
|
||||
auto selectingSome = (_mouseAction == MouseAction::Selecting)
|
||||
const auto selectingSome = (_mouseAction == MouseAction::Selecting)
|
||||
&& hasSelectedText();
|
||||
auto willSelectSome = (_mouseAction == MouseAction::None)
|
||||
const auto willSelectSome = (_mouseAction == MouseAction::None)
|
||||
&& !hasSelectedItems();
|
||||
auto checkSwitchToWordSelection = _overLayout
|
||||
const auto checkSwitchToWordSelection = _overLayout
|
||||
&& (_mouseSelectType == TextSelectType::Letters)
|
||||
&& (selectingSome || willSelectSome);
|
||||
if (checkSwitchToWordSelection) {
|
||||
@@ -1591,7 +1654,7 @@ void ListWidget::switchToWordSelection() {
|
||||
if (_mouseAction == MouseAction::None) {
|
||||
_mouseAction = MouseAction::Selecting;
|
||||
clearSelected();
|
||||
auto selStatus = TextSelection {
|
||||
const auto selStatus = TextSelection {
|
||||
dragState.symbol,
|
||||
dragState.symbol
|
||||
};
|
||||
@@ -1618,7 +1681,7 @@ void ListWidget::applyItemSelection(
|
||||
}
|
||||
|
||||
void ListWidget::toggleItemSelection(not_null<HistoryItem*> item) {
|
||||
auto it = _selected.find(item);
|
||||
const auto it = _selected.find(item);
|
||||
if (it == _selected.cend()) {
|
||||
applyItemSelection(item, FullSelection);
|
||||
} else {
|
||||
@@ -1660,9 +1723,9 @@ bool ListWidget::isPressInSelectedText(TextState state) const {
|
||||
|| !isItemUnderPressSelected()) {
|
||||
return false;
|
||||
}
|
||||
auto pressedSelection = itemUnderPressSelection();
|
||||
auto from = pressedSelection->second.text.from;
|
||||
auto to = pressedSelection->second.text.to;
|
||||
const auto pressedSelection = itemUnderPressSelection();
|
||||
const auto from = pressedSelection->second.text.from;
|
||||
const auto to = pressedSelection->second.text.to;
|
||||
return (state.symbol >= from && state.symbol < to);
|
||||
}
|
||||
|
||||
@@ -1682,7 +1745,7 @@ void ListWidget::clearSelected() {
|
||||
|
||||
void ListWidget::validateTrippleClickStartTime() {
|
||||
if (_trippleClickStartTime) {
|
||||
auto elapsed = (crl::now() - _trippleClickStartTime);
|
||||
const auto elapsed = (crl::now() - _trippleClickStartTime);
|
||||
if (elapsed >= QApplication::doubleClickInterval()) {
|
||||
_trippleClickStartTime = 0;
|
||||
}
|
||||
@@ -1718,16 +1781,22 @@ QPoint ListWidget::clampMousePosition(QPoint position) const {
|
||||
}
|
||||
|
||||
void ListWidget::mouseActionUpdate(const QPoint &globalPosition) {
|
||||
if (_sections.empty() || _visibleBottom <= _visibleTop) {
|
||||
if (_sections.empty()
|
||||
|| _visibleBottom <= _visibleTop
|
||||
|| _returnAnimation.animating()) {
|
||||
return;
|
||||
}
|
||||
|
||||
_mousePosition = globalPosition;
|
||||
|
||||
auto local = mapFromGlobal(_mousePosition);
|
||||
auto point = clampMousePosition(local);
|
||||
auto [layout, geometry, inside] = findItemByPoint(point);
|
||||
auto state = MouseState{
|
||||
const auto local = mapFromGlobal(_mousePosition);
|
||||
const auto point = clampMousePosition(local);
|
||||
const auto [foundItem, section] = findItemByPointWithSection(point);
|
||||
const auto [layout, geometry, inside] = std::tie(
|
||||
foundItem.layout,
|
||||
foundItem.geometry,
|
||||
foundItem.exact);
|
||||
const auto state = MouseState{
|
||||
layout->getItem(),
|
||||
geometry.size(),
|
||||
point - geometry.topLeft(),
|
||||
@@ -1740,17 +1809,30 @@ void ListWidget::mouseActionUpdate(const QPoint &globalPosition) {
|
||||
}
|
||||
_overState = state;
|
||||
|
||||
TextState dragState;
|
||||
ClickHandlerHost *lnkhost = nullptr;
|
||||
const auto inDragArea = canReorder()
|
||||
&& section
|
||||
&& section->isOneColumn()
|
||||
&& point.y() >= geometry.y()
|
||||
&& point.y() < geometry.bottom()
|
||||
&& ((point.x() - geometry.x())
|
||||
>= (geometry.width()
|
||||
- section->oneColumnRightPadding()
|
||||
- st::stickersReorderSkip));
|
||||
if (_inDragArea != inDragArea) {
|
||||
_inDragArea = inDragArea;
|
||||
}
|
||||
|
||||
auto dragState = TextState();
|
||||
auto lnkhost = (ClickHandlerHost*)(nullptr);
|
||||
auto inTextSelection = _overState.inside
|
||||
&& (_overState.item == _pressState.item)
|
||||
&& hasSelectedText();
|
||||
if (_overLayout) {
|
||||
auto cursorDeltaLength = [&] {
|
||||
auto cursorDelta = (_overState.cursor - _pressState.cursor);
|
||||
const auto cursorDeltaLength = [&] {
|
||||
const auto cursorDelta = (_overState.cursor - _pressState.cursor);
|
||||
return cursorDelta.manhattanLength();
|
||||
};
|
||||
auto dragStartLength = [] {
|
||||
const auto dragStartLength = [] {
|
||||
return QApplication::startDragDistance();
|
||||
};
|
||||
if (_overState.item != _pressState.item
|
||||
@@ -1760,8 +1842,13 @@ void ListWidget::mouseActionUpdate(const QPoint &globalPosition) {
|
||||
InvokeQueued(this, [this] { performDrag(); });
|
||||
} else if (_mouseAction == MouseAction::PrepareSelect) {
|
||||
_mouseAction = MouseAction::Selecting;
|
||||
} else if (_mouseAction == MouseAction::PrepareReorder) {
|
||||
updateReorder(_mousePosition);
|
||||
}
|
||||
}
|
||||
if (_mouseAction == MouseAction::Reordering) {
|
||||
updateReorder(_mousePosition);
|
||||
}
|
||||
StateRequest request;
|
||||
if (_mouseAction == MouseAction::Selecting) {
|
||||
request.flags |= Ui::Text::StateRequest::Flag::LookupSymbol;
|
||||
@@ -1781,7 +1868,7 @@ void ListWidget::mouseActionUpdate(const QPoint &globalPosition) {
|
||||
|
||||
if (_mouseAction == MouseAction::None) {
|
||||
_mouseCursorState = dragState.cursor;
|
||||
auto cursor = computeMouseCursor();
|
||||
const auto cursor = computeMouseCursor();
|
||||
if (_cursor != cursor) {
|
||||
setCursor(_cursor = cursor);
|
||||
}
|
||||
@@ -1799,7 +1886,7 @@ void ListWidget::mouseActionUpdate(const QPoint &globalPosition) {
|
||||
selState = _overLayout->adjustSelection(selState, _mouseSelectType);
|
||||
}
|
||||
applyItemSelection(_overState.item, selState);
|
||||
auto hasSelection = (selState == FullSelection)
|
||||
const auto hasSelection = (selState == FullSelection)
|
||||
|| (selState.from != selState.to);
|
||||
if (!_wasSelectedText && hasSelection) {
|
||||
_wasSelectedText = true;
|
||||
@@ -1822,7 +1909,9 @@ void ListWidget::mouseActionUpdate(const QPoint &globalPosition) {
|
||||
}
|
||||
|
||||
style::cursor ListWidget::computeMouseCursor() const {
|
||||
if (ClickHandler::getPressed() || ClickHandler::getActive()) {
|
||||
if (_inDragArea && canReorder()) {
|
||||
return style::cur_sizeall;
|
||||
} else if (ClickHandler::getPressed() || ClickHandler::getActive()) {
|
||||
return style::cur_pointer;
|
||||
} else if (!hasSelectedItems()
|
||||
&& (_mouseCursorState == CursorState::Text)) {
|
||||
@@ -1834,7 +1923,7 @@ style::cursor ListWidget::computeMouseCursor() const {
|
||||
void ListWidget::updateDragSelection() {
|
||||
auto fromState = _pressState;
|
||||
auto tillState = _overState;
|
||||
auto swapStates = isAfter(fromState, tillState);
|
||||
const auto swapStates = isAfter(fromState, tillState);
|
||||
if (swapStates) {
|
||||
std::swap(fromState, tillState);
|
||||
}
|
||||
@@ -1854,7 +1943,7 @@ void ListWidget::updateDragSelection() {
|
||||
if (_dragSelected.empty()) {
|
||||
return DragSelectAction::None;
|
||||
}
|
||||
auto &[firstDragItem, data] = swapStates
|
||||
const auto &[firstDragItem, data] = swapStates
|
||||
? _dragSelected.front()
|
||||
: _dragSelected.back();
|
||||
if (isSelectedItem(_selected.find(firstDragItem))) {
|
||||
@@ -1896,7 +1985,7 @@ void ListWidget::mouseActionStart(
|
||||
_pressState = _overState;
|
||||
repaintItem(_overLayout);
|
||||
}
|
||||
auto pressLayout = _overLayout;
|
||||
const auto pressLayout = _overLayout;
|
||||
|
||||
_mouseAction = MouseAction::None;
|
||||
_pressWasInactive = Ui::WasInactivePress(
|
||||
@@ -1907,8 +1996,18 @@ void ListWidget::mouseActionStart(
|
||||
false);
|
||||
}
|
||||
|
||||
if (_inDragArea && canReorder() && !hasSelected()) {
|
||||
startReorder(globalPosition);
|
||||
if (_mouseAction == MouseAction::PrepareReorder) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (ClickHandler::getPressed() && !hasSelected()) {
|
||||
_mouseAction = MouseAction::PrepareDrag;
|
||||
if (canReorder()) {
|
||||
startReorder(globalPosition);
|
||||
}
|
||||
} else if (hasSelectedItems()) {
|
||||
if (isItemUnderPressSelected() && ClickHandler::getPressed()) {
|
||||
// In shared media overview drag only by click handlers.
|
||||
@@ -1920,17 +2019,25 @@ void ListWidget::mouseActionStart(
|
||||
if (_mouseAction == MouseAction::None && pressLayout) {
|
||||
validateTrippleClickStartTime();
|
||||
TextState dragState;
|
||||
auto startDistance = (globalPosition - _trippleClickPoint).manhattanLength();
|
||||
auto validStartPoint = startDistance < QApplication::startDragDistance();
|
||||
const auto startDistance = (globalPosition
|
||||
- _trippleClickPoint).manhattanLength();
|
||||
const auto validStartPoint = startDistance
|
||||
< QApplication::startDragDistance();
|
||||
if (_trippleClickStartTime != 0 && validStartPoint) {
|
||||
StateRequest request;
|
||||
request.flags = Ui::Text::StateRequest::Flag::LookupSymbol;
|
||||
dragState = pressLayout->getState(_pressState.cursor, request);
|
||||
if (dragState.cursor == CursorState::Text) {
|
||||
TextSelection selStatus = { dragState.symbol, dragState.symbol };
|
||||
if (selStatus != FullSelection && !hasSelectedItems()) {
|
||||
const auto selStatus = TextSelection{
|
||||
dragState.symbol,
|
||||
dragState.symbol,
|
||||
};
|
||||
if (selStatus != FullSelection
|
||||
&& !hasSelectedItems()) {
|
||||
clearSelected();
|
||||
applyItemSelection(_pressState.item, selStatus);
|
||||
applyItemSelection(
|
||||
_pressState.item,
|
||||
selStatus);
|
||||
_mouseTextSymbol = dragState.symbol;
|
||||
_mouseAction = MouseAction::Selecting;
|
||||
_mouseSelectType = TextSelectType::Paragraphs;
|
||||
@@ -1941,7 +2048,9 @@ void ListWidget::mouseActionStart(
|
||||
} else {
|
||||
StateRequest request;
|
||||
request.flags = Ui::Text::StateRequest::Flag::LookupSymbol;
|
||||
dragState = pressLayout->getState(_pressState.cursor, request);
|
||||
dragState = pressLayout->getState(
|
||||
_pressState.cursor,
|
||||
request);
|
||||
}
|
||||
if (_mouseSelectType != TextSelectType::Paragraphs) {
|
||||
if (_pressState.inside) {
|
||||
@@ -1952,14 +2061,19 @@ void ListWidget::mouseActionStart(
|
||||
if (requiredToStartDragging(pressLayout)) {
|
||||
_mouseAction = MouseAction::PrepareDrag;
|
||||
} else {
|
||||
if (dragState.afterSymbol) ++_mouseTextSymbol;
|
||||
TextSelection selStatus = {
|
||||
if (dragState.afterSymbol) {
|
||||
++_mouseTextSymbol;
|
||||
}
|
||||
const auto selStatus = TextSelection{
|
||||
_mouseTextSymbol,
|
||||
_mouseTextSymbol,
|
||||
};
|
||||
if (selStatus != FullSelection && !hasSelectedItems()) {
|
||||
if (selStatus != FullSelection
|
||||
&& !hasSelectedItems()) {
|
||||
clearSelected();
|
||||
applyItemSelection(_pressState.item, selStatus);
|
||||
applyItemSelection(
|
||||
_pressState.item,
|
||||
selStatus);
|
||||
_mouseAction = MouseAction::Selecting;
|
||||
repaintItem(pressLayout);
|
||||
} else if (!_provider->hasSelectRestriction()) {
|
||||
@@ -1986,6 +2100,7 @@ void ListWidget::mouseActionCancel() {
|
||||
_mouseAction = MouseAction::None;
|
||||
clearDragSelection();
|
||||
_wasSelectedText = false;
|
||||
cancelReorder();
|
||||
// _widget->noSelectingScroll(); // #TODO scroll by drag
|
||||
}
|
||||
|
||||
@@ -2078,18 +2193,24 @@ void ListWidget::mouseActionFinish(
|
||||
Qt::MouseButton button) {
|
||||
mouseActionUpdate(globalPosition);
|
||||
|
||||
auto pressState = base::take(_pressState);
|
||||
const auto pressState = base::take(_pressState);
|
||||
repaintItem(pressState.item);
|
||||
|
||||
const auto selectionMode = hasSelectedItems() || _storiesAddToAlbumId;
|
||||
auto simpleSelectionChange = pressState.item
|
||||
const auto simpleSelectionChange = pressState.item
|
||||
&& pressState.inside
|
||||
&& !_pressWasInactive
|
||||
&& (button != Qt::RightButton)
|
||||
&& (_mouseAction == MouseAction::PrepareDrag
|
||||
|| _mouseAction == MouseAction::PrepareSelect);
|
||||
auto needSelectionToggle = simpleSelectionChange && selectionMode;
|
||||
auto needSelectionClear = simpleSelectionChange && hasSelectedText();
|
||||
if (_mouseAction == MouseAction::Reordering
|
||||
|| _mouseAction == MouseAction::PrepareReorder) {
|
||||
finishReorder();
|
||||
return;
|
||||
}
|
||||
const auto needSelectionToggle = simpleSelectionChange && selectionMode;
|
||||
const auto needSelectionClear = simpleSelectionChange
|
||||
&& hasSelectedText();
|
||||
|
||||
auto activated = ClickHandler::unpressed();
|
||||
if (_mouseAction == MouseAction::Dragging
|
||||
@@ -2125,7 +2246,7 @@ void ListWidget::mouseActionFinish(
|
||||
if (!_dragSelected.empty()) {
|
||||
applyDragSelection();
|
||||
} else if (!_selected.empty() && !_pressWasInactive) {
|
||||
auto selection = _selected.cbegin()->second;
|
||||
const auto selection = _selected.cbegin()->second;
|
||||
if (selection.text != FullSelection
|
||||
&& selection.text.from == selection.text.to) {
|
||||
clearSelected();
|
||||
@@ -2180,7 +2301,7 @@ int ListWidget::recountHeight() {
|
||||
}
|
||||
}
|
||||
}
|
||||
auto cachedPadding = padding();
|
||||
const auto cachedPadding = padding();
|
||||
auto result = cachedPadding.top();
|
||||
for (auto §ion : _sections) {
|
||||
section.setTop(result);
|
||||
@@ -2235,6 +2356,350 @@ auto ListWidget::findSectionAfterBottom(
|
||||
[](const Section §ion) { return section.top(); });
|
||||
}
|
||||
|
||||
void ListWidget::startReorder(const QPoint &globalPos) {
|
||||
if (!canReorder() || _mouseAction == MouseAction::Selecting) {
|
||||
return;
|
||||
}
|
||||
const auto mapped = mapFromGlobal(globalPos);
|
||||
const auto foundWithSection = findItemByPointWithSection(mapped);
|
||||
if (!foundWithSection.section) {
|
||||
return;
|
||||
}
|
||||
if (foundWithSection.section->isOneColumn()
|
||||
? !_inDragArea
|
||||
: !foundWithSection.item.exact) {
|
||||
return;
|
||||
}
|
||||
const auto index = itemIndexFromPoint(mapped
|
||||
- QPoint(foundWithSection.section->oneColumnRightPadding(), 0));
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_reorderDescriptor.filter) {
|
||||
const auto item = foundWithSection.item.layout->getItem();
|
||||
if (!_reorderDescriptor.filter(item)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
_reorderState.enabled = true;
|
||||
_reorderState.index = index;
|
||||
_reorderState.targetIndex = index;
|
||||
_reorderState.startPos = globalPos;
|
||||
_reorderState.dragPoint = mapped
|
||||
- foundWithSection.item.geometry.topLeft();
|
||||
_reorderState.item = foundWithSection.item.layout;
|
||||
_reorderState.section = foundWithSection.section;
|
||||
_mouseAction = MouseAction::PrepareReorder;
|
||||
}
|
||||
|
||||
void ListWidget::updateReorder(const QPoint &globalPos) {
|
||||
if (!_reorderState.enabled || _returnAnimation.animating()) {
|
||||
return;
|
||||
}
|
||||
const auto distance
|
||||
= (globalPos - _reorderState.startPos).manhattanLength();
|
||||
if (_mouseAction == MouseAction::PrepareReorder
|
||||
&& distance > QApplication::startDragDistance()) {
|
||||
_mouseAction = MouseAction::Reordering;
|
||||
}
|
||||
if (_mouseAction == MouseAction::Reordering) {
|
||||
const auto mapped = mapFromGlobal(globalPos);
|
||||
auto localPos = mapped - _reorderState.dragPoint;
|
||||
|
||||
auto currentIndex = 0;
|
||||
for (const auto §ion : _sections) {
|
||||
const auto sectionSize = int(section.items().size());
|
||||
if (_reorderState.index >= currentIndex
|
||||
&& _reorderState.index < currentIndex + sectionSize) {
|
||||
if (section.isOneColumn()) {
|
||||
localPos.setX(_reorderState.item
|
||||
? itemGeometryByIndex(_reorderState.index).x()
|
||||
: localPos.x());
|
||||
}
|
||||
break;
|
||||
}
|
||||
currentIndex += sectionSize;
|
||||
}
|
||||
|
||||
_reorderState.currentPos = localPos;
|
||||
const auto newIndex = itemIndexFromPoint(mapped);
|
||||
if (newIndex >= 0 && newIndex != _reorderState.targetIndex) {
|
||||
_reorderState.targetIndex = newIndex;
|
||||
updateShiftAnimations();
|
||||
}
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void ListWidget::finishReorder() {
|
||||
if (_returnAnimation.animating()) {
|
||||
return;
|
||||
}
|
||||
if (!_reorderState.enabled || _mouseAction != MouseAction::Reordering) {
|
||||
cancelReorder();
|
||||
return;
|
||||
}
|
||||
finishShiftAnimations();
|
||||
if (_reorderState.index != _reorderState.targetIndex) {
|
||||
reorderItemsInSections(
|
||||
_reorderState.index,
|
||||
_reorderState.targetIndex);
|
||||
if (_reorderDescriptor.save) {
|
||||
_reorderDescriptor.save(
|
||||
_reorderState.index,
|
||||
_reorderState.targetIndex,
|
||||
[=] { /* done */ },
|
||||
[=] { /* fail */ }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const auto targetIndex = _reorderState.targetIndex;
|
||||
const auto draggedItem = _reorderState.item;
|
||||
if (draggedItem) {
|
||||
const auto targetGeometry = itemGeometryByIndex(targetIndex);
|
||||
if (!targetGeometry.isEmpty()) {
|
||||
const auto startPos = _reorderState.currentPos;
|
||||
const auto endPos = targetGeometry.topLeft()
|
||||
+ rect::m::pos::tl(padding());
|
||||
const auto callback = [=](float64 progress) {
|
||||
const auto currentPos = QPoint(
|
||||
startPos.x() + (endPos.x() - startPos.x()) * progress,
|
||||
startPos.y() + (endPos.y() - startPos.y()) * progress);
|
||||
_reorderState.currentPos = currentPos;
|
||||
update();
|
||||
if (progress == 1.) {
|
||||
cancelReorder();
|
||||
}
|
||||
};
|
||||
_returnAnimation.start(
|
||||
std::move(callback),
|
||||
0.,
|
||||
1.,
|
||||
st::slideWrapDuration);
|
||||
return;
|
||||
}
|
||||
}
|
||||
cancelReorder();
|
||||
}
|
||||
|
||||
void ListWidget::cancelReorder() {
|
||||
_reorderState = {};
|
||||
finishShiftAnimations();
|
||||
_mouseAction = MouseAction::None;
|
||||
update();
|
||||
}
|
||||
|
||||
void ListWidget::updateShiftAnimations() {
|
||||
if (_reorderState.index < 0 || _reorderState.targetIndex < 0) {
|
||||
return;
|
||||
}
|
||||
const auto fromIndex = _reorderState.index;
|
||||
const auto toIndex = _reorderState.targetIndex;
|
||||
auto itemIndex = 0;
|
||||
for (const auto §ion : _sections) {
|
||||
for (const auto &item : section.items()) {
|
||||
if (itemIndex == fromIndex) {
|
||||
++itemIndex;
|
||||
continue;
|
||||
}
|
||||
auto targetShift = 0;
|
||||
if (fromIndex < toIndex
|
||||
&& itemIndex > fromIndex
|
||||
&& itemIndex <= toIndex) {
|
||||
targetShift = -1;
|
||||
} else if (fromIndex > toIndex
|
||||
&& itemIndex >= toIndex
|
||||
&& itemIndex < fromIndex) {
|
||||
targetShift = 1;
|
||||
}
|
||||
auto &animation = _shiftAnimations[itemIndex];
|
||||
if (animation.targetShift != targetShift) {
|
||||
animation.targetShift = targetShift;
|
||||
const auto fromGeometry = itemGeometryByIndex(itemIndex);
|
||||
const auto toGeometry = itemGeometryByIndex(itemIndex
|
||||
+ targetShift);
|
||||
if (!fromGeometry.isEmpty() && !toGeometry.isEmpty()) {
|
||||
const auto deltaX = toGeometry.x() - fromGeometry.x();
|
||||
const auto deltaY = toGeometry.y() - fromGeometry.y();
|
||||
animation.xAnimation.start(
|
||||
[=](float64 progress) {
|
||||
item->setShiftX(progress);
|
||||
update();
|
||||
},
|
||||
0,
|
||||
deltaX,
|
||||
st::slideWrapDuration);
|
||||
animation.yAnimation.start(
|
||||
[=](float64 progress) {
|
||||
item->setShiftY(progress);
|
||||
update();
|
||||
},
|
||||
0,
|
||||
deltaY,
|
||||
st::slideWrapDuration);
|
||||
}
|
||||
animation.shift = targetShift;
|
||||
}
|
||||
++itemIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int ListWidget::itemIndexFromPoint(QPoint point) const {
|
||||
if (_sections.empty()) {
|
||||
return -1;
|
||||
}
|
||||
const auto found = findItemByPoint(point);
|
||||
if (!found.exact) {
|
||||
return -1;
|
||||
}
|
||||
auto index = 0;
|
||||
for (const auto §ion : _sections) {
|
||||
for (const auto &item : section.items()) {
|
||||
if (item == found.layout) {
|
||||
return index;
|
||||
}
|
||||
++index;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
QRect ListWidget::itemGeometryByIndex(int index) {
|
||||
if (index < 0) {
|
||||
return QRect();
|
||||
}
|
||||
auto currentIndex = 0;
|
||||
for (const auto §ion : _sections) {
|
||||
for (const auto &item : section.items()) {
|
||||
if (currentIndex == index) {
|
||||
return section.findItemDetails(item).geometry;
|
||||
}
|
||||
++currentIndex;
|
||||
}
|
||||
}
|
||||
return QRect();
|
||||
}
|
||||
|
||||
BaseLayout* ListWidget::itemByIndex(int index) {
|
||||
if (index < 0) {
|
||||
return nullptr;
|
||||
}
|
||||
auto currentIndex = 0;
|
||||
for (const auto §ion : _sections) {
|
||||
for (const auto &item : section.items()) {
|
||||
if (currentIndex == index) {
|
||||
return item;
|
||||
}
|
||||
++currentIndex;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool ListWidget::canReorder() const {
|
||||
return !!_reorderDescriptor.save;
|
||||
}
|
||||
|
||||
void ListWidget::reorderItemsInSections(int oldIndex, int newIndex) {
|
||||
if (oldIndex == newIndex || _sections.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto currentIndex = 0;
|
||||
auto oldSection = (Section*)(nullptr);
|
||||
auto newSection = (Section*)(nullptr);
|
||||
auto oldSectionIndex = -1;
|
||||
auto newSectionIndex = -1;
|
||||
|
||||
for (auto §ion : _sections) {
|
||||
const auto sectionSize = int(section.items().size());
|
||||
if (oldIndex >= currentIndex
|
||||
&& oldIndex < currentIndex + sectionSize) {
|
||||
oldSection = §ion;
|
||||
oldSectionIndex = oldIndex - currentIndex;
|
||||
}
|
||||
if (newIndex >= currentIndex
|
||||
&& newIndex < currentIndex + sectionSize) {
|
||||
newSection = §ion;
|
||||
newSectionIndex = newIndex - currentIndex;
|
||||
}
|
||||
currentIndex += sectionSize;
|
||||
}
|
||||
|
||||
if (!oldSection || !newSection) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (oldSection == newSection) {
|
||||
oldSection->reorderItems(oldSectionIndex, newSectionIndex);
|
||||
refreshHeight();
|
||||
}
|
||||
}
|
||||
|
||||
void ListWidget::resetAllItemShifts() {
|
||||
for (auto §ion : _sections) {
|
||||
for (const auto &item : section.items()) {
|
||||
item->setShiftX(0);
|
||||
item->setShiftY(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ListWidget::finishShiftAnimations() {
|
||||
for (auto &[index, animation] : _shiftAnimations) {
|
||||
const auto item = itemByIndex(index);
|
||||
if (!item) {
|
||||
continue;
|
||||
}
|
||||
const auto animating = animation.xAnimation.animating()
|
||||
|| animation.yAnimation.animating();
|
||||
const auto geometry = itemGeometryByIndex(index);
|
||||
animation.xAnimation.stop();
|
||||
animation.yAnimation.stop();
|
||||
if (animating) {
|
||||
++_activeShiftAnimations;
|
||||
animation.xAnimation.start(
|
||||
[=](float64 progress) {
|
||||
if (item) item->setShiftX(progress);
|
||||
update();
|
||||
if (progress == 1.) {
|
||||
--_activeShiftAnimations;
|
||||
if (_activeShiftAnimations == 0) {
|
||||
_shiftAnimations.clear();
|
||||
}
|
||||
}
|
||||
},
|
||||
animation.xAnimation.value(0),
|
||||
0,
|
||||
st::slideWrapDuration);
|
||||
++_activeShiftAnimations;
|
||||
animation.yAnimation.start(
|
||||
[=](float64 progress) {
|
||||
if (item) item->setShiftY(progress);
|
||||
update();
|
||||
if (progress == 1.) {
|
||||
--_activeShiftAnimations;
|
||||
if (_activeShiftAnimations == 0) {
|
||||
_shiftAnimations.clear();
|
||||
}
|
||||
}
|
||||
},
|
||||
(animation.targetShift < 0)
|
||||
? (item->shift().y() + geometry.height())
|
||||
: (item->shift().y() - geometry.height()),
|
||||
0,
|
||||
st::slideWrapDuration);
|
||||
}
|
||||
}
|
||||
if (_activeShiftAnimations == 0) {
|
||||
_shiftAnimations.clear();
|
||||
}
|
||||
resetAllItemShifts();
|
||||
}
|
||||
|
||||
ListWidget::~ListWidget() {
|
||||
if (_contextMenu) {
|
||||
// We don't want it to be called after ListWidget is destroyed.
|
||||
|
||||
@@ -48,6 +48,7 @@ class AbstractController;
|
||||
namespace Media {
|
||||
|
||||
struct ListFoundItem;
|
||||
struct ListFoundItemWithSection;
|
||||
struct ListContext;
|
||||
class ListSection;
|
||||
class ListProvider;
|
||||
@@ -70,6 +71,13 @@ public:
|
||||
rpl::producer<SelectedItems> selectedListValue() const;
|
||||
void selectionAction(SelectionAction action);
|
||||
|
||||
struct ReorderDescriptor {
|
||||
Fn<void(int old, int pos, Fn<void()> done, Fn<void()> fail)> save;
|
||||
Fn<bool(HistoryItem*)> filter;
|
||||
};
|
||||
|
||||
void setReorderDescriptor(ReorderDescriptor descriptor);
|
||||
|
||||
QRect getCurrentSongGeometry();
|
||||
rpl::producer<> checkForHide() const {
|
||||
return _checkForHide.events();
|
||||
@@ -113,6 +121,24 @@ private:
|
||||
Dragging,
|
||||
PrepareSelect,
|
||||
Selecting,
|
||||
PrepareReorder,
|
||||
Reordering,
|
||||
};
|
||||
struct ReorderState {
|
||||
bool enabled = false;
|
||||
int index = -1;
|
||||
int targetIndex = -1;
|
||||
QPoint startPos;
|
||||
QPoint dragPoint;
|
||||
QPoint currentPos;
|
||||
BaseLayout *item = nullptr;
|
||||
const Section *section = nullptr;
|
||||
};
|
||||
struct ShiftAnimation {
|
||||
Ui::Animations::Simple xAnimation;
|
||||
Ui::Animations::Simple yAnimation;
|
||||
int shift = 0;
|
||||
int targetShift = 0;
|
||||
};
|
||||
struct MouseState {
|
||||
HistoryItem *item = nullptr;
|
||||
@@ -233,7 +259,10 @@ private:
|
||||
[[nodiscard]] auto findSectionAfterBottom(
|
||||
std::vector<Section>::const_iterator from,
|
||||
int bottom) const -> std::vector<Section>::const_iterator;
|
||||
[[nodiscard]] auto findSectionAndItem(QPoint point) const
|
||||
-> std::pair<std::vector<Section>::const_iterator, FoundItem>;
|
||||
[[nodiscard]] FoundItem findItemByPoint(QPoint point) const;
|
||||
[[nodiscard]] ListFoundItemWithSection findItemByPointWithSection(QPoint point) const;
|
||||
[[nodiscard]] std::optional<FoundItem> findItemByItem(
|
||||
const HistoryItem *item);
|
||||
[[nodiscard]] FoundItem findItemDetails(not_null<BaseLayout*> item);
|
||||
@@ -279,6 +308,19 @@ private:
|
||||
|
||||
void setupStoriesTrackIds();
|
||||
|
||||
void startReorder(const QPoint &globalPos);
|
||||
void updateReorder(const QPoint &globalPos);
|
||||
void finishReorder();
|
||||
void cancelReorder();
|
||||
void updateShiftAnimations();
|
||||
[[nodiscard]] int itemIndexFromPoint(QPoint point) const;
|
||||
[[nodiscard]] QRect itemGeometryByIndex(int index);
|
||||
[[nodiscard]] BaseLayout *itemByIndex(int index);
|
||||
[[nodiscard]] bool canReorder() const;
|
||||
void reorderItemsInSections(int oldIndex, int newIndex);
|
||||
void resetAllItemShifts();
|
||||
void finishShiftAnimations();
|
||||
|
||||
const not_null<AbstractController*> _controller;
|
||||
const std::unique_ptr<ListProvider> _provider;
|
||||
|
||||
@@ -327,6 +369,13 @@ private:
|
||||
|
||||
base::flat_map<not_null<Main::Session*>, rpl::lifetime> _trackedSessions;
|
||||
|
||||
ReorderState _reorderState;
|
||||
base::flat_map<int, ShiftAnimation> _shiftAnimations;
|
||||
int _activeShiftAnimations = 0;
|
||||
Ui::Animations::Simple _returnAnimation;
|
||||
ReorderDescriptor _reorderDescriptor;
|
||||
bool _inDragArea = false;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Media
|
||||
|
||||
@@ -1359,6 +1359,8 @@ object_ptr<Ui::RpWidget> DetailsFiller::setupInfo() {
|
||||
_mainTracker.track(wrap.data());
|
||||
const auto result = wrap->entity();
|
||||
auto tracker = Ui::MultiSlideTracker();
|
||||
add(CreateSlideSkipWidget(wrap))->toggleOn(
|
||||
tracker.atLeastOneShownValueLater());
|
||||
|
||||
// Fill context for a mention / hashtag / bot command link.
|
||||
const auto infoClickFilter = [=,
|
||||
@@ -2206,8 +2208,8 @@ object_ptr<Ui::RpWidget> DetailsFiller::fill() {
|
||||
if (const auto user = _sublist ? nullptr : _peer->asUser()) {
|
||||
add(setupPersonalChannel(user));
|
||||
}
|
||||
add(CreateSlideSkipWidget(_wrap))->toggleOn(
|
||||
_mainTracker.atLeastOneShownValue());
|
||||
// add(CreateSlideSkipWidget(_wrap))->toggleOn(
|
||||
// _mainTracker.atLeastOneShownValueLater());
|
||||
add(setupInfo());
|
||||
auto lastButtonTracker = Ui::MultiSlideTracker();
|
||||
if (const auto user = _peer->asUser()) {
|
||||
@@ -2249,7 +2251,7 @@ object_ptr<Ui::RpWidget> DetailsFiller::fill() {
|
||||
}
|
||||
}
|
||||
add(CreateSlideSkipWidget(_wrap))->toggleOn(
|
||||
lastButtonTracker.atLeastOneShownValue());
|
||||
lastButtonTracker.atLeastOneShownValueLater());
|
||||
|
||||
return std::move(_wrap);
|
||||
}
|
||||
|
||||
@@ -210,7 +210,7 @@ void InnerWidget::setupSavedMusic(not_null<Ui::VerticalLayout*> container) {
|
||||
Info::Saved::SetupSavedMusic(
|
||||
container,
|
||||
_controller,
|
||||
_peer,
|
||||
_sublist ? _sublist->sublistPeer() : _peer,
|
||||
_topBarColor.value());
|
||||
}
|
||||
|
||||
@@ -243,6 +243,7 @@ object_ptr<Ui::RpWidget> InnerWidget::setupSharedMedia(
|
||||
using namespace rpl::mappers;
|
||||
using MediaType = Media::Type;
|
||||
|
||||
const auto peer = _sublist ? _sublist->sublistPeer() : _peer;
|
||||
auto content = object_ptr<Ui::VerticalLayout>(parent);
|
||||
auto &tracker = sharedTracker;
|
||||
auto addMediaButton = [&](
|
||||
@@ -251,7 +252,7 @@ object_ptr<Ui::RpWidget> InnerWidget::setupSharedMedia(
|
||||
auto result = Media::AddButton(
|
||||
content,
|
||||
_controller,
|
||||
_peer,
|
||||
peer,
|
||||
_topic ? _topic->rootId() : MsgId(),
|
||||
_sublist ? _sublist->sublistPeer()->id : PeerId(),
|
||||
_migrated,
|
||||
@@ -332,9 +333,9 @@ object_ptr<Ui::RpWidget> InnerWidget::setupSharedMedia(
|
||||
};
|
||||
|
||||
if (!_topic) {
|
||||
addStoriesButton(_peer, st::infoIconMediaStories);
|
||||
addPeerGiftsButton(_peer, st::infoIconMediaGifts);
|
||||
addSavedSublistButton(_peer, st::infoIconMediaSaved);
|
||||
addStoriesButton(peer, st::infoIconMediaStories);
|
||||
addPeerGiftsButton(peer, st::infoIconMediaGifts);
|
||||
addSavedSublistButton(peer, st::infoIconMediaSaved);
|
||||
}
|
||||
addMediaButton(MediaType::Photo, st::infoIconMediaPhoto);
|
||||
addMediaButton(MediaType::Video, st::infoIconMediaVideo);
|
||||
@@ -343,12 +344,12 @@ object_ptr<Ui::RpWidget> InnerWidget::setupSharedMedia(
|
||||
addMediaButton(MediaType::Link, st::infoIconMediaLink);
|
||||
addMediaButton(MediaType::RoundVoiceFile, st::infoIconMediaVoice);
|
||||
addMediaButton(MediaType::GIF, st::infoIconMediaGif);
|
||||
if (const auto bot = _peer->asBot()) {
|
||||
if (const auto bot = peer->asBot()) {
|
||||
addCommonGroupsButton(bot, st::infoIconMediaGroup);
|
||||
addSimilarPeersButton(bot, st::infoIconMediaBot);
|
||||
} else if (const auto channel = _peer->asBroadcast()) {
|
||||
} else if (const auto channel = peer->asBroadcast()) {
|
||||
addSimilarPeersButton(channel, st::infoIconMediaChannel);
|
||||
} else if (const auto user = _peer->asUser()) {
|
||||
} else if (const auto user = peer->asUser()) {
|
||||
addCommonGroupsButton(user, st::infoIconMediaGroup);
|
||||
}
|
||||
|
||||
@@ -439,6 +440,7 @@ base::weak_qptr<Ui::RpWidget> InnerWidget::createPinnedToTop(
|
||||
.controller = _controller->parentController(),
|
||||
.key = _controller->key(),
|
||||
.wrap = _controller->wrapValue(),
|
||||
.peer = _sublist ? _sublist->sublistPeer().get() : nullptr,
|
||||
.backToggles = _backToggles.value(),
|
||||
.showFinished = _showFinished.events(),
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "base/timer.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "boxes/peers/edit_peer_info_box.h" // EditPeerInfoBox::Available.
|
||||
#include "boxes/peers/edit_forum_topic_box.h"
|
||||
#include "boxes/moderate_messages_box.h"
|
||||
#include "boxes/report_messages_box.h"
|
||||
#include "boxes/star_gift_box.h"
|
||||
@@ -84,10 +85,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/widgets/menu/menu_add_action_callback_factory.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "ui/widgets/tooltip.h"
|
||||
#include "ui/wrap/fade_wrap.h"
|
||||
#include "window/themes/window_theme.h"
|
||||
#include "window/window_peer_menu.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/toast/toast.h"
|
||||
#include "boxes/sticker_set_box.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
#include "styles/style_chat.h"
|
||||
@@ -103,6 +108,41 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
namespace Info::Profile {
|
||||
namespace {
|
||||
|
||||
class Userpic final
|
||||
: public Ui::AbstractButton
|
||||
, public Ui::AbstractTooltipShower {
|
||||
public:
|
||||
Userpic(QWidget *parent, Fn<bool()> hasStories)
|
||||
: Ui::AbstractButton(parent)
|
||||
, _hasStories(std::move(hasStories)) {
|
||||
installEventFilter(this);
|
||||
}
|
||||
|
||||
QString tooltipText() const override {
|
||||
return _hasStories() ? tr::lng_view_button_story(tr::now) : QString();
|
||||
}
|
||||
|
||||
QPoint tooltipPos() const override {
|
||||
return QCursor::pos();
|
||||
}
|
||||
|
||||
bool tooltipWindowActive() const override {
|
||||
return Ui::AppInFocus() && Ui::InFocusChain(window());
|
||||
}
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject *obj, QEvent *e) override {
|
||||
if (obj == this && e->type() == QEvent::Enter && _hasStories()) {
|
||||
Ui::Tooltip::Show(1000, this);
|
||||
}
|
||||
return Ui::AbstractButton::eventFilter(obj, e);
|
||||
}
|
||||
|
||||
private:
|
||||
Fn<bool()> _hasStories;
|
||||
|
||||
};
|
||||
|
||||
constexpr auto kWaitBeforeGiftBadge = crl::time(1000);
|
||||
constexpr auto kGiftBadgeGlares = 3;
|
||||
constexpr auto kMinPatternRadius = 8;
|
||||
@@ -461,9 +501,12 @@ void TopBar::adjustColors(const std::optional<QColor> &edgeColor) {
|
||||
return edgeColor
|
||||
&& (kMinContrast > Ui::CountContrast(color->c, *edgeColor));
|
||||
};
|
||||
const auto collectible = effectiveCollectible();
|
||||
const auto shouldOverrideTitle = shouldOverride(_title->st().textFg);
|
||||
const auto shouldOverrideStatus = shouldOverride(_status->st().textFg);
|
||||
_title->setTextColorOverride(shouldOverrideTitle
|
||||
const auto shouldOverrideStatus = shouldOverrideTitle; // shouldOverride(_status->st().textFg);
|
||||
_title->setTextColorOverride(collectible
|
||||
? collectible->textColor
|
||||
: shouldOverrideTitle
|
||||
? std::optional<QColor>(st::groupCallMembersFg->c)
|
||||
: std::nullopt);
|
||||
if (!_showLastSeen->isHidden()) {
|
||||
@@ -503,7 +546,9 @@ void TopBar::adjustColors(const std::optional<QColor> &edgeColor) {
|
||||
}, _status->lifetime());
|
||||
_statusLabel = std::make_unique<StatusLabel>(_status.data(), _peer);
|
||||
_statusLabel->setMembersLinkCallback(membersLinkCallback);
|
||||
_status->setTextColorOverride(shouldOverrideStatus
|
||||
_status->setTextColorOverride(collectible
|
||||
? collectible->textColor
|
||||
: shouldOverrideStatus
|
||||
? std::optional<QColor>(st::groupCallVideoSubTextFg->c)
|
||||
: std::nullopt);
|
||||
_statusLabel->setColorized(!shouldOverrideStatus);
|
||||
@@ -542,6 +587,9 @@ void TopBar::adjustColors(const std::optional<QColor> &edgeColor) {
|
||||
}
|
||||
|
||||
void TopBar::updateCollectibleStatus() {
|
||||
if (width() <= 0) {
|
||||
return;
|
||||
}
|
||||
const auto collectible = effectiveCollectible();
|
||||
const auto colorProfile = effectiveColorProfile();
|
||||
_hasGradientBg = (collectible != nullptr)
|
||||
@@ -831,13 +879,21 @@ void TopBar::setupActions(not_null<Window::SessionController*> controller) {
|
||||
if (chechMax()) {
|
||||
return;
|
||||
}
|
||||
if (EditPeerInfoBox::Available(peer)) {
|
||||
if ((topic && topic->canEdit()) || EditPeerInfoBox::Available(peer)) {
|
||||
const auto manage = Ui::CreateChild<TopBarActionButton>(
|
||||
this,
|
||||
tr::lng_profile_action_short_manage(tr::now),
|
||||
st::infoProfileTopBarActionManage);
|
||||
manage->setClickedCallback([=, window = controller] {
|
||||
window->showEditPeerBox(peer);
|
||||
if (topic) {
|
||||
window->show(Box(
|
||||
EditForumTopicBox,
|
||||
window,
|
||||
peer->owner().history(peer),
|
||||
topic->rootId()));
|
||||
} else {
|
||||
window->showEditPeerBox(peer);
|
||||
}
|
||||
});
|
||||
buttons.push_back(manage);
|
||||
_actions->add(manage);
|
||||
@@ -908,32 +964,9 @@ void TopBar::setupActions(not_null<Window::SessionController*> controller) {
|
||||
|
||||
void TopBar::setupUserpicButton(
|
||||
not_null<Window::SessionController*> controller) {
|
||||
_userpicButton = base::make_unique_q<Ui::AbstractButton>(this);
|
||||
|
||||
const auto invalidate = [=] {
|
||||
_userpicUniqueKey = InMemoryKey();
|
||||
_userpicButton->setAttribute(
|
||||
Qt::WA_TransparentForMouseEvents,
|
||||
!_peer->userpicPhotoId() && !_hasStories);
|
||||
updateVideoUserpic();
|
||||
};
|
||||
|
||||
rpl::single(
|
||||
rpl::empty_value()
|
||||
) | rpl::then(
|
||||
_peer->session().changes().peerFlagsValue(
|
||||
_peer,
|
||||
Data::PeerUpdate::Flag::Photo
|
||||
| Data::PeerUpdate::Flag::FullInfo) | rpl::to_empty
|
||||
) | rpl::start_with_next(invalidate, lifetime());
|
||||
|
||||
if (const auto broadcast = _peer->monoforumBroadcast()) {
|
||||
_peer->session().changes().peerFlagsValue(
|
||||
broadcast,
|
||||
Data::PeerUpdate::Flag::Photo
|
||||
| Data::PeerUpdate::Flag::FullInfo
|
||||
) | rpl::to_empty | rpl::start_with_next(invalidate, lifetime());
|
||||
}
|
||||
_userpicButton = base::make_unique_q<Userpic>(
|
||||
this,
|
||||
[=] { return _hasStories; });
|
||||
|
||||
const auto openPhoto = [=, peer = _peer] {
|
||||
if (const auto id = peer->userpicPhotoId()) {
|
||||
@@ -967,13 +1000,22 @@ void TopBar::setupUserpicButton(
|
||||
return true;
|
||||
};
|
||||
|
||||
const auto isContact = [=, peer = _peer] {
|
||||
const auto canChangePhoto = [=, peer = _peer] {
|
||||
if (_topicIconView) {
|
||||
return false;
|
||||
}
|
||||
if (const auto user = peer->asUser()) {
|
||||
return user->isContact()
|
||||
&& !user->isSelf()
|
||||
&& !user->isInaccessible()
|
||||
&& !user->isServiceUser();
|
||||
}
|
||||
if (const auto chat = peer->asChat()) {
|
||||
return chat->canEditInformation();
|
||||
}
|
||||
if (const auto channel = peer->asChannel()) {
|
||||
return channel->canEditInformation();
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -987,9 +1029,60 @@ void TopBar::setupUserpicButton(
|
||||
return false;
|
||||
};
|
||||
|
||||
const auto choosePhotoCallback = [=](Ui::UserpicButton::ChosenType type) {
|
||||
const auto hasMenu = [=] {
|
||||
if (canChangePhoto()) {
|
||||
return true;
|
||||
}
|
||||
if (canSuggestPhoto()) {
|
||||
return true;
|
||||
}
|
||||
if (_hasStories || canReport()) {
|
||||
return !!_peer->userpicPhotoId();
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const auto invalidate = [=] {
|
||||
_userpicUniqueKey = InMemoryKey();
|
||||
const auto hasLeftButton = _peer->userpicPhotoId() || _hasStories;
|
||||
_userpicButton->setAttribute(
|
||||
Qt::WA_TransparentForMouseEvents,
|
||||
!hasLeftButton && !hasMenu());
|
||||
_userpicButton->setPointerCursor(hasLeftButton);
|
||||
updateVideoUserpic();
|
||||
_peer->session().downloaderTaskFinished(
|
||||
) | rpl::filter([=] {
|
||||
return !Ui::PeerUserpicLoading(_userpicView);
|
||||
}) | rpl::start_with_next([=] {
|
||||
update();
|
||||
_userpicLoadingLifetime.destroy();
|
||||
}, _userpicLoadingLifetime);
|
||||
Ui::PostponeCall(this, [=] {
|
||||
update();
|
||||
});
|
||||
};
|
||||
|
||||
rpl::single(
|
||||
rpl::empty_value()
|
||||
) | rpl::then(
|
||||
_peer->session().changes().peerFlagsValue(
|
||||
_peer,
|
||||
Data::PeerUpdate::Flag::Photo
|
||||
| Data::PeerUpdate::Flag::FullInfo) | rpl::to_empty
|
||||
) | rpl::start_with_next(invalidate, lifetime());
|
||||
|
||||
if (const auto broadcast = _peer->monoforumBroadcast()) {
|
||||
_peer->session().changes().peerFlagsValue(
|
||||
broadcast,
|
||||
Data::PeerUpdate::Flag::Photo
|
||||
| Data::PeerUpdate::Flag::FullInfo
|
||||
) | rpl::to_empty | rpl::start_with_next(invalidate, lifetime());
|
||||
}
|
||||
|
||||
using ChosenType = Ui::UserpicButton::ChosenType;
|
||||
|
||||
const auto choosePhotoCallback = [=](ChosenType type) {
|
||||
return [=](QImage &&image) {
|
||||
using ChosenType = Ui::UserpicButton::ChosenType;
|
||||
auto result = Api::PeerPhoto::UserPhoto{
|
||||
std::move(image),
|
||||
0,
|
||||
@@ -1010,12 +1103,12 @@ void TopBar::setupUserpicButton(
|
||||
};
|
||||
};
|
||||
|
||||
const auto editorData = [=](Ui::UserpicButton::ChosenType type) {
|
||||
const auto editorData = [=](ChosenType type) {
|
||||
const auto user = _peer->asUser();
|
||||
const auto name = (user && !user->firstName.isEmpty())
|
||||
? user->firstName
|
||||
: _peer->name();
|
||||
const auto phrase = (type == Ui::UserpicButton::ChosenType::Suggest)
|
||||
const auto phrase = (type == ChosenType::Suggest)
|
||||
? &tr::lng_profile_suggest_sure
|
||||
: &tr::lng_profile_set_personal_sure;
|
||||
return Editor::EditorData{
|
||||
@@ -1024,7 +1117,7 @@ void TopBar::setupUserpicButton(
|
||||
lt_user,
|
||||
Ui::Text::Bold(name),
|
||||
Ui::Text::WithEntities),
|
||||
.confirm = ((type == Ui::UserpicButton::ChosenType::Suggest)
|
||||
.confirm = ((type == ChosenType::Suggest)
|
||||
? tr::lng_profile_suggest_button(tr::now)
|
||||
: tr::lng_profile_set_photo_button(tr::now)),
|
||||
.cropType = Editor::EditorData::CropType::Ellipse,
|
||||
@@ -1032,7 +1125,7 @@ void TopBar::setupUserpicButton(
|
||||
};
|
||||
};
|
||||
|
||||
const auto chooseFile = [=](Ui::UserpicButton::ChosenType type) {
|
||||
const auto chooseFile = [=](ChosenType type) {
|
||||
base::call_delayed(
|
||||
st::defaultRippleAnimation.hideDuration,
|
||||
crl::guard(this, [=] {
|
||||
@@ -1046,7 +1139,7 @@ void TopBar::setupUserpicButton(
|
||||
|
||||
const auto addFromClipboard = [=](
|
||||
Ui::PopupMenu *menu,
|
||||
Ui::UserpicButton::ChosenType type,
|
||||
ChosenType type,
|
||||
tr::phrase<> text) {
|
||||
if (const auto data = QGuiApplication::clipboard()->mimeData()) {
|
||||
if (data->hasImage()) {
|
||||
@@ -1068,9 +1161,7 @@ void TopBar::setupUserpicButton(
|
||||
|
||||
_userpicButton->clicks() | rpl::start_with_next([=](
|
||||
Qt::MouseButton button) {
|
||||
if (button == Qt::RightButton
|
||||
&& (_hasStories || canReport() || isContact())
|
||||
&& _peer->userpicPhotoId()) {
|
||||
if (button == Qt::RightButton && hasMenu()) {
|
||||
*menu = base::make_unique_q<Ui::PopupMenu>(
|
||||
this,
|
||||
st::popupMenuWithIcons);
|
||||
@@ -1095,32 +1186,48 @@ void TopBar::setupUserpicButton(
|
||||
&st::menuIconReport);
|
||||
}
|
||||
|
||||
if (isContact()) {
|
||||
if (canChangePhoto()) {
|
||||
if (!(*menu)->empty()) {
|
||||
(*menu)->addSeparator(&st::expandedMenuSeparator);
|
||||
}
|
||||
(*menu)->addAction(
|
||||
tr::lng_profile_set_photo_for(tr::now),
|
||||
[=] { chooseFile(Ui::UserpicButton::ChosenType::Set); },
|
||||
&st::menuIconPhotoSet);
|
||||
addFromClipboard(
|
||||
menu->get(),
|
||||
Ui::UserpicButton::ChosenType::Set,
|
||||
tr::lng_profile_set_photo_for_from_clipboard);
|
||||
if (canSuggestPhoto()) {
|
||||
const auto isUser = _peer->asUser();
|
||||
if (isUser) {
|
||||
(*menu)->addAction(
|
||||
tr::lng_profile_suggest_photo(tr::now),
|
||||
[=] {
|
||||
chooseFile(
|
||||
Ui::UserpicButton::ChosenType::Suggest);
|
||||
},
|
||||
&st::menuIconPhotoSuggest);
|
||||
tr::lng_profile_set_photo_for(tr::now),
|
||||
[=] { chooseFile(ChosenType::Set); },
|
||||
&st::menuIconPhotoSet);
|
||||
addFromClipboard(
|
||||
menu->get(),
|
||||
Ui::UserpicButton::ChosenType::Suggest,
|
||||
tr::lng_profile_suggest_photo_from_clipboard);
|
||||
ChosenType::Set,
|
||||
tr::lng_profile_set_photo_for_from_clipboard);
|
||||
if (canSuggestPhoto()) {
|
||||
(*menu)->addAction(
|
||||
tr::lng_profile_suggest_photo(tr::now),
|
||||
[=] {
|
||||
chooseFile(
|
||||
ChosenType::Suggest);
|
||||
},
|
||||
&st::menuIconPhotoSuggest);
|
||||
addFromClipboard(
|
||||
menu->get(),
|
||||
ChosenType::Suggest,
|
||||
tr::lng_profile_suggest_photo_from_clipboard);
|
||||
}
|
||||
} else {
|
||||
const auto channel = _peer->asChannel();
|
||||
const auto isChannel = channel && !channel->isMegagroup();
|
||||
(*menu)->addAction(
|
||||
isChannel
|
||||
? tr::lng_profile_set_photo_for_channel(tr::now)
|
||||
: tr::lng_profile_set_photo_for_group(tr::now),
|
||||
[=] { chooseFile(ChosenType::Set); },
|
||||
&st::menuIconPhotoSet);
|
||||
addFromClipboard(
|
||||
menu->get(),
|
||||
ChosenType::Set,
|
||||
tr::lng_profile_set_photo_for_from_clipboard);
|
||||
}
|
||||
if (controller) {
|
||||
if (controller && isUser) {
|
||||
const auto done = [=](UserpicBuilder::Result data) {
|
||||
auto result = Api::PeerPhoto::UserPhoto{
|
||||
base::take(data.image),
|
||||
@@ -1140,10 +1247,43 @@ void TopBar::setupUserpicButton(
|
||||
false);
|
||||
}
|
||||
}
|
||||
|
||||
(*menu)->popup(QCursor::pos());
|
||||
if (!(*menu)->empty()) {
|
||||
(*menu)->popup(QCursor::pos());
|
||||
}
|
||||
} else if (button == Qt::LeftButton) {
|
||||
if (_hasStories) {
|
||||
if (_topicIconView && _topic && _topic->iconId()) {
|
||||
const auto document = _peer->owner().document(
|
||||
_topic->iconId());
|
||||
if (const auto sticker = document->sticker()) {
|
||||
const auto packName
|
||||
= _peer->owner().customEmojiManager().lookupSetName(
|
||||
sticker->set.id);
|
||||
if (!packName.isEmpty()) {
|
||||
const auto text = tr::lng_profile_topic_toast(
|
||||
tr::now,
|
||||
lt_name,
|
||||
Ui::Text::Link(packName, u"internal:"_q),
|
||||
Ui::Text::WithEntities);
|
||||
const auto weak = base::make_weak(controller);
|
||||
controller->showToast(Ui::Toast::Config{
|
||||
.text = text,
|
||||
.filter = [=, set = sticker->set](
|
||||
const ClickHandlerPtr &handler,
|
||||
Qt::MouseButton) {
|
||||
if (const auto strong = weak.get()) {
|
||||
strong->show(
|
||||
Box<StickerSetBox>(
|
||||
strong->uiShow(),
|
||||
set,
|
||||
Data::StickersType::Emoji));
|
||||
}
|
||||
return false;
|
||||
},
|
||||
.duration = crl::time(3000),
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (_hasStories) {
|
||||
controller->openPeerStories(_peer->id);
|
||||
} else {
|
||||
openPhoto();
|
||||
@@ -1312,7 +1452,21 @@ int TopBar::statusMostLeft() const {
|
||||
: _st.subtitlePosition.x();
|
||||
}
|
||||
|
||||
int TopBar::calculateRightButtonsWidth() const {
|
||||
auto width = 0;
|
||||
if (_close) {
|
||||
width += _close->width();
|
||||
}
|
||||
if (_topBarButton) {
|
||||
width += _topBarButton->width();
|
||||
}
|
||||
return width;
|
||||
}
|
||||
|
||||
void TopBar::updateLabelsPosition() {
|
||||
if (width() <= 0) {
|
||||
return;
|
||||
}
|
||||
_progress = [&] {
|
||||
const auto max = QWidget::maximumHeight();
|
||||
const auto min = _minForProgress;
|
||||
@@ -1323,13 +1477,7 @@ void TopBar::updateLabelsPosition() {
|
||||
}();
|
||||
const auto progressCurrent = _progress.current();
|
||||
|
||||
auto rightButtonsWidth = 0;
|
||||
if (_close) {
|
||||
rightButtonsWidth += _close->width();
|
||||
}
|
||||
if (_topBarButton) {
|
||||
rightButtonsWidth += _topBarButton->width();
|
||||
}
|
||||
const auto rightButtonsWidth = calculateRightButtonsWidth();
|
||||
|
||||
const auto reservedRight = anim::interpolate(
|
||||
0,
|
||||
@@ -1431,13 +1579,29 @@ void TopBar::updateLabelsPosition() {
|
||||
}
|
||||
|
||||
void TopBar::updateStatusPosition(float64 progressCurrent) {
|
||||
if (width() <= 0) {
|
||||
return;
|
||||
}
|
||||
if (_forumButton) {
|
||||
const auto buttonTop = anim::interpolate(
|
||||
_st.subtitlePosition.y(),
|
||||
st::infoProfileTopBarStatusTop,
|
||||
progressCurrent);
|
||||
const auto mostLeft = statusMostLeft();
|
||||
const auto buttonMostLeft = anim::interpolate(
|
||||
mostLeft,
|
||||
st::infoProfileTopBarActionButtonsPadding.left(),
|
||||
progressCurrent);
|
||||
const auto buttonMostRight = anim::interpolate(
|
||||
calculateRightButtonsWidth(),
|
||||
st::infoProfileTopBarActionButtonsPadding.right(),
|
||||
progressCurrent);
|
||||
const auto maxWidth = width() - buttonMostLeft - buttonMostRight;
|
||||
if (_forumButton->contentWidth() > maxWidth) {
|
||||
_forumButton->setFullWidth(maxWidth);
|
||||
}
|
||||
const auto buttonLeft = anim::interpolate(
|
||||
statusMostLeft(),
|
||||
mostLeft,
|
||||
(width() - _forumButton->width()) / 2,
|
||||
progressCurrent);
|
||||
_forumButton->moveToLeft(buttonLeft, buttonTop);
|
||||
@@ -1516,6 +1680,9 @@ QRect TopBar::userpicGeometry() const {
|
||||
void TopBar::updateGiftButtonsGeometry(
|
||||
float64 progressCurrent,
|
||||
const QRect &userpicRect) {
|
||||
if (width() <= 0) {
|
||||
return;
|
||||
}
|
||||
const auto sz = st::infoProfileTopBarGiftSize;
|
||||
const auto halfSz = sz / 2.;
|
||||
for (const auto &gift : _pinnedToTopGifts) {
|
||||
@@ -1812,6 +1979,9 @@ void TopBar::fillTopBarMenu(
|
||||
}
|
||||
|
||||
void TopBar::updateVideoUserpic() {
|
||||
if (width() <= 0) {
|
||||
return;
|
||||
}
|
||||
const auto id = _peer->userpicPhotoId();
|
||||
if (!id) {
|
||||
_videoUserpicPlayer = nullptr;
|
||||
@@ -2343,7 +2513,8 @@ void TopBar::paintPinnedToTopGifts(
|
||||
|
||||
void TopBar::setupStoryOutline(const QRect &geometry) {
|
||||
const auto user = _peer->asUser();
|
||||
if (!user) {
|
||||
const auto channel = _peer->asChannel();
|
||||
if (!user && !channel) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2352,18 +2523,18 @@ void TopBar::setupStoryOutline(const QRect &geometry) {
|
||||
rpl::merge(
|
||||
rpl::single(rpl::empty_value()),
|
||||
style::PaletteChanged(),
|
||||
user->session().changes().peerUpdates(
|
||||
_peer->session().changes().peerUpdates(
|
||||
Data::PeerUpdate::Flag::StoriesState
|
||||
| Data::PeerUpdate::Flag::ColorProfile
|
||||
) | rpl::filter([=](const Data::PeerUpdate &update) {
|
||||
return update.peer == user;
|
||||
return update.peer == _peer;
|
||||
}) | rpl::to_empty)
|
||||
) | rpl::start_with_next([=](
|
||||
std::optional<QColor> edgeColor,
|
||||
rpl::empty_value) {
|
||||
const auto geometry = QRectF(userpicGeometry());
|
||||
const auto colorProfile
|
||||
= user->session().api().peerColors().colorProfileFor(user);
|
||||
= _peer->session().api().peerColors().colorProfileFor(_peer);
|
||||
const auto hasProfileColor = colorProfile
|
||||
&& colorProfile->story.size() > 1;
|
||||
if (hasProfileColor) {
|
||||
@@ -2380,17 +2551,21 @@ void TopBar::setupStoryOutline(const QRect &geometry) {
|
||||
}
|
||||
|
||||
void TopBar::updateStoryOutline(std::optional<QColor> edgeColor) {
|
||||
if (width() <= 0) {
|
||||
return;
|
||||
}
|
||||
const auto user = _peer->asUser();
|
||||
if (!user) {
|
||||
const auto channel = _peer->asChannel();
|
||||
if (!user && !channel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto hasActiveStories = (_source == Source::Preview)
|
||||
? true
|
||||
: user->hasActiveStories();
|
||||
: (user ? user->hasActiveStories() : channel->hasActiveStories());
|
||||
const auto hasLiveStories = (_source == Source::Preview)
|
||||
? false
|
||||
: user->hasActiveVideoStream();
|
||||
: (user ? user->hasActiveVideoStream() : false);
|
||||
|
||||
if (_hasStories != hasActiveStories
|
||||
|| _hasLiveStories != hasLiveStories) {
|
||||
@@ -2429,8 +2604,8 @@ void TopBar::updateStoryOutline(std::optional<QColor> edgeColor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto &stories = user->owner().stories();
|
||||
const auto source = stories.source(user->id);
|
||||
const auto &stories = _peer->owner().stories();
|
||||
const auto source = stories.source(_peer->id);
|
||||
if (!source) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -183,6 +183,7 @@ private:
|
||||
void updateStoryOutline(std::optional<QColor> edgeColor);
|
||||
void paintStoryOutline(QPainter &p, const QRect &geometry);
|
||||
void updateStatusPosition(float64 progressCurrent);
|
||||
[[nodiscard]] int calculateRightButtonsWidth() const;
|
||||
[[nodiscard]] const style::FlatLabel &statusStyle() const;
|
||||
void setupStatusWithRating();
|
||||
[[nodiscard]] TopBarActionButtonStyle mapActionStyle(
|
||||
@@ -244,12 +245,15 @@ private:
|
||||
std::vector<AnimatedPatternPoint> _animatedPoints;
|
||||
QRect _lastUserpicRect;
|
||||
|
||||
base::unique_qptr<Ui::AbstractButton> _userpicButton;
|
||||
|
||||
Ui::PeerUserpicView _userpicView;
|
||||
InMemoryKey _userpicUniqueKey;
|
||||
QImage _cachedUserpic;
|
||||
QImage _monoforumMask;
|
||||
std::unique_ptr<Ui::VideoUserpicPlayer> _videoUserpicPlayer;
|
||||
std::unique_ptr<TopicIconView> _topicIconView;
|
||||
rpl::lifetime _userpicLoadingLifetime;
|
||||
|
||||
base::unique_qptr<Ui::IconButton> _close;
|
||||
base::unique_qptr<Ui::FadeWrap<Ui::IconButton>> _back;
|
||||
@@ -261,8 +265,6 @@ private:
|
||||
|
||||
Ui::RpWidget *_actionMore = nullptr;
|
||||
|
||||
base::unique_qptr<Ui::AbstractButton> _userpicButton;
|
||||
|
||||
base::unique_qptr<Ui::HorizontalFitContainer> _actions;
|
||||
|
||||
std::unique_ptr<Lottie::MultiPlayer> _lottiePlayer;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user