Compare commits
75 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4505a2bf2d | ||
|
|
a314380b08 | ||
|
|
0c07a015c6 | ||
|
|
a0c7697280 | ||
|
|
04023da723 | ||
|
|
13ea045055 | ||
|
|
f03351d112 | ||
|
|
48d9f10f5b | ||
|
|
2b53df98cd | ||
|
|
99a7a13218 | ||
|
|
2d2d4ac002 | ||
|
|
d12e8023e3 | ||
|
|
17181cee8f | ||
|
|
a74ee911b3 | ||
|
|
b0b37172ce | ||
|
|
cccf048e3f | ||
|
|
82e890746b | ||
|
|
188d65d700 | ||
|
|
4569f93e70 | ||
|
|
bcd1d8461f | ||
|
|
183a9139f9 | ||
|
|
80a1e6ecf3 | ||
|
|
aa1f8cfb8f | ||
|
|
8060691f3d | ||
|
|
bf26de495a | ||
|
|
73b3f7e298 | ||
|
|
22191649aa | ||
|
|
0557907310 | ||
|
|
0f283c484d | ||
|
|
e33ca9d316 | ||
|
|
f93f4c72f7 | ||
|
|
f0b9bc10c2 | ||
|
|
f583879aee | ||
|
|
7ea6c6c84b | ||
|
|
2532a0ff59 | ||
|
|
c3f354826d | ||
|
|
6d1e421ad7 | ||
|
|
29b0055e39 | ||
|
|
2cb20fe342 | ||
|
|
fc97fa4415 | ||
|
|
876a50f759 | ||
|
|
eb0d2868f5 | ||
|
|
e215d5bc64 | ||
|
|
3f0d687656 | ||
|
|
d59eb8e731 | ||
|
|
04e9eed88d | ||
|
|
5072e95f16 | ||
|
|
a2b8366477 | ||
|
|
e9a6bee046 | ||
|
|
080a8d7ee5 | ||
|
|
f94fd3118b | ||
|
|
8ddb13d6e2 | ||
|
|
c6cf8be8d4 | ||
|
|
e92270a9ab | ||
|
|
65d6636a41 | ||
|
|
4701badb2a | ||
|
|
3565215c81 | ||
|
|
3957fea5e4 | ||
|
|
10f1ae152d | ||
|
|
eb29b6bffe | ||
|
|
d157eb0b6e | ||
|
|
0045eb4598 | ||
|
|
b61c66c385 | ||
|
|
56d6c4eb30 | ||
|
|
a6030d708d | ||
|
|
683c3c4f36 | ||
|
|
bd084f9181 | ||
|
|
fef133bf0a | ||
|
|
15c226e6cf | ||
|
|
84f111d641 | ||
|
|
18c1e7ac60 | ||
|
|
ee6dbdced6 | ||
|
|
cb443d797d | ||
|
|
5a6497ec70 | ||
|
|
893ca8bcbd |
2
.github/workflows/mac_packaged.yml
vendored
@@ -69,7 +69,7 @@ jobs:
|
||||
run: |
|
||||
brew update
|
||||
brew upgrade || true
|
||||
brew install ada-url autoconf automake boost cmake ffmpeg@6 libtool openal-soft openh264 openssl opus ninja pkg-config python qt yasm xz
|
||||
brew install ada-url autoconf automake boost cmake ffmpeg libtool openal-soft openh264 openssl opus ninja pkg-config python qt yasm xz
|
||||
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
|
||||
|
||||
xcodebuild -version > CACHE_KEY.txt
|
||||
|
||||
6
.github/workflows/snap.yml
vendored
@@ -57,14 +57,14 @@ jobs:
|
||||
sudo iptables -P FORWARD ACCEPT
|
||||
sudo snap install --classic snapcraft
|
||||
sudo usermod -aG lxd $USER
|
||||
sudo snap run lxd init --auto
|
||||
sudo snap run lxd waitready
|
||||
sudo lxd init --auto
|
||||
sudo lxd waitready
|
||||
|
||||
- name: Free up some disk space.
|
||||
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be
|
||||
|
||||
- name: Telegram Desktop snap build.
|
||||
run: sg lxd -c 'snap run snapcraft --verbosity=debug'
|
||||
run: sudo -u $USER snap run snapcraft --verbosity=debug
|
||||
|
||||
- name: Move artifact.
|
||||
if: env.UPLOAD_ARTIFACT == 'true'
|
||||
|
||||
@@ -946,6 +946,12 @@ PRIVATE
|
||||
info/downloads/info_downloads_provider.h
|
||||
info/downloads/info_downloads_widget.cpp
|
||||
info/downloads/info_downloads_widget.h
|
||||
info/global_media/info_global_media_widget.cpp
|
||||
info/global_media/info_global_media_widget.h
|
||||
info/global_media/info_global_media_inner_widget.cpp
|
||||
info/global_media/info_global_media_inner_widget.h
|
||||
info/global_media/info_global_media_provider.cpp
|
||||
info/global_media/info_global_media_provider.h
|
||||
info/media/info_media_buttons.h
|
||||
info/media/info_media_common.cpp
|
||||
info/media/info_media_common.h
|
||||
|
||||
BIN
Telegram/Resources/icons/menu/caption_hide.png
Normal file
|
After Width: | Height: | Size: 718 B |
BIN
Telegram/Resources/icons/menu/caption_hide@2x.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
Telegram/Resources/icons/menu/caption_hide@3x.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
Telegram/Resources/icons/menu/caption_show.png
Normal file
|
After Width: | Height: | Size: 505 B |
BIN
Telegram/Resources/icons/menu/caption_show@2x.png
Normal file
|
After Width: | Height: | Size: 763 B |
BIN
Telegram/Resources/icons/menu/caption_show@3x.png
Normal file
|
After Width: | Height: | Size: 963 B |
BIN
Telegram/Resources/icons/menu/forwarded_status.png
Normal file
|
After Width: | Height: | Size: 691 B |
BIN
Telegram/Resources/icons/menu/forwarded_status@2x.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
Telegram/Resources/icons/menu/forwarded_status@3x.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
Telegram/Resources/icons/menu/name_hide.png
Normal file
|
After Width: | Height: | Size: 615 B |
BIN
Telegram/Resources/icons/menu/name_hide@2x.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
Telegram/Resources/icons/menu/name_hide@3x.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
Telegram/Resources/icons/menu/name_show.png
Normal file
|
After Width: | Height: | Size: 517 B |
BIN
Telegram/Resources/icons/menu/name_show@2x.png
Normal file
|
After Width: | Height: | Size: 959 B |
BIN
Telegram/Resources/icons/menu/name_show@3x.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
@@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_menu_activate" = "Use this account";
|
||||
"lng_menu_set_status" = "Set Emoji Status";
|
||||
"lng_menu_change_status" = "Change Emoji Status";
|
||||
"lng_menu_my_profile" = "My Profile";
|
||||
"lng_menu_my_stories" = "My Stories";
|
||||
"lng_menu_my_groups" = "My Groups";
|
||||
"lng_menu_my_channels" = "My Channels";
|
||||
@@ -288,6 +289,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_error_cant_add_member" = "Sorry, you can't add the bot to this group. Ask a group admin to do it.";
|
||||
"lng_error_cant_add_bot" = "Sorry, this bot can't be added to groups.";
|
||||
"lng_error_cant_add_admin_invite" = "You can't add this user as an admin because they are not a member of this group and you are not allowed to add them.";
|
||||
"lng_error_you_blocked_user" = "Sorry, you can't add this user or bot to groups because you've blocked them. Please unblock to proceed.";
|
||||
"lng_error_add_admin_not_member" = "You can't add this user as an admin because they are not a member of this group and you are not allowed to add them.";
|
||||
"lng_error_user_admin_invalid" = "You can't ban this user because they are an admin in this group and you are not allowed to demote them.";
|
||||
"lng_error_channel_bots_too_much" = "Sorry, this channel has too many bots.";
|
||||
"lng_error_group_bots_too_much" = "There are too many bots in this group. Please remove some of the bots you're not using first.";
|
||||
"lng_error_cant_add_admin_unban" = "Sorry, you can't add this user as an admin because they are in the Removed Users list and you can't unban them.";
|
||||
"lng_error_cant_ban_admin" = "You can't ban this user because they are an admin in this group and you are not allowed to demote them.";
|
||||
"lng_error_cant_reply_other" = "This message can't be replied in another chat.";
|
||||
@@ -3176,6 +3182,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_gift_stars_sold_out" = "sold out";
|
||||
"lng_gift_stars_tabs_all" = "All Gifts";
|
||||
"lng_gift_stars_tabs_limited" = "Limited";
|
||||
"lng_gift_stars_tabs_in_stock" = "In Stock";
|
||||
"lng_gift_send_title" = "Send a Gift";
|
||||
"lng_gift_send_message" = "Enter Message";
|
||||
"lng_gift_send_anonymous" = "Hide My Name";
|
||||
@@ -3192,11 +3199,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_gift_visible_hint" = "This gift is visible to visitors of your page.";
|
||||
"lng_gift_availability" = "Availability";
|
||||
"lng_gift_from_hidden" = "Hidden User";
|
||||
"lng_gift_visibility" = "Visibility";
|
||||
"lng_gift_visibility_shown" = "Visible on your page";
|
||||
"lng_gift_visibility_hidden" = "Not visible on your page";
|
||||
"lng_gift_visibility_show" = "show";
|
||||
"lng_gift_visibility_hide" = "hide";
|
||||
"lng_gift_availability_left#one" = "{count} of {amount} left";
|
||||
"lng_gift_availability_left#other" = "{count} of {amount} left";
|
||||
"lng_gift_availability_none" = "None of {amount} left";
|
||||
"lng_gift_display_on_page" = "Display on my Page";
|
||||
"lng_gift_display_on_page_hide" = "Hide from my Page";
|
||||
"lng_gift_convert_to_stars#one" = "Convert to {count} Star";
|
||||
"lng_gift_convert_to_stars#other" = "Convert to {count} Stars";
|
||||
"lng_gift_convert_sure_title" = "Convert Gift to Stars";
|
||||
@@ -3647,6 +3657,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_context_mark_read_sure" = "Are you sure you want to mark all chats from this folder as read?";
|
||||
"lng_context_mark_read_all" = "Mark all chats as read";
|
||||
"lng_context_mark_read_all_sure" = "Are you sure you want to mark all chats as read?";
|
||||
"lng_context_mark_read_all_sure_2" = "**This action cannot be undone.**";
|
||||
"lng_context_mark_read_mentions_all" = "Mark all mentions as read";
|
||||
"lng_context_mark_read_reactions_all" = "Read all reactions";
|
||||
"lng_context_archive_expand" = "Expand";
|
||||
@@ -3806,6 +3817,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_paid_react_agree_link" = "Terms of Service";
|
||||
"lng_paid_react_toast#one" = "Star Sent!";
|
||||
"lng_paid_react_toast#other" = "Stars Sent!";
|
||||
"lng_paid_react_toast_anonymous#one" = "Star sent anonymously!";
|
||||
"lng_paid_react_toast_anonymous#other" = "Stars sent anonymously!";
|
||||
"lng_paid_react_toast_text#one" = "You reacted with **{count} Star**.";
|
||||
"lng_paid_react_toast_text#other" = "You reacted with **{count} Stars**.";
|
||||
"lng_paid_react_undo" = "Undo";
|
||||
@@ -4912,6 +4925,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_forward_show_captions" = "Show captions";
|
||||
"lng_forward_change_recipient" = "Change recipient";
|
||||
"lng_forward_sender_names_removed" = "Sender names removed";
|
||||
"lng_forward_header_short" = "Forward";
|
||||
"lng_forward_action_show_sender" = "Show Sender Name";
|
||||
"lng_forward_action_show_senders" = "Show Sender Names";
|
||||
"lng_forward_action_hide_sender" = "Hide Sender Name";
|
||||
"lng_forward_action_hide_senders" = "Hide Sender Names";
|
||||
"lng_forward_action_show_caption" = "Show Caption";
|
||||
"lng_forward_action_show_captions" = "Show Captions";
|
||||
"lng_forward_action_hide_caption" = "Hide Caption";
|
||||
"lng_forward_action_hide_captions" = "Hide Captions";
|
||||
"lng_forward_action_change_recipient" = "Change Recipient";
|
||||
"lng_forward_action_remove" = "Do Not Forward";
|
||||
|
||||
"lng_passport_title" = "Telegram Passport";
|
||||
"lng_passport_request1" = "{bot} requests access to your personal data";
|
||||
@@ -5815,6 +5839,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_recent_chats" = "Chats";
|
||||
"lng_recent_channels" = "Channels";
|
||||
"lng_recent_apps" = "Apps";
|
||||
"lng_all_photos" = "Photos";
|
||||
"lng_all_videos" = "Videos";
|
||||
"lng_all_downloads" = "Downloads";
|
||||
"lng_all_links" = "Links";
|
||||
"lng_all_files" = "Files";
|
||||
"lng_all_music" = "Music";
|
||||
"lng_all_voice" = "Voice";
|
||||
"lng_channels_none_title" = "No channels yet...";
|
||||
"lng_channels_none_about" = "You are not currently subscribed to any channels.";
|
||||
"lng_channels_your_title" = "Channels you joined";
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<Identity Name="TelegramMessengerLLP.TelegramDesktop"
|
||||
ProcessorArchitecture="ARCHITECTURE"
|
||||
Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
|
||||
Version="5.9.0.0" />
|
||||
Version="5.9.2.0" />
|
||||
<Properties>
|
||||
<DisplayName>Telegram Desktop</DisplayName>
|
||||
<PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName>
|
||||
|
||||
@@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico"
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 5,9,0,0
|
||||
PRODUCTVERSION 5,9,0,0
|
||||
FILEVERSION 5,9,2,0
|
||||
PRODUCTVERSION 5,9,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", "5.9.0.0"
|
||||
VALUE "FileVersion", "5.9.2.0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2014-2024"
|
||||
VALUE "ProductName", "Telegram Desktop"
|
||||
VALUE "ProductVersion", "5.9.0.0"
|
||||
VALUE "ProductVersion", "5.9.2.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
@@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 5,9,0,0
|
||||
PRODUCTVERSION 5,9,0,0
|
||||
FILEVERSION 5,9,2,0
|
||||
PRODUCTVERSION 5,9,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", "5.9.0.0"
|
||||
VALUE "FileVersion", "5.9.2.0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2014-2024"
|
||||
VALUE "ProductName", "Telegram Desktop"
|
||||
VALUE "ProductVersion", "5.9.0.0"
|
||||
VALUE "ProductVersion", "5.9.2.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
@@ -140,7 +140,8 @@ void InitFilterLinkHeader(
|
||||
Ui::FilterLinkHeaderType type,
|
||||
const QString &title,
|
||||
const QString &iconEmoji,
|
||||
rpl::producer<int> count) {
|
||||
rpl::producer<int> count,
|
||||
bool horizontalFilters) {
|
||||
const auto icon = Ui::LookupFilterIcon(
|
||||
Ui::LookupFilterIconByEmoji(
|
||||
iconEmoji
|
||||
@@ -154,7 +155,7 @@ void InitFilterLinkHeader(
|
||||
.badge = (type == Ui::FilterLinkHeaderType::AddingChats
|
||||
? std::move(count)
|
||||
: rpl::single(0)),
|
||||
.horizontalFilters = Core::App().settings().chatFiltersHorizontal(),
|
||||
.horizontalFilters = horizontalFilters,
|
||||
});
|
||||
const auto widget = header.widget;
|
||||
widget->resizeToWidth(st::boxWideWidth);
|
||||
@@ -593,6 +594,8 @@ void ProcessFilterInvite(
|
||||
title,
|
||||
std::move(peers),
|
||||
std::move(already));
|
||||
const auto horizontalFilters = !strong->enoughSpaceForFilters()
|
||||
|| Core::App().settings().chatFiltersHorizontal();
|
||||
const auto raw = controller.get();
|
||||
auto initBox = [=](not_null<PeerListBox*> box) {
|
||||
box->setStyle(st::filterInviteBox);
|
||||
@@ -609,7 +612,7 @@ void ProcessFilterInvite(
|
||||
});
|
||||
InitFilterLinkHeader(box, [=](int min, int max, int addedTop) {
|
||||
raw->adjust(min, max, addedTop);
|
||||
}, type, title, iconEmoji, rpl::duplicate(badge));
|
||||
}, type, title, iconEmoji, rpl::duplicate(badge), horizontalFilters);
|
||||
|
||||
raw->setRealContentHeight(box->heightValue());
|
||||
|
||||
@@ -821,6 +824,8 @@ void ProcessFilterRemove(
|
||||
title,
|
||||
std::move(suggest),
|
||||
std::move(all));
|
||||
const auto horizontalFilters = !strong->enoughSpaceForFilters()
|
||||
|| Core::App().settings().chatFiltersHorizontal();
|
||||
const auto raw = controller.get();
|
||||
auto initBox = [=](not_null<PeerListBox*> box) {
|
||||
box->setStyle(st::filterInviteBox);
|
||||
@@ -832,7 +837,7 @@ void ProcessFilterRemove(
|
||||
});
|
||||
InitFilterLinkHeader(box, [=](int min, int max, int addedTop) {
|
||||
raw->adjust(min, max, addedTop);
|
||||
}, type, title, iconEmoji, rpl::single(0));
|
||||
}, type, title, iconEmoji, rpl::single(0), horizontalFilters);
|
||||
|
||||
auto owned = Ui::FilterLinkProcessButton(
|
||||
box,
|
||||
|
||||
@@ -207,32 +207,12 @@ void ConfirmSubscriptionBox(
|
||||
Ui::AddSkip(content);
|
||||
Ui::AddSkip(content);
|
||||
|
||||
{
|
||||
const auto widget = Ui::CreateChild<Ui::RpWidget>(content);
|
||||
using ColoredMiniStars = Ui::Premium::ColoredMiniStars;
|
||||
const auto stars = widget->lifetime().make_state<ColoredMiniStars>(
|
||||
widget,
|
||||
false,
|
||||
Ui::Premium::MiniStars::Type::BiStars);
|
||||
stars->setColorOverride(Ui::Premium::CreditsIconGradientStops());
|
||||
widget->resize(
|
||||
st::boxWideWidth - photoSize,
|
||||
photoSize * 2);
|
||||
content->sizeValue(
|
||||
) | rpl::start_with_next([=](const QSize &size) {
|
||||
widget->moveToLeft(photoSize / 2, 0);
|
||||
const auto starsRect = Rect(widget->size());
|
||||
stars->setPosition(starsRect.topLeft());
|
||||
stars->setSize(starsRect.size());
|
||||
widget->lower();
|
||||
}, widget->lifetime());
|
||||
widget->paintRequest(
|
||||
) | rpl::start_with_next([=](const QRect &r) {
|
||||
auto p = QPainter(widget);
|
||||
p.fillRect(r, Qt::transparent);
|
||||
stars->paint(p);
|
||||
}, widget->lifetime());
|
||||
}
|
||||
Settings::AddMiniStars(
|
||||
content,
|
||||
Ui::CreateChild<Ui::RpWidget>(content),
|
||||
photoSize,
|
||||
box->width(),
|
||||
2.);
|
||||
|
||||
box->addRow(
|
||||
object_ptr<Ui::CenterWrap<Ui::FlatLabel>>(
|
||||
|
||||
@@ -74,12 +74,17 @@ const FoundMessages &MessagesSearchMerged::messages() const {
|
||||
return _concatedFound;
|
||||
}
|
||||
|
||||
const MessagesSearch::Request &MessagesSearchMerged::request() const {
|
||||
return _request;
|
||||
}
|
||||
|
||||
void MessagesSearchMerged::clear() {
|
||||
_concatedFound = {};
|
||||
_migratedFirstFound = {};
|
||||
}
|
||||
|
||||
void MessagesSearchMerged::search(const Request &search) {
|
||||
_request = search;
|
||||
if (_migratedSearch) {
|
||||
_waitingForTotal = true;
|
||||
_migratedSearch->searchMessages(search);
|
||||
|
||||
@@ -31,6 +31,7 @@ public:
|
||||
void searchMore();
|
||||
|
||||
[[nodiscard]] const FoundMessages &messages() const;
|
||||
[[nodiscard]] const Request &request() const;
|
||||
|
||||
[[nodiscard]] rpl::producer<> newFounds() const;
|
||||
[[nodiscard]] rpl::producer<> nextFounds() const;
|
||||
@@ -39,6 +40,7 @@ private:
|
||||
void addFound(const FoundMessages &data);
|
||||
|
||||
MessagesSearch _apiSearch;
|
||||
Request _request;
|
||||
|
||||
std::optional<MessagesSearch> _migratedSearch;
|
||||
FoundMessages _migratedFirstFound;
|
||||
|
||||
@@ -756,19 +756,32 @@ rpl::producer<Ui::WhoReadContent> WhoReacted(
|
||||
const style::WhoRead &st) {
|
||||
return WhoReacted(item, reaction, context, st, nullptr);
|
||||
}
|
||||
rpl::producer<Ui::WhoReadContent> WhenEdited(
|
||||
|
||||
[[nodiscard]] rpl::producer<Ui::WhoReadContent> WhenDate(
|
||||
not_null<PeerData*> author,
|
||||
TimeId date) {
|
||||
TimeId date,
|
||||
Ui::WhoReadType type) {
|
||||
return rpl::single(Ui::WhoReadContent{
|
||||
.participants = { Ui::WhoReadParticipant{
|
||||
.name = author->name(),
|
||||
.date = FormatReadDate(date, QDateTime::currentDateTime()),
|
||||
.id = author->id.value,
|
||||
} },
|
||||
.type = Ui::WhoReadType::Edited,
|
||||
.type = type,
|
||||
.fullReadCount = 1,
|
||||
});
|
||||
}
|
||||
|
||||
rpl::producer<Ui::WhoReadContent> WhenEdited(
|
||||
not_null<PeerData*> author,
|
||||
TimeId date) {
|
||||
return WhenDate(author, date, Ui::WhoReadType::Edited);
|
||||
}
|
||||
|
||||
rpl::producer<Ui::WhoReadContent> WhenOriginal(
|
||||
not_null<PeerData*> author,
|
||||
TimeId date) {
|
||||
return WhenDate(author, date, Ui::WhoReadType::Original);
|
||||
}
|
||||
|
||||
} // namespace Api
|
||||
|
||||
@@ -64,5 +64,8 @@ struct WhoReadList {
|
||||
[[nodiscard]] rpl::producer<Ui::WhoReadContent> WhenEdited(
|
||||
not_null<PeerData*> author,
|
||||
TimeId date);
|
||||
[[nodiscard]] rpl::producer<Ui::WhoReadContent> WhenOriginal(
|
||||
not_null<PeerData*> author,
|
||||
TimeId date);
|
||||
|
||||
} // namespace Api
|
||||
|
||||
@@ -3219,6 +3219,31 @@ void ApiWrap::sharedMediaDone(
|
||||
}
|
||||
}
|
||||
|
||||
mtpRequestId ApiWrap::requestGlobalMedia(
|
||||
Storage::SharedMediaType type,
|
||||
const QString &query,
|
||||
int32 offsetRate,
|
||||
Data::MessagePosition offsetPosition,
|
||||
Fn<void(Api::GlobalMediaResult)> done) {
|
||||
auto prepared = Api::PrepareGlobalMediaRequest(
|
||||
_session,
|
||||
offsetRate,
|
||||
offsetPosition,
|
||||
type,
|
||||
query);
|
||||
if (!prepared) {
|
||||
done({});
|
||||
return 0;
|
||||
}
|
||||
return request(
|
||||
std::move(*prepared)
|
||||
).done([=](const Api::SearchRequestResult &result) {
|
||||
done(Api::ParseGlobalMediaResult(_session, result));
|
||||
}).fail([=] {
|
||||
done({});
|
||||
}).send();
|
||||
}
|
||||
|
||||
void ApiWrap::sendAction(const SendAction &action) {
|
||||
if (!action.options.scheduled
|
||||
&& !action.options.shortcutId
|
||||
|
||||
@@ -59,6 +59,7 @@ class Show;
|
||||
namespace Api {
|
||||
|
||||
struct SearchResult;
|
||||
struct GlobalMediaResult;
|
||||
|
||||
class Updates;
|
||||
class Authorizations;
|
||||
@@ -288,6 +289,12 @@ public:
|
||||
Storage::SharedMediaType type,
|
||||
MsgId messageId,
|
||||
SliceType slice);
|
||||
mtpRequestId requestGlobalMedia(
|
||||
Storage::SharedMediaType type,
|
||||
const QString &query,
|
||||
int32 offsetRate,
|
||||
Data::MessagePosition offsetPosition,
|
||||
Fn<void(Api::GlobalMediaResult)> done);
|
||||
|
||||
void readFeaturedSetDelayed(uint64 setId);
|
||||
|
||||
@@ -509,6 +516,10 @@ private:
|
||||
MsgId topicRootId,
|
||||
SharedMediaType type,
|
||||
Api::SearchResult &&parsed);
|
||||
void globalMediaDone(
|
||||
SharedMediaType type,
|
||||
FullMsgId messageId,
|
||||
Api::GlobalMediaResult &&parsed);
|
||||
|
||||
void sendSharedContact(
|
||||
const QString &phone,
|
||||
@@ -672,6 +683,17 @@ private:
|
||||
};
|
||||
base::flat_set<HistoryRequest> _historyRequests;
|
||||
|
||||
struct GlobalMediaRequest {
|
||||
SharedMediaType mediaType = {};
|
||||
FullMsgId aroundId;
|
||||
SliceType sliceType = {};
|
||||
|
||||
friend inline auto operator<=>(
|
||||
const GlobalMediaRequest&,
|
||||
const GlobalMediaRequest&) = default;
|
||||
};
|
||||
base::flat_set<GlobalMediaRequest> _globalMediaRequests;
|
||||
|
||||
std::unique_ptr<DialogsLoadState> _dialogsLoadState;
|
||||
TimeId _dialogsLoadTill = 0;
|
||||
rpl::variable<bool> _dialogsLoadMayBlockByDate = false;
|
||||
|
||||
@@ -262,10 +262,16 @@ void ShowAddParticipantsError(
|
||||
return tr::lng_bot_already_in_group(tr::now);
|
||||
} else if (error == u"BOT_GROUPS_BLOCKED"_q) {
|
||||
return tr::lng_error_cant_add_bot(tr::now);
|
||||
} else if (error == u"ADMINS_TOO_MUCH"_q) {
|
||||
return ((chat->isChat() || chat->isMegagroup())
|
||||
? tr::lng_error_admin_limit
|
||||
: tr::lng_error_admin_limit_channel)(tr::now);
|
||||
} else if (error == u"YOU_BLOCKED_USER"_q) {
|
||||
return tr::lng_error_you_blocked_user(tr::now);
|
||||
} else if (error == u"CHAT_ADMIN_INVITE_REQUIRED"_q) {
|
||||
return tr::lng_error_add_admin_not_member(tr::now);
|
||||
} else if (error == u"USER_ADMIN_INVALID"_q) {
|
||||
return tr::lng_error_user_admin_invalid(tr::now);
|
||||
} else if (error == u"BOTS_TOO_MUCH"_q) {
|
||||
return (chat->isChannel()
|
||||
? tr::lng_error_channel_bots_too_much
|
||||
: tr::lng_error_group_bots_too_much)(tr::now);
|
||||
}
|
||||
return tr::lng_failed_add_participant(tr::now);
|
||||
}();
|
||||
|
||||
@@ -47,6 +47,7 @@ void GiftCreditsBox(
|
||||
const auto content = box->setPinnedToTopContent(
|
||||
object_ptr<Ui::VerticalLayout>(box));
|
||||
|
||||
Ui::AddSkip(content);
|
||||
Ui::AddSkip(content);
|
||||
Ui::AddSkip(content);
|
||||
const auto &stUser = st::premiumGiftsUserpicButton;
|
||||
@@ -58,39 +59,19 @@ void GiftCreditsBox(
|
||||
Ui::AddSkip(content);
|
||||
Ui::AddSkip(content);
|
||||
|
||||
{
|
||||
const auto widget = Ui::CreateChild<Ui::RpWidget>(content);
|
||||
using ColoredMiniStars = Ui::Premium::ColoredMiniStars;
|
||||
const auto stars = widget->lifetime().make_state<ColoredMiniStars>(
|
||||
widget,
|
||||
false,
|
||||
Ui::Premium::MiniStars::Type::BiStars);
|
||||
stars->setColorOverride(Ui::Premium::CreditsIconGradientStops());
|
||||
widget->resize(
|
||||
st::boxWidth - stUser.photoSize,
|
||||
stUser.photoSize * 2);
|
||||
content->sizeValue(
|
||||
) | rpl::start_with_next([=](const QSize &size) {
|
||||
widget->moveToLeft(stUser.photoSize / 2, 0);
|
||||
const auto starsRect = Rect(widget->size());
|
||||
stars->setPosition(starsRect.topLeft());
|
||||
stars->setSize(starsRect.size());
|
||||
widget->lower();
|
||||
}, widget->lifetime());
|
||||
widget->paintRequest(
|
||||
) | rpl::start_with_next([=](const QRect &r) {
|
||||
auto p = QPainter(widget);
|
||||
p.fillRect(r, Qt::transparent);
|
||||
stars->paint(p);
|
||||
}, widget->lifetime());
|
||||
}
|
||||
Settings::AddMiniStars(
|
||||
content,
|
||||
Ui::CreateChild<Ui::RpWidget>(content),
|
||||
stUser.photoSize,
|
||||
box->width(),
|
||||
2.);
|
||||
{
|
||||
Ui::AddSkip(content);
|
||||
const auto arrow = Ui::Text::SingleCustomEmoji(
|
||||
peer->owner().customEmojiManager().registerInternalEmoji(
|
||||
st::topicButtonArrow,
|
||||
st::channelEarnLearnArrowMargins,
|
||||
false));
|
||||
true));
|
||||
auto link = tr::lng_credits_box_history_entry_gift_about_link(
|
||||
lt_emoji,
|
||||
rpl::single(arrow),
|
||||
|
||||
@@ -237,7 +237,7 @@ void AddTableRow(
|
||||
valueMargins);
|
||||
}
|
||||
|
||||
object_ptr<Ui::RpWidget> MakeStarGiftStarsValue(
|
||||
[[nodiscard]] object_ptr<Ui::RpWidget> MakeStarGiftStarsValue(
|
||||
not_null<QWidget*> parent,
|
||||
not_null<Window::SessionNavigation*> controller,
|
||||
const Data::CreditsHistoryEntry &entry,
|
||||
@@ -302,6 +302,62 @@ object_ptr<Ui::RpWidget> MakeStarGiftStarsValue(
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::RpWidget> MakeVisibilityTableValue(
|
||||
not_null<QWidget*> parent,
|
||||
not_null<Window::SessionNavigation*> controller,
|
||||
bool savedToProfile,
|
||||
Fn<void(bool)> toggleVisibility) {
|
||||
auto result = object_ptr<Ui::RpWidget>(parent);
|
||||
const auto raw = result.data();
|
||||
|
||||
const auto label = Ui::CreateChild<Ui::FlatLabel>(
|
||||
raw,
|
||||
(savedToProfile
|
||||
? tr::lng_gift_visibility_shown()
|
||||
: tr::lng_gift_visibility_hidden()),
|
||||
st::giveawayGiftCodeValue,
|
||||
st::defaultPopupMenu);
|
||||
|
||||
const auto toggle = Ui::CreateChild<Ui::RoundButton>(
|
||||
raw,
|
||||
(savedToProfile
|
||||
? tr::lng_gift_visibility_hide()
|
||||
: tr::lng_gift_visibility_show()),
|
||||
st::starGiftSmallButton);
|
||||
toggle->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
|
||||
toggle->setClickedCallback([=] {
|
||||
toggleVisibility(!savedToProfile);
|
||||
});
|
||||
|
||||
rpl::combine(
|
||||
raw->widthValue(),
|
||||
toggle->widthValue()
|
||||
) | rpl::start_with_next([=](int width, int toggleWidth) {
|
||||
const auto toggleSkip = toggleWidth
|
||||
? (st::normalFont->spacew + toggleWidth)
|
||||
: 0;
|
||||
label->resizeToNaturalWidth(width - toggleSkip);
|
||||
label->moveToLeft(0, 0, width);
|
||||
if (toggle) {
|
||||
toggle->moveToLeft(
|
||||
label->width() + st::normalFont->spacew,
|
||||
(st::giveawayGiftCodeValue.style.font->ascent
|
||||
- st::starGiftSmallButton.style.font->ascent),
|
||||
width);
|
||||
}
|
||||
}, label->lifetime());
|
||||
|
||||
label->heightValue() | rpl::start_with_next([=](int height) {
|
||||
raw->resize(
|
||||
raw->width(),
|
||||
height + st::giveawayGiftCodeValueMargin.bottom());
|
||||
}, raw->lifetime());
|
||||
|
||||
label->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
not_null<Ui::FlatLabel*> AddTableRow(
|
||||
not_null<Ui::TableLayout*> table,
|
||||
rpl::producer<QString> label,
|
||||
@@ -1035,6 +1091,7 @@ void AddStarGiftTable(
|
||||
not_null<Window::SessionNavigation*> controller,
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
const Data::CreditsHistoryEntry &entry,
|
||||
Fn<void(bool)> toggleVisibility,
|
||||
Fn<void()> convertToStars) {
|
||||
auto table = container->add(
|
||||
object_ptr<Ui::TableLayout>(
|
||||
@@ -1072,9 +1129,15 @@ void AddStarGiftTable(
|
||||
rpl::single(Ui::Text::WithEntities(
|
||||
langDateTime(entry.lastSaleDate))));
|
||||
}
|
||||
if (!entry.date.isNull()) {
|
||||
AddTableRow(
|
||||
table,
|
||||
tr::lng_gift_link_label_date(),
|
||||
rpl::single(Ui::Text::WithEntities(langDateTime(entry.date))));
|
||||
}
|
||||
const auto marginWithButton = st::giveawayGiftCodeValueMargin
|
||||
- QMargins(0, 0, 0, st::giveawayGiftCodeValueMargin.bottom());
|
||||
{
|
||||
const auto margin = st::giveawayGiftCodeValueMargin
|
||||
- QMargins(0, 0, 0, st::giveawayGiftCodeValueMargin.bottom());
|
||||
AddTableRow(
|
||||
table,
|
||||
tr::lng_gift_link_label_value(),
|
||||
@@ -1083,13 +1146,18 @@ void AddStarGiftTable(
|
||||
controller,
|
||||
entry,
|
||||
std::move(convertToStars)),
|
||||
margin);
|
||||
marginWithButton);
|
||||
}
|
||||
if (!entry.date.isNull()) {
|
||||
if (toggleVisibility) {
|
||||
AddTableRow(
|
||||
table,
|
||||
tr::lng_gift_link_label_date(),
|
||||
rpl::single(Ui::Text::WithEntities(langDateTime(entry.date))));
|
||||
tr::lng_gift_visibility(),
|
||||
MakeVisibilityTableValue(
|
||||
table,
|
||||
controller,
|
||||
entry.savedToProfile,
|
||||
std::move(toggleVisibility)),
|
||||
marginWithButton);
|
||||
}
|
||||
if (entry.limitedCount > 0) {
|
||||
auto amount = rpl::single(TextWithEntities{
|
||||
|
||||
@@ -58,6 +58,7 @@ void AddStarGiftTable(
|
||||
not_null<Window::SessionNavigation*> controller,
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
const Data::CreditsHistoryEntry &entry,
|
||||
Fn<void(bool)> toggleVisibility,
|
||||
Fn<void()> convertToStars);
|
||||
void AddCreditsHistoryEntryTable(
|
||||
not_null<Window::SessionNavigation*> controller,
|
||||
|
||||
@@ -120,6 +120,9 @@ void PeerListBox::createMultiSelect() {
|
||||
content()->submitted();
|
||||
});
|
||||
_select->entity()->setQueryChangedCallback([=](const QString &query) {
|
||||
if (_customQueryChangedCallback) {
|
||||
_customQueryChangedCallback(query);
|
||||
}
|
||||
searchQueryChanged(query);
|
||||
});
|
||||
_select->entity()->setItemRemovedCallback([=](uint64 itemId) {
|
||||
@@ -138,6 +141,10 @@ void PeerListBox::createMultiSelect() {
|
||||
_select->moveToLeft(0, 0);
|
||||
}
|
||||
|
||||
void PeerListBox::appendQueryChangedCallback(Fn<void(QString)> callback) {
|
||||
_customQueryChangedCallback = std::move(callback);
|
||||
}
|
||||
|
||||
void PeerListBox::setAddedTopScrollSkip(int skip) {
|
||||
_addedTopScrollSkip = skip;
|
||||
_scrollBottomFixed = false;
|
||||
@@ -474,7 +481,7 @@ void PeerListBox::addSelectItem(
|
||||
void PeerListBox::addSelectItem(
|
||||
uint64 itemId,
|
||||
const QString &text,
|
||||
Ui::MultiSelect::PaintRoundImage paintUserpic,
|
||||
PaintRoundImageCallback paintUserpic,
|
||||
anim::type animated) {
|
||||
if (!_select) {
|
||||
createMultiSelect();
|
||||
@@ -549,6 +556,10 @@ rpl::producer<int> PeerListBox::multiSelectHeightValue() const {
|
||||
return _select ? _select->heightValue() : rpl::single(0);
|
||||
}
|
||||
|
||||
rpl::producer<> PeerListBox::noSearchSubmits() const {
|
||||
return content()->noSearchSubmits();
|
||||
}
|
||||
|
||||
PeerListRow::PeerListRow(not_null<PeerData*> peer)
|
||||
: PeerListRow(peer, peer->id.value) {
|
||||
}
|
||||
@@ -1270,6 +1281,9 @@ void PeerListContent::clearAllContent() {
|
||||
= _normalizedSearchQuery
|
||||
= _mentionHighlight
|
||||
= QString();
|
||||
if (_controller->hasComplexSearch()) {
|
||||
_controller->search(QString());
|
||||
}
|
||||
}
|
||||
|
||||
void PeerListContent::convertRowToSearchResult(not_null<PeerListRow*> row) {
|
||||
@@ -2189,6 +2203,9 @@ bool PeerListContent::submitted() {
|
||||
_controller->rowClicked(row);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
_noSearchSubmits.fire({});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -712,7 +712,7 @@ public:
|
||||
update();
|
||||
}
|
||||
|
||||
std::unique_ptr<PeerListState> saveState() const;
|
||||
[[nodiscard]] std::unique_ptr<PeerListState> saveState() const;
|
||||
void restoreState(std::unique_ptr<PeerListState> state);
|
||||
|
||||
void showRowMenu(
|
||||
@@ -720,10 +720,14 @@ public:
|
||||
bool highlightRow,
|
||||
Fn<void(not_null<Ui::PopupMenu*>)> destroyed);
|
||||
|
||||
auto scrollToRequests() const {
|
||||
[[nodiscard]] auto scrollToRequests() const {
|
||||
return _scrollToRequests.events();
|
||||
}
|
||||
|
||||
[[nodiscard]] auto noSearchSubmits() const {
|
||||
return _noSearchSubmits.events();
|
||||
}
|
||||
|
||||
~PeerListContent();
|
||||
|
||||
protected:
|
||||
@@ -890,6 +894,8 @@ private:
|
||||
object_ptr<Ui::FlatLabel> _searchLoading = { nullptr };
|
||||
object_ptr<Ui::RpWidget> _loadingAnimation = { nullptr };
|
||||
|
||||
rpl::event_stream<> _noSearchSubmits;
|
||||
|
||||
std::vector<std::unique_ptr<PeerListRow>> _searchRows;
|
||||
base::Timer _repaintByStatus;
|
||||
base::unique_qptr<Ui::PopupMenu> _contextMenu;
|
||||
@@ -1106,6 +1112,7 @@ public:
|
||||
[[nodiscard]] std::vector<PeerListRowId> collectSelectedIds();
|
||||
[[nodiscard]] std::vector<not_null<PeerData*>> collectSelectedRows();
|
||||
[[nodiscard]] rpl::producer<int> multiSelectHeightValue() const;
|
||||
[[nodiscard]] rpl::producer<> noSearchSubmits() const;
|
||||
|
||||
void peerListSetTitle(rpl::producer<QString> title) override {
|
||||
setTitle(std::move(title));
|
||||
@@ -1130,6 +1137,8 @@ public:
|
||||
|
||||
void showFinished() override;
|
||||
|
||||
void appendQueryChangedCallback(Fn<void(QString)>);
|
||||
|
||||
protected:
|
||||
void prepare() override;
|
||||
void setInnerFocus() override;
|
||||
@@ -1167,6 +1176,7 @@ private:
|
||||
object_ptr<Ui::SlideWrap<Ui::MultiSelect>> _select = { nullptr };
|
||||
|
||||
const std::shared_ptr<Main::SessionShow> _show;
|
||||
Fn<void(QString)> _customQueryChangedCallback;
|
||||
std::unique_ptr<PeerListController> _controller;
|
||||
Fn<void(PeerListBox*)> _init;
|
||||
bool _scrollBottomFixed = false;
|
||||
|
||||
@@ -57,6 +57,7 @@ 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/ui_utility.h"
|
||||
#include "ui/vertical_list.h"
|
||||
#include "ui/widgets/fields/input_field.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
@@ -71,11 +72,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "styles/style_premium.h"
|
||||
#include "styles/style_settings.h"
|
||||
|
||||
#include <QtWidgets/QApplication>
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
constexpr auto kPriceTabAll = 0;
|
||||
constexpr auto kPriceTabLimited = -1;
|
||||
constexpr auto kPriceTabInStock = -2;
|
||||
constexpr auto kGiftMessageLimit = 255;
|
||||
constexpr auto kSentToastDuration = 3 * crl::time(1000);
|
||||
|
||||
@@ -156,6 +160,10 @@ private:
|
||||
return is(now) || is(now.addDays(1)) || is(now.addDays(-1));
|
||||
}
|
||||
|
||||
[[nodiscard]] bool IsSoldOut(const Api::StarGift &info) {
|
||||
return info.limitedCount && !info.limitedLeft;
|
||||
}
|
||||
|
||||
PreviewDelegate::PreviewDelegate(
|
||||
not_null<QWidget*> parent,
|
||||
not_null<Ui::ChatStyle*> st,
|
||||
@@ -554,6 +562,8 @@ void PreviewWrap::paintEvent(QPaintEvent *e) {
|
||||
return simple(tr::lng_gift_stars_tabs_all(tr::now));
|
||||
} else if (price == kPriceTabLimited) {
|
||||
return simple(tr::lng_gift_stars_tabs_limited(tr::now));
|
||||
} else if (price == kPriceTabInStock) {
|
||||
return simple(tr::lng_gift_stars_tabs_in_stock(tr::now));
|
||||
}
|
||||
auto &manager = session->data().customEmojiManager();
|
||||
auto result = Text::String();
|
||||
@@ -589,18 +599,33 @@ struct GiftPriceTabs {
|
||||
struct State {
|
||||
rpl::variable<std::vector<int>> prices;
|
||||
rpl::variable<int> priceTab = kPriceTabAll;
|
||||
rpl::variable<int> fullWidth;
|
||||
std::vector<Button> buttons;
|
||||
int dragx = 0;
|
||||
int pressx = 0;
|
||||
float64 dragscroll = 0.;
|
||||
float64 scroll = 0.;
|
||||
int scrollMax = 0;
|
||||
int selected = -1;
|
||||
int pressed = -1;
|
||||
int active = -1;
|
||||
};
|
||||
const auto state = raw->lifetime().make_state<State>();
|
||||
const auto scroll = [=] {
|
||||
return QPoint(int(base::SafeRound(state->scroll)), 0);
|
||||
};
|
||||
|
||||
state->prices = std::move(
|
||||
gifts
|
||||
) | rpl::map([](const std::vector<GiftTypeStars> &gifts) {
|
||||
auto result = std::vector<int>();
|
||||
result.push_back(kPriceTabAll);
|
||||
auto special = 1;
|
||||
auto same = true;
|
||||
auto sameKey = 0;
|
||||
auto hasNonSoldOut = false;
|
||||
auto hasSoldOut = false;
|
||||
auto hasLimited = false;
|
||||
for (const auto &gift : gifts) {
|
||||
if (same) {
|
||||
const auto key = gift.info.stars
|
||||
@@ -611,10 +636,13 @@ struct GiftPriceTabs {
|
||||
same = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (gift.info.limitedCount
|
||||
&& (result.size() < 2 || result[1] != kPriceTabLimited)) {
|
||||
result.insert(begin(result) + 1, kPriceTabLimited);
|
||||
if (IsSoldOut(gift.info)) {
|
||||
hasSoldOut = true;
|
||||
} else {
|
||||
hasNonSoldOut = true;
|
||||
}
|
||||
if (gift.info.limitedCount) {
|
||||
hasLimited = true;
|
||||
}
|
||||
if (!ranges::contains(result, gift.info.stars)) {
|
||||
result.push_back(gift.info.stars);
|
||||
@@ -623,6 +651,12 @@ struct GiftPriceTabs {
|
||||
if (same) {
|
||||
return std::vector<int>();
|
||||
}
|
||||
if (hasSoldOut && hasNonSoldOut) {
|
||||
result.insert(begin(result) + (special++), kPriceTabInStock);
|
||||
}
|
||||
if (hasLimited) {
|
||||
result.insert(begin(result) + (special++), kPriceTabLimited);
|
||||
}
|
||||
ranges::sort(begin(result) + 1, end(result));
|
||||
return result;
|
||||
});
|
||||
@@ -681,6 +715,9 @@ struct GiftPriceTabs {
|
||||
button.geometry = QRect(QPoint(x, y), r.size());
|
||||
x += r.width() + st::giftBoxTabSkip;
|
||||
}
|
||||
state->fullWidth = x
|
||||
- st::giftBoxTabSkip
|
||||
+ st::giftBoxTabsMargin.right();
|
||||
const auto height = state->buttons.empty()
|
||||
? 0
|
||||
: (y
|
||||
@@ -690,13 +727,35 @@ struct GiftPriceTabs {
|
||||
raw->update();
|
||||
}, raw->lifetime());
|
||||
|
||||
rpl::combine(
|
||||
raw->widthValue(),
|
||||
state->fullWidth.value()
|
||||
) | rpl::start_with_next([=](int outer, int inner) {
|
||||
state->scrollMax = std::max(0, inner - outer);
|
||||
}, raw->lifetime());
|
||||
|
||||
raw->setMouseTracking(true);
|
||||
raw->events() | rpl::start_with_next([=](not_null<QEvent*> e) {
|
||||
const auto type = e->type();
|
||||
switch (type) {
|
||||
case QEvent::Leave: setSelected(-1); break;
|
||||
case QEvent::MouseMove: {
|
||||
const auto position = static_cast<QMouseEvent*>(e.get())->pos();
|
||||
const auto me = static_cast<QMouseEvent*>(e.get());
|
||||
const auto mousex = me->pos().x();
|
||||
const auto drag = QApplication::startDragDistance();
|
||||
if (state->dragx > 0) {
|
||||
state->scroll = std::clamp(
|
||||
state->dragscroll + state->dragx - mousex,
|
||||
0.,
|
||||
state->scrollMax * 1.);
|
||||
raw->update();
|
||||
break;
|
||||
} else if (state->pressx > 0
|
||||
&& std::abs(state->pressx - mousex) > drag) {
|
||||
state->dragx = state->pressx;
|
||||
state->dragscroll = state->scroll;
|
||||
}
|
||||
const auto position = me->pos() + scroll();
|
||||
for (auto i = 0, c = int(state->buttons.size()); i != c; ++i) {
|
||||
if (state->buttons[i].geometry.contains(position)) {
|
||||
setSelected(i);
|
||||
@@ -704,17 +763,32 @@ struct GiftPriceTabs {
|
||||
}
|
||||
}
|
||||
} break;
|
||||
case QEvent::Wheel: {
|
||||
const auto me = static_cast<QWheelEvent*>(e.get());
|
||||
state->scroll = std::clamp(
|
||||
state->scroll - Ui::ScrollDeltaF(me).x(),
|
||||
0.,
|
||||
state->scrollMax * 1.);
|
||||
raw->update();
|
||||
} break;
|
||||
case QEvent::MouseButtonPress: {
|
||||
const auto me = static_cast<QMouseEvent*>(e.get());
|
||||
if (me->button() != Qt::LeftButton) {
|
||||
break;
|
||||
}
|
||||
const auto position = me->pos();
|
||||
for (auto i = 0, c = int(state->buttons.size()); i != c; ++i) {
|
||||
if (state->buttons[i].geometry.contains(position)) {
|
||||
setActive(i);
|
||||
break;
|
||||
}
|
||||
state->pressed = state->selected;
|
||||
state->pressx = me->pos().x();
|
||||
} break;
|
||||
case QEvent::MouseButtonRelease: {
|
||||
const auto me = static_cast<QMouseEvent*>(e.get());
|
||||
if (me->button() != Qt::LeftButton) {
|
||||
break;
|
||||
}
|
||||
const auto dragx = std::exchange(state->dragx, 0);
|
||||
const auto pressed = std::exchange(state->pressed, -1);
|
||||
state->pressx = 0;
|
||||
if (!dragx && pressed >= 0 && state->selected == pressed) {
|
||||
setActive(pressed);
|
||||
}
|
||||
} break;
|
||||
}
|
||||
@@ -724,8 +798,9 @@ struct GiftPriceTabs {
|
||||
auto p = QPainter(raw);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
const auto padding = st::giftBoxTabPadding;
|
||||
const auto shift = -scroll();
|
||||
for (const auto &button : state->buttons) {
|
||||
const auto geometry = button.geometry;
|
||||
const auto geometry = button.geometry.translated(shift);
|
||||
if (button.active) {
|
||||
p.setBrush(st::giftBoxTabBgActive);
|
||||
p.setPen(Qt::NoPen);
|
||||
@@ -740,6 +815,14 @@ struct GiftPriceTabs {
|
||||
.availableWidth = button.text.maxWidth(),
|
||||
});
|
||||
}
|
||||
{
|
||||
const auto &icon = st::defaultEmojiSuggestions;
|
||||
const auto w = icon.fadeRight.width();
|
||||
const auto &c = st::boxDividerBg->c;
|
||||
const auto r = QRect(0, 0, w, raw->height());
|
||||
icon.fadeRight.fill(p, r.translated(raw->width() - w, 0), c);
|
||||
icon.fadeLeft.fill(p, r, c);
|
||||
}
|
||||
}, raw->lifetime());
|
||||
|
||||
return {
|
||||
@@ -1128,9 +1211,7 @@ void SendGiftBox(
|
||||
|
||||
button->setClickedCallback([=] {
|
||||
const auto star = std::get_if<GiftTypeStars>(&descriptor);
|
||||
if (star
|
||||
&& star->info.limitedCount
|
||||
&& !star->info.limitedLeft) {
|
||||
if (star && IsSoldOut(star->info)) {
|
||||
window->show(Box(SoldOutBox, window, *star));
|
||||
} else {
|
||||
window->show(
|
||||
@@ -1235,6 +1316,8 @@ void AddBlock(
|
||||
gifts.erase(ranges::remove_if(gifts, [&](const GiftTypeStars &gift) {
|
||||
return (price == kPriceTabLimited)
|
||||
? (!gift.info.limitedCount)
|
||||
: (price == kPriceTabInStock)
|
||||
? IsSoldOut(gift.info)
|
||||
: (price && gift.info.stars != price);
|
||||
}), end(gifts));
|
||||
return GiftsDescriptor{
|
||||
@@ -1270,32 +1353,12 @@ void GiftBox(
|
||||
AddSkip(content);
|
||||
AddSkip(content);
|
||||
|
||||
{
|
||||
const auto widget = CreateChild<RpWidget>(content);
|
||||
using ColoredMiniStars = Premium::ColoredMiniStars;
|
||||
const auto stars = widget->lifetime().make_state<ColoredMiniStars>(
|
||||
widget,
|
||||
false,
|
||||
Premium::MiniStars::Type::BiStars);
|
||||
stars->setColorOverride(Premium::CreditsIconGradientStops());
|
||||
widget->resize(
|
||||
st::boxWidth - stUser.photoSize,
|
||||
stUser.photoSize * 2);
|
||||
content->sizeValue(
|
||||
) | rpl::start_with_next([=](const QSize &size) {
|
||||
widget->moveToLeft((size.width() - widget->width()) / 2, 0);
|
||||
const auto starsRect = Rect(widget->size());
|
||||
stars->setPosition(starsRect.topLeft());
|
||||
stars->setSize(starsRect.size());
|
||||
widget->lower();
|
||||
}, widget->lifetime());
|
||||
widget->paintRequest(
|
||||
) | rpl::start_with_next([=](const QRect &r) {
|
||||
auto p = QPainter(widget);
|
||||
p.fillRect(r, Qt::transparent);
|
||||
stars->paint(p);
|
||||
}, widget->lifetime());
|
||||
}
|
||||
Settings::AddMiniStars(
|
||||
content,
|
||||
Ui::CreateChild<Ui::RpWidget>(content),
|
||||
stUser.photoSize,
|
||||
box->width(),
|
||||
2.);
|
||||
AddSkip(content);
|
||||
AddSkip(box->verticalLayout());
|
||||
|
||||
|
||||
@@ -1612,7 +1612,7 @@ void Panel::initLayout() {
|
||||
#ifndef Q_OS_MAC
|
||||
_controls->wrap.raise();
|
||||
|
||||
Ui::Platform::TitleControlsLayoutChanged(
|
||||
_controls->controls.layout().changes(
|
||||
) | rpl::start_with_next([=] {
|
||||
// _menuToggle geometry depends on _controls arrangement.
|
||||
crl::on_main(widget(), [=] { updateControlsGeometry(); });
|
||||
|
||||
@@ -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 = 5009000;
|
||||
constexpr auto AppVersionStr = "5.9";
|
||||
constexpr auto AppBetaVersion = false;
|
||||
constexpr auto AppVersion = 5009002;
|
||||
constexpr auto AppVersionStr = "5.9.2";
|
||||
constexpr auto AppBetaVersion = true;
|
||||
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;
|
||||
|
||||
@@ -85,7 +85,7 @@ QByteArray RecentPeers::serialize() const {
|
||||
auto stream = Serialize::ByteArrayWriter(size);
|
||||
stream
|
||||
<< quint32(AppVersion)
|
||||
<< quint32(_list.size());
|
||||
<< quint32(count);
|
||||
for (const auto &peer : list) {
|
||||
Serialize::writePeer(stream, peer);
|
||||
}
|
||||
|
||||
@@ -156,10 +156,11 @@ void SponsoredMessages::inject(
|
||||
if (blockIt == end(history->blocks)) {
|
||||
return;
|
||||
}
|
||||
const auto messages = [&]() -> const std::vector<ViewPtr> & {
|
||||
const auto messages = [&]() -> const std::vector<ViewPtr>& {
|
||||
return (*blockIt)->messages;
|
||||
};
|
||||
auto lastViewIt = ranges::find(messages(), lastView, &ViewPtr::get);
|
||||
auto appendAtLeastToEnd = false;
|
||||
while ((summaryBetween < list.postsBetween)
|
||||
|| (summaryHeight < betweenHeight)) {
|
||||
lastViewIt++;
|
||||
@@ -168,6 +169,10 @@ void SponsoredMessages::inject(
|
||||
if (blockIt != end(history->blocks)) {
|
||||
lastViewIt = begin(messages());
|
||||
} else {
|
||||
if (!list.injectedCount) {
|
||||
appendAtLeastToEnd = true;
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -182,17 +187,24 @@ void SponsoredMessages::inject(
|
||||
entryIt->itemFullId = FullMsgId(
|
||||
history->peer->id,
|
||||
_session->data().nextLocalMessageId());
|
||||
const auto makedMessage = history->makeMessage(
|
||||
entryIt->itemFullId.msg,
|
||||
entryIt->sponsored.from,
|
||||
entryIt->sponsored.textWithEntities,
|
||||
(*lastViewIt)->data());
|
||||
entryIt->item.reset(makedMessage.get());
|
||||
history->addNewInTheMiddle(
|
||||
makedMessage.get(),
|
||||
std::distance(begin(history->blocks), blockIt),
|
||||
std::distance(begin(messages()), lastViewIt) + 1);
|
||||
messages().back().get()->setPendingResize();
|
||||
if (appendAtLeastToEnd) {
|
||||
entryIt->item.reset(history->addSponsoredMessage(
|
||||
entryIt->itemFullId.msg,
|
||||
entryIt->sponsored.from,
|
||||
entryIt->sponsored.textWithEntities));
|
||||
} else {
|
||||
const auto makedMessage = history->makeMessage(
|
||||
entryIt->itemFullId.msg,
|
||||
entryIt->sponsored.from,
|
||||
entryIt->sponsored.textWithEntities,
|
||||
(*lastViewIt)->data());
|
||||
entryIt->item.reset(makedMessage.get());
|
||||
history->addNewInTheMiddle(
|
||||
makedMessage.get(),
|
||||
std::distance(begin(history->blocks), blockIt),
|
||||
std::distance(begin(messages()), lastViewIt) + 1);
|
||||
messages().back().get()->setPendingResize();
|
||||
}
|
||||
list.injectedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,7 +264,7 @@ QByteArray TopPeers::serialize() const {
|
||||
stream
|
||||
<< quint32(AppVersion)
|
||||
<< quint32(_disabled ? 1 : 0)
|
||||
<< quint32(_list.size());
|
||||
<< quint32(count);
|
||||
for (const auto &top : list) {
|
||||
Serialize::writePeer(stream, top.peer);
|
||||
stream << SerializeRating(top.rating);
|
||||
|
||||
@@ -55,7 +55,8 @@ MTPInputReplyTo ReplyToForMTP(
|
||||
? replyingToTopic->rootId()
|
||||
: Data::ForumTopic::kGeneralId)
|
||||
: (to ? to->topicRootId() : Data::ForumTopic::kGeneralId);
|
||||
const auto replyToTopicId = to
|
||||
const auto replyToTopicId = (to
|
||||
&& (to->history() != history || to->id != replyingToTopicId))
|
||||
? to->topicRootId()
|
||||
: replyingToTopicId;
|
||||
const auto external = replyTo.messageId
|
||||
|
||||
@@ -86,7 +86,9 @@ constexpr auto SpecialMsgIdShift = EndStoryMsgId.bare;
|
||||
constexpr auto ShowAtTheEndMsgId = MsgId(SpecialMsgIdShift + 1);
|
||||
constexpr auto SwitchAtTopMsgId = MsgId(SpecialMsgIdShift + 2);
|
||||
constexpr auto ShowAndStartBotMsgId = MsgId(SpecialMsgIdShift + 4);
|
||||
constexpr auto ShowAndMaybeStartBotMsgId = MsgId(SpecialMsgIdShift + 5);
|
||||
constexpr auto ShowForChooseMessagesMsgId = MsgId(SpecialMsgIdShift + 6);
|
||||
constexpr auto kSearchQueryOffsetHint = -1;
|
||||
|
||||
static_assert(SpecialMsgIdShift + 0xFF < 0);
|
||||
static_assert(-(SpecialMsgIdShift + 0xFF) > ServerMaxMsgId);
|
||||
@@ -221,4 +223,13 @@ struct hash<FullStoryId> {
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct hash<FullMsgId> {
|
||||
size_t operator()(FullMsgId value) const {
|
||||
return QtPrivate::QHashCombine().operator()(
|
||||
std::hash<BareId>()(value.peer.value),
|
||||
value.msg.bare);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace std
|
||||
|
||||
@@ -26,6 +26,109 @@ constexpr auto kDefaultSearchTimeoutMs = crl::time(200);
|
||||
|
||||
} // namespace
|
||||
|
||||
MTPMessagesFilter PrepareSearchFilter(Storage::SharedMediaType type) {
|
||||
using Type = Storage::SharedMediaType;
|
||||
switch (type) {
|
||||
case Type::Photo:
|
||||
return MTP_inputMessagesFilterPhotos();
|
||||
case Type::Video:
|
||||
return MTP_inputMessagesFilterVideo();
|
||||
case Type::PhotoVideo:
|
||||
return MTP_inputMessagesFilterPhotoVideo();
|
||||
case Type::MusicFile:
|
||||
return MTP_inputMessagesFilterMusic();
|
||||
case Type::File:
|
||||
return MTP_inputMessagesFilterDocument();
|
||||
case Type::VoiceFile:
|
||||
return MTP_inputMessagesFilterVoice();
|
||||
case Type::RoundVoiceFile:
|
||||
return MTP_inputMessagesFilterRoundVoice();
|
||||
case Type::RoundFile:
|
||||
return MTP_inputMessagesFilterRoundVideo();
|
||||
case Type::GIF:
|
||||
return MTP_inputMessagesFilterGif();
|
||||
case Type::Link:
|
||||
return MTP_inputMessagesFilterUrl();
|
||||
case Type::ChatPhoto:
|
||||
return MTP_inputMessagesFilterChatPhotos();
|
||||
case Type::Pinned:
|
||||
return MTP_inputMessagesFilterPinned();
|
||||
}
|
||||
return MTP_inputMessagesFilterEmpty();
|
||||
}
|
||||
|
||||
std::optional<GlobalMediaRequest> PrepareGlobalMediaRequest(
|
||||
not_null<Main::Session*> session,
|
||||
int32 offsetRate,
|
||||
Data::MessagePosition offsetPosition,
|
||||
Storage::SharedMediaType type,
|
||||
const QString &query) {
|
||||
const auto filter = PrepareSearchFilter(type);
|
||||
if (query.isEmpty() && filter.type() == mtpc_inputMessagesFilterEmpty) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto minDate = 0;
|
||||
const auto maxDate = 0;
|
||||
const auto folderId = 0;
|
||||
const auto limit = offsetPosition.fullId.peer
|
||||
? kSharedMediaLimit
|
||||
: kFirstSharedMediaLimit;
|
||||
return MTPmessages_SearchGlobal(
|
||||
MTP_flags(MTPmessages_SearchGlobal::Flag::f_folder_id), // No archive
|
||||
MTP_int(folderId),
|
||||
MTP_string(query),
|
||||
filter,
|
||||
MTP_int(minDate),
|
||||
MTP_int(maxDate),
|
||||
MTP_int(offsetRate),
|
||||
(offsetPosition.fullId.peer
|
||||
? session->data().peer(PeerId(offsetPosition.fullId.peer))->input
|
||||
: MTP_inputPeerEmpty()),
|
||||
MTP_int(offsetPosition.fullId.msg),
|
||||
MTP_int(limit));
|
||||
}
|
||||
|
||||
GlobalMediaResult ParseGlobalMediaResult(
|
||||
not_null<Main::Session*> session,
|
||||
const MTPmessages_Messages &data) {
|
||||
auto result = GlobalMediaResult();
|
||||
|
||||
auto messages = (const QVector<MTPMessage>*)nullptr;
|
||||
data.match([&](const MTPDmessages_messagesNotModified &) {
|
||||
}, [&](const auto &data) {
|
||||
session->data().processUsers(data.vusers());
|
||||
session->data().processChats(data.vchats());
|
||||
messages = &data.vmessages().v;
|
||||
});
|
||||
data.match([&](const MTPDmessages_messagesNotModified &) {
|
||||
}, [&](const MTPDmessages_messages &data) {
|
||||
result.fullCount = data.vmessages().v.size();
|
||||
}, [&](const MTPDmessages_messagesSlice &data) {
|
||||
result.fullCount = data.vcount().v;
|
||||
result.offsetRate = data.vnext_rate().value_or_empty();
|
||||
}, [&](const MTPDmessages_channelMessages &data) {
|
||||
result.fullCount = data.vcount().v;
|
||||
});
|
||||
data.match([&](const MTPDmessages_channelMessages &data) {
|
||||
LOG(("API Error: received messages.channelMessages when "
|
||||
"no channel was passed! (ParseSearchResult)"));
|
||||
}, [](const auto &) {});
|
||||
|
||||
const auto addType = NewMessageType::Existing;
|
||||
result.messageIds.reserve(messages->size());
|
||||
for (const auto &message : *messages) {
|
||||
const auto item = session->data().addNewMessage(
|
||||
message,
|
||||
MessageFlags(),
|
||||
addType);
|
||||
if (item) {
|
||||
result.messageIds.push_back(item->position());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::optional<SearchRequest> PrepareSearchRequest(
|
||||
not_null<PeerData*> peer,
|
||||
MsgId topicRootId,
|
||||
@@ -33,36 +136,7 @@ std::optional<SearchRequest> PrepareSearchRequest(
|
||||
const QString &query,
|
||||
MsgId messageId,
|
||||
Data::LoadDirection direction) {
|
||||
const auto filter = [&] {
|
||||
using Type = Storage::SharedMediaType;
|
||||
switch (type) {
|
||||
case Type::Photo:
|
||||
return MTP_inputMessagesFilterPhotos();
|
||||
case Type::Video:
|
||||
return MTP_inputMessagesFilterVideo();
|
||||
case Type::PhotoVideo:
|
||||
return MTP_inputMessagesFilterPhotoVideo();
|
||||
case Type::MusicFile:
|
||||
return MTP_inputMessagesFilterMusic();
|
||||
case Type::File:
|
||||
return MTP_inputMessagesFilterDocument();
|
||||
case Type::VoiceFile:
|
||||
return MTP_inputMessagesFilterVoice();
|
||||
case Type::RoundVoiceFile:
|
||||
return MTP_inputMessagesFilterRoundVoice();
|
||||
case Type::RoundFile:
|
||||
return MTP_inputMessagesFilterRoundVideo();
|
||||
case Type::GIF:
|
||||
return MTP_inputMessagesFilterGif();
|
||||
case Type::Link:
|
||||
return MTP_inputMessagesFilterUrl();
|
||||
case Type::ChatPhoto:
|
||||
return MTP_inputMessagesFilterChatPhotos();
|
||||
case Type::Pinned:
|
||||
return MTP_inputMessagesFilterPinned();
|
||||
}
|
||||
return MTP_inputMessagesFilterEmpty();
|
||||
}();
|
||||
const auto filter = PrepareSearchFilter(type);
|
||||
if (query.isEmpty() && filter.type() == mtpc_inputMessagesFilterEmpty) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ class Session;
|
||||
|
||||
namespace Data {
|
||||
enum class LoadDirection : char;
|
||||
struct MessagePosition;
|
||||
} // namespace Data
|
||||
|
||||
namespace Api {
|
||||
@@ -36,6 +37,27 @@ using HistoryResult = SearchResult;
|
||||
using HistoryRequest = MTPmessages_GetHistory;
|
||||
using HistoryRequestResult = MTPmessages_Messages;
|
||||
|
||||
using GlobalMediaRequest = MTPmessages_SearchGlobal;
|
||||
struct GlobalMediaResult {
|
||||
std::vector<Data::MessagePosition> messageIds;
|
||||
int32 offsetRate = 0;
|
||||
int fullCount = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] MTPMessagesFilter PrepareSearchFilter(
|
||||
Storage::SharedMediaType type);
|
||||
|
||||
[[nodiscard]] std::optional<GlobalMediaRequest> PrepareGlobalMediaRequest(
|
||||
not_null<Main::Session*> session,
|
||||
int32 offsetRate,
|
||||
Data::MessagePosition offsetPosition,
|
||||
Storage::SharedMediaType type,
|
||||
const QString &query);
|
||||
|
||||
[[nodiscard]] GlobalMediaResult ParseGlobalMediaResult(
|
||||
not_null<Main::Session*> session,
|
||||
const MTPmessages_Messages &data);
|
||||
|
||||
[[nodiscard]] std::optional<SearchRequest> PrepareSearchRequest(
|
||||
not_null<PeerData*> peer,
|
||||
MsgId topicRootId,
|
||||
|
||||
@@ -36,6 +36,8 @@ using Options = base::flags<Option>;
|
||||
|
||||
namespace Data {
|
||||
|
||||
struct FileOrigin;
|
||||
|
||||
struct UploadState {
|
||||
explicit UploadState(int64 size) : size(size) {
|
||||
}
|
||||
@@ -58,8 +60,6 @@ constexpr auto kVoiceMessageCacheTag = uint8(0x03);
|
||||
constexpr auto kVideoMessageCacheTag = uint8(0x04);
|
||||
constexpr auto kAnimationCacheTag = uint8(0x05);
|
||||
|
||||
struct FileOrigin;
|
||||
|
||||
} // namespace Data
|
||||
|
||||
struct MessageGroupId {
|
||||
@@ -342,3 +342,29 @@ enum class MediaWebPageFlag : uint8 {
|
||||
};
|
||||
inline constexpr bool is_flag_type(MediaWebPageFlag) { return true; }
|
||||
using MediaWebPageFlags = base::flags<MediaWebPageFlag>;
|
||||
|
||||
namespace Data {
|
||||
|
||||
enum class ForwardOptions {
|
||||
PreserveInfo,
|
||||
NoSenderNames,
|
||||
NoNamesAndCaptions,
|
||||
};
|
||||
|
||||
struct ForwardDraft {
|
||||
MessageIdsList ids;
|
||||
ForwardOptions options = ForwardOptions::PreserveInfo;
|
||||
|
||||
friend inline auto operator<=>(
|
||||
const ForwardDraft&,
|
||||
const ForwardDraft&) = default;
|
||||
};
|
||||
|
||||
using ForwardDrafts = base::flat_map<MsgId, ForwardDraft>;
|
||||
|
||||
struct ResolvedForwardDraft {
|
||||
HistoryItemsList items;
|
||||
ForwardOptions options = ForwardOptions::PreserveInfo;
|
||||
};
|
||||
|
||||
} // namespace Data
|
||||
|
||||
@@ -689,7 +689,11 @@ recentPeersSpecialName: PeerListItem(recentPeersItem) {
|
||||
namePosition: point(64px, 19px);
|
||||
}
|
||||
|
||||
dialogsTabsScroll: ScrollArea(defaultScrollArea) {
|
||||
barHidden: true;
|
||||
}
|
||||
dialogsSearchTabs: SettingsSlider(defaultSettingsSlider) {
|
||||
padding: 8px;
|
||||
height: 33px;
|
||||
barTop: 30px;
|
||||
barSkip: 0px;
|
||||
@@ -707,7 +711,6 @@ dialogsSearchTabs: SettingsSlider(defaultSettingsSlider) {
|
||||
rippleBgActive: lightButtonBgOver;
|
||||
ripple: defaultRippleAnimation;
|
||||
}
|
||||
dialogsSearchTabsPadding: 8px;
|
||||
|
||||
chatsFiltersTabs: SettingsSlider(dialogsSearchTabs) {
|
||||
rippleBottomSkip: 0px;
|
||||
|
||||
@@ -130,7 +130,9 @@ constexpr auto kPreviewPostsLimit = 3;
|
||||
if (const auto history = row->key().history()) {
|
||||
if (const auto user = history->peer->asUser()) {
|
||||
if (user->botInfo && user->botInfo->hasMainApp) {
|
||||
return user;
|
||||
if (!history->unreadCount() && !history->unreadMark()) {
|
||||
return user;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1947,7 +1949,10 @@ const std::vector<Key> &InnerWidget::pinnedChatsOrder() const {
|
||||
}
|
||||
|
||||
void InnerWidget::checkReorderPinnedStart(QPoint localPosition) {
|
||||
if (!_pressed || _dragging || _state != WidgetState::Default) {
|
||||
if (!_pressed
|
||||
|| _dragging
|
||||
|| (_state != WidgetState::Default)
|
||||
|| _pressedBotApp) {
|
||||
return;
|
||||
} else if (qAbs(localPosition.y() - _dragStart.y())
|
||||
< style::ConvertScale(kStartReorderThreshold)) {
|
||||
|
||||
@@ -121,8 +121,12 @@ struct EntryState {
|
||||
FilterId filterId = 0;
|
||||
FullReplyTo currentReplyTo;
|
||||
|
||||
friend inline auto operator<=>(EntryState, EntryState) noexcept
|
||||
= default;
|
||||
friend inline auto operator<=>(
|
||||
const EntryState&,
|
||||
const EntryState&) = default;
|
||||
friend inline bool operator==(
|
||||
const EntryState&,
|
||||
const EntryState&) = default;
|
||||
};
|
||||
|
||||
struct SearchState {
|
||||
|
||||
@@ -696,16 +696,19 @@ void Widget::chosenRow(const ChosenRow &row) {
|
||||
}
|
||||
return;
|
||||
} else if (const auto topic = row.key.topic()) {
|
||||
auto params = Window::SectionShow(
|
||||
Window::SectionShow::Way::ClearStack);
|
||||
params.highlightPart.text = _searchState.query;
|
||||
if (!params.highlightPart.empty()) {
|
||||
params.highlightPartOffsetHint = kSearchQueryOffsetHint;
|
||||
}
|
||||
if (row.newWindow) {
|
||||
controller()->showInNewWindow(
|
||||
Window::SeparateId(topic),
|
||||
row.message.fullId.msg);
|
||||
} else {
|
||||
session().data().saveViewAsMessages(topic->forum(), false);
|
||||
controller()->showThread(
|
||||
topic,
|
||||
row.message.fullId.msg,
|
||||
Window::SectionShow::Way::ClearStack);
|
||||
controller()->showThread(topic, row.message.fullId.msg, params);
|
||||
}
|
||||
} else if (history
|
||||
&& row.userpicClick
|
||||
@@ -742,13 +745,16 @@ void Widget::chosenRow(const ChosenRow &row) {
|
||||
const auto showAtMsgId = controller()->uniqueChatsInSearchResults()
|
||||
? ShowAtUnreadMsgId
|
||||
: row.message.fullId.msg;
|
||||
auto params = Window::SectionShow(
|
||||
Window::SectionShow::Way::ClearStack);
|
||||
params.highlightPart.text = _searchState.query;
|
||||
if (!params.highlightPart.empty()) {
|
||||
params.highlightPartOffsetHint = kSearchQueryOffsetHint;
|
||||
}
|
||||
if (row.newWindow) {
|
||||
controller()->showInNewWindow(peer, showAtMsgId);
|
||||
} else {
|
||||
controller()->showThread(
|
||||
history,
|
||||
showAtMsgId,
|
||||
Window::SectionShow::Way::ClearStack);
|
||||
controller()->showThread(history, showAtMsgId, params);
|
||||
hideChildList();
|
||||
}
|
||||
} else if (const auto folder = row.key.folder()) {
|
||||
@@ -1421,6 +1427,9 @@ void Widget::updateSuggestions(anim::type animated) {
|
||||
controller(),
|
||||
TopPeersContent(&session()),
|
||||
RecentPeersContent(&session()));
|
||||
_suggestions->clearSearchQueryRequests() | rpl::start_with_next([=] {
|
||||
setSearchQuery(QString());
|
||||
}, _suggestions->lifetime());
|
||||
_searchSuggestionsLocked = false;
|
||||
|
||||
rpl::merge(
|
||||
@@ -2928,7 +2937,11 @@ void Widget::updateCancelSearch() {
|
||||
|
||||
QString Widget::validateSearchQuery() {
|
||||
const auto query = currentSearchQuery();
|
||||
if (_searchState.tab == ChatSearchTab::PublicPosts) {
|
||||
if (!_subsectionTopBar
|
||||
&& _suggestions
|
||||
&& _suggestions->consumeSearchQuery(query)) {
|
||||
return QString();
|
||||
} else if (_searchState.tab == ChatSearchTab::PublicPosts) {
|
||||
if (_searchHashOrCashtag == HashOrCashtag::None) {
|
||||
_searchHashOrCashtag = HashOrCashtag::Hashtag;
|
||||
}
|
||||
@@ -3265,6 +3278,9 @@ bool Widget::applySearchState(SearchState state) {
|
||||
_openedForum && _searchState.inChat);
|
||||
}
|
||||
if (!_searchState.inChat && _searchState.query.isEmpty()) {
|
||||
if (!_widthAnimationCache.isNull()) {
|
||||
stopWidthAnimation();
|
||||
}
|
||||
setInnerFocus();
|
||||
} else if (!_subsectionTopBar) {
|
||||
_search->setFocus();
|
||||
@@ -3923,9 +3939,18 @@ void Widget::setSearchQuery(const QString &query, int cursorPosition) {
|
||||
}
|
||||
|
||||
bool Widget::cancelSearch(CancelSearchOptions options) {
|
||||
const auto clearingSuggestionsQuery = _suggestions
|
||||
&& _suggestions->consumeSearchQuery(QString());
|
||||
if (clearingSuggestionsQuery) {
|
||||
setSearchQuery(QString());
|
||||
if (!options.forceFullCancel) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
cancelSearchRequest();
|
||||
auto updatedState = _searchState;
|
||||
const auto clearingQuery = !updatedState.query.isEmpty();
|
||||
const auto clearingQuery = clearingSuggestionsQuery
|
||||
|| !updatedState.query.isEmpty();
|
||||
const auto forceFullCancel = options.forceFullCancel;
|
||||
auto clearingInChat = (forceFullCancel || !clearingQuery)
|
||||
&& (updatedState.inChat
|
||||
|
||||
@@ -23,10 +23,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "data/data_user.h"
|
||||
#include "dialogs/ui/chat_search_empty.h"
|
||||
#include "history/history.h"
|
||||
#include "info/downloads/info_downloads_widget.h"
|
||||
#include "info/media/info_media_widget.h"
|
||||
#include "info/info_controller.h"
|
||||
#include "info/info_memento.h"
|
||||
#include "info/info_wrap_widget.h"
|
||||
#include "inline_bots/bot_attach_web_view.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "main/main_session.h"
|
||||
#include "settings/settings_common.h"
|
||||
#include "storage/storage_shared_media.h"
|
||||
#include "ui/boxes/confirm_box.h"
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
@@ -42,6 +48,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "ui/delayed_activation.h"
|
||||
#include "ui/dynamic_thumbnails.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/search_field_controller.h"
|
||||
#include "ui/unread_badge_paint.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "window/window_separate_id.h"
|
||||
@@ -60,6 +67,7 @@ constexpr auto kCollapsedChannelsCount = 5;
|
||||
constexpr auto kProbablyMaxChannels = 1000;
|
||||
constexpr auto kCollapsedAppsCount = 5;
|
||||
constexpr auto kProbablyMaxApps = 100;
|
||||
constexpr auto kSearchQueryDelay = crl::time(900);
|
||||
|
||||
class RecentRow final : public PeerListRow {
|
||||
public:
|
||||
@@ -299,6 +307,7 @@ void RecentRow::rightActionPaint(
|
||||
? st::activeButtonBgOver
|
||||
: st::activeButtonBg);
|
||||
const auto radius = size.height() / 2;
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.drawRoundedRect(QRect(QPoint(x, y), size), radius, radius);
|
||||
if (_actionRipple) {
|
||||
_actionRipple->paint(p, x, y, outerWidth);
|
||||
@@ -1291,7 +1300,23 @@ Suggestions::Suggestions(
|
||||
RecentPeersList recentPeers)
|
||||
: RpWidget(parent)
|
||||
, _controller(controller)
|
||||
, _tabs(std::make_unique<Ui::SettingsSlider>(this, st::dialogsSearchTabs))
|
||||
, _tabsScroll(
|
||||
std::make_unique<Ui::ScrollArea>(this, st::dialogsTabsScroll, true))
|
||||
, _tabs(
|
||||
_tabsScroll->setOwnedWidget(
|
||||
object_ptr<Ui::SettingsSlider>(this, st::dialogsSearchTabs)))
|
||||
, _tabKeys{
|
||||
{ Tab::Chats },
|
||||
{ Tab::Channels },
|
||||
{ Tab::Apps },
|
||||
{ Tab::Media, MediaType::Photo },
|
||||
{ Tab::Media, MediaType::Video },
|
||||
{ Tab::Downloads },
|
||||
{ Tab::Media, MediaType::Link },
|
||||
{ Tab::Media, MediaType::File },
|
||||
{ Tab::Media, MediaType::MusicFile },
|
||||
{ Tab::Media, MediaType::RoundVoiceFile },
|
||||
}
|
||||
, _chatsScroll(std::make_unique<Ui::ElasticScroll>(this))
|
||||
, _chatsContent(
|
||||
_chatsScroll->setOwnedWidget(object_ptr<Ui::VerticalLayout>(this)))
|
||||
@@ -1312,8 +1337,8 @@ Suggestions::Suggestions(
|
||||
, _appsContent(
|
||||
_appsScroll->setOwnedWidget(object_ptr<Ui::VerticalLayout>(this)))
|
||||
, _recentApps(setupRecentApps())
|
||||
, _popularApps(setupPopularApps()) {
|
||||
|
||||
, _popularApps(setupPopularApps())
|
||||
, _searchQueryTimer([=] { applySearchQuery(); }) {
|
||||
setupTabs();
|
||||
setupChats();
|
||||
setupChannels();
|
||||
@@ -1323,10 +1348,46 @@ Suggestions::Suggestions(
|
||||
Suggestions::~Suggestions() = default;
|
||||
|
||||
void Suggestions::setupTabs() {
|
||||
_tabsScroll->setCustomWheelProcess([=](not_null<QWheelEvent*> e) {
|
||||
const auto pixelDelta = e->pixelDelta();
|
||||
const auto angleDelta = e->angleDelta();
|
||||
if (std::abs(pixelDelta.x()) + std::abs(angleDelta.x())) {
|
||||
return false;
|
||||
}
|
||||
const auto y = pixelDelta.y() ? pixelDelta.y() : angleDelta.y();
|
||||
_tabsScroll->scrollToX(_tabsScroll->scrollLeft() - y);
|
||||
return true;
|
||||
});
|
||||
|
||||
const auto scrollToIndex = [=](int index, anim::type type) {
|
||||
const auto to = index
|
||||
? (_tabs->centerOfSection(index) - _tabsScroll->width() / 2)
|
||||
: 0;
|
||||
_tabsScrollAnimation.stop();
|
||||
if (type == anim::type::instant) {
|
||||
_tabsScroll->scrollToX(to);
|
||||
} else {
|
||||
_tabsScrollAnimation.start(
|
||||
[=](float64 v) { _tabsScroll->scrollToX(v); },
|
||||
_tabsScroll->scrollLeft(),
|
||||
std::min(to, _tabsScroll->scrollLeftMax()),
|
||||
st::defaultTabsSlider.duration);
|
||||
}
|
||||
};
|
||||
rpl::single(-1) | rpl::then(
|
||||
_tabs->sectionActivated()
|
||||
) | rpl::combine_previous(
|
||||
) | rpl::start_with_next([=](int was, int index) {
|
||||
if (was != index) {
|
||||
scrollToIndex(index, anim::type::normal);
|
||||
}
|
||||
}, _tabs->lifetime());
|
||||
|
||||
const auto shadow = Ui::CreateChild<Ui::PlainShadow>(this);
|
||||
shadow->lower();
|
||||
|
||||
_tabs->move(st::dialogsSearchTabsPadding, 0);
|
||||
_tabsScroll->move(0, 0);
|
||||
_tabs->move(0, 0);
|
||||
rpl::combine(
|
||||
widthValue(),
|
||||
_tabs->heightValue()
|
||||
@@ -1335,20 +1396,37 @@ void Suggestions::setupTabs() {
|
||||
shadow->setGeometry(0, height - line, width, line);
|
||||
}, shadow->lifetime());
|
||||
|
||||
shadow->showOn(_tabs->shownValue());
|
||||
shadow->showOn(_tabsScroll->shownValue());
|
||||
|
||||
_tabs->setSections({
|
||||
tr::lng_recent_chats(tr::now),
|
||||
tr::lng_recent_channels(tr::now),
|
||||
tr::lng_recent_apps(tr::now),
|
||||
});
|
||||
const auto labels = base::flat_map<Key, QString>{
|
||||
{ Key{ Tab::Chats }, tr::lng_recent_chats(tr::now) },
|
||||
{ Key{ Tab::Channels }, tr::lng_recent_channels(tr::now) },
|
||||
{ Key{ Tab::Apps }, tr::lng_recent_apps(tr::now) },
|
||||
{ Key{ Tab::Media, MediaType::Photo }, tr::lng_all_photos(tr::now) },
|
||||
{ Key{ Tab::Media, MediaType::Video }, tr::lng_all_videos(tr::now) },
|
||||
{ Key{ Tab::Downloads }, tr::lng_all_downloads(tr::now) },
|
||||
{ Key{ Tab::Media, MediaType::Link }, tr::lng_all_links(tr::now) },
|
||||
{ Key{ Tab::Media, MediaType::File }, tr::lng_all_files(tr::now) },
|
||||
{
|
||||
Key{ Tab::Media, MediaType::MusicFile },
|
||||
tr::lng_all_music(tr::now),
|
||||
},
|
||||
{
|
||||
Key{ Tab::Media, MediaType::RoundVoiceFile },
|
||||
tr::lng_all_voice(tr::now),
|
||||
},
|
||||
};
|
||||
auto sections = std::vector<QString>();
|
||||
for (const auto key : _tabKeys) {
|
||||
const auto i = labels.find(key);
|
||||
Assert(i != end(labels));
|
||||
sections.push_back(i->second);
|
||||
}
|
||||
_tabs->setSections(sections);
|
||||
_tabs->sectionActivated(
|
||||
) | rpl::start_with_next([=](int section) {
|
||||
switchTab(section == 2
|
||||
? Tab::Apps
|
||||
: section
|
||||
? Tab::Channels
|
||||
: Tab::Chats);
|
||||
Assert(section >= 0 && section < _tabKeys.size());
|
||||
switchTab(_tabKeys[section]);
|
||||
}, _tabs->lifetime());
|
||||
}
|
||||
|
||||
@@ -1418,7 +1496,7 @@ void Suggestions::setupChats() {
|
||||
_chatsScroll->viewportEvent(e);
|
||||
}, _topPeers->lifetime());
|
||||
|
||||
_chatsScroll->setVisible(_tab.current() == Tab::Chats);
|
||||
_chatsScroll->setVisible(_key.current().tab == Tab::Chats);
|
||||
_chatsScroll->setCustomTouchProcess(_recent->processTouch);
|
||||
}
|
||||
|
||||
@@ -1452,7 +1530,7 @@ void Suggestions::setupChannels() {
|
||||
rpl::mappers::_1 + rpl::mappers::_2 == 0),
|
||||
anim::type::instant);
|
||||
|
||||
_channelsScroll->setVisible(_tab.current() == Tab::Channels);
|
||||
_channelsScroll->setVisible(_key.current().tab == Tab::Channels);
|
||||
_channelsScroll->setCustomTouchProcess([=](not_null<QTouchEvent*> e) {
|
||||
const auto myChannels = _myChannels->processTouch(e);
|
||||
const auto recommendations = _recommendations->processTouch(e);
|
||||
@@ -1469,7 +1547,7 @@ void Suggestions::setupApps() {
|
||||
_popularApps->wrap->toggle(count > 0, anim::type::instant);
|
||||
}, _popularApps->wrap->lifetime());
|
||||
|
||||
_appsScroll->setVisible(_tab.current() == Tab::Apps);
|
||||
_appsScroll->setVisible(_key.current().tab == Tab::Apps);
|
||||
_appsScroll->setCustomTouchProcess([=](not_null<QTouchEvent*> e) {
|
||||
const auto recentApps = _recentApps->processTouch(e);
|
||||
const auto popularApps = _popularApps->processTouch(e);
|
||||
@@ -1478,12 +1556,11 @@ void Suggestions::setupApps() {
|
||||
}
|
||||
|
||||
void Suggestions::selectJump(Qt::Key direction, int pageSize) {
|
||||
switch (_tab.current()) {
|
||||
switch (_key.current().tab) {
|
||||
case Tab::Chats: selectJumpChats(direction, pageSize); return;
|
||||
case Tab::Channels: selectJumpChannels(direction, pageSize); return;
|
||||
case Tab::Apps: selectJumpApps(direction, pageSize); return;
|
||||
}
|
||||
Unexpected("Tab in Suggestions::selectJump.");
|
||||
}
|
||||
|
||||
void Suggestions::selectJumpChats(Qt::Key direction, int pageSize) {
|
||||
@@ -1661,7 +1738,7 @@ void Suggestions::selectJumpApps(Qt::Key direction, int pageSize) {
|
||||
}
|
||||
|
||||
void Suggestions::chooseRow() {
|
||||
switch (_tab.current()) {
|
||||
switch (_key.current().tab) {
|
||||
case Tab::Chats:
|
||||
if (!_topPeers->chooseRow()) {
|
||||
_recent->choose();
|
||||
@@ -1680,10 +1757,49 @@ void Suggestions::chooseRow() {
|
||||
}
|
||||
}
|
||||
|
||||
bool Suggestions::consumeSearchQuery(const QString &query) {
|
||||
using Type = MediaType;
|
||||
const auto key = _key.current();
|
||||
const auto tab = key.tab;
|
||||
const auto type = (key.tab == Tab::Media) ? key.mediaType : Type::kCount;
|
||||
if (tab != Tab::Downloads
|
||||
&& type != Type::File
|
||||
&& type != Type::Link
|
||||
&& type != Type::MusicFile) {
|
||||
return false;
|
||||
} else if (_searchQuery == query) {
|
||||
return false;
|
||||
}
|
||||
_searchQuery = query;
|
||||
_persist = !_searchQuery.isEmpty();
|
||||
if (query.isEmpty() || tab == Tab::Downloads) {
|
||||
_searchQueryTimer.cancel();
|
||||
applySearchQuery();
|
||||
} else {
|
||||
_searchQueryTimer.callOnce(kSearchQueryDelay);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void Suggestions::applySearchQuery() {
|
||||
const auto key = _key.current();
|
||||
const auto controller = _mediaLists[key].wrap->controller();
|
||||
const auto search = controller->searchFieldController();
|
||||
if (search->query() != _searchQuery) {
|
||||
search->setQuery(_searchQuery);
|
||||
}
|
||||
}
|
||||
|
||||
rpl::producer<> Suggestions::clearSearchQueryRequests() const {
|
||||
return _clearSearchQueryRequests.events();
|
||||
}
|
||||
|
||||
Data::Thread *Suggestions::updateFromParentDrag(QPoint globalPosition) {
|
||||
return (_tab.current() == Tab::Chats)
|
||||
? updateFromChatsDrag(globalPosition)
|
||||
: updateFromChannelsDrag(globalPosition);
|
||||
switch (_key.current().tab) {
|
||||
case Tab::Chats: return updateFromChatsDrag(globalPosition);
|
||||
case Tab::Channels: return updateFromChannelsDrag(globalPosition);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Data::Thread *Suggestions::updateFromChatsDrag(QPoint globalPosition) {
|
||||
@@ -1744,39 +1860,71 @@ void Suggestions::hide(anim::type animated, Fn<void()> finish) {
|
||||
}
|
||||
}
|
||||
|
||||
void Suggestions::switchTab(Tab tab) {
|
||||
const auto was = _tab.current();
|
||||
if (was == tab) {
|
||||
void Suggestions::switchTab(Key key) {
|
||||
const auto was = _key.current();
|
||||
if (was == key) {
|
||||
return;
|
||||
}
|
||||
_tab = tab;
|
||||
consumeSearchQuery(QString());
|
||||
_key = key;
|
||||
_persist = false;
|
||||
_clearSearchQueryRequests.fire({});
|
||||
if (_tabs->isHidden()) {
|
||||
return;
|
||||
}
|
||||
startSlideAnimation(was, tab);
|
||||
startSlideAnimation(was, key);
|
||||
}
|
||||
|
||||
void Suggestions::startSlideAnimation(Tab was, Tab now) {
|
||||
if (!_slideAnimation.animating()) {
|
||||
_slideLeft = (was == Tab::Chats || now == Tab::Chats)
|
||||
? Ui::GrabWidget(_chatsScroll.get())
|
||||
: Ui::GrabWidget(_channelsScroll.get());
|
||||
_slideLeftTop = (was == Tab::Chats || now == Tab::Chats)
|
||||
? _chatsScroll->y()
|
||||
: _channelsScroll->y();
|
||||
_slideRight = (was == Tab::Apps || now == Tab::Apps)
|
||||
? Ui::GrabWidget(_appsScroll.get())
|
||||
: Ui::GrabWidget(_channelsScroll.get());
|
||||
_slideRightTop = (was == Tab::Apps || now == Tab::Apps)
|
||||
? _appsScroll->y()
|
||||
: _channelsScroll->y();
|
||||
_chatsScroll->hide();
|
||||
_channelsScroll->hide();
|
||||
_appsScroll->hide();
|
||||
void Suggestions::ensureContent(Key key) {
|
||||
if (key.tab != Tab::Downloads && key.tab != Tab::Media) {
|
||||
return;
|
||||
}
|
||||
const auto from = (now > was) ? 0. : 1.;
|
||||
const auto to = (now > was) ? 1. : 0.;
|
||||
auto &list = _mediaLists[key];
|
||||
if (list.wrap) {
|
||||
return;
|
||||
}
|
||||
const auto self = _controller->session().user();
|
||||
const auto memento = (key.tab == Tab::Downloads)
|
||||
? Info::Downloads::Make(self)
|
||||
: std::make_shared<Info::Memento>(
|
||||
self,
|
||||
Info::Section(key.mediaType, Info::Section::Type::GlobalMedia));
|
||||
list.wrap = Ui::CreateChild<Info::WrapWidget>(
|
||||
this,
|
||||
_controller,
|
||||
Info::Wrap::Search,
|
||||
memento.get());
|
||||
list.wrap->show();
|
||||
updateControlsGeometry();
|
||||
}
|
||||
|
||||
void Suggestions::startSlideAnimation(Key was, Key now) {
|
||||
ensureContent(now);
|
||||
const auto wasIndex = ranges::find(_tabKeys, was);
|
||||
const auto nowIndex = ranges::find(_tabKeys, now);
|
||||
if (!_slideAnimation.animating()) {
|
||||
const auto find = [&](Key key) -> not_null<QWidget*> {
|
||||
switch (key.tab) {
|
||||
case Tab::Chats: return _chatsScroll.get();
|
||||
case Tab::Channels: return _channelsScroll.get();
|
||||
case Tab::Apps: return _appsScroll.get();
|
||||
}
|
||||
return _mediaLists[key].wrap;
|
||||
};
|
||||
auto left = find(was);
|
||||
auto right = find(now);
|
||||
if (wasIndex > nowIndex) {
|
||||
std::swap(left, right);
|
||||
}
|
||||
_slideLeft = Ui::GrabWidget(left);
|
||||
_slideLeftTop = left->y();
|
||||
_slideRight = Ui::GrabWidget(right);
|
||||
_slideRightTop = right->y();
|
||||
left->hide();
|
||||
right->hide();
|
||||
}
|
||||
const auto from = (nowIndex > wasIndex) ? 0. : 1.;
|
||||
const auto to = (nowIndex > wasIndex) ? 1. : 0.;
|
||||
_slideAnimation.start([=] {
|
||||
update();
|
||||
if (!_slideAnimation.animating() && !_shownAnimation.animating()) {
|
||||
@@ -1807,10 +1955,13 @@ void Suggestions::startShownAnimation(bool shown, Fn<void()> finish) {
|
||||
resize(now, height());
|
||||
}
|
||||
}
|
||||
_tabs->hide();
|
||||
_tabsScroll->hide();
|
||||
_chatsScroll->hide();
|
||||
_channelsScroll->hide();
|
||||
_appsScroll->hide();
|
||||
for (const auto &[key, list] : _mediaLists) {
|
||||
list.wrap->hide();
|
||||
}
|
||||
_slideAnimation.stop();
|
||||
}
|
||||
|
||||
@@ -1822,11 +1973,14 @@ void Suggestions::finishShow() {
|
||||
_shownAnimation.stop();
|
||||
_cache = QPixmap();
|
||||
|
||||
_tabs->show();
|
||||
const auto tab = _tab.current();
|
||||
_chatsScroll->setVisible(tab == Tab::Chats);
|
||||
_channelsScroll->setVisible(tab == Tab::Channels);
|
||||
_appsScroll->setVisible(tab == Tab::Apps);
|
||||
_tabsScroll->show();
|
||||
const auto key = _key.current();
|
||||
_chatsScroll->setVisible(key == Key{ Tab::Chats });
|
||||
_channelsScroll->setVisible(key == Key{ Tab::Channels });
|
||||
_appsScroll->setVisible(key == Key{ Tab::Apps });
|
||||
for (const auto &[mediaKey, list] : _mediaLists) {
|
||||
list.wrap->setVisible(key == mediaKey);
|
||||
}
|
||||
}
|
||||
|
||||
float64 Suggestions::shownOpacity() const {
|
||||
@@ -1846,7 +2000,7 @@ void Suggestions::paintEvent(QPaintEvent *e) {
|
||||
p.drawPixmap(0, (opacity - 1.) * slide, _cache);
|
||||
} else if (!_slideLeft.isNull()) {
|
||||
const auto slide = st::topPeers.height + st::searchedBarHeight;
|
||||
const auto right = (_tab.current() == Tab::Channels);
|
||||
const auto right = (_key.current().tab == Tab::Channels);
|
||||
const auto progress = _slideAnimation.value(right ? 1. : 0.);
|
||||
p.setOpacity(1. - progress);
|
||||
p.drawPixmap(
|
||||
@@ -1862,18 +2016,39 @@ void Suggestions::paintEvent(QPaintEvent *e) {
|
||||
}
|
||||
|
||||
void Suggestions::resizeEvent(QResizeEvent *e) {
|
||||
const auto w = std::max(width(), st::columnMinimalWidthLeft);
|
||||
_tabs->resizeToWidth(w);
|
||||
const auto tabs = _tabs->height();
|
||||
updateControlsGeometry();
|
||||
}
|
||||
|
||||
_chatsScroll->setGeometry(0, tabs, w, height() - tabs);
|
||||
void Suggestions::updateControlsGeometry() {
|
||||
const auto w = std::max(width(), st::columnMinimalWidthLeft);
|
||||
_tabs->fitWidthToSections();
|
||||
|
||||
const auto tabs = _tabs->height();
|
||||
_tabsScroll->setGeometry(0, 0, w, tabs);
|
||||
|
||||
const auto content = QRect(0, tabs, w, height() - tabs);
|
||||
|
||||
_chatsScroll->setGeometry(content);
|
||||
_chatsContent->resizeToWidth(w);
|
||||
|
||||
_channelsScroll->setGeometry(0, tabs, w, height() - tabs);
|
||||
_channelsScroll->setGeometry(content);
|
||||
_channelsContent->resizeToWidth(w);
|
||||
|
||||
_appsScroll->setGeometry(0, tabs, w, height() - tabs);
|
||||
_appsScroll->setGeometry(content);
|
||||
_appsContent->resizeToWidth(w);
|
||||
|
||||
const auto expanding = false;
|
||||
for (const auto &[key, list] : _mediaLists) {
|
||||
const auto full = !list.wrap->scrollBottomSkip();
|
||||
const auto additionalScroll = (full ? st::boxRadius : 0);
|
||||
const auto height = content.height() - (full ? 0 : st::boxRadius);
|
||||
const auto wrapGeometry = QRect{ 0, tabs, w, height};
|
||||
list.wrap->updateGeometry(
|
||||
wrapGeometry,
|
||||
expanding,
|
||||
additionalScroll,
|
||||
content.height());
|
||||
}
|
||||
}
|
||||
|
||||
auto Suggestions::setupRecentPeers(RecentPeersList recentPeers)
|
||||
@@ -2027,8 +2202,8 @@ auto Suggestions::setupRecommendations() -> std::unique_ptr<ObjectList> {
|
||||
_persist = true;
|
||||
}, list->lifetime());
|
||||
|
||||
_tab.value() | rpl::filter(
|
||||
rpl::mappers::_1 == Tab::Channels
|
||||
_key.value() | rpl::filter(
|
||||
rpl::mappers::_1 == Key{ Tab::Channels }
|
||||
) | rpl::start_with_next([=] {
|
||||
controller->load();
|
||||
}, list->lifetime());
|
||||
@@ -2142,8 +2317,8 @@ auto Suggestions::setupPopularApps() -> std::unique_ptr<ObjectList> {
|
||||
_persist = true;
|
||||
}, list->lifetime());
|
||||
|
||||
_tab.value() | rpl::filter(
|
||||
rpl::mappers::_1 == Tab::Apps
|
||||
_key.value() | rpl::filter(
|
||||
rpl::mappers::_1 == Key{ Tab::Apps }
|
||||
) | rpl::start_with_next([=] {
|
||||
controller->load();
|
||||
}, list->lifetime());
|
||||
|
||||
@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#pragma once
|
||||
|
||||
#include "base/object_ptr.h"
|
||||
#include "base/timer.h"
|
||||
#include "dialogs/ui/top_peers_strip.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/rp_widget.h"
|
||||
@@ -18,12 +19,21 @@ namespace Data {
|
||||
class Thread;
|
||||
} // namespace Data
|
||||
|
||||
namespace Info {
|
||||
class WrapWidget;
|
||||
} // namespace Info
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace Storage {
|
||||
enum class SharedMediaType : signed char;
|
||||
} // namespace Storage
|
||||
|
||||
namespace Ui {
|
||||
class BoxContent;
|
||||
class ScrollArea;
|
||||
class ElasticScroll;
|
||||
class SettingsSlider;
|
||||
class VerticalLayout;
|
||||
@@ -55,6 +65,9 @@ public:
|
||||
void selectJump(Qt::Key direction, int pageSize = 0);
|
||||
void chooseRow();
|
||||
|
||||
bool consumeSearchQuery(const QString &query);
|
||||
[[nodiscard]] rpl::producer<> clearSearchQueryRequests() const;
|
||||
|
||||
[[nodiscard]] Data::Thread *updateFromParentDrag(QPoint globalPosition);
|
||||
void dragLeft();
|
||||
|
||||
@@ -96,10 +109,13 @@ public:
|
||||
class ObjectListController;
|
||||
|
||||
private:
|
||||
using MediaType = Storage::SharedMediaType;
|
||||
enum class Tab : uchar {
|
||||
Chats,
|
||||
Channels,
|
||||
Apps,
|
||||
Media,
|
||||
Downloads,
|
||||
};
|
||||
enum class JumpResult : uchar {
|
||||
NotApplied,
|
||||
@@ -107,6 +123,14 @@ private:
|
||||
AppliedAndOut,
|
||||
};
|
||||
|
||||
struct Key {
|
||||
Tab tab = Tab::Chats;
|
||||
MediaType mediaType = {};
|
||||
|
||||
friend inline auto operator<=>(Key, Key) = default;
|
||||
friend inline bool operator==(Key, Key) = default;
|
||||
};
|
||||
|
||||
struct ObjectList {
|
||||
not_null<Ui::SlideWrap<PeerListContent>*> wrap;
|
||||
rpl::variable<int> count;
|
||||
@@ -118,6 +142,11 @@ private:
|
||||
rpl::event_stream<not_null<PeerData*>> chosen;
|
||||
};
|
||||
|
||||
struct MediaList {
|
||||
Info::WrapWidget *wrap = nullptr;
|
||||
rpl::variable<int> count;
|
||||
};
|
||||
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
|
||||
@@ -160,17 +189,23 @@ private:
|
||||
SearchEmptyIcon icon,
|
||||
rpl::producer<QString> text);
|
||||
|
||||
void switchTab(Tab tab);
|
||||
void switchTab(Key key);
|
||||
void startShownAnimation(bool shown, Fn<void()> finish);
|
||||
void startSlideAnimation(Tab was, Tab now);
|
||||
void startSlideAnimation(Key was, Key now);
|
||||
void ensureContent(Key key);
|
||||
void finishShow();
|
||||
|
||||
void handlePressForChatPreview(PeerId id, Fn<void(bool)> callback);
|
||||
void updateControlsGeometry();
|
||||
void applySearchQuery();
|
||||
|
||||
const not_null<Window::SessionController*> _controller;
|
||||
|
||||
const std::unique_ptr<Ui::SettingsSlider> _tabs;
|
||||
rpl::variable<Tab> _tab = Tab::Chats;
|
||||
const std::unique_ptr<Ui::ScrollArea> _tabsScroll;
|
||||
const not_null<Ui::SettingsSlider*> _tabs;
|
||||
Ui::Animations::Simple _tabsScrollAnimation;
|
||||
const std::vector<Key> _tabKeys;
|
||||
rpl::variable<Key> _key;
|
||||
|
||||
const std::unique_ptr<Ui::ElasticScroll> _chatsScroll;
|
||||
const not_null<Ui::VerticalLayout*> _chatsContent;
|
||||
@@ -200,6 +235,11 @@ private:
|
||||
const std::unique_ptr<ObjectList> _recentApps;
|
||||
const std::unique_ptr<ObjectList> _popularApps;
|
||||
|
||||
base::flat_map<Key, MediaList> _mediaLists;
|
||||
rpl::event_stream<> _clearSearchQueryRequests;
|
||||
QString _searchQuery;
|
||||
base::Timer _searchQueryTimer;
|
||||
|
||||
Ui::Animations::Simple _shownAnimation;
|
||||
Fn<void()> _showFinished;
|
||||
bool _hidden = false;
|
||||
|
||||
@@ -661,7 +661,7 @@ QByteArray SerializeMessage(
|
||||
push("stars", data.stars);
|
||||
push("is_limited", data.limited);
|
||||
push("is_anonymous", data.anonymous);
|
||||
pushBare("text", SerializeText(context, data.text));
|
||||
pushBare("gift_text", SerializeText(context, data.text));
|
||||
}, [](v::null_t) {});
|
||||
|
||||
if (v::is_null(message.action.content)) {
|
||||
|
||||
@@ -26,7 +26,6 @@ class HistoryMainElementDelegateMixin;
|
||||
struct LanguageId;
|
||||
|
||||
namespace Data {
|
||||
|
||||
struct Draft;
|
||||
class Session;
|
||||
class Folder;
|
||||
@@ -34,29 +33,6 @@ class ChatFilter;
|
||||
struct SponsoredFrom;
|
||||
class SponsoredMessages;
|
||||
class HistoryMessages;
|
||||
|
||||
enum class ForwardOptions {
|
||||
PreserveInfo,
|
||||
NoSenderNames,
|
||||
NoNamesAndCaptions,
|
||||
};
|
||||
|
||||
struct ForwardDraft {
|
||||
MessageIdsList ids;
|
||||
ForwardOptions options = ForwardOptions::PreserveInfo;
|
||||
|
||||
friend inline auto operator<=>(
|
||||
const ForwardDraft&,
|
||||
const ForwardDraft&) = default;
|
||||
};
|
||||
|
||||
using ForwardDrafts = base::flat_map<MsgId, ForwardDraft>;
|
||||
|
||||
struct ResolvedForwardDraft {
|
||||
HistoryItemsList items;
|
||||
ForwardOptions options = ForwardOptions::PreserveInfo;
|
||||
};
|
||||
|
||||
} // namespace Data
|
||||
|
||||
namespace Dialogs {
|
||||
|
||||
@@ -2918,7 +2918,7 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
|
||||
leaderOrSelf,
|
||||
_controller);
|
||||
} else if (leaderOrSelf) {
|
||||
HistoryView::MaybeAddWhenEditedAction(_menu, leaderOrSelf);
|
||||
HistoryView::MaybeAddWhenEditedForwardedAction(_menu, leaderOrSelf);
|
||||
}
|
||||
|
||||
if (_menu->empty()) {
|
||||
|
||||
@@ -136,6 +136,9 @@ template <typename T>
|
||||
HistoryItemCommonFields fields,
|
||||
not_null<History*> history,
|
||||
not_null<HistoryItem*> original) {
|
||||
if (fields.flags & MessageFlag::FakeHistoryItem) {
|
||||
return fields;
|
||||
}
|
||||
fields.flags |= NewForwardedFlags(history->peer, fields.from, original);
|
||||
return fields;
|
||||
}
|
||||
@@ -509,7 +512,8 @@ HistoryItem::HistoryItem(
|
||||
auto config = CreateConfig();
|
||||
|
||||
const auto originalMedia = original->media();
|
||||
const auto dropForwardInfo = original->computeDropForwardedInfo();
|
||||
const auto dropForwardInfo = fields.ignoreForwardFrom
|
||||
|| original->computeDropForwardedInfo();
|
||||
const auto topicRootId = fields.replyTo.topicRootId;
|
||||
config.reply.messageId = config.reply.topMessageId = topicRootId;
|
||||
config.reply.topicPost = (topicRootId != 0) ? 1 : 0;
|
||||
@@ -597,9 +601,22 @@ HistoryItem::HistoryItem(
|
||||
}
|
||||
}
|
||||
|
||||
setText(dropForwardInfo
|
||||
const auto dropText = fields.ignoreForwardCaptions
|
||||
&& _media
|
||||
&& (_media->photo() || _media->document())
|
||||
&& !_media->webpage();
|
||||
setText(dropText
|
||||
? TextWithEntities()
|
||||
: dropForwardInfo
|
||||
? DropDisallowedCustomEmoji(history->peer, original->originalText())
|
||||
: original->originalText());
|
||||
|
||||
if (fields.groupedId) {
|
||||
setGroupId(MessageGroupId::FromRaw(
|
||||
history->peer->id,
|
||||
fields.groupedId,
|
||||
_flags & MessageFlag::IsOrWasScheduled));
|
||||
}
|
||||
}
|
||||
|
||||
HistoryItem::HistoryItem(
|
||||
@@ -935,10 +952,16 @@ void HistoryItem::resolveDependent(not_null<HistoryMessageReply*> reply) {
|
||||
if (!reply->acquireResolve()) {
|
||||
return;
|
||||
} else if (const auto messageId = reply->messageId()) {
|
||||
if (Data::IsScheduledMsgId(messageId)) {
|
||||
reply->updateData(this);
|
||||
if (!reply->acquireResolve()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
RequestDependentMessageItem(
|
||||
this,
|
||||
reply->externalPeerId(),
|
||||
reply->messageId());
|
||||
messageId);
|
||||
} else if (reply->storyId()) {
|
||||
RequestDependentMessageStory(
|
||||
this,
|
||||
@@ -2677,6 +2700,10 @@ int HistoryItem::reactionsPaidScheduled() const {
|
||||
return _reactions ? _reactions->scheduledPaid() : 0;
|
||||
}
|
||||
|
||||
bool HistoryItem::reactionsLocalAnonymous() const {
|
||||
return _reactions ? _reactions->localPaidAnonymous() : false;
|
||||
}
|
||||
|
||||
bool HistoryItem::reactionsAreTags() const {
|
||||
return _flags & MessageFlag::ReactionsAreTags;
|
||||
}
|
||||
@@ -5320,19 +5347,28 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) {
|
||||
&_history->session(),
|
||||
amount,
|
||||
currency);
|
||||
result.links.push_back(peer->createOpenLink());
|
||||
result.text = isSelf
|
||||
? tr::lng_action_gift_sent(tr::now,
|
||||
lt_cost,
|
||||
cost,
|
||||
Ui::Text::WithEntities)
|
||||
: tr::lng_action_gift_received(
|
||||
const auto anonymous = _from->isServiceUser();
|
||||
if (anonymous) {
|
||||
result.text = tr::lng_action_gift_received_anonymous(
|
||||
tr::now,
|
||||
lt_user,
|
||||
Ui::Text::Link(peer->shortName(), 1), // Link 1.
|
||||
lt_cost,
|
||||
cost,
|
||||
Ui::Text::WithEntities);
|
||||
} else {
|
||||
result.links.push_back(peer->createOpenLink());
|
||||
result.text = isSelf
|
||||
? tr::lng_action_gift_sent(tr::now,
|
||||
lt_cost,
|
||||
cost,
|
||||
Ui::Text::WithEntities)
|
||||
: tr::lng_action_gift_received(
|
||||
tr::now,
|
||||
lt_user,
|
||||
Ui::Text::Link(peer->shortName(), 1), // Link 1.
|
||||
lt_cost,
|
||||
cost,
|
||||
Ui::Text::WithEntities);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
@@ -107,6 +107,8 @@ struct HistoryItemCommonFields {
|
||||
uint64 groupedId = 0;
|
||||
EffectId effectId = 0;
|
||||
HistoryMessageMarkupData markup;
|
||||
bool ignoreForwardFrom = false;
|
||||
bool ignoreForwardCaptions = false;
|
||||
};
|
||||
|
||||
enum class HistoryReactionSource : char {
|
||||
@@ -469,6 +471,7 @@ public:
|
||||
[[nodiscard]] auto topPaidReactionsWithLocal() const
|
||||
-> std::vector<Data::MessageReactionsTopPaid>;
|
||||
[[nodiscard]] int reactionsPaidScheduled() const;
|
||||
[[nodiscard]] bool reactionsLocalAnonymous() const;
|
||||
[[nodiscard]] bool canViewReactions() const;
|
||||
[[nodiscard]] std::vector<Data::ReactionId> chosenReactions() const;
|
||||
[[nodiscard]] Data::ReactionId lookupUnreadReaction(
|
||||
|
||||
@@ -1014,7 +1014,7 @@ void HistoryWidget::refreshTopBarActiveChat() {
|
||||
const auto state = computeDialogsEntryState();
|
||||
_topBar->setActiveChat(state, _history->sendActionPainter());
|
||||
if (state.key) {
|
||||
controller()->setCurrentDialogsEntryState(state);
|
||||
controller()->setDialogsEntryState(state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1433,6 +1433,20 @@ int HistoryWidget::itemTopForHighlight(
|
||||
const auto heightLeft = (visibleAreaHeight - viewHeight);
|
||||
if (heightLeft >= 0) {
|
||||
return std::max(itemTop - (heightLeft / 2), 0);
|
||||
} else if (const auto sel = itemHighlight(item).range
|
||||
; !sel.empty() && !IsSubGroupSelection(sel)) {
|
||||
const auto single = st::messageTextStyle.font->height;
|
||||
const auto begin = HistoryView::FindViewY(view, sel.from) - single;
|
||||
const auto end = HistoryView::FindViewY(view, sel.to, begin + single)
|
||||
+ 2 * single;
|
||||
auto result = itemTop;
|
||||
if (end > visibleAreaHeight) {
|
||||
result = std::max(result, itemTop + end - visibleAreaHeight);
|
||||
}
|
||||
if (itemTop + begin < result) {
|
||||
result = itemTop + begin;
|
||||
}
|
||||
return result;
|
||||
} else if (reactionCenter >= 0) {
|
||||
const auto maxSize = st::reactionInlineImage;
|
||||
|
||||
@@ -1946,7 +1960,7 @@ bool HistoryWidget::notify_switchInlineBotButtonReceived(
|
||||
UserData *samePeerBot,
|
||||
MsgId samePeerReplyTo) {
|
||||
if (samePeerBot) {
|
||||
const auto to = controller()->currentDialogsEntryState();
|
||||
const auto to = controller()->dialogsEntryStateCurrent();
|
||||
if (!to.key.owningHistory()) {
|
||||
return false;
|
||||
}
|
||||
@@ -2109,7 +2123,7 @@ bool HistoryWidget::applyDraft(FieldHistoryAction fieldHistoryAction) {
|
||||
if (!_replyEditMsg) {
|
||||
requestMessageData(_editMsgId);
|
||||
}
|
||||
} else if (!readyToForward()) {
|
||||
} else {
|
||||
const auto draft = _history->localDraft({});
|
||||
_processingReplyTo = draft ? draft->reply : FullReplyTo();
|
||||
if (_processingReplyTo) {
|
||||
@@ -2179,9 +2193,10 @@ void HistoryWidget::showHistory(
|
||||
_showAtMsgHighlightPart = {};
|
||||
_showAtMsgHighlightPartOffsetHint = 0;
|
||||
|
||||
const auto wasState = controller()->currentDialogsEntryState();
|
||||
const auto wasState = controller()->dialogsEntryStateCurrent();
|
||||
const auto startBot = (showAtMsgId == ShowAndStartBotMsgId);
|
||||
if (startBot) {
|
||||
_showAndMaybeSendStart = (showAtMsgId == ShowAndMaybeStartBotMsgId);
|
||||
if (startBot || _showAndMaybeSendStart) {
|
||||
showAtMsgId = ShowAtTheEndMsgId;
|
||||
}
|
||||
|
||||
@@ -2283,8 +2298,8 @@ void HistoryWidget::showHistory(
|
||||
|
||||
if (const auto user = _peer->asUser()) {
|
||||
if (const auto &info = user->botInfo) {
|
||||
if (startBot) {
|
||||
if (wasState.key) {
|
||||
if (startBot || clearMaybeSendStart()) {
|
||||
if (startBot && wasState.key) {
|
||||
info->inlineReturnTo = wasState;
|
||||
}
|
||||
sendBotStartCommand();
|
||||
@@ -2519,8 +2534,9 @@ void HistoryWidget::showHistory(
|
||||
|
||||
if (const auto user = _peer->asUser()) {
|
||||
if (const auto &info = user->botInfo) {
|
||||
if (startBot) {
|
||||
if (wasState.key) {
|
||||
if (startBot
|
||||
|| (!_history->isEmpty() && clearMaybeSendStart())) {
|
||||
if (startBot && wasState.key) {
|
||||
info->inlineReturnTo = wasState;
|
||||
}
|
||||
sendBotStartCommand();
|
||||
@@ -3582,6 +3598,21 @@ void HistoryWidget::historyLoaded() {
|
||||
doneShow();
|
||||
}
|
||||
|
||||
bool HistoryWidget::clearMaybeSendStart() {
|
||||
if (!_showAndMaybeSendStart) {
|
||||
return false;
|
||||
}
|
||||
_showAndMaybeSendStart = false;
|
||||
if (const auto user = _history ? _history->peer->asUser() : nullptr) {
|
||||
if (const auto info = user->botInfo.get()) {
|
||||
if (!info->startToken.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void HistoryWidget::windowShown() {
|
||||
updateControlsGeometry();
|
||||
}
|
||||
@@ -3944,6 +3975,11 @@ void HistoryWidget::preloadHistoryIfNeeded() {
|
||||
preloadHistoryByScroll();
|
||||
checkReplyReturns();
|
||||
}
|
||||
if (_history && _history->loadedAtTop() && _history->loadedAtBottom()) {
|
||||
if (clearMaybeSendStart() && !_history->isDisplayedEmpty()) {
|
||||
sendBotStartCommand();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void HistoryWidget::preloadHistoryByScroll() {
|
||||
@@ -4982,7 +5018,7 @@ bool HistoryWidget::showRecordButton() const {
|
||||
&& !_voiceRecordBar->isRecordingByAnotherBar()
|
||||
&& !HasSendText(_field)
|
||||
&& !_previewDrawPreview
|
||||
&& !readyToForward()
|
||||
&& (_replyTo || !readyToForward())
|
||||
&& !_editMsgId;
|
||||
}
|
||||
|
||||
@@ -5163,11 +5199,17 @@ void HistoryWidget::switchToSearch(QString query) {
|
||||
update();
|
||||
setInnerFocus();
|
||||
|
||||
using Activation = HistoryView::ComposeSearch::Activation;
|
||||
_composeSearch->activations(
|
||||
) | rpl::start_with_next([=](not_null<HistoryItem*> item) {
|
||||
) | rpl::start_with_next([=](Activation activation) {
|
||||
const auto item = activation.item;
|
||||
auto params = ::Window::SectionShow(
|
||||
::Window::SectionShow::Way::ClearStack);
|
||||
params.highlightPart = { activation.query };
|
||||
params.highlightPartOffsetHint = kSearchQueryOffsetHint;
|
||||
controller()->showPeerHistory(
|
||||
item->history()->peer->id,
|
||||
::Window::SectionShow::Way::ClearStack,
|
||||
params,
|
||||
item->fullId().msg);
|
||||
}, _composeSearch->lifetime());
|
||||
|
||||
@@ -6825,18 +6867,15 @@ void HistoryWidget::mousePressEvent(QMouseEvent *e) {
|
||||
_peer,
|
||||
Window::SectionShow::Way::Forward,
|
||||
_editMsgId);
|
||||
} else if (isReadyToForward) {
|
||||
if (e->button() != Qt::LeftButton) {
|
||||
_forwardPanel->editToNextOption();
|
||||
} else {
|
||||
_forwardPanel->editOptions(controller()->uiShow());
|
||||
}
|
||||
} else if (_replyTo
|
||||
&& ((e->modifiers() & Qt::ControlModifier)
|
||||
|| (e->button() != Qt::LeftButton))) {
|
||||
jumpToReply(_replyTo);
|
||||
} else if (_replyTo) {
|
||||
} else if (_replyTo
|
||||
|| (isReadyToForward && e->button() == Qt::LeftButton)) {
|
||||
editDraftOptions();
|
||||
} else if (isReadyToForward) {
|
||||
_forwardPanel->editToNextOption();
|
||||
} else if (_kbReplyTo) {
|
||||
controller()->showPeerHistory(
|
||||
_kbReplyTo->history()->peer->id,
|
||||
@@ -6851,15 +6890,18 @@ void HistoryWidget::editDraftOptions() {
|
||||
const auto history = _history;
|
||||
const auto reply = _replyTo;
|
||||
const auto webpage = _preview->draft();
|
||||
const auto forward = _forwardPanel->draft();
|
||||
|
||||
const auto done = [=](
|
||||
FullReplyTo replyTo,
|
||||
Data::WebPageDraft webpage) {
|
||||
Data::WebPageDraft webpage,
|
||||
Data::ForwardDraft forward) {
|
||||
if (replyTo) {
|
||||
replyToMessage(replyTo);
|
||||
} else {
|
||||
cancelReply();
|
||||
}
|
||||
history->setForwardDraft({}, std::move(forward));
|
||||
_preview->apply(webpage);
|
||||
};
|
||||
const auto replyToId = reply.messageId;
|
||||
@@ -6873,6 +6915,7 @@ void HistoryWidget::editDraftOptions() {
|
||||
.history = history,
|
||||
.draft = Data::Draft(_field, reply, _preview->draft()),
|
||||
.usedLink = _preview->link(),
|
||||
.forward = _forwardPanel->draft(),
|
||||
.links = _preview->links(),
|
||||
.resolver = _preview->resolver(),
|
||||
.done = done,
|
||||
@@ -7956,7 +7999,7 @@ void HistoryWidget::processReply() {
|
||||
{ .ids = { 1, itemId } });
|
||||
}),
|
||||
.confirmText = tr::lng_selected_forward(),
|
||||
}));
|
||||
}));
|
||||
}
|
||||
return processCancel();
|
||||
#endif
|
||||
@@ -7985,7 +8028,6 @@ void HistoryWidget::setReplyFieldsFromProcessing() {
|
||||
return;
|
||||
}
|
||||
|
||||
_history->setForwardDraft(MsgId(), {});
|
||||
if (_composeSearch) {
|
||||
_composeSearch->hideAnimated();
|
||||
}
|
||||
@@ -8243,10 +8285,10 @@ void HistoryWidget::cancelFieldAreaState() {
|
||||
_preview->apply({ .removed = true });
|
||||
} else if (_editMsgId) {
|
||||
cancelEdit();
|
||||
} else if (readyToForward()) {
|
||||
_history->setForwardDraft(MsgId(), {});
|
||||
} else if (_replyTo) {
|
||||
cancelReply();
|
||||
} else if (readyToForward()) {
|
||||
_history->setForwardDraft(MsgId(), {});
|
||||
} else if (_kbReplyTo) {
|
||||
toggleKeyboard();
|
||||
}
|
||||
@@ -8595,9 +8637,6 @@ void HistoryWidget::updateForwarding() {
|
||||
_forwardPanel->update(_history, _history
|
||||
? _history->resolveForwardDraft(MsgId())
|
||||
: Data::ResolvedForwardDraft());
|
||||
if (readyToForward()) {
|
||||
cancelReply();
|
||||
}
|
||||
updateControlsVisibility();
|
||||
updateControlsGeometry();
|
||||
}
|
||||
|
||||
@@ -391,6 +391,7 @@ private:
|
||||
void fileChosen(ChatHelpers::FileChosen &&data);
|
||||
|
||||
void updateFieldSubmitSettings();
|
||||
bool clearMaybeSendStart();
|
||||
|
||||
// Checks if we are too close to the top or to the bottom
|
||||
// in the scroll area and preloads history if needed.
|
||||
@@ -726,6 +727,7 @@ private:
|
||||
base::flat_set<MsgId> _topicsRequested;
|
||||
TextWithEntities _showAtMsgHighlightPart;
|
||||
int _showAtMsgHighlightPartOffsetHint = 0;
|
||||
bool _showAndMaybeSendStart = false;
|
||||
|
||||
int _firstLoadRequest = 0; // Not real mtpRequestId.
|
||||
int _preloadRequest = 0; // Not real mtpRequestId.
|
||||
|
||||
@@ -146,6 +146,7 @@ public:
|
||||
[[nodiscard]] bool isEditingMessage() const;
|
||||
[[nodiscard]] bool readyToForward() const;
|
||||
[[nodiscard]] const HistoryItemsList &forwardItems() const;
|
||||
[[nodiscard]] const Data::ResolvedForwardDraft &forwardDraft() const;
|
||||
[[nodiscard]] FullReplyTo replyingToMessage() const;
|
||||
[[nodiscard]] FullMsgId editMsgId() const;
|
||||
[[nodiscard]] rpl::producer<FullMsgId> editMsgIdValue() const;
|
||||
@@ -281,23 +282,23 @@ void FieldHeader::init() {
|
||||
st::historyLinkIcon.paint(p, position, width());
|
||||
} else if (isEditingMessage()) {
|
||||
st::historyEditIcon.paint(p, position, width());
|
||||
} else if (readyToForward()) {
|
||||
st::historyForwardIcon.paint(p, position, width());
|
||||
} else if (const auto reply = replyingToMessage()) {
|
||||
if (!reply.quote.empty()) {
|
||||
st::historyQuoteIcon.paint(p, position, width());
|
||||
} else {
|
||||
st::historyReplyIcon.paint(p, position, width());
|
||||
}
|
||||
} else if (readyToForward()) {
|
||||
st::historyForwardIcon.paint(p, position, width());
|
||||
}
|
||||
|
||||
if (_preview.parsed) {
|
||||
paintWebPage(
|
||||
p,
|
||||
_history ? _history->peer : _data->session().user());
|
||||
} else if (isEditingMessage() || !readyToForward()) {
|
||||
} else if (isEditingMessage() || replyingToMessage()) {
|
||||
paintEditOrReplyToMessage(p);
|
||||
} else {
|
||||
} else if (readyToForward()) {
|
||||
paintForwardInfo(p);
|
||||
}
|
||||
}, lifetime());
|
||||
@@ -339,10 +340,10 @@ void FieldHeader::init() {
|
||||
_previewCancelled.fire({});
|
||||
} else if (_editMsgId.current()) {
|
||||
_editCancelled.fire({});
|
||||
} else if (readyToForward()) {
|
||||
_forwardCancelled.fire({});
|
||||
} else if (_replyTo.current()) {
|
||||
_replyCancelled.fire({});
|
||||
} else if (readyToForward()) {
|
||||
_forwardCancelled.fire({});
|
||||
}
|
||||
updateVisible();
|
||||
update();
|
||||
@@ -403,12 +404,9 @@ void FieldHeader::init() {
|
||||
_jumpToItemRequests.fire(FullReplyTo{
|
||||
.messageId = _editMsgId.current()
|
||||
});
|
||||
} else if (readyToForward()) {
|
||||
_forwardPanel->editOptions(_show);
|
||||
} else if (reply
|
||||
&& (e->modifiers() & Qt::ControlModifier)) {
|
||||
} else if (reply && (e->modifiers() & Qt::ControlModifier)) {
|
||||
_jumpToItemRequests.fire_copy(reply);
|
||||
} else if (reply) {
|
||||
} else if (reply || readyToForward()) {
|
||||
_editOptionsRequests.fire({});
|
||||
}
|
||||
} else if (!isLeftButton) {
|
||||
@@ -419,6 +417,8 @@ void FieldHeader::init() {
|
||||
_hasSendText());
|
||||
} else if (const auto reply = replyingToMessage()) {
|
||||
_jumpToItemRequests.fire_copy(reply);
|
||||
} else if (readyToForward()) {
|
||||
_forwardPanel->editToNextOption();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -713,6 +713,10 @@ const HistoryItemsList &FieldHeader::forwardItems() const {
|
||||
return _forwardPanel->items();
|
||||
}
|
||||
|
||||
const Data::ResolvedForwardDraft &FieldHeader::forwardDraft() const {
|
||||
return _forwardPanel->draft();
|
||||
}
|
||||
|
||||
FullReplyTo FieldHeader::replyingToMessage() const {
|
||||
return _replyTo.current();
|
||||
}
|
||||
@@ -764,9 +768,6 @@ void FieldHeader::updateForwarding(
|
||||
Data::Thread *thread,
|
||||
Data::ResolvedForwardDraft items) {
|
||||
_forwardPanel->update(thread, std::move(items));
|
||||
if (readyToForward()) {
|
||||
replyToMessage({});
|
||||
}
|
||||
updateControlsGeometry(size());
|
||||
}
|
||||
|
||||
@@ -1408,12 +1409,14 @@ void ComposeControls::init() {
|
||||
|
||||
const auto done = [=](
|
||||
FullReplyTo replyTo,
|
||||
Data::WebPageDraft webpage) {
|
||||
Data::WebPageDraft webpage,
|
||||
Data::ForwardDraft forward) {
|
||||
if (replyTo) {
|
||||
replyToMessage(replyTo);
|
||||
} else {
|
||||
cancelReplyMessage();
|
||||
}
|
||||
history->setForwardDraft(topicRootId, std::move(forward));
|
||||
_preview->apply(webpage);
|
||||
_field->setFocus();
|
||||
};
|
||||
@@ -1428,6 +1431,7 @@ void ComposeControls::init() {
|
||||
.history = history,
|
||||
.draft = Data::Draft(_field, reply, _preview->draft()),
|
||||
.usedLink = _preview->link(),
|
||||
.forward = _header->forwardDraft(),
|
||||
.links = _preview->links(),
|
||||
.resolver = _preview->resolver(),
|
||||
.done = done,
|
||||
@@ -2013,9 +2017,6 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) {
|
||||
_canReplaceMedia = _canAddMedia = false;
|
||||
_photoEditMedia = nullptr;
|
||||
_header->replyToMessage(draft->reply);
|
||||
if (_header->replyingToMessage()) {
|
||||
cancelForward();
|
||||
}
|
||||
_header->editMessage({});
|
||||
if (_preview) {
|
||||
_preview->setDisabled(false);
|
||||
@@ -3018,9 +3019,6 @@ void ComposeControls::replyToMessage(FullReplyTo id) {
|
||||
}
|
||||
} else {
|
||||
_header->replyToMessage(id);
|
||||
if (_header->replyingToMessage()) {
|
||||
cancelForward();
|
||||
}
|
||||
}
|
||||
|
||||
_saveDraftText = true;
|
||||
@@ -3071,12 +3069,12 @@ bool ComposeControls::handleCancelRequest() {
|
||||
} else if (isEditingMessage()) {
|
||||
maybeCancelEditMessage();
|
||||
return true;
|
||||
} else if (readyToForward()) {
|
||||
cancelForward();
|
||||
return true;
|
||||
} else if (replyingToMessage()) {
|
||||
cancelReplyMessage();
|
||||
return true;
|
||||
} else if (readyToForward()) {
|
||||
cancelForward();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
namespace HistoryView {
|
||||
namespace {
|
||||
|
||||
using Activation = ComposeSearch::Activation;
|
||||
using SearchRequest = Api::MessagesSearchMerged::Request;
|
||||
|
||||
[[nodiscard]] inline bool HasChooseFrom(not_null<History*> history) {
|
||||
@@ -840,7 +841,7 @@ public:
|
||||
void setInnerFocus();
|
||||
void setQuery(const QString &query);
|
||||
|
||||
[[nodiscard]] rpl::producer<not_null<HistoryItem*>> activations() const;
|
||||
[[nodiscard]] rpl::producer<Activation> activations() const;
|
||||
[[nodiscard]] rpl::producer<> destroyRequests() const;
|
||||
[[nodiscard]] rpl::lifetime &lifetime();
|
||||
|
||||
@@ -864,7 +865,7 @@ private:
|
||||
rpl::event_stream<BottomBar::Index> jumps;
|
||||
} _pendingJump;
|
||||
|
||||
rpl::event_stream<not_null<HistoryItem*>> _activations;
|
||||
rpl::event_stream<Activation> _activations;
|
||||
rpl::event_stream<> _destroyRequests;
|
||||
|
||||
};
|
||||
@@ -966,7 +967,7 @@ ComposeSearch::Inner::Inner(
|
||||
const auto item = _history->owner().message(messages[index]);
|
||||
if (item) {
|
||||
const auto weak = Ui::MakeWeak(_topBar.get());
|
||||
_activations.fire_copy(item);
|
||||
_activations.fire_copy({ item, _apiSearch.request().query });
|
||||
if (weak) {
|
||||
hideList();
|
||||
}
|
||||
@@ -1063,8 +1064,7 @@ void ComposeSearch::Inner::hideList() {
|
||||
}
|
||||
}
|
||||
|
||||
auto ComposeSearch::Inner::activations() const
|
||||
-> rpl::producer<not_null<HistoryItem*>> {
|
||||
rpl::producer<Activation> ComposeSearch::Inner::activations() const {
|
||||
return _activations.events();
|
||||
}
|
||||
|
||||
@@ -1103,7 +1103,7 @@ void ComposeSearch::setQuery(const QString &query) {
|
||||
_inner->setQuery(query);
|
||||
}
|
||||
|
||||
rpl::producer<not_null<HistoryItem*>> ComposeSearch::activations() const {
|
||||
rpl::producer<ComposeSearch::Activation> ComposeSearch::activations() const {
|
||||
return _inner->activations();
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,11 @@ public:
|
||||
void setInnerFocus();
|
||||
void setQuery(const QString &query);
|
||||
|
||||
[[nodiscard]] rpl::producer<not_null<HistoryItem*>> activations() const;
|
||||
struct Activation {
|
||||
not_null<HistoryItem*> item;
|
||||
QString query;
|
||||
};
|
||||
[[nodiscard]] rpl::producer<Activation> activations() const;
|
||||
[[nodiscard]] rpl::producer<> destroyRequests() const;
|
||||
|
||||
[[nodiscard]] rpl::lifetime &lifetime();
|
||||
|
||||
@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "history/view/controls/history_view_draft_options.h"
|
||||
|
||||
#include "base/random.h"
|
||||
#include "base/timer_rpl.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "boxes/filters/edit_filter_chats_list.h"
|
||||
@@ -20,12 +21,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "data/data_thread.h"
|
||||
#include "data/data_user.h"
|
||||
#include "data/data_web_page.h"
|
||||
#include "history/view/controls/history_view_forward_panel.h"
|
||||
#include "history/view/controls/history_view_webpage_processor.h"
|
||||
#include "history/view/history_view_element.h"
|
||||
#include "history/view/history_view_cursor_state.h"
|
||||
#include "history/history.h"
|
||||
#include "history/history_item.h"
|
||||
#include "history/history_item_components.h"
|
||||
#include "history/history_item_helpers.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "main/main_session.h"
|
||||
#include "settings/settings_common.h"
|
||||
@@ -41,6 +44,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "ui/ui_utility.h"
|
||||
#include "window/themes/window_theme.h"
|
||||
#include "window/section_widget.h"
|
||||
#include "window/window_peer_menu.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_chat.h"
|
||||
@@ -56,6 +60,7 @@ namespace {
|
||||
|
||||
enum class Section {
|
||||
Reply,
|
||||
Forward,
|
||||
Link,
|
||||
};
|
||||
|
||||
@@ -104,6 +109,10 @@ public:
|
||||
not_null<History*> history);
|
||||
~PreviewWrap();
|
||||
|
||||
[[nodiscard]] bool hasViewForItem(
|
||||
not_null<const HistoryItem*> item) const;
|
||||
|
||||
void showForwardSelector(Data::ResolvedForwardDraft draft);
|
||||
[[nodiscard]] rpl::producer<SelectedQuote> showQuoteSelector(
|
||||
const SelectedQuote "e);
|
||||
[[nodiscard]] rpl::producer<QString> showLinkSelector(
|
||||
@@ -117,6 +126,11 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
struct Entry {
|
||||
HistoryItem *item = nullptr;
|
||||
std::unique_ptr<Element> view;
|
||||
};
|
||||
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void leaveEventHook(QEvent *e) override;
|
||||
void mouseMoveEvent(QMouseEvent *e) override;
|
||||
@@ -129,7 +143,8 @@ private:
|
||||
_visibleBottom = bottom;
|
||||
}
|
||||
|
||||
void initElement();
|
||||
void clear(std::vector<Entry> entries);
|
||||
void initElements();
|
||||
void highlightUsedLink(
|
||||
const TextWithTags &message,
|
||||
const QString &usedLink,
|
||||
@@ -144,8 +159,8 @@ private:
|
||||
const std::unique_ptr<PreviewDelegate> _delegate;
|
||||
|
||||
Section _section = Section::Reply;
|
||||
HistoryItem *_draftItem = nullptr;
|
||||
std::unique_ptr<Element> _element;
|
||||
std::vector<Entry> _entries;
|
||||
base::flat_set<not_null<const Element*>> _views;
|
||||
rpl::variable<TextSelection> _selection;
|
||||
rpl::event_stream<QString> _chosenUrl;
|
||||
Ui::PeerUserpicView _userpic;
|
||||
@@ -191,7 +206,7 @@ PreviewWrap::PreviewWrap(
|
||||
const auto session = &_history->session();
|
||||
session->data().viewRepaintRequest(
|
||||
) | rpl::start_with_next([=](not_null<const Element*> view) {
|
||||
if (view == _element.get()) {
|
||||
if (_views.contains(view)) {
|
||||
update();
|
||||
}
|
||||
}, lifetime());
|
||||
@@ -221,26 +236,92 @@ PreviewWrap::PreviewWrap(
|
||||
|
||||
PreviewWrap::~PreviewWrap() {
|
||||
_selection.reset(TextSelection());
|
||||
base::take(_views);
|
||||
clear(base::take(_entries));
|
||||
}
|
||||
|
||||
void PreviewWrap::clear(std::vector<Entry> entries) {
|
||||
_elementLifetime.destroy();
|
||||
_element = nullptr;
|
||||
if (_draftItem) {
|
||||
_draftItem->destroy();
|
||||
for (auto &entry : entries) {
|
||||
entry.view = nullptr;
|
||||
if (const auto item = entry.item) {
|
||||
item->destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool PreviewWrap::hasViewForItem(not_null<const HistoryItem*> item) const {
|
||||
return (item->history() == _history)
|
||||
&& ranges::contains(_views, item, &Element::data);
|
||||
}
|
||||
|
||||
void PreviewWrap::showForwardSelector(Data::ResolvedForwardDraft draft) {
|
||||
Expects(!draft.items.empty());
|
||||
|
||||
_selection.reset(TextSelection());
|
||||
|
||||
auto was = base::take(_entries);
|
||||
auto groups = base::flat_map<MessageGroupId, uint64>();
|
||||
const auto groupByItem = [&](not_null<HistoryItem*> item) {
|
||||
const auto groupId = item->groupId();
|
||||
if (!groupId) {
|
||||
return uint64();
|
||||
}
|
||||
auto i = groups.find(groupId);
|
||||
if (i == end(groups)) {
|
||||
i = groups.emplace(groupId, base::RandomValue<uint64>()).first;
|
||||
}
|
||||
return i->second;
|
||||
};
|
||||
const auto wasViews = base::take(_views);
|
||||
using Options = Data::ForwardOptions;
|
||||
const auto dropNames = (draft.options != Options::PreserveInfo);
|
||||
const auto dropCaptions = (draft.options == Options::NoNamesAndCaptions);
|
||||
for (const auto &source : draft.items) {
|
||||
const auto groupedId = groupByItem(source);
|
||||
const auto item = _history->addNewLocalMessage({
|
||||
.id = _history->nextNonHistoryEntryId(),
|
||||
.flags = (MessageFlag::FakeHistoryItem
|
||||
| MessageFlag::Outgoing
|
||||
| MessageFlag::HasFromId
|
||||
| (source->invertMedia()
|
||||
? MessageFlag::InvertMedia
|
||||
: MessageFlag())),
|
||||
.from = _history->session().userPeerId(),
|
||||
.date = base::unixtime::now(),
|
||||
.groupedId = groupedId,
|
||||
.ignoreForwardFrom = dropNames,
|
||||
.ignoreForwardCaptions = dropCaptions,
|
||||
}, source);
|
||||
_entries.push_back({ item });
|
||||
}
|
||||
for (auto &entry : _entries) {
|
||||
entry.view = entry.item->createView(_delegate.get());
|
||||
_views.emplace(entry.view.get());
|
||||
}
|
||||
_link = _pressedLink = nullptr;
|
||||
clear(std::move(was));
|
||||
|
||||
_section = Section::Forward;
|
||||
|
||||
initElements();
|
||||
}
|
||||
|
||||
rpl::producer<SelectedQuote> PreviewWrap::showQuoteSelector(
|
||||
const SelectedQuote "e) {
|
||||
_selection.reset(TextSelection());
|
||||
|
||||
auto was = base::take(_entries);
|
||||
const auto wasViews = base::take(_views);
|
||||
const auto item = quote.item;
|
||||
const auto group = item->history()->owner().groups().find(item);
|
||||
const auto leader = group ? group->items.front().get() : item;
|
||||
_element = leader->createView(_delegate.get());
|
||||
_entries.push_back({
|
||||
.view = leader->createView(_delegate.get()),
|
||||
});
|
||||
_views.emplace(_entries.back().view.get());
|
||||
_link = _pressedLink = nullptr;
|
||||
|
||||
if (const auto was = base::take(_draftItem)) {
|
||||
was->destroy();
|
||||
}
|
||||
clear(std::move(was));
|
||||
|
||||
const auto media = item->media();
|
||||
_onlyMessageText = media
|
||||
@@ -249,12 +330,13 @@ rpl::producer<SelectedQuote> PreviewWrap::showQuoteSelector(
|
||||
|| (!media->photo() && !media->document()));
|
||||
_section = Section::Reply;
|
||||
|
||||
initElement();
|
||||
initElements();
|
||||
|
||||
_selection = _element->selectionFromQuote(quote);
|
||||
const auto view = _entries.back().view.get();
|
||||
_selection = view->selectionFromQuote(quote);
|
||||
return _selection.value(
|
||||
) | rpl::map([=](TextSelection selection) {
|
||||
if (const auto result = _element->selectedQuote(selection)) {
|
||||
if (const auto result = view->selectedQuote(selection)) {
|
||||
return result;
|
||||
}
|
||||
return SelectedQuote{ item };
|
||||
@@ -267,13 +349,11 @@ rpl::producer<QString> PreviewWrap::showLinkSelector(
|
||||
const std::vector<MessageLinkRange> &links,
|
||||
const QString &usedLink) {
|
||||
_selection.reset(TextSelection());
|
||||
base::take(_views);
|
||||
clear(base::take(_entries));
|
||||
|
||||
_element = nullptr;
|
||||
if (const auto was = base::take(_draftItem)) {
|
||||
was->destroy();
|
||||
}
|
||||
using Flag = MTPDmessageMediaWebPage::Flag;
|
||||
_draftItem = _history->addNewLocalMessage({
|
||||
const auto item = _history->addNewLocalMessage({
|
||||
.id = _history->nextNonHistoryEntryId(),
|
||||
.flags = (MessageFlag::FakeHistoryItem
|
||||
| MessageFlag::Outgoing
|
||||
@@ -299,13 +379,15 @@ rpl::producer<QString> PreviewWrap::showLinkSelector(
|
||||
MTP_long(webpage.id),
|
||||
MTP_string(webpage.url),
|
||||
MTP_int(0))));
|
||||
_element = _draftItem->createView(_delegate.get());
|
||||
_entries.push_back({ item, item->createView(_delegate.get()) });
|
||||
_views.emplace(_entries.back().view.get());
|
||||
|
||||
_selectType = TextSelectType::Letters;
|
||||
_symbol = _selectionStartSymbol = 0;
|
||||
_afterSymbol = _selectionStartAfterSymbol = false;
|
||||
_section = Section::Link;
|
||||
|
||||
initElement();
|
||||
initElements();
|
||||
highlightUsedLink(message, usedLink, links);
|
||||
|
||||
return _chosenUrl.events();
|
||||
@@ -338,7 +420,8 @@ void PreviewWrap::highlightUsedLink(
|
||||
text = text.mid(0, text.size() - 1);
|
||||
--selection.to;
|
||||
}
|
||||
const auto basic = _element->textState(QPoint(0, 0), {
|
||||
const auto view = _entries.back().view.get();
|
||||
const auto basic = view->textState(QPoint(0, 0), {
|
||||
.flags = Ui::Text::StateRequest::Flag::LookupSymbol,
|
||||
.onlyMessageText = true,
|
||||
});
|
||||
@@ -353,30 +436,31 @@ void PreviewWrap::highlightUsedLink(
|
||||
}
|
||||
|
||||
void PreviewWrap::paintEvent(QPaintEvent *e) {
|
||||
if (!_element) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto p = Painter(this);
|
||||
|
||||
p.translate(_position);
|
||||
|
||||
auto context = _theme->preparePaintContext(
|
||||
_style.get(),
|
||||
rect(),
|
||||
e->rect(),
|
||||
!window()->isActiveWindow());
|
||||
context.outbg = _element->hasOutLayout();
|
||||
context.selection = _selecting
|
||||
? resolveNewSelection()
|
||||
: _selection.current();
|
||||
for (const auto &entry : _entries) {
|
||||
context.outbg = entry.view->hasOutLayout();
|
||||
context.selection = _selecting
|
||||
? resolveNewSelection()
|
||||
: _selection.current();
|
||||
|
||||
p.translate(_position);
|
||||
_element->draw(p, context);
|
||||
entry.view->draw(p, context);
|
||||
|
||||
if (_element->displayFromPhoto()) {
|
||||
p.translate(0, entry.view->height());
|
||||
}
|
||||
const auto top = _entries.empty() ? nullptr : _entries.back().view.get();
|
||||
if (top && top->displayFromPhoto()) {
|
||||
auto userpicBottom = height()
|
||||
- _element->marginBottom()
|
||||
- _element->marginTop();
|
||||
const auto item = _element->data();
|
||||
- top->marginBottom()
|
||||
- top->marginTop();
|
||||
const auto item = top->data();
|
||||
const auto userpicTop = userpicBottom - st::msgPhotoSize;
|
||||
if (const auto from = item->displayFrom()) {
|
||||
from->paintUserpicLeft(
|
||||
@@ -415,7 +499,7 @@ void PreviewWrap::paintEvent(QPaintEvent *e) {
|
||||
}
|
||||
|
||||
void PreviewWrap::leaveEventHook(QEvent *e) {
|
||||
if (!_element || !_over) {
|
||||
if (!_over) {
|
||||
return;
|
||||
}
|
||||
_over = false;
|
||||
@@ -427,7 +511,7 @@ void PreviewWrap::leaveEventHook(QEvent *e) {
|
||||
}
|
||||
|
||||
void PreviewWrap::mouseMoveEvent(QMouseEvent *e) {
|
||||
if (!_element) {
|
||||
if (_entries.empty()) {
|
||||
return;
|
||||
}
|
||||
using Flag = Ui::Text::StateRequest::Flag;
|
||||
@@ -438,7 +522,16 @@ void PreviewWrap::mouseMoveEvent(QMouseEvent *e) {
|
||||
.onlyMessageText = (_section == Section::Link || _onlyMessageText),
|
||||
};
|
||||
const auto position = e->pos();
|
||||
auto resolved = _element->textState(position - _position, request);
|
||||
auto local = position - _position;
|
||||
auto resolved = TextState();
|
||||
for (auto &entry : _entries) {
|
||||
const auto height = entry.view->height();
|
||||
if (local.y() < height) {
|
||||
resolved = entry.view->textState(local, request);
|
||||
break;
|
||||
}
|
||||
local.setY(local.y() - height);
|
||||
}
|
||||
_over = true;
|
||||
const auto text = (_section == Section::Reply)
|
||||
&& (resolved.cursor == CursorState::Text);
|
||||
@@ -518,27 +611,25 @@ void PreviewWrap::mouseDoubleClickEvent(QMouseEvent *e) {
|
||||
}
|
||||
}
|
||||
|
||||
void PreviewWrap::initElement() {
|
||||
_elementLifetime.destroy();
|
||||
|
||||
if (!_element) {
|
||||
return;
|
||||
void PreviewWrap::initElements() {
|
||||
for (auto &entry : _entries) {
|
||||
entry.view->initDimensions();
|
||||
}
|
||||
_element->initDimensions();
|
||||
|
||||
widthValue(
|
||||
) | rpl::filter([=](int width) {
|
||||
return width > st::msgMinWidth;
|
||||
}) | rpl::start_with_next([=](int width) {
|
||||
const auto height = _position.y()
|
||||
+ _element->resizeGetHeight(width)
|
||||
+ st::msgMargin.top();
|
||||
auto height = _position.y();
|
||||
for (const auto &entry : _entries) {
|
||||
height += entry.view->resizeGetHeight(width);
|
||||
}
|
||||
height += st::msgMargin.top();
|
||||
resize(width, height);
|
||||
}, _elementLifetime);
|
||||
}
|
||||
|
||||
TextSelection PreviewWrap::resolveNewSelection() const {
|
||||
if (_section != Section::Reply) {
|
||||
if (_section != Section::Reply || _entries.empty()) {
|
||||
return TextSelection();
|
||||
}
|
||||
const auto make = [](uint16 symbol, bool afterSymbol) {
|
||||
@@ -551,7 +642,7 @@ TextSelection PreviewWrap::resolveNewSelection() const {
|
||||
const auto result = (first <= second)
|
||||
? TextSelection{ first, second }
|
||||
: TextSelection{ second, first };
|
||||
return _element->adjustSelection(result, _selectType);
|
||||
return _entries.back().view->adjustSelection(result, _selectType);
|
||||
}
|
||||
|
||||
void PreviewWrap::startSelection(TextSelectType type) {
|
||||
@@ -607,9 +698,10 @@ void DraftOptionsBox(
|
||||
|
||||
const auto &draft = args.draft;
|
||||
struct State {
|
||||
rpl::variable<Section> shown;
|
||||
rpl::variable<Section> shown = Section::Link;
|
||||
rpl::lifetime shownLifetime;
|
||||
rpl::variable<SelectedQuote> quote;
|
||||
Data::ResolvedForwardDraft forward;
|
||||
Data::WebPageDraft webpage;
|
||||
WebPageData *preview = nullptr;
|
||||
QString link;
|
||||
@@ -619,41 +711,90 @@ void DraftOptionsBox(
|
||||
Fn<void(const QString &link, WebPageData *page)> performSwitch;
|
||||
Fn<void(const QString &link, bool force)> requestAndSwitch;
|
||||
rpl::lifetime resolveLifetime;
|
||||
|
||||
Fn<void()> rebuild;
|
||||
};
|
||||
const auto state = box->lifetime().make_state<State>();
|
||||
state->link = args.usedLink;
|
||||
state->quote = SelectedQuote{
|
||||
replyItem,
|
||||
draft.reply.quote,
|
||||
draft.reply.quoteOffset,
|
||||
};
|
||||
state->forward = std::move(args.forward);
|
||||
state->webpage = draft.webpage;
|
||||
state->preview = previewData;
|
||||
state->shown = previewData ? Section::Link : Section::Reply;
|
||||
if (replyItem && previewData) {
|
||||
box->setNoContentMargin(true);
|
||||
state->tabs = box->setPinnedToTopContent(
|
||||
object_ptr<Ui::SettingsSlider>(
|
||||
box.get(),
|
||||
st::defaultTabsSlider));
|
||||
state->tabs->resizeToWidth(st::boxWideWidth);
|
||||
state->tabs->move(0, 0);
|
||||
state->tabs->setRippleTopRoundRadius(st::boxRadius);
|
||||
state->tabs->setSections({
|
||||
tr::lng_reply_header_short(tr::now),
|
||||
tr::lng_link_header_short(tr::now),
|
||||
});
|
||||
state->tabs->setActiveSectionFast(1);
|
||||
state->tabs->sectionActivated(
|
||||
) | rpl::start_with_next([=](int section) {
|
||||
state->shown = section ? Section::Link : Section::Reply;
|
||||
}, box->lifetime());
|
||||
} else {
|
||||
box->setTitle(previewData
|
||||
? tr::lng_link_options_header()
|
||||
: draft.reply.quote.empty()
|
||||
? tr::lng_reply_options_header()
|
||||
: tr::lng_reply_options_quote());
|
||||
}
|
||||
|
||||
state->rebuild = [=] {
|
||||
const auto hasLink = (state->preview != nullptr);
|
||||
const auto hasReply = (state->quote.current().item != nullptr);
|
||||
const auto hasForward = !state->forward.items.empty();
|
||||
if (!hasLink && !hasReply && !hasForward) {
|
||||
box->closeBox();
|
||||
return;
|
||||
}
|
||||
const auto section = state->shown.current();
|
||||
const auto changeSection = (section == Section::Link)
|
||||
? !hasLink
|
||||
: (section == Section::Reply)
|
||||
? !hasReply
|
||||
: !hasForward;
|
||||
const auto now = !changeSection
|
||||
? section
|
||||
: hasLink
|
||||
? Section::Link
|
||||
: hasReply
|
||||
? Section::Reply
|
||||
: Section::Forward;
|
||||
auto labels = std::vector<QString>();
|
||||
auto indices = base::flat_map<Section, int>();
|
||||
auto sections = std::vector<Section>();
|
||||
const auto push = [&](Section section, tr::phrase<> phrase) {
|
||||
indices[section] = labels.size();
|
||||
labels.push_back(phrase(tr::now));
|
||||
sections.push_back(section);
|
||||
};
|
||||
if (hasLink) {
|
||||
push(Section::Link, tr::lng_link_header_short);
|
||||
}
|
||||
if (hasReply) {
|
||||
push(Section::Reply, tr::lng_reply_header_short);
|
||||
}
|
||||
if (hasForward) {
|
||||
push(Section::Forward, tr::lng_forward_header_short);
|
||||
}
|
||||
if (labels.size() > 1) {
|
||||
box->setNoContentMargin(true);
|
||||
state->tabs = box->setPinnedToTopContent(
|
||||
object_ptr<Ui::SettingsSlider>(
|
||||
box.get(),
|
||||
st::defaultTabsSlider));
|
||||
state->tabs->resizeToWidth(st::boxWideWidth);
|
||||
state->tabs->move(0, 0);
|
||||
state->tabs->setRippleTopRoundRadius(st::boxRadius);
|
||||
state->tabs->setSections(labels);
|
||||
state->tabs->setActiveSectionFast(indices[now]);
|
||||
state->tabs->sectionActivated(
|
||||
) | rpl::start_with_next([=](int index) {
|
||||
state->shown = sections[index];
|
||||
}, box->lifetime());
|
||||
} else {
|
||||
const auto forwardCount = state->forward.items.size();
|
||||
box->setTitle(hasLink
|
||||
? tr::lng_link_options_header()
|
||||
: hasReply
|
||||
? (state->quote.current().text.empty()
|
||||
? tr::lng_reply_options_header()
|
||||
: tr::lng_reply_options_quote())
|
||||
: (forwardCount == 1)
|
||||
? tr::lng_forward_title()
|
||||
: tr::lng_forward_many_title(
|
||||
lt_count,
|
||||
rpl::single(forwardCount * 1.0)));
|
||||
}
|
||||
state->shown.force_assign(now);
|
||||
};
|
||||
state->rebuild();
|
||||
|
||||
const auto bottom = box->setPinnedToBottomContent(
|
||||
object_ptr<Ui::VerticalLayout>(box));
|
||||
@@ -675,9 +816,17 @@ void DraftOptionsBox(
|
||||
};
|
||||
const auto finish = [=](
|
||||
FullReplyTo result,
|
||||
Data::WebPageDraft webpage) {
|
||||
Data::WebPageDraft webpage,
|
||||
std::optional<Data::ForwardOptions> options) {
|
||||
const auto weak = Ui::MakeWeak(box);
|
||||
done(std::move(result), std::move(webpage));
|
||||
auto forward = Data::ForwardDraft();
|
||||
if (options) {
|
||||
forward.options = *options;
|
||||
for (const auto &item : state->forward.items) {
|
||||
forward.ids.push_back(item->fullId());
|
||||
}
|
||||
}
|
||||
done(std::move(result), std::move(webpage), std::move(forward));
|
||||
if (const auto strong = weak.data()) {
|
||||
strong->closeBox();
|
||||
}
|
||||
@@ -716,7 +865,7 @@ void DraftOptionsBox(
|
||||
st::settingsAttentionButtonWithIcon,
|
||||
{ &st::menuIconDeleteAttention }
|
||||
)->setClickedCallback([=] {
|
||||
finish({}, state->webpage);
|
||||
finish({}, state->webpage, state->forward.options);
|
||||
});
|
||||
|
||||
if (!item->originalText().empty()) {
|
||||
@@ -774,7 +923,8 @@ void DraftOptionsBox(
|
||||
st::settingsAttentionButtonWithIcon,
|
||||
{ &st::menuIconDeleteAttention }
|
||||
)->setClickedCallback([=] {
|
||||
finish(resolveReply(), { .removed = true });
|
||||
const auto options = state->forward.options;
|
||||
finish(resolveReply(), { .removed = true }, options);
|
||||
});
|
||||
|
||||
if (args.links.size() > 1) {
|
||||
@@ -783,6 +933,92 @@ void DraftOptionsBox(
|
||||
}
|
||||
};
|
||||
|
||||
const auto setupForwardActions = [=] {
|
||||
using Options = Data::ForwardOptions;
|
||||
const auto now = state->forward.options;
|
||||
const auto &items = state->forward.items;
|
||||
const auto count = items.size();
|
||||
const auto dropNames = (now != Options::PreserveInfo);
|
||||
const auto sendersCount = ItemsForwardSendersCount(items);
|
||||
const auto captionsCount = ItemsForwardCaptionsCount(items);
|
||||
const auto hasOnlyForcedForwardedInfo = !captionsCount
|
||||
&& HasOnlyForcedForwardedInfo(items);
|
||||
const auto dropCaptions = (now == Options::NoNamesAndCaptions);
|
||||
|
||||
AddFilledSkip(bottom);
|
||||
|
||||
if (!hasOnlyForcedForwardedInfo) {
|
||||
Settings::AddButtonWithIcon(
|
||||
bottom,
|
||||
(dropNames
|
||||
? (sendersCount == 1
|
||||
? tr::lng_forward_action_show_sender
|
||||
: tr::lng_forward_action_show_senders)
|
||||
: (sendersCount == 1
|
||||
? tr::lng_forward_action_hide_sender
|
||||
: tr::lng_forward_action_hide_senders))(),
|
||||
st::settingsButton,
|
||||
{ dropNames
|
||||
? &st::menuIconUserShow
|
||||
: &st::menuIconUserHide }
|
||||
)->setClickedCallback([=] {
|
||||
state->forward.options = dropNames
|
||||
? Options::PreserveInfo
|
||||
: Options::NoSenderNames;
|
||||
state->shown.force_assign(Section::Forward);
|
||||
});
|
||||
}
|
||||
if (captionsCount) {
|
||||
Settings::AddButtonWithIcon(
|
||||
bottom,
|
||||
(dropCaptions
|
||||
? (captionsCount == 1
|
||||
? tr::lng_forward_action_show_caption
|
||||
: tr::lng_forward_action_show_captions)
|
||||
: (captionsCount == 1
|
||||
? tr::lng_forward_action_hide_caption
|
||||
: tr::lng_forward_action_hide_captions))(),
|
||||
st::settingsButton,
|
||||
{ dropCaptions
|
||||
? &st::menuIconCaptionShow
|
||||
: &st::menuIconCaptionHide }
|
||||
)->setClickedCallback([=] {
|
||||
state->forward.options = dropCaptions
|
||||
? Options::NoSenderNames
|
||||
: Options::NoNamesAndCaptions;
|
||||
state->shown.force_assign(Section::Forward);
|
||||
});
|
||||
}
|
||||
|
||||
Settings::AddButtonWithIcon(
|
||||
bottom,
|
||||
tr::lng_forward_action_change_recipient(),
|
||||
st::settingsButton,
|
||||
{ &st::menuIconReplace }
|
||||
)->setClickedCallback([=] {
|
||||
auto draft = base::take(state->forward);
|
||||
finish(resolveReply(), state->webpage, std::nullopt);
|
||||
Window::ShowForwardMessagesBox(show, {
|
||||
.ids = show->session().data().itemsToIds(draft.items),
|
||||
.options = draft.options,
|
||||
});
|
||||
});
|
||||
|
||||
Settings::AddButtonWithIcon(
|
||||
bottom,
|
||||
tr::lng_forward_action_remove(),
|
||||
st::settingsAttentionButtonWithIcon,
|
||||
{ &st::menuIconDeleteAttention }
|
||||
)->setClickedCallback([=] {
|
||||
finish(resolveReply(), state->webpage, std::nullopt);
|
||||
});
|
||||
|
||||
AddFilledSkip(bottom);
|
||||
Ui::AddDividerText(bottom, (count == 1
|
||||
? tr::lng_forward_about()
|
||||
: tr::lng_forward_many_about()));
|
||||
};
|
||||
|
||||
const auto &resolver = args.resolver;
|
||||
state->performSwitch = [=](const QString &link, WebPageData *page) {
|
||||
const auto now = base::unixtime::now();
|
||||
@@ -847,20 +1083,27 @@ void DraftOptionsBox(
|
||||
state->shown.value() | rpl::start_with_next([=](Section shown) {
|
||||
bottom->clear();
|
||||
state->shownLifetime.destroy();
|
||||
if (shown == Section::Reply) {
|
||||
state->quote = state->wrap->showQuoteSelector(
|
||||
state->quote.current());
|
||||
setupReplyActions();
|
||||
} else {
|
||||
state->wrap->showLinkSelector(
|
||||
draft.textWithTags,
|
||||
state->webpage,
|
||||
linkRanges,
|
||||
state->link
|
||||
) | rpl::start_with_next([=](QString link) {
|
||||
switchTo(link);
|
||||
}, state->shownLifetime);
|
||||
setupLinkActions();
|
||||
switch (shown) {
|
||||
case Section::Reply: {
|
||||
state->quote = state->wrap->showQuoteSelector(
|
||||
state->quote.current());
|
||||
setupReplyActions();
|
||||
} break;
|
||||
case Section::Link: {
|
||||
state->wrap->showLinkSelector(
|
||||
draft.textWithTags,
|
||||
state->webpage,
|
||||
linkRanges,
|
||||
state->link
|
||||
) | rpl::start_with_next([=](QString link) {
|
||||
switchTo(link);
|
||||
}, state->shownLifetime);
|
||||
setupLinkActions();
|
||||
} break;
|
||||
case Section::Forward: {
|
||||
state->wrap->showForwardSelector(state->forward);
|
||||
setupForwardActions();
|
||||
} break;
|
||||
}
|
||||
}, box->lifetime());
|
||||
|
||||
@@ -879,7 +1122,8 @@ void DraftOptionsBox(
|
||||
.text = { tr::lng_reply_quote_long_text(tr::now) },
|
||||
});
|
||||
} else {
|
||||
finish(resolveReply(), state->webpage);
|
||||
const auto options = state->forward.options;
|
||||
finish(resolveReply(), state->webpage, options);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -887,36 +1131,35 @@ void DraftOptionsBox(
|
||||
box->closeBox();
|
||||
});
|
||||
|
||||
if (replyItem) {
|
||||
args.show->session().data().itemRemoved(
|
||||
) | rpl::filter([=](not_null<const HistoryItem*> removed) {
|
||||
const auto current = state->quote.current().item;
|
||||
if ((removed == replyItem) || (removed == current)) {
|
||||
return true;
|
||||
}
|
||||
const auto group = current->history()->owner().groups().find(
|
||||
current);
|
||||
return (group && ranges::contains(group->items, removed));
|
||||
}) | rpl::start_with_next([=] {
|
||||
if (previewData) {
|
||||
state->tabs = nullptr;
|
||||
box->setPinnedToTopContent(
|
||||
object_ptr<Ui::RpWidget>(nullptr));
|
||||
box->setNoContentMargin(false);
|
||||
box->setTitle(state->quote.current().text.empty()
|
||||
? tr::lng_reply_options_header()
|
||||
: tr::lng_reply_options_quote());
|
||||
state->shown = Section::Link;
|
||||
} else {
|
||||
box->closeBox();
|
||||
}
|
||||
}, box->lifetime());
|
||||
}
|
||||
args.show->session().data().itemRemoved(
|
||||
) | rpl::start_with_next([=](not_null<const HistoryItem*> removed) {
|
||||
const auto inReply = (state->quote.current().item == removed);
|
||||
if (inReply) {
|
||||
state->quote = SelectedQuote();
|
||||
}
|
||||
const auto i = ranges::find(state->forward.items, removed);
|
||||
const auto inForward = (i != end(state->forward.items));
|
||||
if (inForward) {
|
||||
state->forward.items.erase(i);
|
||||
}
|
||||
if (inReply || inForward) {
|
||||
state->rebuild();
|
||||
}
|
||||
}, box->lifetime());
|
||||
|
||||
args.show->session().data().itemViewRefreshRequest(
|
||||
) | rpl::start_with_next([=](not_null<const HistoryItem*> item) {
|
||||
if (state->wrap->hasViewForItem(item)) {
|
||||
state->rebuild();
|
||||
}
|
||||
}, box->lifetime());
|
||||
|
||||
}
|
||||
|
||||
struct AuthorSelector {
|
||||
object_ptr<Ui::RpWidget> content = { nullptr };
|
||||
Fn<bool(int, int, int)> overrideKey;
|
||||
Fn<void()> activate;
|
||||
};
|
||||
[[nodiscard]] AuthorSelector AuthorRowSelector(
|
||||
not_null<Main::Session*> session,
|
||||
@@ -975,12 +1218,15 @@ struct AuthorSelector {
|
||||
tr::lng_reply_in_author()));
|
||||
Ui::AddSkip(container);
|
||||
|
||||
const auto activate = [=] {
|
||||
chosen(from->owner().history(from));
|
||||
};
|
||||
const auto delegate = container->lifetime().make_state<
|
||||
PeerListContentDelegateSimple
|
||||
>();
|
||||
const auto controller = container->lifetime().make_state<
|
||||
AuthorController
|
||||
>(from, [=] { chosen(from->owner().history(from)); });
|
||||
>(from, activate);
|
||||
controller->setStyleOverrides(&st::peerListSingleRow);
|
||||
const auto content = container->add(object_ptr<PeerListContent>(
|
||||
container,
|
||||
@@ -1021,6 +1267,7 @@ struct AuthorSelector {
|
||||
return {
|
||||
.content = std::move(result),
|
||||
.overrideKey = overrideKey,
|
||||
.activate = activate,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1051,6 +1298,12 @@ void ShowReplyToChatBox(
|
||||
}
|
||||
}
|
||||
|
||||
void noSearchSubmit() {
|
||||
if (const auto onstack = _authorRow.activate) {
|
||||
onstack();
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] rpl::producer<Chosen> singleChosen() const {
|
||||
return _singleChosen.events();
|
||||
}
|
||||
@@ -1094,6 +1347,10 @@ void ShowReplyToChatBox(
|
||||
auto box = Box<PeerListBox>(std::move(controller), [=](
|
||||
not_null<PeerListBox*> box) {
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
|
||||
box->noSearchSubmits() | rpl::start_with_next([=] {
|
||||
controllerRaw->noSearchSubmit();
|
||||
}, box->lifetime());
|
||||
});
|
||||
const auto boxRaw = box.data();
|
||||
show->show(std::move(box));
|
||||
@@ -1150,7 +1407,7 @@ void EditDraftOptions(EditDraftOptionsArgs &&args) {
|
||||
&& !previewDataRaw->failed)
|
||||
? previewDataRaw
|
||||
: nullptr;
|
||||
if (!replyItem && !previewData) {
|
||||
if (!replyItem && !previewData && args.forward.items.empty()) {
|
||||
return;
|
||||
}
|
||||
args.show->show(
|
||||
|
||||
@@ -29,9 +29,10 @@ struct EditDraftOptionsArgs {
|
||||
not_null<History*> history;
|
||||
Data::Draft draft;
|
||||
QString usedLink;
|
||||
Data::ResolvedForwardDraft forward;
|
||||
std::vector<MessageLinkRange> links;
|
||||
std::shared_ptr<WebpageResolver> resolver;
|
||||
Fn<void(FullReplyTo, Data::WebPageDraft)> done;
|
||||
Fn<void(FullReplyTo, Data::WebPageDraft, Data::ForwardDraft)> done;
|
||||
Fn<void(FullReplyTo)> highlight;
|
||||
Fn<void()> clearOldDraft;
|
||||
};
|
||||
|
||||
@@ -44,19 +44,6 @@ constexpr auto kUnknownVersion = -1;
|
||||
constexpr auto kNameWithCaptionsVersion = -2;
|
||||
constexpr auto kNameNoCaptionsVersion = -3;
|
||||
|
||||
[[nodiscard]] bool HasOnlyForcedForwardedInfo(const HistoryItemsList &list) {
|
||||
for (const auto &item : list) {
|
||||
if (const auto media = item->media()) {
|
||||
if (!media->forceForwardedInfo()) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ForwardPanel::ForwardPanel(Fn<void()> repaint)
|
||||
@@ -228,6 +215,10 @@ void ForwardPanel::itemRemoved(not_null<const HistoryItem*> item) {
|
||||
}
|
||||
}
|
||||
|
||||
const Data::ResolvedForwardDraft &ForwardPanel::draft() const {
|
||||
return _data;
|
||||
}
|
||||
|
||||
const HistoryItemsList &ForwardPanel::items() const {
|
||||
return _data.items;
|
||||
}
|
||||
@@ -236,63 +227,17 @@ bool ForwardPanel::empty() const {
|
||||
return _data.items.empty();
|
||||
}
|
||||
|
||||
void ForwardPanel::editOptions(std::shared_ptr<ChatHelpers::Show> show) {
|
||||
using Options = Data::ForwardOptions;
|
||||
const auto now = _data.options;
|
||||
const auto count = _data.items.size();
|
||||
const auto dropNames = (now != Options::PreserveInfo);
|
||||
const auto sendersCount = ItemsForwardSendersCount(_data.items);
|
||||
const auto captionsCount = ItemsForwardCaptionsCount(_data.items);
|
||||
const auto hasOnlyForcedForwardedInfo = !captionsCount
|
||||
&& HasOnlyForcedForwardedInfo(_data.items);
|
||||
const auto dropCaptions = (now == Options::NoNamesAndCaptions);
|
||||
const auto weak = base::make_weak(this);
|
||||
const auto changeRecipient = crl::guard(this, [=] {
|
||||
if (_data.items.empty()) {
|
||||
return;
|
||||
}
|
||||
auto data = base::take(_data);
|
||||
_to->owningHistory()->setForwardDraft(_to->topicRootId(), {});
|
||||
Window::ShowForwardMessagesBox(show, {
|
||||
.ids = _to->owner().itemsToIds(data.items),
|
||||
.options = data.options,
|
||||
});
|
||||
});
|
||||
if (hasOnlyForcedForwardedInfo) {
|
||||
changeRecipient();
|
||||
void ForwardPanel::applyOptions(Data::ForwardOptions options) {
|
||||
if (_data.items.empty()) {
|
||||
return;
|
||||
} else if (_data.options != options) {
|
||||
_data.options = options;
|
||||
_to->owningHistory()->setForwardDraft(_to->topicRootId(), {
|
||||
.ids = _to->owner().itemsToIds(_data.items),
|
||||
.options = options,
|
||||
});
|
||||
_repaint();
|
||||
}
|
||||
const auto optionsChanged = crl::guard(weak, [=](
|
||||
Ui::ForwardOptions options) {
|
||||
if (_data.items.empty()) {
|
||||
return;
|
||||
}
|
||||
const auto newOptions = (options.captionsCount
|
||||
&& options.dropCaptions)
|
||||
? Options::NoNamesAndCaptions
|
||||
: options.dropNames
|
||||
? Options::NoSenderNames
|
||||
: Options::PreserveInfo;
|
||||
if (_data.options != newOptions) {
|
||||
_data.options = newOptions;
|
||||
_to->owningHistory()->setForwardDraft(_to->topicRootId(), {
|
||||
.ids = _to->owner().itemsToIds(_data.items),
|
||||
.options = newOptions,
|
||||
});
|
||||
_repaint();
|
||||
}
|
||||
});
|
||||
show->showBox(Box(
|
||||
Ui::ForwardOptionsBox,
|
||||
count,
|
||||
Ui::ForwardOptions{
|
||||
.sendersCount = sendersCount,
|
||||
.captionsCount = captionsCount,
|
||||
.dropNames = dropNames,
|
||||
.dropCaptions = dropCaptions,
|
||||
},
|
||||
optionsChanged,
|
||||
changeRecipient));
|
||||
}
|
||||
|
||||
void ForwardPanel::editToNextOption() {
|
||||
@@ -485,7 +430,19 @@ void EditWebPageOptions(
|
||||
box->closeBox();
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
bool HasOnlyForcedForwardedInfo(const HistoryItemsList &list) {
|
||||
for (const auto &item : list) {
|
||||
if (const auto media = item->media()) {
|
||||
if (!media->forceForwardedInfo()) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace HistoryView::Controls
|
||||
|
||||
@@ -47,9 +47,10 @@ public:
|
||||
|
||||
[[nodiscard]] rpl::producer<> itemsUpdated() const;
|
||||
|
||||
void editOptions(std::shared_ptr<ChatHelpers::Show> show);
|
||||
void applyOptions(Data::ForwardOptions options);
|
||||
void editToNextOption();
|
||||
|
||||
[[nodiscard]] const Data::ResolvedForwardDraft &draft() const;
|
||||
[[nodiscard]] const HistoryItemsList &items() const;
|
||||
[[nodiscard]] bool empty() const;
|
||||
|
||||
@@ -83,4 +84,6 @@ void EditWebPageOptions(
|
||||
Data::WebPageDraft draft,
|
||||
Fn<void(Data::WebPageDraft)> done);
|
||||
|
||||
[[nodiscard]] bool HasOnlyForcedForwardedInfo(const HistoryItemsList &list);
|
||||
|
||||
} // namespace HistoryView::Controls
|
||||
|
||||
@@ -24,6 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "history/history.h"
|
||||
#include "history/history_item.h"
|
||||
#include "history/history_item_components.h"
|
||||
#include "info/profile/info_profile_badge.h"
|
||||
#include "info/profile/info_profile_cover.h"
|
||||
#include "info/profile/info_profile_values.h"
|
||||
#include "lang/lang_keys.h"
|
||||
@@ -38,11 +39,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "ui/widgets/shadow.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "ui/unread_badge.h"
|
||||
#include "window/themes/window_theme.h"
|
||||
#include "window/section_widget.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
#include "styles/style_settings.h"
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
#include "ui/platform/win/ui_windows_direct_manipulation.h"
|
||||
@@ -191,6 +194,8 @@ private:
|
||||
const std::unique_ptr<Ui::ElasticScroll> _scroll;
|
||||
const std::unique_ptr<Ui::FlatButton> _markRead;
|
||||
|
||||
Info::Profile::Badge _badge;
|
||||
|
||||
QPointer<ListWidget> _inner;
|
||||
std::unique_ptr<CornerButtons> _cornerButtons;
|
||||
rpl::event_stream<ChatPreviewAction> _actions;
|
||||
@@ -265,7 +270,14 @@ Item::Item(not_null<Ui::RpWidget*> parent, not_null<Data::Thread*> thread)
|
||||
std::make_unique<Ui::FlatButton>(
|
||||
this,
|
||||
tr::lng_context_mark_read(tr::now),
|
||||
st::previewMarkRead)) {
|
||||
st::previewMarkRead))
|
||||
, _badge(
|
||||
_top.get(),
|
||||
st::settingsInfoPeerBadge,
|
||||
_peer,
|
||||
nullptr,
|
||||
nullptr,
|
||||
1) {
|
||||
_chatStyle->apply(_theme.get());
|
||||
setPointerCursor(false);
|
||||
setMinWidth(st::previewMenu.menu.widthMin);
|
||||
@@ -359,12 +371,14 @@ void Item::setupTop() {
|
||||
const auto shadow = Ui::CreateChild<Ui::PlainShadow>(this);
|
||||
rpl::combine(
|
||||
_top->widthValue(),
|
||||
std::move(nameValue)
|
||||
) | rpl::start_with_next([=](int width, const auto &) {
|
||||
std::move(nameValue),
|
||||
rpl::single(rpl::empty) | rpl::then(_badge.updated())
|
||||
) | rpl::start_with_next([=](int width, const auto &, const auto &) {
|
||||
const auto &st = st::previewTop;
|
||||
name->resizeToNaturalWidth(width
|
||||
- st.namePosition.x()
|
||||
- st.photoPosition.x());
|
||||
- st.photoPosition.x()
|
||||
- (_badge.widget() ? _badge.widget()->width() : 0));
|
||||
if (status) {
|
||||
name->move(st::previewTop.namePosition);
|
||||
} else {
|
||||
@@ -372,6 +386,10 @@ void Item::setupTop() {
|
||||
st::previewTop.namePosition.x(),
|
||||
(st::previewTop.height - name->height()) / 2);
|
||||
}
|
||||
_badge.move(
|
||||
name->x() + name->width() + st::normalFont->spacew,
|
||||
name->y(),
|
||||
name->y() + name->height());
|
||||
}, name->lifetime());
|
||||
|
||||
_top->geometryValue() | rpl::start_with_next([=](QRect geometry) {
|
||||
|
||||
@@ -1286,7 +1286,7 @@ base::unique_qptr<Ui::PopupMenu> FillContextMenu(
|
||||
if (hasWhoReactedItem) {
|
||||
AddWhoReactedAction(result, list, item, list->controller());
|
||||
} else if (item) {
|
||||
MaybeAddWhenEditedAction(result, item);
|
||||
MaybeAddWhenEditedForwardedAction(result, item);
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -1457,20 +1457,27 @@ void AddSaveSoundForNotifications(
|
||||
}, &st::menuIconSoundAdd);
|
||||
}
|
||||
|
||||
void AddWhenEditedActionHelper(
|
||||
void AddWhenEditedForwardedActionHelper(
|
||||
not_null<Ui::PopupMenu*> menu,
|
||||
not_null<HistoryItem*> item,
|
||||
bool insertSeparator) {
|
||||
if (item->history()->peer->isUser()) {
|
||||
if (const auto edited = item->Get<HistoryMessageEdited>()) {
|
||||
if (!item->hideEditedBadge()) {
|
||||
if (insertSeparator && !menu->empty()) {
|
||||
menu->addSeparator(&st::expandedMenuSeparator);
|
||||
}
|
||||
menu->addAction(Ui::WhenReadContextAction(
|
||||
menu.get(),
|
||||
Api::WhenEdited(item->from(), edited->date)));
|
||||
if (const auto edited = item->Get<HistoryMessageEdited>()) {
|
||||
if (!item->hideEditedBadge()) {
|
||||
if (insertSeparator && !menu->empty()) {
|
||||
menu->addSeparator(&st::expandedMenuSeparator);
|
||||
}
|
||||
menu->addAction(Ui::WhenReadContextAction(
|
||||
menu.get(),
|
||||
Api::WhenEdited(item->from(), edited->date)));
|
||||
}
|
||||
} else if (const auto forwarded = item->Get<HistoryMessageForwarded>()) {
|
||||
if (!forwarded->story && forwarded->psaType.isEmpty()) {
|
||||
if (insertSeparator && !menu->empty()) {
|
||||
menu->addSeparator(&st::expandedMenuSeparator);
|
||||
}
|
||||
menu->addAction(Ui::WhenReadContextAction(
|
||||
menu.get(),
|
||||
Api::WhenOriginal(item->from(), forwarded->originalDate)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1522,8 +1529,8 @@ void AddWhoReactedAction(
|
||||
if (!menu->empty()) {
|
||||
menu->addSeparator(&st::expandedMenuSeparator);
|
||||
}
|
||||
AddWhenEditedActionHelper(menu, item, false);
|
||||
if (item->history()->peer->isUser()) {
|
||||
AddWhenEditedForwardedActionHelper(menu, item, false);
|
||||
menu->addAction(Ui::WhenReadContextAction(
|
||||
menu.get(),
|
||||
Api::WhoReacted(item, context, st::defaultWhoRead, whoReadIds),
|
||||
@@ -1535,13 +1542,14 @@ void AddWhoReactedAction(
|
||||
Data::ReactedMenuFactory(&controller->session()),
|
||||
participantChosen,
|
||||
showAllChosen));
|
||||
AddWhenEditedForwardedActionHelper(menu, item, true);
|
||||
}
|
||||
}
|
||||
|
||||
void MaybeAddWhenEditedAction(
|
||||
void MaybeAddWhenEditedForwardedAction(
|
||||
not_null<Ui::PopupMenu*> menu,
|
||||
not_null<HistoryItem*> item) {
|
||||
AddWhenEditedActionHelper(menu, item, true);
|
||||
AddWhenEditedForwardedActionHelper(menu, item, true);
|
||||
}
|
||||
|
||||
void AddEditTagAction(
|
||||
|
||||
@@ -84,7 +84,7 @@ void AddWhoReactedAction(
|
||||
not_null<QWidget*> context,
|
||||
not_null<HistoryItem*> item,
|
||||
not_null<Window::SessionController*> controller);
|
||||
void MaybeAddWhenEditedAction(
|
||||
void MaybeAddWhenEditedForwardedAction(
|
||||
not_null<Ui::PopupMenu*> menu,
|
||||
not_null<HistoryItem*> item);
|
||||
void ShowWhoReactedMenu(
|
||||
|
||||
@@ -94,6 +94,89 @@ Element *MousedElement/* = nullptr*/;
|
||||
return session->tryResolveWindow();
|
||||
}
|
||||
|
||||
[[nodiscard]] TextSelection FindSearchQueryHighlight(
|
||||
const QString &text,
|
||||
const QString &query) {
|
||||
const auto lower = query.toLower();
|
||||
const auto inside = text.toLower();
|
||||
const auto find = [&](QStringView part) {
|
||||
auto skip = 0;
|
||||
if (const auto from = inside.indexOf(part, skip); from >= 0) {
|
||||
if (!from || !inside[from - 1].isLetterOrNumber()) {
|
||||
return int(from);
|
||||
}
|
||||
skip = from + 1;
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
if (const auto from = find(lower); from >= 0) {
|
||||
const auto till = from + query.size();
|
||||
if (till >= inside.size() || !inside[till].isLetterOrNumber()) {
|
||||
return { uint16(from), uint16(till) };
|
||||
}
|
||||
}
|
||||
const auto tillEndOfWord = [&](int from) {
|
||||
for (auto till = from + 1; till != inside.size(); ++till) {
|
||||
if (!inside[till].isLetterOrNumber()) {
|
||||
return TextSelection{ uint16(from), uint16(till) };
|
||||
}
|
||||
}
|
||||
return TextSelection{ uint16(from), uint16(inside.size()) };
|
||||
};
|
||||
const auto words = QStringView(lower).split(
|
||||
QRegularExpression(
|
||||
u"[\\W]"_q,
|
||||
QRegularExpression::UseUnicodePropertiesOption),
|
||||
Qt::SkipEmptyParts);
|
||||
for (const auto &word : words) {
|
||||
const auto length = int(word.size());
|
||||
const auto cut = length / 2;
|
||||
const auto part = word.mid(0, length - cut);
|
||||
const auto offset = find(part);
|
||||
if (offset < 0) {
|
||||
continue;
|
||||
}
|
||||
for (auto i = 0; i != cut; ++i) {
|
||||
const auto part = word.mid(0, length - i);
|
||||
if (const auto from = find(part); from >= 0) {
|
||||
return tillEndOfWord(from);
|
||||
}
|
||||
}
|
||||
return tillEndOfWord(offset);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
[[nodiscard]] TextSelection ApplyModificationsFrom(
|
||||
TextSelection result,
|
||||
const Ui::Text::String &text) {
|
||||
if (result.empty()) {
|
||||
return result;
|
||||
}
|
||||
for (const auto &modification : text.modifications()) {
|
||||
if (modification.position >= result.to) {
|
||||
break;
|
||||
}
|
||||
if (modification.added) {
|
||||
++result.to;
|
||||
}
|
||||
const auto shiftTo = std::min(
|
||||
int(modification.skipped),
|
||||
result.to - modification.position);
|
||||
result.to -= shiftTo;
|
||||
if (modification.position <= result.from) {
|
||||
if (modification.added) {
|
||||
++result.from;
|
||||
}
|
||||
const auto shiftFrom = std::min(
|
||||
int(modification.skipped),
|
||||
result.from - modification.position);
|
||||
result.from -= shiftFrom;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<Ui::PathShiftGradient> MakePathShiftGradient(
|
||||
@@ -1726,6 +1809,11 @@ TextSelection Element::FindSelectionFromQuote(
|
||||
return {};
|
||||
}
|
||||
const auto &original = quote.item->originalText();
|
||||
if (quote.offset == kSearchQueryOffsetHint) {
|
||||
return ApplyModificationsFrom(
|
||||
FindSearchQueryHighlight(original.text, quote.text.text),
|
||||
text);
|
||||
}
|
||||
const auto length = int(original.text.size());
|
||||
const auto qlength = int(quote.text.text.size());
|
||||
const auto checkAt = [&](int offset) {
|
||||
@@ -1789,28 +1877,7 @@ TextSelection Element::FindSelectionFromQuote(
|
||||
if (result.empty()) {
|
||||
return {};
|
||||
}
|
||||
for (const auto &modification : text.modifications()) {
|
||||
if (modification.position >= result.to) {
|
||||
break;
|
||||
}
|
||||
if (modification.added) {
|
||||
++result.to;
|
||||
}
|
||||
const auto shiftTo = std::min(
|
||||
int(modification.skipped),
|
||||
result.to - modification.position);
|
||||
result.to -= shiftTo;
|
||||
if (modification.position <= result.from) {
|
||||
if (modification.added) {
|
||||
++result.from;
|
||||
}
|
||||
const auto shiftFrom = std::min(
|
||||
int(modification.skipped),
|
||||
result.from - modification.position);
|
||||
result.from -= shiftFrom;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
return ApplyModificationsFrom(result, text);
|
||||
}
|
||||
|
||||
Reactions::ButtonParameters Element::reactionButtonParameters(
|
||||
@@ -1948,4 +2015,47 @@ void Element::ClearGlobal() {
|
||||
MousedElement = nullptr;
|
||||
}
|
||||
|
||||
int FindViewY(not_null<Element*> view, uint16 symbol, int yfrom) {
|
||||
auto request = HistoryView::StateRequest();
|
||||
request.flags = Ui::Text::StateRequest::Flag::LookupSymbol;
|
||||
const auto single = st::messageTextStyle.font->height;
|
||||
const auto inner = view->innerGeometry();
|
||||
const auto origin = inner.topLeft();
|
||||
const auto top = 0;
|
||||
const auto bottom = view->height();
|
||||
if (origin.y() < top
|
||||
|| origin.y() + inner.height() > bottom
|
||||
|| inner.height() <= 0) {
|
||||
return yfrom;
|
||||
}
|
||||
const auto fory = [&](int y) {
|
||||
return view->textState(origin + QPoint(0, y), request).symbol;
|
||||
};
|
||||
yfrom = std::max(yfrom - origin.y(), 0);
|
||||
auto ytill = inner.height() - 1;
|
||||
auto symbolfrom = fory(yfrom);
|
||||
auto symboltill = fory(ytill);
|
||||
if ((yfrom >= ytill) || (symbolfrom >= symbol)) {
|
||||
return origin.y() + yfrom;
|
||||
} else if (symboltill <= symbol) {
|
||||
return origin.y() + ytill;
|
||||
}
|
||||
while (ytill - yfrom >= 2 * single) {
|
||||
const auto middle = (yfrom + ytill) / 2;
|
||||
const auto found = fory(middle);
|
||||
if (found == symbol
|
||||
|| symbolfrom > found
|
||||
|| symboltill < found) {
|
||||
return middle;
|
||||
} else if (found < symbol) {
|
||||
yfrom = middle;
|
||||
symbolfrom = found;
|
||||
} else {
|
||||
ytill = middle;
|
||||
symboltill = found;
|
||||
}
|
||||
}
|
||||
return origin.y() + (yfrom + ytill) / 2;
|
||||
}
|
||||
|
||||
} // namespace HistoryView
|
||||
|
||||
@@ -668,4 +668,9 @@ private:
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] int FindViewY(
|
||||
not_null<Element*> view,
|
||||
uint16 symbol,
|
||||
int yfrom = 0);
|
||||
|
||||
} // namespace HistoryView
|
||||
|
||||
@@ -711,7 +711,25 @@ std::optional<int> ListWidget::scrollTopForView(
|
||||
const auto top = view->y();
|
||||
const auto height = view->height();
|
||||
const auto available = _visibleBottom - _visibleTop;
|
||||
return top - std::max((available - height) / 2, 0);
|
||||
const auto heightLeft = (available - height);
|
||||
if (heightLeft >= 0) {
|
||||
return std::max(top - (heightLeft / 2), 0);
|
||||
} else if (const auto sel = _highlighter.state(view->data()).range
|
||||
; !sel.empty() && !IsSubGroupSelection(sel)) {
|
||||
const auto single = st::messageTextStyle.font->height;
|
||||
const auto begin = HistoryView::FindViewY(view, sel.from) - single;
|
||||
const auto end = HistoryView::FindViewY(view, sel.to, begin + single)
|
||||
+ 2 * single;
|
||||
auto result = top;
|
||||
if (end > available) {
|
||||
result = std::max(result, top + end - available);
|
||||
}
|
||||
if (top + begin < result) {
|
||||
result = top + begin;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return top;
|
||||
}
|
||||
|
||||
void ListWidget::scrollTo(
|
||||
@@ -873,24 +891,31 @@ bool ListWidget::showAtPositionNow(
|
||||
Data::MessagePosition position,
|
||||
const Window::SectionShow ¶ms,
|
||||
Fn<void(bool found)> done) {
|
||||
if (const auto scrollTop = scrollTopForPosition(position)) {
|
||||
computeScrollTo(*scrollTop, position, params.animated);
|
||||
if (position != Data::MaxMessagePosition
|
||||
&& position != Data::UnreadMessagePosition) {
|
||||
highlightMessage(
|
||||
position.fullId,
|
||||
params.highlightPart,
|
||||
params.highlightPartOffsetHint);
|
||||
}
|
||||
if (done) {
|
||||
const auto found = !position.fullId.peer
|
||||
|| !IsServerMsgId(position.fullId.msg)
|
||||
|| viewForItem(position.fullId);
|
||||
done(found);
|
||||
}
|
||||
return true;
|
||||
auto scrollTop = scrollTopForPosition(position);
|
||||
if (!scrollTop.has_value()) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
if (position != Data::MaxMessagePosition
|
||||
&& position != Data::UnreadMessagePosition) {
|
||||
const auto hasHighlight = !params.highlightPart.empty();
|
||||
highlightMessage(
|
||||
position.fullId,
|
||||
params.highlightPart,
|
||||
params.highlightPartOffsetHint);
|
||||
if (hasHighlight) {
|
||||
// We may want to scroll to a different part of the message.
|
||||
scrollTop = scrollTopForPosition(position);
|
||||
Assert(scrollTop.has_value());
|
||||
}
|
||||
}
|
||||
computeScrollTo(*scrollTop, position, params.animated);
|
||||
if (done) {
|
||||
const auto found = !position.fullId.peer
|
||||
|| !IsServerMsgId(position.fullId.msg)
|
||||
|| viewForItem(position.fullId);
|
||||
done(found);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void ListWidget::computeScrollTo(
|
||||
|
||||
@@ -4299,14 +4299,18 @@ QRect Message::innerGeometry() const {
|
||||
width()));
|
||||
}
|
||||
if (hasBubble()) {
|
||||
result.translate(0, st::msgPadding.top() + st::mediaInBubbleSkip);
|
||||
const auto cut = [&](int amount) {
|
||||
amount = std::min(amount, result.height());
|
||||
result.setTop(result.top() + amount);
|
||||
};
|
||||
cut(st::msgPadding.top() + st::mediaInBubbleSkip);
|
||||
|
||||
if (displayFromName()) {
|
||||
// See paintFromName().
|
||||
result.translate(0, st::msgNameFont->height);
|
||||
cut(st::msgNameFont->height);
|
||||
}
|
||||
if (displayedTopicButton()) {
|
||||
result.translate(0, st::topicButtonSkip
|
||||
cut(st::topicButtonSkip
|
||||
+ st::topicButtonPadding.top()
|
||||
+ st::msgNameFont->height
|
||||
+ st::topicButtonPadding.bottom()
|
||||
@@ -4315,13 +4319,13 @@ QRect Message::innerGeometry() const {
|
||||
if (!displayFromName() && !displayForwardedFrom()) {
|
||||
// See paintViaBotIdInfo().
|
||||
if (data()->Has<HistoryMessageVia>()) {
|
||||
result.translate(0, st::msgServiceNameFont->height);
|
||||
cut(st::msgServiceNameFont->height);
|
||||
}
|
||||
}
|
||||
// Skip displayForwardedFrom() until there are no animations for it.
|
||||
if (const auto reply = Get<Reply>()) {
|
||||
// See paintReplyInfo().
|
||||
result.translate(0, reply->height());
|
||||
cut(reply->height());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
||||
@@ -163,6 +163,7 @@ PaidReactionToast::~PaidReactionToast() {
|
||||
|
||||
bool PaidReactionToast::maybeShowFor(not_null<HistoryItem*> item) {
|
||||
const auto count = item->reactionsPaidScheduled();
|
||||
const auto anonymous = item->reactionsLocalAnonymous();
|
||||
const auto at = _owner->reactions().sendingScheduledPaidAt(item);
|
||||
if (!count || !at) {
|
||||
return false;
|
||||
@@ -172,13 +173,14 @@ bool PaidReactionToast::maybeShowFor(not_null<HistoryItem*> item) {
|
||||
if (at <= crl::now() + ignore) {
|
||||
return false;
|
||||
}
|
||||
showFor(item->fullId(), count, at - ignore, total);
|
||||
showFor(item->fullId(), count, anonymous, at - ignore, total);
|
||||
return true;
|
||||
}
|
||||
|
||||
void PaidReactionToast::showFor(
|
||||
FullMsgId itemId,
|
||||
int count,
|
||||
bool anonymous,
|
||||
crl::time finish,
|
||||
crl::time total) {
|
||||
const auto old = _weak.get();
|
||||
@@ -186,6 +188,7 @@ void PaidReactionToast::showFor(
|
||||
if (i != end(_stack)) {
|
||||
if (old && i + 1 == end(_stack)) {
|
||||
_count = count;
|
||||
_anonymous = anonymous;
|
||||
_timeFinish = finish;
|
||||
return;
|
||||
}
|
||||
@@ -199,14 +202,22 @@ void PaidReactionToast::showFor(
|
||||
_hiding.push_back(base::take(_weak));
|
||||
}
|
||||
_count.reset();
|
||||
_anonymous.reset();
|
||||
_timeFinish.reset();
|
||||
_count = count;
|
||||
_anonymous = anonymous;
|
||||
_timeFinish = finish;
|
||||
auto text = rpl::combine(
|
||||
tr::lng_paid_react_toast(
|
||||
lt_count,
|
||||
_count.value() | tr::to_count(),
|
||||
Ui::Text::Bold),
|
||||
rpl::conditional(
|
||||
_anonymous.value(),
|
||||
tr::lng_paid_react_toast_anonymous(
|
||||
lt_count,
|
||||
_count.value() | tr::to_count(),
|
||||
Ui::Text::Bold),
|
||||
tr::lng_paid_react_toast(
|
||||
lt_count,
|
||||
_count.value() | tr::to_count(),
|
||||
Ui::Text::Bold)),
|
||||
tr::lng_paid_react_toast_text(
|
||||
lt_count_decimal,
|
||||
_count.value() | tr::to_count(),
|
||||
|
||||
@@ -40,6 +40,7 @@ private:
|
||||
void showFor(
|
||||
FullMsgId itemId,
|
||||
int count,
|
||||
bool anonymous,
|
||||
crl::time left,
|
||||
crl::time total);
|
||||
|
||||
@@ -53,6 +54,7 @@ private:
|
||||
base::weak_ptr<Ui::Toast::Instance> _weak;
|
||||
std::vector<base::weak_ptr<Ui::Toast::Instance>> _hiding;
|
||||
rpl::variable<int> _count;
|
||||
rpl::variable<bool> _anonymous;
|
||||
rpl::variable<crl::time> _timeFinish;
|
||||
|
||||
std::vector<FullMsgId> _stack;
|
||||
|
||||
@@ -1521,7 +1521,7 @@ void RepliesWidget::refreshTopBarActiveChat() {
|
||||
};
|
||||
_topBar->setActiveChat(state, _sendAction.get());
|
||||
_composeControls->setCurrentDialogsEntryState(state);
|
||||
controller()->setCurrentDialogsEntryState(state);
|
||||
controller()->setDialogsEntryState(state);
|
||||
}
|
||||
|
||||
void RepliesWidget::refreshUnreadCountBadge(std::optional<int> count) {
|
||||
|
||||
@@ -186,7 +186,7 @@ ScheduledWidget::ScheduledWidget(
|
||||
};
|
||||
_topBar->setActiveChat(state, nullptr);
|
||||
_composeControls->setCurrentDialogsEntryState(state);
|
||||
controller->setCurrentDialogsEntryState(state);
|
||||
controller->setDialogsEntryState(state);
|
||||
|
||||
_topBar->move(0, 0);
|
||||
_topBar->resizeToWidth(width());
|
||||
|
||||
@@ -341,7 +341,7 @@ bool SublistWidget::searchInChatEmbedded(
|
||||
_composeSearch->setInnerFocus();
|
||||
return true;
|
||||
}
|
||||
_composeSearch = std::make_unique<HistoryView::ComposeSearch>(
|
||||
_composeSearch = std::make_unique<ComposeSearch>(
|
||||
this,
|
||||
controller(),
|
||||
_history,
|
||||
@@ -352,10 +352,15 @@ bool SublistWidget::searchInChatEmbedded(
|
||||
setInnerFocus();
|
||||
|
||||
_composeSearch->activations(
|
||||
) | rpl::start_with_next([=](not_null<HistoryItem*> item) {
|
||||
) | rpl::start_with_next([=](ComposeSearch::Activation activation) {
|
||||
const auto item = activation.item;
|
||||
auto params = ::Window::SectionShow(
|
||||
::Window::SectionShow::Way::ClearStack);
|
||||
params.highlightPart = { activation.query };
|
||||
params.highlightPartOffsetHint = kSearchQueryOffsetHint;
|
||||
controller()->showPeerHistory(
|
||||
item->history()->peer->id,
|
||||
::Window::SectionShow::Way::ClearStack,
|
||||
params,
|
||||
item->fullId().msg);
|
||||
}, _composeSearch->lifetime());
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
namespace HistoryView {
|
||||
namespace {
|
||||
|
||||
class ContactClickHandler : public LambdaClickHandler {
|
||||
class ContactClickHandler final : public LambdaClickHandler {
|
||||
public:
|
||||
using LambdaClickHandler::LambdaClickHandler;
|
||||
|
||||
@@ -45,7 +45,7 @@ public:
|
||||
_dragText = t;
|
||||
}
|
||||
|
||||
QString dragText() const override {
|
||||
QString dragText() const override final {
|
||||
return _dragText;
|
||||
}
|
||||
|
||||
@@ -537,7 +537,7 @@ TextState Contact::textState(QPoint point, StateRequest request) const {
|
||||
|
||||
_lastPoint = point;
|
||||
|
||||
if (_buttons.size() > 1) {
|
||||
if (!hasSingleLink()) {
|
||||
const auto end = rect::bottom(inner) + _st.padding.bottom();
|
||||
const auto bWidth = inner.width() / float64(_buttons.size());
|
||||
const auto bHeight = rect::bottom(outer) - end;
|
||||
@@ -564,6 +564,14 @@ bool Contact::hasHeavyPart() const {
|
||||
return !_userpic.null();
|
||||
}
|
||||
|
||||
bool Contact::hasSingleLink() const {
|
||||
return (_buttons.size() > 1)
|
||||
? false
|
||||
: (_buttons.size() == 1 && _buttons.front().link == _mainButton.link)
|
||||
? true
|
||||
: (_buttons.empty() && _mainButton.link);
|
||||
}
|
||||
|
||||
void Contact::clickHandlerPressedChanged(
|
||||
const ClickHandlerPtr &p,
|
||||
bool pressed) {
|
||||
@@ -571,7 +579,7 @@ void Contact::clickHandlerPressedChanged(
|
||||
const auto outer = full - inBubblePadding();
|
||||
const auto inner = outer - innerMargin();
|
||||
const auto end = rect::bottom(inner) + _st.padding.bottom();
|
||||
if ((_lastPoint.y() < end) || (_buttons.size() <= 1)) {
|
||||
if ((_lastPoint.y() < end) || hasSingleLink()) {
|
||||
if (p != _mainButton.link) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -68,6 +68,8 @@ private:
|
||||
[[nodiscard]] TextSelection toDescriptionSelection(
|
||||
TextSelection selection) const;
|
||||
|
||||
[[nodiscard]] bool hasSingleLink() const;
|
||||
|
||||
const style::QuoteStyle &_st;
|
||||
const int _pixh;
|
||||
|
||||
|
||||
@@ -441,7 +441,7 @@ void StickerInBubblePart::ensureCreated(Element *replacing) const {
|
||||
_skipTop = data.skipTop;
|
||||
_sticker.emplace(_parent, sticker, skipPremiumEffect, replacing);
|
||||
if (data.singleTimePlayback) {
|
||||
_sticker->setDiceIndex(info->alt, 1);
|
||||
_sticker->setPlayingOnce(true);
|
||||
}
|
||||
_sticker->initSize(data.size);
|
||||
_sticker->setCustomCachingTag(data.cacheTag);
|
||||
|
||||
@@ -295,7 +295,7 @@ void PremiumGift::ensureStickerCreated() const {
|
||||
if (const auto sticker = document->sticker()) {
|
||||
const auto skipPremiumEffect = false;
|
||||
_sticker.emplace(_parent, document, skipPremiumEffect, _parent);
|
||||
_sticker->setDiceIndex(sticker->alt, 1);
|
||||
_sticker->setPlayingOnce(true);
|
||||
_sticker->initSize(st::msgServiceGiftBoxStickerSize);
|
||||
return;
|
||||
}
|
||||
@@ -308,7 +308,7 @@ void PremiumGift::ensureStickerCreated() const {
|
||||
if (const auto sticker = document->sticker()) {
|
||||
const auto skipPremiumEffect = false;
|
||||
_sticker.emplace(_parent, document, skipPremiumEffect, _parent);
|
||||
_sticker->setDiceIndex(sticker->alt, 1);
|
||||
_sticker->setPlayingOnce(true);
|
||||
_sticker->initSize(st::msgServiceGiftBoxStickerSize);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,7 +249,9 @@ DocumentData *Sticker::document() {
|
||||
}
|
||||
|
||||
void Sticker::stickerClearLoopPlayed() {
|
||||
_oncePlayed = false;
|
||||
if (!_playingOnce) {
|
||||
_oncePlayed = false;
|
||||
}
|
||||
_premiumEffectSkipped = false;
|
||||
}
|
||||
|
||||
@@ -304,7 +306,7 @@ void Sticker::paintAnimationFrame(
|
||||
_nextLastDiceFrame = !paused
|
||||
&& (_diceIndex > 0)
|
||||
&& (_frameIndex + 2 == count);
|
||||
const auto playOnce = (_diceIndex > 0)
|
||||
const auto playOnce = (_playingOnce || _diceIndex > 0)
|
||||
? true
|
||||
: (_diceIndex == 0)
|
||||
? false
|
||||
@@ -332,10 +334,10 @@ bool Sticker::paintPixmap(
|
||||
if (pixmap.isNull()) {
|
||||
return false;
|
||||
}
|
||||
const auto position = QPoint(
|
||||
r.x() + (r.width() - _size.width()) / 2,
|
||||
r.y() + (r.height() - _size.height()) / 2);
|
||||
const auto size = pixmap.size() / pixmap.devicePixelRatio();
|
||||
const auto position = QPoint(
|
||||
r.x() + (r.width() - size.width()) / 2,
|
||||
r.y() + (r.height() - size.height()) / 2);
|
||||
const auto mirror = mirrorHorizontal();
|
||||
if (mirror) {
|
||||
p.save();
|
||||
@@ -386,6 +388,14 @@ void Sticker::paintPath(
|
||||
|
||||
QPixmap Sticker::paintedPixmap(const PaintContext &context) const {
|
||||
auto helper = std::optional<style::owned_color>();
|
||||
const auto sticker = _data->sticker();
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
const auto adjust = [&](int side) {
|
||||
return (((side * ratio) / 8) * 8) / ratio;
|
||||
};
|
||||
const auto useSize = (sticker && sticker->type == StickerType::Tgs)
|
||||
? QSize(adjust(_size.width()), adjust(_size.height()))
|
||||
: _size;
|
||||
const auto colored = (customEmojiPart() && _data->emojiUsesTextColor())
|
||||
? &helper.emplace(ComputeEmojiTextColor(context)).color()
|
||||
: context.selected()
|
||||
@@ -393,19 +403,19 @@ QPixmap Sticker::paintedPixmap(const PaintContext &context) const {
|
||||
: nullptr;
|
||||
const auto good = _dataMedia->goodThumbnail();
|
||||
if (const auto image = _dataMedia->getStickerLarge()) {
|
||||
return image->pix(_size, { .colored = colored });
|
||||
return image->pix(useSize, { .colored = colored });
|
||||
//
|
||||
// Inline thumbnails can't have alpha channel.
|
||||
//
|
||||
//} else if (const auto blurred = _data->thumbnailInline()) {
|
||||
// return blurred->pix(
|
||||
// _size,
|
||||
// useSize,
|
||||
// { .colored = colored, .options = Images::Option::Blur });
|
||||
} else if (good) {
|
||||
return good->pix(_size, { .colored = colored });
|
||||
return good->pix(useSize, { .colored = colored });
|
||||
} else if (const auto thumbnail = _dataMedia->thumbnail()) {
|
||||
return thumbnail->pix(
|
||||
_size,
|
||||
useSize,
|
||||
{ .colored = colored, .options = Images::Option::Blur });
|
||||
}
|
||||
return QPixmap();
|
||||
@@ -511,6 +521,10 @@ void Sticker::setDiceIndex(const QString &emoji, int index) {
|
||||
_diceIndex = index;
|
||||
}
|
||||
|
||||
void Sticker::setPlayingOnce(bool once) {
|
||||
_playingOnce = once;
|
||||
}
|
||||
|
||||
void Sticker::setCustomCachingTag(ChatHelpers::StickerLottieSize tag) {
|
||||
_cachingTag = tag;
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ public:
|
||||
}
|
||||
|
||||
void setDiceIndex(const QString &emoji, int index);
|
||||
void setPlayingOnce(bool once);
|
||||
void setCustomCachingTag(ChatHelpers::StickerLottieSize tag);
|
||||
void setCustomEmojiPart();
|
||||
void setEmojiSticker();
|
||||
@@ -141,6 +142,7 @@ private:
|
||||
bool _customEmojiPart : 1 = false;
|
||||
bool _emojiSticker : 1 = false;
|
||||
bool _webpagePart : 1 = false;
|
||||
bool _playingOnce : 1 = false;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -244,9 +244,7 @@ void InnerWidget::fill() {
|
||||
),
|
||||
rpl::duplicate(availableBalanceValue),
|
||||
rpl::duplicate(dateValue),
|
||||
rpl::duplicate(dateValue) | rpl::map([=](const QDateTime &dt) {
|
||||
return !dt.isNull() || (!_state.isWithdrawalEnabled);
|
||||
}),
|
||||
_state.isWithdrawalEnabled,
|
||||
rpl::duplicate(
|
||||
availableBalanceValue
|
||||
) | rpl::map([=](StarsAmount v) {
|
||||
|
||||
@@ -37,6 +37,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "ui/new_badges.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/vertical_list.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
#include "styles/style_dialogs.h"
|
||||
@@ -753,7 +754,7 @@ object_ptr<Ui::BoxContent> ConfirmEndBox(Fn<void()> finish) {
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box,
|
||||
text(Ui::Text::RichLangValue),
|
||||
st::boxLabel),
|
||||
st::blockUserConfirmation),
|
||||
QMargins(st::boxTextFont->height, 0, 0, 0)),
|
||||
margins);
|
||||
padded->paintRequest() | rpl::start_with_next([=] {
|
||||
|
||||
@@ -417,7 +417,7 @@ void InnerWidget::fill() {
|
||||
session->data().customEmojiManager().registerInternalEmoji(
|
||||
st::topicButtonArrow,
|
||||
st::channelEarnLearnArrowMargins,
|
||||
false));
|
||||
true));
|
||||
const auto addAboutWithLearn = [&](const tr::phrase<lngtag_link> &text) {
|
||||
auto label = Ui::CreateLabelWithCustomEmoji(
|
||||
container,
|
||||
@@ -954,10 +954,7 @@ void InnerWidget::fill() {
|
||||
),
|
||||
rpl::duplicate(availableBalanceValue),
|
||||
rpl::duplicate(dateValue),
|
||||
std::move(dateValue) | rpl::map([=](const QDateTime &dt) {
|
||||
return !dt.isNull()
|
||||
|| (!_state.creditsEarn.isWithdrawalEnabled);
|
||||
}),
|
||||
_state.creditsEarn.isWithdrawalEnabled,
|
||||
rpl::duplicate(
|
||||
availableBalanceValue
|
||||
) | rpl::map(creditsToUsdMap));
|
||||
|
||||
@@ -108,9 +108,7 @@ bool InnerWidget::showInternal(not_null<Memento*> memento) {
|
||||
}
|
||||
|
||||
object_ptr<Media::ListWidget> InnerWidget::setupList() {
|
||||
auto result = object_ptr<Media::ListWidget>(
|
||||
this,
|
||||
_controller);
|
||||
auto result = object_ptr<Media::ListWidget>(this, _controller);
|
||||
result->heightValue(
|
||||
) | rpl::start_with_next(
|
||||
[this] { refreshHeight(); },
|
||||
|
||||
@@ -65,9 +65,6 @@ Widget::Widget(
|
||||
}
|
||||
|
||||
bool Widget::showInternal(not_null<ContentMemento*> memento) {
|
||||
if (!controller()->validateMementoPeer(memento)) {
|
||||
return false;
|
||||
}
|
||||
if (auto downloadsMemento = dynamic_cast<Memento*>(memento.get())) {
|
||||
restoreState(downloadsMemento);
|
||||
return true;
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "info/global_media/info_global_media_inner_widget.h"
|
||||
|
||||
#include "info/global_media/info_global_media_provider.h"
|
||||
#include "info/global_media/info_global_media_widget.h"
|
||||
#include "info/media/info_media_empty_widget.h"
|
||||
#include "info/media/info_media_list_widget.h"
|
||||
#include "info/info_controller.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/search_field_controller.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "styles/style_info.h"
|
||||
|
||||
namespace Info::GlobalMedia {
|
||||
|
||||
InnerWidget::InnerWidget(
|
||||
QWidget *parent,
|
||||
not_null<Controller*> controller)
|
||||
: RpWidget(parent)
|
||||
, _controller(controller)
|
||||
, _empty(this) {
|
||||
_empty->setType(type());
|
||||
_empty->heightValue(
|
||||
) | rpl::start_with_next(
|
||||
[this] { refreshHeight(); },
|
||||
_empty->lifetime());
|
||||
_list = setupList();
|
||||
}
|
||||
|
||||
object_ptr<Media::ListWidget> InnerWidget::setupList() {
|
||||
auto result = object_ptr<Media::ListWidget>(this, _controller);
|
||||
|
||||
// Setup list widget connections
|
||||
result->heightValue(
|
||||
) | rpl::start_with_next([this] {
|
||||
refreshHeight();
|
||||
}, result->lifetime());
|
||||
|
||||
using namespace rpl::mappers;
|
||||
result->scrollToRequests(
|
||||
) | rpl::map([widget = result.data()](int to) {
|
||||
return Ui::ScrollToRequest{
|
||||
widget->y() + to,
|
||||
-1
|
||||
};
|
||||
}) | rpl::start_to_stream(
|
||||
_scrollToRequests,
|
||||
result->lifetime());
|
||||
|
||||
_controller->searchQueryValue(
|
||||
) | rpl::start_with_next([this](const QString &query) {
|
||||
_empty->setSearchQuery(query);
|
||||
}, result->lifetime());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Storage::SharedMediaType InnerWidget::type() const {
|
||||
return _controller->section().mediaType();
|
||||
}
|
||||
|
||||
void InnerWidget::visibleTopBottomUpdated(
|
||||
int visibleTop,
|
||||
int visibleBottom) {
|
||||
setChildVisibleTopBottom(_list, visibleTop, visibleBottom);
|
||||
}
|
||||
|
||||
bool InnerWidget::showInternal(not_null<Memento*> memento) {
|
||||
if (memento->section().type() == Section::Type::GlobalMedia
|
||||
&& memento->section().mediaType() == type()) {
|
||||
restoreState(memento);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void InnerWidget::saveState(not_null<Memento*> memento) {
|
||||
_list->saveState(&memento->media());
|
||||
}
|
||||
|
||||
void InnerWidget::restoreState(not_null<Memento*> memento) {
|
||||
_list->restoreState(&memento->media());
|
||||
}
|
||||
|
||||
rpl::producer<SelectedItems> InnerWidget::selectedListValue() const {
|
||||
return _selectedLists.events_starting_with(
|
||||
_list->selectedListValue()
|
||||
) | rpl::flatten_latest();
|
||||
}
|
||||
|
||||
void InnerWidget::selectionAction(SelectionAction action) {
|
||||
_list->selectionAction(action);
|
||||
}
|
||||
|
||||
InnerWidget::~InnerWidget() = default;
|
||||
|
||||
int InnerWidget::resizeGetHeight(int newWidth) {
|
||||
_inResize = true;
|
||||
auto guard = gsl::finally([this] { _inResize = false; });
|
||||
|
||||
_list->resizeToWidth(newWidth);
|
||||
_empty->resizeToWidth(newWidth);
|
||||
return recountHeight();
|
||||
}
|
||||
|
||||
void InnerWidget::refreshHeight() {
|
||||
if (_inResize) {
|
||||
return;
|
||||
}
|
||||
resize(width(), recountHeight());
|
||||
}
|
||||
|
||||
int InnerWidget::recountHeight() {
|
||||
auto top = 0;
|
||||
auto listHeight = 0;
|
||||
if (_list) {
|
||||
_list->moveToLeft(0, top);
|
||||
listHeight = _list->heightNoMargins();
|
||||
top += listHeight;
|
||||
}
|
||||
if (listHeight > 0) {
|
||||
_empty->hide();
|
||||
} else {
|
||||
_empty->show();
|
||||
_empty->moveToLeft(0, top);
|
||||
top += _empty->heightNoMargins();
|
||||
}
|
||||
return top;
|
||||
}
|
||||
|
||||
void InnerWidget::setScrollHeightValue(rpl::producer<int> value) {
|
||||
using namespace rpl::mappers;
|
||||
_empty->setFullHeight(rpl::combine(
|
||||
std::move(value),
|
||||
_listTops.events_starting_with(
|
||||
_list->topValue()
|
||||
) | rpl::flatten_latest(),
|
||||
_1 - _2));
|
||||
}
|
||||
|
||||
rpl::producer<Ui::ScrollToRequest> InnerWidget::scrollToRequests() const {
|
||||
return _scrollToRequests.events();
|
||||
}
|
||||
|
||||
} // namespace Info::GlobalMedia
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
#include "ui/widgets/scroll_area.h"
|
||||
#include "base/unique_qptr.h"
|
||||
|
||||
namespace Ui {
|
||||
class VerticalLayout;
|
||||
class SearchFieldController;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Storage {
|
||||
enum class SharedMediaType : signed char;
|
||||
} // namespace Storage
|
||||
|
||||
namespace Info {
|
||||
class Controller;
|
||||
struct SelectedItems;
|
||||
enum class SelectionAction;
|
||||
} // namespace Info
|
||||
|
||||
namespace Info::Media {
|
||||
class ListWidget;
|
||||
class EmptyWidget;
|
||||
} // namespace Info::Media
|
||||
|
||||
namespace Info::GlobalMedia {
|
||||
|
||||
class Memento;
|
||||
class EmptyWidget;
|
||||
|
||||
class InnerWidget final : public Ui::RpWidget {
|
||||
public:
|
||||
InnerWidget(
|
||||
QWidget *parent,
|
||||
not_null<Controller*> controller);
|
||||
|
||||
bool showInternal(not_null<Memento*> memento);
|
||||
|
||||
void saveState(not_null<Memento*> memento);
|
||||
void restoreState(not_null<Memento*> memento);
|
||||
|
||||
void setScrollHeightValue(rpl::producer<int> value);
|
||||
|
||||
rpl::producer<Ui::ScrollToRequest> scrollToRequests() const;
|
||||
rpl::producer<SelectedItems> selectedListValue() const;
|
||||
void selectionAction(SelectionAction action);
|
||||
|
||||
~InnerWidget();
|
||||
|
||||
protected:
|
||||
int resizeGetHeight(int newWidth) override;
|
||||
void visibleTopBottomUpdated(
|
||||
int visibleTop,
|
||||
int visibleBottom) override;
|
||||
|
||||
private:
|
||||
int recountHeight();
|
||||
void refreshHeight();
|
||||
|
||||
Storage::SharedMediaType type() const;
|
||||
|
||||
object_ptr<Media::ListWidget> setupList();
|
||||
|
||||
const not_null<Controller*> _controller;
|
||||
|
||||
object_ptr<Media::ListWidget> _list = { nullptr };
|
||||
object_ptr<Media::EmptyWidget> _empty;
|
||||
|
||||
bool _inResize = false;
|
||||
|
||||
rpl::event_stream<Ui::ScrollToRequest> _scrollToRequests;
|
||||
rpl::event_stream<rpl::producer<SelectedItems>> _selectedLists;
|
||||
rpl::event_stream<rpl::producer<int>> _listTops;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Info::GlobalMedia
|
||||
@@ -0,0 +1,631 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "info/global_media/info_global_media_provider.h"
|
||||
|
||||
#include "apiwrap.h"
|
||||
#include "info/media/info_media_widget.h"
|
||||
#include "info/media/info_media_list_section.h"
|
||||
#include "info/info_controller.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/text/format_song_document_name.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "data/data_document.h"
|
||||
#include "data/data_media_types.h"
|
||||
#include "data/data_session.h"
|
||||
#include "main/main_session.h"
|
||||
#include "main/main_account.h"
|
||||
#include "history/history_item.h"
|
||||
#include "history/history_item_helpers.h"
|
||||
#include "history/history.h"
|
||||
#include "core/application.h"
|
||||
#include "storage/storage_shared_media.h"
|
||||
#include "layout/layout_selection.h"
|
||||
#include "styles/style_overview.h"
|
||||
|
||||
namespace Info::GlobalMedia {
|
||||
namespace {
|
||||
|
||||
constexpr auto kPerPage = 50;
|
||||
constexpr auto kPreloadedScreensCount = 4;
|
||||
constexpr auto kPreloadedScreensCountFull
|
||||
= kPreloadedScreensCount + 1 + kPreloadedScreensCount;
|
||||
|
||||
} // namespace
|
||||
|
||||
GlobalMediaSlice::GlobalMediaSlice(
|
||||
Key key,
|
||||
std::vector<Data::MessagePosition> items,
|
||||
std::optional<int> fullCount,
|
||||
int skippedAfter)
|
||||
: _key(key)
|
||||
, _items(std::move(items))
|
||||
, _fullCount(fullCount)
|
||||
, _skippedAfter(skippedAfter) {
|
||||
}
|
||||
|
||||
std::optional<int> GlobalMediaSlice::fullCount() const {
|
||||
return _fullCount;
|
||||
}
|
||||
|
||||
std::optional<int> GlobalMediaSlice::skippedBefore() const {
|
||||
return _fullCount
|
||||
? int(*_fullCount - _skippedAfter - _items.size())
|
||||
: std::optional<int>();
|
||||
}
|
||||
|
||||
std::optional<int> GlobalMediaSlice::skippedAfter() const {
|
||||
return _skippedAfter;
|
||||
}
|
||||
|
||||
std::optional<int> GlobalMediaSlice::indexOf(Value position) const {
|
||||
const auto it = ranges::find(_items, position);
|
||||
return (it != end(_items))
|
||||
? std::make_optional(int(it - begin(_items)))
|
||||
: std::nullopt;
|
||||
}
|
||||
|
||||
int GlobalMediaSlice::size() const {
|
||||
return _items.size();
|
||||
}
|
||||
|
||||
GlobalMediaSlice::Value GlobalMediaSlice::operator[](int index) const {
|
||||
Expects(index >= 0 && index < size());
|
||||
|
||||
return _items[index];
|
||||
}
|
||||
|
||||
std::optional<int> GlobalMediaSlice::distance(
|
||||
const Key &a,
|
||||
const Key &b) const {
|
||||
const auto i = indexOf(a.aroundId);
|
||||
const auto j = indexOf(b.aroundId);
|
||||
return (i && j) ? std::make_optional(*j - *i) : std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<GlobalMediaSlice::Value> GlobalMediaSlice::nearest(
|
||||
Value position) const {
|
||||
if (_items.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto it = ranges::lower_bound(
|
||||
_items,
|
||||
position,
|
||||
std::greater<>{});
|
||||
|
||||
if (it == end(_items)) {
|
||||
return _items.back();
|
||||
} else if (it == begin(_items)) {
|
||||
return _items.front();
|
||||
}
|
||||
return *it;
|
||||
}
|
||||
|
||||
Provider::Provider(not_null<AbstractController*> controller)
|
||||
: _controller(controller)
|
||||
, _type(_controller->section().mediaType())
|
||||
, _slice(sliceKey(_aroundId)) {
|
||||
_controller->session().data().itemRemoved(
|
||||
) | rpl::start_with_next([this](auto item) {
|
||||
itemRemoved(item);
|
||||
}, _lifetime);
|
||||
|
||||
style::PaletteChanged(
|
||||
) | rpl::start_with_next([=] {
|
||||
for (auto &layout : _layouts) {
|
||||
layout.second.item->invalidateCache();
|
||||
}
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
Provider::Type Provider::type() {
|
||||
return _type;
|
||||
}
|
||||
|
||||
bool Provider::hasSelectRestriction() {
|
||||
return true;
|
||||
}
|
||||
|
||||
rpl::producer<bool> Provider::hasSelectRestrictionChanges() {
|
||||
return rpl::never<bool>();
|
||||
}
|
||||
|
||||
bool Provider::sectionHasFloatingHeader() {
|
||||
switch (_type) {
|
||||
case Type::Photo:
|
||||
case Type::GIF:
|
||||
case Type::Video:
|
||||
case Type::RoundFile:
|
||||
case Type::RoundVoiceFile:
|
||||
case Type::MusicFile:
|
||||
return false;
|
||||
case Type::File:
|
||||
case Type::Link:
|
||||
return true;
|
||||
}
|
||||
Unexpected("Type in HasFloatingHeader()");
|
||||
}
|
||||
|
||||
QString Provider::sectionTitle(not_null<const BaseLayout*> item) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
bool Provider::sectionItemBelongsHere(
|
||||
not_null<const BaseLayout*> item,
|
||||
not_null<const BaseLayout*> previous) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Provider::isPossiblyMyItem(not_null<const HistoryItem*> item) {
|
||||
return item->media() != nullptr;
|
||||
}
|
||||
|
||||
std::optional<int> Provider::fullCount() {
|
||||
return _slice.fullCount();
|
||||
}
|
||||
|
||||
void Provider::restart() {
|
||||
_layouts.clear();
|
||||
_aroundId = Data::MaxMessagePosition;
|
||||
_idsLimit = kMinimalIdsLimit;
|
||||
_slice = GlobalMediaSlice(sliceKey(_aroundId));
|
||||
refreshViewer();
|
||||
}
|
||||
|
||||
void Provider::checkPreload(
|
||||
QSize viewport,
|
||||
not_null<BaseLayout*> topLayout,
|
||||
not_null<BaseLayout*> bottomLayout,
|
||||
bool preloadTop,
|
||||
bool preloadBottom) {
|
||||
const auto visibleWidth = viewport.width();
|
||||
const auto visibleHeight = viewport.height();
|
||||
const auto preloadedHeight = kPreloadedScreensCountFull * visibleHeight;
|
||||
const auto minItemHeight = Media::MinItemHeight(_type, visibleWidth);
|
||||
const auto preloadedCount = preloadedHeight / minItemHeight;
|
||||
const auto preloadIdsLimitMin = (preloadedCount / 2) + 1;
|
||||
const auto preloadIdsLimit = preloadIdsLimitMin
|
||||
+ (visibleHeight / minItemHeight);
|
||||
const auto after = _slice.skippedAfter();
|
||||
const auto topLoaded = after && (*after == 0);
|
||||
const auto before = _slice.skippedBefore();
|
||||
const auto bottomLoaded = before && (*before == 0);
|
||||
|
||||
const auto minScreenDelta = kPreloadedScreensCount
|
||||
- Media::kPreloadIfLessThanScreens;
|
||||
const auto minUniversalIdDelta = (minScreenDelta * visibleHeight)
|
||||
/ minItemHeight;
|
||||
const auto preloadAroundItem = [&](not_null<BaseLayout*> layout) {
|
||||
auto preloadRequired = false;
|
||||
auto aroundId = layout->getItem()->position();
|
||||
if (!preloadRequired) {
|
||||
preloadRequired = (_idsLimit < preloadIdsLimitMin);
|
||||
}
|
||||
if (!preloadRequired) {
|
||||
auto delta = _slice.distance(
|
||||
sliceKey(_aroundId),
|
||||
sliceKey(aroundId));
|
||||
Assert(delta != std::nullopt);
|
||||
preloadRequired = (qAbs(*delta) >= minUniversalIdDelta);
|
||||
}
|
||||
if (preloadRequired) {
|
||||
_idsLimit = preloadIdsLimit;
|
||||
_aroundId = aroundId;
|
||||
refreshViewer();
|
||||
}
|
||||
};
|
||||
|
||||
if (preloadTop && !topLoaded) {
|
||||
preloadAroundItem(topLayout);
|
||||
} else if (preloadBottom && !bottomLoaded) {
|
||||
preloadAroundItem(bottomLayout);
|
||||
}
|
||||
}
|
||||
|
||||
rpl::producer<GlobalMediaSlice> Provider::source(
|
||||
Type type,
|
||||
Data::MessagePosition aroundId,
|
||||
QString query,
|
||||
int limitBefore,
|
||||
int limitAfter) {
|
||||
Expects(_type == type);
|
||||
|
||||
_totalListQuery = query;
|
||||
return [=](auto consumer) {
|
||||
auto lifetime = rpl::lifetime();
|
||||
const auto session = &_controller->session();
|
||||
|
||||
struct State : base::has_weak_ptr {
|
||||
State(not_null<Main::Session*> session) : session(session) {
|
||||
}
|
||||
~State() {
|
||||
session->api().request(requestId).cancel();
|
||||
}
|
||||
|
||||
const not_null<Main::Session*> session;
|
||||
Fn<void()> pushAndLoadMore;
|
||||
mtpRequestId requestId = 0;
|
||||
};
|
||||
const auto state = lifetime.make_state<State>(session);
|
||||
const auto guard = base::make_weak(state);
|
||||
|
||||
state->pushAndLoadMore = [=] {
|
||||
auto result = fillRequest(aroundId, limitBefore, limitAfter);
|
||||
|
||||
// May destroy 'state' by calling source() with different args.
|
||||
consumer.put_next(std::move(result.slice));
|
||||
|
||||
if (guard && !currentList()->loaded && result.notEnough) {
|
||||
state->requestId = requestMore(state->pushAndLoadMore);
|
||||
}
|
||||
};
|
||||
state->pushAndLoadMore();
|
||||
|
||||
return lifetime;
|
||||
};
|
||||
}
|
||||
|
||||
mtpRequestId Provider::requestMore(Fn<void()> loaded) {
|
||||
const auto done = [=](const Api::GlobalMediaResult &result) {
|
||||
const auto list = currentList();
|
||||
if (result.messageIds.empty()) {
|
||||
list->loaded = true;
|
||||
list->fullCount = list->list.size();
|
||||
} else {
|
||||
list->list.reserve(list->list.size() + result.messageIds.size());
|
||||
list->fullCount = result.fullCount;
|
||||
for (const auto &position : result.messageIds) {
|
||||
_seenIds.emplace(position.fullId);
|
||||
list->offsetPosition = position;
|
||||
list->list.push_back(position);
|
||||
}
|
||||
}
|
||||
if (!result.offsetRate) {
|
||||
list->loaded = true;
|
||||
} else {
|
||||
list->offsetRate = result.offsetRate;
|
||||
}
|
||||
loaded();
|
||||
};
|
||||
const auto list = currentList();
|
||||
return _controller->session().api().requestGlobalMedia(
|
||||
_type,
|
||||
_totalListQuery,
|
||||
list->offsetRate,
|
||||
list->offsetPosition,
|
||||
done);
|
||||
}
|
||||
|
||||
Provider::FillResult Provider::fillRequest(
|
||||
Data::MessagePosition aroundId,
|
||||
int limitBefore,
|
||||
int limitAfter) {
|
||||
const auto list = currentList();
|
||||
const auto i = ranges::lower_bound(
|
||||
list->list,
|
||||
aroundId,
|
||||
std::greater<>());
|
||||
const auto hasAfter = int(i - begin(list->list));
|
||||
const auto hasBefore = int(end(list->list) - i);
|
||||
const auto takeAfter = std::min(limitAfter, hasAfter);
|
||||
const auto takeBefore = std::min(limitBefore, hasBefore);
|
||||
auto messages = std::vector<Data::MessagePosition>{
|
||||
i - takeAfter,
|
||||
i + takeBefore,
|
||||
};
|
||||
return FillResult{
|
||||
.slice = GlobalMediaSlice(
|
||||
GlobalMediaKey{ aroundId },
|
||||
std::move(messages),
|
||||
((!list->list.empty() || list->loaded)
|
||||
? list->fullCount
|
||||
: std::optional<int>()),
|
||||
hasAfter - takeAfter),
|
||||
.notEnough = (takeBefore < limitBefore),
|
||||
};
|
||||
}
|
||||
|
||||
void Provider::refreshViewer() {
|
||||
_viewerLifetime.destroy();
|
||||
_controller->searchQueryValue(
|
||||
) | rpl::map([=](QString query) {
|
||||
return source(
|
||||
_type,
|
||||
sliceKey(_aroundId).aroundId,
|
||||
query,
|
||||
_idsLimit,
|
||||
_idsLimit);
|
||||
}) | rpl::flatten_latest(
|
||||
) | rpl::start_with_next([=](GlobalMediaSlice &&slice) {
|
||||
if (!slice.fullCount()) {
|
||||
// Don't display anything while full count is unknown.
|
||||
return;
|
||||
}
|
||||
_slice = std::move(slice);
|
||||
if (auto nearest = _slice.nearest(_aroundId)) {
|
||||
_aroundId = *nearest;
|
||||
}
|
||||
_refreshed.fire({});
|
||||
}, _viewerLifetime);
|
||||
}
|
||||
|
||||
rpl::producer<> Provider::refreshed() {
|
||||
return _refreshed.events();
|
||||
}
|
||||
|
||||
std::vector<Media::ListSection> Provider::fillSections(
|
||||
not_null<Overview::Layout::Delegate*> delegate) {
|
||||
markLayoutsStale();
|
||||
const auto guard = gsl::finally([&] { clearStaleLayouts(); });
|
||||
|
||||
auto result = std::vector<Media::ListSection>();
|
||||
result.emplace_back(_type, sectionDelegate());
|
||||
auto §ion = result.back();
|
||||
for (auto i = 0, count = int(_slice.size()); i != count; ++i) {
|
||||
auto position = _slice[i];
|
||||
if (auto layout = getLayout(position.fullId, delegate)) {
|
||||
section.addItem(layout);
|
||||
}
|
||||
}
|
||||
if (section.empty()) {
|
||||
result.pop_back();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void Provider::markLayoutsStale() {
|
||||
for (auto &layout : _layouts) {
|
||||
layout.second.stale = true;
|
||||
}
|
||||
}
|
||||
|
||||
void Provider::clearStaleLayouts() {
|
||||
for (auto i = _layouts.begin(); i != _layouts.end();) {
|
||||
if (i->second.stale) {
|
||||
_layoutRemoved.fire(i->second.item.get());
|
||||
i = _layouts.erase(i);
|
||||
} else {
|
||||
++i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Provider::List *Provider::currentList() {
|
||||
return &_totalLists[_totalListQuery];
|
||||
}
|
||||
|
||||
rpl::producer<not_null<Media::BaseLayout*>> Provider::layoutRemoved() {
|
||||
return _layoutRemoved.events();
|
||||
}
|
||||
|
||||
Media::BaseLayout *Provider::lookupLayout(
|
||||
const HistoryItem *item) {
|
||||
const auto i = _layouts.find(item ? item->fullId() : FullMsgId());
|
||||
return (i != _layouts.end()) ? i->second.item.get() : nullptr;
|
||||
}
|
||||
|
||||
bool Provider::isMyItem(not_null<const HistoryItem*> item) {
|
||||
return _seenIds.contains(item->fullId());
|
||||
}
|
||||
|
||||
bool Provider::isAfter(
|
||||
not_null<const HistoryItem*> a,
|
||||
not_null<const HistoryItem*> b) {
|
||||
return (a->fullId() < b->fullId());
|
||||
}
|
||||
|
||||
void Provider::setSearchQuery(QString query) {
|
||||
Unexpected("Media::Provider::setSearchQuery.");
|
||||
}
|
||||
|
||||
GlobalMediaKey Provider::sliceKey(Data::MessagePosition aroundId) const {
|
||||
return GlobalMediaKey{ aroundId };
|
||||
}
|
||||
|
||||
void Provider::itemRemoved(not_null<const HistoryItem*> item) {
|
||||
const auto id = item->fullId();
|
||||
if (const auto i = _layouts.find(id); i != end(_layouts)) {
|
||||
_layoutRemoved.fire(i->second.item.get());
|
||||
_layouts.erase(i);
|
||||
}
|
||||
}
|
||||
|
||||
Media::BaseLayout *Provider::getLayout(
|
||||
FullMsgId itemId,
|
||||
not_null<Overview::Layout::Delegate*> delegate) {
|
||||
auto it = _layouts.find(itemId);
|
||||
if (it == _layouts.end()) {
|
||||
if (auto layout = createLayout(itemId, delegate, _type)) {
|
||||
layout->initDimensions();
|
||||
it = _layouts.emplace(
|
||||
itemId,
|
||||
std::move(layout)).first;
|
||||
} else {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
it->second.stale = false;
|
||||
return it->second.item.get();
|
||||
}
|
||||
|
||||
std::unique_ptr<Media::BaseLayout> Provider::createLayout(
|
||||
FullMsgId itemId,
|
||||
not_null<Overview::Layout::Delegate*> delegate,
|
||||
Type type) {
|
||||
const auto item = _controller->session().data().message(itemId);
|
||||
if (!item) {
|
||||
return nullptr;
|
||||
}
|
||||
const auto getPhoto = [&]() -> PhotoData* {
|
||||
if (const auto media = item->media()) {
|
||||
return media->photo();
|
||||
}
|
||||
return nullptr;
|
||||
};
|
||||
const auto getFile = [&]() -> DocumentData* {
|
||||
if (const auto media = item->media()) {
|
||||
return media->document();
|
||||
}
|
||||
return nullptr;
|
||||
};
|
||||
|
||||
const auto &songSt = st::overviewFileLayout;
|
||||
using namespace Overview::Layout;
|
||||
const auto options = [&] {
|
||||
const auto media = item->media();
|
||||
return MediaOptions{ .spoiler = media && media->hasSpoiler() };
|
||||
};
|
||||
switch (type) {
|
||||
case Type::Photo:
|
||||
if (const auto photo = getPhoto()) {
|
||||
return std::make_unique<Photo>(
|
||||
delegate,
|
||||
item,
|
||||
photo,
|
||||
options());
|
||||
}
|
||||
return nullptr;
|
||||
case Type::GIF:
|
||||
if (const auto file = getFile()) {
|
||||
return std::make_unique<Gif>(delegate, item, file);
|
||||
}
|
||||
return nullptr;
|
||||
case Type::Video:
|
||||
if (const auto file = getFile()) {
|
||||
return std::make_unique<Video>(delegate, item, file, options());
|
||||
}
|
||||
return nullptr;
|
||||
case Type::File:
|
||||
if (const auto file = getFile()) {
|
||||
return std::make_unique<Document>(
|
||||
delegate,
|
||||
item,
|
||||
DocumentFields{ .document = file },
|
||||
songSt);
|
||||
}
|
||||
return nullptr;
|
||||
case Type::MusicFile:
|
||||
if (const auto file = getFile()) {
|
||||
return std::make_unique<Document>(
|
||||
delegate,
|
||||
item,
|
||||
DocumentFields{ .document = file },
|
||||
songSt);
|
||||
}
|
||||
return nullptr;
|
||||
case Type::RoundVoiceFile:
|
||||
if (const auto file = getFile()) {
|
||||
return std::make_unique<Voice>(delegate, item, file, songSt);
|
||||
}
|
||||
return nullptr;
|
||||
case Type::Link:
|
||||
return std::make_unique<Link>(delegate, item, item->media());
|
||||
case Type::RoundFile:
|
||||
return nullptr;
|
||||
}
|
||||
Unexpected("Type in ListWidget::createLayout()");
|
||||
}
|
||||
|
||||
Media::ListItemSelectionData Provider::computeSelectionData(
|
||||
not_null<const HistoryItem*> item,
|
||||
TextSelection selection) {
|
||||
auto result = Media::ListItemSelectionData(selection);
|
||||
result.canDelete = item->canDelete();
|
||||
result.canForward = item->allowsForward();
|
||||
return result;
|
||||
}
|
||||
|
||||
bool Provider::allowSaveFileAs(
|
||||
not_null<const HistoryItem*> item,
|
||||
not_null<DocumentData*> document) {
|
||||
return item->allowsForward();
|
||||
}
|
||||
|
||||
QString Provider::showInFolderPath(
|
||||
not_null<const HistoryItem*> item,
|
||||
not_null<DocumentData*> document) {
|
||||
return document->filepath(true);
|
||||
}
|
||||
|
||||
void Provider::applyDragSelection(
|
||||
Media::ListSelectedMap &selected,
|
||||
not_null<const HistoryItem*> fromItem,
|
||||
bool skipFrom,
|
||||
not_null<const HistoryItem*> tillItem,
|
||||
bool skipTill) {
|
||||
#if 0 // not used for now
|
||||
const auto fromId = GetUniversalId(fromItem) - (skipFrom ? 1 : 0);
|
||||
const auto tillId = GetUniversalId(tillItem) - (skipTill ? 0 : 1);
|
||||
for (auto i = selected.begin(); i != selected.end();) {
|
||||
const auto itemId = GetUniversalId(i->first);
|
||||
if (itemId > fromId || itemId <= tillId) {
|
||||
i = selected.erase(i);
|
||||
} else {
|
||||
++i;
|
||||
}
|
||||
}
|
||||
for (auto &layoutItem : _layouts) {
|
||||
auto &&universalId = layoutItem.first;
|
||||
if (universalId <= fromId && universalId > tillId) {
|
||||
const auto item = layoutItem.second.item->getItem();
|
||||
ChangeItemSelection(
|
||||
selected,
|
||||
item,
|
||||
computeSelectionData(item, FullSelection));
|
||||
}
|
||||
}
|
||||
#endif // todo global media
|
||||
}
|
||||
|
||||
int64 Provider::scrollTopStatePosition(not_null<HistoryItem*> item) {
|
||||
return item->position().date;
|
||||
}
|
||||
|
||||
HistoryItem *Provider::scrollTopStateItem(Media::ListScrollTopState state) {
|
||||
const auto maybe = Data::MessagePosition{
|
||||
.date = TimeId(state.position),
|
||||
};
|
||||
if (state.item && _slice.indexOf(state.item->position())) {
|
||||
return state.item;
|
||||
} else if (const auto position = _slice.nearest(maybe)) {
|
||||
const auto id = position->fullId;
|
||||
if (const auto item = _controller->session().data().message(id)) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
return state.item;
|
||||
}
|
||||
|
||||
void Provider::saveState(
|
||||
not_null<Media::Memento*> memento,
|
||||
Media::ListScrollTopState scrollState) {
|
||||
if (_aroundId != Data::MaxMessagePosition && scrollState.item) {
|
||||
memento->setAroundId(_aroundId.fullId);
|
||||
memento->setIdsLimit(_idsLimit);
|
||||
memento->setScrollTopItem(scrollState.item->globalId());
|
||||
memento->setScrollTopItemPosition(scrollState.position);
|
||||
memento->setScrollTopShift(scrollState.shift);
|
||||
}
|
||||
}
|
||||
|
||||
void Provider::restoreState(
|
||||
not_null<Media::Memento*> memento,
|
||||
Fn<void(Media::ListScrollTopState)> restoreScrollState) {
|
||||
if (const auto limit = memento->idsLimit()) {
|
||||
_idsLimit = limit;
|
||||
_aroundId = { memento->aroundId() };
|
||||
restoreScrollState({
|
||||
.position = memento->scrollTopItemPosition(),
|
||||
.item = MessageByGlobalId(memento->scrollTopItem()),
|
||||
.shift = memento->scrollTopShift(),
|
||||
});
|
||||
refreshViewer();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Info::GlobalMedia
|
||||
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "data/data_messages.h"
|
||||
#include "info/media/info_media_common.h"
|
||||
#include "base/weak_ptr.h"
|
||||
|
||||
namespace Info {
|
||||
class AbstractController;
|
||||
} // namespace Info
|
||||
|
||||
namespace Info::GlobalMedia {
|
||||
|
||||
struct GlobalMediaKey {
|
||||
Data::MessagePosition aroundId;
|
||||
|
||||
friend inline constexpr bool operator==(
|
||||
const GlobalMediaKey &,
|
||||
const GlobalMediaKey &) = default;
|
||||
};
|
||||
|
||||
class GlobalMediaSlice final {
|
||||
public:
|
||||
using Key = GlobalMediaKey;
|
||||
using Value = Data::MessagePosition;
|
||||
|
||||
explicit GlobalMediaSlice(
|
||||
Key key,
|
||||
std::vector<Data::MessagePosition> items = {},
|
||||
std::optional<int> fullCount = std::nullopt,
|
||||
int skippedAfter = 0);
|
||||
|
||||
[[nodiscard]] std::optional<int> fullCount() const;
|
||||
[[nodiscard]] std::optional<int> skippedBefore() const;
|
||||
[[nodiscard]] std::optional<int> skippedAfter() const;
|
||||
[[nodiscard]] std::optional<int> indexOf(Value fullId) const;
|
||||
[[nodiscard]] int size() const;
|
||||
[[nodiscard]] Value operator[](int index) const;
|
||||
[[nodiscard]] std::optional<int> distance(
|
||||
const Key &a,
|
||||
const Key &b) const;
|
||||
[[nodiscard]] std::optional<Value> nearest(Value id) const;
|
||||
|
||||
private:
|
||||
GlobalMediaKey _key;
|
||||
std::vector<Data::MessagePosition> _items;
|
||||
std::optional<int> _fullCount;
|
||||
int _skippedAfter = 0;
|
||||
|
||||
};
|
||||
|
||||
class Provider final
|
||||
: public Media::ListProvider
|
||||
, private Media::ListSectionDelegate {
|
||||
public:
|
||||
using Type = Media::Type;
|
||||
using BaseLayout = Media::BaseLayout;
|
||||
|
||||
explicit Provider(not_null<AbstractController*> controller);
|
||||
|
||||
Type type() override;
|
||||
bool hasSelectRestriction() override;
|
||||
rpl::producer<bool> hasSelectRestrictionChanges() override;
|
||||
bool isPossiblyMyItem(not_null<const HistoryItem*> item) override;
|
||||
|
||||
std::optional<int> fullCount() override;
|
||||
|
||||
void restart() override;
|
||||
void checkPreload(
|
||||
QSize viewport,
|
||||
not_null<BaseLayout*> topLayout,
|
||||
not_null<BaseLayout*> bottomLayout,
|
||||
bool preloadTop,
|
||||
bool preloadBottom) override;
|
||||
void refreshViewer() override;
|
||||
rpl::producer<> refreshed() override;
|
||||
|
||||
std::vector<Media::ListSection> fillSections(
|
||||
not_null<Overview::Layout::Delegate*> delegate) override;
|
||||
rpl::producer<not_null<BaseLayout*>> layoutRemoved() override;
|
||||
BaseLayout *lookupLayout(const HistoryItem *item) override;
|
||||
bool isMyItem(not_null<const HistoryItem*> item) override;
|
||||
bool isAfter(
|
||||
not_null<const HistoryItem*> a,
|
||||
not_null<const HistoryItem*> b) override;
|
||||
|
||||
void setSearchQuery(QString query) override;
|
||||
|
||||
Media::ListItemSelectionData computeSelectionData(
|
||||
not_null<const HistoryItem*> item,
|
||||
TextSelection selection) override;
|
||||
void applyDragSelection(
|
||||
Media::ListSelectedMap &selected,
|
||||
not_null<const HistoryItem*> fromItem,
|
||||
bool skipFrom,
|
||||
not_null<const HistoryItem*> tillItem,
|
||||
bool skipTill) override;
|
||||
|
||||
bool allowSaveFileAs(
|
||||
not_null<const HistoryItem*> item,
|
||||
not_null<DocumentData*> document) override;
|
||||
QString showInFolderPath(
|
||||
not_null<const HistoryItem*> item,
|
||||
not_null<DocumentData*> document) override;
|
||||
|
||||
int64 scrollTopStatePosition(not_null<HistoryItem*> item) override;
|
||||
HistoryItem *scrollTopStateItem(
|
||||
Media::ListScrollTopState state) override;
|
||||
void saveState(
|
||||
not_null<Media::Memento*> memento,
|
||||
Media::ListScrollTopState scrollState) override;
|
||||
void restoreState(
|
||||
not_null<Media::Memento*> memento,
|
||||
Fn<void(Media::ListScrollTopState)> restoreScrollState) override;
|
||||
|
||||
private:
|
||||
static constexpr auto kMinimalIdsLimit = 16;
|
||||
|
||||
struct FillResult {
|
||||
GlobalMediaSlice slice;
|
||||
bool notEnough = false;
|
||||
};
|
||||
struct List {
|
||||
std::vector<Data::MessagePosition> list;
|
||||
Data::MessagePosition offsetPosition;
|
||||
int32 offsetRate = 0;
|
||||
int fullCount = 0;
|
||||
bool loaded = false;
|
||||
};
|
||||
|
||||
bool sectionHasFloatingHeader() override;
|
||||
QString sectionTitle(not_null<const BaseLayout*> item) override;
|
||||
bool sectionItemBelongsHere(
|
||||
not_null<const BaseLayout*> item,
|
||||
not_null<const BaseLayout*> previous) override;
|
||||
|
||||
[[nodiscard]] rpl::producer<GlobalMediaSlice> source(
|
||||
Type type,
|
||||
Data::MessagePosition aroundId,
|
||||
QString query,
|
||||
int limitBefore,
|
||||
int limitAfter);
|
||||
|
||||
[[nodiscard]] BaseLayout *getLayout(
|
||||
FullMsgId itemId,
|
||||
not_null<Overview::Layout::Delegate*> delegate);
|
||||
[[nodiscard]] std::unique_ptr<BaseLayout> createLayout(
|
||||
FullMsgId itemId,
|
||||
not_null<Overview::Layout::Delegate*> delegate,
|
||||
Type type);
|
||||
|
||||
[[nodiscard]] GlobalMediaKey sliceKey(
|
||||
Data::MessagePosition aroundId) const;
|
||||
|
||||
void itemRemoved(not_null<const HistoryItem*> item);
|
||||
void markLayoutsStale();
|
||||
void clearStaleLayouts();
|
||||
[[nodiscard]] List *currentList();
|
||||
[[nodiscard]] FillResult fillRequest(
|
||||
Data::MessagePosition aroundId,
|
||||
int limitBefore,
|
||||
int limitAfter);
|
||||
mtpRequestId requestMore(Fn<void()> loaded);
|
||||
|
||||
const not_null<AbstractController*> _controller;
|
||||
const Type _type = {};
|
||||
|
||||
Data::MessagePosition _aroundId = Data::MaxMessagePosition;
|
||||
int _idsLimit = kMinimalIdsLimit;
|
||||
GlobalMediaSlice _slice;
|
||||
|
||||
base::flat_set<FullMsgId> _seenIds;
|
||||
std::unordered_map<FullMsgId, Media::CachedItem> _layouts;
|
||||
rpl::event_stream<not_null<BaseLayout*>> _layoutRemoved;
|
||||
rpl::event_stream<> _refreshed;
|
||||
|
||||
QString _totalListQuery;
|
||||
base::flat_map<QString, List> _totalLists;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
rpl::lifetime _viewerLifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Info::GlobalMedia
|
||||
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "info/global_media/info_global_media_widget.h"
|
||||
|
||||
#include "info/global_media/info_global_media_inner_widget.h"
|
||||
#include "info/info_controller.h"
|
||||
#include "info/info_memento.h"
|
||||
#include "main/main_session.h"
|
||||
#include "ui/boxes/confirm_box.h"
|
||||
#include "ui/search_field_controller.h"
|
||||
#include "ui/widgets/menu/menu_add_action_callback.h"
|
||||
#include "ui/widgets/scroll_area.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "data/data_download_manager.h"
|
||||
#include "data/data_user.h"
|
||||
#include "core/application.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "styles/style_info.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_menu_icons.h"
|
||||
|
||||
namespace Info::GlobalMedia {
|
||||
|
||||
Memento::Memento(not_null<Controller*> controller)
|
||||
: ContentMemento(Tag{ controller->session().user() })
|
||||
, _media(controller) {
|
||||
}
|
||||
|
||||
Memento::Memento(not_null<UserData*> self, Storage::SharedMediaType type)
|
||||
: ContentMemento(Tag{ self })
|
||||
, _media(self, 0, type) {
|
||||
}
|
||||
|
||||
Memento::~Memento() = default;
|
||||
|
||||
Section Memento::section() const {
|
||||
return Section(_media.type(), Section::Type::GlobalMedia);
|
||||
}
|
||||
|
||||
object_ptr<ContentWidget> Memento::createWidget(
|
||||
QWidget *parent,
|
||||
not_null<Controller*> controller,
|
||||
const QRect &geometry) {
|
||||
auto result = object_ptr<Widget>(parent, controller);
|
||||
result->setInternalState(geometry, this);
|
||||
return result;
|
||||
}
|
||||
|
||||
Widget::Widget(QWidget *parent, not_null<Controller*> controller)
|
||||
: ContentWidget(parent, controller) {
|
||||
_inner = setInnerWidget(object_ptr<InnerWidget>(
|
||||
this,
|
||||
controller));
|
||||
_inner->setScrollHeightValue(scrollHeightValue());
|
||||
_inner->scrollToRequests(
|
||||
) | rpl::start_with_next([this](Ui::ScrollToRequest request) {
|
||||
scrollTo(request);
|
||||
}, _inner->lifetime());
|
||||
}
|
||||
|
||||
bool Widget::showInternal(not_null<ContentMemento*> memento) {
|
||||
if (auto globalMediaMemento = dynamic_cast<Memento*>(memento.get())) {
|
||||
restoreState(globalMediaMemento);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Widget::setInternalState(
|
||||
const QRect &geometry,
|
||||
not_null<Memento*> memento) {
|
||||
setGeometry(geometry);
|
||||
Ui::SendPendingMoveResizeEvents(this);
|
||||
restoreState(memento);
|
||||
}
|
||||
|
||||
std::shared_ptr<ContentMemento> Widget::doCreateMemento() {
|
||||
auto result = std::make_shared<Memento>(controller());
|
||||
saveState(result.get());
|
||||
return result;
|
||||
}
|
||||
|
||||
void Widget::saveState(not_null<Memento*> memento) {
|
||||
memento->setScrollTop(scrollTopSave());
|
||||
_inner->saveState(memento);
|
||||
}
|
||||
|
||||
void Widget::restoreState(not_null<Memento*> memento) {
|
||||
_inner->restoreState(memento);
|
||||
scrollTopRestore(memento->scrollTop());
|
||||
}
|
||||
|
||||
rpl::producer<SelectedItems> Widget::selectedListValue() const {
|
||||
return _inner->selectedListValue();
|
||||
}
|
||||
|
||||
void Widget::selectionAction(SelectionAction action) {
|
||||
_inner->selectionAction(action);
|
||||
}
|
||||
|
||||
void Widget::fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) {
|
||||
const auto window = controller()->parentController();
|
||||
const auto deleteAll = [=] {
|
||||
auto &manager = Core::App().downloadManager();
|
||||
const auto phrase = tr::lng_downloads_delete_sure_all(tr::now);
|
||||
const auto added = manager.loadedHasNonCloudFile()
|
||||
? QString()
|
||||
: tr::lng_downloads_delete_in_cloud(tr::now);
|
||||
const auto deleteSure = [=, &manager](Fn<void()> close) {
|
||||
Ui::PostponeCall(this, close);
|
||||
manager.deleteAll();
|
||||
};
|
||||
window->show(Ui::MakeConfirmBox({
|
||||
.text = phrase + (added.isEmpty() ? QString() : "\n\n" + added),
|
||||
.confirmed = deleteSure,
|
||||
.confirmText = tr::lng_box_delete(tr::now),
|
||||
.confirmStyle = &st::attentionBoxButton,
|
||||
}));
|
||||
};
|
||||
addAction(
|
||||
tr::lng_context_delete_all_files(tr::now),
|
||||
deleteAll,
|
||||
&st::menuIconDelete);
|
||||
}
|
||||
|
||||
rpl::producer<QString> Widget::title() {
|
||||
return tr::lng_profile_shared_media();
|
||||
}
|
||||
|
||||
std::shared_ptr<Info::Memento> Make(
|
||||
not_null<UserData*> self,
|
||||
Storage::SharedMediaType type) {
|
||||
return std::make_shared<Info::Memento>(
|
||||
std::vector<std::shared_ptr<ContentMemento>>(
|
||||
1,
|
||||
std::make_shared<Memento>(self, type)));
|
||||
}
|
||||
|
||||
} // namespace Info::GlobalMedia
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "info/info_content_widget.h"
|
||||
#include "info/media/info_media_widget.h"
|
||||
|
||||
namespace Storage {
|
||||
enum class SharedMediaType : signed char;
|
||||
} // namespace Storage
|
||||
|
||||
namespace Info::GlobalMedia {
|
||||
|
||||
class InnerWidget;
|
||||
|
||||
class Memento final : public ContentMemento {
|
||||
public:
|
||||
Memento(not_null<Controller*> controller);
|
||||
Memento(not_null<UserData*> self, Storage::SharedMediaType type);
|
||||
~Memento();
|
||||
|
||||
object_ptr<ContentWidget> createWidget(
|
||||
QWidget *parent,
|
||||
not_null<Controller*> controller,
|
||||
const QRect &geometry) override;
|
||||
|
||||
Section section() const override;
|
||||
|
||||
[[nodiscard]] Media::Memento &media() {
|
||||
return _media;
|
||||
}
|
||||
[[nodiscard]] const Media::Memento &media() const {
|
||||
return _media;
|
||||
}
|
||||
|
||||
private:
|
||||
Media::Memento _media;
|
||||
|
||||
};
|
||||
|
||||
class Widget final : public ContentWidget {
|
||||
public:
|
||||
Widget(QWidget *parent, not_null<Controller*> controller);
|
||||
|
||||
bool showInternal(
|
||||
not_null<ContentMemento*> memento) override;
|
||||
|
||||
void setInternalState(
|
||||
const QRect &geometry,
|
||||
not_null<Memento*> memento);
|
||||
|
||||
rpl::producer<SelectedItems> selectedListValue() const override;
|
||||
void selectionAction(SelectionAction action) override;
|
||||
|
||||
void fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) override;
|
||||
|
||||
rpl::producer<QString> title() override;
|
||||
|
||||
private:
|
||||
void saveState(not_null<Memento*> memento);
|
||||
void restoreState(not_null<Memento*> memento);
|
||||
|
||||
std::shared_ptr<ContentMemento> doCreateMemento() override;
|
||||
|
||||
InnerWidget *_inner = nullptr;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] std::shared_ptr<Info::Memento> Make(
|
||||
not_null<UserData*> self,
|
||||
Storage::SharedMediaType type);
|
||||
|
||||
} // namespace Info::GlobalMedia
|
||||
@@ -168,10 +168,17 @@ infoTopBarCall: IconButton(infoTopBarMenu) {
|
||||
rippleAreaPosition: point(0px, 6px);
|
||||
}
|
||||
infoTopBarQr: IconButton(infoTopBarMenu) {
|
||||
width: 52px;
|
||||
width: 48px;
|
||||
icon: icon {{ "menu/qr_code", boxTitleCloseFg }};
|
||||
iconOver: icon {{ "menu/qr_code", boxTitleCloseFgOver }};
|
||||
iconPosition: point(9px, -1px);
|
||||
iconPosition: point(8px, -1px);
|
||||
rippleAreaPosition: point(0px, 6px);
|
||||
}
|
||||
infoTopBarEdit: IconButton(infoTopBarMenu) {
|
||||
width: 48px;
|
||||
icon: icon {{ "menu/edit", boxTitleCloseFg }};
|
||||
iconOver: icon {{ "menu/edit", boxTitleCloseFgOver }};
|
||||
iconPosition: point(8px, -1px);
|
||||
rippleAreaPosition: point(0px, 6px);
|
||||
}
|
||||
infoTopBarForward: IconButton(infoTopBarBack) {
|
||||
@@ -273,6 +280,12 @@ infoLayerTopBarQr: IconButton(infoLayerTopBarClose) {
|
||||
iconOver: icon {{ "menu/qr_code", boxTitleCloseFgOver }};
|
||||
iconPosition: point(8px, -1px);
|
||||
}
|
||||
infoLayerTopBarEdit: IconButton(infoLayerTopBarClose) {
|
||||
width: 40px;
|
||||
icon: icon {{ "menu/edit", boxTitleCloseFg }};
|
||||
iconOver: icon {{ "menu/edit", boxTitleCloseFgOver }};
|
||||
iconPosition: point(8px, -1px);
|
||||
}
|
||||
infoLayerTopBarForward: IconButton(infoLayerTopBarBack) {
|
||||
width: 45px;
|
||||
icon: icon {{ "info/info_media_forward", boxTitleCloseFg }};
|
||||
@@ -631,7 +644,7 @@ infoMediaHeaderPosition: point(14px, 6px);
|
||||
infoMediaSkip: 2px;
|
||||
infoMediaLeft: 3px;
|
||||
infoMediaMargin: margins(0px, 6px, 0px, 2px);
|
||||
infoMediaMinGridSize: 90px;
|
||||
infoMediaMinGridSize: 82px;
|
||||
|
||||
infoCommonGroupsMargin: margins(0px, 2px, 0px, 2px);
|
||||
infoCommonGroupsListItem: PeerListItem(defaultPeerListItem) {
|
||||
@@ -1150,3 +1163,9 @@ infoHoursOuter: RoundButton(defaultActiveButton) {
|
||||
}
|
||||
infoHoursOuterMargin: margins(8px, 4px, 8px, 4px);
|
||||
infoHoursDaySkip: 6px;
|
||||
|
||||
infoSharedMediaScroll: ScrollArea(defaultScrollArea) {
|
||||
round: 1px;
|
||||
width: 5px;
|
||||
deltax: 2px;
|
||||
}
|
||||
|
||||
@@ -42,7 +42,11 @@ ContentWidget::ContentWidget(
|
||||
not_null<Controller*> controller)
|
||||
: RpWidget(parent)
|
||||
, _controller(controller)
|
||||
, _scroll(this) {
|
||||
, _scroll(
|
||||
this,
|
||||
(_controller->wrap() == Wrap::Search
|
||||
? st::infoSharedMediaScroll
|
||||
: st::defaultScrollArea)) {
|
||||
using namespace rpl::mappers;
|
||||
|
||||
setAttribute(Qt::WA_OpaquePaintEvent);
|
||||
@@ -289,6 +293,18 @@ void ContentWidget::fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) {
|
||||
addAction);
|
||||
}
|
||||
|
||||
void ContentWidget::checkBeforeCloseByEscape(Fn<void()> close) {
|
||||
if (_searchField) {
|
||||
if (!_searchField->empty()) {
|
||||
_searchField->setText({});
|
||||
} else {
|
||||
close();
|
||||
}
|
||||
} else {
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
rpl::producer<SelectedItems> ContentWidget::selectedListValue() const {
|
||||
return rpl::single(SelectedItems(Storage::SharedMediaType::Photo));
|
||||
}
|
||||
@@ -382,6 +398,8 @@ Key ContentMemento::key() const {
|
||||
return BotStarRef::Tag(starref, starrefType());
|
||||
} else if (const auto who = reactionsWhoReadIds()) {
|
||||
return Key(who, _reactionsSelected, _pollReactionsContextId);
|
||||
} else if (const auto another = globalMediaSelf()) {
|
||||
return GlobalMedia::Tag{ another };
|
||||
} else {
|
||||
return Downloads::Tag();
|
||||
}
|
||||
@@ -427,6 +445,10 @@ ContentMemento::ContentMemento(BotStarRef::Tag starref)
|
||||
, _starrefType(starref.type) {
|
||||
}
|
||||
|
||||
ContentMemento::ContentMemento(GlobalMedia::Tag global)
|
||||
: _globalMediaSelf(global.self) {
|
||||
}
|
||||
|
||||
ContentMemento::ContentMemento(
|
||||
std::shared_ptr<Api::WhoReadList> whoReadIds,
|
||||
FullMsgId contextId,
|
||||
|
||||
@@ -57,6 +57,10 @@ enum class Type : uchar;
|
||||
struct Tag;
|
||||
} // namespace Info::BotStarRef
|
||||
|
||||
namespace Info::GlobalMedia {
|
||||
struct Tag;
|
||||
} // namespace Info::GlobalMedia
|
||||
|
||||
namespace Info {
|
||||
|
||||
class ContentMemento;
|
||||
@@ -115,6 +119,7 @@ public:
|
||||
virtual void checkBeforeClose(Fn<void()> close) {
|
||||
close();
|
||||
}
|
||||
virtual void checkBeforeCloseByEscape(Fn<void()> close);
|
||||
[[nodiscard]] virtual rpl::producer<QString> title() = 0;
|
||||
[[nodiscard]] virtual rpl::producer<QString> subtitle() {
|
||||
return nullptr;
|
||||
@@ -197,6 +202,7 @@ public:
|
||||
explicit ContentMemento(Stories::Tag stories);
|
||||
explicit ContentMemento(Statistics::Tag statistics);
|
||||
explicit ContentMemento(BotStarRef::Tag starref);
|
||||
explicit ContentMemento(GlobalMedia::Tag global);
|
||||
ContentMemento(not_null<PollData*> poll, FullMsgId contextId)
|
||||
: _poll(poll)
|
||||
, _pollReactionsContextId(contextId) {
|
||||
@@ -253,6 +259,9 @@ public:
|
||||
FullMsgId reactionsContextId() const {
|
||||
return _reactionsWhoReadIds ? _pollReactionsContextId : FullMsgId();
|
||||
}
|
||||
UserData *globalMediaSelf() const {
|
||||
return _globalMediaSelf;
|
||||
}
|
||||
Key key() const;
|
||||
|
||||
virtual Section section() const = 0;
|
||||
@@ -298,6 +307,7 @@ private:
|
||||
std::shared_ptr<Api::WhoReadList> _reactionsWhoReadIds;
|
||||
Data::ReactionId _reactionsSelected;
|
||||
const FullMsgId _pollReactionsContextId;
|
||||
UserData * const _globalMediaSelf = nullptr;
|
||||
|
||||
int _scrollTop = 0;
|
||||
QString _searchFieldQuery;
|
||||
|
||||
@@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "data/data_shared_media.h"
|
||||
#include "info/info_content_widget.h"
|
||||
#include "info/info_memento.h"
|
||||
#include "info/global_media/info_global_media_widget.h"
|
||||
#include "info/media/info_media_widget.h"
|
||||
#include "core/application.h"
|
||||
#include "data/data_changes.h"
|
||||
@@ -49,6 +50,9 @@ Key::Key(Statistics::Tag statistics) : _value(statistics) {
|
||||
Key::Key(BotStarRef::Tag starref) : _value(starref) {
|
||||
}
|
||||
|
||||
Key::Key(GlobalMedia::Tag global) : _value(global) {
|
||||
}
|
||||
|
||||
Key::Key(not_null<PollData*> poll, FullMsgId contextId)
|
||||
: _value(PollKey{ poll, contextId }) {
|
||||
}
|
||||
@@ -88,6 +92,10 @@ bool Key::isDownloads() const {
|
||||
return v::is<Downloads::Tag>(_value);
|
||||
}
|
||||
|
||||
bool Key::isGlobalMedia() const {
|
||||
return v::is<GlobalMedia::Tag>(_value);
|
||||
}
|
||||
|
||||
PeerData *Key::storiesPeer() const {
|
||||
if (const auto tag = std::get_if<Stories::Tag>(&_value)) {
|
||||
return tag->peer;
|
||||
@@ -350,7 +358,8 @@ void Controller::updateSearchControllers(
|
||||
not_null<ContentMemento*> memento) {
|
||||
using Type = Section::Type;
|
||||
const auto type = _section.type();
|
||||
const auto isMedia = (type == Type::Media);
|
||||
const auto isMedia = (type == Type::Media)
|
||||
|| (type == Type::GlobalMedia);
|
||||
const auto mediaType = isMedia
|
||||
? _section.mediaType()
|
||||
: Section::MediaType::kCount;
|
||||
@@ -362,13 +371,12 @@ void Controller::updateSearchControllers(
|
||||
const auto hasMembersSearch = (type == Type::Members)
|
||||
|| (type == Type::Profile);
|
||||
const auto searchQuery = memento->searchFieldQuery();
|
||||
if (isMedia) {
|
||||
if (type == Type::Media) {
|
||||
_searchController
|
||||
= std::make_unique<Api::DelayedSearchController>(&session());
|
||||
auto mediaMemento = dynamic_cast<Media::Memento*>(memento.get());
|
||||
Assert(mediaMemento != nullptr);
|
||||
_searchController->restoreState(
|
||||
mediaMemento->searchState());
|
||||
_searchController->restoreState(mediaMemento->searchState());
|
||||
} else {
|
||||
_searchController = nullptr;
|
||||
}
|
||||
@@ -449,7 +457,8 @@ rpl::producer<QString> Controller::mediaSourceQueryValue() const {
|
||||
}
|
||||
|
||||
rpl::producer<QString> Controller::searchQueryValue() const {
|
||||
return searchFieldController()->queryValue();
|
||||
const auto controller = searchFieldController();
|
||||
return controller ? controller->queryValue() : rpl::single(QString());
|
||||
}
|
||||
|
||||
rpl::producer<SparseIdsMergedSlice> Controller::mediaSource(
|
||||
|
||||