Merge tag 'v4.14.6' into dev

# Conflicts:
#	Telegram/Resources/winrc/Telegram.rc
#	Telegram/Resources/winrc/Updater.rc
#	Telegram/SourceFiles/core/version.h
#	Telegram/SourceFiles/dialogs/dialogs_widget.cpp
#	Telegram/SourceFiles/dialogs/dialogs_widget.h
#	Telegram/SourceFiles/history/history_item_helpers.cpp
#	Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp
#	Telegram/lib_ui
#	lib/xdg/org.telegram.desktop.metainfo.xml
#	snap/snapcraft.yaml
This commit is contained in:
ZavaruKitsu 2024-01-16 23:44:24 +03:00
commit abf17407ea
149 changed files with 2580 additions and 501 deletions

View file

@ -73,6 +73,7 @@ jobs:
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
xcodebuild -version > CACHE_KEY.txt xcodebuild -version > CACHE_KEY.txt
brew list --versions >> CACHE_KEY.txt
echo $MANUAL_CACHING >> CACHE_KEY.txt echo $MANUAL_CACHING >> CACHE_KEY.txt
echo "$GITHUB_WORKSPACE" >> CACHE_KEY.txt echo "$GITHUB_WORKSPACE" >> CACHE_KEY.txt
if [ "$AUTO_CACHING" = "1" ]; then if [ "$AUTO_CACHING" = "1" ]; then

View file

@ -478,6 +478,8 @@ PRIVATE
chat_helpers/tabbed_section.h chat_helpers/tabbed_section.h
chat_helpers/tabbed_selector.cpp chat_helpers/tabbed_selector.cpp
chat_helpers/tabbed_selector.h chat_helpers/tabbed_selector.h
chat_helpers/ttl_media_layer_widget.cpp
chat_helpers/ttl_media_layer_widget.h
core/application.cpp core/application.cpp
core/application.h core/application.h
core/base_integration.cpp core/base_integration.cpp
@ -681,6 +683,8 @@ PRIVATE
dialogs/dialogs_row.h dialogs/dialogs_row.h
dialogs/dialogs_search_from_controllers.cpp dialogs/dialogs_search_from_controllers.cpp
dialogs/dialogs_search_from_controllers.h dialogs/dialogs_search_from_controllers.h
dialogs/dialogs_search_tags.cpp
dialogs/dialogs_search_tags.h
dialogs/dialogs_widget.cpp dialogs/dialogs_widget.cpp
dialogs/dialogs_widget.h dialogs/dialogs_widget.h
dialogs/ui/dialogs_layout.cpp dialogs/ui/dialogs_layout.cpp

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg baseProfile="tiny" version="1.2" viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
<path d="m27.57 30.79q0.77-0.44 1.14-1.28 4.38-9.86 4.67-24.25 0.03-1.64 1.63-1.54 1.14 0.07 1.9 0.65c14.45 10.9 28.35 31.97 18.06 50.37-9.55 17.08-32.38 15.75-41.59-0.69-5.25-9.37-0.83-23.06 4.26-32.03a2.13 2.12 43.5 0 1 3.64-0.09l5.53 8.68a0.57 0.56-31.3 0 0 0.76 0.18z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 427 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 776 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: 561 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1,011 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 895 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 529 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -1719,6 +1719,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_ttl_voice_expired" = "Voice message expired"; "lng_ttl_voice_expired" = "Voice message expired";
"lng_ttl_round_sent" = "You sent a self-destructing video message."; "lng_ttl_round_sent" = "You sent a self-destructing video message.";
"lng_ttl_round_expired" = "Round message expired"; "lng_ttl_round_expired" = "Round message expired";
"lng_ttl_voice_tooltip_in" = "This voice message can only be played once.";
"lng_ttl_voice_tooltip_out" = "This message will disappear once **{user}** plays it once.";
"lng_ttl_voice_close_in" = "Delete and close";
"lng_ttl_round_tooltip_in" = "This video message can only be played once.";
"lng_ttl_round_tooltip_out" = "This message will disappear once **{user}** plays it once.";
"lng_profile_add_more_after_create" = "You will be able to add more members after you create the group."; "lng_profile_add_more_after_create" = "You will be able to add more members after you create the group.";
"lng_profile_camera_title" = "Capture yourself"; "lng_profile_camera_title" = "Capture yourself";
@ -2487,6 +2492,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_record_listen_cancel_sure" = "Are you sure you want to discard your recorded voice message?"; "lng_record_listen_cancel_sure" = "Are you sure you want to discard your recorded voice message?";
"lng_record_lock_discard" = "Discard"; "lng_record_lock_discard" = "Discard";
"lng_record_hold_tip" = "Please hold the mouse button pressed to record a voice message."; "lng_record_hold_tip" = "Please hold the mouse button pressed to record a voice message.";
"lng_record_once_first_tooltip" = "Tap to set this message to **Play Once**.";
"lng_record_once_active_tooltip" = "The recipients will be able to listen to it only once.";
"lng_will_be_notified" = "Members will be notified when you post"; "lng_will_be_notified" = "Members will be notified when you post";
"lng_wont_be_notified" = "Members will not be notified when you post"; "lng_wont_be_notified" = "Members will not be notified when you post";
"lng_willbe_history" = "Please select a chat to start messaging"; "lng_willbe_history" = "Please select a chat to start messaging";
@ -2749,7 +2756,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_context_seen_reacted#other" = "{count} Reacted"; "lng_context_seen_reacted#other" = "{count} Reacted";
"lng_context_seen_reacted_none" = "Nobody Reacted"; "lng_context_seen_reacted_none" = "Nobody Reacted";
"lng_context_seen_reacted_all" = "Show All Reactions"; "lng_context_seen_reacted_all" = "Show All Reactions";
"lng_context_set_as_quick" = "Set As Quick"; "lng_context_set_as_quick" = "Set as Quick";
"lng_context_filter_by_tag" = "Filter by Tag";
"lng_context_remove_tag" = "Remove Tag";
"lng_context_delete_from_disk" = "Delete from disk"; "lng_context_delete_from_disk" = "Delete from disk";
"lng_context_delete_all_files" = "Delete all files"; "lng_context_delete_all_files" = "Delete all files";
"lng_context_save_custom_sound" = "Save for notifications"; "lng_context_save_custom_sound" = "Save for notifications";
@ -4346,6 +4355,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_stories_views#one" = "{count} view"; "lng_stories_views#one" = "{count} view";
"lng_stories_views#other" = "{count} views"; "lng_stories_views#other" = "{count} views";
"lng_stories_no_views" = "No views"; "lng_stories_no_views" = "No views";
"lng_stories_view_reactions" = "View reactions";
"lng_stories_unsupported" = "This story is not supported\nby your version of Telegram."; "lng_stories_unsupported" = "This story is not supported\nby your version of Telegram.";
"lng_stories_cant_reply" = "You can't reply to this story."; "lng_stories_cant_reply" = "You can't reply to this story.";
"lng_stories_about_silent" = "This video has no sound."; "lng_stories_about_silent" = "This video has no sound.";

View file

@ -26,6 +26,7 @@
<file alias="recording/info_audio.svg">../../art/recording/recording_info_audio.svg</file> <file alias="recording/info_audio.svg">../../art/recording/recording_info_audio.svg</file>
<file alias="recording/info_video_landscape.svg">../../art/recording/recording_info_video_landscape.svg</file> <file alias="recording/info_video_landscape.svg">../../art/recording/recording_info_video_landscape.svg</file>
<file alias="recording/info_video_portrait.svg">../../art/recording/recording_info_video_portrait.svg</file> <file alias="recording/info_video_portrait.svg">../../art/recording/recording_info_video_portrait.svg</file>
<file alias="ttl/video_message_icon.svg">../../art/ttl/video_message_icon.svg</file>
<file alias="icons/settings/dino.svg">../../icons/settings/dino.svg</file> <file alias="icons/settings/dino.svg">../../icons/settings/dino.svg</file>
<file alias="icons/settings/star.svg">../../icons/settings/star.svg</file> <file alias="icons/settings/star.svg">../../icons/settings/star.svg</file>
<file alias="icons/settings/starmini.svg">../../icons/settings/starmini.svg</file> <file alias="icons/settings/starmini.svg">../../icons/settings/starmini.svg</file>

View file

@ -10,7 +10,7 @@
<Identity Name="TelegramMessengerLLP.TelegramDesktop" <Identity Name="TelegramMessengerLLP.TelegramDesktop"
ProcessorArchitecture="ARCHITECTURE" ProcessorArchitecture="ARCHITECTURE"
Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A" Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
Version="4.14.3.0" /> Version="4.14.6.0" />
<Properties> <Properties>
<DisplayName>Telegram Desktop</DisplayName> <DisplayName>Telegram Desktop</DisplayName>
<PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName> <PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName>

View file

@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico"
// //
VS_VERSION_INFO VERSIONINFO VS_VERSION_INFO VERSIONINFO
FILEVERSION 4,14,3,0 FILEVERSION 4,14,6,0
PRODUCTVERSION 4,14,3,0 PRODUCTVERSION 4,14,6,0
FILEFLAGSMASK 0x3fL FILEFLAGSMASK 0x3fL
#ifdef _DEBUG #ifdef _DEBUG
FILEFLAGS 0x1L FILEFLAGS 0x1L
@ -62,10 +62,10 @@ BEGIN
BEGIN BEGIN
VALUE "CompanyName", "Radolyn Labs" VALUE "CompanyName", "Radolyn Labs"
VALUE "FileDescription", "AyuGram Desktop" VALUE "FileDescription", "AyuGram Desktop"
VALUE "FileVersion", "4.14.3.0" VALUE "FileVersion", "4.14.6.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2024" VALUE "LegalCopyright", "Copyright (C) 2014-2024"
VALUE "ProductName", "AyuGram Desktop" VALUE "ProductName", "AyuGram Desktop"
VALUE "ProductVersion", "4.14.3.0" VALUE "ProductVersion", "4.14.6.0"
END END
END END
BLOCK "VarFileInfo" BLOCK "VarFileInfo"

View file

@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
// //
VS_VERSION_INFO VERSIONINFO VS_VERSION_INFO VERSIONINFO
FILEVERSION 4,14,3,0 FILEVERSION 4,14,6,0
PRODUCTVERSION 4,14,3,0 PRODUCTVERSION 4,14,6,0
FILEFLAGSMASK 0x3fL FILEFLAGSMASK 0x3fL
#ifdef _DEBUG #ifdef _DEBUG
FILEFLAGS 0x1L FILEFLAGS 0x1L
@ -53,10 +53,10 @@ BEGIN
BEGIN BEGIN
VALUE "CompanyName", "Radolyn Labs" VALUE "CompanyName", "Radolyn Labs"
VALUE "FileDescription", "AyuGram Desktop Updater" VALUE "FileDescription", "AyuGram Desktop Updater"
VALUE "FileVersion", "4.14.3.0" VALUE "FileVersion", "4.14.6.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2024" VALUE "LegalCopyright", "Copyright (C) 2014-2024"
VALUE "ProductName", "AyuGram Desktop" VALUE "ProductName", "AyuGram Desktop"
VALUE "ProductVersion", "4.14.3.0" VALUE "ProductVersion", "4.14.6.0"
END END
END END
BLOCK "VarFileInfo" BLOCK "VarFileInfo"

View file

@ -91,6 +91,7 @@ void MessagesSearch::searchRequest() {
? _from->input ? _from->input
: MTP_inputPeerEmpty()), : MTP_inputPeerEmpty()),
MTPInputPeer(), // saved_peer_id MTPInputPeer(), // saved_peer_id
MTPVector<MTPReaction>(), // saved_reaction
MTPint(), // top_msg_id MTPint(), // top_msg_id
MTP_inputMessagesFilterEmpty(), MTP_inputMessagesFilterEmpty(),
MTP_int(0), // min_date MTP_int(0), // min_date

View file

@ -402,7 +402,6 @@ void SendConfirmedFile(
flags |= MessageFlag::HasReplyInfo; flags |= MessageFlag::HasReplyInfo;
} }
const auto anonymousPost = peer->amAnonymous(); const auto anonymousPost = peer->amAnonymous();
const auto silentPost = ShouldSendSilent(peer, file->to.options);
FillMessagePostFlags(action, peer, flags); FillMessagePostFlags(action, peer, flags);
if (file->to.options.scheduled) { if (file->to.options.scheduled) {
flags |= MessageFlag::IsOrWasScheduled; flags |= MessageFlag::IsOrWasScheduled;

View file

@ -2540,6 +2540,10 @@ void Updates::feedUpdate(const MTPUpdate &update) {
session().data().reactions().refreshRecentDelayed(); session().data().reactions().refreshRecentDelayed();
} break; } break;
case mtpc_updateSavedReactionTags: {
session().data().reactions().refreshMyTagsDelayed();
} break;
////// Cloud saved GIFs ////// Cloud saved GIFs
case mtpc_updateSavedGifs: { case mtpc_updateSavedGifs: {
session().data().stickers().setLastSavedGifsUpdate(0); session().data().stickers().setLastSavedGifsUpdate(0);

View file

@ -162,7 +162,7 @@ void AutoDownloadBox::setupContent() {
*downloadValues, *downloadValues,
*autoPlayValues); *autoPlayValues);
auto allowMore = values | ranges::views::filter([&](Pair pair) { auto allowMore = values | ranges::views::filter([&](Pair pair) {
const auto [type, enabled] = pair; const auto &[type, enabled] = pair;
const auto value = enabled ? limitByType(type) : 0; const auto value = enabled ? limitByType(type) : 0;
const auto old = settings->bytesLimit(_source, type); const auto old = settings->bytesLimit(_source, type);
return (old < value); return (old < value);
@ -170,7 +170,7 @@ void AutoDownloadBox::setupContent() {
return pair.first; return pair.first;
}); });
const auto less = ranges::any_of(*autoPlayValues, [&](Pair pair) { const auto less = ranges::any_of(*autoPlayValues, [&](Pair pair) {
const auto [type, enabled] = pair; const auto &[type, enabled] = pair;
const auto value = enabled ? limitByType(type) : 0; const auto value = enabled ? limitByType(type) : 0;
return value < settings->bytesLimit(_source, type); return value < settings->bytesLimit(_source, type);
}); });
@ -179,7 +179,7 @@ void AutoDownloadBox::setupContent() {
allowMore.end()); allowMore.end());
const auto changed = ranges::any_of(values, [&](Pair pair) { const auto changed = ranges::any_of(values, [&](Pair pair) {
const auto [type, enabled] = pair; const auto &[type, enabled] = pair;
const auto value = enabled ? limitByType(type) : 0; const auto value = enabled ? limitByType(type) : 0;
return value != settings->bytesLimit(_source, type); return value != settings->bytesLimit(_source, type);
}); });

View file

@ -748,7 +748,7 @@ void ProxiesBox::applyView(View &&view) {
const auto wrap = _wrap const auto wrap = _wrap
? _wrap.data() ? _wrap.data()
: _initialWrap.data(); : _initialWrap.data();
const auto [i, ok] = _rows.emplace(id, nullptr); const auto &[i, ok] = _rows.emplace(id, nullptr);
i->second.reset(wrap->insert( i->second.reset(wrap->insert(
0, 0,
object_ptr<ProxyRow>( object_ptr<ProxyRow>(

View file

@ -1121,7 +1121,7 @@ void LanguageBox::prepare() {
using namespace rpl::mappers; using namespace rpl::mappers;
const auto [recent, official] = PrepareLists(); const auto &[recent, official] = PrepareLists();
const auto inner = setInnerWidget( const auto inner = setInnerWidget(
object_ptr<Content>(this, recent, official), object_ptr<Content>(this, recent, official),
st::boxScroll, st::boxScroll,

View file

@ -882,7 +882,7 @@ auto ShareBox::Inner::getChat(not_null<Dialogs::Row*> row)
row->attached = i->second.get(); row->attached = i->second.get();
return i->second.get(); return i->second.get();
} }
const auto [i, ok] = _dataMap.emplace( const auto &[i, ok] = _dataMap.emplace(
peer, peer,
std::make_unique<Chat>(peer, _st.item, [=] { repaintChat(peer); })); std::make_unique<Chat>(peer, _st.item, [=] { repaintChat(peer); }));
updateChatName(i->second.get()); updateChatName(i->second.get());

View file

@ -521,6 +521,7 @@ void BoxController::loadMoreRows() {
MTP_string(), // q MTP_string(), // q
MTP_inputPeerEmpty(), MTP_inputPeerEmpty(),
MTPInputPeer(), // saved_peer_id MTPInputPeer(), // saved_peer_id
MTPVector<MTPReaction>(), // saved_reaction
MTPint(), // top_msg_id MTPint(), // top_msg_id
MTP_inputMessagesFilterPhoneCalls(MTP_flags(0)), MTP_inputMessagesFilterPhoneCalls(MTP_flags(0)),
MTP_int(0), // min_date MTP_int(0), // min_date

View file

@ -267,7 +267,7 @@ TopBar::TopBar(
? object_ptr<Ui::LabelSimple>( ? object_ptr<Ui::LabelSimple>(
this, this,
st::callBarLabel, st::callBarLabel,
tr::lng_call_bar_hangup(tr::now).toUpper()) tr::lng_call_bar_hangup(tr::now))
: object_ptr<Ui::LabelSimple>(nullptr)) : object_ptr<Ui::LabelSimple>(nullptr))
, _mute(this, st::callBarMuteToggle) , _mute(this, st::callBarMuteToggle)
, _info(this) , _info(this)

View file

@ -127,12 +127,13 @@ void VideoBubble::paint() {
const auto inner = _content.rect().marginsRemoved(padding); const auto inner = _content.rect().marginsRemoved(padding);
Ui::Shadow::paint(p, inner, _content.width(), st::boxRoundShadow); Ui::Shadow::paint(p, inner, _content.width(), st::boxRoundShadow);
const auto factor = cIntRetinaFactor(); const auto factor = cIntRetinaFactor();
const auto left = _mirrored
? (_frame.width() - (inner.width() * factor))
: 0;
p.drawImage( p.drawImage(
inner, inner,
_frame, _frame,
QRect( QRect(QPoint(left, 0), inner.size() * factor));
QPoint(_frame.width() - (inner.width() * factor), 0),
inner.size() * factor));
} }
_track->markFrameShown(); _track->markFrameShown();
} }
@ -152,11 +153,10 @@ void VideoBubble::prepareFrame() {
.resize = size, .resize = size,
.outer = size, .outer = size,
}; };
const auto frame = _track->frame(request).mirrored(!_mirrored, false); const auto frame = _track->frame(request);
if (_frame.width() < size.width() || _frame.height() < size.height()) { if (_frame.width() < size.width() || _frame.height() < size.height()) {
_frame = QImage( _frame = QImage(size, QImage::Format_ARGB32_Premultiplied);
size * cIntRetinaFactor(), _frame.fill(Qt::transparent);
QImage::Format_ARGB32_Premultiplied);
} }
Assert(_frame.width() >= frame.width() Assert(_frame.width() >= frame.width()
&& _frame.height() >= frame.height()); && _frame.height() >= frame.height());
@ -174,7 +174,7 @@ void VideoBubble::prepareFrame() {
ImageRoundRadius::Large, ImageRoundRadius::Large,
RectPart::AllCorners, RectPart::AllCorners,
QRect(QPoint(), size) QRect(QPoint(), size)
).mirrored(true, false); ).mirrored(_mirrored, false);
} }
void VideoBubble::setState(Webrtc::VideoState state) { void VideoBubble::setState(Webrtc::VideoState state) {

View file

@ -3013,7 +3013,7 @@ void GroupCall::checkLastSpoke() {
const auto now = crl::now(); const auto now = crl::now();
auto list = base::take(_lastSpoke); auto list = base::take(_lastSpoke);
for (auto i = list.begin(); i != list.end();) { for (auto i = list.begin(); i != list.end();) {
const auto [ssrc, when] = *i; const auto &[ssrc, when] = *i;
if (when.anything + kKeepInListFor >= now) { if (when.anything + kKeepInListFor >= now) {
hasRecent = true; hasRecent = true;
++i; ++i;

View file

@ -1060,11 +1060,13 @@ void Panel::setupVideo(not_null<Viewport*> viewport) {
_call->videoEndpointLargeValue(), _call->videoEndpointLargeValue(),
_call->videoEndpointPinnedValue() _call->videoEndpointPinnedValue()
) | rpl::map(_1 == endpoint && _2); ) | rpl::map(_1 == endpoint && _2);
const auto self = (endpoint.peer == _call->joinAs());
viewport->add( viewport->add(
endpoint, endpoint,
VideoTileTrack{ GroupCall::TrackPointer(track), row }, VideoTileTrack{ GroupCall::TrackPointer(track), row },
GroupCall::TrackSizeValue(track), GroupCall::TrackSizeValue(track),
std::move(pinned)); std::move(pinned),
self);
}; };
for (const auto &[endpoint, track] : _call->activeVideoTracks()) { for (const auto &[endpoint, track] : _call->activeVideoTracks()) {
setupTile(endpoint, track); setupTile(endpoint, track);

View file

@ -237,13 +237,15 @@ void Viewport::add(
const VideoEndpoint &endpoint, const VideoEndpoint &endpoint,
VideoTileTrack track, VideoTileTrack track,
rpl::producer<QSize> trackSize, rpl::producer<QSize> trackSize,
rpl::producer<bool> pinned) { rpl::producer<bool> pinned,
bool self) {
_tiles.push_back(std::make_unique<VideoTile>( _tiles.push_back(std::make_unique<VideoTile>(
endpoint, endpoint,
track, track,
std::move(trackSize), std::move(trackSize),
std::move(pinned), std::move(pinned),
[=] { widget()->update(); })); [=] { widget()->update(); },
self));
_tiles.back()->trackSizeValue( _tiles.back()->trackSizeValue(
) | rpl::filter([](QSize size) { ) | rpl::filter([](QSize size) {

View file

@ -80,7 +80,8 @@ public:
const VideoEndpoint &endpoint, const VideoEndpoint &endpoint,
VideoTileTrack track, VideoTileTrack track,
rpl::producer<QSize> trackSize, rpl::producer<QSize> trackSize,
rpl::producer<bool> pinned); rpl::producer<bool> pinned,
bool self);
void remove(const VideoEndpoint &endpoint); void remove(const VideoEndpoint &endpoint);
void showLarge(const VideoEndpoint &endpoint); void showLarge(const VideoEndpoint &endpoint);

View file

@ -531,6 +531,12 @@ void Viewport::RendererGL::paintTile(
{ { 1.f, 0.f } }, { { 1.f, 0.f } },
{ { 0.f, 0.f } }, { { 0.f, 0.f } },
} }; } };
if (tile->mirror()) {
std::swap(toBlurTexCoords[0], toBlurTexCoords[1]);
std::swap(toBlurTexCoords[2], toBlurTexCoords[3]);
std::swap(texCoords[0], texCoords[1]);
std::swap(texCoords[2], texCoords[3]);
}
if (const auto shift = (frameRotation / 90); shift > 0) { if (const auto shift = (frameRotation / 90); shift > 0) {
std::rotate( std::rotate(
toBlurTexCoords.begin(), toBlurTexCoords.begin(),

View file

@ -105,14 +105,14 @@ void Viewport::RendererSW::paintTile(
tileData.blurredFrame = Images::BlurLargeImage( tileData.blurredFrame = Images::BlurLargeImage(
data.original.scaled( data.original.scaled(
VideoTile::PausedVideoSize(), VideoTile::PausedVideoSize(),
Qt::KeepAspectRatio), Qt::KeepAspectRatio).mirrored(tile->mirror(), false),
kBlurRadius); kBlurRadius);
} }
const auto &image = _userpicFrame const auto &image = _userpicFrame
? tileData.userpicFrame ? tileData.userpicFrame
: _pausedFrame : _pausedFrame
? tileData.blurredFrame ? tileData.blurredFrame
: data.original; : data.original.mirrored(tile->mirror(), false);
const auto frameRotation = _userpicFrame ? 0 : data.rotation; const auto frameRotation = _userpicFrame ? 0 : data.rotation;
Assert(!image.isNull()); Assert(!image.isNull());

View file

@ -28,12 +28,14 @@ Viewport::VideoTile::VideoTile(
VideoTileTrack track, VideoTileTrack track,
rpl::producer<QSize> trackSize, rpl::producer<QSize> trackSize,
rpl::producer<bool> pinned, rpl::producer<bool> pinned,
Fn<void()> update) Fn<void()> update,
bool self)
: _endpoint(endpoint) : _endpoint(endpoint)
, _update(std::move(update)) , _update(std::move(update))
, _track(std::move(track)) , _track(std::move(track))
, _trackSize(std::move(trackSize)) , _trackSize(std::move(trackSize))
, _rtmp(endpoint.rtmp()) { , _rtmp(endpoint.rtmp())
, _self(self) {
Expects(_track.track != nullptr); Expects(_track.track != nullptr);
Expects(_track.row != nullptr); Expects(_track.row != nullptr);
@ -48,6 +50,10 @@ Viewport::VideoTile::VideoTile(
setup(std::move(pinned)); setup(std::move(pinned));
} }
bool Viewport::VideoTile::mirror() const {
return _self && (_endpoint.type == VideoEndpointType::Camera);
}
QRect Viewport::VideoTile::pinOuter() const { QRect Viewport::VideoTile::pinOuter() const {
return _pinOuter; return _pinOuter;
} }

View file

@ -28,7 +28,8 @@ public:
VideoTileTrack track, VideoTileTrack track,
rpl::producer<QSize> trackSize, rpl::producer<QSize> trackSize,
rpl::producer<bool> pinned, rpl::producer<bool> pinned,
Fn<void()> update); Fn<void()> update,
bool self);
[[nodiscard]] not_null<Webrtc::VideoTrack*> track() const { [[nodiscard]] not_null<Webrtc::VideoTrack*> track() const {
return _track.track; return _track.track;
@ -54,6 +55,10 @@ public:
[[nodiscard]] bool visible() const { [[nodiscard]] bool visible() const {
return !_hidden && !_geometry.isEmpty(); return !_hidden && !_geometry.isEmpty();
} }
[[nodiscard]] bool self() const {
return _self;
}
[[nodiscard]] bool mirror() const;
[[nodiscard]] QRect pinOuter() const; [[nodiscard]] QRect pinOuter() const;
[[nodiscard]] QRect pinInner() const; [[nodiscard]] QRect pinInner() const;
[[nodiscard]] QRect backOuter() const; [[nodiscard]] QRect backOuter() const;
@ -123,6 +128,7 @@ private:
bool _pinned = false; bool _pinned = false;
bool _hidden = true; bool _hidden = true;
bool _rtmp = false; bool _rtmp = false;
bool _self = false;
std::optional<VideoQuality> _quality; std::optional<VideoQuality> _quality;
rpl::lifetime _lifetime; rpl::lifetime _lifetime;

View file

@ -114,6 +114,7 @@ private:
const not_null<RoundButton*> _finish; const not_null<RoundButton*> _finish;
const not_null<Checkbox*> _withAudio; const not_null<Checkbox*> _withAudio;
QSize _fixedSize;
std::vector<std::unique_ptr<Source>> _sources; std::vector<std::unique_ptr<Source>> _sources;
Source *_selected = nullptr; Source *_selected = nullptr;
QString _selectedId; QString _selectedId;
@ -337,7 +338,7 @@ void ChooseSourceProcess::setupPanel() {
+ (kRows - 1) * skips.height() + (kRows - 1) * skips.height()
+ (st::desktopCaptureSourceSize.height() / 2) + (st::desktopCaptureSourceSize.height() / 2)
+ bottomHeight; + bottomHeight;
_window->setFixedSize({ width, height }); _fixedSize = QSize(width, height);
_window->setStaysOnTop(true); _window->setStaysOnTop(true);
_window->body()->paintRequest( _window->body()->paintRequest(
@ -598,6 +599,7 @@ void ChooseSourceProcess::setupGeometryWithParent(
if (parentScreen && myScreen != parentScreen) { if (parentScreen && myScreen != parentScreen) {
_window->windowHandle()->setScreen(parentScreen); _window->windowHandle()->setScreen(parentScreen);
} }
_window->setFixedSize(_fixedSize);
_window->move( _window->move(
parent->x() + (parent->width() - _window->width()) / 2, parent->x() + (parent->width() - _window->width()) / 2,
parent->y() + (parent->height() - _window->height()) / 2); parent->y() + (parent->height() - _window->height()) / 2);

View file

@ -1063,12 +1063,17 @@ historyRecordVoiceShowDuration: 120;
historyRecordVoiceDuration: 120; historyRecordVoiceDuration: 120;
historyRecordVoice: icon {{ "chat/input_record", historyRecordVoiceFg }}; historyRecordVoice: icon {{ "chat/input_record", historyRecordVoiceFg }};
historyRecordVoiceOver: icon {{ "chat/input_record", historyRecordVoiceFgOver }}; historyRecordVoiceOver: icon {{ "chat/input_record", historyRecordVoiceFgOver }};
historyRecordVoiceOnceBg: icon {{ "voice_lock/audio_once_bg", historySendIconFg }};
historyRecordVoiceOnceBgOver: icon {{ "voice_lock/audio_once_bg", historySendIconFgOver }};
historyRecordVoiceOnceFg: icon {{ "voice_lock/audio_once_number", windowFgActive }};
historyRecordVoiceOnceFgOver: icon {{ "voice_lock/audio_once_number", windowFgActive }};
historyRecordVoiceOnceInactive: icon {{ "chat/audio_once", windowSubTextFg }};
historyRecordVoiceActive: icon {{ "chat/input_record_filled", historyRecordVoiceFgActiveIcon }}; historyRecordVoiceActive: icon {{ "chat/input_record_filled", historyRecordVoiceFgActiveIcon }};
historyRecordSendIconPosition: point(2px, 0px); historyRecordSendIconPosition: point(2px, 0px);
historyRecordVoiceRippleBgActive: lightButtonBgOver; historyRecordVoiceRippleBgActive: lightButtonBgOver;
historyRecordSignalRadius: 5px; historyRecordSignalRadius: 5px;
historyRecordCancel: windowSubTextFg; historyRecordCancel: windowSubTextFg;
historyRecordCancelActive: windowActiveTextFg; historyRecordCancelActive: historySendIconFg;
historyRecordFont: font(13px); historyRecordFont: font(13px);
historyRecordDurationSkip: 12px; historyRecordDurationSkip: 12px;
historyRecordDurationFg: historyComposeAreaFg; historyRecordDurationFg: historyComposeAreaFg;
@ -1111,20 +1116,26 @@ historyRecordLockArrow: icon {{ "voice_lock/voice_arrow", historyToDownFg }};
historyRecordLockRippleMargin: margins(6px, 6px, 6px, 6px); historyRecordLockRippleMargin: margins(6px, 6px, 6px, 6px);
historyRecordDelete: IconButton(historyAttach) { historyRecordDelete: IconButton(historyAttach) {
icon: icon {{ "info/info_media_delete", historyComposeIconFg }}; icon: icon {{ "voice_lock/recorded_delete", historyComposeIconFg }};
iconOver: icon {{ "info/info_media_delete", historyComposeIconFgOver }}; iconOver: icon {{ "voice_lock/recorded_delete", historyComposeIconFgOver }};
iconPosition: point(10px, 11px); iconPosition: point(10px, 11px);
} }
historyRecordWaveformRightSkip: 10px; historyRecordWaveformRightSkip: 10px;
historyRecordWaveformBgMargins: margins(5px, 7px, 5px, 7px); historyRecordWaveformBgMargins: margins(5px, 8px, 5px, 9px);
historyRecordWaveformBar: 3px; historyRecordWaveformBar: 3px;
historyRecordLockPosition: point(1px, 35px); historyRecordLockPosition: point(1px, 22px);
historyRecordCancelButtonWidth: 100px; historyRecordCancelButtonWidth: 100px;
historyRecordCancelButtonFg: lightButtonFg; historyRecordCancelButtonFg: lightButtonFg;
historyRecordTooltip: ImportantTooltip(defaultImportantTooltip) {
padding: margins(4px, 4px, 4px, 4px);
radius: 11px;
arrow: 6px;
}
historySilentToggle: IconButton(historyBotKeyboardShow) { historySilentToggle: IconButton(historyBotKeyboardShow) {
icon: icon {{ "chat/input_silent", historyComposeIconFg }}; icon: icon {{ "chat/input_silent", historyComposeIconFg }};
iconOver: icon {{ "chat/input_silent", historyComposeIconFgOver }}; iconOver: icon {{ "chat/input_silent", historyComposeIconFgOver }};
@ -1266,3 +1277,17 @@ dragDropColor: windowActiveTextFg;
dragMargin: margins(0px, 10px, 0px, 10px); dragMargin: margins(0px, 10px, 0px, 10px);
dragPadding: margins(20px, 10px, 20px, 10px); dragPadding: margins(20px, 10px, 20px, 10px);
dragHeight: 72px; dragHeight: 72px;
ttlMediaImportantTooltipLabel: FlatLabel(defaultImportantTooltipLabel) {
style: TextStyle(defaultTextStyle) {
font: font(14px);
}
}
ttlMediaButton: RoundButton(defaultActiveButton) {
textBg: shadowFg;
textBgOver: shadowFg;
ripple: universalRippleAnimation;
height: 31px;
textTop: 6px;
}
ttlMediaButtonBottomSkip: 14px;

View file

@ -247,7 +247,7 @@ void Row::paintPreview(QPainter &p) const {
const auto width = st::manageEmojiPreviewWidth; const auto width = st::manageEmojiPreviewWidth;
const auto height = st::manageEmojiPreviewWidth; const auto height = st::manageEmojiPreviewWidth;
auto &&preview = ranges::views::zip(_preview, ranges::views::ints(0, int(_preview.size()))); auto &&preview = ranges::views::zip(_preview, ranges::views::ints(0, int(_preview.size())));
for (const auto [pixmap, index] : preview) { for (const auto &[pixmap, index] : preview) {
const auto row = (index / 2); const auto row = (index / 2);
const auto column = (index % 2); const auto column = (index % 2);
const auto left = x + (column ? width - st::manageEmojiPreview : 0); const auto left = x + (column ? width - st::manageEmojiPreview : 0);

View file

@ -26,6 +26,8 @@ DocumentData *GiftBoxPack::lookup(int months) const {
const auto fallback = _documents.empty() ? nullptr : _documents[0]; const auto fallback = _documents.empty() ? nullptr : _documents[0];
if (it == begin(_localMonths)) { if (it == begin(_localMonths)) {
return fallback; return fallback;
} else if (it == end(_localMonths)) {
return _documents.back();
} }
const auto left = *(it - 1); const auto left = *(it - 1);
const auto right = *it; const auto right = *it;

View file

@ -0,0 +1,392 @@
/*
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 "chat_helpers/ttl_media_layer_widget.h"
#include "base/event_filter.h"
#include "data/data_document.h"
#include "data/data_session.h"
#include "editor/editor_layer_widget.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/view/history_view_element.h"
#include "history/view/media/history_view_document.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "mainwidget.h"
#include "media/audio/media_audio.h"
#include "media/player/media_player_instance.h"
#include "ui/chat/chat_style.h"
#include "ui/chat/chat_theme.h"
#include "ui/effects/path_shift_gradient.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/tooltip.h"
#include "window/section_widget.h" // Window::ChatThemeValueFromPeer.
#include "window/themes/window_theme.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_dialogs.h"
namespace ChatHelpers {
namespace {
class PreviewDelegate final : public HistoryView::DefaultElementDelegate {
public:
PreviewDelegate(
not_null<QWidget*> parent,
not_null<Ui::ChatStyle*> st,
rpl::producer<bool> chatWideValue,
Fn<void()> update);
bool elementAnimationsPaused() override;
not_null<Ui::PathShiftGradient*> elementPathShiftGradient() override;
HistoryView::Context elementContext() override;
bool elementIsChatWide() override;
private:
const not_null<QWidget*> _parent;
const std::unique_ptr<Ui::PathShiftGradient> _pathGradient;
rpl::variable<bool> _chatWide;
};
PreviewDelegate::PreviewDelegate(
not_null<QWidget*> parent,
not_null<Ui::ChatStyle*> st,
rpl::producer<bool> chatWideValue,
Fn<void()> update)
: _parent(parent)
, _pathGradient(HistoryView::MakePathShiftGradient(st, update))
, _chatWide(std::move(chatWideValue)) {
}
bool PreviewDelegate::elementAnimationsPaused() {
return _parent->window()->isActiveWindow();
}
not_null<Ui::PathShiftGradient*> PreviewDelegate::elementPathShiftGradient() {
return _pathGradient.get();
}
HistoryView::Context PreviewDelegate::elementContext() {
return HistoryView::Context::TTLViewer;
}
bool PreviewDelegate::elementIsChatWide() {
return _chatWide.current();
}
class PreviewWrap final : public Ui::RpWidget {
public:
PreviewWrap(
not_null<Ui::RpWidget*> parent,
not_null<HistoryItem*> item,
rpl::producer<QRect> viewportValue,
rpl::producer<bool> chatWideValue,
rpl::producer<std::shared_ptr<Ui::ChatTheme>> theme);
~PreviewWrap();
[[nodiscard]] rpl::producer<> closeRequests() const;
private:
void paintEvent(QPaintEvent *e) override;
void createView();
[[nodiscard]] bool goodItem() const;
void clear();
const not_null<HistoryItem*> _item;
const std::unique_ptr<Ui::ChatStyle> _style;
const std::unique_ptr<PreviewDelegate> _delegate;
rpl::variable<QRect> _globalViewport;
rpl::variable<bool> _chatWide;
std::shared_ptr<Ui::ChatTheme> _theme;
std::unique_ptr<HistoryView::Element> _element;
QRect _viewport;
QRect _elementGeometry;
rpl::variable<QRect> _elementInner;
rpl::lifetime _elementLifetime;
QImage _lastFrameCache;
rpl::event_stream<> _closeRequests;
};
PreviewWrap::PreviewWrap(
not_null<Ui::RpWidget*> parent,
not_null<HistoryItem*> item,
rpl::producer<QRect> viewportValue,
rpl::producer<bool> chatWideValue,
rpl::producer<std::shared_ptr<Ui::ChatTheme>> theme)
: RpWidget(parent)
, _item(item)
, _style(std::make_unique<Ui::ChatStyle>(
item->history()->session().colorIndicesValue()))
, _delegate(std::make_unique<PreviewDelegate>(
parent,
_style.get(),
std::move(chatWideValue),
[=] { update(_elementGeometry); }))
, _globalViewport(std::move(viewportValue)) {
const auto closeCallback = [=] { _closeRequests.fire({}); };
HistoryView::TTLVoiceStops(
item->fullId()
) | rpl::start_with_next([=] {
_lastFrameCache = Ui::GrabWidgetToImage(this, _elementGeometry);
closeCallback();
}, lifetime());
const auto isRound = _item
&& _item->media()
&& _item->media()->document()
&& _item->media()->document()->isVideoMessage();
std::move(
theme
) | rpl::start_with_next([=](std::shared_ptr<Ui::ChatTheme> theme) {
_theme = std::move(theme);
_style->apply(_theme.get());
}, lifetime());
const auto session = &_item->history()->session();
session->data().viewRepaintRequest(
) | rpl::start_with_next([=](not_null<const HistoryView::Element*> view) {
if (view == _element.get()) {
update(_elementGeometry);
}
}, lifetime());
session->data().itemViewRefreshRequest(
) | rpl::start_with_next([=](not_null<HistoryItem*> item) {
if (item == _item) {
if (goodItem()) {
createView();
update();
} else {
clear();
_closeRequests.fire({});
}
}
}, lifetime());
session->data().itemDataChanges(
) | rpl::start_with_next([=](not_null<HistoryItem*> item) {
if (item == _item) {
_element->itemDataChanged();
}
}, lifetime());
session->data().itemRemoved(
) | rpl::start_with_next([=](not_null<const HistoryItem*> item) {
if (item == _item) {
_closeRequests.fire({});
}
}, lifetime());
{
const auto close = Ui::CreateChild<Ui::RoundButton>(
this,
item->out()
? tr::lng_close()
: tr::lng_ttl_voice_close_in(),
st::ttlMediaButton);
close->setFullRadius(true);
close->setClickedCallback(closeCallback);
close->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
rpl::combine(
sizeValue(),
_elementInner.value()
) | rpl::start_with_next([=](QSize size, QRect inner) {
close->moveToLeft(
inner.x() + (inner.width() - close->width()) / 2,
(size.height()
- close->height()
- st::ttlMediaButtonBottomSkip));
}, close->lifetime());
}
QWidget::setAttribute(Qt::WA_OpaquePaintEvent, false);
createView();
{
auto text = item->out()
? (isRound
? tr::lng_ttl_round_tooltip_out
: tr::lng_ttl_voice_tooltip_out)(
lt_user,
rpl::single(
item->history()->peer->shortName()
) | rpl::map(Ui::Text::RichLangValue),
Ui::Text::RichLangValue)
: (isRound
? tr::lng_ttl_round_tooltip_in
: tr::lng_ttl_voice_tooltip_in)(Ui::Text::RichLangValue);
const auto tooltip = Ui::CreateChild<Ui::ImportantTooltip>(
this,
object_ptr<Ui::PaddingWrap<Ui::FlatLabel>>(
this,
Ui::MakeNiceTooltipLabel(
parent,
std::move(text),
st::dialogsStoriesTooltipMaxWidth,
st::ttlMediaImportantTooltipLabel),
st::defaultImportantTooltip.padding),
st::dialogsStoriesTooltip);
tooltip->toggleFast(true);
_elementInner.value(
) | rpl::filter([](const QRect &inner) {
return !inner.isEmpty();
}) | rpl::start_with_next([=](const QRect &inner) {
tooltip->pointAt(inner, RectPart::Top, [=](QSize size) {
return QPoint{
inner.x() + (inner.width() - size.width()) / 2,
(inner.y()
- st::normalFont->height
- size.height()
- st::defaultImportantTooltip.padding.top()),
};
});
}, tooltip->lifetime());
}
}
rpl::producer<> PreviewWrap::closeRequests() const {
return _closeRequests.events();
}
bool PreviewWrap::goodItem() const {
const auto media = _item->media();
if (!media || !media->ttlSeconds()) {
return false;
}
const auto document = media->document();
return document
&& (document->isVoiceMessage() || document->isVideoMessage());
}
void PreviewWrap::createView() {
clear();
_element = _item->createView(_delegate.get());
_element->initDimensions();
rpl::combine(
sizeValue(),
_globalViewport.value()
) | rpl::start_with_next([=](QSize outer, QRect globalViewport) {
_viewport = globalViewport.isEmpty()
? rect()
: mapFromGlobal(globalViewport);
if (_viewport.width() < st::msgMinWidth) {
return;
}
_element->resizeGetHeight(_viewport.width());
_elementGeometry = QRect(
(_viewport.width() - _element->width()) / 2,
(_viewport.height() - _element->height()) / 2,
_element->width(),
_element->height()
).translated(_viewport.topLeft());
_elementInner = _element->innerGeometry().translated(
_elementGeometry.topLeft());
update();
}, _elementLifetime);
}
void PreviewWrap::clear() {
_elementLifetime.destroy();
_element = nullptr;
}
PreviewWrap::~PreviewWrap() {
clear();
}
void PreviewWrap::paintEvent(QPaintEvent *e) {
if (!_element || _elementGeometry.isEmpty()) {
return;
}
auto p = Painter(this);
p.translate(_elementGeometry.topLeft());
if (!_lastFrameCache.isNull()) {
p.drawImage(0, 0, _lastFrameCache);
} else {
auto context = _theme->preparePaintContext(
_style.get(),
Rect(_element->currentSize()),
Rect(_element->currentSize()),
!window()->isActiveWindow());
context.outbg = _element->hasOutLayout();
_element->draw(p, context);
}
}
rpl::producer<QRect> GlobalViewportForWindow(
not_null<Window::SessionController*> controller) {
const auto delegate = controller->window().floatPlayerDelegate();
return rpl::single(rpl::empty) | rpl::then(
delegate->floatPlayerAreaUpdates()
) | rpl::map([=] {
auto section = (Media::Player::FloatSectionDelegate*)nullptr;
delegate->floatPlayerEnumerateSections([&](
not_null<Media::Player::FloatSectionDelegate*> check,
Window::Column column) {
if ((column == Window::Column::First && !section)
|| column == Window::Column::Second) {
section = check;
}
});
if (section) {
const auto rect = section->floatPlayerAvailableRect();
if (rect.width() >= st::msgMinWidth) {
return rect;
}
}
return QRect();
});
}
} // namespace
void ShowTTLMediaLayerWidget(
not_null<Window::SessionController*> controller,
not_null<HistoryItem*> item) {
const auto parent = controller->content();
const auto show = controller->uiShow();
auto preview = base::make_unique_q<PreviewWrap>(
parent,
item,
GlobalViewportForWindow(controller),
controller->adaptive().chatWideValue(),
Window::ChatThemeValueFromPeer(
controller,
item->history()->peer));
preview->closeRequests(
) | rpl::start_with_next([=] {
show->hideLayer();
}, preview->lifetime());
auto layer = std::make_unique<Editor::LayerWidget>(
parent,
std::move(preview));
layer->lifetime().add([] { ::Media::Player::instance()->stop(); });
base::install_event_filter(layer.get(), [=](not_null<QEvent*> e) {
if (e->type() == QEvent::KeyPress) {
const auto k = static_cast<QKeyEvent*>(e.get());
if (k->key() == Qt::Key_Escape) {
show->hideLayer();
}
return base::EventFilterResult::Cancel;
}
return base::EventFilterResult::Continue;
});
controller->showLayer(std::move(layer), Ui::LayerOption::KeepOther);
}
} // namespace ChatHelpers

View file

@ -0,0 +1,22 @@
/*
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
class HistoryItem;
namespace Window {
class SessionController;
} // namespace Window
namespace ChatHelpers {
void ShowTTLMediaLayerWidget(
not_null<Window::SessionController*> controller,
not_null<HistoryItem*> item);
} // namespace ChatHelpers

View file

@ -360,7 +360,7 @@ void Application::run() {
startDomain(); startDomain();
startTray(); startTray();
_lastActivePrimaryWindow->widget()->show(); _lastActivePrimaryWindow->firstShow();
startMediaView(); startMediaView();

View file

@ -128,7 +128,7 @@ CloudPasswordResult ComputeCheck(
} }
}; };
const auto [a, AForHash, u] = GenerateAndCheckRandom(); const auto &[a, AForHash, u] = GenerateAndCheckRandom();
const auto g_b = BigNum::ModSub(B, kg_x, p, context); const auto g_b = BigNum::ModSub(B, kg_x, p, context);
if (!MTP::IsGoodModExpFirst(g_b, p)) { if (!MTP::IsGoodModExpFirst(g_b, p)) {
LOG(("API Error: Bad g_b in cloud password check!")); LOG(("API Error: Bad g_b in cloud password check!"));

View file

@ -343,7 +343,8 @@ QByteArray Settings::serialize() const {
<< qint32(_ignoreBatterySaving.current() ? 1 : 0) << qint32(_ignoreBatterySaving.current() ? 1 : 0)
<< quint64(_macRoundIconDigest.value_or(0)) << quint64(_macRoundIconDigest.value_or(0))
<< qint32(_storiesClickTooltipHidden.current() ? 1 : 0) << qint32(_storiesClickTooltipHidden.current() ? 1 : 0)
<< qint32(_recentEmojiSkip.size()); << qint32(_recentEmojiSkip.size())
<< qint32(_ttlVoiceClickTooltipHidden.current() ? 1 : 0);
for (const auto &id : _recentEmojiSkip) { for (const auto &id : _recentEmojiSkip) {
stream << id; stream << id;
} }
@ -459,6 +460,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
qint32 storiesClickTooltipHidden = _storiesClickTooltipHidden.current() ? 1 : 0; qint32 storiesClickTooltipHidden = _storiesClickTooltipHidden.current() ? 1 : 0;
base::flat_set<QString> recentEmojiSkip; base::flat_set<QString> recentEmojiSkip;
qint32 trayIconMonochrome = (_trayIconMonochrome.current() ? 1 : 0); qint32 trayIconMonochrome = (_trayIconMonochrome.current() ? 1 : 0);
qint32 ttlVoiceClickTooltipHidden = _ttlVoiceClickTooltipHidden.current() ? 1 : 0;
stream >> themesAccentColors; stream >> themesAccentColors;
if (!stream.atEnd()) { if (!stream.atEnd()) {
@ -715,6 +717,9 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
// Let existing clients use the old value. // Let existing clients use the old value.
trayIconMonochrome = 0; trayIconMonochrome = 0;
} }
if (!stream.atEnd()) {
stream >> ttlVoiceClickTooltipHidden;
}
if (stream.status() != QDataStream::Ok) { if (stream.status() != QDataStream::Ok) {
LOG(("App Error: " LOG(("App Error: "
"Bad data for Core::Settings::constructFromSerialized()")); "Bad data for Core::Settings::constructFromSerialized()"));
@ -910,6 +915,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
_storiesClickTooltipHidden = (storiesClickTooltipHidden == 1); _storiesClickTooltipHidden = (storiesClickTooltipHidden == 1);
_recentEmojiSkip = std::move(recentEmojiSkip); _recentEmojiSkip = std::move(recentEmojiSkip);
_trayIconMonochrome = (trayIconMonochrome == 1); _trayIconMonochrome = (trayIconMonochrome == 1);
_ttlVoiceClickTooltipHidden = (ttlVoiceClickTooltipHidden == 1);
} }
QString Settings::getSoundPath(const QString &key) const { QString Settings::getSoundPath(const QString &key) const {
@ -1266,6 +1272,7 @@ void Settings::resetOnLastLogout() {
_systemDarkModeEnabled = false; _systemDarkModeEnabled = false;
_hiddenGroupCallTooltips = 0; _hiddenGroupCallTooltips = 0;
_storiesClickTooltipHidden = false; _storiesClickTooltipHidden = false;
_ttlVoiceClickTooltipHidden = false;
_recentEmojiPreload.clear(); _recentEmojiPreload.clear();
_recentEmoji.clear(); _recentEmoji.clear();

View file

@ -826,6 +826,15 @@ public:
void setStoriesClickTooltipHidden(bool value) { void setStoriesClickTooltipHidden(bool value) {
_storiesClickTooltipHidden = value; _storiesClickTooltipHidden = value;
} }
[[nodiscard]] bool ttlVoiceClickTooltipHidden() const {
return _ttlVoiceClickTooltipHidden.current();
}
[[nodiscard]] rpl::producer<bool> ttlVoiceClickTooltipHiddenValue() const {
return _ttlVoiceClickTooltipHidden.value();
}
void setTtlVoiceClickTooltipHidden(bool value) {
_ttlVoiceClickTooltipHidden = value;
}
[[nodiscard]] static bool ThirdColumnByDefault(); [[nodiscard]] static bool ThirdColumnByDefault();
[[nodiscard]] static float64 DefaultDialogsWidthRatio(); [[nodiscard]] static float64 DefaultDialogsWidthRatio();
@ -951,6 +960,7 @@ private:
rpl::variable<bool> _ignoreBatterySaving = false; rpl::variable<bool> _ignoreBatterySaving = false;
std::optional<uint64> _macRoundIconDigest; std::optional<uint64> _macRoundIconDigest;
rpl::variable<bool> _storiesClickTooltipHidden = false; rpl::variable<bool> _storiesClickTooltipHidden = false;
rpl::variable<bool> _ttlVoiceClickTooltipHidden = false;
bool _tabbedReplacedWithInfo = false; // per-window bool _tabbedReplacedWithInfo = false; // per-window
rpl::event_stream<bool> _tabbedReplacedWithInfoValue; // per-window rpl::event_stream<bool> _tabbedReplacedWithInfoValue; // per-window

View file

@ -80,6 +80,13 @@ const auto CommandByName = base::flat_map<QString, Command>{
{ u"next_folder"_q , Command::FolderNext }, { u"next_folder"_q , Command::FolderNext },
{ u"all_chats"_q , Command::ShowAllChats }, { u"all_chats"_q , Command::ShowAllChats },
{ u"account1"_q , Command::ShowAccount1 },
{ u"account2"_q , Command::ShowAccount2 },
{ u"account3"_q , Command::ShowAccount3 },
{ u"account4"_q , Command::ShowAccount4 },
{ u"account5"_q , Command::ShowAccount5 },
{ u"account6"_q , Command::ShowAccount6 },
{ u"folder1"_q , Command::ShowFolder1 }, { u"folder1"_q , Command::ShowFolder1 },
{ u"folder2"_q , Command::ShowFolder2 }, { u"folder2"_q , Command::ShowFolder2 },
{ u"folder3"_q , Command::ShowFolder3 }, { u"folder3"_q , Command::ShowFolder3 },
@ -126,6 +133,13 @@ const auto CommandNames = base::flat_map<Command, QString>{
{ Command::FolderNext , u"next_folder"_q }, { Command::FolderNext , u"next_folder"_q },
{ Command::ShowAllChats , u"all_chats"_q }, { Command::ShowAllChats , u"all_chats"_q },
{ Command::ShowAccount1 , u"account1"_q },
{ Command::ShowAccount2 , u"account2"_q },
{ Command::ShowAccount3 , u"account3"_q },
{ Command::ShowAccount4 , u"account4"_q },
{ Command::ShowAccount5 , u"account5"_q },
{ Command::ShowAccount6 , u"account6"_q },
{ Command::ShowFolder1 , u"folder1"_q }, { Command::ShowFolder1 , u"folder1"_q },
{ Command::ShowFolder2 , u"folder2"_q }, { Command::ShowFolder2 , u"folder2"_q },
{ Command::ShowFolder3 , u"folder3"_q }, { Command::ShowFolder3 , u"folder3"_q },
@ -388,10 +402,18 @@ void Manager::fillDefaults() {
kShowFolder, kShowFolder,
ranges::views::ints(1, ranges::unreachable)); ranges::views::ints(1, ranges::unreachable));
for (const auto [command, index] : folders) { for (const auto &[command, index] : folders) {
set(u"%1+%2"_q.arg(ctrl).arg(index), command); set(u"%1+%2"_q.arg(ctrl).arg(index), command);
} }
//auto &&accounts = ranges::views::zip(
// kShowAccount,
// ranges::views::ints(1, ranges::unreachable));
//for (const auto &[command, index] : accounts) {
// set(u"%1+shift+%2"_q.arg(ctrl).arg(index), command);
//}
set(u"%1+shift+down"_q.arg(ctrl), Command::FolderNext); set(u"%1+shift+down"_q.arg(ctrl), Command::FolderNext);
set(u"%1+shift+up"_q.arg(ctrl), Command::FolderPrevious); set(u"%1+shift+up"_q.arg(ctrl), Command::FolderPrevious);
@ -436,6 +458,18 @@ void Manager::writeDefaultFile() {
} }
} }
// Commands without a default value.
for (const auto command : kShowAccount) {
const auto j = CommandNames.find(command);
if (j != CommandNames.end()) {
QJsonObject entry;
entry.insert(u"keys"_q, QJsonValue());
entry.insert(u"command"_q, j->second);
shortcuts.append(entry);
}
}
auto document = QJsonDocument(); auto document = QJsonDocument();
document.setArray(shortcuts); document.setArray(shortcuts);
file.write(document.toJson(QJsonDocument::Indented)); file.write(document.toJson(QJsonDocument::Indented));

View file

@ -38,6 +38,13 @@ enum class Command {
ChatPinned7, ChatPinned7,
ChatPinned8, ChatPinned8,
ShowAccount1,
ShowAccount2,
ShowAccount3,
ShowAccount4,
ShowAccount5,
ShowAccount6,
ShowAllChats, ShowAllChats,
ShowFolder1, ShowFolder1,
ShowFolder2, ShowFolder2,
@ -79,6 +86,15 @@ enum class Command {
Command::ShowFolderLast, Command::ShowFolderLast,
}; };
[[maybe_unused]] constexpr auto kShowAccount = {
Command::ShowAccount1,
Command::ShowAccount2,
Command::ShowAccount3,
Command::ShowAccount4,
Command::ShowAccount5,
Command::ShowAccount6,
};
[[nodiscard]] FnMut<bool()> RequestHandler(Command command); [[nodiscard]] FnMut<bool()> RequestHandler(Command command);
class Request { class Request {

View file

@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D666}"_cs;
constexpr auto AppNameOld = "AyuGram for Windows"_cs; constexpr auto AppNameOld = "AyuGram for Windows"_cs;
constexpr auto AppName = "AyuGram Desktop"_cs; constexpr auto AppName = "AyuGram Desktop"_cs;
constexpr auto AppFile = "AyuGram"_cs; constexpr auto AppFile = "AyuGram"_cs;
constexpr auto AppVersion = 4014003; constexpr auto AppVersion = 4014006;
constexpr auto AppVersionStr = "4.14.3"; constexpr auto AppVersionStr = "4.14.6";
constexpr auto AppBetaVersion = false; constexpr auto AppBetaVersion = false;
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION; constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;

View file

@ -56,7 +56,7 @@ rpl::producer<UpdateType> Changes::Manager<DataType, UpdateType>::updates(
Flags flags) const { Flags flags) const {
return _stream.events( return _stream.events(
) | rpl::filter([=](const UpdateType &update) { ) | rpl::filter([=](const UpdateType &update) {
const auto [updateData, updateFlags] = update; const auto &[updateData, updateFlags] = update;
return (updateData == data) && (updateFlags & flags); return (updateData == data) && (updateFlags & flags);
}); });
} }

View file

@ -9,7 +9,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/options.h" #include "base/options.h"
#include "base/platform/base_platform_info.h" #include "base/platform/base_platform_info.h"
#include "ui/boxes/confirm_box.h" #include "boxes/abstract_box.h" // Ui::show().
#include "chat_helpers/ttl_media_layer_widget.h"
#include "core/application.h" #include "core/application.h"
#include "core/core_settings.h" #include "core/core_settings.h"
#include "core/mime_type.h" #include "core/mime_type.h"
@ -17,17 +18,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_document_media.h" #include "data/data_document_media.h"
#include "data/data_file_click_handler.h" #include "data/data_file_click_handler.h"
#include "data/data_session.h" #include "data/data_session.h"
#include "history/view/media/history_view_gif.h"
#include "history/history.h" #include "history/history.h"
#include "history/history_item.h" #include "history/history_item.h"
#include "media/player/media_player_instance.h" #include "history/view/media/history_view_gif.h"
#include "lang/lang_keys.h" #include "lang/lang_keys.h"
#include "media/player/media_player_instance.h"
#include "platform/platform_file_utilities.h" #include "platform/platform_file_utilities.h"
#include "ui/boxes/confirm_box.h"
#include "ui/chat/chat_theme.h" #include "ui/chat/chat_theme.h"
#include "ui/text/text_utilities.h" #include "ui/text/text_utilities.h"
#include "ui/widgets/checkbox.h" #include "ui/widgets/checkbox.h"
#include "window/window_session_controller.h" #include "window/window_session_controller.h"
#include "boxes/abstract_box.h" // Ui::show().
#include "styles/style_layers.h" #include "styles/style_layers.h"
#include <QtCore/QBuffer> #include <QtCore/QBuffer>
@ -298,6 +299,12 @@ void ResolveDocument(
|| document->isVoiceMessage() || document->isVoiceMessage()
|| document->isVideoMessage()) { || document->isVideoMessage()) {
::Media::Player::instance()->playPause({ document, msgId }); ::Media::Player::instance()->playPause({ document, msgId });
if (controller
&& item
&& item->media()
&& item->media()->ttlSeconds()) {
ChatHelpers::ShowTTLMediaLayerWidget(controller, item);
}
} else { } else {
showDocument(); showDocument();
} }

View file

@ -847,7 +847,7 @@ void GroupCall::requestUnknownParticipants() {
auto result = base::flat_map<uint32, LastSpokeTimes>(); auto result = base::flat_map<uint32, LastSpokeTimes>();
result.reserve(kRequestPerPage); result.reserve(kRequestPerPage);
while (result.size() < kRequestPerPage) { while (result.size() < kRequestPerPage) {
const auto [ssrc, when] = _unknownSpokenSsrcs.back(); const auto &[ssrc, when] = _unknownSpokenSsrcs.back();
result.emplace(ssrc, when); result.emplace(ssrc, when);
_unknownSpokenSsrcs.erase(_unknownSpokenSsrcs.end() - 1); _unknownSpokenSsrcs.erase(_unknownSpokenSsrcs.end() - 1);
} }
@ -863,7 +863,7 @@ void GroupCall::requestUnknownParticipants() {
result.reserve(available); result.reserve(available);
while (result.size() < available) { while (result.size() < available) {
const auto &back = _unknownSpokenPeerIds.back(); const auto &back = _unknownSpokenPeerIds.back();
const auto [participantPeerId, when] = back; const auto &[participantPeerId, when] = back;
result.emplace(participantPeerId, when); result.emplace(participantPeerId, when);
_unknownSpokenPeerIds.erase(_unknownSpokenPeerIds.end() - 1); _unknownSpokenPeerIds.erase(_unknownSpokenPeerIds.end() - 1);
} }

View file

@ -129,7 +129,7 @@ not_null<History*> Histories::findOrCreate(PeerId peerId) {
if (const auto result = find(peerId)) { if (const auto result = find(peerId)) {
return result; return result;
} }
const auto [i, ok] = _map.emplace( const auto &[i, ok] = _map.emplace(
peerId, peerId,
std::make_unique<History>(&owner(), peerId)); std::make_unique<History>(&owner(), peerId));
return i->second.get(); return i->second.get();
@ -363,7 +363,7 @@ void Histories::requestDialogEntry(
return; return;
} }
const auto [j, ok] = _dialogRequestsPending.try_emplace(history); const auto &[j, ok] = _dialogRequestsPending.try_emplace(history);
if (callback) { if (callback) {
j->second.push_back(std::move(callback)); j->second.push_back(std::move(callback));
} }
@ -1152,7 +1152,7 @@ void Histories::finishSentRequest(
if (state->postponedRequestEntry && !postponeEntryRequest(*state)) { if (state->postponedRequestEntry && !postponeEntryRequest(*state)) {
const auto i = _dialogRequests.find(history); const auto i = _dialogRequests.find(history);
Assert(i != end(_dialogRequests)); Assert(i != end(_dialogRequests));
const auto [j, ok] = _dialogRequestsPending.emplace( const auto &[j, ok] = _dialogRequestsPending.emplace(
history, history,
std::move(i->second)); std::move(i->second));
Assert(ok); Assert(ok);

View file

@ -902,7 +902,7 @@ bool MediaFile::uploading() const {
Storage::SharedMediaTypesMask MediaFile::sharedMediaTypes() const { Storage::SharedMediaTypesMask MediaFile::sharedMediaTypes() const {
using Type = Storage::SharedMediaType; using Type = Storage::SharedMediaType;
if (_document->sticker()) { if (_document->sticker() || ttlSeconds()) {
return {}; return {};
} else if (_document->isVideoMessage()) { } else if (_document->isVideoMessage()) {
return Storage::SharedMediaTypesMask{} return Storage::SharedMediaTypesMask{}

View file

@ -11,6 +11,26 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Data { namespace Data {
QString SearchTagToQuery(const ReactionId &tagId) {
if (const auto customId = tagId.custom()) {
return u"#tag-custom:%1"_q.arg(customId);
} else if (!tagId) {
return QString();
}
return u"#tag-emoji:"_q + tagId.emoji();
}
ReactionId SearchTagFromQuery(const QString &query) {
const auto list = query.split(QChar(' '));
const auto tag = list.isEmpty() ? QString() : list[0];
if (tag.startsWith(u"#tag-custom:"_q)) {
return ReactionId{ DocumentId(tag.mid(12).toULongLong()) };
} else if (tag.startsWith(u"#tag-emoji:"_q)) {
return ReactionId{ tag.mid(11) };
}
return {};
}
QString ReactionEntityData(const ReactionId &id) { QString ReactionEntityData(const ReactionId &id) {
if (id.empty()) { if (id.empty()) {
return {}; return {};

View file

@ -27,6 +27,10 @@ struct ReactionId {
return custom ? *custom : DocumentId(); return custom ? *custom : DocumentId();
} }
explicit operator bool() const {
return !empty();
}
friend inline auto operator<=>( friend inline auto operator<=>(
const ReactionId &, const ReactionId &,
const ReactionId &) = default; const ReactionId &) = default;
@ -41,6 +45,9 @@ struct MessageReaction {
bool my = false; bool my = false;
}; };
[[nodiscard]] QString SearchTagToQuery(const ReactionId &tagId);
[[nodiscard]] ReactionId SearchTagFromQuery(const QString &query);
[[nodiscard]] QString ReactionEntityData(const ReactionId &id); [[nodiscard]] QString ReactionEntityData(const ReactionId &id);
[[nodiscard]] ReactionId ReactionFromMTP(const MTPReaction &reaction); [[nodiscard]] ReactionId ReactionFromMTP(const MTPReaction &reaction);

View file

@ -43,6 +43,7 @@ constexpr auto kPollEach = 20 * crl::time(1000);
constexpr auto kSizeForDownscale = 64; constexpr auto kSizeForDownscale = 64;
constexpr auto kRecentRequestTimeout = 10 * crl::time(1000); constexpr auto kRecentRequestTimeout = 10 * crl::time(1000);
constexpr auto kRecentReactionsLimit = 40; constexpr auto kRecentReactionsLimit = 40;
constexpr auto kMyTagsRequestTimeout = crl::time(1000);
constexpr auto kTopRequestDelay = 60 * crl::time(1000); constexpr auto kTopRequestDelay = 60 * crl::time(1000);
constexpr auto kTopReactionsLimit = 14; constexpr auto kTopReactionsLimit = 14;
@ -69,6 +70,27 @@ constexpr auto kTopReactionsLimit = 14;
return result; return result;
} }
[[nodiscard]] std::vector<MyTagInfo> ListFromMTP(
const MTPDmessages_savedReactionTags &data) {
const auto &list = data.vtags().v;
auto result = std::vector<MyTagInfo>();
result.reserve(list.size());
for (const auto &reaction : list) {
const auto &data = reaction.data();
const auto id = ReactionFromMTP(data.vreaction());
if (id.empty()) {
LOG(("API Error: reactionEmpty in messages.reactions."));
} else {
result.push_back({
.id = id,
.title = qs(data.vtitle().value_or_empty()),
.count = data.vcount().v,
});
}
}
return result;
}
[[nodiscard]] Reaction CustomReaction(not_null<DocumentData*> document) { [[nodiscard]] Reaction CustomReaction(not_null<DocumentData*> document) {
return Reaction{ return Reaction{
.id = { { document->id } }, .id = { { document->id } },
@ -126,6 +148,8 @@ PossibleItemReactionsRef LookupPossibleReactions(
const auto &full = reactions->list(Reactions::Type::Active); const auto &full = reactions->list(Reactions::Type::Active);
const auto &top = reactions->list(Reactions::Type::Top); const auto &top = reactions->list(Reactions::Type::Top);
const auto &recent = reactions->list(Reactions::Type::Recent); const auto &recent = reactions->list(Reactions::Type::Recent);
const auto &myTags = reactions->list(Reactions::Type::MyTags);
const auto &tags = reactions->list(Reactions::Type::Tags);
const auto &all = item->reactions(); const auto &all = item->reactions();
const auto limit = UniqueReactionsLimit(peer); const auto limit = UniqueReactionsLimit(peer);
const auto premiumPossible = session->premiumPossible(); const auto premiumPossible = session->premiumPossible();
@ -148,7 +172,20 @@ PossibleItemReactionsRef LookupPossibleReactions(
} }
}; };
reactions->clearTemporary(); reactions->clearTemporary();
if (limited) { if (item->reactionsAreTags()) {
auto &&all = ranges::views::concat(myTags, tags);
result.recent.reserve(myTags.size() + tags.size());
for (const auto &reaction : all) {
if (premiumPossible
|| ranges::contains(tags, reaction.id, &Reaction::id)) {
if (added.emplace(reaction.id).second) {
result.recent.push_back(&reaction);
}
}
}
result.customAllowed = premiumPossible;
result.tags = true;
} else if (limited) {
result.recent.reserve(all.size()); result.recent.reserve(all.size());
add([&](const Reaction &reaction) { add([&](const Reaction &reaction) {
return ranges::contains(all, reaction.id, &MessageReaction::id); return ranges::contains(all, reaction.id, &MessageReaction::id);
@ -198,23 +235,26 @@ PossibleItemReactionsRef LookupPossibleReactions(
result.customAllowed = (allowed.type == AllowedReactionsType::All) result.customAllowed = (allowed.type == AllowedReactionsType::All)
&& premiumPossible; && premiumPossible;
} }
const auto i = ranges::find( if (!item->reactionsAreTags()) {
result.recent, const auto i = ranges::find(
reactions->favoriteId(), result.recent,
&Reaction::id); reactions->favoriteId(),
if (i != end(result.recent) && i != begin(result.recent)) { &Reaction::id);
std::rotate(begin(result.recent), i, i + 1); if (i != end(result.recent) && i != begin(result.recent)) {
std::rotate(begin(result.recent), i, i + 1);
}
} }
return result; return result;
} }
PossibleItemReactions::PossibleItemReactions( PossibleItemReactions::PossibleItemReactions(
const PossibleItemReactionsRef &other) const PossibleItemReactionsRef &other)
: recent(other.recent | ranges::views::transform([](const auto &value) { : recent(other.recent | ranges::views::transform([](const auto &value) {
return *value; return *value;
}) | ranges::to_vector) }) | ranges::to_vector)
, morePremiumAvailable(other.morePremiumAvailable) , morePremiumAvailable(other.morePremiumAvailable)
, customAllowed(other.customAllowed) { , customAllowed(other.customAllowed)
, tags(other.tags){
} }
Reactions::Reactions(not_null<Session*> owner) Reactions::Reactions(not_null<Session*> owner)
@ -285,16 +325,42 @@ void Reactions::refreshDefault() {
requestDefault(); requestDefault();
} }
void Reactions::refreshMyTags() {
requestMyTags();
}
void Reactions::refreshMyTagsDelayed() {
if (_myTagsRequestId || _myTagsRequestScheduled) {
return;
}
_myTagsRequestScheduled = true;
base::call_delayed(kMyTagsRequestTimeout, &_owner->session(), [=] {
if (_myTagsRequestScheduled) {
requestMyTags();
}
});
}
void Reactions::refreshTags() {
requestTags();
}
const std::vector<Reaction> &Reactions::list(Type type) const { const std::vector<Reaction> &Reactions::list(Type type) const {
switch (type) { switch (type) {
case Type::Active: return _active; case Type::Active: return _active;
case Type::Recent: return _recent; case Type::Recent: return _recent;
case Type::Top: return _top; case Type::Top: return _top;
case Type::All: return _available; case Type::All: return _available;
case Type::MyTags: return _myTags;
case Type::Tags: return _tags;
} }
Unexpected("Type in Reactions::list."); Unexpected("Type in Reactions::list.");
} }
const std::vector<MyTagInfo> &Reactions::myTagsInfo() const {
return _myTagsInfo;
}
ReactionId Reactions::favoriteId() const { ReactionId Reactions::favoriteId() const {
return _favoriteId; return _favoriteId;
} }
@ -319,6 +385,56 @@ void Reactions::setFavorite(const ReactionId &id) {
applyFavorite(id); applyFavorite(id);
} }
void Reactions::incrementMyTag(const ReactionId &id) {
auto i = ranges::find(_myTagsInfo, id, &MyTagInfo::id);
if (i == end(_myTagsInfo)) {
_myTagsInfo.push_back({ .id = id, .count = 0 });
i = end(_myTagsInfo) - 1;
}
++i->count;
while (i != begin(_myTagsInfo)) {
auto j = i - 1;
if (j->count >= i->count) {
break;
}
std::swap(*i, *j);
i = j;
}
scheduleMyTagsUpdate();
}
void Reactions::decrementMyTag(const ReactionId &id) {
auto i = ranges::find(_myTagsInfo, id, &MyTagInfo::id);
if (i->count <= 0) {
return;
}
--i->count;
while (i + 1 != end(_myTagsInfo)) {
auto j = i + 1;
if (j->count <= i->count) {
break;
}
std::swap(*i, *j);
i = j;
}
scheduleMyTagsUpdate();
}
void Reactions::scheduleMyTagsUpdate() {
_myTagsUpdateScheduled = true;
crl::on_main(&session(), [=] {
if (!_myTagsUpdateScheduled) {
return;
}
_myTagsUpdateScheduled = false;
_myTagsIds = _myTagsInfo | ranges::views::transform(
&MyTagInfo::id
) | ranges::to_vector;
_myTags = resolveByIds(_myTagsIds, _unresolvedMyTags);
_myTagsUpdated.fire({});
});
}
DocumentData *Reactions::chooseGenericAnimation( DocumentData *Reactions::chooseGenericAnimation(
not_null<DocumentData*> custom) const { not_null<DocumentData*> custom) const {
const auto sticker = custom->sticker(); const auto sticker = custom->sticker();
@ -380,6 +496,14 @@ rpl::producer<> Reactions::favoriteUpdates() const {
return _favoriteUpdated.events(); return _favoriteUpdated.events();
} }
rpl::producer<> Reactions::myTagsUpdates() const {
return _myTagsUpdated.events();
}
rpl::producer<> Reactions::tagsUpdates() const {
return _tagsUpdated.events();
}
void Reactions::preloadImageFor(const ReactionId &id) { void Reactions::preloadImageFor(const ReactionId &id) {
if (_images.contains(id) || id.emoji().isEmpty()) { if (_images.contains(id) || id.emoji().isEmpty()) {
return; return;
@ -622,6 +746,46 @@ void Reactions::requestGeneric() {
}).send(); }).send();
} }
void Reactions::requestMyTags() {
if (_myTagsRequestId) {
return;
}
auto &api = _owner->session().api();
_myTagsRequestScheduled = false;
_myTagsRequestId = api.request(MTPmessages_GetSavedReactionTags(
MTP_long(_myTagsHash)
)).done([=](const MTPmessages_SavedReactionTags &result) {
_myTagsRequestId = 0;
result.match([&](const MTPDmessages_savedReactionTags &data) {
updateMyTags(data);
}, [](const MTPDmessages_savedReactionTagsNotModified&) {
});
}).fail([=] {
_myTagsRequestId = 0;
_myTagsHash = 0;
}).send();
}
void Reactions::requestTags() {
if (_tagsRequestId) {
return;
}
auto &api = _owner->session().api();
_tagsRequestId = api.request(MTPmessages_GetDefaultTagReactions(
MTP_long(_tagsHash)
)).done([=](const MTPmessages_Reactions &result) {
_tagsRequestId = 0;
result.match([&](const MTPDmessages_reactions &data) {
updateTags(data);
}, [](const MTPDmessages_reactionsNotModified&) {
});
}).fail([=] {
_tagsRequestId = 0;
_tagsHash = 0;
}).send();
}
void Reactions::updateTop(const MTPDmessages_reactions &data) { void Reactions::updateTop(const MTPDmessages_reactions &data) {
_topHash = data.vhash().v; _topHash = data.vhash().v;
_topIds = ListFromMTP(data); _topIds = ListFromMTP(data);
@ -690,6 +854,23 @@ void Reactions::updateGeneric(const MTPDmessages_stickerSet &data) {
} }
} }
void Reactions::updateMyTags(const MTPDmessages_savedReactionTags &data) {
_myTagsHash = data.vhash().v;
_myTagsInfo = ListFromMTP(data);
_myTagsIds = _myTagsInfo | ranges::views::transform(
&MyTagInfo::id
) | ranges::to_vector;
_myTags = resolveByIds(_myTagsIds, _unresolvedMyTags);
_myTagsUpdated.fire({});
}
void Reactions::updateTags(const MTPDmessages_reactions &data) {
_tagsHash = data.vhash().v;
_tagsIds = ListFromMTP(data);
_tags = resolveByIds(_tagsIds, _unresolvedTags);
_tagsUpdated.fire({});
}
void Reactions::recentUpdated() { void Reactions::recentUpdated() {
_topRefreshTimer.callOnce(kTopRequestDelay); _topRefreshTimer.callOnce(kTopRequestDelay);
_recentUpdated.fire({}); _recentUpdated.fire({});
@ -701,9 +882,25 @@ void Reactions::defaultUpdated() {
if (_genericAnimations.empty()) { if (_genericAnimations.empty()) {
requestGeneric(); requestGeneric();
} }
refreshMyTags();
refreshTags();
_defaultUpdated.fire({}); _defaultUpdated.fire({});
} }
void Reactions::myTagsUpdated() {
if (_genericAnimations.empty()) {
requestGeneric();
}
_myTagsUpdated.fire({});
}
void Reactions::tagsUpdated() {
if (_genericAnimations.empty()) {
requestGeneric();
}
_tagsUpdated.fire({});
}
not_null<CustomEmojiManager::Listener*> Reactions::resolveListener() { not_null<CustomEmojiManager::Listener*> Reactions::resolveListener() {
return static_cast<CustomEmojiManager::Listener*>(this); return static_cast<CustomEmojiManager::Listener*>(this);
} }
@ -715,6 +912,10 @@ void Reactions::customEmojiResolveDone(not_null<DocumentData*> document) {
const auto top = (i != end(_unresolvedTop)); const auto top = (i != end(_unresolvedTop));
const auto j = _unresolvedRecent.find(id); const auto j = _unresolvedRecent.find(id);
const auto recent = (j != end(_unresolvedRecent)); const auto recent = (j != end(_unresolvedRecent));
const auto k = _unresolvedMyTags.find(id);
const auto myTag = (k != end(_unresolvedMyTags));
const auto l = _unresolvedTags.find(id);
const auto tag = (l != end(_unresolvedTags));
if (favorite) { if (favorite) {
_unresolvedFavoriteId = ReactionId(); _unresolvedFavoriteId = ReactionId();
_favorite = resolveById(_favoriteId); _favorite = resolveById(_favoriteId);
@ -727,6 +928,14 @@ void Reactions::customEmojiResolveDone(not_null<DocumentData*> document) {
_unresolvedRecent.erase(j); _unresolvedRecent.erase(j);
_recent = resolveByIds(_recentIds, _unresolvedRecent); _recent = resolveByIds(_recentIds, _unresolvedRecent);
} }
if (myTag) {
_unresolvedMyTags.erase(k);
_myTags = resolveByIds(_myTagsIds, _unresolvedMyTags);
}
if (tag) {
_unresolvedTags.erase(l);
_tags = resolveByIds(_tagsIds, _unresolvedTags);
}
if (favorite) { if (favorite) {
_favoriteUpdated.fire({}); _favoriteUpdated.fire({});
} }
@ -736,6 +945,12 @@ void Reactions::customEmojiResolveDone(not_null<DocumentData*> document) {
if (recent) { if (recent) {
_recentUpdated.fire({}); _recentUpdated.fire({});
} }
if (myTag) {
_myTagsUpdated.fire({});
}
if (tag) {
_tagsUpdated.fire({});
}
} }
std::optional<Reaction> Reactions::resolveById(const ReactionId &id) { std::optional<Reaction> Reactions::resolveById(const ReactionId &id) {
@ -1003,6 +1218,10 @@ void MessageReactions::add(const ReactionId &id, bool addToRecent) {
return; return;
} }
auto my = 0; auto my = 0;
const auto tags = _item->reactionsAreTags();
if (tags) {
history->owner().reactions().incrementMyTag(id);
}
_list.erase(ranges::remove_if(_list, [&](MessageReaction &one) { _list.erase(ranges::remove_if(_list, [&](MessageReaction &one) {
const auto removing = one.my && (my == myLimit || ++my == myLimit); const auto removing = one.my && (my == myLimit || ++my == myLimit);
if (!removing) { if (!removing) {
@ -1024,6 +1243,9 @@ void MessageReactions::add(const ReactionId &id, bool addToRecent) {
} }
} }
} }
if (tags) {
history->owner().reactions().decrementMyTag(one.id);
}
return removed; return removed;
}), end(_list)); }), end(_list));
const auto peer = history->peer; const auto peer = history->peer;

View file

@ -42,6 +42,7 @@ struct PossibleItemReactionsRef {
std::vector<not_null<const Reaction*>> recent; std::vector<not_null<const Reaction*>> recent;
bool morePremiumAvailable = false; bool morePremiumAvailable = false;
bool customAllowed = false; bool customAllowed = false;
bool tags = false;
}; };
struct PossibleItemReactions { struct PossibleItemReactions {
@ -51,11 +52,18 @@ struct PossibleItemReactions {
std::vector<Reaction> recent; std::vector<Reaction> recent;
bool morePremiumAvailable = false; bool morePremiumAvailable = false;
bool customAllowed = false; bool customAllowed = false;
bool tags = false;
}; };
[[nodiscard]] PossibleItemReactionsRef LookupPossibleReactions( [[nodiscard]] PossibleItemReactionsRef LookupPossibleReactions(
not_null<HistoryItem*> item); not_null<HistoryItem*> item);
struct MyTagInfo {
ReactionId id;
QString title;
int count = 0;
};
class Reactions final : private CustomEmojiManager::Listener { class Reactions final : private CustomEmojiManager::Listener {
public: public:
explicit Reactions(not_null<Session*> owner); explicit Reactions(not_null<Session*> owner);
@ -70,17 +78,25 @@ public:
void refreshRecent(); void refreshRecent();
void refreshRecentDelayed(); void refreshRecentDelayed();
void refreshDefault(); void refreshDefault();
void refreshMyTags();
void refreshMyTagsDelayed();
void refreshTags();
enum class Type { enum class Type {
Active, Active,
Recent, Recent,
Top, Top,
All, All,
MyTags,
Tags,
}; };
[[nodiscard]] const std::vector<Reaction> &list(Type type) const; [[nodiscard]] const std::vector<Reaction> &list(Type type) const;
[[nodiscard]] const std::vector<MyTagInfo> &myTagsInfo() const;
[[nodiscard]] ReactionId favoriteId() const; [[nodiscard]] ReactionId favoriteId() const;
[[nodiscard]] const Reaction *favorite() const; [[nodiscard]] const Reaction *favorite() const;
void setFavorite(const ReactionId &id); void setFavorite(const ReactionId &id);
void incrementMyTag(const ReactionId &id);
void decrementMyTag(const ReactionId &id);
[[nodiscard]] DocumentData *chooseGenericAnimation( [[nodiscard]] DocumentData *chooseGenericAnimation(
not_null<DocumentData*> custom) const; not_null<DocumentData*> custom) const;
@ -88,6 +104,8 @@ public:
[[nodiscard]] rpl::producer<> recentUpdates() const; [[nodiscard]] rpl::producer<> recentUpdates() const;
[[nodiscard]] rpl::producer<> defaultUpdates() const; [[nodiscard]] rpl::producer<> defaultUpdates() const;
[[nodiscard]] rpl::producer<> favoriteUpdates() const; [[nodiscard]] rpl::producer<> favoriteUpdates() const;
[[nodiscard]] rpl::producer<> myTagsUpdates() const;
[[nodiscard]] rpl::producer<> tagsUpdates() const;
enum class ImageSize { enum class ImageSize {
BottomInfo, BottomInfo,
@ -130,14 +148,20 @@ private:
void requestRecent(); void requestRecent();
void requestDefault(); void requestDefault();
void requestGeneric(); void requestGeneric();
void requestMyTags();
void requestTags();
void updateTop(const MTPDmessages_reactions &data); void updateTop(const MTPDmessages_reactions &data);
void updateRecent(const MTPDmessages_reactions &data); void updateRecent(const MTPDmessages_reactions &data);
void updateDefault(const MTPDmessages_availableReactions &data); void updateDefault(const MTPDmessages_availableReactions &data);
void updateGeneric(const MTPDmessages_stickerSet &data); void updateGeneric(const MTPDmessages_stickerSet &data);
void updateMyTags(const MTPDmessages_savedReactionTags &data);
void updateTags(const MTPDmessages_reactions &data);
void recentUpdated(); void recentUpdated();
void defaultUpdated(); void defaultUpdated();
void myTagsUpdated();
void tagsUpdated();
[[nodiscard]] std::optional<Reaction> resolveById(const ReactionId &id); [[nodiscard]] std::optional<Reaction> resolveById(const ReactionId &id);
[[nodiscard]] std::vector<Reaction> resolveByIds( [[nodiscard]] std::vector<Reaction> resolveByIds(
@ -145,6 +169,7 @@ private:
base::flat_set<ReactionId> &unresolved); base::flat_set<ReactionId> &unresolved);
void resolve(const ReactionId &id); void resolve(const ReactionId &id);
void applyFavorite(const ReactionId &id); void applyFavorite(const ReactionId &id);
void scheduleMyTagsUpdate();
[[nodiscard]] std::optional<Reaction> parse( [[nodiscard]] std::optional<Reaction> parse(
const MTPAvailableReaction &entry); const MTPAvailableReaction &entry);
@ -167,6 +192,13 @@ private:
std::vector<Reaction> _recent; std::vector<Reaction> _recent;
std::vector<ReactionId> _recentIds; std::vector<ReactionId> _recentIds;
base::flat_set<ReactionId> _unresolvedRecent; base::flat_set<ReactionId> _unresolvedRecent;
std::vector<Reaction> _myTags;
std::vector<ReactionId> _myTagsIds;
std::vector<MyTagInfo> _myTagsInfo;
base::flat_set<ReactionId> _unresolvedMyTags;
std::vector<Reaction> _tags;
std::vector<ReactionId> _tagsIds;
base::flat_set<ReactionId> _unresolvedTags;
std::vector<Reaction> _top; std::vector<Reaction> _top;
std::vector<ReactionId> _topIds; std::vector<ReactionId> _topIds;
base::flat_set<ReactionId> _unresolvedTop; base::flat_set<ReactionId> _unresolvedTop;
@ -184,6 +216,8 @@ private:
rpl::event_stream<> _recentUpdated; rpl::event_stream<> _recentUpdated;
rpl::event_stream<> _defaultUpdated; rpl::event_stream<> _defaultUpdated;
rpl::event_stream<> _favoriteUpdated; rpl::event_stream<> _favoriteUpdated;
rpl::event_stream<> _myTagsUpdated;
rpl::event_stream<> _tagsUpdated;
// We need &i->second stay valid while inserting new items. // We need &i->second stay valid while inserting new items.
// So we use std::map instead of base::flat_map here. // So we use std::map instead of base::flat_map here.
@ -203,6 +237,14 @@ private:
mtpRequestId _genericRequestId = 0; mtpRequestId _genericRequestId = 0;
mtpRequestId _myTagsRequestId = 0;
bool _myTagsRequestScheduled = false;
bool _myTagsUpdateScheduled = false;
uint64 _myTagsHash = 0;
mtpRequestId _tagsRequestId = 0;
uint64 _tagsHash = 0;
base::flat_map<ReactionId, ImageSet> _images; base::flat_map<ReactionId, ImageSet> _images;
rpl::lifetime _imagesLoadLifetime; rpl::lifetime _imagesLoadLifetime;
bool _waitingForList = false; bool _waitingForList = false;

View file

@ -20,6 +20,8 @@ namespace {
constexpr auto kPerPage = 50; constexpr auto kPerPage = 50;
constexpr auto kFirstPerPage = 10; constexpr auto kFirstPerPage = 10;
constexpr auto kListPerPage = 100;
constexpr auto kListFirstPerPage = 20;
} // namespace } // namespace
@ -82,7 +84,7 @@ void SavedMessages::sendLoadMore() {
MTP_int(_offsetDate), MTP_int(_offsetDate),
MTP_int(_offsetId), MTP_int(_offsetId),
_offsetPeer ? _offsetPeer->input : MTP_inputPeerEmpty(), _offsetPeer ? _offsetPeer->input : MTP_inputPeerEmpty(),
MTP_int(kPerPage), MTP_int(_offsetId ? kListPerPage : kListFirstPerPage),
MTP_long(0)) // hash MTP_long(0)) // hash
).done([=](const MTPmessages_SavedDialogs &result) { ).done([=](const MTPmessages_SavedDialogs &result) {
apply(result, false); apply(result, false);

View file

@ -98,6 +98,7 @@ std::optional<SearchRequest> PrepareSearchRequest(
MTP_string(query), MTP_string(query),
MTP_inputPeerEmpty(), MTP_inputPeerEmpty(),
MTPInputPeer(), // saved_peer_id MTPInputPeer(), // saved_peer_id
MTPVector<MTPReaction>(), // saved_reaction
MTP_int(topicRootId), MTP_int(topicRootId),
filter, filter,
MTP_int(0), // min_date MTP_int(0), // min_date

View file

@ -1109,7 +1109,7 @@ void Session::watchForOffline(not_null<UserData*> user, TimeId now) {
return; return;
} }
const auto till = user->onlineTill; const auto till = user->onlineTill;
const auto [i, ok] = _watchingForOffline.emplace(user, till); const auto &[i, ok] = _watchingForOffline.emplace(user, till);
if (!ok) { if (!ok) {
if (i->second == till) { if (i->second == till) {
return; return;
@ -1638,7 +1638,7 @@ HistoryItem *Session::changeMessageId(PeerId peerId, MsgId wasId, MsgId nowId) {
} }
const auto item = i->second; const auto item = i->second;
list->erase(i); list->erase(i);
const auto [j, ok] = list->emplace(nowId, item); const auto &[j, ok] = list->emplace(nowId, item);
if (!peerIsChannel(peerId)) { if (!peerIsChannel(peerId)) {
if (IsServerMsgId(wasId)) { if (IsServerMsgId(wasId)) {
@ -1801,7 +1801,7 @@ void Session::registerHighlightProcess(
not_null<HistoryItem*> item) { not_null<HistoryItem*> item) {
Expects(item->inHighlightProcess()); Expects(item->inHighlightProcess());
const auto [i, ok] = _highlightings.emplace(processId, item); const auto &[i, ok] = _highlightings.emplace(processId, item);
Ensures(ok); Ensures(ok);
} }
@ -4272,7 +4272,7 @@ not_null<Folder*> Session::folder(FolderId id) {
if (const auto result = folderLoaded(id)) { if (const auto result = folderLoaded(id)) {
return result; return result;
} }
const auto [it, ok] = _folders.emplace( const auto &[it, ok] = _folders.emplace(
id, id,
std::make_unique<Folder>(this, id)); std::make_unique<Folder>(this, id));
return it->second.get(); return it->second.get();

View file

@ -238,7 +238,7 @@ Story *Stories::applySingle(PeerId peerId, const MTPstoryItem &story) {
void Stories::requestPeerStories( void Stories::requestPeerStories(
not_null<PeerData*> peer, not_null<PeerData*> peer,
Fn<void()> done) { Fn<void()> done) {
const auto [i, ok] = _requestingPeerStories.emplace(peer); const auto &[i, ok] = _requestingPeerStories.emplace(peer);
if (done) { if (done) {
i->second.push_back(std::move(done)); i->second.push_back(std::move(done));
} }

View file

@ -313,6 +313,8 @@ enum class MessageFlag : uint64 {
ShowSimilarChannels = (1ULL << 41), ShowSimilarChannels = (1ULL << 41),
Sponsored = (1ULL << 42), Sponsored = (1ULL << 42),
ReactionsAreTags = (1ULL << 43),
}; };
inline constexpr bool is_flag_type(MessageFlag) { return true; } inline constexpr bool is_flag_type(MessageFlag) { return true; }
using MessageFlags = base::flags<MessageFlag>; using MessageFlags = base::flags<MessageFlag>;

View file

@ -623,3 +623,6 @@ dialogsStoriesTooltipHide: IconButton(defaultIconButton) {
searchedBarHeight: 32px; searchedBarHeight: 32px;
searchedBarFont: normalFont; searchedBarFont: normalFont;
searchedBarPosition: point(17px, 7px); searchedBarPosition: point(17px, 7px);
dialogsSearchTagSkip: point(8px, 4px);
dialogsSearchTagBottom: 10px;

View file

@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "dialogs/dialogs_indexed_list.h" #include "dialogs/dialogs_indexed_list.h"
#include "dialogs/dialogs_widget.h" #include "dialogs/dialogs_widget.h"
#include "dialogs/dialogs_search_from_controllers.h" #include "dialogs/dialogs_search_from_controllers.h"
#include "dialogs/dialogs_search_tags.h"
#include "history/history.h" #include "history/history.h"
#include "history/history_item.h" #include "history/history_item.h"
#include "core/shortcuts.h" #include "core/shortcuts.h"
@ -40,7 +41,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_chat_filters.h" #include "data/data_chat_filters.h"
#include "data/data_cloud_file.h" #include "data/data_cloud_file.h"
#include "data/data_changes.h" #include "data/data_changes.h"
#include "data/data_message_reactions.h"
#include "data/data_saved_messages.h" #include "data/data_saved_messages.h"
#include "data/data_saved_sublist.h"
#include "data/data_stories.h" #include "data/data_stories.h"
#include "data/stickers/data_stickers.h" #include "data/stickers/data_stickers.h"
#include "data/data_send_action.h" #include "data/data_send_action.h"
@ -477,22 +480,30 @@ int InnerWidget::peerSearchOffset() const {
+ st::searchedBarHeight; + st::searchedBarHeight;
} }
int InnerWidget::searchedOffset() const { int InnerWidget::searchInChatOffset() const {
auto result = peerSearchOffset(); auto result = peerSearchOffset() - st::searchedBarHeight;
if (!_peerSearchResults.empty()) { if (!_peerSearchResults.empty()) {
result += (_peerSearchResults.size() * st::dialogsRowHeight) result += (_peerSearchResults.size() * st::dialogsRowHeight)
+ st::searchedBarHeight; + st::searchedBarHeight;
} }
result += searchInChatSkip();
return result; return result;
} }
int InnerWidget::searchedOffset() const {
return searchInChatOffset()
+ searchInChatSkip()
+ st::searchedBarHeight;
}
int InnerWidget::searchInChatSkip() const { int InnerWidget::searchInChatSkip() const {
auto result = 0; auto result = 0;
if (_searchTags) {
result += _searchTags->height();
}
if (_searchInChat) { if (_searchInChat) {
result += st::searchedBarHeight + st::dialogsSearchInHeight; result += st::searchedBarHeight + st::dialogsSearchInHeight;
} }
if (_searchFromPeer) { if (_searchFromShown) {
if (_searchInChat) { if (_searchInChat) {
result += st::lineWidth; result += st::lineWidth;
} }
@ -1111,17 +1122,25 @@ void InnerWidget::paintSearchInChat(
auto height = searchInChatSkip(); auto height = searchInChatSkip();
auto top = 0; auto top = 0;
if (_searchTags) {
const auto height = _searchTags->height();
p.fillRect(0, top, width(), height, currentBg());
const auto position = QPoint(_searchTagsLeft, 0);
_searchTags->paint(p, position, context.now, context.paused);
top += height;
}
p.setFont(st::searchedBarFont); p.setFont(st::searchedBarFont);
if (_searchInChat) { if (_searchInChat) {
top += st::searchedBarHeight; const auto bar = st::searchedBarHeight;
p.fillRect(0, 0, width(), top, st::searchedBarBg); p.fillRect(0, top, width(), top + bar, st::searchedBarBg);
p.setPen(st::searchedBarFg); p.setPen(st::searchedBarFg);
p.drawTextLeft(st::searchedBarPosition.x(), st::searchedBarPosition.y(), width(), tr::lng_dlg_search_in(tr::now)); p.drawTextLeft(st::searchedBarPosition.x(), top + st::searchedBarPosition.y(), width(), tr::lng_dlg_search_in(tr::now));
top += bar;
} }
auto fullRect = QRect(0, top, width(), height - top); auto fullRect = QRect(0, top, width(), height - top);
p.fillRect(fullRect, currentBg()); p.fillRect(fullRect, currentBg());
if (_searchInChat) { if (_searchInChat) {
if (_searchFromPeer) { if (_searchFromShown) {
p.fillRect(QRect(0, top + st::dialogsSearchInHeight, width(), st::lineWidth), st::shadowFg); p.fillRect(QRect(0, top + st::dialogsSearchInHeight, width(), st::lineWidth), st::shadowFg);
} }
p.setPen(st::dialogsNameFg); p.setPen(st::dialogsNameFg);
@ -1135,15 +1154,17 @@ void InnerWidget::paintSearchInChat(
} else { } else {
paintSearchInPeer(p, peer, _searchInChatUserpic, top, _searchInChatText); paintSearchInPeer(p, peer, _searchInChatUserpic, top, _searchInChatText);
} }
} else if (const auto sublist = _searchInChat.sublist()) {
paintSearchInSaved(p, top, _searchInChatText);
} else { } else {
Unexpected("Empty Key in paintSearchInChat."); Unexpected("Empty Key in paintSearchInChat.");
} }
top += st::dialogsSearchInHeight + st::lineWidth; top += st::dialogsSearchInHeight + st::lineWidth;
} }
if (_searchFromPeer) { if (_searchFromShown) {
p.setPen(st::dialogsTextFg); p.setPen(st::dialogsTextFg);
p.setTextPalette(st::dialogsSearchFromPalette); p.setTextPalette(st::dialogsSearchFromPalette);
paintSearchInPeer(p, _searchFromPeer, _searchFromUserUserpic, top, _searchFromUserText); paintSearchInPeer(p, _searchFromShown, _searchFromUserUserpic, top, _searchFromUserText);
p.restoreTextPalette(); p.restoreTextPalette();
} }
} }
@ -1276,6 +1297,21 @@ void InnerWidget::selectByMouse(QPoint globalPosition) {
_lastMousePosition = globalPosition; _lastMousePosition = globalPosition;
_lastRowLocalMouseX = local.x(); _lastRowLocalMouseX = local.x();
const auto tagBase = QPoint(_searchTagsLeft, searchInChatOffset());
const auto tagPoint = local - tagBase;
const auto inTags = _searchTags
&& QRect(
tagBase,
QSize(width() - 2 * _searchTagsLeft, _searchTags->height())
).contains(local);
const auto tagLink = inTags
? _searchTags->lookupHandler(tagPoint)
: nullptr;
ClickHandler::setActive(tagLink);
if (inTags) {
setCursor(tagLink ? style::cur_pointer : style::cur_default);
}
const auto w = width(); const auto w = width();
const auto mouseY = local.y(); const auto mouseY = local.y();
clearIrrelevantState(); clearIrrelevantState();
@ -1370,7 +1406,7 @@ void InnerWidget::selectByMouse(QPoint globalPosition) {
updateSelectedRow(); updateSelectedRow();
} }
} }
if (wasSelected != isSelected()) { if (!inTags && wasSelected != isSelected()) {
setCursor(wasSelected ? style::cur_default : style::cur_pointer); setCursor(wasSelected ? style::cur_default : style::cur_pointer);
} }
} }
@ -1452,6 +1488,7 @@ void InnerWidget::mousePressEvent(QMouseEvent *e) {
QSize(width(), _st->height), QSize(width(), _st->height),
row->repaint()); row->repaint());
} }
ClickHandler::pressed();
if (anim::Disabled() if (anim::Disabled()
&& (!_pressed || !_pressed->entry()->isPinnedDialog(_filterId))) { && (!_pressed || !_pressed->entry()->isPinnedDialog(_filterId))) {
mousePressReleased(e->globalPos(), e->button(), e->modifiers()); mousePressReleased(e->globalPos(), e->button(), e->modifiers());
@ -1743,6 +1780,9 @@ void InnerWidget::mousePressReleased(
chooseRow(modifiers, pressedTopicRootId); chooseRow(modifiers, pressedTopicRootId);
} }
} }
if (auto activated = ClickHandler::unpressed()) {
ActivateClickHandler(window(), activated, ClickContext{ button });
}
} }
void InnerWidget::setCollapsedPressed(int pressed) { void InnerWidget::setCollapsedPressed(int pressed) {
@ -1825,9 +1865,10 @@ void InnerWidget::moveCancelSearchButtons() {
st::columnMinimalWidthLeft - _narrowWidth); st::columnMinimalWidthLeft - _narrowWidth);
const auto left = widthForCancelButton - st::dialogsSearchInSkip - _cancelSearchInChat->width(); const auto left = widthForCancelButton - st::dialogsSearchInSkip - _cancelSearchInChat->width();
const auto top = (st::dialogsSearchInHeight - st::dialogsCancelSearchInPeer.height) / 2; const auto top = (st::dialogsSearchInHeight - st::dialogsCancelSearchInPeer.height) / 2;
_cancelSearchInChat->moveToLeft(left, st::searchedBarHeight + top); const auto skip = st::searchedBarHeight + (_searchTags ? _searchTags->height() : 0);
const auto skip = _searchInChat ? (st::searchedBarHeight + st::dialogsSearchInHeight + st::lineWidth) : 0; _cancelSearchInChat->moveToLeft(left, skip + top);
_cancelSearchFromUser->moveToLeft(left, skip + top); const auto next = _searchInChat ? (skip + st::dialogsSearchInHeight + st::lineWidth) : 0;
_cancelSearchFromUser->moveToLeft(left, next + top);
} }
void InnerWidget::dialogRowReplaced( void InnerWidget::dialogRowReplaced(
@ -2330,7 +2371,9 @@ void InnerWidget::applyFilterUpdate(QString newFilter, bool force) {
newFilter = words.isEmpty() ? QString() : words.join(' '); newFilter = words.isEmpty() ? QString() : words.join(' ');
if (newFilter != _filter || force) { if (newFilter != _filter || force) {
_filter = newFilter; _filter = newFilter;
if (_filter.isEmpty() && !_searchFromPeer) { if (_filter.isEmpty()
&& !_searchFromPeer
&& _searchTagsSelected.empty()) {
clearFilter(); clearFilter();
} else { } else {
setState(WidgetState::Filtered); setState(WidgetState::Filtered);
@ -2350,7 +2393,9 @@ void InnerWidget::applyFilterUpdate(QString newFilter, bool force) {
top += i->row->height(); top += i->row->height();
} }
}; };
if (!_searchInChat && !_searchFromPeer && !words.isEmpty()) { if (!_searchInChat
&& !_searchFromPeer
&& !words.isEmpty()) {
if (_savedSublists) { if (_savedSublists) {
const auto owner = &session().data(); const auto owner = &session().data();
append(owner->savedMessages().chatsList()->indexed()); append(owner->savedMessages().chatsList()->indexed());
@ -2413,7 +2458,7 @@ void InnerWidget::appendToFiltered(Key key) {
} }
auto row = std::make_unique<Row>(key, 0, 0); auto row = std::make_unique<Row>(key, 0, 0);
row->recountHeight(_narrowRatio); row->recountHeight(_narrowRatio);
const auto [i, ok] = _filterResultsGlobal.emplace(key, std::move(row)); const auto &[i, ok] = _filterResultsGlobal.emplace(key, std::move(row));
const auto height = filteredHeight(); const auto height = filteredHeight();
_filterResults.emplace_back(i->second.get()); _filterResults.emplace_back(i->second.get());
_filterResults.back().top = height; _filterResults.back().top = height;
@ -2791,6 +2836,11 @@ void InnerWidget::refresh(bool toTop) {
return refreshWithCollapsedRows(toTop); return refreshWithCollapsedRows(toTop);
} }
refreshEmptyLabel(); refreshEmptyLabel();
if (_searchTags) {
_searchTagsLeft = st::dialogsFilterSkip
+ st::dialogsFilterPadding.x();
_searchTags->resizeToWidth(width() - 2 * _searchTagsLeft);
}
auto h = 0; auto h = 0;
if (_state == WidgetState::Default) { if (_state == WidgetState::Default) {
if (_shownList->empty()) { if (_shownList->empty()) {
@ -2918,26 +2968,73 @@ bool InnerWidget::hasFilteredResults() const {
return !_filterResults.empty() && _hashtagResults.empty(); return !_filterResults.empty() && _hashtagResults.empty();
} }
void InnerWidget::searchInChat(Key key, PeerData *from) { void InnerWidget::searchInChat(
Key key,
PeerData *from,
std::vector<Data::ReactionId> tags) {
_searchInMigrated = nullptr; _searchInMigrated = nullptr;
if (const auto peer = key.peer()) { const auto sublist = key.sublist();
const auto peer = sublist ? session().user().get() : key.peer();
if (peer) {
if (const auto migrateTo = peer->migrateTo()) { if (const auto migrateTo = peer->migrateTo()) {
return searchInChat(peer->owner().history(migrateTo), from); const auto to = peer->owner().history(migrateTo);
return searchInChat(to, from, tags);
} else if (const auto migrateFrom = peer->migrateFrom()) { } else if (const auto migrateFrom = peer->migrateFrom()) {
_searchInMigrated = peer->owner().history(migrateFrom); _searchInMigrated = peer->owner().history(migrateFrom);
} }
if (peer->isSelf()) {
const auto reactions = &peer->owner().reactions();
const auto list = [=] {
// Disable reactions as tags for now.
//return reactions->list(Data::Reactions::Type::MyTags);
return std::vector<Data::Reaction>();
};
_searchTags = std::make_unique<SearchTags>(
&peer->owner(),
rpl::single(
list()
) | rpl::then(
reactions->myTagsUpdates() | rpl::map(list)
),
tags);
_searchTags->selectedValue(
) | rpl::start_with_next([=](std::vector<Data::ReactionId> &&list) {
_searchTagsSelected = std::move(list);
}, _searchTags->lifetime());
_searchTags->repaintRequests() | rpl::start_with_next([=] {
const auto height = _searchTags->height();
update(0, searchInChatOffset(), width(), height);
}, _searchTags->lifetime());
_searchTags->heightValue() | rpl::filter(
rpl::mappers::_1 > 0
) | rpl::start_with_next([=] {
refresh();
moveCancelSearchButtons();
}, _searchTags->lifetime());
} else {
_searchTags = nullptr;
_searchTagsSelected.clear();
}
} else {
_searchTags = nullptr;
_searchTagsSelected.clear();
} }
_searchInChat = key; _searchInChat = key;
_searchFromPeer = from; _searchFromPeer = from;
_searchFromShown = key.sublist() ? key.sublist()->peer().get() : from;
if (_searchInChat) { if (_searchInChat) {
onHashtagFilterUpdate(QStringView()); onHashtagFilterUpdate(QStringView());
_cancelSearchInChat->show(); _cancelSearchInChat->show();
} else { } else {
_cancelSearchInChat->hide(); _cancelSearchInChat->hide();
} }
if (_searchFromPeer) { if (_searchFromShown) {
_cancelSearchFromUser->show(); _cancelSearchFromUser->show();
_searchFromUserUserpic = _searchFromPeer->createUserpicView(); _searchFromUserUserpic = _searchFromShown->createUserpicView();
} else { } else {
_cancelSearchFromUser->hide(); _cancelSearchFromUser->hide();
_searchFromUserUserpic = {}; _searchFromUserUserpic = {};
@ -2946,7 +3043,7 @@ void InnerWidget::searchInChat(Key key, PeerData *from) {
refreshSearchInChatLabel(); refreshSearchInChatLabel();
} }
if (const auto peer = _searchInChat.peer()) { if (peer) {
_searchInChatUserpic = peer->createUserpicView(); _searchInChatUserpic = peer->createUserpicView();
} else { } else {
_searchInChatUserpic = {}; _searchInChatUserpic = {};
@ -2957,6 +3054,13 @@ void InnerWidget::searchInChat(Key key, PeerData *from) {
_searchInChat || !_filter.isEmpty()); _searchInChat || !_filter.isEmpty());
} }
auto InnerWidget::searchTagsValue() const
-> rpl::producer<std::vector<Data::ReactionId>> {
return _searchTags
? _searchTags->selectedValue()
: rpl::single(std::vector<Data::ReactionId>());
}
void InnerWidget::refreshSearchInChatLabel() { void InnerWidget::refreshSearchInChatLabel() {
const auto dialog = [&] { const auto dialog = [&] {
if (const auto topic = _searchInChat.topic()) { if (const auto topic = _searchInChat.topic()) {
@ -2968,6 +3072,8 @@ void InnerWidget::refreshSearchInChatLabel() {
return tr::lng_replies_messages(tr::now); return tr::lng_replies_messages(tr::now);
} }
return peer->name(); return peer->name();
} else if (_searchInChat.sublist()) {
return tr::lng_saved_messages(tr::now);
} }
return QString(); return QString();
}(); }();
@ -2977,7 +3083,7 @@ void InnerWidget::refreshSearchInChatLabel() {
dialog, dialog,
Ui::DialogTextOptions()); Ui::DialogTextOptions());
} }
const auto from = _searchFromPeer ? _searchFromPeer->name() : QString(); const auto from = _searchFromShown ? _searchFromShown->name() : u""_q;
if (!from.isEmpty()) { if (!from.isEmpty()) {
const auto fromUserText = tr::lng_dlg_search_from( const auto fromUserText = tr::lng_dlg_search_from(
tr::now, tr::now,
@ -3809,7 +3915,7 @@ void InnerWidget::setupShortcuts() {
auto &&folders = ranges::views::zip( auto &&folders = ranges::views::zip(
Shortcuts::kShowFolder, Shortcuts::kShowFolder,
ranges::views::ints(0, ranges::unreachable)); ranges::views::ints(0, ranges::unreachable));
for (const auto [command, index] : folders) { for (const auto &[command, index] : folders) {
const auto select = (command == Command::ShowFolderLast) const auto select = (command == Command::ShowFolderLast)
? (filtersCount - 1) ? (filtersCount - 1)
: std::clamp(index, 0, filtersCount - 1); : std::clamp(index, 0, filtersCount - 1);
@ -3836,7 +3942,7 @@ void InnerWidget::setupShortcuts() {
auto &&pinned = ranges::views::zip( auto &&pinned = ranges::views::zip(
kPinned, kPinned,
ranges::views::ints(0, ranges::unreachable)); ranges::views::ints(0, ranges::unreachable));
for (const auto [command, index] : pinned) { for (const auto &[command, index] : pinned) {
request->check(command) && request->handle([=, index = index] { request->check(command) && request->handle([=, index = index] {
const auto list = (_filterId const auto list = (_filterId
? session().data().chatsFilters().chatsList(_filterId) ? session().data().chatsFilters().chatsList(_filterId)

View file

@ -43,6 +43,7 @@ namespace Data {
class Thread; class Thread;
class Folder; class Folder;
class Forum; class Forum;
struct ReactionId;
} // namespace Data } // namespace Data
namespace Dialogs::Ui { namespace Dialogs::Ui {
@ -57,6 +58,7 @@ namespace Dialogs {
class Row; class Row;
class FakeRow; class FakeRow;
class IndexedList; class IndexedList;
class SearchTags;
struct ChosenRow { struct ChosenRow {
Key key; Key key;
@ -137,7 +139,12 @@ public:
} }
[[nodiscard]] bool hasFilteredResults() const; [[nodiscard]] bool hasFilteredResults() const;
void searchInChat(Key key, PeerData *from); void searchInChat(
Key key,
PeerData *from,
std::vector<Data::ReactionId> tags);
[[nodiscard]] auto searchTagsValue() const
-> rpl::producer<std::vector<Data::ReactionId>>;
void applyFilterUpdate(QString newFilter, bool force = false); void applyFilterUpdate(QString newFilter, bool force = false);
void onHashtagFilterUpdate(QStringView newFilter); void onHashtagFilterUpdate(QStringView newFilter);
@ -325,6 +332,7 @@ private:
[[nodiscard]] int filteredIndex(int y) const; [[nodiscard]] int filteredIndex(int y) const;
[[nodiscard]] int filteredHeight(int till = -1) const; [[nodiscard]] int filteredHeight(int till = -1) const;
[[nodiscard]] int peerSearchOffset() const; [[nodiscard]] int peerSearchOffset() const;
[[nodiscard]] int searchInChatOffset() const;
[[nodiscard]] int searchedOffset() const; [[nodiscard]] int searchedOffset() const;
[[nodiscard]] int searchInChatSkip() const; [[nodiscard]] int searchInChatSkip() const;
@ -478,10 +486,14 @@ private:
Key _searchInChat; Key _searchInChat;
History *_searchInMigrated = nullptr; History *_searchInMigrated = nullptr;
PeerData *_searchFromPeer = nullptr; PeerData *_searchFromPeer = nullptr;
PeerData *_searchFromShown = nullptr;
mutable Ui::PeerUserpicView _searchInChatUserpic; mutable Ui::PeerUserpicView _searchInChatUserpic;
mutable Ui::PeerUserpicView _searchFromUserUserpic; mutable Ui::PeerUserpicView _searchFromUserUserpic;
Ui::Text::String _searchInChatText; Ui::Text::String _searchInChatText;
Ui::Text::String _searchFromUserText; Ui::Text::String _searchFromUserText;
std::unique_ptr<SearchTags> _searchTags;
std::vector<Data::ReactionId> _searchTagsSelected;
int _searchTagsLeft = 0;
RowDescriptor _menuRow; RowDescriptor _menuRow;
base::flat_map< base::flat_map<

View file

@ -0,0 +1,269 @@
/*
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 "dialogs/dialogs_search_tags.h"
#include "base/qt/qt_key_modifiers.h"
#include "data/stickers/data_custom_emoji.h"
#include "data/data_document.h"
#include "data/data_message_reactions.h"
#include "data/data_session.h"
#include "history/view/reactions/history_view_reactions.h"
#include "ui/effects/animation_value.h"
#include "ui/power_saving.h"
#include "styles/style_chat.h"
#include "styles/style_dialogs.h"
namespace Dialogs {
struct SearchTags::Tag {
Data::ReactionId id;
std::unique_ptr<Ui::Text::CustomEmoji> custom;
mutable QImage image;
QRect geometry;
ClickHandlerPtr link;
bool selected = false;
};
SearchTags::SearchTags(
not_null<Data::Session*> owner,
rpl::producer<std::vector<Data::Reaction>> tags,
std::vector<Data::ReactionId> selected)
: _owner(owner)
, _added(selected) {
std::move(
tags
) | rpl::start_with_next([=](const std::vector<Data::Reaction> &list) {
fill(list);
}, _lifetime);
// Mark the `selected` reactions as selected in `_tags`.
for (const auto &id : selected) {
const auto i = ranges::find(_tags, id, &Tag::id);
if (i != end(_tags)) {
i->selected = true;
}
}
style::PaletteChanged(
) | rpl::start_with_next([=] {
_normalBg = _selectedBg = QImage();
}, _lifetime);
}
SearchTags::~SearchTags() = default;
void SearchTags::fill(const std::vector<Data::Reaction> &list) {
const auto selected = collectSelected();
_tags.clear();
_tags.reserve(list.size());
const auto link = [&](Data::ReactionId id) {
return std::make_shared<LambdaClickHandler>(crl::guard(this, [=] {
const auto i = ranges::find(_tags, id, &Tag::id);
if (i != end(_tags)) {
if (!i->selected && !base::IsShiftPressed()) {
for (auto &tag : _tags) {
tag.selected = false;
}
}
i->selected = !i->selected;
_selectedChanges.fire({});
}
}));
};
const auto push = [&](Data::ReactionId id) {
const auto customId = id.custom();
_tags.push_back({
.id = id,
.custom = (customId
? _owner->customEmojiManager().create(
customId,
[=] { _repaintRequests.fire({}); })
: nullptr),
.link = link(id),
.selected = ranges::contains(selected, id),
});
if (!customId) {
_owner->reactions().preloadImageFor(id);
}
};
for (const auto &reaction : list) {
push(reaction.id);
}
for (const auto &reaction : _added) {
if (!ranges::contains(_tags, reaction, &Tag::id)) {
push(reaction);
}
}
if (_width > 0) {
layout();
}
}
void SearchTags::layout() {
Expects(_width > 0);
const auto &bg = validateBg(false);
const auto skip = st::dialogsSearchTagSkip;
const auto size = bg.size() / bg.devicePixelRatio();
const auto xsingle = size.width() + skip.x();
const auto ysingle = size.height() + skip.y();
const auto columns = std::max((_width + skip.x()) / xsingle, 1);
const auto rows = (_tags.size() + columns - 1) / columns;
for (auto row = 0; row != rows; ++row) {
for (auto column = 0; column != columns; ++column) {
const auto index = row * columns + column;
if (index >= _tags.size()) {
break;
}
const auto x = column * xsingle;
const auto y = row * ysingle;
_tags[index].geometry = QRect(QPoint(x, y), size);
}
}
const auto bottom = st::dialogsSearchTagBottom;
_height = rows ? (rows * ysingle - skip.y() + bottom) : 0;
}
void SearchTags::resizeToWidth(int width) {
if (_width == width || width <= 0) {
return;
}
_width = width;
layout();
}
int SearchTags::height() const {
return _height.current();
}
rpl::producer<int> SearchTags::heightValue() const {
return _height.value();
}
rpl::producer<> SearchTags::repaintRequests() const {
return _repaintRequests.events();
}
ClickHandlerPtr SearchTags::lookupHandler(QPoint point) const {
for (const auto &tag : _tags) {
if (tag.geometry.contains(point.x(), point.y())) {
return tag.link;
}
}
return nullptr;
}
auto SearchTags::selectedValue() const
-> rpl::producer<std::vector<Data::ReactionId>> {
return _selectedChanges.events() | rpl::map([=] {
return collectSelected();
});
}
void SearchTags::paintCustomFrame(
QPainter &p,
not_null<Ui::Text::CustomEmoji*> emoji,
QPoint innerTopLeft,
crl::time now,
bool paused,
const QColor &textColor) const {
if (_customCache.isNull()) {
using namespace Ui::Text;
const auto size = st::emojiSize;
const auto factor = style::DevicePixelRatio();
const auto adjusted = AdjustCustomEmojiSize(size);
_customCache = QImage(
QSize(adjusted, adjusted) * factor,
QImage::Format_ARGB32_Premultiplied);
_customCache.setDevicePixelRatio(factor);
_customSkip = (size - adjusted) / 2;
}
_customCache.fill(Qt::transparent);
auto q = QPainter(&_customCache);
emoji->paint(q, {
.textColor = textColor,
.now = now,
.paused = paused || On(PowerSaving::kEmojiChat),
});
q.end();
_customCache = Images::Round(
std::move(_customCache),
(Images::Option::RoundLarge
| Images::Option::RoundSkipTopRight
| Images::Option::RoundSkipBottomRight));
p.drawImage(
innerTopLeft + QPoint(_customSkip, _customSkip),
_customCache);
}
void SearchTags::paint(
QPainter &p,
QPoint position,
crl::time now,
bool paused) const {
const auto size = st::reactionInlineSize;
const auto skip = (size - st::reactionInlineImage) / 2;
const auto padding = st::reactionInlinePadding;
for (const auto &tag : _tags) {
const auto geometry = tag.geometry.translated(position);
p.drawImage(geometry.topLeft(), validateBg(tag.selected));
if (!tag.custom && tag.image.isNull()) {
tag.image = _owner->reactions().resolveImageFor(
tag.id,
::Data::Reactions::ImageSize::InlineList);
}
const auto inner = geometry.marginsRemoved(padding);
const auto image = QRect(
inner.topLeft() + QPoint(skip, skip),
QSize(st::reactionInlineImage, st::reactionInlineImage));
if (const auto custom = tag.custom.get()) {
const auto textFg = tag.selected
? st::dialogsNameFgActive->c
: st::dialogsNameFgOver->c;
paintCustomFrame(
p,
custom,
inner.topLeft(),
now,
paused,
textFg);
} else if (!tag.image.isNull()) {
p.drawImage(image.topLeft(), tag.image);
}
}
}
const QImage &SearchTags::validateBg(bool selected) const {
using namespace HistoryView::Reactions;
auto &image = selected ? _selectedBg : _normalBg;
if (image.isNull()) {
const auto tagBg = selected
? st::dialogsBgActive->c
: st::dialogsBgOver->c;
const auto dotBg = selected
? anim::with_alpha(tagBg, InlineList::TagDotAlpha())
: st::windowSubTextFg->c;
image = InlineList::PrepareTagBg(tagBg, dotBg);
}
return image;
}
std::vector<Data::ReactionId> SearchTags::collectSelected() const {
return _tags | ranges::views::filter(
&Tag::selected
) | ranges::views::transform(
&Tag::id
) | ranges::to_vector;
}
rpl::lifetime &SearchTags::lifetime() {
return _lifetime;
}
} // namespace Dialogs

View file

@ -0,0 +1,80 @@
/*
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 "base/weak_ptr.h"
namespace Data {
class Session;
struct Reaction;
struct ReactionId;
} // namespace Data
namespace Ui::Text {
class CustomEmoji;
} // namespace Ui::Text
namespace Dialogs {
class SearchTags final : public base::has_weak_ptr {
public:
SearchTags(
not_null<Data::Session*> owner,
rpl::producer<std::vector<Data::Reaction>> tags,
std::vector<Data::ReactionId> selected);
~SearchTags();
void resizeToWidth(int width);
[[nodiscard]] int height() const;
[[nodiscard]] rpl::producer<int> heightValue() const;
[[nodiscard]] rpl::producer<> repaintRequests() const;
[[nodiscard]] ClickHandlerPtr lookupHandler(QPoint point) const;
[[nodiscard]] auto selectedValue() const
-> rpl::producer<std::vector<Data::ReactionId>>;
void paint(
QPainter &p,
QPoint position,
crl::time now,
bool paused) const;
[[nodiscard]] rpl::lifetime &lifetime();
private:
struct Tag;
void fill(const std::vector<Data::Reaction> &list);
void paintCustomFrame(
QPainter &p,
not_null<Ui::Text::CustomEmoji*> emoji,
QPoint innerTopLeft,
crl::time now,
bool paused,
const QColor &textColor) const;
void layout();
[[nodiscard]] std::vector<Data::ReactionId> collectSelected() const;
[[nodiscard]] const QImage &validateBg(bool selected) const;
const not_null<Data::Session*> _owner;
std::vector<Data::ReactionId> _added;
std::vector<Tag> _tags;
rpl::event_stream<> _selectedChanges;
rpl::event_stream<> _repaintRequests;
mutable QImage _normalBg;
mutable QImage _selectedBg;
mutable QImage _customCache;
mutable int _customSkip = 0;
rpl::variable<int> _height;
int _width = 0;
rpl::lifetime _lifetime;
};
} // namespace Dialogs

View file

@ -67,6 +67,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_changes.h" #include "data/data_changes.h"
#include "data/data_download_manager.h" #include "data/data_download_manager.h"
#include "data/data_chat_filters.h" #include "data/data_chat_filters.h"
#include "data/data_saved_sublist.h"
#include "data/data_stories.h" #include "data/data_stories.h"
#include "info/downloads/info_downloads_widget.h" #include "info/downloads/info_downloads_widget.h"
#include "info/info_memento.h" #include "info/info_memento.h"
@ -832,7 +833,7 @@ void Widget::setupStories() {
{ {
return; return;
} }
_stories->verticalScrollEvents( _stories->verticalScrollEvents(
) | rpl::start_with_next([=](not_null<QWheelEvent*> e) { ) | rpl::start_with_next([=](not_null<QWheelEvent*> e) {
_scroll->viewportEvent(e); _scroll->viewportEvent(e);
@ -1733,7 +1734,7 @@ void Widget::loadMoreBlockedByDate() {
bool Widget::searchMessages(bool searchCache) { bool Widget::searchMessages(bool searchCache) {
auto result = false; auto result = false;
auto q = currentSearchQuery().trimmed(); auto q = currentSearchQuery().trimmed();
if (q.isEmpty() && !_searchFromAuthor) { if (q.isEmpty() && !_searchFromAuthor && _searchTags.empty()) {
cancelSearchRequest(); cancelSearchRequest();
_api.request(base::take(_peerSearchRequest)).cancel(); _api.request(base::take(_peerSearchRequest)).cancel();
_api.request(base::take(_topicSearchRequest)).cancel(); _api.request(base::take(_topicSearchRequest)).cancel();
@ -1750,6 +1751,7 @@ bool Widget::searchMessages(bool searchCache) {
if (i != _searchCache.end()) { if (i != _searchCache.end()) {
_searchQuery = q; _searchQuery = q;
_searchQueryFrom = _searchFromAuthor; _searchQueryFrom = _searchFromAuthor;
_searchQueryTags = _searchTags;
_searchNextRate = 0; _searchNextRate = 0;
_searchFull = _searchFullMigrated = false; _searchFull = _searchFullMigrated = false;
cancelSearchRequest(); cancelSearchRequest();
@ -1761,9 +1763,12 @@ bool Widget::searchMessages(bool searchCache) {
0); 0);
result = true; result = true;
} }
} else if (_searchQuery != q || _searchQueryFrom != _searchFromAuthor) { } else if (_searchQuery != q
|| _searchQueryFrom != _searchFromAuthor
|| _searchQueryTags != _searchTags) {
_searchQuery = q; _searchQuery = q;
_searchQueryFrom = _searchFromAuthor; _searchQueryFrom = _searchFromAuthor;
_searchQueryTags = _searchTags;
_searchNextRate = 0; _searchNextRate = 0;
_searchFull = _searchFullMigrated = false; _searchFull = _searchFullMigrated = false;
cancelSearchRequest(); cancelSearchRequest();
@ -1772,18 +1777,31 @@ bool Widget::searchMessages(bool searchCache) {
auto &histories = session().data().histories(); auto &histories = session().data().histories();
const auto type = Data::Histories::RequestType::History; const auto type = Data::Histories::RequestType::History;
const auto history = session().data().history(peer); const auto history = session().data().history(peer);
const auto sublist = _openedForum
? nullptr
: _searchInChat.sublist();
const auto fromPeer = sublist ? nullptr : _searchQueryFrom;
const auto savedPeer = sublist
? sublist->peer().get()
: nullptr;
_searchInHistoryRequest = histories.sendRequest(history, type, [=](Fn<void()> finish) { _searchInHistoryRequest = histories.sendRequest(history, type, [=](Fn<void()> finish) {
const auto type = SearchRequestType::PeerFromStart; const auto type = SearchRequestType::PeerFromStart;
using Flag = MTPmessages_Search::Flag; using Flag = MTPmessages_Search::Flag;
_searchRequest = session().api().request(MTPmessages_Search( _searchRequest = session().api().request(MTPmessages_Search(
MTP_flags((topic ? Flag::f_top_msg_id : Flag()) MTP_flags((topic ? Flag::f_top_msg_id : Flag())
| (_searchQueryFrom ? Flag::f_from_id : Flag())), | (fromPeer ? Flag::f_from_id : Flag())
| (savedPeer ? Flag::f_saved_peer_id : Flag())
| (_searchQueryTags.empty()
? Flag()
: Flag::f_saved_reaction)),
peer->input, peer->input,
MTP_string(_searchQuery), MTP_string(_searchQuery),
(_searchQueryFrom (fromPeer ? fromPeer->input : MTP_inputPeerEmpty()),
? _searchQueryFrom->input (savedPeer ? savedPeer->input : MTP_inputPeerEmpty()),
: MTP_inputPeerEmpty()), MTP_vector_from_range(
MTPInputPeer(), // saved_peer_id _searchQueryTags | ranges::views::transform(
Data::ReactionToMTP
)),
MTP_int(topic ? topic->rootId() : 0), MTP_int(topic ? topic->rootId() : 0),
MTP_inputMessagesFilterEmpty(), MTP_inputMessagesFilterEmpty(),
MTP_int(0), // min_date MTP_int(0), // min_date
@ -1887,6 +1905,7 @@ bool Widget::searchMessages(bool searchCache) {
bool Widget::searchForPeersRequired(const QString &query) const { bool Widget::searchForPeersRequired(const QString &query) const {
return !_searchInChat return !_searchInChat
&& !_searchFromAuthor && !_searchFromAuthor
&& _searchTags.empty()
&& !_openedForum && !_openedForum
&& !query.isEmpty() && !query.isEmpty()
&& (query[0] != '#'); && (query[0] != '#');
@ -1895,6 +1914,7 @@ bool Widget::searchForPeersRequired(const QString &query) const {
bool Widget::searchForTopicsRequired(const QString &query) const { bool Widget::searchForTopicsRequired(const QString &query) const {
return !_searchInChat return !_searchInChat
&& !_searchFromAuthor && !_searchFromAuthor
&& _searchTags.empty()
&& _openedForum && _openedForum
&& !query.isEmpty() && !query.isEmpty()
&& (query[0] != '#') && (query[0] != '#')
@ -1911,7 +1931,7 @@ void Widget::showMainMenu() {
controller()->widget()->showMainMenu(); controller()->widget()->showMainMenu();
} }
void Widget::searchMessages(const QString &query, Key inChat, UserData *from) { void Widget::searchMessages(QString query, Key inChat, UserData *from) {
if (_childList) { if (_childList) {
const auto forum = controller()->shownForum().current(); const auto forum = controller()->shownForum().current();
const auto topic = inChat.topic(); const auto topic = inChat.topic();
@ -1926,6 +1946,12 @@ void Widget::searchMessages(const QString &query, Key inChat, UserData *from) {
controller()->closeFolder(); controller()->closeFolder();
} }
auto tags = std::vector<Data::ReactionId>();
if (const auto tagId = Data::SearchTagFromQuery(query)) {
inChat = session().data().history(session().user());
query = QString();
tags.push_back(tagId);
}
const auto inChatChanged = [&] { const auto inChatChanged = [&] {
const auto inPeer = inChat.peer(); const auto inPeer = inChat.peer();
const auto inTopic = inChat.topic(); const auto inTopic = inChat.topic();
@ -1938,7 +1964,7 @@ void Widget::searchMessages(const QString &query, Key inChat, UserData *from) {
} else if ((inTopic || (inPeer && !inPeer->isForum())) } else if ((inTopic || (inPeer && !inPeer->isForum()))
&& (inChat == _searchInChat)) { && (inChat == _searchInChat)) {
return false; return false;
} else if (const auto inPeer = inChat.peer()) { } else if (inPeer) {
if (const auto to = inPeer->migrateTo()) { if (const auto to = inPeer->migrateTo()) {
if (to == _searchInChat.peer() && !_searchInChat.topic()) { if (to == _searchInChat.peer() && !_searchInChat.topic()) {
return false; return false;
@ -1947,10 +1973,12 @@ void Widget::searchMessages(const QString &query, Key inChat, UserData *from) {
} }
return true; return true;
}(); }();
if ((currentSearchQuery() != query) || inChatChanged) { if ((currentSearchQuery() != query)
|| inChatChanged
|| _searchTags != tags) {
if (inChat) { if (inChat) {
cancelSearch(); cancelSearch();
setSearchInChat(inChat); setSearchInChat(inChat, nullptr, tags);
} }
setSearchQuery(query); setSearchQuery(query);
applyFilterUpdate(true); applyFilterUpdate(true);
@ -2017,6 +2045,13 @@ void Widget::searchMore() {
const auto topic = searchInTopic(); const auto topic = searchInTopic();
const auto type = Data::Histories::RequestType::History; const auto type = Data::Histories::RequestType::History;
const auto history = session().data().history(peer); const auto history = session().data().history(peer);
const auto sublist = _openedForum
? nullptr
: _searchInChat.sublist();
const auto fromPeer = sublist ? nullptr : _searchQueryFrom;
const auto savedPeer = sublist
? sublist->peer().get()
: nullptr;
_searchInHistoryRequest = histories.sendRequest(history, type, [=](Fn<void()> finish) { _searchInHistoryRequest = histories.sendRequest(history, type, [=](Fn<void()> finish) {
const auto type = _lastSearchId const auto type = _lastSearchId
? SearchRequestType::PeerFromOffset ? SearchRequestType::PeerFromOffset
@ -2024,13 +2059,19 @@ void Widget::searchMore() {
using Flag = MTPmessages_Search::Flag; using Flag = MTPmessages_Search::Flag;
_searchRequest = session().api().request(MTPmessages_Search( _searchRequest = session().api().request(MTPmessages_Search(
MTP_flags((topic ? Flag::f_top_msg_id : Flag()) MTP_flags((topic ? Flag::f_top_msg_id : Flag())
| (_searchQueryFrom ? Flag::f_from_id : Flag())), | (fromPeer ? Flag::f_from_id : Flag())
| (savedPeer ? Flag::f_saved_peer_id : Flag())
| (_searchQueryTags.empty()
? Flag()
: Flag::f_saved_reaction)),
peer->input, peer->input,
MTP_string(_searchQuery), MTP_string(_searchQuery),
(_searchQueryFrom (fromPeer ? fromPeer->input : MTP_inputPeerEmpty()),
? _searchQueryFrom->input (savedPeer ? savedPeer->input : MTP_inputPeerEmpty()),
: MTP_inputPeerEmpty()), MTP_vector_from_range(
MTPInputPeer(), // saved_peer_id _searchQueryTags | ranges::views::transform(
Data::ReactionToMTP
)),
MTP_int(topic ? topic->rootId() : 0), MTP_int(topic ? topic->rootId() : 0),
MTP_inputMessagesFilterEmpty(), MTP_inputMessagesFilterEmpty(),
MTP_int(0), // min_date MTP_int(0), // min_date
@ -2104,6 +2145,7 @@ void Widget::searchMore() {
? _searchQueryFrom->input ? _searchQueryFrom->input
: MTP_inputPeerEmpty()), : MTP_inputPeerEmpty()),
MTPInputPeer(), // saved_peer_id MTPInputPeer(), // saved_peer_id
MTPVector<MTPReaction>(), // saved_reaction
MTPint(), // top_msg_id MTPint(), // top_msg_id
MTP_inputMessagesFilterEmpty(), MTP_inputMessagesFilterEmpty(),
MTP_int(0), // min_date MTP_int(0), // min_date
@ -2423,7 +2465,7 @@ void Widget::applyFilterUpdate(bool force) {
updateStoriesVisibility(); updateStoriesVisibility();
const auto filterText = currentSearchQuery(); const auto filterText = currentSearchQuery();
_inner->applyFilterUpdate(filterText, force); _inner->applyFilterUpdate(filterText, force);
if (filterText.isEmpty() && !_searchFromAuthor) { if (filterText.isEmpty() && !_searchFromAuthor && _searchTags.empty()) {
clearSearchCache(); clearSearchCache();
} }
_cancelSearch->toggle(!filterText.isEmpty(), anim::type::normal); _cancelSearch->toggle(!filterText.isEmpty(), anim::type::normal);
@ -2439,7 +2481,9 @@ void Widget::applyFilterUpdate(bool force) {
_peerSearchQuery = QString(); _peerSearchQuery = QString();
} }
if (_chooseFromUser->toggled() || _searchFromAuthor) { if (_chooseFromUser->toggled()
|| _searchFromAuthor
|| !_searchTags.empty()) {
auto switchToChooseFrom = HistoryView::SwitchToChooseFromQuery(); auto switchToChooseFrom = HistoryView::SwitchToChooseFromQuery();
if (_lastFilterText != switchToChooseFrom if (_lastFilterText != switchToChooseFrom
&& switchToChooseFrom.startsWith(_lastFilterText) && switchToChooseFrom.startsWith(_lastFilterText)
@ -2583,9 +2627,12 @@ void Widget::searchInChat(Key chat) {
searchMessages(QString(), chat); searchMessages(QString(), chat);
} }
bool Widget::setSearchInChat(Key chat, PeerData *from) { bool Widget::setSearchInChat(
Key chat,
PeerData *from,
std::vector<Data::ReactionId> tags) {
if (_childList) { if (_childList) {
if (_childList->setSearchInChat(chat, from)) { if (_childList->setSearchInChat(chat, from, tags)) {
return true; return true;
} }
hideChildList(); hideChildList();
@ -2621,7 +2668,8 @@ bool Widget::setSearchInChat(Key chat, PeerData *from) {
if (_layout != Layout::Main) { if (_layout != Layout::Main) {
return false; return false;
} else if (const auto migrateTo = peer->migrateTo()) { } else if (const auto migrateTo = peer->migrateTo()) {
return setSearchInChat(peer->owner().history(migrateTo), from); const auto to = peer->owner().history(migrateTo);
return setSearchInChat(to, from, tags);
} else if (const auto migrateFrom = peer->migrateFrom()) { } else if (const auto migrateFrom = peer->migrateFrom()) {
_searchInMigrated = peer->owner().history(migrateFrom); _searchInMigrated = peer->owner().history(migrateFrom);
} }
@ -2640,7 +2688,20 @@ bool Widget::setSearchInChat(Key chat, PeerData *from) {
if (_searchInChat && _layout == Layout::Main) { if (_searchInChat && _layout == Layout::Main) {
controller()->closeFolder(); controller()->closeFolder();
} }
_inner->searchInChat(_searchInChat, _searchFromAuthor); _searchTags = std::move(tags);
_inner->searchInChat(_searchInChat, _searchFromAuthor, _searchTags);
_searchTagsLifetime = _inner->searchTagsValue(
) | rpl::start_with_next([=](std::vector<Data::ReactionId> &&list) {
if (_searchTags != list) {
clearSearchCache();
_searchTags = std::move(list);
if (_searchTags.empty()) {
applyFilterUpdate(true);
} else {
searchMessages();
}
}
});
if (_subsectionTopBar) { if (_subsectionTopBar) {
_subsectionTopBar->searchEnableJumpToDate( _subsectionTopBar->searchEnableJumpToDate(
_openedForum && _searchInChat); _openedForum && _searchInChat);
@ -2653,6 +2714,12 @@ bool Widget::setSearchInChat(Key chat, PeerData *from) {
return true; return true;
} }
bool Widget::setSearchInChat(
Key chat,
PeerData *from) {
return setSearchInChat(chat, from, {});
}
void Widget::clearSearchCache() { void Widget::clearSearchCache() {
_searchCache.clear(); _searchCache.clear();
_singleMessageSearch.clear(); _singleMessageSearch.clear();
@ -2661,6 +2728,7 @@ void Widget::clearSearchCache() {
} }
_searchQuery = QString(); _searchQuery = QString();
_searchQueryFrom = nullptr; _searchQueryFrom = nullptr;
_searchQueryTags.clear();
_topicSearchQuery = QString(); _topicSearchQuery = QString();
_topicSearchOffsetDate = 0; _topicSearchOffsetDate = 0;
_topicSearchOffsetId = _topicSearchOffsetTopicId = 0; _topicSearchOffsetId = _topicSearchOffsetTopicId = 0;
@ -3072,6 +3140,8 @@ void Widget::cancelSearchRequest() {
PeerData *Widget::searchInPeer() const { PeerData *Widget::searchInPeer() const {
return _openedForum return _openedForum
? _openedForum->channel().get() ? _openedForum->channel().get()
: _searchInChat.sublist()
? session().user().get()
: _searchInChat.peer(); : _searchInChat.peer();
} }

View file

@ -22,6 +22,7 @@ class Error;
namespace Data { namespace Data {
class Forum; class Forum;
enum class StorySourcesList : uchar; enum class StorySourcesList : uchar;
struct ReactionId;
} // namespace Data } // namespace Data
namespace Main { namespace Main {
@ -116,7 +117,7 @@ public:
void scrollToEntry(const RowDescriptor &entry); void scrollToEntry(const RowDescriptor &entry);
void searchMessages(const QString &query, Key inChat = {}, UserData *from = nullptr); void searchMessages(QString query, Key inChat = {}, UserData *from = nullptr);
void searchTopics(); void searchTopics();
void searchMore(); void searchMore();
@ -179,7 +180,13 @@ private:
void trackScroll(not_null<Ui::RpWidget*> widget); void trackScroll(not_null<Ui::RpWidget*> widget);
[[nodiscard]] bool searchForPeersRequired(const QString &query) const; [[nodiscard]] bool searchForPeersRequired(const QString &query) const;
[[nodiscard]] bool searchForTopicsRequired(const QString &query) const; [[nodiscard]] bool searchForTopicsRequired(const QString &query) const;
bool setSearchInChat(Key chat, PeerData *from = nullptr); bool setSearchInChat(
Key chat,
PeerData *from,
std::vector<Data::ReactionId> tags);
bool setSearchInChat(
Key chat,
PeerData *from = nullptr);
void showCalendar(); void showCalendar();
void showSearchFrom(); void showSearchFrom();
void showMainMenu(); void showMainMenu();
@ -285,6 +292,8 @@ private:
Dialogs::Key _searchInChat; Dialogs::Key _searchInChat;
History *_searchInMigrated = nullptr; History *_searchInMigrated = nullptr;
PeerData *_searchFromAuthor = nullptr; PeerData *_searchFromAuthor = nullptr;
std::vector<Data::ReactionId> _searchTags;
rpl::lifetime _searchTagsLifetime;
QString _lastFilterText; QString _lastFilterText;
rpl::event_stream<rpl::producer<Stories::Content>> _storiesContents; rpl::event_stream<rpl::producer<Stories::Content>> _storiesContents;
@ -313,6 +322,7 @@ private:
QString _searchQuery; QString _searchQuery;
PeerData *_searchQueryFrom = nullptr; PeerData *_searchQueryFrom = nullptr;
std::vector<Data::ReactionId> _searchQueryTags;
int32 _searchNextRate = 0; int32 _searchNextRate = 0;
bool _searchFull = false; bool _searchFull = false;
bool _searchFullMigrated = false; bool _searchFullMigrated = false;

View file

@ -212,8 +212,8 @@ void Crop::computeDownState(const QPoint &p) {
const auto edge = mouseState(p); const auto edge = mouseState(p);
const auto &inner = _innerRect; const auto &inner = _innerRect;
const auto &crop = _cropPaint; const auto &crop = _cropPaint;
const auto [iLeft, iTop, iRight, iBottom] = RectEdges(inner); const auto &[iLeft, iTop, iRight, iBottom] = RectEdges(inner);
const auto [cLeft, cTop, cRight, cBottom] = RectEdges(crop); const auto &[cLeft, cTop, cRight, cBottom] = RectEdges(crop);
_down = InfoAtDown{ _down = InfoAtDown{
.rect = crop, .rect = crop,
.edge = edge, .edge = edge,

View file

@ -1553,7 +1553,7 @@ void ApiWrap::appendChatsSlice(
continue; continue;
} }
} }
const auto [i, ok] = process.indexByPeer.emplace( const auto &[i, ok] = process.indexByPeer.emplace(
info.peerId, info.peerId,
nextIndex); nextIndex);
if (ok) { if (ok) {
@ -1625,6 +1625,7 @@ void ApiWrap::requestChatMessages(
MTP_string(), // query MTP_string(), // query
MTP_inputPeerSelf(), MTP_inputPeerSelf(),
MTPInputPeer(), // saved_peer_id MTPInputPeer(), // saved_peer_id
MTPVector<MTPReaction>(), // saved_reaction
MTPint(), // top_msg_id MTPint(), // top_msg_id
MTP_inputMessagesFilterEmpty(), MTP_inputMessagesFilterEmpty(),
MTP_int(0), // min_date MTP_int(0), // min_date

View file

@ -1563,7 +1563,7 @@ QByteArray HtmlWriter::Wrap::pushStickerMedia(
const QString &basePath) { const QString &basePath) {
using namespace Data; using namespace Data;
const auto [thumb, size] = WriteImageThumb( const auto &[thumb, size] = WriteImageThumb(
basePath, basePath,
data.file.relativePath, data.file.relativePath,
CalculateThumbSize( CalculateThumbSize(
@ -1730,7 +1730,7 @@ QByteArray HtmlWriter::Wrap::pushPhotoMedia(
const QString &basePath) { const QString &basePath) {
using namespace Data; using namespace Data;
const auto [thumb, size] = WriteImageThumb( const auto &[thumb, size] = WriteImageThumb(
basePath, basePath,
data.image.file.relativePath, data.image.file.relativePath,
CalculateThumbSize( CalculateThumbSize(
@ -2790,7 +2790,7 @@ Result HtmlWriter::writeDialogSlice(const Data::MessagesSlice &data) {
_settings.path, _settings.path,
FormatDateText(date))); FormatDateText(date)));
} }
const auto [info, content] = _chat->pushMessage( const auto &[info, content] = _chat->pushMessage(
message, message,
previous, previous,
_dialog, _dialog,

View file

@ -943,10 +943,10 @@ void InnerWidget::paintEvent(QPaintEvent *e) {
auto clip = e->rect(); auto clip = e->rect();
auto context = _controller->preparePaintContext({ auto context = _controller->preparePaintContext({
.theme = _theme.get(), .theme = _theme.get(),
.visibleAreaTop = _visibleTop,
.visibleAreaTopGlobal = mapToGlobal(QPoint(0, _visibleTop)).y(),
.visibleAreaWidth = width(),
.clip = clip, .clip = clip,
.visibleAreaPositionGlobal = mapToGlobal(QPoint(0, _visibleTop)),
.visibleAreaTop = _visibleTop,
.visibleAreaWidth = width(),
}); });
if (_items.empty() && _upLoaded && _downLoaded) { if (_items.empty() && _upLoaded && _downLoaded) {
paintEmpty(p, context.st); paintEmpty(p, context.st);

View file

@ -470,7 +470,7 @@ not_null<HistoryItem*> History::insertItem(
std::unique_ptr<HistoryItem> item) { std::unique_ptr<HistoryItem> item) {
Expects(item != nullptr); Expects(item != nullptr);
const auto [i, ok] = _messages.insert(std::move(item)); const auto &[i, ok] = _messages.insert(std::move(item));
const auto result = i->get(); const auto result = i->get();
owner().registerMessage(result); owner().registerMessage(result);

View file

@ -984,14 +984,14 @@ void HistoryInner::paintEmpty(
Ui::ChatPaintContext HistoryInner::preparePaintContext( Ui::ChatPaintContext HistoryInner::preparePaintContext(
const QRect &clip) const { const QRect &clip) const {
const auto visibleAreaTopGlobal = mapToGlobal( const auto visibleAreaPositionGlobal = mapToGlobal(
QPoint(0, _visibleAreaTop)).y(); QPoint(0, _visibleAreaTop));
return _controller->preparePaintContext({ return _controller->preparePaintContext({
.theme = _theme.get(), .theme = _theme.get(),
.visibleAreaTop = _visibleAreaTop,
.visibleAreaTopGlobal = visibleAreaTopGlobal,
.visibleAreaWidth = width(),
.clip = clip, .clip = clip,
.visibleAreaPositionGlobal = visibleAreaPositionGlobal,
.visibleAreaTop = _visibleAreaTop,
.visibleAreaWidth = width(),
}); });
} }
@ -1995,7 +1995,7 @@ void HistoryInner::mouseActionFinish(
&& !_selected.empty() && !_selected.empty()
&& _selected.cbegin()->second != FullSelection && _selected.cbegin()->second != FullSelection
&& !hasCopyRestriction(_selected.cbegin()->first)) { && !hasCopyRestriction(_selected.cbegin()->first)) {
const auto [item, selection] = *_selected.cbegin(); const auto &[item, selection] = *_selected.cbegin();
if (const auto view = viewByItem(item)) { if (const auto view = viewByItem(item)) {
TextUtilities::SetClipboardText( TextUtilities::SetClipboardText(
view->selectedText(selection), view->selectedText(selection),
@ -2935,7 +2935,7 @@ TextForMimeData HistoryInner::getSelectedText() const {
return TextForMimeData(); return TextForMimeData();
} }
if (selected.cbegin()->second != FullSelection) { if (selected.cbegin()->second != FullSelection) {
const auto [item, selection] = *selected.cbegin(); const auto &[item, selection] = *selected.cbegin();
if (const auto view = viewByItem(item)) { if (const auto view = viewByItem(item)) {
return view->selectedText(selection); return view->selectedText(selection);
} }

View file

@ -2461,6 +2461,11 @@ const std::vector<Data::MessageReaction> &HistoryItem::reactions() const {
return _reactions ? _reactions->list() : kEmpty; return _reactions ? _reactions->list() : kEmpty;
} }
bool HistoryItem::reactionsAreTags() const {
// Disable reactions as tags for now.
return false;// _flags & MessageFlag::ReactionsAreTags;
}
auto HistoryItem::recentReactions() const auto HistoryItem::recentReactions() const
-> const base::flat_map< -> const base::flat_map<
Data::ReactionId, Data::ReactionId,
@ -3634,31 +3639,40 @@ bool HistoryItem::changeReactions(const MTPMessageReactions *reactions) {
} }
if (!reactions) { if (!reactions) {
_flags &= ~MessageFlag::CanViewReactions; _flags &= ~MessageFlag::CanViewReactions;
if (_history->peer->isSelf()) {
_flags |= MessageFlag::ReactionsAreTags;
}
return (base::take(_reactions) != nullptr); return (base::take(_reactions) != nullptr);
} }
return reactions->match([&](const MTPDmessageReactions &data) { const auto &data = reactions->data();
if (data.is_can_see_list()) { const auto empty = data.vresults().v.isEmpty();
_flags |= MessageFlag::CanViewReactions; if (data.is_reactions_as_tags()
} else { || (empty && _history->peer->isSelf())) {
_flags &= ~MessageFlag::CanViewReactions; _flags |= MessageFlag::ReactionsAreTags;
} else {
_flags &= ~MessageFlag::ReactionsAreTags;
}
if (data.is_can_see_list()) {
_flags |= MessageFlag::CanViewReactions;
} else {
_flags &= ~MessageFlag::CanViewReactions;
}
if (empty) {
return (base::take(_reactions) != nullptr);
} else if (!_reactions) {
_reactions = std::make_unique<Data::MessageReactions>(this);
}
const auto min = data.is_min();
const auto &list = data.vresults().v;
const auto &recent = data.vrecent_reactions().value_or_empty();
if (min && hasUnreadReaction()) {
// We can't update reactions from min if we have unread.
if (_reactions->checkIfChanged(list, recent, min)) {
updateReactionsUnknown();
} }
if (data.vresults().v.isEmpty()) { return false;
return (base::take(_reactions) != nullptr); }
} else if (!_reactions) { return _reactions->change(list, recent, min);
_reactions = std::make_unique<Data::MessageReactions>(this);
}
const auto min = data.is_min();
const auto &list = data.vresults().v;
const auto &recent = data.vrecent_reactions().value_or_empty();
if (min && hasUnreadReaction()) {
// We can't update reactions from min if we have unread.
if (_reactions->checkIfChanged(list, recent, min)) {
updateReactionsUnknown();
}
return false;
}
return _reactions->change(list, recent, min);
});
} }
void HistoryItem::applyTTL(const MTPDmessage &data) { void HistoryItem::applyTTL(const MTPDmessage &data) {

View file

@ -455,6 +455,7 @@ public:
not_null<UserData*> from) const; not_null<UserData*> from) const;
[[nodiscard]] crl::time lastReactionsRefreshTime() const; [[nodiscard]] crl::time lastReactionsRefreshTime() const;
[[nodiscard]] bool reactionsAreTags() const;
[[nodiscard]] bool hasDirectLink() const; [[nodiscard]] bool hasDirectLink() const;
[[nodiscard]] bool changesWallPaper() const; [[nodiscard]] bool changesWallPaper() const;

View file

@ -813,8 +813,3 @@ void ClearMediaAsExpired(not_null<HistoryItem*> item) {
} }
} }
} }
[[nodiscard]] bool IsVoiceOncePlayable(not_null<HistoryItem*> item) {
const auto settings = &AyuSettings::getInstance();
return !item->out() && item->media()->ttlSeconds() && !settings->saveDeletedMessages;
}

View file

@ -158,4 +158,3 @@ ClickHandlerPtr JumpToStoryClickHandler(
void ShowTrialTranscribesToast(int left, TimeId until); void ShowTrialTranscribesToast(int left, TimeId until);
void ClearMediaAsExpired(not_null<HistoryItem*> item); void ClearMediaAsExpired(not_null<HistoryItem*> item);
[[nodiscard]] bool IsVoiceOncePlayable(not_null<HistoryItem*> item);

View file

@ -2614,12 +2614,15 @@ bool HistoryWidget::updateReplaceMediaButton() {
return false; return false;
} }
_replaceMedia.create(this, st::historyReplaceMedia); _replaceMedia.create(this, st::historyReplaceMedia);
const auto hideDuration = st::historyReplaceMedia.ripple.hideDuration;
_replaceMedia->setClickedCallback([=] { _replaceMedia->setClickedCallback([=] {
EditCaptionBox::StartMediaReplace( base::call_delayed(hideDuration, this, [=] {
controller(), EditCaptionBox::StartMediaReplace(
{ _history->peer->id, _editMsgId }, controller(),
_field->getTextWithTags(), { _history->peer->id, _editMsgId },
crl::guard(_list, [=] { cancelEdit(); })); _field->getTextWithTags(),
crl::guard(_list, [=] { cancelEdit(); }));
});
}); });
return true; return true;
} }
@ -6309,7 +6312,8 @@ std::optional<bool> HistoryWidget::cornerButtonsDownShown() {
if (!_list || _firstLoadRequest) { if (!_list || _firstLoadRequest) {
return false; return false;
} }
if (_voiceRecordBar->isLockPresent()) { if (_voiceRecordBar->isLockPresent()
|| _voiceRecordBar->isTTLButtonShown()) {
return false; return false;
} }
if (!_history->loadedAtBottom() || _cornerButtons.replyReturn()) { if (!_history->loadedAtBottom() || _cornerButtons.replyReturn()) {

View file

@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#include "history/view/controls/history_view_compose_controls.h" #include "history/view/controls/history_view_compose_controls.h"
#include "base/call_delayed.h"
#include "base/event_filter.h" #include "base/event_filter.h"
#include "base/platform/base_platform_info.h" #include "base/platform/base_platform_info.h"
#include "base/qt_signal_producer.h" #include "base/qt_signal_producer.h"
@ -2692,12 +2693,15 @@ bool ComposeControls::updateReplaceMediaButton() {
_replaceMedia = std::make_unique<Ui::IconButton>( _replaceMedia = std::make_unique<Ui::IconButton>(
_wrap.get(), _wrap.get(),
st::historyReplaceMedia); st::historyReplaceMedia);
const auto hideDuration = st::historyReplaceMedia.ripple.hideDuration;
_replaceMedia->setClickedCallback([=] { _replaceMedia->setClickedCallback([=] {
EditCaptionBox::StartMediaReplace( base::call_delayed(hideDuration, _wrap.get(), [=] {
_regularWindow, EditCaptionBox::StartMediaReplace(
_editingId, _regularWindow,
_field->getTextWithTags(), _editingId,
crl::guard(_wrap.get(), [=] { cancelEditMessage(); })); _field->getTextWithTags(),
crl::guard(_wrap.get(), [=] { cancelEditMessage(); }));
});
}); });
return true; return true;
} }
@ -2921,6 +2925,10 @@ bool ComposeControls::isLockPresent() const {
return _voiceRecordBar->isLockPresent(); return _voiceRecordBar->isLockPresent();
} }
bool ComposeControls::isTTLButtonShown() const {
return _voiceRecordBar->isTTLButtonShown();
}
rpl::producer<bool> ComposeControls::lockShowStarts() const { rpl::producer<bool> ComposeControls::lockShowStarts() const {
return _voiceRecordBar->lockShowStarts(); return _voiceRecordBar->lockShowStarts();
} }

View file

@ -220,6 +220,7 @@ public:
[[nodiscard]] rpl::producer<bool> lockShowStarts() const; [[nodiscard]] rpl::producer<bool> lockShowStarts() const;
[[nodiscard]] bool isLockPresent() const; [[nodiscard]] bool isLockPresent() const;
[[nodiscard]] bool isTTLButtonShown() const;
[[nodiscard]] bool isRecording() const; [[nodiscard]] bool isRecording() const;
[[nodiscard]] bool isRecordingPressed() const; [[nodiscard]] bool isRecordingPressed() const;
[[nodiscard]] rpl::producer<bool> recordingActiveValue() const; [[nodiscard]] rpl::producer<bool> recordingActiveValue() const;

View file

@ -31,7 +31,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/effects/animation_value.h" #include "ui/effects/animation_value.h"
#include "ui/effects/ripple_animation.h" #include "ui/effects/ripple_animation.h"
#include "ui/text/format_values.h" #include "ui/text/format_values.h"
#include "ui/text/text_utilities.h"
#include "ui/painter.h" #include "ui/painter.h"
#include "ui/widgets/tooltip.h"
#include "ui/rect.h" #include "ui/rect.h"
#include "styles/style_chat.h" #include "styles/style_chat.h"
#include "styles/style_chat_helpers.h" #include "styles/style_chat_helpers.h"
@ -284,7 +286,6 @@ protected:
private: private:
const style::RecordBar &_st; const style::RecordBar &_st;
const QRect _rippleRect; const QRect _rippleRect;
const QString _text;
Ui::Animations::Simple _activeAnimation; Ui::Animations::Simple _activeAnimation;
@ -296,11 +297,11 @@ TTLButton::TTLButton(
: RippleButton(parent, st.lock.ripple) : RippleButton(parent, st.lock.ripple)
, _st(st) , _st(st)
, _rippleRect(Rect(Size(st::historyRecordLockTopShadow.width())) , _rippleRect(Rect(Size(st::historyRecordLockTopShadow.width()))
- (st::historyRecordLockRippleMargin)) - (st::historyRecordLockRippleMargin)) {
, _text(u"1"_q) { QWidget::resize(Size(st::historyRecordLockTopShadow.width()));
resize(Size(st::historyRecordLockTopShadow.width())); Ui::AbstractButton::setDisabled(true);
setClickedCallback([=] { Ui::AbstractButton::setClickedCallback([=] {
Ui::AbstractButton::setDisabled(!Ui::AbstractButton::isDisabled()); Ui::AbstractButton::setDisabled(!Ui::AbstractButton::isDisabled());
const auto isActive = !Ui::AbstractButton::isDisabled(); const auto isActive = !Ui::AbstractButton::isDisabled();
_activeAnimation.start( _activeAnimation.start(
@ -310,6 +311,77 @@ TTLButton::TTLButton(
st::historyRecordVoiceShowDuration); st::historyRecordVoiceShowDuration);
}); });
Ui::RpWidget::shownValue() | rpl::filter(
rpl::mappers::_1
) | rpl::take(1) | rpl::start_with_next([=] {
auto text = rpl::conditional(
Core::App().settings().ttlVoiceClickTooltipHiddenValue(),
tr::lng_record_once_active_tooltip(
Ui::Text::RichLangValue),
tr::lng_record_once_first_tooltip(
Ui::Text::RichLangValue));
const auto tooltip = Ui::CreateChild<Ui::ImportantTooltip>(
parent.get(),
object_ptr<Ui::PaddingWrap<Ui::FlatLabel>>(
parent.get(),
Ui::MakeNiceTooltipLabel(
parent,
std::move(text),
st::historyMessagesTTLLabel.minWidth,
st::ttlMediaImportantTooltipLabel),
st::defaultImportantTooltip.padding),
st::historyRecordTooltip);
Ui::RpWidget::geometryValue(
) | rpl::start_with_next([=](const QRect &r) {
if (r.isEmpty()) {
return;
}
tooltip->pointAt(r, RectPart::Right, [=](QSize size) {
return QPoint(
r.left()
- size.width()
- st::defaultImportantTooltip.padding.left(),
r.top()
+ r.height()
- size.height()
+ st::historyRecordTooltip.padding.top());
});
}, tooltip->lifetime());
tooltip->show();
if (!Core::App().settings().ttlVoiceClickTooltipHidden()) {
clicks(
) | rpl::take(1) | rpl::start_with_next([=] {
Core::App().settings().setTtlVoiceClickTooltipHidden(true);
}, tooltip->lifetime());
tooltip->toggleAnimated(true);
} else {
tooltip->toggleFast(false);
}
clicks(
) | rpl::start_with_next([=] {
const auto toggled = !Ui::AbstractButton::isDisabled();
tooltip->toggleAnimated(toggled);
if (toggled) {
constexpr auto kTimeout = crl::time(3000);
tooltip->hideAfter(kTimeout);
}
}, tooltip->lifetime());
Ui::RpWidget::geometryValue(
) | rpl::map([=](const QRect &r) {
return (r.left() + r.width() > parentWidget()->width());
}) | rpl::distinct_until_changed(
) | rpl::start_with_next([=](bool toHide) {
const auto isFirstTooltip =
!Core::App().settings().ttlVoiceClickTooltipHidden();
if (isFirstTooltip || (!isFirstTooltip && toHide)) {
tooltip->toggleAnimated(!toHide);
}
}, tooltip->lifetime());
}, lifetime());
paintRequest( paintRequest(
) | rpl::start_with_next([=](const QRect &clip) { ) | rpl::start_with_next([=](const QRect &clip) {
auto p = QPainter(this); auto p = QPainter(this);
@ -318,49 +390,16 @@ TTLButton::TTLButton(
Ui::RippleButton::paintRipple(p, _rippleRect.x(), _rippleRect.y()); Ui::RippleButton::paintRipple(p, _rippleRect.x(), _rippleRect.y());
const auto innerRect = QRectF(inner)
- st::historyRecordLockMargin * 2;
auto hq = PainterHighQualityEnabler(p);
p.setFont(st::semiboldFont);
p.setPen(_st.lock.fg);
p.drawText(inner, _text, style::al_center);
const auto penWidth = st::historyRecordTTLLineWidth;
auto pen = QPen(_st.lock.fg);
pen.setJoinStyle(Qt::RoundJoin);
pen.setCapStyle(Qt::RoundCap);
pen.setWidthF(penWidth);
p.setPen(pen);
p.setBrush(Qt::NoBrush);
p.drawArc(innerRect, arc::kQuarterLength, arc::kHalfLength);
{
p.setClipRect(innerRect
- QMarginsF(
innerRect.width() / 2,
-penWidth,
-penWidth,
-penWidth));
pen.setStyle(Qt::DotLine);
p.setPen(pen);
p.drawEllipse(innerRect);
p.setClipping(false);
}
const auto activeProgress = _activeAnimation.value( const auto activeProgress = _activeAnimation.value(
!Ui::AbstractButton::isDisabled() ? 1 : 0); !Ui::AbstractButton::isDisabled() ? 1 : 0);
p.setOpacity(1. - activeProgress);
st::historyRecordVoiceOnceInactive.paintInCenter(p, inner);
if (activeProgress) { if (activeProgress) {
p.setOpacity(activeProgress); p.setOpacity(activeProgress);
pen.setStyle(Qt::SolidLine); st::historyRecordVoiceOnceBg.paintInCenter(p, inner);
pen.setBrush(st::windowBgActive); st::historyRecordVoiceOnceFg.paintInCenter(p, inner);
p.setPen(pen);
p.setBrush(pen.brush());
p.drawEllipse(innerRect);
p.setPen(st::windowFgActive);
p.drawText(innerRect, _text, style::al_center);
} }
}, lifetime()); }, lifetime());
@ -368,7 +407,7 @@ TTLButton::TTLButton(
void TTLButton::clearState() { void TTLButton::clearState() {
Ui::AbstractButton::setDisabled(true); Ui::AbstractButton::setDisabled(true);
update(); QWidget::update();
Ui::RpWidget::hide(); Ui::RpWidget::hide();
} }
@ -1136,7 +1175,6 @@ VoiceRecordBar::VoiceRecordBar(
, _show(std::move(descriptor.show)) , _show(std::move(descriptor.show))
, _send(std::move(descriptor.send)) , _send(std::move(descriptor.send))
, _lock(std::make_unique<RecordLock>(_outerContainer, _st.lock)) , _lock(std::make_unique<RecordLock>(_outerContainer, _st.lock))
, _ttlButton(std::make_unique<TTLButton>(_outerContainer, _st))
, _level(std::make_unique<VoiceRecordButton>(_outerContainer, _st)) , _level(std::make_unique<VoiceRecordButton>(_outerContainer, _st))
, _cancel(std::make_unique<CancelButton>(this, _st, descriptor.recorderHeight)) , _cancel(std::make_unique<CancelButton>(this, _st, descriptor.recorderHeight))
, _startTimer([=] { startRecording(); }) , _startTimer([=] { startRecording(); })
@ -1215,6 +1253,9 @@ void VoiceRecordBar::updateLockGeometry() {
void VoiceRecordBar::updateTTLGeometry( void VoiceRecordBar::updateTTLGeometry(
TTLAnimationType type, TTLAnimationType type,
float64 progress) { float64 progress) {
if (!_ttlButton) {
return;
}
const auto parent = parentWidget(); const auto parent = parentWidget();
const auto me = Ui::MapFrom(_outerContainer, parent, geometry()); const auto me = Ui::MapFrom(_outerContainer, parent, geometry());
const auto anyTop = me.y() - st::historyRecordLockPosition.y(); const auto anyTop = me.y() - st::historyRecordLockPosition.y();
@ -1364,6 +1405,7 @@ void VoiceRecordBar::init() {
} }
updateTTLGeometry(TTLAnimationType::TopBottom, 1. - value); updateTTLGeometry(TTLAnimationType::TopBottom, 1. - value);
}; };
_showListenAnimation.stop();
_showListenAnimation.start(std::move(callback), 0., to, duration); _showListenAnimation.start(std::move(callback), 0., to, duration);
}, lifetime()); }, lifetime());
@ -1373,6 +1415,11 @@ void VoiceRecordBar::init() {
_lock->locks( _lock->locks(
) | rpl::start_with_next([=] { ) | rpl::start_with_next([=] {
if (_hasTTLFilter && _hasTTLFilter()) { if (_hasTTLFilter && _hasTTLFilter()) {
if (!_ttlButton) {
_ttlButton = std::make_unique<TTLButton>(
_outerContainer,
_st);
}
_ttlButton->show(); _ttlButton->show();
} }
updateTTLGeometry(TTLAnimationType::RightTopStatic, 0); updateTTLGeometry(TTLAnimationType::RightTopStatic, 0);
@ -1495,13 +1542,18 @@ void VoiceRecordBar::setTTLFilter(FilterCallback &&callback) {
} }
void VoiceRecordBar::initLockGeometry() { void VoiceRecordBar::initLockGeometry() {
rpl::combine( const auto parent = static_cast<Ui::RpWidget*>(parentWidget());
_lock->heightValue(), rpl::merge(
geometryValue(), _lock->heightValue() | rpl::to_empty,
static_cast<Ui::RpWidget*>(parentWidget())->geometryValue() geometryValue() | rpl::to_empty,
parent->geometryValue() | rpl::to_empty
) | rpl::start_with_next([=] { ) | rpl::start_with_next([=] {
updateLockGeometry(); updateLockGeometry();
}, lifetime()); }, lifetime());
parent->geometryValue(
) | rpl::start_with_next([=] {
updateTTLGeometry(TTLAnimationType::RightLeft, 1.);
}, lifetime());
} }
void VoiceRecordBar::initLevelGeometry() { void VoiceRecordBar::initLevelGeometry() {
@ -1600,10 +1652,12 @@ void VoiceRecordBar::stop(bool send) {
if (isHidden() && !send) { if (isHidden() && !send) {
return; return;
} }
const auto ttlBeforeHide = peekTTLState();
auto disappearanceCallback = [=] { auto disappearanceCallback = [=] {
hide(); hide();
stopRecording(send ? StopType::Send : StopType::Cancel); const auto type = send ? StopType::Send : StopType::Cancel;
stopRecording(type, ttlBeforeHide);
}; };
_lockShowing = false; _lockShowing = false;
visibilityAnimate(false, std::move(disappearanceCallback)); visibilityAnimate(false, std::move(disappearanceCallback));
@ -1621,6 +1675,8 @@ void VoiceRecordBar::finish() {
_listen = nullptr; _listen = nullptr;
[[maybe_unused]] const auto s = takeTTLState();
_sendActionUpdates.fire({ Api::SendProgressType::RecordVoice, -1 }); _sendActionUpdates.fire({ Api::SendProgressType::RecordVoice, -1 });
} }
@ -1631,7 +1687,7 @@ void VoiceRecordBar::hideFast() {
[[maybe_unused]] const auto s = takeTTLState(); [[maybe_unused]] const auto s = takeTTLState();
} }
void VoiceRecordBar::stopRecording(StopType type) { void VoiceRecordBar::stopRecording(StopType type, bool ttlBeforeHide) {
using namespace ::Media::Capture; using namespace ::Media::Capture;
if (type == StopType::Cancel) { if (type == StopType::Cancel) {
instance()->stop(crl::guard(this, [=](Result &&data) { instance()->stop(crl::guard(this, [=](Result &&data) {
@ -1652,9 +1708,9 @@ void VoiceRecordBar::stopRecording(StopType type) {
if (type == StopType::Send) { if (type == StopType::Send) {
const auto options = Api::SendOptions{ const auto options = Api::SendOptions{
.ttlSeconds = takeTTLState() .ttlSeconds = (ttlBeforeHide
? std::numeric_limits<int>::max() ? std::numeric_limits<int>::max()
: 0 : 0),
}; };
auto settings = &AyuSettings::getInstance(); auto settings = &AyuSettings::getInstance();
@ -1861,6 +1917,10 @@ bool VoiceRecordBar::isRecordingByAnotherBar() const {
return !isRecording() && ::Media::Capture::instance()->started(); return !isRecording() && ::Media::Capture::instance()->started();
} }
bool VoiceRecordBar::isTTLButtonShown() const {
return _ttlButton && !_ttlButton->isHidden();
}
bool VoiceRecordBar::hasDuration() const { bool VoiceRecordBar::hasDuration() const {
return _recordingSamples > 0; return _recordingSamples > 0;
} }
@ -1892,7 +1952,14 @@ void VoiceRecordBar::computeAndSetLockProgress(QPoint globalPos) {
_lock->requestPaintProgress(Progress(localPos.y(), higher - lower)); _lock->requestPaintProgress(Progress(localPos.y(), higher - lower));
} }
bool VoiceRecordBar::peekTTLState() const {
return _ttlButton && !_ttlButton->isDisabled();
}
bool VoiceRecordBar::takeTTLState() const { bool VoiceRecordBar::takeTTLState() const {
if (!_ttlButton) {
return false;
}
const auto hasTtl = !_ttlButton->isDisabled(); const auto hasTtl = !_ttlButton->isDisabled();
_ttlButton->clearState(); _ttlButton->clearState();
return hasTtl; return hasTtl;

View file

@ -98,6 +98,7 @@ public:
[[nodiscard]] bool isListenState() const; [[nodiscard]] bool isListenState() const;
[[nodiscard]] bool isActive() const; [[nodiscard]] bool isActive() const;
[[nodiscard]] bool isRecordingByAnotherBar() const; [[nodiscard]] bool isRecordingByAnotherBar() const;
[[nodiscard]] bool isTTLButtonShown() const;
private: private:
enum class StopType { enum class StopType {
@ -125,7 +126,7 @@ private:
[[nodiscard]] bool recordingAnimationCallback(crl::time now); [[nodiscard]] bool recordingAnimationCallback(crl::time now);
void stop(bool send); void stop(bool send);
void stopRecording(StopType type); void stopRecording(StopType type, bool ttlBeforeHide = false);
void visibilityAnimate(bool show, Fn<void()> &&callback); void visibilityAnimate(bool show, Fn<void()> &&callback);
[[nodiscard]] bool showRecordButton() const; [[nodiscard]] bool showRecordButton() const;
@ -148,6 +149,7 @@ private:
void computeAndSetLockProgress(QPoint globalPos); void computeAndSetLockProgress(QPoint globalPos);
[[nodiscard]] bool peekTTLState() const;
[[nodiscard]] bool takeTTLState() const; [[nodiscard]] bool takeTTLState() const;
const style::RecordBar &_st; const style::RecordBar &_st;
@ -155,9 +157,9 @@ private:
const std::shared_ptr<ChatHelpers::Show> _show; const std::shared_ptr<ChatHelpers::Show> _show;
const std::shared_ptr<Ui::SendButton> _send; const std::shared_ptr<Ui::SendButton> _send;
const std::unique_ptr<RecordLock> _lock; const std::unique_ptr<RecordLock> _lock;
const std::unique_ptr<Ui::AbstractButton> _ttlButton;
const std::unique_ptr<VoiceRecordButton> _level; const std::unique_ptr<VoiceRecordButton> _level;
const std::unique_ptr<CancelButton> _cancel; const std::unique_ptr<CancelButton> _cancel;
std::unique_ptr<Ui::AbstractButton> _ttlButton;
std::unique_ptr<ListenWrap> _listen; std::unique_ptr<ListenWrap> _listen;
base::Timer _startTimer; base::Timer _startTimer;

View file

@ -26,6 +26,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/media/history_view_web_page.h" #include "history/view/media/history_view_web_page.h"
#include "history/view/reactions/history_view_reactions_list.h" #include "history/view/reactions/history_view_reactions_list.h"
#include "ui/widgets/popup_menu.h" #include "ui/widgets/popup_menu.h"
#include "ui/widgets/menu/menu_action.h"
#include "ui/widgets/menu/menu_common.h"
#include "ui/widgets/menu/menu_multiline_action.h" #include "ui/widgets/menu/menu_multiline_action.h"
#include "ui/image/image.h" #include "ui/image/image.h"
#include "ui/toast/toast.h" #include "ui/toast/toast.h"
@ -1318,6 +1320,62 @@ void AddWhoReactedAction(
showAllChosen)); showAllChosen));
} }
void ShowTagMenu(
not_null<base::unique_qptr<Ui::PopupMenu>*> menu,
QPoint position,
not_null<QWidget*> context,
not_null<HistoryItem*> item,
const Data::ReactionId &id,
not_null<Window::SessionController*> controller) {
using namespace Data;
const auto itemId = item->fullId();
const auto owner = &controller->session().data();
*menu = base::make_unique_q<Ui::PopupMenu>(
context,
st::popupMenuExpandedSeparator);
(*menu)->addAction(tr::lng_context_filter_by_tag(tr::now), [=] {
HashtagClickHandler(SearchTagToQuery(id)).onClick({
.button = Qt::LeftButton,
.other = QVariant::fromValue(ClickHandlerContext{
.sessionWindow = controller,
}),
});
}, &st::menuIconFave);
const auto removeTag = [=] {
if (const auto item = owner->message(itemId)) {
const auto &list = item->reactions();
if (ranges::contains(list, id, &MessageReaction::id)) {
item->toggleReaction(
id,
HistoryItem::ReactionSource::Quick);
}
}
};
(*menu)->addAction(base::make_unique_q<Ui::Menu::Action>(
(*menu)->menu(),
st::menuWithIconsAttention,
Ui::Menu::CreateAction(
(*menu)->menu(),
tr::lng_context_remove_tag(tr::now),
removeTag),
&st::menuIconDisableAttention,
&st::menuIconDisableAttention));
if (const auto custom = id.custom()) {
if (const auto set = owner->document(custom)->sticker()) {
if (set->set.id) {
AddEmojiPacksAction(
menu->get(),
{ set->set },
EmojiPacksSource::Reaction,
controller);
}
}
}
(*menu)->popup(position);
}
void ShowWhoReactedMenu( void ShowWhoReactedMenu(
not_null<base::unique_qptr<Ui::PopupMenu>*> menu, not_null<base::unique_qptr<Ui::PopupMenu>*> menu,
QPoint position, QPoint position,
@ -1326,6 +1384,11 @@ void ShowWhoReactedMenu(
const Data::ReactionId &id, const Data::ReactionId &id,
not_null<Window::SessionController*> controller, not_null<Window::SessionController*> controller,
rpl::lifetime &lifetime) { rpl::lifetime &lifetime) {
if (item->reactionsAreTags()) {
ShowTagMenu(menu, position, context, item, id, controller);
return;
}
struct State { struct State {
int addedToBottom = 0; int addedToBottom = 0;
}; };

View file

@ -58,6 +58,7 @@ enum class Context : char {
AdminLog, AdminLog,
ContactPreview, ContactPreview,
SavedSublist, SavedSublist,
TTLViewer,
}; };
enum class OnlyEmojiAndSpaces : char { enum class OnlyEmojiAndSpaces : char {

View file

@ -901,7 +901,7 @@ not_null<Element*> ListWidget::enforceViewForItem(
return j->second.get(); return j->second.get();
} }
} }
const auto [i, ok] = _views.emplace( const auto &[i, ok] = _views.emplace(
item, item,
item->createView(this)); item->createView(this));
return i->second.get(); return i->second.get();
@ -1094,7 +1094,7 @@ void ListWidget::repaintScrollDateCallback() {
auto ListWidget::collectSelectedItems() const -> SelectedItems { auto ListWidget::collectSelectedItems() const -> SelectedItems {
auto transformation = [&](const auto &item) { auto transformation = [&](const auto &item) {
const auto [itemId, selection] = item; const auto &[itemId, selection] = item;
auto result = SelectedItem(itemId); auto result = SelectedItem(itemId);
result.canDelete = selection.canDelete; result.canDelete = selection.canDelete;
result.canForward = selection.canForward; result.canForward = selection.canForward;
@ -2009,10 +2009,10 @@ Ui::ChatPaintContext ListWidget::preparePaintContext(
const QRect &clip) const { const QRect &clip) const {
return controller()->preparePaintContext({ return controller()->preparePaintContext({
.theme = _delegate->listChatTheme(), .theme = _delegate->listChatTheme(),
.visibleAreaTop = _visibleTop,
.visibleAreaTopGlobal = mapToGlobal(QPoint(0, _visibleTop)).y(),
.visibleAreaWidth = width(),
.clip = clip, .clip = clip,
.visibleAreaPositionGlobal = mapToGlobal(QPoint(0, _visibleTop)),
.visibleAreaTop = _visibleTop,
.visibleAreaWidth = width(),
}); });
} }
@ -3766,7 +3766,7 @@ void ListWidget::refreshItem(not_null<const Element*> view) {
} }
return nullptr; return nullptr;
}(); }();
const auto [i, ok] = _views.emplace( const auto &[i, ok] = _views.emplace(
item, item,
item->createView(this, was.get())); item->createView(this, was.get()));
const auto now = i->second.get(); const auto now = i->second.get();

View file

@ -1984,6 +1984,7 @@ bool Message::hasFromPhoto() const {
case Context::AdminLog: case Context::AdminLog:
return true; return true;
case Context::History: case Context::History:
case Context::TTLViewer:
case Context::Pinned: case Context::Pinned:
case Context::Replies: case Context::Replies:
case Context::SavedSublist: { case Context::SavedSublist: {
@ -2917,8 +2918,12 @@ bool Message::isSignedAuthorElided() const {
bool Message::embedReactionsInBottomInfo() const { bool Message::embedReactionsInBottomInfo() const {
const auto item = data(); const auto item = data();
const auto user = item->history()->peer->asUser(); const auto user = item->history()->peer->asUser();
if (!user || user->isPremium() || user->session().premium()) { if (!user
|| user->isPremium()
|| user->isSelf()
|| user->session().premium()) {
// Only in messages of a non premium user with a non premium user. // Only in messages of a non premium user with a non premium user.
// In saved messages we use reactions for tags, we don't embed them.
return false; return false;
} }
auto seenMy = false; auto seenMy = false;
@ -2961,8 +2966,14 @@ void Message::refreshReactions() {
if (!_reactions) { if (!_reactions) {
const auto handlerFactory = [=](ReactionId id) { const auto handlerFactory = [=](ReactionId id) {
const auto weak = base::make_weak(this); const auto weak = base::make_weak(this);
return std::make_shared<LambdaClickHandler>([=] { return std::make_shared<LambdaClickHandler>([=](
ClickContext context) {
if (const auto strong = weak.get()) { if (const auto strong = weak.get()) {
if (strong->data()->reactionsAreTags()) {
const auto tag = Data::SearchTagToQuery(id);
HashtagClickHandler(tag).onClick(context);
return;
}
strong->data()->toggleReaction( strong->data()->toggleReaction(
id, id,
HistoryItem::ReactionSource::Existing); HistoryItem::ReactionSource::Existing);
@ -3160,6 +3171,7 @@ bool Message::hasFromName() const {
case Context::AdminLog: case Context::AdminLog:
return true; return true;
case Context::History: case Context::History:
case Context::TTLViewer:
case Context::Pinned: case Context::Pinned:
case Context::Replies: case Context::Replies:
case Context::SavedSublist: { case Context::SavedSublist: {
@ -3192,7 +3204,7 @@ bool Message::hasFromName() const {
case Context::ContactPreview: case Context::ContactPreview:
return false; return false;
} }
Unexpected("Context in Message::hasFromPhoto."); Unexpected("Context in Message::hasFromName.");
} }
bool Message::displayFromName() const { bool Message::displayFromName() const {

View file

@ -1838,7 +1838,8 @@ bool RepliesWidget::cornerButtonsIgnoreVisibility() {
} }
std::optional<bool> RepliesWidget::cornerButtonsDownShown() { std::optional<bool> RepliesWidget::cornerButtonsDownShown() {
if (_composeControls->isLockPresent()) { if (_composeControls->isLockPresent()
|| _composeControls->isTTLButtonShown()) {
return false; return false;
} }
const auto top = _scroll->scrollTop() + st::historyToDownShownAfter; const auto top = _scroll->scrollTop() + st::historyToDownShownAfter;
@ -1851,7 +1852,9 @@ std::optional<bool> RepliesWidget::cornerButtonsDownShown() {
} }
bool RepliesWidget::cornerButtonsUnreadMayBeShown() { bool RepliesWidget::cornerButtonsUnreadMayBeShown() {
return _loaded && !_composeControls->isLockPresent(); return _loaded
&& !_composeControls->isLockPresent()
&& !_composeControls->isTTLButtonShown();
} }
bool RepliesWidget::cornerButtonsHas(CornerButtonType type) { bool RepliesWidget::cornerButtonsHas(CornerButtonType type) {

View file

@ -843,7 +843,8 @@ bool ScheduledWidget::cornerButtonsIgnoreVisibility() {
} }
std::optional<bool> ScheduledWidget::cornerButtonsDownShown() { std::optional<bool> ScheduledWidget::cornerButtonsDownShown() {
if (_composeControls->isLockPresent()) { if (_composeControls->isLockPresent()
|| _composeControls->isTTLButtonShown()) {
return false; return false;
} }
const auto top = _scroll->scrollTop() + st::historyToDownShownAfter; const auto top = _scroll->scrollTop() + st::historyToDownShownAfter;
@ -857,7 +858,8 @@ std::optional<bool> ScheduledWidget::cornerButtonsDownShown() {
bool ScheduledWidget::cornerButtonsUnreadMayBeShown() { bool ScheduledWidget::cornerButtonsUnreadMayBeShown() {
return _inner->loadedAtBottomKnown() return _inner->loadedAtBottomKnown()
&& !_composeControls->isLockPresent(); && !_composeControls->isLockPresent()
&& !_composeControls->isTTLButtonShown();
} }
bool ScheduledWidget::cornerButtonsHas(CornerButtonType type) { bool ScheduledWidget::cornerButtonsHas(CornerButtonType type) {

View file

@ -8,6 +8,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/history_view_sublist_section.h" #include "history/view/history_view_sublist_section.h"
#include "main/main_session.h" #include "main/main_session.h"
#include "core/application.h"
#include "core/shortcuts.h"
#include "data/data_saved_messages.h" #include "data/data_saved_messages.h"
#include "data/data_saved_sublist.h" #include "data/data_saved_sublist.h"
#include "data/data_session.h" #include "data/data_session.h"
@ -19,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history.h" #include "history/history.h"
#include "history/history_item.h" #include "history/history_item.h"
#include "lang/lang_keys.h" #include "lang/lang_keys.h"
#include "mainwidget.h"
#include "ui/chat/chat_style.h" #include "ui/chat/chat_style.h"
#include "ui/widgets/buttons.h" #include "ui/widgets/buttons.h"
#include "ui/widgets/scroll_area.h" #include "ui/widgets/scroll_area.h"
@ -115,6 +118,10 @@ SublistWidget::SublistWidget(
) | rpl::start_with_next([=] { ) | rpl::start_with_next([=] {
clearSelected(); clearSelected();
}, _topBar->lifetime()); }, _topBar->lifetime());
_topBar->searchRequest(
) | rpl::start_with_next([=] {
searchInSublist();
}, _topBar->lifetime());
_translateBar->raise(); _translateBar->raise();
_topBarShadow->raise(); _topBarShadow->raise();
@ -134,6 +141,7 @@ SublistWidget::SublistWidget(
onScroll(); onScroll();
}, lifetime()); }, lifetime());
setupShortcuts();
setupTranslateBar(); setupTranslateBar();
} }
@ -658,4 +666,24 @@ void SublistWidget::clearSelected() {
_inner->cancelSelection(); _inner->cancelSelection();
} }
void SublistWidget::setupShortcuts() {
Shortcuts::Requests(
) | rpl::filter([=] {
return Ui::AppInFocus()
&& Ui::InFocusChain(this)
&& !controller()->isLayerShown()
&& (Core::App().activeWindow() == &controller()->window());
}) | rpl::start_with_next([=](not_null<Shortcuts::Request*> request) {
using Command = Shortcuts::Command;
request->check(Command::Search, 1) && request->handle([=] {
searchInSublist();
return true;
});
}, lifetime());
}
void SublistWidget::searchInSublist() {
controller()->content()->searchInChat(_sublist);
}
} // namespace HistoryView } // namespace HistoryView

View file

@ -167,11 +167,13 @@ private:
void setupOpenChatButton(); void setupOpenChatButton();
void setupAboutHiddenAuthor(); void setupAboutHiddenAuthor();
void setupTranslateBar(); void setupTranslateBar();
void setupShortcuts();
void confirmDeleteSelected(); void confirmDeleteSelected();
void confirmForwardSelected(); void confirmForwardSelected();
void clearSelected(); void clearSelected();
void recountChatWidth(); void recountChatWidth();
void searchInSublist();
const not_null<Data::SavedSublist*> _sublist; const not_null<Data::SavedSublist*> _sublist;
const not_null<History*> _history; const not_null<History*> _history;

View file

@ -55,6 +55,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_send_action.h" #include "data/data_send_action.h"
#include "chat_helpers/emoji_interactions.h" #include "chat_helpers/emoji_interactions.h"
#include "base/unixtime.h" #include "base/unixtime.h"
#include "base/event_filter.h"
#include "support/support_helper.h" #include "support/support_helper.h"
#include "apiwrap.h" #include "apiwrap.h"
#include "api/api_chat_participants.h" #include "api/api_chat_participants.h"
@ -64,6 +65,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/style_info.h" #include "styles/style_info.h"
#include "styles/style_menu_icons.h" #include "styles/style_menu_icons.h"
#include <QtGui/QWindow>
// AyuGram includes // AyuGram includes
#include "ayu/ayu_settings.h" #include "ayu/ayu_settings.h"
#include "data/data_chat_filters.h" #include "data/data_chat_filters.h"
@ -235,6 +238,16 @@ TopBarWidget::TopBarWidget(
updateConnectingState(); updateConnectingState();
}, lifetime()); }, lifetime());
base::install_event_filter(
this,
window()->windowHandle(),
[=](not_null<QEvent*> e) {
if (e->type() == QEvent::Expose) {
updateConnectingState();
}
return base::EventFilterResult::Continue;
});
setCursor(style::cur_pointer); setCursor(style::cur_pointer);
} }
@ -246,7 +259,8 @@ Main::Session &TopBarWidget::session() const {
void TopBarWidget::updateConnectingState() { void TopBarWidget::updateConnectingState() {
const auto state = _controller->session().mtp().dcstate(); const auto state = _controller->session().mtp().dcstate();
if (state == MTP::ConnectedState) { const auto exposed = window()->windowHandle()->isExposed();
if (state == MTP::ConnectedState || !exposed) {
if (_connecting) { if (_connecting) {
_connecting = nullptr; _connecting = nullptr;
update(); update();
@ -926,7 +940,9 @@ int TopBarWidget::countSelectedButtonsTop(float64 selectedShown) {
void TopBarWidget::updateSearchVisibility() { void TopBarWidget::updateSearchVisibility() {
const auto searchAllowedMode = (_activeChat.section == Section::History) const auto searchAllowedMode = (_activeChat.section == Section::History)
|| (_activeChat.section == Section::Replies || (_activeChat.section == Section::Replies
&& _activeChat.key.topic()); && _activeChat.key.topic())
|| (_activeChat.section == Section::SavedSublist
&& _activeChat.key.sublist());
_search->setVisible(searchAllowedMode && !_chooseForReportReason); _search->setVisible(searchAllowedMode && !_chooseForReportReason);
} }

View file

@ -45,52 +45,18 @@ namespace {
constexpr auto kAudioVoiceMsgUpdateView = crl::time(100); constexpr auto kAudioVoiceMsgUpdateView = crl::time(100);
void DrawCornerBadgeTTL( [[nodiscard]] QRect TTLRectFromInner(const QRect &inner) {
QPainter &p, return QRect(
const style::color &bg, rect::right(inner)
const style::color &fg,
const QRect &circleRect) {
p.save();
const auto partRect = QRectF(
rect::right(circleRect)
- st::dialogsTTLBadgeSize - st::dialogsTTLBadgeSize
+ rect::m::sum::h(st::dialogsTTLBadgeInnerMargins), + rect::m::sum::h(st::dialogsTTLBadgeInnerMargins)
rect::bottom(circleRect) - st::dialogsTTLBadgeSkip.x(),
rect::bottom(inner)
- st::dialogsTTLBadgeSize - st::dialogsTTLBadgeSize
+ rect::m::sum::v(st::dialogsTTLBadgeInnerMargins), + rect::m::sum::v(st::dialogsTTLBadgeInnerMargins)
- st::dialogsTTLBadgeSkip.y(),
st::dialogsTTLBadgeSize, st::dialogsTTLBadgeSize,
st::dialogsTTLBadgeSize); st::dialogsTTLBadgeSize);
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(bg);
p.drawEllipse(partRect);
const auto innerRect = partRect - st::dialogsTTLBadgeInnerMargins;
const auto ttlText = u"1"_q;
p.setFont(st::dialogsScamFont);
p.setPen(fg);
p.drawText(innerRect, ttlText, style::al_center);
constexpr auto kPenWidth = 1.5;
const auto penWidth = style::ConvertScaleExact(kPenWidth);
auto pen = QPen(fg);
pen.setJoinStyle(Qt::RoundJoin);
pen.setCapStyle(Qt::RoundCap);
pen.setWidthF(penWidth);
p.setPen(pen);
p.setBrush(Qt::NoBrush);
p.drawArc(innerRect, arc::kQuarterLength, arc::kHalfLength);
p.setClipRect(innerRect
- QMarginsF(innerRect.width() / 2, -penWidth, -penWidth, -penWidth));
pen.setStyle(Qt::DotLine);
p.setPen(pen);
p.drawEllipse(innerRect);
p.restore();
} }
[[nodiscard]] HistoryView::TtlPaintCallback CreateTtlPaintCallback( [[nodiscard]] HistoryView::TtlPaintCallback CreateTtlPaintCallback(
@ -99,43 +65,54 @@ void DrawCornerBadgeTTL(
struct State final { struct State final {
std::unique_ptr<Lottie::Icon> start; std::unique_ptr<Lottie::Icon> start;
std::unique_ptr<Lottie::Icon> idle; std::unique_ptr<Lottie::Icon> idle;
bool started = false;
}; };
const auto iconSize = Size(std::min( const auto iconSize = Size(std::min(
st::historyFileInPause.width(), st::historyFileInPause.width(),
st::historyFileInPause.height())); st::historyFileInPause.height()));
const auto state = lifetime->make_state<State>(); const auto state = lifetime->make_state<State>();
state->start = Lottie::MakeIcon({ //state->start = Lottie::MakeIcon({
.name = u"voice_ttl_start"_q, // .name = u"voice_ttl_start"_q,
// .color = &st::historyFileInIconFg,
// .sizeOverride = iconSize,
//});
state->idle = Lottie::MakeIcon({
.name = u"voice_ttl_idle"_q,
.color = &st::historyFileInIconFg, .color = &st::historyFileInIconFg,
.sizeOverride = iconSize, .sizeOverride = iconSize,
}); });
const auto animateSingle = [=]( const auto weak = std::weak_ptr(lifetime);
not_null<Lottie::Icon*> icon,
Fn<void()> next) {
auto callback = [=] {
update();
if (icon->frameIndex() == icon->framesCount()) {
next();
}
};
icon->animate(std::move(callback), 0, icon->framesCount());
};
const auto animate = [=](auto reanimate) -> void {
animateSingle(state->idle.get(), [=] { reanimate(reanimate); });
};
animateSingle(
state->start.get(),
[=] {
state->idle = Lottie::MakeIcon({
.name = u"voice_ttl_idle"_q,
.color = &st::historyFileInIconFg,
.sizeOverride = iconSize,
});
animate(animate);
});
return [=](QPainter &p, QRect r, QColor c) { return [=](QPainter &p, QRect r, QColor c) {
(state->idle ? state->idle : state->start)->paintInCenter(p, r, c); if (weak.expired()) {
return;
}
{
const auto &icon = state->idle;
if (icon) {
icon->paintInCenter(p, r, c);
if (!icon->animating()) {
icon->animate(update, 0, icon->framesCount());
}
return;
}
}
{
const auto &icon = state->start;
icon->paintInCenter(p, r, c);
if (!icon->animating()) {
if (!state->started) {
icon->animate(update, 0, icon->framesCount());
state->started = true;
} else {
state->idle = Lottie::MakeIcon({
.name = u"voice_ttl_idle"_q,
.color = &st::historyFileInIconFg,
.sizeOverride = iconSize,
});
}
}
}
}; };
} }
@ -214,7 +191,8 @@ void PaintWaveform(
const PaintContext &context, const PaintContext &context,
const VoiceData *voiceData, const VoiceData *voiceData,
int availableWidth, int availableWidth,
float64 progress) { float64 progress,
bool ttl) {
const auto wf = [&]() -> const VoiceWaveform* { const auto wf = [&]() -> const VoiceWaveform* {
if (!voiceData) { if (!voiceData) {
return nullptr; return nullptr;
@ -226,11 +204,14 @@ void PaintWaveform(
} }
return &voiceData->waveform; return &voiceData->waveform;
}(); }();
if (ttl) {
progress = 1. - progress;
}
const auto stm = context.messageStyle(); const auto stm = context.messageStyle();
// Rescale waveform by going in waveform.size * bar_count 1D grid. // Rescale waveform by going in waveform.size * bar_count 1D grid.
const auto active = stm->msgWaveformActive; const auto active = stm->msgWaveformActive;
const auto inactive = stm->msgWaveformInactive; const auto inactive = ttl ? stm->msgBg : stm->msgWaveformInactive;
const auto wfSize = wf const auto wfSize = wf
? int(wf->size()) ? int(wf->size())
: ::Media::Player::kWaveformSamplesCount; : ::Media::Player::kWaveformSamplesCount;
@ -267,10 +248,12 @@ void PaintWaveform(
p.fillRect( p.fillRect(
QRectF(barLeft, barTop, leftWidth, barHeight), QRectF(barLeft, barTop, leftWidth, barHeight),
active); active);
p.fillRect( if (!ttl) {
QRectF(activeWidth, barTop, rightWidth, barHeight), p.fillRect(
inactive); QRectF(activeWidth, barTop, rightWidth, barHeight),
} else { inactive);
}
} else if (!ttl || barLeft < activeWidth) {
const auto &color = (barLeft >= activeWidth) ? inactive : active; const auto &color = (barLeft >= activeWidth) ? inactive : active;
p.fillRect(QRectF(barLeft, barTop, barWidth, barHeight), color); p.fillRect(QRectF(barLeft, barTop, barWidth, barHeight), color);
} }
@ -326,41 +309,38 @@ Document::Document(
} }
if ((_data->isVoiceMessage() || isRound) if ((_data->isVoiceMessage() || isRound)
&& IsVoiceOncePlayable(_parent->data())) { && _parent->data()->media()->ttlSeconds()) {
_parent->data()->removeFromSharedMediaIndex(); const auto fullId = _realParent->fullId();
setDocumentLinks(_data, realParent, [=] { if (_parent->delegate()->elementContext() == Context::TTLViewer) {
_openl = nullptr;
auto lifetime = std::make_shared<rpl::lifetime>(); auto lifetime = std::make_shared<rpl::lifetime>();
rpl::merge( TTLVoiceStops(fullId) | rpl::start_with_next([=]() mutable {
::Media::Player::instance()->updatedNotifier(
) | rpl::filter([=](::Media::Player::TrackState state) {
using State = ::Media::Player::State;
const auto badState = state.state == State::Stopped
|| state.state == State::StoppedAtEnd
|| state.state == State::StoppedAtError
|| state.state == State::StoppedAtStart;
return (state.id.contextId() != _realParent->fullId())
&& !badState;
}) | rpl::to_empty,
::Media::Player::instance()->tracksFinished(
) | rpl::filter([=](AudioMsgId::Type type) {
return (type == AudioMsgId::Type::Voice);
}) | rpl::to_empty,
::Media::Player::instance()->stops(AudioMsgId::Type::Voice)
) | rpl::start_with_next([=]() mutable {
_drawTtl = nullptr;
const auto item = _parent->data();
if (lifetime) { if (lifetime) {
base::take(lifetime)->destroy(); base::take(lifetime)->destroy();
} }
// Destroys this.
ClearMediaAsExpired(item);
}, *lifetime); }, *lifetime);
_drawTtl = CreateTtlPaintCallback(lifetime, [=] { repaint(); }); _drawTtl = CreateTtlPaintCallback(lifetime, [=] { repaint(); });
} else if (!_parent->data()->out()) {
const auto &data = &_parent->data()->history()->owner();
_parent->data()->removeFromSharedMediaIndex();
setDocumentLinks(_data, realParent, [=] {
_openl = nullptr;
return false; auto lifetime = std::make_shared<rpl::lifetime>();
}); TTLVoiceStops(fullId) | rpl::start_with_next([=]() mutable {
if (lifetime) {
base::take(lifetime)->destroy();
}
if (const auto item = data->message(fullId)) {
// Destroys this.
ClearMediaAsExpired(item);
}
}, *lifetime);
return false;
});
} else {
setDocumentLinks(_data, realParent);
}
} else { } else {
setDocumentLinks(_data, realParent); setDocumentLinks(_data, realParent);
} }
@ -427,7 +407,7 @@ void Document::createComponents(bool caption) {
_realParent->fullId()); _realParent->fullId());
} }
if (const auto voice = Get<HistoryDocumentVoice>()) { if (const auto voice = Get<HistoryDocumentVoice>()) {
voice->seekl = !IsVoiceOncePlayable(_parent->data()) voice->seekl = !_parent->data()->media()->ttlSeconds()
? std::make_shared<VoiceSeekClickHandler>(_data, [](FullMsgId) {}) ? std::make_shared<VoiceSeekClickHandler>(_data, [](FullMsgId) {})
: nullptr; : nullptr;
if (_transcribedRound) { if (_transcribedRound) {
@ -715,6 +695,11 @@ void Document::draw(
} else { } else {
p.setPen(Qt::NoPen); p.setPen(Qt::NoPen);
const auto hasTtlBadge = _parent->data()->media()
&& _parent->data()->media()->ttlSeconds()
&& _openl;
const auto ttlRect = hasTtlBadge ? TTLRectFromInner(inner) : QRect();
const auto coverDrawn = _data->isSongWithCover() const auto coverDrawn = _data->isSongWithCover()
&& DrawThumbnailAsSongCover( && DrawThumbnailAsSongCover(
p, p,
@ -739,9 +724,12 @@ void Document::draw(
} }
} }
} else { } else {
PainterHighQualityEnabler hq(p); auto hq = PainterHighQualityEnabler(p);
p.setBrush(stm->msgFileBg); p.setBrush(stm->msgFileBg);
p.drawEllipse(inner); p.drawEllipse(inner);
if (hasTtlBadge) {
p.drawEllipse(ttlRect);
}
} }
} }
@ -778,8 +766,28 @@ void Document::draw(
: nullptr; : nullptr;
const auto paintContent = [&](QPainter &q) { const auto paintContent = [&](QPainter &q) {
constexpr auto kPenWidth = 1.5;
if (_drawTtl) { if (_drawTtl) {
_drawTtl(q, inner, context.st->historyFileInIconFg()->c); _drawTtl(q, inner, context.st->historyFileInIconFg()->c);
const auto voice = Get<HistoryDocumentVoice>();
const auto progress = (voice && voice->playback)
? voice->playback->progress.current()
: 0.;
if (progress > 0.) {
auto pen = stm->msgBg->p;
pen.setWidthF(style::ConvertScaleExact(kPenWidth));
pen.setCapStyle(Qt::RoundCap);
q.setPen(pen);
const auto from = arc::kQuarterLength;
const auto len = std::round(arc::kFullLength
* (1. - progress));
const auto stepInside = pen.widthF() * 2;
auto hq = PainterHighQualityEnabler(q);
q.drawArc(inner - Margins(stepInside), from, len);
}
} else if (previous && radialOpacity > 0. && radialOpacity < 1.) { } else if (previous && radialOpacity > 0. && radialOpacity < 1.) {
PaintInterpolatedIcon(q, icon, *previous, radialOpacity, inner); PaintInterpolatedIcon(q, icon, *previous, radialOpacity, inner);
} else { } else {
@ -790,6 +798,17 @@ void Document::draw(
QRect rinner(inner.marginsRemoved(QMargins(st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine))); QRect rinner(inner.marginsRemoved(QMargins(st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine)));
_animation->radial.draw(q, rinner, st::msgFileRadialLine, stm->historyFileRadialFg); _animation->radial.draw(q, rinner, st::msgFileRadialLine, stm->historyFileRadialFg);
} }
if (hasTtlBadge) {
{
auto hq = PainterHighQualityEnabler(q);
auto pen = stm->msgBg->p;
pen.setWidthF(style::ConvertScaleExact(kPenWidth));
q.setPen(pen);
q.setBrush(Qt::NoBrush);
q.drawEllipse(ttlRect);
}
stm->historyVoiceMessageTTL.paintInCenter(q, ttlRect);
}
}; };
if (_data->isSongWithCover() || !usesBubblePattern(context)) { if (_data->isSongWithCover() || !usesBubblePattern(context)) {
paintContent(p); paintContent(p);
@ -798,7 +817,7 @@ void Document::draw(
p, p,
context.viewport, context.viewport,
context.bubblesPattern->pixmap, context.bubblesPattern->pixmap,
inner, hasTtlBadge ? inner.united(ttlRect) : inner,
paintContent, paintContent,
_iconCache); _iconCache);
} }
@ -855,11 +874,14 @@ void Document::draw(
if (_transcribedRound) { if (_transcribedRound) {
FillWaveform(_data->round()); FillWaveform(_data->round());
} }
const auto inTTLViewer = _parent->delegate()->elementContext()
== Context::TTLViewer;
PaintWaveform(p, PaintWaveform(p,
context, context,
_transcribedRound ? _data->round() : _data->voice(), _transcribedRound ? _data->round() : _data->voice(),
namewidth + st::msgWaveformSkip, namewidth + st::msgWaveformSkip,
progress); progress,
inTTLViewer);
p.restore(); p.restore();
} else if (auto named = Get<HistoryDocumentNamed>()) { } else if (auto named = Get<HistoryDocumentNamed>()) {
p.setFont(st::semiboldFont); p.setFont(st::semiboldFont);
@ -918,12 +940,6 @@ void Document::draw(
.highlight = highlightRequest ? &*highlightRequest : nullptr, .highlight = highlightRequest ? &*highlightRequest : nullptr,
}); });
} }
if (_parent->data()->media() && _parent->data()->media()->ttlSeconds()) {
const auto &fg = context.outbg
? st::historyFileOutIconFg
: st::historyFileInIconFg;
DrawCornerBadgeTTL(p, stm->msgFileBg, fg, inner);
}
} }
Ui::BubbleRounding Document::thumbRounding( Ui::BubbleRounding Document::thumbRounding(
@ -1259,18 +1275,22 @@ TextState Document::textState(
void Document::updatePressed(QPoint point) { void Document::updatePressed(QPoint point) {
// LayoutMode should be passed here. // LayoutMode should be passed here.
if (const auto voice = Get<HistoryDocumentVoice>()) { if (const auto voice = Get<HistoryDocumentVoice>()) {
if (voice->seeking()) { if (!voice->seeking()) {
const auto thumbed = Get<HistoryDocumentThumbed>(); return;
const auto &st = thumbed ? st::msgFileThumbLayout : st::msgFileLayout;
const auto nameleft = st.padding.left() + st.thumbSize + st.thumbSkip;
const auto nameright = st.padding.right();
voice->setSeekingCurrent(std::clamp(
(point.x() - nameleft)
/ float64(width() - nameleft - nameright),
0.,
1.));
repaint();
} }
const auto thumbed = Get<HistoryDocumentThumbed>();
const auto &st = thumbed ? st::msgFileThumbLayout : st::msgFileLayout;
const auto nameleft = st.padding.left() + st.thumbSize + st.thumbSkip;
const auto nameright = st.padding.right();
const auto transcribeWidth = voice->transcribe
? (st::historyTranscribeSkip + voice->transcribe->size().width())
: 0;
voice->setSeekingCurrent(std::clamp(
(point.x() - nameleft)
/ float64(width() - transcribeWidth - nameleft - nameright),
0.,
1.));
repaint();
} }
} }
@ -1739,4 +1759,23 @@ bool DrawThumbnailAsSongCover(
return true; return true;
} }
rpl::producer<> TTLVoiceStops(FullMsgId fullId) {
return rpl::merge(
::Media::Player::instance()->updatedNotifier(
) | rpl::filter([=](::Media::Player::TrackState state) {
using State = ::Media::Player::State;
const auto badState = state.state == State::Stopped
|| state.state == State::StoppedAtEnd
|| state.state == State::StoppedAtError
|| state.state == State::StoppedAtStart;
return (state.id.contextId() != fullId) && !badState;
}) | rpl::to_empty,
::Media::Player::instance()->tracksFinished(
) | rpl::filter([=](AudioMsgId::Type type) {
return (type == AudioMsgId::Type::Voice);
}) | rpl::to_empty,
::Media::Player::instance()->stops(AudioMsgId::Type::Voice)
);
}
} // namespace HistoryView } // namespace HistoryView

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