Compare commits

...

75 Commits

Author SHA1 Message Date
John Preston
4505a2bf2d Beta version 5.9.2.
- Fix some jump-to-message highlightings.
- Fix some long round video messages sending.
- Fix possible crash in global photos / videos.
- Fix streaming starting for some large video files.
- Fix possible drop of top peers / recent chats cache.
- Fix possible crashes in audio on macOS (rollback OpenAL).
2024-12-24 10:44:15 +04:00
John Preston
a314380b08 Rollback OpenAL on macOS. 2024-12-23 22:51:33 +04:00
John Preston
0c07a015c6 Always show VIEW DISCUSSION from comments. 2024-12-23 22:46:43 +04:00
John Preston
a0c7697280 Fix affiliate program ending confirmation box.
Fixes #28780.
2024-12-23 22:46:42 +04:00
John Preston
04023da723 Highlight search query part in separate window chat. 2024-12-23 21:29:40 +04:00
John Preston
13ea045055 Fix collision of dice_sticker last frames. 2024-12-23 21:29:40 +04:00
John Preston
f03351d112 Allow filter gifts by "In Stock". 2024-12-23 21:29:40 +04:00
John Preston
48d9f10f5b Improve message part highlighting. 2024-12-23 21:29:39 +04:00
John Preston
2b53df98cd Fix long message parts highlighting in topics. 2024-12-23 21:29:34 +04:00
John Preston
99a7a13218 Always send video messages as not "big" files. 2024-12-23 21:27:12 +04:00
John Preston
2d2d4ac002 Reduce video message bitrate so they always fit. 2024-12-23 21:27:12 +04:00
John Preston
d12e8023e3 Fix scheduled replies loading. 2024-12-23 21:27:12 +04:00
John Preston
17181cee8f Fix possible crash in Forward folders switch. 2024-12-23 21:27:12 +04:00
23rd
a74ee911b3 Removed chats filters strip from forward box while search. 2024-12-23 11:02:09 +04:00
Ilya Fedin
b0b37172ce Fix the naming of TitleControlsLayout static methods 2024-12-23 11:01:35 +04:00
Pavel Zolotarevskiy
cccf048e3f Don't export a duplicate "text" field on star gift
Fixes #28781
2024-12-21 22:32:12 +04:00
John Preston
82e890746b Fix possible crash in global media search. 2024-12-20 21:07:20 +04:00
John Preston
188d65d700 Improve streaming of large files. 2024-12-20 21:07:20 +04:00
John Preston
4569f93e70 Fix username disappearance in My Profile. 2024-12-20 21:07:20 +04:00
23rd
bcd1d8461f Added ability to open context menu for active account from main menu. 2024-12-20 19:33:06 +03:00
23rd
183a9139f9 Moved out ability to mark as read all chats from hidden shortcut. 2024-12-20 19:33:06 +03:00
23rd
80a1e6ecf3 Fixed ability to mark as read all chats for wrong account. 2024-12-20 19:33:06 +03:00
23rd
aa1f8cfb8f Fixed display at least one injected sponsored message. 2024-12-20 15:53:43 +03:00
23rd
8060691f3d Fixed preview of chats filters in filter link box when window is small. 2024-12-19 15:54:26 +03:00
Ilya Fedin
bf26de495a Adapt to TitleControlsLayout change 2024-12-19 15:59:25 +04:00
Ilya Fedin
73b3f7e298 Adapt to TitleControlsOnLeft change 2024-12-19 15:59:25 +04:00
Ilya Fedin
22191649aa Update lib_base and lib_ui 2024-12-19 15:59:25 +04:00
bitxer
0557907310 Enhance experimental setting description 2024-12-19 15:49:24 +04:00
bitxer
0f283c484d Added experimental settings to prefer ipv6 when it is available 2024-12-19 15:49:24 +04:00
John Preston
e33ca9d316 Fix top/recent peers cache write error. 2024-12-19 13:51:47 +04:00
John Preston
f93f4c72f7 Beta version 5.9.1.
- Add global media overview tabs in chats search.
- Add main menu "My Profile" with my stories and gifts access.
- Highlight some of search query on result message open.
- Fix highlighting quotes from bottom parts of long messages.
- Allow forward and reply bars together.
- Make gift price categories scrollable.
- Auto-send deep-link /start in existing bot chats.
2024-12-18 19:12:12 +04:00
23rd
f0b9bc10c2 Added fade effect to price categories in star gift box. 2024-12-18 17:39:46 +03:00
John Preston
f583879aee Update OpenAL to 1.24.1 on Windows/macOS. 2024-12-18 18:22:54 +04:00
23rd
7ea6c6c84b Fixed width of username label with button for QR in profiles. 2024-12-18 16:21:07 +03:00
23rd
2532a0ff59 Moved out to single place ministars creation in top of box. 2024-12-18 15:47:57 +03:00
23rd
c3f354826d Fixed text color of custom icon in some phrases with links. 2024-12-18 15:01:57 +03:00
23rd
6d1e421ad7 Fixed color of strikeout format when spellcheck underline is present. 2024-12-18 11:54:26 +03:00
23rd
29b0055e39 Slightly improved some phrases on error while add participant to chat. 2024-12-18 11:03:44 +03:00
Ilya Fedin
2cb20fe342 Switch macOS packaged action to latest ffmpeg 2024-12-18 10:53:43 +04:00
Ilya Fedin
fc97fa4415 Fix snap action 2024-12-18 09:56:23 +04:00
Ilya Fedin
876a50f759 Update OpenAL to 1.24.1 on Linux 2024-12-18 09:55:58 +04:00
Ilya Fedin
eb0d2868f5 Expand "always run in background" behavior from GNOME/Pantheon to all Linux
Right now it checks the title controls layout that is typically set only by gtk based DEs and KDE matching the GNOME's and Pantheon's defaults.

There are more and more reports about window manager not to supporting both tray and minimization out of the box and title controls layout seem to typically be either unset or set to nothing leaving users with no way to run tdesktop with no window open.
2024-12-18 09:55:35 +04:00
John Preston
e215d5bc64 Fix build with Xcode. 2024-12-17 21:27:08 +04:00
John Preston
3f0d687656 Add cache for global media search requests. 2024-12-17 21:17:14 +04:00
John Preston
d59eb8e731 Support global media in chats search. 2024-12-17 21:17:14 +04:00
John Preston
04e9eed88d Reuse filters slider scroll for search. 2024-12-17 21:17:14 +04:00
John Preston
5072e95f16 Support folders strip touch-screen scroll. 2024-12-17 21:17:14 +04:00
John Preston
a2b8366477 Show forward original date in context menu. 2024-12-17 21:17:14 +04:00
John Preston
e9a6bee046 Fix sending old topic messages. 2024-12-17 21:13:29 +04:00
John Preston
080a8d7ee5 Fix "Open" miniapp button antialiasing. 2024-12-17 21:13:29 +04:00
John Preston
f94fd3118b Add "My Profile" instead of "My Stories". 2024-12-17 21:13:29 +04:00
John Preston
8ddb13d6e2 Show verified/premium badge in chat preview. 2024-12-17 21:13:29 +04:00
John Preston
c6cf8be8d4 Redesign gift visibility toggle. 2024-12-17 21:13:29 +04:00
John Preston
e92270a9ab Add "View Discussion" button to third column. 2024-12-17 21:13:29 +04:00
John Preston
65d6636a41 Add special toast title for anonymous stars. 2024-12-17 21:13:29 +04:00
John Preston
4701badb2a Highlight text in bottom of a long bubble. 2024-12-17 21:13:29 +04:00
John Preston
3565215c81 Highlight word from search query. 2024-12-17 21:13:29 +04:00
John Preston
3957fea5e4 Send start in bots auto in existing bot chats. 2024-12-17 21:13:29 +04:00
John Preston
10f1ae152d Fix crash in sending games from inline bots.
Regression was introduced in 2d1fb0562d.
2024-12-17 21:13:29 +04:00
John Preston
eb29b6bffe Allow forward+reply, options in single box. 2024-12-17 21:13:29 +04:00
John Preston
d157eb0b6e Enter selects from-row in reply-in-another-chat. 2024-12-17 21:13:29 +04:00
John Preston
0045eb4598 Make price categories scrollable. 2024-12-17 21:13:27 +04:00
23rd
b61c66c385 Fixed display of title widgets in separate panel while show animation. 2024-12-16 06:46:36 +03:00
23rd
56d6c4eb30 Fixed mouse track on right button for bots when there is unread badge. 2024-12-16 05:45:56 +03:00
23rd
a6030d708d Fixed display of unread state in chats filters strip after reorder. 2024-12-16 05:45:56 +03:00
23rd
683c3c4f36 Fixed Escape hotkey in info sections with search field. 2024-12-16 05:45:56 +03:00
23rd
bd084f9181 Fixed blink of submenu in profile section on section destroy. 2024-12-16 05:45:56 +03:00
23rd
fef133bf0a Fixed incorrect action type of userpic change with image from clipboard.
Fixed #28731.
2024-12-16 05:45:56 +03:00
23rd
15c226e6cf Removed confusing lock state from button in earn out section. 2024-12-16 05:45:56 +03:00
23rd
84f111d641 Fixed unreachable bottom button from contact media in some cases. 2024-12-16 05:45:55 +03:00
23rd
18c1e7ac60 Removed animation cache from dialogs widget on instant clear search. 2024-12-16 05:45:55 +03:00
Daniel Novomeský
ee6dbdced6 Update libjxl and libheif on Linux 2024-12-14 17:50:51 +04:00
Daniel Novomeský
cb443d797d Update kimageformats submodule 2024-12-14 17:50:51 +04:00
Daniel Novomeský
5a6497ec70 Upgrade libheif to 1.18.2, upgrade libjxl to 0.11.1 2024-12-14 17:50:51 +04:00
Andrey Egorov
893ca8bcbd Residence country instead of Citizenship 2024-12-06 18:29:17 +04:00
168 changed files with 3950 additions and 1189 deletions

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 718 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 763 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 963 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 691 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 615 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 959 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D1ED}"_cs;
constexpr auto AppNameOld = "Telegram Win (Unofficial)"_cs;
constexpr auto AppName = "Telegram Desktop"_cs;
constexpr auto AppFile = "Telegram"_cs;
constexpr auto AppVersion = 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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 &quote);
[[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 &quote) {
_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(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -668,4 +668,9 @@ private:
};
[[nodiscard]] int FindViewY(
not_null<Element*> view,
uint16 symbol,
int yfrom = 0);
} // namespace HistoryView

View File

@@ -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 &params,
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(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -68,6 +68,8 @@ private:
[[nodiscard]] TextSelection toDescriptionSelection(
TextSelection selection) const;
[[nodiscard]] bool hasSingleLink() const;
const style::QuoteStyle &_st;
const int _pixh;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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([=] {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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