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
xcodebuild -version > CACHE_KEY.txt
brew list --versions >> CACHE_KEY.txt
echo $MANUAL_CACHING >> CACHE_KEY.txt
echo "$GITHUB_WORKSPACE" >> CACHE_KEY.txt
if [ "$AUTO_CACHING" = "1" ]; then

View file

@ -478,6 +478,8 @@ PRIVATE
chat_helpers/tabbed_section.h
chat_helpers/tabbed_selector.cpp
chat_helpers/tabbed_selector.h
chat_helpers/ttl_media_layer_widget.cpp
chat_helpers/ttl_media_layer_widget.h
core/application.cpp
core/application.h
core/base_integration.cpp
@ -681,6 +683,8 @@ PRIVATE
dialogs/dialogs_row.h
dialogs/dialogs_search_from_controllers.cpp
dialogs/dialogs_search_from_controllers.h
dialogs/dialogs_search_tags.cpp
dialogs/dialogs_search_tags.h
dialogs/dialogs_widget.cpp
dialogs/dialogs_widget.h
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_round_sent" = "You sent a self-destructing video message.";
"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_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_lock_discard" = "Discard";
"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_wont_be_notified" = "Members will not be notified when you post";
"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_none" = "Nobody Reacted";
"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_all_files" = "Delete all files";
"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#other" = "{count} 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_cant_reply" = "You can't reply to this story.";
"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_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="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/star.svg">../../icons/settings/star.svg</file>
<file alias="icons/settings/starmini.svg">../../icons/settings/starmini.svg</file>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -162,7 +162,7 @@ void AutoDownloadBox::setupContent() {
*downloadValues,
*autoPlayValues);
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 old = settings->bytesLimit(_source, type);
return (old < value);
@ -170,7 +170,7 @@ void AutoDownloadBox::setupContent() {
return pair.first;
});
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;
return value < settings->bytesLimit(_source, type);
});
@ -179,7 +179,7 @@ void AutoDownloadBox::setupContent() {
allowMore.end());
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;
return value != settings->bytesLimit(_source, type);
});

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -531,6 +531,12 @@ void Viewport::RendererGL::paintTile(
{ { 1.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) {
std::rotate(
toBlurTexCoords.begin(),

View file

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

View file

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

View file

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

View file

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

View file

@ -1063,12 +1063,17 @@ historyRecordVoiceShowDuration: 120;
historyRecordVoiceDuration: 120;
historyRecordVoice: icon {{ "chat/input_record", historyRecordVoiceFg }};
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 }};
historyRecordSendIconPosition: point(2px, 0px);
historyRecordVoiceRippleBgActive: lightButtonBgOver;
historyRecordSignalRadius: 5px;
historyRecordCancel: windowSubTextFg;
historyRecordCancelActive: windowActiveTextFg;
historyRecordCancelActive: historySendIconFg;
historyRecordFont: font(13px);
historyRecordDurationSkip: 12px;
historyRecordDurationFg: historyComposeAreaFg;
@ -1111,20 +1116,26 @@ historyRecordLockArrow: icon {{ "voice_lock/voice_arrow", historyToDownFg }};
historyRecordLockRippleMargin: margins(6px, 6px, 6px, 6px);
historyRecordDelete: IconButton(historyAttach) {
icon: icon {{ "info/info_media_delete", historyComposeIconFg }};
iconOver: icon {{ "info/info_media_delete", historyComposeIconFgOver }};
icon: icon {{ "voice_lock/recorded_delete", historyComposeIconFg }};
iconOver: icon {{ "voice_lock/recorded_delete", historyComposeIconFgOver }};
iconPosition: point(10px, 11px);
}
historyRecordWaveformRightSkip: 10px;
historyRecordWaveformBgMargins: margins(5px, 7px, 5px, 7px);
historyRecordWaveformBgMargins: margins(5px, 8px, 5px, 9px);
historyRecordWaveformBar: 3px;
historyRecordLockPosition: point(1px, 35px);
historyRecordLockPosition: point(1px, 22px);
historyRecordCancelButtonWidth: 100px;
historyRecordCancelButtonFg: lightButtonFg;
historyRecordTooltip: ImportantTooltip(defaultImportantTooltip) {
padding: margins(4px, 4px, 4px, 4px);
radius: 11px;
arrow: 6px;
}
historySilentToggle: IconButton(historyBotKeyboardShow) {
icon: icon {{ "chat/input_silent", historyComposeIconFg }};
iconOver: icon {{ "chat/input_silent", historyComposeIconFgOver }};
@ -1266,3 +1277,17 @@ dragDropColor: windowActiveTextFg;
dragMargin: margins(0px, 10px, 0px, 10px);
dragPadding: margins(20px, 10px, 20px, 10px);
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 height = st::manageEmojiPreviewWidth;
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 column = (index % 2);
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];
if (it == begin(_localMonths)) {
return fallback;
} else if (it == end(_localMonths)) {
return _documents.back();
}
const auto left = *(it - 1);
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();
startTray();
_lastActivePrimaryWindow->widget()->show();
_lastActivePrimaryWindow->firstShow();
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);
if (!MTP::IsGoodModExpFirst(g_b, p)) {
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)
<< quint64(_macRoundIconDigest.value_or(0))
<< qint32(_storiesClickTooltipHidden.current() ? 1 : 0)
<< qint32(_recentEmojiSkip.size());
<< qint32(_recentEmojiSkip.size())
<< qint32(_ttlVoiceClickTooltipHidden.current() ? 1 : 0);
for (const auto &id : _recentEmojiSkip) {
stream << id;
}
@ -459,6 +460,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
qint32 storiesClickTooltipHidden = _storiesClickTooltipHidden.current() ? 1 : 0;
base::flat_set<QString> recentEmojiSkip;
qint32 trayIconMonochrome = (_trayIconMonochrome.current() ? 1 : 0);
qint32 ttlVoiceClickTooltipHidden = _ttlVoiceClickTooltipHidden.current() ? 1 : 0;
stream >> themesAccentColors;
if (!stream.atEnd()) {
@ -715,6 +717,9 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
// Let existing clients use the old value.
trayIconMonochrome = 0;
}
if (!stream.atEnd()) {
stream >> ttlVoiceClickTooltipHidden;
}
if (stream.status() != QDataStream::Ok) {
LOG(("App Error: "
"Bad data for Core::Settings::constructFromSerialized()"));
@ -910,6 +915,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
_storiesClickTooltipHidden = (storiesClickTooltipHidden == 1);
_recentEmojiSkip = std::move(recentEmojiSkip);
_trayIconMonochrome = (trayIconMonochrome == 1);
_ttlVoiceClickTooltipHidden = (ttlVoiceClickTooltipHidden == 1);
}
QString Settings::getSoundPath(const QString &key) const {
@ -1266,6 +1272,7 @@ void Settings::resetOnLastLogout() {
_systemDarkModeEnabled = false;
_hiddenGroupCallTooltips = 0;
_storiesClickTooltipHidden = false;
_ttlVoiceClickTooltipHidden = false;
_recentEmojiPreload.clear();
_recentEmoji.clear();

View file

@ -826,6 +826,15 @@ public:
void setStoriesClickTooltipHidden(bool 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 float64 DefaultDialogsWidthRatio();
@ -951,6 +960,7 @@ private:
rpl::variable<bool> _ignoreBatterySaving = false;
std::optional<uint64> _macRoundIconDigest;
rpl::variable<bool> _storiesClickTooltipHidden = false;
rpl::variable<bool> _ttlVoiceClickTooltipHidden = false;
bool _tabbedReplacedWithInfo = false; // 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"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"folder2"_q , Command::ShowFolder2 },
{ u"folder3"_q , Command::ShowFolder3 },
@ -126,6 +133,13 @@ const auto CommandNames = base::flat_map<Command, QString>{
{ Command::FolderNext , u"next_folder"_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::ShowFolder2 , u"folder2"_q },
{ Command::ShowFolder3 , u"folder3"_q },
@ -388,10 +402,18 @@ void Manager::fillDefaults() {
kShowFolder,
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);
}
//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+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();
document.setArray(shortcuts);
file.write(document.toJson(QJsonDocument::Indented));

View file

@ -38,6 +38,13 @@ enum class Command {
ChatPinned7,
ChatPinned8,
ShowAccount1,
ShowAccount2,
ShowAccount3,
ShowAccount4,
ShowAccount5,
ShowAccount6,
ShowAllChats,
ShowFolder1,
ShowFolder2,
@ -79,6 +86,15 @@ enum class Command {
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);
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 AppName = "AyuGram Desktop"_cs;
constexpr auto AppFile = "AyuGram"_cs;
constexpr auto AppVersion = 4014003;
constexpr auto AppVersionStr = "4.14.3";
constexpr auto AppVersion = 4014006;
constexpr auto AppVersionStr = "4.14.6";
constexpr auto AppBetaVersion = false;
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;

View file

@ -56,7 +56,7 @@ rpl::producer<UpdateType> Changes::Manager<DataType, UpdateType>::updates(
Flags flags) const {
return _stream.events(
) | rpl::filter([=](const UpdateType &update) {
const auto [updateData, updateFlags] = update;
const auto &[updateData, updateFlags] = update;
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/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/core_settings.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_file_click_handler.h"
#include "data/data_session.h"
#include "history/view/media/history_view_gif.h"
#include "history/history.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 "media/player/media_player_instance.h"
#include "platform/platform_file_utilities.h"
#include "ui/boxes/confirm_box.h"
#include "ui/chat/chat_theme.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/checkbox.h"
#include "window/window_session_controller.h"
#include "boxes/abstract_box.h" // Ui::show().
#include "styles/style_layers.h"
#include <QtCore/QBuffer>
@ -298,6 +299,12 @@ void ResolveDocument(
|| document->isVoiceMessage()
|| document->isVideoMessage()) {
::Media::Player::instance()->playPause({ document, msgId });
if (controller
&& item
&& item->media()
&& item->media()->ttlSeconds()) {
ChatHelpers::ShowTTLMediaLayerWidget(controller, item);
}
} else {
showDocument();
}

View file

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

View file

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

View file

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

View file

@ -11,6 +11,26 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
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) {
if (id.empty()) {
return {};

View file

@ -27,6 +27,10 @@ struct ReactionId {
return custom ? *custom : DocumentId();
}
explicit operator bool() const {
return !empty();
}
friend inline auto operator<=>(
const ReactionId &,
const ReactionId &) = default;
@ -41,6 +45,9 @@ struct MessageReaction {
bool my = false;
};
[[nodiscard]] QString SearchTagToQuery(const ReactionId &tagId);
[[nodiscard]] ReactionId SearchTagFromQuery(const QString &query);
[[nodiscard]] QString ReactionEntityData(const ReactionId &id);
[[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 kRecentRequestTimeout = 10 * crl::time(1000);
constexpr auto kRecentReactionsLimit = 40;
constexpr auto kMyTagsRequestTimeout = crl::time(1000);
constexpr auto kTopRequestDelay = 60 * crl::time(1000);
constexpr auto kTopReactionsLimit = 14;
@ -69,6 +70,27 @@ constexpr auto kTopReactionsLimit = 14;
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) {
return Reaction{
.id = { { document->id } },
@ -126,6 +148,8 @@ PossibleItemReactionsRef LookupPossibleReactions(
const auto &full = reactions->list(Reactions::Type::Active);
const auto &top = reactions->list(Reactions::Type::Top);
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 limit = UniqueReactionsLimit(peer);
const auto premiumPossible = session->premiumPossible();
@ -148,7 +172,20 @@ PossibleItemReactionsRef LookupPossibleReactions(
}
};
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());
add([&](const Reaction &reaction) {
return ranges::contains(all, reaction.id, &MessageReaction::id);
@ -198,23 +235,26 @@ PossibleItemReactionsRef LookupPossibleReactions(
result.customAllowed = (allowed.type == AllowedReactionsType::All)
&& premiumPossible;
}
const auto i = ranges::find(
result.recent,
reactions->favoriteId(),
&Reaction::id);
if (i != end(result.recent) && i != begin(result.recent)) {
std::rotate(begin(result.recent), i, i + 1);
if (!item->reactionsAreTags()) {
const auto i = ranges::find(
result.recent,
reactions->favoriteId(),
&Reaction::id);
if (i != end(result.recent) && i != begin(result.recent)) {
std::rotate(begin(result.recent), i, i + 1);
}
}
return result;
}
PossibleItemReactions::PossibleItemReactions(
const PossibleItemReactionsRef &other)
: recent(other.recent | ranges::views::transform([](const auto &value) {
: recent(other.recent | ranges::views::transform([](const auto &value) {
return *value;
}) | ranges::to_vector)
, morePremiumAvailable(other.morePremiumAvailable)
, customAllowed(other.customAllowed) {
, customAllowed(other.customAllowed)
, tags(other.tags){
}
Reactions::Reactions(not_null<Session*> owner)
@ -285,16 +325,42 @@ void Reactions::refreshDefault() {
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 {
switch (type) {
case Type::Active: return _active;
case Type::Recent: return _recent;
case Type::Top: return _top;
case Type::All: return _available;
case Type::MyTags: return _myTags;
case Type::Tags: return _tags;
}
Unexpected("Type in Reactions::list.");
}
const std::vector<MyTagInfo> &Reactions::myTagsInfo() const {
return _myTagsInfo;
}
ReactionId Reactions::favoriteId() const {
return _favoriteId;
}
@ -319,6 +385,56 @@ void Reactions::setFavorite(const ReactionId &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(
not_null<DocumentData*> custom) const {
const auto sticker = custom->sticker();
@ -380,6 +496,14 @@ rpl::producer<> Reactions::favoriteUpdates() const {
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) {
if (_images.contains(id) || id.emoji().isEmpty()) {
return;
@ -622,6 +746,46 @@ void Reactions::requestGeneric() {
}).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) {
_topHash = data.vhash().v;
_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() {
_topRefreshTimer.callOnce(kTopRequestDelay);
_recentUpdated.fire({});
@ -701,9 +882,25 @@ void Reactions::defaultUpdated() {
if (_genericAnimations.empty()) {
requestGeneric();
}
refreshMyTags();
refreshTags();
_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() {
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 j = _unresolvedRecent.find(id);
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) {
_unresolvedFavoriteId = ReactionId();
_favorite = resolveById(_favoriteId);
@ -727,6 +928,14 @@ void Reactions::customEmojiResolveDone(not_null<DocumentData*> document) {
_unresolvedRecent.erase(j);
_recent = resolveByIds(_recentIds, _unresolvedRecent);
}
if (myTag) {
_unresolvedMyTags.erase(k);
_myTags = resolveByIds(_myTagsIds, _unresolvedMyTags);
}
if (tag) {
_unresolvedTags.erase(l);
_tags = resolveByIds(_tagsIds, _unresolvedTags);
}
if (favorite) {
_favoriteUpdated.fire({});
}
@ -736,6 +945,12 @@ void Reactions::customEmojiResolveDone(not_null<DocumentData*> document) {
if (recent) {
_recentUpdated.fire({});
}
if (myTag) {
_myTagsUpdated.fire({});
}
if (tag) {
_tagsUpdated.fire({});
}
}
std::optional<Reaction> Reactions::resolveById(const ReactionId &id) {
@ -1003,6 +1218,10 @@ void MessageReactions::add(const ReactionId &id, bool addToRecent) {
return;
}
auto my = 0;
const auto tags = _item->reactionsAreTags();
if (tags) {
history->owner().reactions().incrementMyTag(id);
}
_list.erase(ranges::remove_if(_list, [&](MessageReaction &one) {
const auto removing = one.my && (my == myLimit || ++my == myLimit);
if (!removing) {
@ -1024,6 +1243,9 @@ void MessageReactions::add(const ReactionId &id, bool addToRecent) {
}
}
}
if (tags) {
history->owner().reactions().decrementMyTag(one.id);
}
return removed;
}), end(_list));
const auto peer = history->peer;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -623,3 +623,6 @@ dialogsStoriesTooltipHide: IconButton(defaultIconButton) {
searchedBarHeight: 32px;
searchedBarFont: normalFont;
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_widget.h"
#include "dialogs/dialogs_search_from_controllers.h"
#include "dialogs/dialogs_search_tags.h"
#include "history/history.h"
#include "history/history_item.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_cloud_file.h"
#include "data/data_changes.h"
#include "data/data_message_reactions.h"
#include "data/data_saved_messages.h"
#include "data/data_saved_sublist.h"
#include "data/data_stories.h"
#include "data/stickers/data_stickers.h"
#include "data/data_send_action.h"
@ -477,22 +480,30 @@ int InnerWidget::peerSearchOffset() const {
+ st::searchedBarHeight;
}
int InnerWidget::searchedOffset() const {
auto result = peerSearchOffset();
int InnerWidget::searchInChatOffset() const {
auto result = peerSearchOffset() - st::searchedBarHeight;
if (!_peerSearchResults.empty()) {
result += (_peerSearchResults.size() * st::dialogsRowHeight)
+ st::searchedBarHeight;
}
result += searchInChatSkip();
return result;
}
int InnerWidget::searchedOffset() const {
return searchInChatOffset()
+ searchInChatSkip()
+ st::searchedBarHeight;
}
int InnerWidget::searchInChatSkip() const {
auto result = 0;
if (_searchTags) {
result += _searchTags->height();
}
if (_searchInChat) {
result += st::searchedBarHeight + st::dialogsSearchInHeight;
}
if (_searchFromPeer) {
if (_searchFromShown) {
if (_searchInChat) {
result += st::lineWidth;
}
@ -1111,17 +1122,25 @@ void InnerWidget::paintSearchInChat(
auto height = searchInChatSkip();
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);
if (_searchInChat) {
top += st::searchedBarHeight;
p.fillRect(0, 0, width(), top, st::searchedBarBg);
const auto bar = st::searchedBarHeight;
p.fillRect(0, top, width(), top + bar, st::searchedBarBg);
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);
p.fillRect(fullRect, currentBg());
if (_searchInChat) {
if (_searchFromPeer) {
if (_searchFromShown) {
p.fillRect(QRect(0, top + st::dialogsSearchInHeight, width(), st::lineWidth), st::shadowFg);
}
p.setPen(st::dialogsNameFg);
@ -1135,15 +1154,17 @@ void InnerWidget::paintSearchInChat(
} else {
paintSearchInPeer(p, peer, _searchInChatUserpic, top, _searchInChatText);
}
} else if (const auto sublist = _searchInChat.sublist()) {
paintSearchInSaved(p, top, _searchInChatText);
} else {
Unexpected("Empty Key in paintSearchInChat.");
}
top += st::dialogsSearchInHeight + st::lineWidth;
}
if (_searchFromPeer) {
if (_searchFromShown) {
p.setPen(st::dialogsTextFg);
p.setTextPalette(st::dialogsSearchFromPalette);
paintSearchInPeer(p, _searchFromPeer, _searchFromUserUserpic, top, _searchFromUserText);
paintSearchInPeer(p, _searchFromShown, _searchFromUserUserpic, top, _searchFromUserText);
p.restoreTextPalette();
}
}
@ -1276,6 +1297,21 @@ void InnerWidget::selectByMouse(QPoint globalPosition) {
_lastMousePosition = globalPosition;
_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 mouseY = local.y();
clearIrrelevantState();
@ -1370,7 +1406,7 @@ void InnerWidget::selectByMouse(QPoint globalPosition) {
updateSelectedRow();
}
}
if (wasSelected != isSelected()) {
if (!inTags && wasSelected != isSelected()) {
setCursor(wasSelected ? style::cur_default : style::cur_pointer);
}
}
@ -1452,6 +1488,7 @@ void InnerWidget::mousePressEvent(QMouseEvent *e) {
QSize(width(), _st->height),
row->repaint());
}
ClickHandler::pressed();
if (anim::Disabled()
&& (!_pressed || !_pressed->entry()->isPinnedDialog(_filterId))) {
mousePressReleased(e->globalPos(), e->button(), e->modifiers());
@ -1743,6 +1780,9 @@ void InnerWidget::mousePressReleased(
chooseRow(modifiers, pressedTopicRootId);
}
}
if (auto activated = ClickHandler::unpressed()) {
ActivateClickHandler(window(), activated, ClickContext{ button });
}
}
void InnerWidget::setCollapsedPressed(int pressed) {
@ -1825,9 +1865,10 @@ void InnerWidget::moveCancelSearchButtons() {
st::columnMinimalWidthLeft - _narrowWidth);
const auto left = widthForCancelButton - st::dialogsSearchInSkip - _cancelSearchInChat->width();
const auto top = (st::dialogsSearchInHeight - st::dialogsCancelSearchInPeer.height) / 2;
_cancelSearchInChat->moveToLeft(left, st::searchedBarHeight + top);
const auto skip = _searchInChat ? (st::searchedBarHeight + st::dialogsSearchInHeight + st::lineWidth) : 0;
_cancelSearchFromUser->moveToLeft(left, skip + top);
const auto skip = st::searchedBarHeight + (_searchTags ? _searchTags->height() : 0);
_cancelSearchInChat->moveToLeft(left, skip + top);
const auto next = _searchInChat ? (skip + st::dialogsSearchInHeight + st::lineWidth) : 0;
_cancelSearchFromUser->moveToLeft(left, next + top);
}
void InnerWidget::dialogRowReplaced(
@ -2330,7 +2371,9 @@ void InnerWidget::applyFilterUpdate(QString newFilter, bool force) {
newFilter = words.isEmpty() ? QString() : words.join(' ');
if (newFilter != _filter || force) {
_filter = newFilter;
if (_filter.isEmpty() && !_searchFromPeer) {
if (_filter.isEmpty()
&& !_searchFromPeer
&& _searchTagsSelected.empty()) {
clearFilter();
} else {
setState(WidgetState::Filtered);
@ -2350,7 +2393,9 @@ void InnerWidget::applyFilterUpdate(QString newFilter, bool force) {
top += i->row->height();
}
};
if (!_searchInChat && !_searchFromPeer && !words.isEmpty()) {
if (!_searchInChat
&& !_searchFromPeer
&& !words.isEmpty()) {
if (_savedSublists) {
const auto owner = &session().data();
append(owner->savedMessages().chatsList()->indexed());
@ -2413,7 +2458,7 @@ void InnerWidget::appendToFiltered(Key key) {
}
auto row = std::make_unique<Row>(key, 0, 0);
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();
_filterResults.emplace_back(i->second.get());
_filterResults.back().top = height;
@ -2791,6 +2836,11 @@ void InnerWidget::refresh(bool toTop) {
return refreshWithCollapsedRows(toTop);
}
refreshEmptyLabel();
if (_searchTags) {
_searchTagsLeft = st::dialogsFilterSkip
+ st::dialogsFilterPadding.x();
_searchTags->resizeToWidth(width() - 2 * _searchTagsLeft);
}
auto h = 0;
if (_state == WidgetState::Default) {
if (_shownList->empty()) {
@ -2918,26 +2968,73 @@ bool InnerWidget::hasFilteredResults() const {
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;
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()) {
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()) {
_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;
_searchFromPeer = from;
_searchFromShown = key.sublist() ? key.sublist()->peer().get() : from;
if (_searchInChat) {
onHashtagFilterUpdate(QStringView());
_cancelSearchInChat->show();
} else {
_cancelSearchInChat->hide();
}
if (_searchFromPeer) {
if (_searchFromShown) {
_cancelSearchFromUser->show();
_searchFromUserUserpic = _searchFromPeer->createUserpicView();
_searchFromUserUserpic = _searchFromShown->createUserpicView();
} else {
_cancelSearchFromUser->hide();
_searchFromUserUserpic = {};
@ -2946,7 +3043,7 @@ void InnerWidget::searchInChat(Key key, PeerData *from) {
refreshSearchInChatLabel();
}
if (const auto peer = _searchInChat.peer()) {
if (peer) {
_searchInChatUserpic = peer->createUserpicView();
} else {
_searchInChatUserpic = {};
@ -2957,6 +3054,13 @@ void InnerWidget::searchInChat(Key key, PeerData *from) {
_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() {
const auto dialog = [&] {
if (const auto topic = _searchInChat.topic()) {
@ -2968,6 +3072,8 @@ void InnerWidget::refreshSearchInChatLabel() {
return tr::lng_replies_messages(tr::now);
}
return peer->name();
} else if (_searchInChat.sublist()) {
return tr::lng_saved_messages(tr::now);
}
return QString();
}();
@ -2977,7 +3083,7 @@ void InnerWidget::refreshSearchInChatLabel() {
dialog,
Ui::DialogTextOptions());
}
const auto from = _searchFromPeer ? _searchFromPeer->name() : QString();
const auto from = _searchFromShown ? _searchFromShown->name() : u""_q;
if (!from.isEmpty()) {
const auto fromUserText = tr::lng_dlg_search_from(
tr::now,
@ -3809,7 +3915,7 @@ void InnerWidget::setupShortcuts() {
auto &&folders = ranges::views::zip(
Shortcuts::kShowFolder,
ranges::views::ints(0, ranges::unreachable));
for (const auto [command, index] : folders) {
for (const auto &[command, index] : folders) {
const auto select = (command == Command::ShowFolderLast)
? (filtersCount - 1)
: std::clamp(index, 0, filtersCount - 1);
@ -3836,7 +3942,7 @@ void InnerWidget::setupShortcuts() {
auto &&pinned = ranges::views::zip(
kPinned,
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] {
const auto list = (_filterId
? session().data().chatsFilters().chatsList(_filterId)

View file

@ -43,6 +43,7 @@ namespace Data {
class Thread;
class Folder;
class Forum;
struct ReactionId;
} // namespace Data
namespace Dialogs::Ui {
@ -57,6 +58,7 @@ namespace Dialogs {
class Row;
class FakeRow;
class IndexedList;
class SearchTags;
struct ChosenRow {
Key key;
@ -137,7 +139,12 @@ public:
}
[[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 onHashtagFilterUpdate(QStringView newFilter);
@ -325,6 +332,7 @@ private:
[[nodiscard]] int filteredIndex(int y) const;
[[nodiscard]] int filteredHeight(int till = -1) const;
[[nodiscard]] int peerSearchOffset() const;
[[nodiscard]] int searchInChatOffset() const;
[[nodiscard]] int searchedOffset() const;
[[nodiscard]] int searchInChatSkip() const;
@ -478,10 +486,14 @@ private:
Key _searchInChat;
History *_searchInMigrated = nullptr;
PeerData *_searchFromPeer = nullptr;
PeerData *_searchFromShown = nullptr;
mutable Ui::PeerUserpicView _searchInChatUserpic;
mutable Ui::PeerUserpicView _searchFromUserUserpic;
Ui::Text::String _searchInChatText;
Ui::Text::String _searchFromUserText;
std::unique_ptr<SearchTags> _searchTags;
std::vector<Data::ReactionId> _searchTagsSelected;
int _searchTagsLeft = 0;
RowDescriptor _menuRow;
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_download_manager.h"
#include "data/data_chat_filters.h"
#include "data/data_saved_sublist.h"
#include "data/data_stories.h"
#include "info/downloads/info_downloads_widget.h"
#include "info/info_memento.h"
@ -832,7 +833,7 @@ void Widget::setupStories() {
{
return;
}
_stories->verticalScrollEvents(
) | rpl::start_with_next([=](not_null<QWheelEvent*> e) {
_scroll->viewportEvent(e);
@ -1733,7 +1734,7 @@ void Widget::loadMoreBlockedByDate() {
bool Widget::searchMessages(bool searchCache) {
auto result = false;
auto q = currentSearchQuery().trimmed();
if (q.isEmpty() && !_searchFromAuthor) {
if (q.isEmpty() && !_searchFromAuthor && _searchTags.empty()) {
cancelSearchRequest();
_api.request(base::take(_peerSearchRequest)).cancel();
_api.request(base::take(_topicSearchRequest)).cancel();
@ -1750,6 +1751,7 @@ bool Widget::searchMessages(bool searchCache) {
if (i != _searchCache.end()) {
_searchQuery = q;
_searchQueryFrom = _searchFromAuthor;
_searchQueryTags = _searchTags;
_searchNextRate = 0;
_searchFull = _searchFullMigrated = false;
cancelSearchRequest();
@ -1761,9 +1763,12 @@ bool Widget::searchMessages(bool searchCache) {
0);
result = true;
}
} else if (_searchQuery != q || _searchQueryFrom != _searchFromAuthor) {
} else if (_searchQuery != q
|| _searchQueryFrom != _searchFromAuthor
|| _searchQueryTags != _searchTags) {
_searchQuery = q;
_searchQueryFrom = _searchFromAuthor;
_searchQueryTags = _searchTags;
_searchNextRate = 0;
_searchFull = _searchFullMigrated = false;
cancelSearchRequest();
@ -1772,18 +1777,31 @@ bool Widget::searchMessages(bool searchCache) {
auto &histories = session().data().histories();
const auto type = Data::Histories::RequestType::History;
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) {
const auto type = SearchRequestType::PeerFromStart;
using Flag = MTPmessages_Search::Flag;
_searchRequest = session().api().request(MTPmessages_Search(
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,
MTP_string(_searchQuery),
(_searchQueryFrom
? _searchQueryFrom->input
: MTP_inputPeerEmpty()),
MTPInputPeer(), // saved_peer_id
(fromPeer ? fromPeer->input : MTP_inputPeerEmpty()),
(savedPeer ? savedPeer->input : MTP_inputPeerEmpty()),
MTP_vector_from_range(
_searchQueryTags | ranges::views::transform(
Data::ReactionToMTP
)),
MTP_int(topic ? topic->rootId() : 0),
MTP_inputMessagesFilterEmpty(),
MTP_int(0), // min_date
@ -1887,6 +1905,7 @@ bool Widget::searchMessages(bool searchCache) {
bool Widget::searchForPeersRequired(const QString &query) const {
return !_searchInChat
&& !_searchFromAuthor
&& _searchTags.empty()
&& !_openedForum
&& !query.isEmpty()
&& (query[0] != '#');
@ -1895,6 +1914,7 @@ bool Widget::searchForPeersRequired(const QString &query) const {
bool Widget::searchForTopicsRequired(const QString &query) const {
return !_searchInChat
&& !_searchFromAuthor
&& _searchTags.empty()
&& _openedForum
&& !query.isEmpty()
&& (query[0] != '#')
@ -1911,7 +1931,7 @@ void 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) {
const auto forum = controller()->shownForum().current();
const auto topic = inChat.topic();
@ -1926,6 +1946,12 @@ void Widget::searchMessages(const QString &query, Key inChat, UserData *from) {
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 inPeer = inChat.peer();
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()))
&& (inChat == _searchInChat)) {
return false;
} else if (const auto inPeer = inChat.peer()) {
} else if (inPeer) {
if (const auto to = inPeer->migrateTo()) {
if (to == _searchInChat.peer() && !_searchInChat.topic()) {
return false;
@ -1947,10 +1973,12 @@ void Widget::searchMessages(const QString &query, Key inChat, UserData *from) {
}
return true;
}();
if ((currentSearchQuery() != query) || inChatChanged) {
if ((currentSearchQuery() != query)
|| inChatChanged
|| _searchTags != tags) {
if (inChat) {
cancelSearch();
setSearchInChat(inChat);
setSearchInChat(inChat, nullptr, tags);
}
setSearchQuery(query);
applyFilterUpdate(true);
@ -2017,6 +2045,13 @@ void Widget::searchMore() {
const auto topic = searchInTopic();
const auto type = Data::Histories::RequestType::History;
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) {
const auto type = _lastSearchId
? SearchRequestType::PeerFromOffset
@ -2024,13 +2059,19 @@ void Widget::searchMore() {
using Flag = MTPmessages_Search::Flag;
_searchRequest = session().api().request(MTPmessages_Search(
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,
MTP_string(_searchQuery),
(_searchQueryFrom
? _searchQueryFrom->input
: MTP_inputPeerEmpty()),
MTPInputPeer(), // saved_peer_id
(fromPeer ? fromPeer->input : MTP_inputPeerEmpty()),
(savedPeer ? savedPeer->input : MTP_inputPeerEmpty()),
MTP_vector_from_range(
_searchQueryTags | ranges::views::transform(
Data::ReactionToMTP
)),
MTP_int(topic ? topic->rootId() : 0),
MTP_inputMessagesFilterEmpty(),
MTP_int(0), // min_date
@ -2104,6 +2145,7 @@ void Widget::searchMore() {
? _searchQueryFrom->input
: MTP_inputPeerEmpty()),
MTPInputPeer(), // saved_peer_id
MTPVector<MTPReaction>(), // saved_reaction
MTPint(), // top_msg_id
MTP_inputMessagesFilterEmpty(),
MTP_int(0), // min_date
@ -2423,7 +2465,7 @@ void Widget::applyFilterUpdate(bool force) {
updateStoriesVisibility();
const auto filterText = currentSearchQuery();
_inner->applyFilterUpdate(filterText, force);
if (filterText.isEmpty() && !_searchFromAuthor) {
if (filterText.isEmpty() && !_searchFromAuthor && _searchTags.empty()) {
clearSearchCache();
}
_cancelSearch->toggle(!filterText.isEmpty(), anim::type::normal);
@ -2439,7 +2481,9 @@ void Widget::applyFilterUpdate(bool force) {
_peerSearchQuery = QString();
}
if (_chooseFromUser->toggled() || _searchFromAuthor) {
if (_chooseFromUser->toggled()
|| _searchFromAuthor
|| !_searchTags.empty()) {
auto switchToChooseFrom = HistoryView::SwitchToChooseFromQuery();
if (_lastFilterText != switchToChooseFrom
&& switchToChooseFrom.startsWith(_lastFilterText)
@ -2583,9 +2627,12 @@ void Widget::searchInChat(Key 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->setSearchInChat(chat, from)) {
if (_childList->setSearchInChat(chat, from, tags)) {
return true;
}
hideChildList();
@ -2621,7 +2668,8 @@ bool Widget::setSearchInChat(Key chat, PeerData *from) {
if (_layout != Layout::Main) {
return false;
} 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()) {
_searchInMigrated = peer->owner().history(migrateFrom);
}
@ -2640,7 +2688,20 @@ bool Widget::setSearchInChat(Key chat, PeerData *from) {
if (_searchInChat && _layout == Layout::Main) {
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) {
_subsectionTopBar->searchEnableJumpToDate(
_openedForum && _searchInChat);
@ -2653,6 +2714,12 @@ bool Widget::setSearchInChat(Key chat, PeerData *from) {
return true;
}
bool Widget::setSearchInChat(
Key chat,
PeerData *from) {
return setSearchInChat(chat, from, {});
}
void Widget::clearSearchCache() {
_searchCache.clear();
_singleMessageSearch.clear();
@ -2661,6 +2728,7 @@ void Widget::clearSearchCache() {
}
_searchQuery = QString();
_searchQueryFrom = nullptr;
_searchQueryTags.clear();
_topicSearchQuery = QString();
_topicSearchOffsetDate = 0;
_topicSearchOffsetId = _topicSearchOffsetTopicId = 0;
@ -3072,6 +3140,8 @@ void Widget::cancelSearchRequest() {
PeerData *Widget::searchInPeer() const {
return _openedForum
? _openedForum->channel().get()
: _searchInChat.sublist()
? session().user().get()
: _searchInChat.peer();
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -470,7 +470,7 @@ not_null<HistoryItem*> History::insertItem(
std::unique_ptr<HistoryItem> item) {
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();
owner().registerMessage(result);

View file

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

View file

@ -2461,6 +2461,11 @@ const std::vector<Data::MessageReaction> &HistoryItem::reactions() const {
return _reactions ? _reactions->list() : kEmpty;
}
bool HistoryItem::reactionsAreTags() const {
// Disable reactions as tags for now.
return false;// _flags & MessageFlag::ReactionsAreTags;
}
auto HistoryItem::recentReactions() const
-> const base::flat_map<
Data::ReactionId,
@ -3634,31 +3639,40 @@ bool HistoryItem::changeReactions(const MTPMessageReactions *reactions) {
}
if (!reactions) {
_flags &= ~MessageFlag::CanViewReactions;
if (_history->peer->isSelf()) {
_flags |= MessageFlag::ReactionsAreTags;
}
return (base::take(_reactions) != nullptr);
}
return reactions->match([&](const MTPDmessageReactions &data) {
if (data.is_can_see_list()) {
_flags |= MessageFlag::CanViewReactions;
} else {
_flags &= ~MessageFlag::CanViewReactions;
const auto &data = reactions->data();
const auto empty = data.vresults().v.isEmpty();
if (data.is_reactions_as_tags()
|| (empty && _history->peer->isSelf())) {
_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 (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();
}
return false;
}
return _reactions->change(list, recent, min);
});
return false;
}
return _reactions->change(list, recent, min);
}
void HistoryItem::applyTTL(const MTPDmessage &data) {

View file

@ -455,6 +455,7 @@ public:
not_null<UserData*> from) const;
[[nodiscard]] crl::time lastReactionsRefreshTime() const;
[[nodiscard]] bool reactionsAreTags() const;
[[nodiscard]] bool hasDirectLink() 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 ClearMediaAsExpired(not_null<HistoryItem*> item);
[[nodiscard]] bool IsVoiceOncePlayable(not_null<HistoryItem*> item);

View file

@ -2614,12 +2614,15 @@ bool HistoryWidget::updateReplaceMediaButton() {
return false;
}
_replaceMedia.create(this, st::historyReplaceMedia);
const auto hideDuration = st::historyReplaceMedia.ripple.hideDuration;
_replaceMedia->setClickedCallback([=] {
EditCaptionBox::StartMediaReplace(
controller(),
{ _history->peer->id, _editMsgId },
_field->getTextWithTags(),
crl::guard(_list, [=] { cancelEdit(); }));
base::call_delayed(hideDuration, this, [=] {
EditCaptionBox::StartMediaReplace(
controller(),
{ _history->peer->id, _editMsgId },
_field->getTextWithTags(),
crl::guard(_list, [=] { cancelEdit(); }));
});
});
return true;
}
@ -6309,7 +6312,8 @@ std::optional<bool> HistoryWidget::cornerButtonsDownShown() {
if (!_list || _firstLoadRequest) {
return false;
}
if (_voiceRecordBar->isLockPresent()) {
if (_voiceRecordBar->isLockPresent()
|| _voiceRecordBar->isTTLButtonShown()) {
return false;
}
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 "base/call_delayed.h"
#include "base/event_filter.h"
#include "base/platform/base_platform_info.h"
#include "base/qt_signal_producer.h"
@ -2692,12 +2693,15 @@ bool ComposeControls::updateReplaceMediaButton() {
_replaceMedia = std::make_unique<Ui::IconButton>(
_wrap.get(),
st::historyReplaceMedia);
const auto hideDuration = st::historyReplaceMedia.ripple.hideDuration;
_replaceMedia->setClickedCallback([=] {
EditCaptionBox::StartMediaReplace(
_regularWindow,
_editingId,
_field->getTextWithTags(),
crl::guard(_wrap.get(), [=] { cancelEditMessage(); }));
base::call_delayed(hideDuration, _wrap.get(), [=] {
EditCaptionBox::StartMediaReplace(
_regularWindow,
_editingId,
_field->getTextWithTags(),
crl::guard(_wrap.get(), [=] { cancelEditMessage(); }));
});
});
return true;
}
@ -2921,6 +2925,10 @@ bool ComposeControls::isLockPresent() const {
return _voiceRecordBar->isLockPresent();
}
bool ComposeControls::isTTLButtonShown() const {
return _voiceRecordBar->isTTLButtonShown();
}
rpl::producer<bool> ComposeControls::lockShowStarts() const {
return _voiceRecordBar->lockShowStarts();
}

View file

@ -220,6 +220,7 @@ public:
[[nodiscard]] rpl::producer<bool> lockShowStarts() const;
[[nodiscard]] bool isLockPresent() const;
[[nodiscard]] bool isTTLButtonShown() const;
[[nodiscard]] bool isRecording() const;
[[nodiscard]] bool isRecordingPressed() 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/ripple_animation.h"
#include "ui/text/format_values.h"
#include "ui/text/text_utilities.h"
#include "ui/painter.h"
#include "ui/widgets/tooltip.h"
#include "ui/rect.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
@ -284,7 +286,6 @@ protected:
private:
const style::RecordBar &_st;
const QRect _rippleRect;
const QString _text;
Ui::Animations::Simple _activeAnimation;
@ -296,11 +297,11 @@ TTLButton::TTLButton(
: RippleButton(parent, st.lock.ripple)
, _st(st)
, _rippleRect(Rect(Size(st::historyRecordLockTopShadow.width()))
- (st::historyRecordLockRippleMargin))
, _text(u"1"_q) {
resize(Size(st::historyRecordLockTopShadow.width()));
- (st::historyRecordLockRippleMargin)) {
QWidget::resize(Size(st::historyRecordLockTopShadow.width()));
Ui::AbstractButton::setDisabled(true);
setClickedCallback([=] {
Ui::AbstractButton::setClickedCallback([=] {
Ui::AbstractButton::setDisabled(!Ui::AbstractButton::isDisabled());
const auto isActive = !Ui::AbstractButton::isDisabled();
_activeAnimation.start(
@ -310,6 +311,77 @@ TTLButton::TTLButton(
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(
) | rpl::start_with_next([=](const QRect &clip) {
auto p = QPainter(this);
@ -318,49 +390,16 @@ TTLButton::TTLButton(
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(
!Ui::AbstractButton::isDisabled() ? 1 : 0);
p.setOpacity(1. - activeProgress);
st::historyRecordVoiceOnceInactive.paintInCenter(p, inner);
if (activeProgress) {
p.setOpacity(activeProgress);
pen.setStyle(Qt::SolidLine);
pen.setBrush(st::windowBgActive);
p.setPen(pen);
p.setBrush(pen.brush());
p.drawEllipse(innerRect);
p.setPen(st::windowFgActive);
p.drawText(innerRect, _text, style::al_center);
st::historyRecordVoiceOnceBg.paintInCenter(p, inner);
st::historyRecordVoiceOnceFg.paintInCenter(p, inner);
}
}, lifetime());
@ -368,7 +407,7 @@ TTLButton::TTLButton(
void TTLButton::clearState() {
Ui::AbstractButton::setDisabled(true);
update();
QWidget::update();
Ui::RpWidget::hide();
}
@ -1136,7 +1175,6 @@ VoiceRecordBar::VoiceRecordBar(
, _show(std::move(descriptor.show))
, _send(std::move(descriptor.send))
, _lock(std::make_unique<RecordLock>(_outerContainer, _st.lock))
, _ttlButton(std::make_unique<TTLButton>(_outerContainer, _st))
, _level(std::make_unique<VoiceRecordButton>(_outerContainer, _st))
, _cancel(std::make_unique<CancelButton>(this, _st, descriptor.recorderHeight))
, _startTimer([=] { startRecording(); })
@ -1215,6 +1253,9 @@ void VoiceRecordBar::updateLockGeometry() {
void VoiceRecordBar::updateTTLGeometry(
TTLAnimationType type,
float64 progress) {
if (!_ttlButton) {
return;
}
const auto parent = parentWidget();
const auto me = Ui::MapFrom(_outerContainer, parent, geometry());
const auto anyTop = me.y() - st::historyRecordLockPosition.y();
@ -1364,6 +1405,7 @@ void VoiceRecordBar::init() {
}
updateTTLGeometry(TTLAnimationType::TopBottom, 1. - value);
};
_showListenAnimation.stop();
_showListenAnimation.start(std::move(callback), 0., to, duration);
}, lifetime());
@ -1373,6 +1415,11 @@ void VoiceRecordBar::init() {
_lock->locks(
) | rpl::start_with_next([=] {
if (_hasTTLFilter && _hasTTLFilter()) {
if (!_ttlButton) {
_ttlButton = std::make_unique<TTLButton>(
_outerContainer,
_st);
}
_ttlButton->show();
}
updateTTLGeometry(TTLAnimationType::RightTopStatic, 0);
@ -1495,13 +1542,18 @@ void VoiceRecordBar::setTTLFilter(FilterCallback &&callback) {
}
void VoiceRecordBar::initLockGeometry() {
rpl::combine(
_lock->heightValue(),
geometryValue(),
static_cast<Ui::RpWidget*>(parentWidget())->geometryValue()
const auto parent = static_cast<Ui::RpWidget*>(parentWidget());
rpl::merge(
_lock->heightValue() | rpl::to_empty,
geometryValue() | rpl::to_empty,
parent->geometryValue() | rpl::to_empty
) | rpl::start_with_next([=] {
updateLockGeometry();
}, lifetime());
parent->geometryValue(
) | rpl::start_with_next([=] {
updateTTLGeometry(TTLAnimationType::RightLeft, 1.);
}, lifetime());
}
void VoiceRecordBar::initLevelGeometry() {
@ -1600,10 +1652,12 @@ void VoiceRecordBar::stop(bool send) {
if (isHidden() && !send) {
return;
}
const auto ttlBeforeHide = peekTTLState();
auto disappearanceCallback = [=] {
hide();
stopRecording(send ? StopType::Send : StopType::Cancel);
const auto type = send ? StopType::Send : StopType::Cancel;
stopRecording(type, ttlBeforeHide);
};
_lockShowing = false;
visibilityAnimate(false, std::move(disappearanceCallback));
@ -1621,6 +1675,8 @@ void VoiceRecordBar::finish() {
_listen = nullptr;
[[maybe_unused]] const auto s = takeTTLState();
_sendActionUpdates.fire({ Api::SendProgressType::RecordVoice, -1 });
}
@ -1631,7 +1687,7 @@ void VoiceRecordBar::hideFast() {
[[maybe_unused]] const auto s = takeTTLState();
}
void VoiceRecordBar::stopRecording(StopType type) {
void VoiceRecordBar::stopRecording(StopType type, bool ttlBeforeHide) {
using namespace ::Media::Capture;
if (type == StopType::Cancel) {
instance()->stop(crl::guard(this, [=](Result &&data) {
@ -1652,9 +1708,9 @@ void VoiceRecordBar::stopRecording(StopType type) {
if (type == StopType::Send) {
const auto options = Api::SendOptions{
.ttlSeconds = takeTTLState()
.ttlSeconds = (ttlBeforeHide
? std::numeric_limits<int>::max()
: 0
: 0),
};
auto settings = &AyuSettings::getInstance();
@ -1861,6 +1917,10 @@ bool VoiceRecordBar::isRecordingByAnotherBar() const {
return !isRecording() && ::Media::Capture::instance()->started();
}
bool VoiceRecordBar::isTTLButtonShown() const {
return _ttlButton && !_ttlButton->isHidden();
}
bool VoiceRecordBar::hasDuration() const {
return _recordingSamples > 0;
}
@ -1892,7 +1952,14 @@ void VoiceRecordBar::computeAndSetLockProgress(QPoint globalPos) {
_lock->requestPaintProgress(Progress(localPos.y(), higher - lower));
}
bool VoiceRecordBar::peekTTLState() const {
return _ttlButton && !_ttlButton->isDisabled();
}
bool VoiceRecordBar::takeTTLState() const {
if (!_ttlButton) {
return false;
}
const auto hasTtl = !_ttlButton->isDisabled();
_ttlButton->clearState();
return hasTtl;

View file

@ -98,6 +98,7 @@ public:
[[nodiscard]] bool isListenState() const;
[[nodiscard]] bool isActive() const;
[[nodiscard]] bool isRecordingByAnotherBar() const;
[[nodiscard]] bool isTTLButtonShown() const;
private:
enum class StopType {
@ -125,7 +126,7 @@ private:
[[nodiscard]] bool recordingAnimationCallback(crl::time now);
void stop(bool send);
void stopRecording(StopType type);
void stopRecording(StopType type, bool ttlBeforeHide = false);
void visibilityAnimate(bool show, Fn<void()> &&callback);
[[nodiscard]] bool showRecordButton() const;
@ -148,6 +149,7 @@ private:
void computeAndSetLockProgress(QPoint globalPos);
[[nodiscard]] bool peekTTLState() const;
[[nodiscard]] bool takeTTLState() const;
const style::RecordBar &_st;
@ -155,9 +157,9 @@ private:
const std::shared_ptr<ChatHelpers::Show> _show;
const std::shared_ptr<Ui::SendButton> _send;
const std::unique_ptr<RecordLock> _lock;
const std::unique_ptr<Ui::AbstractButton> _ttlButton;
const std::unique_ptr<VoiceRecordButton> _level;
const std::unique_ptr<CancelButton> _cancel;
std::unique_ptr<Ui::AbstractButton> _ttlButton;
std::unique_ptr<ListenWrap> _listen;
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/reactions/history_view_reactions_list.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/image/image.h"
#include "ui/toast/toast.h"
@ -1318,6 +1320,62 @@ void AddWhoReactedAction(
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(
not_null<base::unique_qptr<Ui::PopupMenu>*> menu,
QPoint position,
@ -1326,6 +1384,11 @@ void ShowWhoReactedMenu(
const Data::ReactionId &id,
not_null<Window::SessionController*> controller,
rpl::lifetime &lifetime) {
if (item->reactionsAreTags()) {
ShowTagMenu(menu, position, context, item, id, controller);
return;
}
struct State {
int addedToBottom = 0;
};

View file

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

View file

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

View file

@ -1984,6 +1984,7 @@ bool Message::hasFromPhoto() const {
case Context::AdminLog:
return true;
case Context::History:
case Context::TTLViewer:
case Context::Pinned:
case Context::Replies:
case Context::SavedSublist: {
@ -2917,8 +2918,12 @@ bool Message::isSignedAuthorElided() const {
bool Message::embedReactionsInBottomInfo() const {
const auto item = data();
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.
// In saved messages we use reactions for tags, we don't embed them.
return false;
}
auto seenMy = false;
@ -2961,8 +2966,14 @@ void Message::refreshReactions() {
if (!_reactions) {
const auto handlerFactory = [=](ReactionId id) {
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 (strong->data()->reactionsAreTags()) {
const auto tag = Data::SearchTagToQuery(id);
HashtagClickHandler(tag).onClick(context);
return;
}
strong->data()->toggleReaction(
id,
HistoryItem::ReactionSource::Existing);
@ -3160,6 +3171,7 @@ bool Message::hasFromName() const {
case Context::AdminLog:
return true;
case Context::History:
case Context::TTLViewer:
case Context::Pinned:
case Context::Replies:
case Context::SavedSublist: {
@ -3192,7 +3204,7 @@ bool Message::hasFromName() const {
case Context::ContactPreview:
return false;
}
Unexpected("Context in Message::hasFromPhoto.");
Unexpected("Context in Message::hasFromName.");
}
bool Message::displayFromName() const {

View file

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

View file

@ -843,7 +843,8 @@ bool ScheduledWidget::cornerButtonsIgnoreVisibility() {
}
std::optional<bool> ScheduledWidget::cornerButtonsDownShown() {
if (_composeControls->isLockPresent()) {
if (_composeControls->isLockPresent()
|| _composeControls->isTTLButtonShown()) {
return false;
}
const auto top = _scroll->scrollTop() + st::historyToDownShownAfter;
@ -857,7 +858,8 @@ std::optional<bool> ScheduledWidget::cornerButtonsDownShown() {
bool ScheduledWidget::cornerButtonsUnreadMayBeShown() {
return _inner->loadedAtBottomKnown()
&& !_composeControls->isLockPresent();
&& !_composeControls->isLockPresent()
&& !_composeControls->isTTLButtonShown();
}
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 "main/main_session.h"
#include "core/application.h"
#include "core/shortcuts.h"
#include "data/data_saved_messages.h"
#include "data/data_saved_sublist.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_item.h"
#include "lang/lang_keys.h"
#include "mainwidget.h"
#include "ui/chat/chat_style.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/scroll_area.h"
@ -115,6 +118,10 @@ SublistWidget::SublistWidget(
) | rpl::start_with_next([=] {
clearSelected();
}, _topBar->lifetime());
_topBar->searchRequest(
) | rpl::start_with_next([=] {
searchInSublist();
}, _topBar->lifetime());
_translateBar->raise();
_topBarShadow->raise();
@ -134,6 +141,7 @@ SublistWidget::SublistWidget(
onScroll();
}, lifetime());
setupShortcuts();
setupTranslateBar();
}
@ -658,4 +666,24 @@ void SublistWidget::clearSelected() {
_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

View file

@ -167,11 +167,13 @@ private:
void setupOpenChatButton();
void setupAboutHiddenAuthor();
void setupTranslateBar();
void setupShortcuts();
void confirmDeleteSelected();
void confirmForwardSelected();
void clearSelected();
void recountChatWidth();
void searchInSublist();
const not_null<Data::SavedSublist*> _sublist;
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 "chat_helpers/emoji_interactions.h"
#include "base/unixtime.h"
#include "base/event_filter.h"
#include "support/support_helper.h"
#include "apiwrap.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_menu_icons.h"
#include <QtGui/QWindow>
// AyuGram includes
#include "ayu/ayu_settings.h"
#include "data/data_chat_filters.h"
@ -235,6 +238,16 @@ TopBarWidget::TopBarWidget(
updateConnectingState();
}, 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);
}
@ -246,7 +259,8 @@ Main::Session &TopBarWidget::session() const {
void TopBarWidget::updateConnectingState() {
const auto state = _controller->session().mtp().dcstate();
if (state == MTP::ConnectedState) {
const auto exposed = window()->windowHandle()->isExposed();
if (state == MTP::ConnectedState || !exposed) {
if (_connecting) {
_connecting = nullptr;
update();
@ -926,7 +940,9 @@ int TopBarWidget::countSelectedButtonsTop(float64 selectedShown) {
void TopBarWidget::updateSearchVisibility() {
const auto searchAllowedMode = (_activeChat.section == Section::History)
|| (_activeChat.section == Section::Replies
&& _activeChat.key.topic());
&& _activeChat.key.topic())
|| (_activeChat.section == Section::SavedSublist
&& _activeChat.key.sublist());
_search->setVisible(searchAllowedMode && !_chooseForReportReason);
}

View file

@ -45,52 +45,18 @@ namespace {
constexpr auto kAudioVoiceMsgUpdateView = crl::time(100);
void DrawCornerBadgeTTL(
QPainter &p,
const style::color &bg,
const style::color &fg,
const QRect &circleRect) {
p.save();
const auto partRect = QRectF(
rect::right(circleRect)
[[nodiscard]] QRect TTLRectFromInner(const QRect &inner) {
return QRect(
rect::right(inner)
- st::dialogsTTLBadgeSize
+ rect::m::sum::h(st::dialogsTTLBadgeInnerMargins),
rect::bottom(circleRect)
+ rect::m::sum::h(st::dialogsTTLBadgeInnerMargins)
- st::dialogsTTLBadgeSkip.x(),
rect::bottom(inner)
- st::dialogsTTLBadgeSize
+ rect::m::sum::v(st::dialogsTTLBadgeInnerMargins),
+ rect::m::sum::v(st::dialogsTTLBadgeInnerMargins)
- st::dialogsTTLBadgeSkip.y(),
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(
@ -99,43 +65,54 @@ void DrawCornerBadgeTTL(
struct State final {
std::unique_ptr<Lottie::Icon> start;
std::unique_ptr<Lottie::Icon> idle;
bool started = false;
};
const auto iconSize = Size(std::min(
st::historyFileInPause.width(),
st::historyFileInPause.height()));
const auto state = lifetime->make_state<State>();
state->start = Lottie::MakeIcon({
.name = u"voice_ttl_start"_q,
//state->start = Lottie::MakeIcon({
// .name = u"voice_ttl_start"_q,
// .color = &st::historyFileInIconFg,
// .sizeOverride = iconSize,
//});
state->idle = Lottie::MakeIcon({
.name = u"voice_ttl_idle"_q,
.color = &st::historyFileInIconFg,
.sizeOverride = iconSize,
});
const auto animateSingle = [=](
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);
});
const auto weak = std::weak_ptr(lifetime);
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 VoiceData *voiceData,
int availableWidth,
float64 progress) {
float64 progress,
bool ttl) {
const auto wf = [&]() -> const VoiceWaveform* {
if (!voiceData) {
return nullptr;
@ -226,11 +204,14 @@ void PaintWaveform(
}
return &voiceData->waveform;
}();
if (ttl) {
progress = 1. - progress;
}
const auto stm = context.messageStyle();
// Rescale waveform by going in waveform.size * bar_count 1D grid.
const auto active = stm->msgWaveformActive;
const auto inactive = stm->msgWaveformInactive;
const auto inactive = ttl ? stm->msgBg : stm->msgWaveformInactive;
const auto wfSize = wf
? int(wf->size())
: ::Media::Player::kWaveformSamplesCount;
@ -267,10 +248,12 @@ void PaintWaveform(
p.fillRect(
QRectF(barLeft, barTop, leftWidth, barHeight),
active);
p.fillRect(
QRectF(activeWidth, barTop, rightWidth, barHeight),
inactive);
} else {
if (!ttl) {
p.fillRect(
QRectF(activeWidth, barTop, rightWidth, barHeight),
inactive);
}
} else if (!ttl || barLeft < activeWidth) {
const auto &color = (barLeft >= activeWidth) ? inactive : active;
p.fillRect(QRectF(barLeft, barTop, barWidth, barHeight), color);
}
@ -326,41 +309,38 @@ Document::Document(
}
if ((_data->isVoiceMessage() || isRound)
&& IsVoiceOncePlayable(_parent->data())) {
_parent->data()->removeFromSharedMediaIndex();
setDocumentLinks(_data, realParent, [=] {
_openl = nullptr;
&& _parent->data()->media()->ttlSeconds()) {
const auto fullId = _realParent->fullId();
if (_parent->delegate()->elementContext() == Context::TTLViewer) {
auto lifetime = std::make_shared<rpl::lifetime>();
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() != _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();
TTLVoiceStops(fullId) | rpl::start_with_next([=]() mutable {
if (lifetime) {
base::take(lifetime)->destroy();
}
// Destroys this.
ClearMediaAsExpired(item);
}, *lifetime);
_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 {
setDocumentLinks(_data, realParent);
}
@ -427,7 +407,7 @@ void Document::createComponents(bool caption) {
_realParent->fullId());
}
if (const auto voice = Get<HistoryDocumentVoice>()) {
voice->seekl = !IsVoiceOncePlayable(_parent->data())
voice->seekl = !_parent->data()->media()->ttlSeconds()
? std::make_shared<VoiceSeekClickHandler>(_data, [](FullMsgId) {})
: nullptr;
if (_transcribedRound) {
@ -715,6 +695,11 @@ void Document::draw(
} else {
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()
&& DrawThumbnailAsSongCover(
p,
@ -739,9 +724,12 @@ void Document::draw(
}
}
} else {
PainterHighQualityEnabler hq(p);
auto hq = PainterHighQualityEnabler(p);
p.setBrush(stm->msgFileBg);
p.drawEllipse(inner);
if (hasTtlBadge) {
p.drawEllipse(ttlRect);
}
}
}
@ -778,8 +766,28 @@ void Document::draw(
: nullptr;
const auto paintContent = [&](QPainter &q) {
constexpr auto kPenWidth = 1.5;
if (_drawTtl) {
_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.) {
PaintInterpolatedIcon(q, icon, *previous, radialOpacity, inner);
} else {
@ -790,6 +798,17 @@ void Document::draw(
QRect rinner(inner.marginsRemoved(QMargins(st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine)));
_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)) {
paintContent(p);
@ -798,7 +817,7 @@ void Document::draw(
p,
context.viewport,
context.bubblesPattern->pixmap,
inner,
hasTtlBadge ? inner.united(ttlRect) : inner,
paintContent,
_iconCache);
}
@ -855,11 +874,14 @@ void Document::draw(
if (_transcribedRound) {
FillWaveform(_data->round());
}
const auto inTTLViewer = _parent->delegate()->elementContext()
== Context::TTLViewer;
PaintWaveform(p,
context,
_transcribedRound ? _data->round() : _data->voice(),
namewidth + st::msgWaveformSkip,
progress);
progress,
inTTLViewer);
p.restore();
} else if (auto named = Get<HistoryDocumentNamed>()) {
p.setFont(st::semiboldFont);
@ -918,12 +940,6 @@ void Document::draw(
.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(
@ -1259,18 +1275,22 @@ TextState Document::textState(
void Document::updatePressed(QPoint point) {
// LayoutMode should be passed here.
if (const auto voice = Get<HistoryDocumentVoice>()) {
if (voice->seeking()) {
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();
voice->setSeekingCurrent(std::clamp(
(point.x() - nameleft)
/ float64(width() - nameleft - nameright),
0.,
1.));
repaint();
if (!voice->seeking()) {
return;
}
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;
}
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

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