diff --git a/.github/workflows/mac_packaged.yml b/.github/workflows/mac_packaged.yml index 631ff2a70..51ba9917a 100644 --- a/.github/workflows/mac_packaged.yml +++ b/.github/workflows/mac_packaged.yml @@ -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 diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 154b4a02c..462e737c6 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -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 diff --git a/Telegram/Resources/art/ttl/video_message_icon.svg b/Telegram/Resources/art/ttl/video_message_icon.svg new file mode 100644 index 000000000..aeadc8385 --- /dev/null +++ b/Telegram/Resources/art/ttl/video_message_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Telegram/Resources/icons/chat/audio_once.png b/Telegram/Resources/icons/chat/audio_once.png new file mode 100644 index 000000000..4d1f93652 Binary files /dev/null and b/Telegram/Resources/icons/chat/audio_once.png differ diff --git a/Telegram/Resources/icons/chat/audio_once@2x.png b/Telegram/Resources/icons/chat/audio_once@2x.png new file mode 100644 index 000000000..f66cfbbcb Binary files /dev/null and b/Telegram/Resources/icons/chat/audio_once@2x.png differ diff --git a/Telegram/Resources/icons/chat/audio_once@3x.png b/Telegram/Resources/icons/chat/audio_once@3x.png new file mode 100644 index 000000000..bce2481c5 Binary files /dev/null and b/Telegram/Resources/icons/chat/audio_once@3x.png differ diff --git a/Telegram/Resources/icons/chat/mini_media_once.png b/Telegram/Resources/icons/chat/mini_media_once.png new file mode 100644 index 000000000..8984491b7 Binary files /dev/null and b/Telegram/Resources/icons/chat/mini_media_once.png differ diff --git a/Telegram/Resources/icons/chat/mini_media_once@2x.png b/Telegram/Resources/icons/chat/mini_media_once@2x.png new file mode 100644 index 000000000..f8514d3af Binary files /dev/null and b/Telegram/Resources/icons/chat/mini_media_once@2x.png differ diff --git a/Telegram/Resources/icons/chat/mini_media_once@3x.png b/Telegram/Resources/icons/chat/mini_media_once@3x.png new file mode 100644 index 000000000..3900061be Binary files /dev/null and b/Telegram/Resources/icons/chat/mini_media_once@3x.png differ diff --git a/Telegram/Resources/icons/voice_lock/audio_once_bg.png b/Telegram/Resources/icons/voice_lock/audio_once_bg.png new file mode 100644 index 000000000..62778676c Binary files /dev/null and b/Telegram/Resources/icons/voice_lock/audio_once_bg.png differ diff --git a/Telegram/Resources/icons/voice_lock/audio_once_bg@2x.png b/Telegram/Resources/icons/voice_lock/audio_once_bg@2x.png new file mode 100644 index 000000000..225d6af3b Binary files /dev/null and b/Telegram/Resources/icons/voice_lock/audio_once_bg@2x.png differ diff --git a/Telegram/Resources/icons/voice_lock/audio_once_bg@3x.png b/Telegram/Resources/icons/voice_lock/audio_once_bg@3x.png new file mode 100644 index 000000000..1d82b2d6e Binary files /dev/null and b/Telegram/Resources/icons/voice_lock/audio_once_bg@3x.png differ diff --git a/Telegram/Resources/icons/voice_lock/audio_once_number.png b/Telegram/Resources/icons/voice_lock/audio_once_number.png new file mode 100644 index 000000000..61e22f105 Binary files /dev/null and b/Telegram/Resources/icons/voice_lock/audio_once_number.png differ diff --git a/Telegram/Resources/icons/voice_lock/audio_once_number@2x.png b/Telegram/Resources/icons/voice_lock/audio_once_number@2x.png new file mode 100644 index 000000000..15047cb48 Binary files /dev/null and b/Telegram/Resources/icons/voice_lock/audio_once_number@2x.png differ diff --git a/Telegram/Resources/icons/voice_lock/audio_once_number@3x.png b/Telegram/Resources/icons/voice_lock/audio_once_number@3x.png new file mode 100644 index 000000000..6e39511e1 Binary files /dev/null and b/Telegram/Resources/icons/voice_lock/audio_once_number@3x.png differ diff --git a/Telegram/Resources/icons/voice_lock/recorded_delete.png b/Telegram/Resources/icons/voice_lock/recorded_delete.png new file mode 100644 index 000000000..f204f8f22 Binary files /dev/null and b/Telegram/Resources/icons/voice_lock/recorded_delete.png differ diff --git a/Telegram/Resources/icons/voice_lock/recorded_delete@2x.png b/Telegram/Resources/icons/voice_lock/recorded_delete@2x.png new file mode 100644 index 000000000..8bebe1a61 Binary files /dev/null and b/Telegram/Resources/icons/voice_lock/recorded_delete@2x.png differ diff --git a/Telegram/Resources/icons/voice_lock/recorded_delete@3x.png b/Telegram/Resources/icons/voice_lock/recorded_delete@3x.png new file mode 100644 index 000000000..db0067c53 Binary files /dev/null and b/Telegram/Resources/icons/voice_lock/recorded_delete@3x.png differ diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 97b30588e..c6570fe5b 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -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."; diff --git a/Telegram/Resources/qrc/telegram/telegram.qrc b/Telegram/Resources/qrc/telegram/telegram.qrc index f2f085027..38e9f48e4 100644 --- a/Telegram/Resources/qrc/telegram/telegram.qrc +++ b/Telegram/Resources/qrc/telegram/telegram.qrc @@ -26,6 +26,7 @@ ../../art/recording/recording_info_audio.svg ../../art/recording/recording_info_video_landscape.svg ../../art/recording/recording_info_video_portrait.svg + ../../art/ttl/video_message_icon.svg ../../icons/settings/dino.svg ../../icons/settings/star.svg ../../icons/settings/starmini.svg diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml index bc517dc13..732d62eb2 100644 --- a/Telegram/Resources/uwp/AppX/AppxManifest.xml +++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml @@ -10,7 +10,7 @@ + Version="4.14.6.0" /> Telegram Desktop Telegram Messenger LLP diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc index f30626251..169be97b2 100644 --- a/Telegram/Resources/winrc/Telegram.rc +++ b/Telegram/Resources/winrc/Telegram.rc @@ -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" diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc index 014686458..6aca907d9 100644 --- a/Telegram/Resources/winrc/Updater.rc +++ b/Telegram/Resources/winrc/Updater.rc @@ -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" diff --git a/Telegram/SourceFiles/api/api_messages_search.cpp b/Telegram/SourceFiles/api/api_messages_search.cpp index 15119b906..cc97e22c0 100644 --- a/Telegram/SourceFiles/api/api_messages_search.cpp +++ b/Telegram/SourceFiles/api/api_messages_search.cpp @@ -91,6 +91,7 @@ void MessagesSearch::searchRequest() { ? _from->input : MTP_inputPeerEmpty()), MTPInputPeer(), // saved_peer_id + MTPVector(), // saved_reaction MTPint(), // top_msg_id MTP_inputMessagesFilterEmpty(), MTP_int(0), // min_date diff --git a/Telegram/SourceFiles/api/api_sending.cpp b/Telegram/SourceFiles/api/api_sending.cpp index d971a03f4..0e5d7c585 100644 --- a/Telegram/SourceFiles/api/api_sending.cpp +++ b/Telegram/SourceFiles/api/api_sending.cpp @@ -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; diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index 897a78d72..c862cc9ee 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -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); diff --git a/Telegram/SourceFiles/boxes/auto_download_box.cpp b/Telegram/SourceFiles/boxes/auto_download_box.cpp index ae42f3cc8..3617a9eb6 100644 --- a/Telegram/SourceFiles/boxes/auto_download_box.cpp +++ b/Telegram/SourceFiles/boxes/auto_download_box.cpp @@ -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); }); diff --git a/Telegram/SourceFiles/boxes/connection_box.cpp b/Telegram/SourceFiles/boxes/connection_box.cpp index 3bdaa932d..4403f56fa 100644 --- a/Telegram/SourceFiles/boxes/connection_box.cpp +++ b/Telegram/SourceFiles/boxes/connection_box.cpp @@ -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( diff --git a/Telegram/SourceFiles/boxes/language_box.cpp b/Telegram/SourceFiles/boxes/language_box.cpp index ab709a87d..aa69a6fee 100644 --- a/Telegram/SourceFiles/boxes/language_box.cpp +++ b/Telegram/SourceFiles/boxes/language_box.cpp @@ -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(this, recent, official), st::boxScroll, diff --git a/Telegram/SourceFiles/boxes/share_box.cpp b/Telegram/SourceFiles/boxes/share_box.cpp index d1b3dbb37..007355317 100644 --- a/Telegram/SourceFiles/boxes/share_box.cpp +++ b/Telegram/SourceFiles/boxes/share_box.cpp @@ -882,7 +882,7 @@ auto ShareBox::Inner::getChat(not_null 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(peer, _st.item, [=] { repaintChat(peer); })); updateChatName(i->second.get()); diff --git a/Telegram/SourceFiles/calls/calls_box_controller.cpp b/Telegram/SourceFiles/calls/calls_box_controller.cpp index 9eb53580f..11c6347d5 100644 --- a/Telegram/SourceFiles/calls/calls_box_controller.cpp +++ b/Telegram/SourceFiles/calls/calls_box_controller.cpp @@ -521,6 +521,7 @@ void BoxController::loadMoreRows() { MTP_string(), // q MTP_inputPeerEmpty(), MTPInputPeer(), // saved_peer_id + MTPVector(), // saved_reaction MTPint(), // top_msg_id MTP_inputMessagesFilterPhoneCalls(MTP_flags(0)), MTP_int(0), // min_date diff --git a/Telegram/SourceFiles/calls/calls_top_bar.cpp b/Telegram/SourceFiles/calls/calls_top_bar.cpp index 17f0008e8..6a3314578 100644 --- a/Telegram/SourceFiles/calls/calls_top_bar.cpp +++ b/Telegram/SourceFiles/calls/calls_top_bar.cpp @@ -267,7 +267,7 @@ TopBar::TopBar( ? object_ptr( this, st::callBarLabel, - tr::lng_call_bar_hangup(tr::now).toUpper()) + tr::lng_call_bar_hangup(tr::now)) : object_ptr(nullptr)) , _mute(this, st::callBarMuteToggle) , _info(this) diff --git a/Telegram/SourceFiles/calls/calls_video_bubble.cpp b/Telegram/SourceFiles/calls/calls_video_bubble.cpp index aecabfd55..043ee8c41 100644 --- a/Telegram/SourceFiles/calls/calls_video_bubble.cpp +++ b/Telegram/SourceFiles/calls/calls_video_bubble.cpp @@ -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) { diff --git a/Telegram/SourceFiles/calls/group/calls_group_call.cpp b/Telegram/SourceFiles/calls/group/calls_group_call.cpp index 49dd3db91..5ec38443d 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_call.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_call.cpp @@ -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; diff --git a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp index d3bac6aeb..6455ad040 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp @@ -1060,11 +1060,13 @@ void Panel::setupVideo(not_null 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); diff --git a/Telegram/SourceFiles/calls/group/calls_group_viewport.cpp b/Telegram/SourceFiles/calls/group/calls_group_viewport.cpp index 48c76d0ae..9623171b9 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_viewport.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_viewport.cpp @@ -237,13 +237,15 @@ void Viewport::add( const VideoEndpoint &endpoint, VideoTileTrack track, rpl::producer trackSize, - rpl::producer pinned) { + rpl::producer pinned, + bool self) { _tiles.push_back(std::make_unique( endpoint, track, std::move(trackSize), std::move(pinned), - [=] { widget()->update(); })); + [=] { widget()->update(); }, + self)); _tiles.back()->trackSizeValue( ) | rpl::filter([](QSize size) { diff --git a/Telegram/SourceFiles/calls/group/calls_group_viewport.h b/Telegram/SourceFiles/calls/group/calls_group_viewport.h index c6567e037..9d5021f85 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_viewport.h +++ b/Telegram/SourceFiles/calls/group/calls_group_viewport.h @@ -80,7 +80,8 @@ public: const VideoEndpoint &endpoint, VideoTileTrack track, rpl::producer trackSize, - rpl::producer pinned); + rpl::producer pinned, + bool self); void remove(const VideoEndpoint &endpoint); void showLarge(const VideoEndpoint &endpoint); diff --git a/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.cpp b/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.cpp index 946c6dca8..e11644e68 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.cpp @@ -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(), diff --git a/Telegram/SourceFiles/calls/group/calls_group_viewport_raster.cpp b/Telegram/SourceFiles/calls/group/calls_group_viewport_raster.cpp index f85929ea1..ab335c55c 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_viewport_raster.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_viewport_raster.cpp @@ -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()); diff --git a/Telegram/SourceFiles/calls/group/calls_group_viewport_tile.cpp b/Telegram/SourceFiles/calls/group/calls_group_viewport_tile.cpp index 58ac0fb9d..1b31a2012 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_viewport_tile.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_viewport_tile.cpp @@ -28,12 +28,14 @@ Viewport::VideoTile::VideoTile( VideoTileTrack track, rpl::producer trackSize, rpl::producer pinned, - Fn update) + Fn 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; } diff --git a/Telegram/SourceFiles/calls/group/calls_group_viewport_tile.h b/Telegram/SourceFiles/calls/group/calls_group_viewport_tile.h index 445bf2d7e..6f85f4a01 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_viewport_tile.h +++ b/Telegram/SourceFiles/calls/group/calls_group_viewport_tile.h @@ -28,7 +28,8 @@ public: VideoTileTrack track, rpl::producer trackSize, rpl::producer pinned, - Fn update); + Fn update, + bool self); [[nodiscard]] not_null 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 _quality; rpl::lifetime _lifetime; diff --git a/Telegram/SourceFiles/calls/group/ui/desktop_capture_choose_source.cpp b/Telegram/SourceFiles/calls/group/ui/desktop_capture_choose_source.cpp index 87ac643d3..ad59290c5 100644 --- a/Telegram/SourceFiles/calls/group/ui/desktop_capture_choose_source.cpp +++ b/Telegram/SourceFiles/calls/group/ui/desktop_capture_choose_source.cpp @@ -114,6 +114,7 @@ private: const not_null _finish; const not_null _withAudio; + QSize _fixedSize; std::vector> _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); diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index c86e98b75..99f6f9900 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -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; diff --git a/Telegram/SourceFiles/chat_helpers/emoji_sets_manager.cpp b/Telegram/SourceFiles/chat_helpers/emoji_sets_manager.cpp index 7c6ae96c8..fed24fc5b 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_sets_manager.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_sets_manager.cpp @@ -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); diff --git a/Telegram/SourceFiles/chat_helpers/stickers_gift_box_pack.cpp b/Telegram/SourceFiles/chat_helpers/stickers_gift_box_pack.cpp index e20c361d6..f54b1f1d3 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_gift_box_pack.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_gift_box_pack.cpp @@ -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; diff --git a/Telegram/SourceFiles/chat_helpers/ttl_media_layer_widget.cpp b/Telegram/SourceFiles/chat_helpers/ttl_media_layer_widget.cpp new file mode 100644 index 000000000..f2445a166 --- /dev/null +++ b/Telegram/SourceFiles/chat_helpers/ttl_media_layer_widget.cpp @@ -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 parent, + not_null st, + rpl::producer chatWideValue, + Fn update); + + bool elementAnimationsPaused() override; + not_null elementPathShiftGradient() override; + HistoryView::Context elementContext() override; + bool elementIsChatWide() override; + +private: + const not_null _parent; + const std::unique_ptr _pathGradient; + rpl::variable _chatWide; + +}; + +PreviewDelegate::PreviewDelegate( + not_null parent, + not_null st, + rpl::producer chatWideValue, + Fn update) +: _parent(parent) +, _pathGradient(HistoryView::MakePathShiftGradient(st, update)) +, _chatWide(std::move(chatWideValue)) { +} + +bool PreviewDelegate::elementAnimationsPaused() { + return _parent->window()->isActiveWindow(); +} + +not_null 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 parent, + not_null item, + rpl::producer viewportValue, + rpl::producer chatWideValue, + rpl::producer> theme); + ~PreviewWrap(); + + [[nodiscard]] rpl::producer<> closeRequests() const; + +private: + void paintEvent(QPaintEvent *e) override; + void createView(); + [[nodiscard]] bool goodItem() const; + void clear(); + + const not_null _item; + const std::unique_ptr _style; + const std::unique_ptr _delegate; + rpl::variable _globalViewport; + rpl::variable _chatWide; + std::shared_ptr _theme; + std::unique_ptr _element; + QRect _viewport; + QRect _elementGeometry; + rpl::variable _elementInner; + rpl::lifetime _elementLifetime; + + QImage _lastFrameCache; + + rpl::event_stream<> _closeRequests; + +}; + +PreviewWrap::PreviewWrap( + not_null parent, + not_null item, + rpl::producer viewportValue, + rpl::producer chatWideValue, + rpl::producer> theme) +: RpWidget(parent) +, _item(item) +, _style(std::make_unique( + item->history()->session().colorIndicesValue())) +, _delegate(std::make_unique( + 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 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 view) { + if (view == _element.get()) { + update(_elementGeometry); + } + }, lifetime()); + session->data().itemViewRefreshRequest( + ) | rpl::start_with_next([=](not_null item) { + if (item == _item) { + if (goodItem()) { + createView(); + update(); + } else { + clear(); + _closeRequests.fire({}); + } + } + }, lifetime()); + session->data().itemDataChanges( + ) | rpl::start_with_next([=](not_null item) { + if (item == _item) { + _element->itemDataChanged(); + } + }, lifetime()); + session->data().itemRemoved( + ) | rpl::start_with_next([=](not_null item) { + if (item == _item) { + _closeRequests.fire({}); + } + }, lifetime()); + + { + const auto close = Ui::CreateChild( + 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( + this, + object_ptr>( + 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 GlobalViewportForWindow( + not_null 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 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 controller, + not_null item) { + const auto parent = controller->content(); + const auto show = controller->uiShow(); + auto preview = base::make_unique_q( + 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( + parent, + std::move(preview)); + layer->lifetime().add([] { ::Media::Player::instance()->stop(); }); + base::install_event_filter(layer.get(), [=](not_null e) { + if (e->type() == QEvent::KeyPress) { + const auto k = static_cast(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 diff --git a/Telegram/SourceFiles/chat_helpers/ttl_media_layer_widget.h b/Telegram/SourceFiles/chat_helpers/ttl_media_layer_widget.h new file mode 100644 index 000000000..f31d04982 --- /dev/null +++ b/Telegram/SourceFiles/chat_helpers/ttl_media_layer_widget.h @@ -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 controller, + not_null item); + +} // namespace ChatHelpers diff --git a/Telegram/SourceFiles/core/application.cpp b/Telegram/SourceFiles/core/application.cpp index 57747616b..5e474569e 100644 --- a/Telegram/SourceFiles/core/application.cpp +++ b/Telegram/SourceFiles/core/application.cpp @@ -360,7 +360,7 @@ void Application::run() { startDomain(); startTray(); - _lastActivePrimaryWindow->widget()->show(); + _lastActivePrimaryWindow->firstShow(); startMediaView(); diff --git a/Telegram/SourceFiles/core/core_cloud_password.cpp b/Telegram/SourceFiles/core/core_cloud_password.cpp index 99b5d589e..3a65f24d5 100644 --- a/Telegram/SourceFiles/core/core_cloud_password.cpp +++ b/Telegram/SourceFiles/core/core_cloud_password.cpp @@ -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!")); diff --git a/Telegram/SourceFiles/core/core_settings.cpp b/Telegram/SourceFiles/core/core_settings.cpp index f22fe917d..a8a4a74c5 100644 --- a/Telegram/SourceFiles/core/core_settings.cpp +++ b/Telegram/SourceFiles/core/core_settings.cpp @@ -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 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(); diff --git a/Telegram/SourceFiles/core/core_settings.h b/Telegram/SourceFiles/core/core_settings.h index 2ab3d21dd..337f5810a 100644 --- a/Telegram/SourceFiles/core/core_settings.h +++ b/Telegram/SourceFiles/core/core_settings.h @@ -826,6 +826,15 @@ public: void setStoriesClickTooltipHidden(bool value) { _storiesClickTooltipHidden = value; } + [[nodiscard]] bool ttlVoiceClickTooltipHidden() const { + return _ttlVoiceClickTooltipHidden.current(); + } + [[nodiscard]] rpl::producer 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 _ignoreBatterySaving = false; std::optional _macRoundIconDigest; rpl::variable _storiesClickTooltipHidden = false; + rpl::variable _ttlVoiceClickTooltipHidden = false; bool _tabbedReplacedWithInfo = false; // per-window rpl::event_stream _tabbedReplacedWithInfoValue; // per-window diff --git a/Telegram/SourceFiles/core/shortcuts.cpp b/Telegram/SourceFiles/core/shortcuts.cpp index 8b90122b1..c90e88233 100644 --- a/Telegram/SourceFiles/core/shortcuts.cpp +++ b/Telegram/SourceFiles/core/shortcuts.cpp @@ -80,6 +80,13 @@ const auto CommandByName = base::flat_map{ { 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::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)); diff --git a/Telegram/SourceFiles/core/shortcuts.h b/Telegram/SourceFiles/core/shortcuts.h index d7b3dc3c5..b562517d5 100644 --- a/Telegram/SourceFiles/core/shortcuts.h +++ b/Telegram/SourceFiles/core/shortcuts.h @@ -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 RequestHandler(Command command); class Request { diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h index b2db36f80..946974abd 100644 --- a/Telegram/SourceFiles/core/version.h +++ b/Telegram/SourceFiles/core/version.h @@ -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; diff --git a/Telegram/SourceFiles/data/data_changes.cpp b/Telegram/SourceFiles/data/data_changes.cpp index f20041565..773c50d5d 100644 --- a/Telegram/SourceFiles/data/data_changes.cpp +++ b/Telegram/SourceFiles/data/data_changes.cpp @@ -56,7 +56,7 @@ rpl::producer Changes::Manager::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); }); } diff --git a/Telegram/SourceFiles/data/data_document_resolver.cpp b/Telegram/SourceFiles/data/data_document_resolver.cpp index 2c34719ff..8ab502564 100644 --- a/Telegram/SourceFiles/data/data_document_resolver.cpp +++ b/Telegram/SourceFiles/data/data_document_resolver.cpp @@ -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 @@ -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(); } diff --git a/Telegram/SourceFiles/data/data_group_call.cpp b/Telegram/SourceFiles/data/data_group_call.cpp index 0567477c4..72b4d387f 100644 --- a/Telegram/SourceFiles/data/data_group_call.cpp +++ b/Telegram/SourceFiles/data/data_group_call.cpp @@ -847,7 +847,7 @@ void GroupCall::requestUnknownParticipants() { auto result = base::flat_map(); 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); } diff --git a/Telegram/SourceFiles/data/data_histories.cpp b/Telegram/SourceFiles/data/data_histories.cpp index 6824531fa..454ae4ab7 100644 --- a/Telegram/SourceFiles/data/data_histories.cpp +++ b/Telegram/SourceFiles/data/data_histories.cpp @@ -129,7 +129,7 @@ not_null 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(&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); diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp index e9c4dd66e..fb6560e51 100644 --- a/Telegram/SourceFiles/data/data_media_types.cpp +++ b/Telegram/SourceFiles/data/data_media_types.cpp @@ -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{} diff --git a/Telegram/SourceFiles/data/data_message_reaction_id.cpp b/Telegram/SourceFiles/data/data_message_reaction_id.cpp index 1103d2e15..2c1a9e503 100644 --- a/Telegram/SourceFiles/data/data_message_reaction_id.cpp +++ b/Telegram/SourceFiles/data/data_message_reaction_id.cpp @@ -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 {}; diff --git a/Telegram/SourceFiles/data/data_message_reaction_id.h b/Telegram/SourceFiles/data/data_message_reaction_id.h index 8c50fd9de..53d4a2df8 100644 --- a/Telegram/SourceFiles/data/data_message_reaction_id.h +++ b/Telegram/SourceFiles/data/data_message_reaction_id.h @@ -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); diff --git a/Telegram/SourceFiles/data/data_message_reactions.cpp b/Telegram/SourceFiles/data/data_message_reactions.cpp index 9ff9205f7..e3b87201f 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.cpp +++ b/Telegram/SourceFiles/data/data_message_reactions.cpp @@ -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 ListFromMTP( + const MTPDmessages_savedReactionTags &data) { + const auto &list = data.vtags().v; + auto result = std::vector(); + 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 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 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 &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 &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 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 Reactions::resolveListener() { return static_cast(this); } @@ -715,6 +912,10 @@ void Reactions::customEmojiResolveDone(not_null 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 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 document) { if (recent) { _recentUpdated.fire({}); } + if (myTag) { + _myTagsUpdated.fire({}); + } + if (tag) { + _tagsUpdated.fire({}); + } } std::optional 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; diff --git a/Telegram/SourceFiles/data/data_message_reactions.h b/Telegram/SourceFiles/data/data_message_reactions.h index 61b29107d..5e8721665 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.h +++ b/Telegram/SourceFiles/data/data_message_reactions.h @@ -42,6 +42,7 @@ struct PossibleItemReactionsRef { std::vector> recent; bool morePremiumAvailable = false; bool customAllowed = false; + bool tags = false; }; struct PossibleItemReactions { @@ -51,11 +52,18 @@ struct PossibleItemReactions { std::vector recent; bool morePremiumAvailable = false; bool customAllowed = false; + bool tags = false; }; [[nodiscard]] PossibleItemReactionsRef LookupPossibleReactions( not_null item); +struct MyTagInfo { + ReactionId id; + QString title; + int count = 0; +}; + class Reactions final : private CustomEmojiManager::Listener { public: explicit Reactions(not_null 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 &list(Type type) const; + [[nodiscard]] const std::vector &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 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 resolveById(const ReactionId &id); [[nodiscard]] std::vector resolveByIds( @@ -145,6 +169,7 @@ private: base::flat_set &unresolved); void resolve(const ReactionId &id); void applyFavorite(const ReactionId &id); + void scheduleMyTagsUpdate(); [[nodiscard]] std::optional parse( const MTPAvailableReaction &entry); @@ -167,6 +192,13 @@ private: std::vector _recent; std::vector _recentIds; base::flat_set _unresolvedRecent; + std::vector _myTags; + std::vector _myTagsIds; + std::vector _myTagsInfo; + base::flat_set _unresolvedMyTags; + std::vector _tags; + std::vector _tagsIds; + base::flat_set _unresolvedTags; std::vector _top; std::vector _topIds; base::flat_set _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 _images; rpl::lifetime _imagesLoadLifetime; bool _waitingForList = false; diff --git a/Telegram/SourceFiles/data/data_saved_messages.cpp b/Telegram/SourceFiles/data/data_saved_messages.cpp index 66bc87609..d81f97fe5 100644 --- a/Telegram/SourceFiles/data/data_saved_messages.cpp +++ b/Telegram/SourceFiles/data/data_saved_messages.cpp @@ -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); diff --git a/Telegram/SourceFiles/data/data_search_controller.cpp b/Telegram/SourceFiles/data/data_search_controller.cpp index 688ce4ae9..43c5efb66 100644 --- a/Telegram/SourceFiles/data/data_search_controller.cpp +++ b/Telegram/SourceFiles/data/data_search_controller.cpp @@ -98,6 +98,7 @@ std::optional PrepareSearchRequest( MTP_string(query), MTP_inputPeerEmpty(), MTPInputPeer(), // saved_peer_id + MTPVector(), // saved_reaction MTP_int(topicRootId), filter, MTP_int(0), // min_date diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 3885b9155..05405f78e 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -1109,7 +1109,7 @@ void Session::watchForOffline(not_null 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 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 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(this, id)); return it->second.get(); diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp index 5d9326465..79d573bab 100644 --- a/Telegram/SourceFiles/data/data_stories.cpp +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -238,7 +238,7 @@ Story *Stories::applySingle(PeerId peerId, const MTPstoryItem &story) { void Stories::requestPeerStories( not_null peer, Fn done) { - const auto [i, ok] = _requestingPeerStories.emplace(peer); + const auto &[i, ok] = _requestingPeerStories.emplace(peer); if (done) { i->second.push_back(std::move(done)); } diff --git a/Telegram/SourceFiles/data/data_types.h b/Telegram/SourceFiles/data/data_types.h index 31c976859..46f75419a 100644 --- a/Telegram/SourceFiles/data/data_types.h +++ b/Telegram/SourceFiles/data/data_types.h @@ -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; diff --git a/Telegram/SourceFiles/dialogs/dialogs.style b/Telegram/SourceFiles/dialogs/dialogs.style index b42c490a3..083cf3a54 100644 --- a/Telegram/SourceFiles/dialogs/dialogs.style +++ b/Telegram/SourceFiles/dialogs/dialogs.style @@ -623,3 +623,6 @@ dialogsStoriesTooltipHide: IconButton(defaultIconButton) { searchedBarHeight: 32px; searchedBarFont: normalFont; searchedBarPosition: point(17px, 7px); + +dialogsSearchTagSkip: point(8px, 4px); +dialogsSearchTagBottom: 10px; diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index afbf0ca77..cea479bb4 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -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(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 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(); + }; + _searchTags = std::make_unique( + &peer->owner(), + rpl::single( + list() + ) | rpl::then( + reactions->myTagsUpdates() | rpl::map(list) + ), + tags); + + _searchTags->selectedValue( + ) | rpl::start_with_next([=](std::vector &&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> { + return _searchTags + ? _searchTags->selectedValue() + : rpl::single(std::vector()); +} + 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) diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h index 7915bd912..0f12e7211 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h @@ -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 tags); + [[nodiscard]] auto searchTagsValue() const + -> rpl::producer>; 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; + std::vector _searchTagsSelected; + int _searchTagsLeft = 0; RowDescriptor _menuRow; base::flat_map< diff --git a/Telegram/SourceFiles/dialogs/dialogs_search_tags.cpp b/Telegram/SourceFiles/dialogs/dialogs_search_tags.cpp new file mode 100644 index 000000000..7f0aa16eb --- /dev/null +++ b/Telegram/SourceFiles/dialogs/dialogs_search_tags.cpp @@ -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 custom; + mutable QImage image; + QRect geometry; + ClickHandlerPtr link; + bool selected = false; +}; + +SearchTags::SearchTags( + not_null owner, + rpl::producer> tags, + std::vector selected) +: _owner(owner) +, _added(selected) { + std::move( + tags + ) | rpl::start_with_next([=](const std::vector &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 &list) { + const auto selected = collectSelected(); + _tags.clear(); + _tags.reserve(list.size()); + const auto link = [&](Data::ReactionId id) { + return std::make_shared(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 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> { + return _selectedChanges.events() | rpl::map([=] { + return collectSelected(); + }); +} + +void SearchTags::paintCustomFrame( + QPainter &p, + not_null 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 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 diff --git a/Telegram/SourceFiles/dialogs/dialogs_search_tags.h b/Telegram/SourceFiles/dialogs/dialogs_search_tags.h new file mode 100644 index 000000000..4c52162eb --- /dev/null +++ b/Telegram/SourceFiles/dialogs/dialogs_search_tags.h @@ -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 owner, + rpl::producer> tags, + std::vector selected); + ~SearchTags(); + + void resizeToWidth(int width); + [[nodiscard]] int height() const; + [[nodiscard]] rpl::producer heightValue() const; + [[nodiscard]] rpl::producer<> repaintRequests() const; + + [[nodiscard]] ClickHandlerPtr lookupHandler(QPoint point) const; + [[nodiscard]] auto selectedValue() const + -> rpl::producer>; + + void paint( + QPainter &p, + QPoint position, + crl::time now, + bool paused) const; + + [[nodiscard]] rpl::lifetime &lifetime(); + +private: + struct Tag; + + void fill(const std::vector &list); + void paintCustomFrame( + QPainter &p, + not_null emoji, + QPoint innerTopLeft, + crl::time now, + bool paused, + const QColor &textColor) const; + void layout(); + [[nodiscard]] std::vector collectSelected() const; + [[nodiscard]] const QImage &validateBg(bool selected) const; + + const not_null _owner; + std::vector _added; + std::vector _tags; + rpl::event_stream<> _selectedChanges; + rpl::event_stream<> _repaintRequests; + mutable QImage _normalBg; + mutable QImage _selectedBg; + mutable QImage _customCache; + mutable int _customSkip = 0; + rpl::variable _height; + int _width = 0; + + rpl::lifetime _lifetime; + +}; + +} // namespace Dialogs diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index 4dc510f95..f0104f030 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -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 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 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(); + 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 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(), // 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 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 &&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(); } diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.h b/Telegram/SourceFiles/dialogs/dialogs_widget.h index 5fce5f9e3..3713ed721 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.h @@ -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 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 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 _searchTags; + rpl::lifetime _searchTagsLifetime; QString _lastFilterText; rpl::event_stream> _storiesContents; @@ -313,6 +322,7 @@ private: QString _searchQuery; PeerData *_searchQueryFrom = nullptr; + std::vector _searchQueryTags; int32 _searchNextRate = 0; bool _searchFull = false; bool _searchFullMigrated = false; diff --git a/Telegram/SourceFiles/editor/editor_crop.cpp b/Telegram/SourceFiles/editor/editor_crop.cpp index 78a91aa6a..54f8369c2 100644 --- a/Telegram/SourceFiles/editor/editor_crop.cpp +++ b/Telegram/SourceFiles/editor/editor_crop.cpp @@ -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, diff --git a/Telegram/SourceFiles/export/export_api_wrap.cpp b/Telegram/SourceFiles/export/export_api_wrap.cpp index 61f29b16c..b23329c03 100644 --- a/Telegram/SourceFiles/export/export_api_wrap.cpp +++ b/Telegram/SourceFiles/export/export_api_wrap.cpp @@ -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(), // saved_reaction MTPint(), // top_msg_id MTP_inputMessagesFilterEmpty(), MTP_int(0), // min_date diff --git a/Telegram/SourceFiles/export/output/export_output_html.cpp b/Telegram/SourceFiles/export/output/export_output_html.cpp index a5181618d..74c1f1f3d 100644 --- a/Telegram/SourceFiles/export/output/export_output_html.cpp +++ b/Telegram/SourceFiles/export/output/export_output_html.cpp @@ -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, diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp index fb6f950e0..2598e1f36 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp @@ -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); diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index 629fae9c9..8c4dfa5dc 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -470,7 +470,7 @@ not_null History::insertItem( std::unique_ptr 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); diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 54241b6b7..b5083b3b3 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -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); } diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 3ce819c7c..85d9bce45 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -2461,6 +2461,11 @@ const std::vector &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(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(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) { diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index 63db67fc3..929c286cf 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -455,6 +455,7 @@ public: not_null from) const; [[nodiscard]] crl::time lastReactionsRefreshTime() const; + [[nodiscard]] bool reactionsAreTags() const; [[nodiscard]] bool hasDirectLink() const; [[nodiscard]] bool changesWallPaper() const; diff --git a/Telegram/SourceFiles/history/history_item_helpers.cpp b/Telegram/SourceFiles/history/history_item_helpers.cpp index 3521646d3..06bd10837 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.cpp +++ b/Telegram/SourceFiles/history/history_item_helpers.cpp @@ -813,8 +813,3 @@ void ClearMediaAsExpired(not_null item) { } } } - -[[nodiscard]] bool IsVoiceOncePlayable(not_null item) { - const auto settings = &AyuSettings::getInstance(); - return !item->out() && item->media()->ttlSeconds() && !settings->saveDeletedMessages; -} diff --git a/Telegram/SourceFiles/history/history_item_helpers.h b/Telegram/SourceFiles/history/history_item_helpers.h index f96d9c092..c438eccbe 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.h +++ b/Telegram/SourceFiles/history/history_item_helpers.h @@ -158,4 +158,3 @@ ClickHandlerPtr JumpToStoryClickHandler( void ShowTrialTranscribesToast(int left, TimeId until); void ClearMediaAsExpired(not_null item); -[[nodiscard]] bool IsVoiceOncePlayable(not_null item); diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 2db71e78b..682509c4b 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -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 HistoryWidget::cornerButtonsDownShown() { if (!_list || _firstLoadRequest) { return false; } - if (_voiceRecordBar->isLockPresent()) { + if (_voiceRecordBar->isLockPresent() + || _voiceRecordBar->isTTLButtonShown()) { return false; } if (!_history->loadedAtBottom() || _cornerButtons.replyReturn()) { diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp index b25be58f9..e8e81511e 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -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( _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 ComposeControls::lockShowStarts() const { return _voiceRecordBar->lockShowStarts(); } diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h index 9ae515d6c..a846cd4ee 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h @@ -220,6 +220,7 @@ public: [[nodiscard]] rpl::producer lockShowStarts() const; [[nodiscard]] bool isLockPresent() const; + [[nodiscard]] bool isTTLButtonShown() const; [[nodiscard]] bool isRecording() const; [[nodiscard]] bool isRecordingPressed() const; [[nodiscard]] rpl::producer recordingActiveValue() const; diff --git a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp index 85bee7a6c..d958139c2 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp @@ -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( + parent.get(), + object_ptr>( + 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(_outerContainer, _st.lock)) -, _ttlButton(std::make_unique(_outerContainer, _st)) , _level(std::make_unique(_outerContainer, _st)) , _cancel(std::make_unique(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( + _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(parentWidget())->geometryValue() + const auto parent = static_cast(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::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; diff --git a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.h b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.h index 31c0eb30e..f1a58465e 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.h @@ -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 &&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 _show; const std::shared_ptr _send; const std::unique_ptr _lock; - const std::unique_ptr _ttlButton; const std::unique_ptr _level; const std::unique_ptr _cancel; + std::unique_ptr _ttlButton; std::unique_ptr _listen; base::Timer _startTimer; diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index a6ba67967..eb2af43bb 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -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*> menu, + QPoint position, + not_null context, + not_null item, + const Data::ReactionId &id, + not_null controller) { + using namespace Data; + const auto itemId = item->fullId(); + const auto owner = &controller->session().data(); + *menu = base::make_unique_q( + 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( + (*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*> menu, QPoint position, @@ -1326,6 +1384,11 @@ void ShowWhoReactedMenu( const Data::ReactionId &id, not_null controller, rpl::lifetime &lifetime) { + if (item->reactionsAreTags()) { + ShowTagMenu(menu, position, context, item, id, controller); + return; + } + struct State { int addedToBottom = 0; }; diff --git a/Telegram/SourceFiles/history/view/history_view_element.h b/Telegram/SourceFiles/history/view/history_view_element.h index 3b524915f..134a7bf70 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.h +++ b/Telegram/SourceFiles/history/view/history_view_element.h @@ -58,6 +58,7 @@ enum class Context : char { AdminLog, ContactPreview, SavedSublist, + TTLViewer, }; enum class OnlyEmojiAndSpaces : char { diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index 50822615c..6dcc3497f 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -901,7 +901,7 @@ not_null 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 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(); diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 628e3eb10..e3263e41d 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -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([=] { + return std::make_shared([=]( + 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 { diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp index 530af5fae..157a61977 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp @@ -1838,7 +1838,8 @@ bool RepliesWidget::cornerButtonsIgnoreVisibility() { } std::optional 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 RepliesWidget::cornerButtonsDownShown() { } bool RepliesWidget::cornerButtonsUnreadMayBeShown() { - return _loaded && !_composeControls->isLockPresent(); + return _loaded + && !_composeControls->isLockPresent() + && !_composeControls->isTTLButtonShown(); } bool RepliesWidget::cornerButtonsHas(CornerButtonType type) { diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp index ed57d5ee8..a086d71cb 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp @@ -843,7 +843,8 @@ bool ScheduledWidget::cornerButtonsIgnoreVisibility() { } std::optional 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 ScheduledWidget::cornerButtonsDownShown() { bool ScheduledWidget::cornerButtonsUnreadMayBeShown() { return _inner->loadedAtBottomKnown() - && !_composeControls->isLockPresent(); + && !_composeControls->isLockPresent() + && !_composeControls->isTTLButtonShown(); } bool ScheduledWidget::cornerButtonsHas(CornerButtonType type) { diff --git a/Telegram/SourceFiles/history/view/history_view_sublist_section.cpp b/Telegram/SourceFiles/history/view/history_view_sublist_section.cpp index a1c9578f8..e96738d0d 100644 --- a/Telegram/SourceFiles/history/view/history_view_sublist_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_sublist_section.cpp @@ -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 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 diff --git a/Telegram/SourceFiles/history/view/history_view_sublist_section.h b/Telegram/SourceFiles/history/view/history_view_sublist_section.h index 819ce363c..ff6d34ace 100644 --- a/Telegram/SourceFiles/history/view/history_view_sublist_section.h +++ b/Telegram/SourceFiles/history/view/history_view_sublist_section.h @@ -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 _sublist; const not_null _history; diff --git a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp index 744d4ad3a..555de8e18 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp @@ -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 + // 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 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); } diff --git a/Telegram/SourceFiles/history/view/media/history_view_document.cpp b/Telegram/SourceFiles/history/view/media/history_view_document.cpp index 7ca7bc747..3eb378a17 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_document.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_document.cpp @@ -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 start; std::unique_ptr idle; + bool started = false; }; const auto iconSize = Size(std::min( st::historyFileInPause.width(), st::historyFileInPause.height())); const auto state = lifetime->make_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 icon, - Fn 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::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(); + 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()) { - voice->seekl = !IsVoiceOncePlayable(_parent->data()) + voice->seekl = !_parent->data()->media()->ttlSeconds() ? std::make_shared(_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(); + 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()) { 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()) { - if (voice->seeking()) { - const auto thumbed = Get(); - 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(); + 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 diff --git a/Telegram/SourceFiles/history/view/media/history_view_document.h b/Telegram/SourceFiles/history/view/media/history_view_document.h index 5fa8dc1da..3d29651c4 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_document.h +++ b/Telegram/SourceFiles/history/view/media/history_view_document.h @@ -181,4 +181,6 @@ bool DrawThumbnailAsSongCover( const QRect &rect, bool selected = false); +rpl::producer<> TTLVoiceStops(FullMsgId fullId); + } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp index 87a0e00bf..03164193b 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp @@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "media/view/media_view_playback_progress.h" #include "ui/boxes/confirm_box.h" #include "ui/painter.h" +#include "ui/rect.h" #include "history/history_item_components.h" #include "history/history_item_helpers.h" #include "history/history_item.h" @@ -30,6 +31,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_cursor_state.h" #include "history/view/history_view_reply.h" #include "history/view/history_view_transcribe_button.h" +#include "history/view/media/history_view_document.h" // TTLVoiceStops #include "history/view/media/history_view_media_common.h" #include "history/view/media/history_view_media_spoiler.h" #include "window/window_session_controller.h" @@ -52,6 +54,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_document_media.h" #include "styles/style_chat.h" +#include + namespace HistoryView { namespace { @@ -65,6 +69,38 @@ int gifMaxStatusWidth(DocumentData *document) { return result; } +[[nodiscard]] HistoryView::TtlRoundPaintCallback CreateTtlPaintCallback( + Fn update) { + const auto centerMargins = Margins(st::historyFileInPause.width() * 3); + + const auto renderer = std::make_shared( + u":/gui/ttl/video_message_icon.svg"_q); + + return [=](QPainter &p, QRect r, const PaintContext &context) { + const auto centerRect = r - centerMargins; + const auto &icon = context.imageStyle()->historyVideoMessageTtlIcon; + const auto iconRect = QRect( + rect::right(centerRect) - icon.width() * 0.75, + rect::bottom(centerRect) - icon.height() * 0.75, + icon.width(), + icon.height()); + { + auto hq = PainterHighQualityEnabler(p); + auto path = QPainterPath(); + path.setFillRule(Qt::WindingFill); + path.addEllipse(centerRect); + path.addEllipse(iconRect); + p.fillPath(path, st::shadowFg); + p.fillPath(path, st::shadowFg); + p.fillPath(path, st::shadowFg); + } + + renderer->render(&p, centerRect - Margins(centerRect.width() / 4)); + + icon.paint(p, iconRect.topLeft(), centerRect.width()); + }; +} + } // namespace struct Gif::Streamed { @@ -83,6 +119,12 @@ Gif::Streamed::Streamed( : instance(std::move(shared), std::move(waitingCallback)) { } +[[nodiscard]] bool IsHiddenRoundMessage(not_null parent) { + return parent->delegate()->elementContext() != Context::TTLViewer + && parent->data()->media() + && parent->data()->media()->ttlSeconds(); +} + Gif::Gif( not_null parent, not_null realParent, @@ -95,18 +137,47 @@ Gif::Gif( : FullStoryId()) , _caption( st::minPhotoSize - st::msgPadding.left() - st::msgPadding.right()) -, _spoiler(spoiler ? std::make_unique() : nullptr) +, _spoiler((spoiler || IsHiddenRoundMessage(_parent)) + ? std::make_unique() + : nullptr) , _downloadSize(Ui::FormatSizeText(_data->size)) { - setDocumentLinks(_data, realParent, [=] { - if (!_data->createMediaView()->canBePlayed(realParent) - || !_data->isAnimation() - || _data->isVideoMessage() - || !CanPlayInline(_data)) { - return false; + if (_data->isVideoMessage() && _parent->data()->media()->ttlSeconds()) { + if (_spoiler) { + _drawTtl = CreateTtlPaintCallback([=] { repaint(); }); } - playAnimation(false); - return true; - }); + const auto fullId = _realParent->fullId(); + const auto &data = &_parent->data()->history()->owner(); + const auto isOut = _parent->data()->out(); + _parent->data()->removeFromSharedMediaIndex(); + setDocumentLinks(_data, realParent, [=] { + auto lifetime = std::make_shared(); + TTLVoiceStops(fullId) | rpl::start_with_next([=]() mutable { + if (lifetime) { + base::take(lifetime)->destroy(); + } + if (!isOut) { + if (const auto item = data->message(fullId)) { + // Destroys this. + ClearMediaAsExpired(item); + } + } + }, *lifetime); + + return false; + }); + } else { + setDocumentLinks(_data, realParent, [=] { + if (!_data->createMediaView()->canBePlayed(realParent) + || !_data->isAnimation() + || _data->isVideoMessage() + || !CanPlayInline(_data)) { + return false; + } + playAnimation(false); + return true; + }); + } + setStatusSize(Ui::FileStatusSizeReady); if (_spoiler) { @@ -403,7 +474,13 @@ void Gif::draw(Painter &p, const PaintContext &context) const { QRect rthumb(style::rtlrect(usex + paintx, painty, usew, painth, width())); - const auto revealed = (!isRound && _spoiler) + const auto inTTLViewer = _parent->delegate()->elementContext() + == Context::TTLViewer; + const auto revealed = (isRound + && item->media()->ttlSeconds() + && !inTTLViewer) + ? 0 + : (!isRound && _spoiler) ? _spoiler->revealAnimation.value(_spoiler->revealed ? 1. : 0.) : 1.; const auto fullHiddenBySpoiler = (revealed == 0.); @@ -501,18 +578,19 @@ void Gif::draw(Painter &p, const PaintContext &context) const { const auto value = playback->value(); if (value > 0.) { auto pen = st->historyVideoMessageProgressFg()->p; - auto was = p.pen(); + const auto was = p.pen(); pen.setWidth(st::radialLine); pen.setCapStyle(Qt::RoundCap); p.setPen(pen); p.setOpacity(st::historyVideoMessageProgressOpacity); - auto from = arc::kQuarterLength; - auto len = -qRound(arc::kFullLength * value); - auto stepInside = st::radialLine / 2; + const auto from = arc::kQuarterLength; + const auto len = std::round(arc::kFullLength + * (inTTLViewer ? (1. - value) : -value)); + const auto stepInside = st::radialLine / 2; { - PainterHighQualityEnabler hq(p); - p.drawArc(rthumb.marginsRemoved(QMargins(stepInside, stepInside, stepInside, stepInside)), from, len); + auto hq = PainterHighQualityEnabler(p); + p.drawArc(rthumb - Margins(stepInside), from, len); } p.setPen(was); @@ -525,10 +603,19 @@ void Gif::draw(Painter &p, const PaintContext &context) const { p.drawImage(rthumb, _thumbCache); } - if (!isRound && revealed < 1.) { + if (revealed < 1.) { p.setOpacity(1. - revealed); - p.drawImage(rthumb.topLeft(), _spoiler->background); - fillImageSpoiler(p, _spoiler.get(), rthumb, context); + if (!isRound) { + p.drawImage(rthumb.topLeft(), _spoiler->background); + fillImageSpoiler(p, _spoiler.get(), rthumb, context); + } else { + auto frame = _spoiler->background; + { + auto q = QPainter(&frame); + fillImageSpoiler(q, _spoiler.get(), rthumb, context); + } + p.drawImage(rthumb.topLeft(), Images::Circle(std::move(frame))); + } p.setOpacity(1.); } if (context.selected()) { @@ -793,6 +880,9 @@ void Gif::draw(Painter &p, const PaintContext &context) const { paintTranscribe(p, usex, fullBottom, false, context); } } + if (_drawTtl) { + _drawTtl(p, rthumb, context); + } } void Gif::paintTranscribe( @@ -1108,7 +1198,9 @@ TextState Gif::textState(QPoint point, StateRequest request) const { } if (QRect(usex + paintx, painty, usew, painth).contains(point)) { ensureDataMediaCreated(); - result.link = (_spoiler && !_spoiler->revealed) + result.link = (isRound && _parent->data()->media()->ttlSeconds()) + ? _openl // Overriden. + : (_spoiler && !_spoiler->revealed) ? _spoiler->link : _data->uploading() ? _cancell @@ -1970,7 +2062,7 @@ bool Gif::needCornerStatusDisplay() const { void Gif::ensureTranscribeButton() const { if (_data->isVideoMessage() - && !IsVoiceOncePlayable(_parent->data()) + && !_parent->data()->media()->ttlSeconds() && (_data->session().premium() || _data->session().api().transcribes().trialsSupport())) { if (!_transcribe) { diff --git a/Telegram/SourceFiles/history/view/media/history_view_gif.h b/Telegram/SourceFiles/history/view/media/history_view_gif.h index 1d1b30e4a..dad49b351 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_gif.h +++ b/Telegram/SourceFiles/history/view/media/history_view_gif.h @@ -40,6 +40,11 @@ namespace HistoryView { class Reply; class TranscribeButton; +using TtlRoundPaintCallback = Fn; + class Gif final : public File { public: Gif( @@ -214,6 +219,8 @@ private: void togglePollingStory(bool enabled) const; + TtlRoundPaintCallback _drawTtl; + const not_null _data; const FullStoryId _storyId; Ui::Text::String _caption; diff --git a/Telegram/SourceFiles/history/view/media/history_view_poll.cpp b/Telegram/SourceFiles/history/view/media/history_view_poll.cpp index c08d8fde5..7e96e068f 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_poll.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_poll.cpp @@ -761,7 +761,7 @@ void Poll::draw(Painter &p, const PaintContext &context) const { auto &&answers = ranges::views::zip( _answers, ranges::views::ints(0, int(_answers.size()))); - for (const auto [answer, index] : answers) { + for (const auto &[answer, index] : answers) { const auto animation = _answersAnimation ? &_answersAnimation->data[index] : nullptr; diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp b/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp index 911472560..39ae8087a 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp @@ -55,6 +55,7 @@ struct InlineList::Button { int count = 0; int countTextWidth = 0; bool chosen = false; + bool tag = false; }; InlineList::InlineList( @@ -118,6 +119,7 @@ void InlineList::layoutButtons() { ) | ranges::views::transform([](const MessageReaction &reaction) { return not_null{ &reaction }; }) | ranges::to_vector; + const auto tags = _data.flags & Data::Flag::Tags; const auto &list = _owner->list(::Data::Reactions::Type::All); ranges::sort(sorted, [&]( not_null a, @@ -142,8 +144,10 @@ void InlineList::layoutButtons() { buttons.push_back((i != end(_buttons)) ? std::move(*i) : prepareButtonWithId(id)); - const auto j = _data.recent.find(id); - if (j != end(_data.recent) && !j->second.empty()) { + if (tags) { + setButtonTag(buttons.back()); + } else if (const auto j = _data.recent.find(id) + ; j != end(_data.recent) && !j->second.empty()) { setButtonUserpics(buttons.back(), j->second); } else { setButtonCount(buttons.back(), reaction->count); @@ -168,12 +172,22 @@ InlineList::Button InlineList::prepareButtonWithId(const ReactionId &id) { return result; } +void InlineList::setButtonTag(Button &button) { + if (button.tag) { + return; + } + button.userpics = nullptr; + button.count = 0; + button.tag = true; +} + void InlineList::setButtonCount(Button &button, int count) { - if (button.count == count && !button.userpics) { + if (!button.tag && button.count == count && !button.userpics) { return; } button.userpics = nullptr; button.count = count; + button.tag = false; button.countText = Lang::FormatCountToShort(count).string; button.countTextWidth = st::semiboldFont->width(button.countText); } @@ -181,6 +195,7 @@ void InlineList::setButtonCount(Button &button, int count) { void InlineList::setButtonUserpics( Button &button, const std::vector> &peers) { + button.tag = false; if (!button.userpics) { button.userpics = std::make_unique(); } @@ -228,6 +243,10 @@ QSize InlineList::countOptimalSize() { const auto between = st::reactionInlineBetween; const auto padding = st::reactionInlinePadding; const auto size = st::reactionInlineSize; + const auto widthBaseTag = padding.left() + + size + + st::reactionInlineTagSkip + + padding.right(); const auto widthBaseCount = padding.left() + size + st::reactionInlineSkip @@ -245,7 +264,9 @@ QSize InlineList::countOptimalSize() { }; const auto height = padding.top() + size + padding.bottom(); for (auto &button : _buttons) { - const auto width = button.userpics + const auto width = button.tag + ? widthBaseTag + : button.userpics ? (widthBaseUserpics + userpicsWidth(button)) : (widthBaseCount + button.countTextWidth); button.geometry.setSize({ width, height }); @@ -336,7 +357,8 @@ void InlineList::paint( const auto padding = st::reactionInlinePadding; const auto size = st::reactionInlineSize; const auto skip = (size - st::reactionInlineImage) / 2; - const auto inbubble = (_data.flags & InlineListData::Flag::InBubble); + const auto tags = (_data.flags & Data::Flag::Tags); + const auto inbubble = (_data.flags & Data::Flag::InBubble); const auto flipped = (_data.flags & Data::Flag::Flipped); p.setFont(st::semiboldFont); for (const auto &button : _buttons) { @@ -366,29 +388,33 @@ void InlineList::paint( if (bubbleProgress > 0.) { auto hq = PainterHighQualityEnabler(p); p.setPen(Qt::NoPen); + auto opacity = 1.; + auto color = QColor(); if (inbubble) { if (!chosen) { - p.setOpacity(bubbleProgress * (context.outbg + opacity = bubbleProgress * (context.outbg ? kOutNonChosenOpacity - : kInNonChosenOpacity)); + : kInNonChosenOpacity); } else if (!bubbleReady) { - p.setOpacity(bubbleProgress); + opacity = bubbleProgress; } - p.setBrush(stm->msgFileBg); + color = stm->msgFileBg->c; } else { if (!bubbleReady) { - p.setOpacity(bubbleProgress); + opacity = bubbleProgress; } - p.setBrush(chosen ? st->msgServiceFg() : st->msgServiceBg()); + color = (chosen + ? st->msgServiceFg() + : st->msgServiceBg())->c; } - const auto radius = geometry.height() / 2.; + const auto fill = geometry.marginsAdded({ flipped ? bubbleSkip : 0, 0, flipped ? 0 : bubbleSkip, 0, }); - p.drawRoundedRect(fill, radius, radius); + paintSingleBg(p, fill, color, opacity); if (inbubble && !chosen) { p.setOpacity(bubbleProgress); } @@ -434,7 +460,8 @@ void InlineList::paint( .target = image, }); } - if (bubbleProgress == 0.) { + if (tags || bubbleProgress == 0.) { + p.setOpacity(1.); continue; } resolveUserpicsImage(button); @@ -479,6 +506,129 @@ void InlineList::paint( } } +float64 InlineList::TagDotAlpha() { + return 0.6; +} + +QImage InlineList::PrepareTagBg(QColor tagBg, QColor dotBg) { + const auto padding = st::reactionInlinePadding; + const auto size = st::reactionInlineSize; + const auto width = padding.left() + + size + + st::reactionInlineTagSkip + + padding.right(); + const auto height = padding.top() + size + padding.bottom(); + const auto ratio = style::DevicePixelRatio(); + + auto result = QImage( + QSize(width, height) * ratio, + QImage::Format_ARGB32_Premultiplied); + result.setDevicePixelRatio(ratio); + + result.fill(Qt::transparent); + auto p = QPainter(&result); + + auto path = QPainterPath(); + const auto arrow = st::reactionInlineTagArrow; + const auto rradius = st::reactionInlineTagRightRadius * 1.; + const auto radius = st::reactionInlineTagLeftRadius - rradius; + auto pen = QPen(tagBg); + pen.setWidthF(rradius * 2.); + pen.setJoinStyle(Qt::RoundJoin); + const auto rect = QRectF(0, 0, width, height).marginsRemoved( + { rradius, rradius, rradius, rradius }); + + const auto right = rect.x() + rect.width(); + const auto bottom = rect.y() + rect.height(); + path.moveTo(rect.x() + radius, rect.y()); + path.lineTo(right - arrow, rect.y()); + path.lineTo(right, rect.y() + rect.height() / 2); + path.lineTo(right - arrow, bottom); + path.lineTo(rect.x() + radius, bottom); + path.arcTo( + QRectF(rect.x(), bottom - radius * 2, radius * 2, radius * 2), + 270, + -90); + path.lineTo(rect.x(), rect.y() + radius); + path.arcTo( + QRectF(rect.x(), rect.y(), radius * 2, radius * 2), + 180, + -90); + path.closeSubpath(); + + const auto dsize = st::reactionInlineTagDot; + const auto dot = QRectF( + right - st::reactionInlineTagDotSkip - dsize, + rect.y() + (rect.height() - dsize) / 2., + dsize, + dsize); + + auto hq = PainterHighQualityEnabler(p); + p.setCompositionMode(QPainter::CompositionMode_Source); + p.setPen(pen); + p.setBrush(tagBg); + p.drawPath(path); + + p.setPen(Qt::NoPen); + p.setBrush(dotBg); + p.drawEllipse(dot); + + p.end(); + + return result; +} + +void InlineList::validateTagBg(const QColor &color) const { + if (!_tagBg.isNull() && _tagBgColor == color) { + return; + } + _tagBgColor = color; + auto dot = color; + dot.setAlphaF(dot.alphaF() * TagDotAlpha()); + _tagBg = PrepareTagBg(color, anim::with_alpha(color, TagDotAlpha())); +} + +void InlineList::paintSingleBg( + Painter &p, + const QRect &fill, + const QColor &color, + float64 opacity) const { + p.setOpacity(opacity); + if (!(_data.flags & Data::Flag::Tags)) { + const auto radius = fill.height() / 2.; + p.setBrush(color); + p.drawRoundedRect(fill, radius, radius); + return; + } + validateTagBg(color); + const auto ratio = style::DevicePixelRatio(); + const auto left = st::reactionInlineTagLeftRadius; + const auto right = (_tagBg.width() / ratio) - left; + Assert(right > 0); + const auto useLeft = std::min(fill.width(), left); + p.drawImage( + QRect(fill.x(), fill.y(), useLeft, fill.height()), + _tagBg, + QRect(0, 0, useLeft * ratio, _tagBg.height())); + const auto middle = fill.width() - left - right; + if (middle > 0) { + p.fillRect(fill.x() + left, fill.y(), middle, fill.height(), color); + } + if (const auto useRight = fill.width() - left; useRight > 0) { + p.drawImage( + QRect( + fill.x() + fill.width() - useRight, + fill.y(), + useRight, + fill.height()), + _tagBg, + QRect(_tagBg.width() - useRight * ratio, + 0, + useRight * ratio, + _tagBg.height())); + } +} + bool InlineList::getState( QPoint point, not_null outResult) const { @@ -654,7 +804,8 @@ InlineListData InlineListDataFromMessage(not_null message) { } } result.flags = (message->hasOutLayout() ? Flag::OutLayout : Flag()) - | (message->embedReactionsInBubble() ? Flag::InBubble : Flag()); + | (message->embedReactionsInBubble() ? Flag::InBubble : Flag()) + | (item->reactionsAreTags() ? Flag::Tags : Flag()); return result; } diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions.h b/Telegram/SourceFiles/history/view/reactions/history_view_reactions.h index 398ec0cd8..619e1f053 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions.h +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions.h @@ -41,6 +41,7 @@ struct InlineListData { InBubble = 0x01, OutLayout = 0x02, Flipped = 0x04, + Tags = 0x08, }; friend inline constexpr bool is_flag_type(Flag) { return true; }; using Flags = base::flags; @@ -92,6 +93,9 @@ public: ReactionId, std::unique_ptr> animations); + [[nodiscard]] static float64 TagDotAlpha(); + [[nodiscard]] static QImage PrepareTagBg(QColor tagBg, QColor dotBg); + private: struct Userpics { QImage image; @@ -103,6 +107,7 @@ private: void layout(); void layoutButtons(); + void setButtonTag(Button &button); void setButtonCount(Button &button, int count); void setButtonUserpics( Button &button, @@ -115,6 +120,13 @@ private: QPoint innerTopLeft, const PaintContext &context, const QColor &textColor) const; + void paintSingleBg( + Painter &p, + const QRect &fill, + const QColor &color, + float64 opacity) const; + + void validateTagBg(const QColor &color) const; QSize countOptimalSize() override; @@ -124,6 +136,8 @@ private: Data _data; std::vector