Compare commits

..

79 Commits

Author SHA1 Message Date
John Preston
7204c3c25d Version 6.3.2: Fix build with GCC. 2025-11-20 23:58:26 +04:00
23rd
b412241d25 Added minimal threshold to emoji preview for reaction selector. 2025-11-20 22:39:01 +04:00
23rd
f9883afd61 Removed assertion from stats point details widget to avoid crash. 2025-11-20 22:39:01 +04:00
GitHub Action
181f811f18 Update User-Agent for DNS to Chrome 142.0.0.0. 2025-11-20 22:25:19 +04:00
dependabot[bot]
2f0bd3c085 Bump endersonmenezes/free-disk-space from 2.1.1 to 3.0.0
Bumps [endersonmenezes/free-disk-space](https://github.com/endersonmenezes/free-disk-space) from 2.1.1 to 3.0.0.
- [Release notes](https://github.com/endersonmenezes/free-disk-space/releases)
- [Commits](713d134e24...6c4664f433)

---
updated-dependencies:
- dependency-name: endersonmenezes/free-disk-space
  dependency-version: 3.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-20 22:25:03 +04:00
Ilya Fedin
a9d8332766 macos-15-intel -> macos-latest 2025-11-20 22:24:51 +04:00
John Preston
f5036171cf Add actions menu (about/copy/share) for auctions. 2025-11-20 22:24:16 +04:00
John Preston
cdc8b8e473 Show nice userpics in change recipient. 2025-11-20 21:53:35 +04:00
John Preston
ab404c5452 Fix possible crash in sticker preview. 2025-11-20 21:48:30 +04:00
John Preston
0585f9d667 Show correct value of gifted auction gift. 2025-11-20 21:12:20 +04:00
John Preston
19225c7dd3 Rebuild slider when relevant values change. 2025-11-20 20:47:56 +04:00
23rd
8c1844b1c0 Improved unloading heavy part from MediaGeneric for gift's backgrounds. 2025-11-20 19:25:44 +03:00
23rd
af63d86e24 Replaced timer with single-shot updates in auction from view web page. 2025-11-20 19:02:55 +03:00
23rd
1f2fd3ad96 Added support of auctions to history view web page. 2025-11-20 18:22:43 +03:00
23rd
f41fdcdb98 Added saved music to data export. 2025-11-20 15:43:21 +03:00
23rd
f78a9b4220 Removed display of empty menu from userpic in profile top bar. 2025-11-20 15:43:21 +03:00
23rd
06b3ce58ed Fixed stories mouse interaction after album switch in My Profile.
When switching between cached albums with equal story counts,
stories become unresponsive until scrolling occurs. This happens
because the list is recreated with zero visible top/bottom values.

Added deferred visible range update after album switching.
2025-11-20 15:43:20 +03:00
John Preston
7c70d8b1c2 Show some rounds info on details page. 2025-11-20 15:01:26 +04:00
John Preston
2b1e032a9b Show by color if you'll be winning the round. 2025-11-20 14:34:19 +04:00
23rd
4f5d6a2fd5 Replaced dependence from scroll events in flexible scroll with custom. 2025-11-20 14:15:56 +04:00
23rd
c3b90aa492 Improved calculation of filler height for flexible scroll. 2025-11-20 14:15:56 +04:00
23rd
bb2daac007 Added support of text color from emoji status in profile top bar. 2025-11-20 14:15:56 +04:00
23rd
bc2449c3f9 Fixed update of local state after reorder stories in albums. 2025-11-20 14:15:56 +04:00
23rd
0bf50de77a Fixed display of menu for actions in profile top bar for sublists. 2025-11-20 14:15:56 +04:00
23rd
473bc32b71 Fixed peer in info profile opened from sublist. 2025-11-20 14:15:55 +04:00
23rd
e5e143dcf8 Extended behavior of manage button in profile top bar to edit topic. 2025-11-20 14:15:55 +04:00
23rd
2de08746ac Added toast to topic icon in profile top bar. 2025-11-20 14:15:55 +04:00
23rd
ae6833b4d5 Added width checks to profile top bar to prevent redundant calls. 2025-11-20 14:15:55 +04:00
23rd
f1fe5f6a71 Fixed width of very long go-to-forum button in profile top bar. 2025-11-20 14:15:55 +04:00
23rd
5054d0615e Fixed events handler of self forwards tagger.
Regression was introduced in e62a4b065a.
2025-11-20 14:15:55 +04:00
John Preston
4f5007ea64 Use a separate lang key for reset photo button. 2025-11-20 14:14:08 +04:00
John Preston
233f6ed13b Improve bid information display. 2025-11-20 14:13:43 +04:00
John Preston
f15b883471 Support subtext in Stars Bubble in bids. 2025-11-20 13:29:19 +04:00
John Preston
312d5f0121 Improve auction gift display. 2025-11-20 11:39:22 +04:00
Ilya Fedin
75a1657c49 Log Linux notification daemon startup errors 2025-11-20 10:42:13 +04:00
Ilya Fedin
667d92100e Switch Linux notification service watcher to std::optional 2025-11-20 10:42:13 +04:00
Ilya Fedin
8ff4bc8cff Port IconButton back to setAccessibleName 2025-11-19 16:06:56 +04:00
John Preston
656b262648 Allow setting custom auction bid. 2025-11-19 15:00:06 +04:00
John Preston
c1769b9ba2 Correctly show/hide price selection. 2025-11-18 20:30:43 +04:00
John Preston
04c9d92b4a Fix couple of comments layout bugs. 2025-11-18 20:11:22 +04:00
John Preston
0babef5a09 Always show comments, send stars if disabled. 2025-11-18 19:44:24 +04:00
John Preston
cd52407723 Fix back button restore in new profile cover. 2025-11-18 18:24:16 +04:00
John Preston
68c0aa7fb9 Fix quadratic loop on many same custom emoji. 2025-11-18 17:57:00 +04:00
John Preston
f1622c40a4 Fix possible crash from accessibility text insertion. 2025-11-18 17:56:33 +04:00
John Preston
688e7316eb Version 6.3.1.
- Reorder saved music in My Profile.
- Fix new cover overlapping information in My Profile.
- Fix opening stories from My Profile.
- Fix media viewer breaking after viewing Live Stories.
- Fix several possible crashes.
2025-11-17 20:45:32 +04:00
23rd
ef5ec47797 Slightly improved code style of info media list section. 2025-11-17 18:30:36 +03:00
23rd
147ad4a773 Slightly improved code style of info media list widget. 2025-11-17 18:30:36 +03:00
23rd
9752145b49 Fixed userpic paint on new self upload in profile top bar. 2025-11-17 18:30:36 +03:00
23rd
662b862d2f Fixed edge skips in list of actions from info profile. 2025-11-17 18:30:36 +03:00
23rd
6d469509a4 Added tooltip to userpic in profile top bar. 2025-11-17 18:30:36 +03:00
23rd
0dfbb8a5ae Added ability to set group or channel photo from profile top bar. 2025-11-17 18:30:36 +03:00
23rd
1e12ecda70 Extended story outline support to channels in profile top bar. 2025-11-17 18:30:36 +03:00
23rd
035087987c Added icon for reorder to media list section with one column mode. 2025-11-17 18:30:36 +03:00
23rd
18422c4193 Added ability to get both item and section by point in layout overview. 2025-11-17 18:30:36 +03:00
John Preston
f1f9fe27a9 Provide better error on specific bad proxy links.
There are a lot of incorrect MTProxy links with secret in base64
starting with "EE" in base64 encoded format.
2025-11-17 19:26:06 +04:00
John Preston
10667e14e2 Fix crash in ShareBox scheduling.
Fixes #30012.
2025-11-17 18:36:43 +04:00
John Preston
b04c7efdf4 Fix last server message tracking in sublist/forum.
Fixes #30011.
2025-11-17 18:15:16 +04:00
John Preston
0119731360 Fix style of username input for a proxy. 2025-11-17 18:15:00 +04:00
John Preston
8f337684d5 Add close buttons to AuctionGotGiftsBox. 2025-11-17 13:58:19 +04:00
John Preston
dae93552f0 Show correct months count in premium gifts. 2025-11-17 13:57:18 +04:00
John Preston
351bbb240f Shorten bid amount for huge bids. 2025-11-17 13:44:16 +04:00
John Preston
5716de2e6e Fix LIVE badge translations. 2025-11-17 13:03:53 +04:00
John Preston
14295d59d1 Add some debug logs on app startup timing. 2025-11-17 12:55:44 +04:00
John Preston
df0849473c Fix possible crash in button accessibility. 2025-11-17 12:22:54 +04:00
John Preston
9736706894 Fix initial loading of My Profile with stories. 2025-11-17 12:19:23 +04:00
John Preston
12343a5c31 Less templates in Info::FlexibleScroll. 2025-11-17 12:02:42 +04:00
John Preston
a7f046a617 Fix incorrect GL texture deletion. 2025-11-17 11:23:40 +04:00
Ilya Fedin
67bf796f1e Split scheme check code in Linux launch maps implementation 2025-11-17 10:48:01 +04:00
John Preston
22d632abc3 Update hidden gift icon. 2025-11-17 10:47:07 +04:00
John Preston
25094c1ee6 Support paddings in icon parts. 2025-11-17 10:47:07 +04:00
John Preston
5b71ad0456 Fix build with MSVC. 2025-11-17 10:47:07 +04:00
John Preston
9146ba996f Force v143 toolset on Windows for now (win7). 2025-11-17 09:39:25 +04:00
23rd
42900787e1 Added initial ability to filter layout items for reordering. 2025-11-16 23:52:56 +03:00
23rd
ba10c10a94 Added initial ability to reorder tracks in saved music section. 2025-11-16 23:52:56 +03:00
23rd
3f37e9ca6f Added initial ability to reorder stories in albums. 2025-11-16 23:52:56 +03:00
23rd
c5ea86b474 Added initial implementation of reorder items in media layouts.
Except mosaic.
2025-11-16 23:52:56 +03:00
23rd
76720092a5 Added api support to reorder track from saved music. 2025-11-16 23:52:56 +03:00
23rd
7a75c80b27 Attempted to improve status color with darkest gifts in profile top bar. 2025-11-16 13:01:57 +03:00
23rd
b2dcbebb5b Added proper visibility forwarding to flexible scroll content widget. 2025-11-16 12:54:15 +03:00
139 changed files with 3534 additions and 878 deletions

View File

@@ -40,7 +40,7 @@ jobs:
macos:
name: MacOS
runs-on: macos-15-intel
runs-on: macos-latest
strategy:
matrix:

View File

@@ -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

View File

@@ -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 {

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 777 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 938 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -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";

View File

@@ -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>

View 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>

View File

@@ -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"

View File

@@ -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"

View File

@@ -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 {

View File

@@ -378,10 +378,8 @@ connectionPortInputField: InputField(defaultInputField) {
width: 55px;
}
connectionUserInputField: InputField(defaultInputField) {
width: 95px;
}
connectionPasswordInputField: InputField(defaultInputField) {
width: 120px;
}
connectionIPv6Skip: 11px;

View File

@@ -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 {

View File

@@ -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();

View File

@@ -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),
}));
});

View File

@@ -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);

View File

@@ -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,

View File

@@ -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,

View File

@@ -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 &current = 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 &current = 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

View File

@@ -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

View File

@@ -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(

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);

View File

@@ -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, {

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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();
}
}

View File

@@ -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;

View File

@@ -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,

View File

@@ -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;

View File

@@ -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({});
}
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);

View File

@@ -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();
}

View File

@@ -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;

View File

@@ -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,
});
}

View File

@@ -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;

View File

@@ -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];

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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,

View File

@@ -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;

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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,

View File

@@ -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);

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -39,6 +39,7 @@ struct ProcessingState {
PersonalInfo,
Userpics,
Stories,
ProfileMusic,
Contacts,
Sessions,
OtherData,

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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;

View File

@@ -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);

View File

@@ -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;

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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(

View File

@@ -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 &) {
});
}

View File

@@ -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(),

View File

@@ -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);

View File

@@ -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;

View File

@@ -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,

View File

@@ -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;

View File

@@ -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);

View File

@@ -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

View File

@@ -415,7 +415,7 @@ void SelfForwardsTagger::setupToastTimer(
return base::EventFilterResult::Continue;
}
return base::EventFilterResult::Continue;
}, state->timerLifetime);
}, widget->lifetime());
restartTimer(kInitTimer);
}

View File

@@ -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();
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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,

View File

@@ -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,

View File

@@ -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({

View File

@@ -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();
}

View File

@@ -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);

View File

@@ -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

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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)) {
};

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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 &section : _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 &section : _sections) {
section.setTop(result);
@@ -2235,6 +2356,350 @@ auto ListWidget::findSectionAfterBottom(
[](const Section &section) { 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 &section : _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 &section : _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 &section : _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 &section : _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 &section : _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 &section : _sections) {
const auto sectionSize = int(section.items().size());
if (oldIndex >= currentIndex
&& oldIndex < currentIndex + sectionSize) {
oldSection = &section;
oldSectionIndex = oldIndex - currentIndex;
}
if (newIndex >= currentIndex
&& newIndex < currentIndex + sectionSize) {
newSection = &section;
newSectionIndex = newIndex - currentIndex;
}
currentIndex += sectionSize;
}
if (!oldSection || !newSection) {
return;
}
if (oldSection == newSection) {
oldSection->reorderItems(oldSectionIndex, newSectionIndex);
refreshHeight();
}
}
void ListWidget::resetAllItemShifts() {
for (auto &section : _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.

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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(),
});

View File

@@ -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;
}

View File

@@ -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