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
1
.github/workflows/mac_packaged.yml
vendored
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
4
Telegram/Resources/art/ttl/video_message_icon.svg
Normal 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 |
BIN
Telegram/Resources/icons/chat/audio_once.png
Normal file
After Width: | Height: | Size: 776 B |
BIN
Telegram/Resources/icons/chat/audio_once@2x.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
Telegram/Resources/icons/chat/audio_once@3x.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
Telegram/Resources/icons/chat/mini_media_once.png
Normal file
After Width: | Height: | Size: 561 B |
BIN
Telegram/Resources/icons/chat/mini_media_once@2x.png
Normal file
After Width: | Height: | Size: 1,011 B |
BIN
Telegram/Resources/icons/chat/mini_media_once@3x.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
Telegram/Resources/icons/voice_lock/audio_once_bg.png
Normal file
After Width: | Height: | Size: 496 B |
BIN
Telegram/Resources/icons/voice_lock/audio_once_bg@2x.png
Normal file
After Width: | Height: | Size: 895 B |
BIN
Telegram/Resources/icons/voice_lock/audio_once_bg@3x.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
Telegram/Resources/icons/voice_lock/audio_once_number.png
Normal file
After Width: | Height: | Size: 269 B |
BIN
Telegram/Resources/icons/voice_lock/audio_once_number@2x.png
Normal file
After Width: | Height: | Size: 389 B |
BIN
Telegram/Resources/icons/voice_lock/audio_once_number@3x.png
Normal file
After Width: | Height: | Size: 529 B |
BIN
Telegram/Resources/icons/voice_lock/recorded_delete.png
Normal file
After Width: | Height: | Size: 614 B |
BIN
Telegram/Resources/icons/voice_lock/recorded_delete@2x.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
Telegram/Resources/icons/voice_lock/recorded_delete@3x.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
|
@ -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.";
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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>(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
392
Telegram/SourceFiles/chat_helpers/ttl_media_layer_widget.cpp
Normal 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
|
22
Telegram/SourceFiles/chat_helpers/ttl_media_layer_widget.h
Normal 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
|
|
@ -360,7 +360,7 @@ void Application::run() {
|
|||
startDomain();
|
||||
startTray();
|
||||
|
||||
_lastActivePrimaryWindow->widget()->show();
|
||||
_lastActivePrimaryWindow->firstShow();
|
||||
|
||||
startMediaView();
|
||||
|
||||
|
|
|
@ -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!"));
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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{}
|
||||
|
|
|
@ -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 {};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -623,3 +623,6 @@ dialogsStoriesTooltipHide: IconButton(defaultIconButton) {
|
|||
searchedBarHeight: 32px;
|
||||
searchedBarFont: normalFont;
|
||||
searchedBarPosition: point(17px, 7px);
|
||||
|
||||
dialogsSearchTagSkip: point(8px, 4px);
|
||||
dialogsSearchTagBottom: 10px;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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<
|
||||
|
|
269
Telegram/SourceFiles/dialogs/dialogs_search_tags.cpp
Normal 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
|
80
Telegram/SourceFiles/dialogs/dialogs_search_tags.h
Normal 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
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -58,6 +58,7 @@ enum class Context : char {
|
|||
AdminLog,
|
||||
ContactPreview,
|
||||
SavedSublist,
|
||||
TTLViewer,
|
||||
};
|
||||
|
||||
enum class OnlyEmojiAndSpaces : char {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|