diff --git a/.devcontainer.json b/.devcontainer.json new file mode 100644 index 000000000..30bff840d --- /dev/null +++ b/.devcontainer.json @@ -0,0 +1,35 @@ +{ + "name": "CentOS", + "image": "tdesktop:centos_env", + "customizations": { + "vscode": { + "settings": { + "C_Cpp.intelliSenseEngine": "disabled", + "clangd.arguments": [ + "--compile-commands-dir=${workspaceFolder}/out" + ], + "cmake.generator": "Ninja Multi-Config", + "cmake.buildDirectory": "${workspaceFolder}/out", + "cmake.configureSettings": { + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + } + }, + "extensions": [ + "ms-vscode.cpptools-extension-pack", + "llvm-vs-code-extensions.vscode-clangd", + "TheQtCompany.qt", + "ms-python.python", + "ms-azuretools.vscode-docker", + "eamodio.gitlens" + ] + } + }, + "capAdd": [ + "SYS_PTRACE" + ], + "securityOpt": [ + "seccomp=unconfined" + ], + "workspaceMount": "source=${localWorkspaceFolder},target=/usr/src/tdesktop,type=bind,consistency=cached", + "workspaceFolder": "/usr/src/tdesktop" +} diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 8eb20a2e5..eae84dc67 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -83,6 +83,7 @@ jobs: fi docker run --rm \ + -u $(id -u) \ -v $PWD:/usr/src/tdesktop \ -e CONFIG=Debug \ tdesktop:centos_env \ @@ -114,8 +115,8 @@ jobs: if: env.UPLOAD_ARTIFACT == 'true' run: | cd $REPO_NAME/out/Debug - sudo mkdir artifact - sudo mv {Telegram,Updater} artifact/ + mkdir artifact + mv {Telegram,Updater} artifact/ - uses: actions/upload-artifact@v4 if: env.UPLOAD_ARTIFACT == 'true' name: Upload artifact. diff --git a/.gitignore b/.gitignore index 4e6ebb2cf..42b53b2f2 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ Release/ ipch/ .vs/ .vscode/ +.cache/ /Telegram/log.txt /Telegram/data diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 92d2d0ff6..f79ceb35b 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -1041,6 +1041,7 @@ PRIVATE info/global_media/info_global_media_inner_widget.h info/global_media/info_global_media_provider.cpp info/global_media/info_global_media_provider.h + info/media/info_media_buttons.cpp info/media/info_media_buttons.h info/media/info_media_common.cpp info/media/info_media_common.h @@ -1285,6 +1286,8 @@ PRIVATE media/streaming/media_streaming_video_track.h media/view/media_view_group_thumbs.cpp media/view/media_view_group_thumbs.h + media/view/media_view_open_common.cpp + media/view/media_view_open_common.h media/view/media_view_overlay_opengl.cpp media/view/media_view_overlay_opengl.h media/view/media_view_overlay_raster.cpp @@ -1303,7 +1306,6 @@ PRIVATE media/view/media_view_playback_controls.h media/view/media_view_playback_progress.cpp media/view/media_view_playback_progress.h - media/view/media_view_open_common.h media/system_media_controls_manager.h media/system_media_controls_manager.cpp menu/menu_antispam_validator.cpp @@ -1553,6 +1555,8 @@ PRIVATE settings/settings_privacy_security.h settings/settings_scale_preview.cpp settings/settings_scale_preview.h + settings/settings_shortcuts.cpp + settings/settings_shortcuts.h settings/settings_type.h settings/settings_websites.cpp settings/settings_websites.h diff --git a/Telegram/Resources/default_shortcuts-custom.json b/Telegram/Resources/default_shortcuts-custom.json index 42d412b15..054ad70d0 100644 --- a/Telegram/Resources/default_shortcuts-custom.json +++ b/Telegram/Resources/default_shortcuts-custom.json @@ -1,6 +1,7 @@ // This is a list of your own shortcuts for Telegram Desktop // You can see full list of commands in the 'shortcuts-default.json' file // Place a null value instead of a command string to switch the shortcut off +// You can also edit them in Settings > Chat Settings > Keyboard Shortcuts. [ // { diff --git a/Telegram/Resources/icons/menu/shortcut.png b/Telegram/Resources/icons/menu/shortcut.png new file mode 100644 index 000000000..e5e3389e9 Binary files /dev/null and b/Telegram/Resources/icons/menu/shortcut.png differ diff --git a/Telegram/Resources/icons/menu/shortcut@2x.png b/Telegram/Resources/icons/menu/shortcut@2x.png new file mode 100644 index 000000000..f50a5dc71 Binary files /dev/null and b/Telegram/Resources/icons/menu/shortcut@2x.png differ diff --git a/Telegram/Resources/icons/menu/shortcut@3x.png b/Telegram/Resources/icons/menu/shortcut@3x.png new file mode 100644 index 000000000..5eb87725c Binary files /dev/null and b/Telegram/Resources/icons/menu/shortcut@3x.png differ diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 87554281d..1cf93bef0 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -642,6 +642,46 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_settings_chat_quick_action_react" = "Send reaction with double click"; "lng_settings_chat_corner_reaction" = "Reaction button on messages"; +"lng_settings_shortcuts" = "Keyboard shortcuts"; + +"lng_shortcuts_reset" = "Reset to default"; +"lng_shortcuts_recording" = "Recording..."; +"lng_shortcuts_add_another" = "Add another"; + +"lng_shortcuts_close" = "Close the window"; +"lng_shortcuts_lock" = "Lock the application"; +"lng_shortcuts_minimize" = "Minimize the window"; +"lng_shortcuts_quit" = "Quit the application"; +"lng_shortcuts_media_play" = "Play the media"; +"lng_shortcuts_media_pause" = "Pause the media"; +"lng_shortcuts_media_play_pause" = "Toggle media playback"; +"lng_shortcuts_media_stop" = "Stop media playback"; +"lng_shortcuts_media_previous" = "Previous track"; +"lng_shortcuts_media_next" = "Next track"; +"lng_shortcuts_search" = "Search messages"; +"lng_shortcuts_chat_previous" = "Previous chat"; +"lng_shortcuts_chat_next" = "Next chat"; +"lng_shortcuts_chat_first" = "First chat"; +"lng_shortcuts_chat_last" = "Last chat"; +"lng_shortcuts_chat_self" = "Saved Messages"; +"lng_shortcuts_chat_pinned_n" = "Pinned chat #{index}"; +"lng_shortcuts_show_account_n" = "Account #{index}"; +"lng_shortcuts_show_all_chats" = "All Chats folder"; +"lng_shortcuts_show_folder_n" = "Folder #{index}"; +"lng_shortcuts_show_folder_last" = "Last folder"; +"lng_shortcuts_folder_next" = "Next folder"; +"lng_shortcuts_folder_previous" = "Previous folder"; +"lng_shortcuts_scheduled" = "Scheduled messages"; +"lng_shortcuts_archive" = "Archived chats"; +"lng_shortcuts_contacts" = "Contacts list"; +"lng_shortcuts_just_send" = "Just send"; +"lng_shortcuts_silent_send" = "Silent send"; +"lng_shortcuts_schedule" = "Schedule"; +"lng_shortcuts_read_chat" = "Mark chat as read"; +"lng_shortcuts_archive_chat" = "Archive chat"; +"lng_shortcuts_media_fullscreen" = "Toggle video fullscreen"; +"lng_shortcuts_show_chat_menu" = "Show chat menu"; + "lng_settings_chat_reactions_title" = "Quick Reaction"; "lng_settings_chat_reactions_subtitle" = "Choose your favorite reaction"; "lng_settings_chat_message_reply_from" = "Bob Harris"; @@ -2551,6 +2591,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_premium_double_limits_subtitle_accounts" = "Connected Accounts"; "lng_premium_double_limits_about_accounts#one" = "Connect {count} account with different mobile numbers"; "lng_premium_double_limits_about_accounts#other" = "Connect {count} accounts with different mobile numbers"; + +"lng_premium_double_limits_subtitle_similar_channels" = "Similar Channel"; +"lng_premium_double_limits_about_similar_channels#one" = "View up to {count} similar channel"; +"lng_premium_double_limits_about_similar_channels#other" = "View up to {count} similar channels"; // "lng_premium_gift_title" = "Gift Telegram Premium"; @@ -3000,9 +3044,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_boost_channel_ask" = "Ask your **Premium** subscribers to boost your channel with this link:"; "lng_boost_channel_ask_button" = "Copy Link"; -"lng_boost_channel_or" = "or"; -"lng_boost_channel_gifting" = "Boost your channel by gifting your subscribers Telegram Premium. {link}"; -"lng_boost_channel_gifting_link" = "Get boosts >"; +//"lng_boost_channel_or" = "or"; +//"lng_boost_channel_gifting" = "Boost your channel by gifting your subscribers Telegram Premium. {link}"; +//"lng_boost_channel_gifting_link" = "Get boosts >"; +"lng_boost_group_ask" = "Ask your **Premium** members to boost your group with this link:"; +//"lng_boost_group_gifting" = "Boost your group by gifting your members Telegram Premium. {link}"; "lng_feature_stories#one" = "**{count}** Story Per Day"; "lng_feature_stories#other" = "**{count}** Stories Per Day"; @@ -3326,6 +3372,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_unique_backdrop" = "Backdrop"; "lng_gift_unique_symbol" = "Symbol"; "lng_gift_unique_rarity" = "Only {percent} of such collectibles have this attribute."; +"lng_gift_unique_availability_label" = "Quantity"; "lng_gift_unique_availability#one" = "{count} of {amount} issued"; "lng_gift_unique_availability#other" = "{count} of {amount} issued"; "lng_gift_unique_info" = "Gifted to {recipient} on {date}."; @@ -3836,6 +3883,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_choose_image" = "Choose an image"; "lng_choose_file" = "Choose a file"; "lng_choose_files" = "Choose Files"; +"lng_choose_cover" = "Choose video cover"; +"lng_choose_cover_bad" = "Can't use this file as a caption."; "lng_game_tag" = "Game"; "lng_context_new_window" = "Open in new window"; @@ -3952,6 +4001,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_context_read_show" = "show when"; "lng_context_edit_shortcut" = "Edit Shortcut"; "lng_context_delete_shortcut" = "Delete Quick Reply"; +"lng_context_gift_send" = "Send Another Gift"; "lng_add_tag_about" = "Tag this message with an emoji for quick search."; "lng_subscribe_tag_about" = "Organize your Saved Messages with tags. {link}"; @@ -3980,6 +4030,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_context_disable_spoiler" = "Remove Spoiler"; "lng_context_make_paid" = "Make This Content Paid"; "lng_context_change_price" = "Change Price"; +"lng_context_edit_cover" = "Edit Cover"; +"lng_context_clear_cover" = "Clear Cover"; "lng_context_mention" = "Mention"; "lng_context_search_from" = "Search messages"; @@ -4109,6 +4161,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_reply_cant_forward" = "Sorry, you can't reply to a message that was sent before the group was upgraded to a supergroup. Do you wish to forward it and add your comment?"; "lng_share_title" = "Share to"; +"lng_share_at_time_title" = "Share at {time} to"; "lng_share_copy_link" = "Copy share link"; "lng_share_confirm" = "Send"; "lng_share_wrong_user" = "This game was opened from a different user."; @@ -4232,7 +4285,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_media_save_progress" = "{ready} of {total} {mb}"; "lng_mediaview_save_as" = "Save As..."; "lng_mediaview_copy" = "Copy"; +"lng_mediaview_copy_frame" = "Copy Frame"; "lng_mediaview_forward" = "Forward"; +"lng_mediaview_share_at_time" = "Share at {time}"; "lng_mediaview_delete" = "Delete"; "lng_mediaview_save_to_profile" = "Post to Profile"; "lng_mediaview_pin_story_done" = "Story pinned"; diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml index a9f716c9e..f6451e78d 100644 --- a/Telegram/Resources/uwp/AppX/AppxManifest.xml +++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml @@ -10,7 +10,7 @@ + Version="5.11.1.0" /> Telegram Desktop Telegram Messenger LLP diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc index 2d5590296..a5a4b7eaa 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 5,10,7,0 - PRODUCTVERSION 5,10,7,0 + FILEVERSION 5,11,1,0 + PRODUCTVERSION 5,11,1,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -62,10 +62,10 @@ BEGIN BEGIN VALUE "CompanyName", "Radolyn Labs" VALUE "FileDescription", "AyuGram Desktop" - VALUE "FileVersion", "5.10.7.0" + VALUE "FileVersion", "5.11.1.0" VALUE "LegalCopyright", "Copyright (C) 2014-2025" VALUE "ProductName", "AyuGram Desktop" - VALUE "ProductVersion", "5.10.7.0" + VALUE "ProductVersion", "5.11.1.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc index 39318b864..783f6f30f 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 5,10,7,0 - PRODUCTVERSION 5,10,7,0 + FILEVERSION 5,11,1,0 + PRODUCTVERSION 5,11,1,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -53,10 +53,10 @@ BEGIN BEGIN VALUE "CompanyName", "Radolyn Labs" VALUE "FileDescription", "AyuGram Desktop Updater" - VALUE "FileVersion", "5.10.7.0" + VALUE "FileVersion", "5.11.1.0" VALUE "LegalCopyright", "Copyright (C) 2014-2025" VALUE "ProductName", "AyuGram Desktop" - VALUE "ProductVersion", "5.10.7.0" + VALUE "ProductVersion", "5.11.1.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/SourceFiles/api/api_authorizations.cpp b/Telegram/SourceFiles/api/api_authorizations.cpp index 78e463c11..6cd28824e 100644 --- a/Telegram/SourceFiles/api/api_authorizations.cpp +++ b/Telegram/SourceFiles/api/api_authorizations.cpp @@ -217,7 +217,11 @@ void Authorizations::toggleCallsDisabled(uint64 hash, bool disabled) { MTP_bool(disabled) )).done([=] { _toggleCallsDisabledRequests.remove(hash); - }).fail([=] { + }).fail([=](const MTP::Error &error) { + LOG(("API Error: toggle calls %1. Hash: %2. %3.") + .arg(disabled ? u"disabled"_q : u"enabled"_q) + .arg(hash) + .arg(error.type())); _toggleCallsDisabledRequests.remove(hash); }).send(); _toggleCallsDisabledRequests.emplace(hash, id); diff --git a/Telegram/SourceFiles/api/api_common.h b/Telegram/SourceFiles/api/api_common.h index 81f098d67..77c30d095 100644 --- a/Telegram/SourceFiles/api/api_common.h +++ b/Telegram/SourceFiles/api/api_common.h @@ -74,6 +74,7 @@ struct MessageToSend { struct RemoteFileInfo { MTPInputFile file; std::optional thumb; + std::optional videoCover; std::vector attachedStickers; }; diff --git a/Telegram/SourceFiles/api/api_editing.cpp b/Telegram/SourceFiles/api/api_editing.cpp index 313d6bfef..4f5073a3b 100644 --- a/Telegram/SourceFiles/api/api_editing.cpp +++ b/Telegram/SourceFiles/api/api_editing.cpp @@ -276,16 +276,22 @@ mtpRequestId EditTextMessage( takeFileReference = [=] { return photo->fileReference(); }; } else if (const auto document = media->document()) { using Flag = MTPDinputMediaDocument::Flag; + const auto videoCover = media->videoCover(); + const auto videoTimestamp = media->videoTimestamp(); const auto flags = Flag() | (media->ttlSeconds() ? Flag::f_ttl_seconds : Flag()) - | (spoilered ? Flag::f_spoiler : Flag()); + | (spoilered ? Flag::f_spoiler : Flag()) + | (videoTimestamp ? Flag::f_video_timestamp : Flag()) + | (videoCover ? Flag::f_video_cover : Flag()); takeInputMedia = [=] { return MTP_inputMediaDocument( MTP_flags(flags), document->mtpInput(), - MTPInputPhoto(), // video_cover + (videoCover + ? videoCover->mtpInput() + : MTPInputPhoto()), MTP_int(media->ttlSeconds()), - MTPint(), // video_timestamp + MTP_int(videoTimestamp), MTPstring()); // query }; takeFileReference = [=] { return document->fileReference(); }; diff --git a/Telegram/SourceFiles/api/api_global_privacy.cpp b/Telegram/SourceFiles/api/api_global_privacy.cpp index 83ad8d156..9e2ab1a38 100644 --- a/Telegram/SourceFiles/api/api_global_privacy.cpp +++ b/Telegram/SourceFiles/api/api_global_privacy.cpp @@ -13,6 +13,32 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Api { +PeerId ParsePaidReactionShownPeer( + not_null session, + const MTPPaidReactionPrivacy &value) { + return value.match([&](const MTPDpaidReactionPrivacyDefault &) { + return session->userPeerId(); + }, [](const MTPDpaidReactionPrivacyAnonymous &) { + return PeerId(); + }, [&](const MTPDpaidReactionPrivacyPeer &data) { + return data.vpeer().match([&](const MTPDinputPeerSelf &) { + return session->userPeerId(); + }, [](const MTPDinputPeerUser &data) { + return peerFromUser(data.vuser_id()); + }, [](const MTPDinputPeerChat &data) { + return peerFromChat(data.vchat_id()); + }, [](const MTPDinputPeerChannel &data) { + return peerFromChannel(data.vchannel_id()); + }, [](const MTPDinputPeerUserFromMessage &data) -> PeerId { + Unexpected("From message peer in ParsePaidReactionShownPeer."); + }, [](const MTPDinputPeerChannelFromMessage &data) -> PeerId { + Unexpected("From message peer in ParsePaidReactionShownPeer."); + }, [](const MTPDinputPeerEmpty &) -> PeerId { + Unexpected("Empty peer in ParsePaidReactionShownPeer."); + }); + }); +} + GlobalPrivacy::GlobalPrivacy(not_null api) : _session(&api->session()) , _api(&api->instance()) { @@ -115,27 +141,27 @@ rpl::producer GlobalPrivacy::newRequirePremium() const { return _newRequirePremium.value(); } -void GlobalPrivacy::loadPaidReactionAnonymous() { - if (_paidReactionAnonymousLoaded) { +void GlobalPrivacy::loadPaidReactionShownPeer() { + if (_paidReactionShownPeerLoaded) { return; } - _paidReactionAnonymousLoaded = true; + _paidReactionShownPeerLoaded = true; _api.request(MTPmessages_GetPaidReactionPrivacy( )).done([=](const MTPUpdates &result) { _session->api().applyUpdates(result); }).send(); } -void GlobalPrivacy::updatePaidReactionAnonymous(bool value) { - _paidReactionAnonymous = value; +void GlobalPrivacy::updatePaidReactionShownPeer(PeerId shownPeer) { + _paidReactionShownPeer = shownPeer; } -bool GlobalPrivacy::paidReactionAnonymousCurrent() const { - return _paidReactionAnonymous.current(); +PeerId GlobalPrivacy::paidReactionShownPeerCurrent() const { + return _paidReactionShownPeer.current(); } -rpl::producer GlobalPrivacy::paidReactionAnonymous() const { - return _paidReactionAnonymous.value(); +rpl::producer GlobalPrivacy::paidReactionShownPeer() const { + return _paidReactionShownPeer.value(); } void GlobalPrivacy::updateArchiveAndMute(bool value) { diff --git a/Telegram/SourceFiles/api/api_global_privacy.h b/Telegram/SourceFiles/api/api_global_privacy.h index aa7bb28fe..6c5848961 100644 --- a/Telegram/SourceFiles/api/api_global_privacy.h +++ b/Telegram/SourceFiles/api/api_global_privacy.h @@ -23,6 +23,10 @@ enum class UnarchiveOnNewMessage { AnyUnmuted, }; +[[nodiscard]] PeerId ParsePaidReactionShownPeer( + not_null session, + const MTPPaidReactionPrivacy &value); + class GlobalPrivacy final { public: explicit GlobalPrivacy(not_null api); @@ -49,10 +53,10 @@ public: [[nodiscard]] bool newRequirePremiumCurrent() const; [[nodiscard]] rpl::producer newRequirePremium() const; - void loadPaidReactionAnonymous(); - void updatePaidReactionAnonymous(bool value); - [[nodiscard]] bool paidReactionAnonymousCurrent() const; - [[nodiscard]] rpl::producer paidReactionAnonymous() const; + void loadPaidReactionShownPeer(); + void updatePaidReactionShownPeer(PeerId shownPeer); + [[nodiscard]] PeerId paidReactionShownPeerCurrent() const; + [[nodiscard]] rpl::producer paidReactionShownPeer() const; private: void apply(const MTPGlobalPrivacySettings &data); @@ -72,9 +76,9 @@ private: rpl::variable _showArchiveAndMute = false; rpl::variable _hideReadTime = false; rpl::variable _newRequirePremium = false; - rpl::variable _paidReactionAnonymous = false; + rpl::variable _paidReactionShownPeer = false; std::vector> _callbacks; - bool _paidReactionAnonymousLoaded = false; + bool _paidReactionShownPeerLoaded = false; }; diff --git a/Telegram/SourceFiles/api/api_media.cpp b/Telegram/SourceFiles/api/api_media.cpp index 3bbc00f5f..f7331694b 100644 --- a/Telegram/SourceFiles/api/api_media.cpp +++ b/Telegram/SourceFiles/api/api_media.cpp @@ -111,7 +111,8 @@ MTPInputMedia PrepareUploadedDocument( | (info.thumb ? Flag::f_thumb : Flag()) | (item->groupId() ? Flag::f_nosound_video : Flag()) | (info.attachedStickers.empty() ? Flag::f_stickers : Flag()) - | (ttlSeconds ? Flag::f_ttl_seconds : Flag()); + | (ttlSeconds ? Flag::f_ttl_seconds : Flag()) + | (info.videoCover ? Flag::f_video_cover : Flag()); const auto document = item->media()->document(); return MTP_inputMediaUploadedDocument( MTP_flags(flags), @@ -121,7 +122,7 @@ MTPInputMedia PrepareUploadedDocument( ComposeSendingDocumentAttributes(document), MTP_vector( ranges::to>(info.attachedStickers)), - MTPInputPhoto(), // video_cover + info.videoCover.value_or(MTPInputPhoto()), MTP_int(0), // video_timestamp MTP_int(ttlSeconds)); } diff --git a/Telegram/SourceFiles/api/api_sending.cpp b/Telegram/SourceFiles/api/api_sending.cpp index 193e2e71b..80b6f1007 100644 --- a/Telegram/SourceFiles/api/api_sending.cpp +++ b/Telegram/SourceFiles/api/api_sending.cpp @@ -549,10 +549,11 @@ void SendConfirmedFile( using Flag = MTPDmessageMediaDocument::Flag; return MTP_messageMediaDocument( MTP_flags(Flag::f_document - | (file->spoiler ? Flag::f_spoiler : Flag())), + | (file->spoiler ? Flag::f_spoiler : Flag()) + | (file->videoCover ? Flag::f_video_cover : Flag())), file->document, MTPVector(), // alt_documents - MTPPhoto(), // video_cover + file->videoCover ? file->videoCover->photo : MTPPhoto(), MTPint(), // video_timestamp MTPint()); } else if (file->type == SendMediaType::Audio) { @@ -561,10 +562,11 @@ void SendConfirmedFile( return MTP_messageMediaDocument( MTP_flags(Flag::f_document | Flag::f_voice - | (ttlSeconds ? Flag::f_ttl_seconds : Flag())), + | (ttlSeconds ? Flag::f_ttl_seconds : Flag()) + | (file->videoCover ? Flag::f_video_cover : Flag())), file->document, MTPVector(), // alt_documents - MTPPhoto(), // video_cover + file->videoCover ? file->videoCover->photo : MTPPhoto(), MTPint(), // video_timestamp MTP_int(ttlSeconds)); } else if (file->type == SendMediaType::Round) { diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index afcd00979..e34ef373d 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -2717,8 +2717,8 @@ void Updates::feedUpdate(const MTPUpdate &update) { case mtpc_updatePaidReactionPrivacy: { const auto &data = update.c_updatePaidReactionPrivacy(); - _session->api().globalPrivacy().updatePaidReactionAnonymous( - mtpIsTrue(data.vprivate())); + _session->api().globalPrivacy().updatePaidReactionShownPeer( + Api::ParsePaidReactionShownPeer(_session, data.vprivate())); } break; } diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 86cfd12f2..86a10680a 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -148,6 +148,16 @@ void ShowChannelsLimitBox(not_null peer) { action.replaceMediaOf); } +[[nodiscard]] QString FormatVideoTimestamp(TimeId seconds) { + const auto minutes = seconds / 60; + const auto hours = minutes / 60; + return hours + ? u"%1h%2m%3s"_q.arg(hours).arg(minutes % 60).arg(seconds % 60) + : minutes + ? u"%1m%2s"_q.arg(minutes).arg(seconds % 60) + : QString::number(seconds); +} + } // namespace ApiWrap::ApiWrap(not_null session) @@ -740,7 +750,8 @@ void ApiWrap::finalizeMessageDataRequest( QString ApiWrap::exportDirectMessageLink( not_null item, bool inRepliesContext, - bool forceNonPublicLink) { + bool forceNonPublicLink, + std::optional videoTimestamp) { Expects(item->history()->peer->isChannel()); const auto itemId = item->fullId(); @@ -792,19 +803,6 @@ QString ApiWrap::exportDirectMessageLink( : linkThreadId ? (QString::number(linkThreadId.bare) + '/' + post) : post); - if (linkChannel->hasUsername() - && !forceNonPublicLink - && !linkChannel->isMegagroup() - && !linkCommentId - && !linkThreadId) { - if (const auto media = item->media()) { - if (const auto document = media->document()) { - if (document->isVideoMessage()) { - return u"https://telesco.pe/"_q + query; - } - } - } - } return session().createInternalLinkFull(query); }; if (forceNonPublicLink) { @@ -826,7 +824,14 @@ QString ApiWrap::exportDirectMessageLink( _unlikelyMessageLinks.emplace_or_assign(itemId, link); } }).send(); - return current; + const auto addTimestamp = channel->hasUsername() + && !inRepliesContext + && videoTimestamp.has_value(); + const auto addedSeparator = (current.indexOf('?') >= 0) ? '&' : '?'; + const auto addedTimestamp = addTimestamp + ? (addedSeparator + u"t="_q + FormatVideoTimestamp(*videoTimestamp)) + : QString(); + return current + addedTimestamp; } QString ApiWrap::exportDirectStoryLink(not_null story) { @@ -3624,6 +3629,18 @@ void ApiWrap::editMedia( file.path, file.content, std::move(file.information), + (file.videoCover + ? std::make_unique( + &session(), + file.videoCover->path, + file.videoCover->content, + std::move(file.videoCover->information), + nullptr, + SendMediaType::Photo, + to, + TextWithTags(), + false) + : nullptr), type, to, caption, @@ -3665,6 +3682,19 @@ void ApiWrap::sendFiles( file.path, file.content, std::move(file.information), + (file.videoCover + ? std::make_unique( + &session(), + file.videoCover->path, + file.videoCover->content, + std::move(file.videoCover->information), + nullptr, + SendMediaType::Photo, + to, + TextWithTags(), + false, + nullptr) + : nullptr), uploadWithType, to, caption, @@ -3690,11 +3720,13 @@ void ApiWrap::sendFile( auto caption = TextWithTags(); const auto spoiler = false; const auto information = nullptr; + const auto videoCover = nullptr; _fileLoader->addTask(std::make_unique( &session(), QString(), fileContent, information, + videoCover, type, to, caption, @@ -4209,19 +4241,30 @@ void ApiWrap::uploadAlbumMedia( return; } const auto &fields = document->c_document(); + const auto mtpCover = data.vvideo_cover(); + const auto cover = (mtpCover && mtpCover->type() == mtpc_photo) + ? &(mtpCover->c_photo()) + : (const MTPDphoto*)nullptr; using Flag = MTPDinputMediaDocument::Flag; const auto flags = Flag() | (data.vttl_seconds() ? Flag::f_ttl_seconds : Flag()) - | (spoiler ? Flag::f_spoiler : Flag()); + | (spoiler ? Flag::f_spoiler : Flag()) + | (data.vvideo_timestamp() ? Flag::f_video_timestamp : Flag()) + | (cover ? Flag::f_video_cover : Flag()); const auto media = MTP_inputMediaDocument( MTP_flags(flags), MTP_inputDocument( fields.vid(), fields.vaccess_hash(), fields.vfile_reference()), - MTPInputPhoto(), // video_cover + (cover + ? MTP_inputPhoto( + cover->vid(), + cover->vaccess_hash(), + cover->vfile_reference()) + : MTPInputPhoto()), + MTP_int(data.vvideo_timestamp().value_or_empty()), MTP_int(data.vttl_seconds().value_or_empty()), - MTPint(), // video_timestamp MTPstring()); // query sendAlbumWithUploaded(item, groupId, media); } break; diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h index cfbb71256..d6e2bbc67 100644 --- a/Telegram/SourceFiles/apiwrap.h +++ b/Telegram/SourceFiles/apiwrap.h @@ -166,7 +166,8 @@ public: QString exportDirectMessageLink( not_null item, bool inRepliesContext, - bool forceNonPublicLink = false); + bool forceNonPublicLink = false, + std::optional videoTimestamp = {}); QString exportDirectStoryLink(not_null item); void requestContacts(); diff --git a/Telegram/SourceFiles/boxes/edit_caption_box.cpp b/Telegram/SourceFiles/boxes/edit_caption_box.cpp index ae2cbc3d7..25501bd29 100644 --- a/Telegram/SourceFiles/boxes/edit_caption_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_caption_box.cpp @@ -467,13 +467,16 @@ void EditCaptionBox::rebuildPreview() { } } else { const auto &file = _preparedList.files.front(); - + const auto isVideoFile = file.isVideoFile(); const auto media = Ui::SingleMediaPreview::Create( this, st::defaultComposeControls, gifPaused, file, - [] { return true; }, + [=](Ui::AttachActionType type) { + return (type != Ui::AttachActionType::EditCover) + || isVideoFile; + }, Ui::AttachControls::Type::EditOnly); _isPhoto = (media && media->isPhoto()); const auto withCheckbox = _isPhoto && CanBeCompressed(_albumType); @@ -719,7 +722,7 @@ void EditCaptionBox::setupPhotoEditorEventHandler() { controller->uiShow(), &_preparedList.files.front(), st::sendMediaPreviewSize, - [=] { rebuildPreview(); }); + [=](bool ok) { if (ok) rebuildPreview(); }); } else { EditPhotoImage(_controller, _photoMedia, hasSpoiler(), [=]( Ui::PreparedList &&list) { diff --git a/Telegram/SourceFiles/boxes/gift_premium_box.cpp b/Telegram/SourceFiles/boxes/gift_premium_box.cpp index 987538be5..db9eaf3a9 100644 --- a/Telegram/SourceFiles/boxes/gift_premium_box.cpp +++ b/Telegram/SourceFiles/boxes/gift_premium_box.cpp @@ -44,6 +44,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/basic_click_handlers.h" // UrlClickHandler::Open. #include "ui/boxes/boost_box.h" // StartFireworks. #include "ui/controls/userpic_button.h" +#include "ui/effects/credits_graphics.h" #include "ui/effects/premium_graphics.h" #include "ui/effects/premium_stars_colored.h" #include "ui/effects/premium_top_bar.h" @@ -405,21 +406,14 @@ void AddTableRow( auto result = object_ptr(table); const auto raw = result.data(); - const auto session = &show->session(); - const auto makeContext = [session](Fn update) { - return Core::MarkedTextContext{ - .session = session, - .customEmojiRepaint = std::move(update), - }; - }; - auto star = session->data().customEmojiManager().creditsEmoji(); + const auto star = Ui::CreateSingleStarWidget( + raw, + table->st().defaultValue.style.font->height); const auto label = Ui::CreateChild( raw, - rpl::single(star.append( - ' ' + Lang::FormatStarsAmountDecimal(entry.credits))), + Lang::FormatStarsAmountDecimal(entry.credits), table->st().defaultValue, - st::defaultPopupMenu, - std::move(makeContext)); + st::defaultPopupMenu); const auto convert = convertToStars ? Ui::CreateChild( @@ -430,7 +424,8 @@ void AddTableRow( table->st().smallButton) : nullptr; if (convert) { - convert->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); + using namespace Ui; + convert->setTextTransform(RoundButton::TextTransform::NoTransform); convert->setClickedCallback(std::move(convertToStars)); } rpl::combine( @@ -440,11 +435,13 @@ void AddTableRow( const auto convertSkip = convertWidth ? (st::normalFont->spacew + convertWidth) : 0; - label->resizeToNaturalWidth(width - convertSkip); - label->moveToLeft(0, 0, width); + const auto labelLeft = rect::right(star) + st::normalFont->spacew; + label->resizeToNaturalWidth(width - convertSkip - labelLeft); + star->moveToLeft(0, 0, width); + label->moveToLeft(labelLeft, 0, width); if (convert) { convert->moveToLeft( - label->width() + st::normalFont->spacew, + rect::right(label) + st::normalFont->spacew, (table->st().defaultValue.style.font->ascent - table->st().smallButton.style.font->ascent), width); @@ -1490,10 +1487,15 @@ void AddStarGiftTable( auto amount = rpl::single(TextWithEntities{ Lang::FormatCountDecimal(entry.limitedCount) }); + const auto count = unique + ? (entry.limitedCount - entry.limitedLeft) + : entry.limitedLeft; AddTableRow( table, - tr::lng_gift_availability(), - ((!unique && !entry.limitedLeft) + (unique + ? tr::lng_gift_unique_availability_label() + : tr::lng_gift_availability()), + ((!unique && !count) ? tr::lng_gift_availability_none( lt_amount, std::move(amount), @@ -1502,12 +1504,12 @@ void AddStarGiftTable( ? tr::lng_gift_unique_availability : tr::lng_gift_availability_left)( lt_count_decimal, - rpl::single(entry.limitedLeft * 1.), + rpl::single(count * 1.), lt_amount, std::move(amount), Ui::Text::WithEntities))); } - if (!unique && !entry.soldOutInfo) { + if (!unique && !entry.soldOutInfo && startUpgrade) { AddTableRow( table, tr::lng_gift_unique_status(), diff --git a/Telegram/SourceFiles/boxes/peer_list_box.cpp b/Telegram/SourceFiles/boxes/peer_list_box.cpp index 67ef1c81d..a9e33955b 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_box.cpp @@ -922,7 +922,13 @@ void PeerListRow::paintDisabledCheckUserpic( p.setPen(userpicBorderPen); p.setBrush(Qt::NoBrush); - p.drawEllipse(userpicEllipse); + if (peer()->forum()) { + const auto radius = userpicDiameter + * Ui::ForumUserpicRadiusMultiplier(); + p.drawRoundedRect(userpicEllipse, radius, radius); + } else { + p.drawEllipse(userpicEllipse); + } p.setPen(iconBorderPen); p.setBrush(st.disabledCheckFg); diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp index 8b827dc7c..73b3004f0 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp @@ -317,6 +317,7 @@ PreviewWrap::PreviewWrap( 0, // duration QString(), // author false, // hasLargeMedia + false, // photoIsVideoCover 0)) // pendingTill , _theme(theme) , _style(style) diff --git a/Telegram/SourceFiles/boxes/peers/peer_short_info_box.cpp b/Telegram/SourceFiles/boxes/peers/peer_short_info_box.cpp index 0275358f0..12fab358c 100644 --- a/Telegram/SourceFiles/boxes/peers/peer_short_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/peer_short_info_box.cpp @@ -106,6 +106,8 @@ PeerShortInfoCover::PeerShortInfoCover( , _statusStyle(std::make_unique(_st.status)) , _status(_widget.get(), std::move(status), _statusStyle->st) , _roundMask(Images::CornersMask(_st.radius)) +, _roundMaskRetina( + Images::CornersMask(_st.radius / style::DevicePixelRatio())) , _videoPaused(std::move(videoPaused)) { _widget->setCursor(_cursor); @@ -190,7 +192,7 @@ void PeerShortInfoCover::paint(QPainter &p) { if (!frame.isNull()) { frame = Images::Round( std::move(frame), - _roundMask, + _roundMaskRetina, RectPart::TopLeft | RectPart::TopRight); } else if (_userpicImage.isNull()) { auto image = QImage( @@ -226,10 +228,11 @@ void PeerShortInfoCover::paintCoverImage(QPainter &p, const QImage &image) { const auto top = _widget->height() - fill; const auto factor = style::DevicePixelRatio(); if (fill > 0) { + const auto t = roundedHeight + _scrollTop; p.drawImage( - QRect(0, top, roundedWidth, fill), + QRect(0, t, roundedWidth * factor, (roundedWidth - t) * factor), image, - QRect(0, top * factor, roundedWidth * factor, fill * factor)); + QRect(0, t, roundedWidth * factor, (roundedWidth - t) * factor)); } if (covered <= 0) { return; @@ -238,9 +241,9 @@ void PeerShortInfoCover::paintCoverImage(QPainter &p, const QImage &image) { const auto from = top - rounded; auto q = QPainter(&_roundedTopImage); q.drawImage( - QRect(0, 0, roundedWidth, rounded), + QRect(0, 0, roundedWidth * factor, rounded * factor), image, - QRect(0, from * factor, roundedWidth * factor, rounded * factor)); + QRect(0, _scrollTop, roundedWidth * factor, rounded * factor)); q.end(); _roundedTopImage = Images::Round( std::move(_roundedTopImage), diff --git a/Telegram/SourceFiles/boxes/peers/peer_short_info_box.h b/Telegram/SourceFiles/boxes/peers/peer_short_info_box.h index 271e1826c..f6baf8c74 100644 --- a/Telegram/SourceFiles/boxes/peers/peer_short_info_box.h +++ b/Telegram/SourceFiles/boxes/peers/peer_short_info_box.h @@ -123,6 +123,7 @@ private: object_ptr _additionalStatus = { nullptr }; std::array _roundMask; + std::array _roundMaskRetina; QImage _userpicImage; QImage _roundedTopImage; QImage _barSmall; diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.cpp b/Telegram/SourceFiles/boxes/premium_preview_box.cpp index 570fb75e6..53c6a6744 100644 --- a/Telegram/SourceFiles/boxes/premium_preview_box.cpp +++ b/Telegram/SourceFiles/boxes/premium_preview_box.cpp @@ -1526,6 +1526,18 @@ void DoubledLimitsPreviewBox( Main::Domain::kPremiumMaxAccounts, till, }); + { + const auto premium = limits.similarChannelsPremium(); + entries.push_back({ + tr::lng_premium_double_limits_subtitle_similar_channels(), + tr::lng_premium_double_limits_about_similar_channels( + lt_count, + rpl::single(float64(premium)), + Ui::Text::RichLangValue), + limits.similarChannelsDefault(), + premium, + }); + } Ui::Premium::ShowListBox( box, st::defaultPremiumLimits, diff --git a/Telegram/SourceFiles/boxes/send_files_box.cpp b/Telegram/SourceFiles/boxes/send_files_box.cpp index 0031f2d42..ad208b2db 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.cpp +++ b/Telegram/SourceFiles/boxes/send_files_box.cpp @@ -250,7 +250,7 @@ SendFilesBox::Block::Block( int till, Fn gifPaused, SendFilesWay way, - Fn canToggleSpoiler) + Fn actionAllowed) : _items(items) , _from(from) , _till(till) { @@ -268,7 +268,9 @@ SendFilesBox::Block::Block( st, my, way, - std::move(canToggleSpoiler)); + [=](int index, Ui::AttachActionType type) { + return actionAllowed((*_items)[from + index], type); + }); _preview.reset(preview); } else { const auto media = Ui::SingleMediaPreview::Create( @@ -276,7 +278,9 @@ SendFilesBox::Block::Block( st, gifPaused, first, - std::move(canToggleSpoiler)); + [=](Ui::AttachActionType type) { + return actionAllowed((*_items)[from], type); + }); if (media) { _isSingleMedia = true; _preview.reset(media); @@ -352,6 +356,38 @@ rpl::producer SendFilesBox::Block::itemModifyRequest() const { } } +rpl::producer SendFilesBox::Block::itemEditCoverRequest() const { + using namespace rpl::mappers; + + const auto preview = _preview.get(); + const auto from = _from; + if (_isAlbum) { + const auto album = static_cast(preview); + return album->thumbEditCoverRequested() | rpl::map(_1 + from); + } else if (_isSingleMedia) { + const auto media = static_cast(preview); + return media->editCoverRequests() | rpl::map_to(from); + } else { + return rpl::never(); + } +} + +rpl::producer SendFilesBox::Block::itemClearCoverRequest() const { + using namespace rpl::mappers; + + const auto preview = _preview.get(); + const auto from = _from; + if (_isAlbum) { + const auto album = static_cast(preview); + return album->thumbClearCoverRequested() | rpl::map(_1 + from); + } else if (_isSingleMedia) { + const auto media = static_cast(preview); + return media->clearCoverRequests() | rpl::map_to(from); + } else { + return rpl::never(); + } +} + rpl::producer<> SendFilesBox::Block::orderUpdated() const { if (_isAlbum) { const auto album = static_cast(_preview.get()); @@ -1046,7 +1082,16 @@ void SendFilesBox::pushBlock(int from, int till) { till, gifPaused, _sendWay.current(), - [=] { return !hasPrice(); }); + [=](const Ui::PreparedFile &file, Ui::AttachActionType type) { + return (type == Ui::AttachActionType::ToggleSpoiler) + ? !hasPrice() + : (type == Ui::AttachActionType::EditCover) + ? (file.isVideoFile() + && _captionToPeer + && (_captionToPeer->isBroadcast() + || _captionToPeer->isSelf())) + : (file.videoCover != nullptr); + }); auto &block = _blocks.back(); const auto widget = _inner->add( block.takeWidget(), @@ -1167,7 +1212,79 @@ void SendFilesBox::pushBlock(int from, int till) { show, &_list.files[index], st::sendMediaPreviewSize, - [=] { refreshAllAfterChanges(from); }); + [=](bool ok) { if (ok) refreshAllAfterChanges(from); }); + }, widget->lifetime()); + + block.itemEditCoverRequest( + ) | rpl::start_with_next([=, show = _show](int index) { + applyBlockChanges(); + + const auto replace = [=](Ui::PreparedList list) { + if (list.files.empty()) { + return; + } + auto &entry = _list.files[index]; + const auto video = entry.information + ? std::get_if( + &entry.information->media) + : nullptr; + if (!video) { + return; + } + auto old = std::shared_ptr( + std::move(entry.videoCover)); + entry.videoCover = std::make_unique( + std::move(list.files.front())); + Editor::OpenWithPreparedFile( + this, + show, + entry.videoCover.get(), + st::sendMediaPreviewSize, + crl::guard(this, [=](bool ok) { + if (!ok) { + _list.files[index].videoCover = old + ? std::make_unique( + std::move(*old)) + : nullptr; + } + refreshAllAfterChanges(from); + }), + video->thumbnail.size()); + }; + const auto checkResult = [=](const Ui::PreparedList &list) { + if (list.files.empty()) { + return true; + } + if (list.files.front().type != Ui::PreparedFile::Type::Photo) { + show->showToast(tr::lng_choose_cover_bad(tr::now)); + return false; + } + return true; + }; + const auto callback = [=](FileDialog::OpenResult &&result) { + const auto premium = _show->session().premium(); + FileDialogCallback( + std::move(result), + checkResult, + replace, + premium, + show); + }; + + FileDialog::GetOpenPath( + this, + tr::lng_choose_cover(tr::now), + FileDialog::ImagesFilter(), + crl::guard(this, callback)); + }, widget->lifetime()); + + block.itemClearCoverRequest( + ) | rpl::start_with_next([=](int index) { + applyBlockChanges(); + refreshAllAfterChanges(from, [&] { + auto &entry = _list.files[index]; + entry.videoCover = nullptr; + }); }, widget->lifetime()); block.orderUpdated() | rpl::start_with_next([=]{ diff --git a/Telegram/SourceFiles/boxes/send_files_box.h b/Telegram/SourceFiles/boxes/send_files_box.h index 9b4123d1c..9a0fea06d 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.h +++ b/Telegram/SourceFiles/boxes/send_files_box.h @@ -153,7 +153,9 @@ private: int till, Fn gifPaused, Ui::SendFilesWay way, - Fn canToggleSpoiler); + Fn actionAllowed); Block(Block &&other) = default; Block &operator=(Block &&other) = default; @@ -164,6 +166,8 @@ private: [[nodiscard]] rpl::producer itemDeleteRequest() const; [[nodiscard]] rpl::producer itemReplaceRequest() const; [[nodiscard]] rpl::producer itemModifyRequest() const; + [[nodiscard]] rpl::producer itemEditCoverRequest() const; + [[nodiscard]] rpl::producer itemClearCoverRequest() const; [[nodiscard]] rpl::producer<> orderUpdated() const; void setSendWay(Ui::SendFilesWay way); diff --git a/Telegram/SourceFiles/boxes/share_box.cpp b/Telegram/SourceFiles/boxes/share_box.cpp index 0c8079585..bfd4dfe9c 100644 --- a/Telegram/SourceFiles/boxes/share_box.cpp +++ b/Telegram/SourceFiles/boxes/share_box.cpp @@ -282,7 +282,9 @@ void ShareBox::prepare() { _select->resizeToWidth(st::boxWideWidth); Ui::SendPendingMoveResizeEvents(_select); - setTitle(tr::lng_share_title()); + setTitle(_descriptor.titleOverride + ? std::move(_descriptor.titleOverride) + : tr::lng_share_title()); _inner = setInnerWidget( object_ptr(this, _descriptor, uiShow()), @@ -1500,7 +1502,8 @@ ChatHelpers::ForwardedMessagePhraseArgs CreateForwardedMessagePhraseArgs( ShareBox::SubmitCallback ShareBox::DefaultForwardCallback( std::shared_ptr show, not_null history, - MessageIdsList msgIds) { + MessageIdsList msgIds, + std::optional videoTimestamp) { struct State final { base::flat_set requests; }; @@ -1536,6 +1539,9 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback( : Flag(0)) | ((forwardOptions == Data::ForwardOptions::NoNamesAndCaptions) ? Flag::f_drop_media_captions + : Flag(0)) + | (videoTimestamp.has_value() + ? Flag::f_video_timestamp : Flag(0)); auto mtpMsgIds = QVector(); mtpMsgIds.reserve(existingIds.size()); @@ -1593,7 +1599,7 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback( MTP_int(options.scheduled), MTP_inputPeerEmpty(), // send_as Data::ShortcutIdToMTP(session, options.shortcutId), - MTPint() // video_timestamp + MTP_int(videoTimestamp.value_or(0)) )).done([=](const MTPUpdates &updates, mtpRequestId reqId) { threadHistory->session().api().applyUpdates(updates); state->requests.remove(reqId); diff --git a/Telegram/SourceFiles/boxes/share_box.h b/Telegram/SourceFiles/boxes/share_box.h index f21b4505b..779a60e0a 100644 --- a/Telegram/SourceFiles/boxes/share_box.h +++ b/Telegram/SourceFiles/boxes/share_box.h @@ -104,7 +104,8 @@ public: [[nodiscard]] static SubmitCallback DefaultForwardCallback( std::shared_ptr show, not_null history, - MessageIdsList msgIds); + MessageIdsList msgIds, + std::optional videoTimestamp = {}); struct Descriptor { not_null session; @@ -113,7 +114,9 @@ public: FilterCallback filterCallback; object_ptr bottomWidget = { nullptr }; rpl::producer copyLinkText; + rpl::producer titleOverride; ShareBoxStyleOverrides st; + std::optional videoTimestamp; struct { int sendersCount = 0; int captionsCount = 0; diff --git a/Telegram/SourceFiles/boxes/star_gift_box.cpp b/Telegram/SourceFiles/boxes/star_gift_box.cpp index 50011546d..c99b64959 100644 --- a/Telegram/SourceFiles/boxes/star_gift_box.cpp +++ b/Telegram/SourceFiles/boxes/star_gift_box.cpp @@ -99,8 +99,8 @@ namespace Ui { namespace { constexpr auto kPriceTabAll = 0; -constexpr auto kPriceTabLimited = -1; -constexpr auto kPriceTabInStock = -2; +constexpr auto kPriceTabLimited = -2; +constexpr auto kPriceTabInStock = -1; constexpr auto kGiftMessageLimit = 255; constexpr auto kSentToastDuration = 3 * crl::time(1000); constexpr auto kSwitchUpgradeCoverInterval = 3 * crl::time(1000); @@ -2371,7 +2371,7 @@ void ShowUniqueGiftWearBox( object_ptr( raw, std::move(text), - st.infoAbout ? *st.infoAbout : st::boxDividerLabel), + st.infoAbout ? *st.infoAbout : st::upgradeGiftSubtext), st::settingsPremiumRowAboutPadding); object_ptr( raw, @@ -2603,7 +2603,7 @@ void UpgradeBox( object_ptr( raw, std::move(text), - st::boxDividerLabel), + st::upgradeGiftSubtext), st::settingsPremiumRowAboutPadding); object_ptr( raw, diff --git a/Telegram/SourceFiles/calls/calls_instance.cpp b/Telegram/SourceFiles/calls/calls_instance.cpp index 7d5793727..02c5fb144 100644 --- a/Telegram/SourceFiles/calls/calls_instance.cpp +++ b/Telegram/SourceFiles/calls/calls_instance.cpp @@ -578,8 +578,7 @@ void Instance::handleCallUpdate( if (inCall() && _currentCall->type() == Call::Type::Outgoing && _currentCall->user()->id == session->userPeerId() - && (peerFromUser(phoneCall.vparticipant_id()) - == _currentCall->user()->session().userPeerId())) { + && (user->id == _currentCall->user()->session().userPeerId())) { // Ignore call from the same running app, other account. return; } diff --git a/Telegram/SourceFiles/calls/calls_panel.cpp b/Telegram/SourceFiles/calls/calls_panel.cpp index 74ddcb8f2..673216650 100644 --- a/Telegram/SourceFiles/calls/calls_panel.cpp +++ b/Telegram/SourceFiles/calls/calls_panel.cpp @@ -42,6 +42,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/emoji_config.h" #include "ui/painter.h" #include "ui/rect.h" +#include "ui/integration.h" #include "core/application.h" #include "lang/lang_keys.h" #include "main/main_session.h" @@ -65,6 +66,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Calls { namespace { +constexpr auto kHideControlsTimeout = 5 * crl::time(1000); +constexpr auto kHideControlsQuickTimeout = 2 * crl::time(1000); + [[nodiscard]] QByteArray BatterySvg( const QSize &s, const QColor &c) { @@ -118,7 +122,9 @@ Panel::Panel(not_null call) st::callMicrophoneMute, &st::callMicrophoneUnmute)) , _name(widget(), st::callName) -, _status(widget(), st::callStatus) { +, _status(widget(), st::callStatus) +, _hideControlsTimer([=] { requestControlsHidden(true); }) +, _controlsShownForceTimer([=] { controlsShownForce(false); }) { _layerBg->setStyleOverrides(&st::groupCallBox, &st::groupCallLayerBox); _layerBg->setHideByBackgroundClick(true); @@ -188,6 +194,25 @@ void Panel::initWindow() { && window()->isFullScreen()) { window()->showNormal(); } + } else if (e->type() == QEvent::WindowStateChange) { + const auto state = window()->windowState(); + _fullScreenOrMaximized = (state & Qt::WindowFullScreen) + || (state & Qt::WindowMaximized); + } else if (e->type() == QEvent::Enter) { + _mouseInside = true; + Ui::Integration::Instance().registerLeaveSubscription( + window().get()); + if (!_fullScreenOrMaximized.current()) { + requestControlsHidden(false); + _hideControlsTimer.cancel(); + } + } else if (e->type() == QEvent::Leave) { + _mouseInside = false; + Ui::Integration::Instance().unregisterLeaveSubscription( + window().get()); + if (!_fullScreenOrMaximized.current()) { + _hideControlsTimer.callOnce(kHideControlsQuickTimeout); + } } return base::EventFilterResult::Continue; }); @@ -415,7 +440,11 @@ void Panel::refreshIncomingGeometry() { void Panel::reinitWithCall(Call *call) { _callLifetime.destroy(); _call = call; + const auto guard = gsl::finally([&] { + updateControlsShown(); + }); if (!_call) { + _fingerprint.destroy(); _incoming = nullptr; _outgoingVideoBubble = nullptr; _powerSaveBlocker = nullptr; @@ -457,6 +486,51 @@ void Panel::reinitWithCall(Call *call) { _window.backend()); _incoming->widget()->hide(); + _incoming->rp()->shownValue() | rpl::start_with_next([=] { + updateControlsShown(); + }, _incoming->rp()->lifetime()); + + _hideControlsFilter = nullptr; + _fullScreenOrMaximized.value( + ) | rpl::start_with_next([=](bool fullScreenOrMaximized) { + if (fullScreenOrMaximized) { + class Filter final : public QObject { + public: + explicit Filter(Fn moved) : _moved(moved) { + qApp->installEventFilter(this); + } + + bool eventFilter(QObject *watched, QEvent *event) { + if (event->type() == QEvent::MouseMove) { + _moved(watched); + } + return false; + } + + private: + Fn _moved; + + }; + _hideControlsFilter.reset(new Filter([=](QObject *what) { + _mouseInside = true; + if (what->isWidgetType() + && window()->isAncestorOf(static_cast(what))) { + _hideControlsTimer.callOnce(kHideControlsTimeout); + requestControlsHidden(false); + updateControlsShown(); + } + })); + _hideControlsTimer.callOnce(kHideControlsTimeout); + } else { + _hideControlsFilter = nullptr; + _hideControlsTimer.cancel(); + if (_mouseInside) { + requestControlsHidden(false); + updateControlsShown(); + } + } + }, _incoming->rp()->lifetime()); + _call->mutedValue( ) | rpl::start_with_next([=](bool mute) { _mute->entity()->setProgress(mute ? 1. : 0.); @@ -603,6 +677,8 @@ void Panel::createRemoteAudioMute() { const auto r = _remoteAudioMute->rect(); auto hq = PainterHighQualityEnabler(p); + p.setOpacity(_controlsShownAnimation.value( + _controlsShown ? 1. : 0.)); p.setBrush(st::videoPlayIconBg); p.setPen(Qt::NoPen); p.drawRoundedRect(r, r.height() / 2, r.height() / 2); @@ -661,6 +737,8 @@ void Panel::createRemoteLowBattery() { const auto r = _remoteLowBattery->rect(); auto hq = PainterHighQualityEnabler(p); + p.setOpacity(_controlsShownAnimation.value( + _controlsShown ? 1. : 0.)); p.setBrush(st::videoPlayIconBg); p.setPen(Qt::NoPen); p.drawRoundedRect(r, r.height() / 2, r.height() / 2); @@ -782,6 +860,9 @@ void Panel::showDevicesMenu( } Core::App().saveSettingsDelayed(); }; + controlsShownForce(true); + updateControlsShown(); + _devicesMenu = MakeDeviceSelectionMenu( widget(), &Core::App().mediaDevices(), @@ -791,6 +872,9 @@ void Panel::showDevicesMenu( Ui::PopupMenu::VerticalOrigin::Bottom); _devicesMenu->popup(button->mapToGlobal(QPoint()) - QPoint(st::callDeviceSelectionMenu.menu.widthMin / 2, 0)); + QObject::connect(_devicesMenu.get(), &QObject::destroyed, window(), [=] { + _controlsShownForceTimer.callOnce(kHideControlsQuickTimeout); + }); } void Panel::refreshOutgoingPreviewInBody(State state) { @@ -823,6 +907,33 @@ QRect Panel::outgoingFrameGeometry() const { return _outgoingVideoBubble->geometry(); } +void Panel::requestControlsHidden(bool hidden) { + _hideControlsRequested = hidden; + updateControlsShown(); +} + +void Panel::controlsShownForce(bool shown) { + _controlsShownForce = shown; + if (shown) { + _controlsShownForceTimer.cancel(); + } + updateControlsShown(); +} + +void Panel::updateControlsShown() { + const auto shown = !_incoming + || _incoming->widget()->isHidden() + || _controlsShownForce + || !_hideControlsRequested; + if (_controlsShown != shown) { + _controlsShown = shown; + _controlsShownAnimation.start([=] { + updateControlsGeometry(); + }, shown ? 0. : 1., shown ? 1. : 0., st::slideDuration); + updateControlsGeometry(); + } +} + void Panel::updateControlsGeometry() { if (widget()->size().isEmpty()) { return; @@ -830,6 +941,8 @@ void Panel::updateControlsGeometry() { if (_incoming) { refreshIncomingGeometry(); } + const auto shown = _controlsShownAnimation.value( + _controlsShown ? 1. : 0.); if (_fingerprint) { #ifndef Q_OS_MAC const auto controlsGeometry = _controls->controls.geometry(); @@ -848,14 +961,14 @@ void Panel::updateControlsGeometry() { const auto minRight = 0; #endif // _controls const auto desired = (widget()->width() - _fingerprint->width()) / 2; + const auto top = anim::interpolate( + -_fingerprint->height(), + st::callFingerprintTop, + shown); if (minLeft) { - _fingerprint->moveToLeft( - std::max(desired, minLeft), - st::callFingerprintTop); + _fingerprint->moveToLeft(std::max(desired, minLeft), top); } else { - _fingerprint->moveToRight( - std::max(desired, minRight), - st::callFingerprintTop); + _fingerprint->moveToRight(std::max(desired, minRight), top); } } const auto innerHeight = std::max(widget()->height(), st::callHeightMin); @@ -885,7 +998,11 @@ void Panel::updateControlsGeometry() { / (_outgoingPreviewInBody ? 3 : 2); _bodyTop = availableTop + skipHeight; - _buttonsTop = availableTop + available; + _buttonsTopShown = availableTop + available; + _buttonsTop = anim::interpolate( + widget()->height(), + _buttonsTopShown, + shown); const auto previewTop = _bodyTop + _bodySt->height + skipHeight; _userpic->setGeometry( @@ -908,6 +1025,8 @@ void Panel::updateControlsGeometry() { (_buttonsTop - st::callRemoteAudioMuteSkip - _remoteAudioMute->height())); + _remoteAudioMute->update(); + _remoteAudioMute->entity()->setOpacity(shown); } if (_remoteLowBattery) { _remoteLowBattery->moveToLeft( @@ -915,6 +1034,8 @@ void Panel::updateControlsGeometry() { (_buttonsTop - st::callRemoteAudioMuteSkip - _remoteLowBattery->height())); + _remoteLowBattery->update(); + _remoteLowBattery->entity()->setOpacity(shown); } if (_outgoingPreviewInBody) { @@ -925,7 +1046,7 @@ void Panel::updateControlsGeometry() { previewTop, bodyPreviewSize.width(), bodyPreviewSize.height())); - } else { + } else if (_outgoingVideoBubble) { updateOutgoingVideoBubbleGeometry(); } diff --git a/Telegram/SourceFiles/calls/calls_panel.h b/Telegram/SourceFiles/calls/calls_panel.h index e53045a44..ee24ab2c4 100644 --- a/Telegram/SourceFiles/calls/calls_panel.h +++ b/Telegram/SourceFiles/calls/calls_panel.h @@ -111,6 +111,9 @@ private: [[nodiscard]] bool handleClose() const; + void requestControlsHidden(bool hidden); + void controlsShownForce(bool shown); + void updateControlsShown(); void updateControlsGeometry(); void updateHangupGeometry(); void updateStatusGeometry(); @@ -177,8 +180,19 @@ private: std::unique_ptr _outgoingVideoBubble; QPixmap _bottomShadow; int _bodyTop = 0; + int _buttonsTopShown = 0; int _buttonsTop = 0; + base::Timer _hideControlsTimer; + base::Timer _controlsShownForceTimer; + std::unique_ptr _hideControlsFilter; + bool _hideControlsRequested = false; + rpl::variable _fullScreenOrMaximized; + Ui::Animations::Simple _controlsShownAnimation; + bool _controlsShownForce = false; + bool _controlsShown = true; + bool _mouseInside = false; + base::unique_qptr _devicesMenu; base::Timer _updateDurationTimer; diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index 7da5ba6c6..46e4aa04a 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -855,6 +855,17 @@ historyComposeButton: FlatButton { color: historyComposeButtonBgRipple; } } +historyGiftToChannel: IconButton(defaultIconButton) { + width: 46px; + height: 46px; + + icon: icon{{ "menu/gift_premium", windowActiveTextFg }}; + iconOver: icon{{ "menu/gift_premium", windowActiveTextFg }}; + + rippleAreaPosition: point(3px, 3px); + rippleAreaSize: 40px; + ripple: universalRippleAnimation; +} historyUnblock: FlatButton(historyComposeButton) { color: attentionButtonFg; overColor: attentionButtonFgOver; diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp index 6c6ad61dd..ba80c9604 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp @@ -628,6 +628,13 @@ void EmojiListWidget::applyNextSearchQuery() { const auto modeChanged = (_searchMode != searching); clearSelection(); if (modeChanged) { + if (_picker) { + _picker->hideAnimated(); + } + _colorAllRipple = nullptr; + for (auto &set : _custom) { + set.ripple = nullptr; + } _searchMode = searching; } if (!searching) { diff --git a/Telegram/SourceFiles/chat_helpers/stickers_lottie.cpp b/Telegram/SourceFiles/chat_helpers/stickers_lottie.cpp index 1a189fa7f..04ceda97c 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_lottie.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_lottie.cpp @@ -329,6 +329,7 @@ not_null GenerateLocalSticker( path, QByteArray(), nullptr, + nullptr, SendMediaType::File, FileLoadTo(0, {}, {}, 0), {}, diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp index 567caa00a..39a4c9701 100644 --- a/Telegram/SourceFiles/core/local_url_handlers.cpp +++ b/Telegram/SourceFiles/core/local_url_handlers.cpp @@ -61,6 +61,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/settings_privacy_security.h" #include "settings/settings_chat.h" #include "settings/settings_premium.h" +#include "storage/storage_account.h" #include "mainwidget.h" #include "main/main_account.h" #include "main/main_app_config.h" @@ -602,6 +603,8 @@ bool ResolveUsernameOrPhone( const auto threadParam = params.value(u"thread"_q); const auto threadId = topicId ? topicId : threadParam.toInt(); const auto gameParam = params.value(u"game"_q); + const auto videot = params.value(u"t"_q); + if (!gameParam.isEmpty() && validDomain(gameParam)) { startToken = gameParam; resolveType = ResolveType::ShareGame; @@ -618,6 +621,9 @@ bool ResolveUsernameOrPhone( .phone = phone, .messageId = post, .storyId = storyId, + .videoTimestamp = (!videot.isEmpty() + ? ParseVideoTimestamp(videot) + : std::optional()), .text = params.value(u"text"_q), .repliesInfo = commentId ? Window::RepliesByLinkInfo{ @@ -781,8 +787,8 @@ bool OpenMediaTimestamp( if (!controller) { return false; } - const auto time = match->captured(2).toInt(); - if (time < 0) { + const auto position = match->captured(2).toInt(); + if (position < 0) { return false; } const auto base = match->captured(1); @@ -795,7 +801,7 @@ bool OpenMediaTimestamp( const auto session = &controller->session(); const auto document = session->data().document(documentId); const auto context = session->data().message(itemId); - const auto timeMs = time * crl::time(1000); + const auto time = position * crl::time(1000); if (document->isVideoFile()) { controller->window().openInMediaView(Media::View::OpenRequest( controller, @@ -803,11 +809,9 @@ bool OpenMediaTimestamp( context, context ? context->topicRootId() : MsgId(0), false, - timeMs)); + time)); } else if (document->isSong() || document->isVoiceMessage()) { - session->settings().setMediaLastPlaybackPosition( - documentId, - timeMs); + session->local().setMediaLastPlaybackPosition(documentId, time); Media::Player::instance()->play({ document, itemId }); } return true; @@ -1759,4 +1763,14 @@ void ResolveAndShowUniqueGift( ResolveAndShowUniqueGift(std::move(show), slug, {}); } +TimeId ParseVideoTimestamp(QStringView value) { + const auto kExp = u"^(?:(\\d+)h)?(?:(\\d+)m)?(?:(\\d+)s)?$"_q; + const auto m = QRegularExpression(kExp).match(value); + return m.hasMatch() + ? (m.capturedView(1).toInt() * 3600 + + m.capturedView(2).toInt() * 60 + + m.capturedView(3).toInt()) + : value.toInt(); +} + } // namespace Core diff --git a/Telegram/SourceFiles/core/local_url_handlers.h b/Telegram/SourceFiles/core/local_url_handlers.h index 4f9b68003..ce71def0f 100644 --- a/Telegram/SourceFiles/core/local_url_handlers.h +++ b/Telegram/SourceFiles/core/local_url_handlers.h @@ -50,4 +50,6 @@ void ResolveAndShowUniqueGift( std::shared_ptr show, const QString &slug); +[[nodiscard]] TimeId ParseVideoTimestamp(QStringView value); + } // namespace Core diff --git a/Telegram/SourceFiles/core/shortcuts.cpp b/Telegram/SourceFiles/core/shortcuts.cpp index 93c6db96a..bd96398d4 100644 --- a/Telegram/SourceFiles/core/shortcuts.cpp +++ b/Telegram/SourceFiles/core/shortcuts.cpp @@ -28,6 +28,7 @@ namespace { constexpr auto kCountLimit = 2048; // How many shortcuts can be in json file. rpl::event_stream> RequestsStream; +bool Paused/* = false*/; const auto AutoRepeatCommands = base::flat_set{ Command::MediaPrevious, @@ -112,45 +113,15 @@ const auto CommandByName = base::flat_map{ // }; -const auto CommandNames = base::flat_map{ - { Command::Close , u"close_telegram"_q }, - { Command::Lock , u"lock_telegram"_q }, - { Command::Minimize , u"minimize_telegram"_q }, - { Command::Quit , u"quit_telegram"_q }, - - { Command::MediaPlay , u"media_play"_q }, - { Command::MediaPause , u"media_pause"_q }, - { Command::MediaPlayPause , u"media_playpause"_q }, - { Command::MediaStop , u"media_stop"_q }, - { Command::MediaPrevious , u"media_previous"_q }, - { Command::MediaNext , u"media_next"_q }, - - { Command::Search , u"search"_q }, - - { Command::ChatPrevious , u"previous_chat"_q }, - { Command::ChatNext , u"next_chat"_q }, - { Command::ChatFirst , u"first_chat"_q }, - { Command::ChatLast , u"last_chat"_q }, - { Command::ChatSelf , u"self_chat"_q }, - - { Command::FolderPrevious , u"previous_folder"_q }, - { Command::FolderNext , u"next_folder"_q }, - { Command::ShowAllChats , u"all_chats"_q }, - - { Command::ShowFolder1 , u"folder1"_q }, - { Command::ShowFolder2 , u"folder2"_q }, - { Command::ShowFolder3 , u"folder3"_q }, - { Command::ShowFolder4 , u"folder4"_q }, - { Command::ShowFolder5 , u"folder5"_q }, - { Command::ShowFolder6 , u"folder6"_q }, - { Command::ShowFolderLast , u"last_folder"_q }, - - { Command::ShowArchive , u"show_archive"_q }, - { Command::ShowContacts , u"show_contacts"_q }, - - { Command::ReadChat , u"read_chat"_q }, - - { Command::ShowChatMenu , u"show_chat_menu"_q }, +const base::flat_map &CommandNames() { + static const auto result = [&] { + auto result = base::flat_map(); + for (const auto &[name, command] : CommandByName) { + result.emplace(command, name); + } + return result; + }(); + return result; }; [[maybe_unused]] constexpr auto kNoValue = { @@ -175,19 +146,39 @@ public: [[nodiscard]] const QStringList &errors() const; + [[nodiscard]] auto keysDefaults() const + -> base::flat_map>; + [[nodiscard]] auto keysCurrents() const + -> base::flat_map>; + + void change( + QKeySequence was, + QKeySequence now, + Command command, + std::optional restore); + void resetToDefaults(); + private: void fillDefaults(); void writeDefaultFile(); + void writeCustomFile(); bool readCustomFile(); void set(const QString &keys, Command command, bool replace = false); + void set(const QKeySequence &result, Command command, bool replace); void remove(const QString &keys); + void remove(const QKeySequence &keys); void unregister(base::unique_qptr shortcut); + void pruneListened(); + QStringList _errors; base::flat_map> _shortcuts; base::flat_multi_map, Command> _commandByObject; + std::vector> _listened; + + base::flat_map> _defaults; base::flat_set _mediaShortcuts; base::flat_set _supportShortcuts; @@ -278,6 +269,54 @@ const QStringList &Manager::errors() const { return _errors; } +auto Manager::keysDefaults() const +-> base::flat_map> { + return _defaults; +} + +auto Manager::keysCurrents() const +-> base::flat_map> { + auto result = base::flat_map>(); + for (const auto &[keys, command] : _shortcuts) { + auto i = _commandByObject.findFirst(command); + const auto end = _commandByObject.end(); + for (; i != end && (i->first == command); ++i) { + result[keys].emplace(i->second); + } + } + return result; +} + +void Manager::change( + QKeySequence was, + QKeySequence now, + Command command, + std::optional restore) { + if (!was.isEmpty()) { + remove(was); + } + if (!now.isEmpty()) { + set(now, command, true); + } + if (restore) { + Assert(!was.isEmpty()); + set(was, *restore, true); + } + writeCustomFile(); +} + +void Manager::resetToDefaults() { + while (!_shortcuts.empty()) { + remove(_shortcuts.begin()->first); + } + for (const auto &[sequence, commands] : _defaults) { + for (const auto command : commands) { + set(sequence, command, false); + } + } + writeCustomFile(); +} + std::vector Manager::lookup(not_null object) const { auto result = std::vector(); auto i = _commandByObject.findFirst(object); @@ -301,11 +340,23 @@ void Manager::toggleSupport(bool toggled) { } void Manager::listen(not_null widget) { + pruneListened(); + _listened.push_back(widget.get()); for (const auto &[keys, shortcut] : _shortcuts) { widget->addAction(shortcut.get()); } } +void Manager::pruneListened() { + for (auto i = begin(_listened); i != end(_listened);) { + if (i->data()) { + ++i; + } else { + i = _listened.erase(i); + } + } +} + bool Manager::readCustomFile() { // read custom shortcuts from file if it exists or write an empty custom shortcuts file QFile file(CustomFilePath()); @@ -440,7 +491,9 @@ void Manager::fillDefaults() { set(u"ctrl+r"_q, Command::ReadChat); - set(u"ctrl+="_q, Command::ShowChatMenu); + set(u"ctrl+\\"_q, Command::ShowChatMenu); + + _defaults = keysCurrents(); } void Manager::writeDefaultFile() { @@ -466,8 +519,8 @@ void Manager::writeDefaultFile() { auto i = _commandByObject.findFirst(object); const auto end = _commandByObject.end(); for (; i != end && i->first == object; ++i) { - const auto j = CommandNames.find(i->second); - if (j != CommandNames.end()) { + const auto j = CommandNames().find(i->second); + if (j != CommandNames().end()) { QJsonObject entry; entry.insert(u"keys"_q, sequence.toString().toLower()); entry.insert(u"command"_q, j->second); @@ -488,6 +541,55 @@ void Manager::writeDefaultFile() { } } + auto document = QJsonDocument(); + document.setArray(shortcuts); + file.write(document.toJson(QJsonDocument::Indented)); +} + +void Manager::writeCustomFile() { + auto shortcuts = QJsonArray(); + for (const auto &[sequence, shortcut] : _shortcuts) { + const auto object = shortcut.get(); + auto i = _commandByObject.findFirst(object); + const auto end = _commandByObject.end(); + for (; i != end && i->first == object; ++i) { + const auto d = _defaults.find(sequence); + if (d == _defaults.end() || !d->second.contains(i->second)) { + const auto j = CommandNames().find(i->second); + if (j != CommandNames().end()) { + QJsonObject entry; + entry.insert(u"keys"_q, sequence.toString().toLower()); + entry.insert(u"command"_q, j->second); + shortcuts.append(entry); + } + } + } + } + for (const auto &[sequence, command] : _defaults) { + if (!_shortcuts.contains(sequence)) { + QJsonObject entry; + entry.insert(u"keys"_q, sequence.toString().toLower()); + entry.insert(u"command"_q, QJsonValue()); + shortcuts.append(entry); + } + } + + if (shortcuts.isEmpty()) { + WriteDefaultCustomFile(); + return; + } + + auto file = QFile(CustomFilePath()); + if (!file.open(QIODevice::WriteOnly)) { + LOG(("Shortcut Warning: could not write custom shortcuts file.")); + return; + } + const char *customHeader = R"HEADER( +// This is a list of changed shortcuts for Telegram Desktop +// You can edit them in Settings > Chat Settings > Keyboard Shortcuts. + +)HEADER"; + file.write(customHeader); auto document = QJsonDocument(); document.setArray(shortcuts); @@ -504,8 +606,15 @@ void Manager::set(const QString &keys, Command command, bool replace) { _errors.push_back(u"Could not derive key sequence '%1'!"_q.arg(keys)); return; } + set(result, command, replace); +} + +void Manager::set( + const QKeySequence &keys, + Command command, + bool replace) { auto shortcut = base::make_unique_q(); - shortcut->setShortcut(result); + shortcut->setShortcut(keys); shortcut->setShortcutContext(Qt::ApplicationShortcut); if (!AutoRepeatCommands.contains(command)) { shortcut->setAutoRepeat(false); @@ -516,20 +625,26 @@ void Manager::set(const QString &keys, Command command, bool replace) { shortcut->setEnabled(false); } auto object = shortcut.get(); - auto i = _shortcuts.find(result); + auto i = _shortcuts.find(keys); if (i == end(_shortcuts)) { - i = _shortcuts.emplace(result, std::move(shortcut)).first; + i = _shortcuts.emplace(keys, std::move(shortcut)).first; } else if (replace) { unregister(std::exchange(i->second, std::move(shortcut))); } else { object = i->second.get(); } _commandByObject.emplace(object, command); - if (!shortcut && isMediaShortcut) { - _mediaShortcuts.emplace(i->second.get()); - } - if (!shortcut && isSupportShortcut) { - _supportShortcuts.emplace(i->second.get()); + if (!shortcut) { // Added the new one. + if (isMediaShortcut) { + _mediaShortcuts.emplace(i->second.get()); + } + if (isSupportShortcut) { + _supportShortcuts.emplace(i->second.get()); + } + pruneListened(); + for (const auto &widget : _listened) { + widget->addAction(i->second.get()); + } } } @@ -543,7 +658,11 @@ void Manager::remove(const QString &keys) { _errors.push_back(u"Could not derive key sequence '%1'!"_q.arg(keys)); return; } - const auto i = _shortcuts.find(result); + remove(result); +} + +void Manager::remove(const QKeySequence &keys) { + const auto i = _shortcuts.find(keys); if (i != end(_shortcuts)) { unregister(std::move(i->second)); _shortcuts.erase(i); @@ -552,7 +671,7 @@ void Manager::remove(const QString &keys) { void Manager::unregister(base::unique_qptr shortcut) { if (shortcut) { - _commandByObject.erase(shortcut.get()); + _commandByObject.removeAll(shortcut.get()); _mediaShortcuts.erase(shortcut.get()); _supportShortcuts.erase(shortcut.get()); } @@ -598,7 +717,9 @@ bool Launch(Command command) { } bool Launch(std::vector commands) { - if (auto handler = RequestHandler(std::move(commands))) { + if (Paused) { + return false; + } else if (auto handler = RequestHandler(std::move(commands))) { return handler(); } return false; @@ -630,6 +751,69 @@ void ToggleSupportShortcuts(bool toggled) { Data.toggleSupport(toggled); } +void Pause() { + Paused = true; +} + +void Unpause() { + Paused = false; +} + +auto KeysDefaults() +-> base::flat_map> { + return Data.keysDefaults(); +} + +auto KeysCurrents() +-> base::flat_map> { + return Data.keysCurrents(); +} + +void Change( + QKeySequence was, + QKeySequence now, + Command command, + std::optional restore) { + Data.change(was, now, command, restore); +} + +void ResetToDefaults() { + Data.resetToDefaults(); +} + +bool AllowWithoutModifiers(int key) { + const auto service = { + Qt::Key_Escape, + Qt::Key_Tab, + Qt::Key_Backtab, + Qt::Key_Backspace, + Qt::Key_Return, + Qt::Key_Enter, + Qt::Key_Insert, + Qt::Key_Delete, + Qt::Key_Pause, + Qt::Key_Print, + Qt::Key_SysReq, + Qt::Key_Clear, + Qt::Key_Home, + Qt::Key_End, + Qt::Key_Left, + Qt::Key_Up, + Qt::Key_Right, + Qt::Key_Down, + Qt::Key_PageUp, + Qt::Key_PageDown, + Qt::Key_Shift, + Qt::Key_Control, + Qt::Key_Meta, + Qt::Key_Alt, + Qt::Key_CapsLock, + Qt::Key_NumLock, + Qt::Key_ScrollLock, + }; + return (key >= 0x80) && !ranges::contains(service, key); +} + void Finish() { Data.clear(); } diff --git a/Telegram/SourceFiles/core/shortcuts.h b/Telegram/SourceFiles/core/shortcuts.h index 5355d1681..70c8eb63a 100644 --- a/Telegram/SourceFiles/core/shortcuts.h +++ b/Telegram/SourceFiles/core/shortcuts.h @@ -139,4 +139,21 @@ void ToggleMediaShortcuts(bool toggled); // have some conflicts with default input shortcuts, like Ctrl+Delete. void ToggleSupportShortcuts(bool toggled); +void Pause(); +void Unpause(); + +[[nodiscard]] auto KeysDefaults() +-> base::flat_map>; +[[nodiscard]] auto KeysCurrents() +-> base::flat_map>; + +void Change( + QKeySequence was, + QKeySequence now, + Command command, + std::optional restore = {}); +void ResetToDefaults(); + +[[nodiscard]] bool AllowWithoutModifiers(int key); + } // namespace Shortcuts diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h index ba95d96d5..68e59591d 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 = 5010007; -constexpr auto AppVersionStr = "5.10.7"; +constexpr auto AppVersion = 5011001; +constexpr auto AppVersionStr = "5.11.1"; constexpr auto AppBetaVersion = false; constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION; diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp index 22041523d..79b9fe4e6 100644 --- a/Telegram/SourceFiles/data/data_channel.cpp +++ b/Telegram/SourceFiles/data/data_channel.cpp @@ -1012,7 +1012,7 @@ PeerId ChannelData::groupCallDefaultJoinAs() const { void ChannelData::setAllowedReactions(Data::AllowedReactions value) { if (_allowedReactions != value) { if (value.paidEnabled) { - session().api().globalPrivacy().loadPaidReactionAnonymous(); + session().api().globalPrivacy().loadPaidReactionShownPeer(); } const auto enabled = [](const Data::AllowedReactions &allowed) { return (allowed.type != Data::AllowedReactionsType::Some) diff --git a/Telegram/SourceFiles/data/data_document.cpp b/Telegram/SourceFiles/data/data_document.cpp index c14d29bfc..c6befff04 100644 --- a/Telegram/SourceFiles/data/data_document.cpp +++ b/Telegram/SourceFiles/data/data_document.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_streaming.h" #include "data/data_document_media.h" #include "data/data_reply_preview.h" +#include "data/data_web_page.h" #include "lang/lang_keys.h" #include "inline_bots/inline_bot_layout_item.h" #include "main/main_session.h" @@ -1881,3 +1882,18 @@ void DocumentData::collectLocalData(not_null local) { session().local().writeFileLocation(mediaKey(), _location); } } + +PhotoData *LookupVideoCover( + not_null document, + HistoryItem *item) { + const auto media = item ? item->media() : nullptr; + if (const auto webpage = media ? media->webpage() : nullptr) { + if (webpage->document == document && webpage->photoIsVideoCover) { + return webpage->photo; + } + return nullptr; + } + return (media && media->document() == document) + ? media->videoCover() + : nullptr; +} diff --git a/Telegram/SourceFiles/data/data_document.h b/Telegram/SourceFiles/data/data_document.h index 29f4bde56..eb01b6076 100644 --- a/Telegram/SourceFiles/data/data_document.h +++ b/Telegram/SourceFiles/data/data_document.h @@ -13,6 +13,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_cloud_file.h" #include "core/file_location.h" +class HistoryItem; +class PhotoData; enum class ChatRestriction; class mtpFileLoader; @@ -402,6 +404,10 @@ private: }; +[[nodiscard]] PhotoData *LookupVideoCover( + not_null document, + HistoryItem *item); + VoiceWaveform documentWaveformDecode(const QByteArray &encoded5bit); QByteArray documentWaveformEncode5bit(const VoiceWaveform &waveform); diff --git a/Telegram/SourceFiles/data/data_file_origin.cpp b/Telegram/SourceFiles/data/data_file_origin.cpp index 06c39cb91..e92845840 100644 --- a/Telegram/SourceFiles/data/data_file_origin.cpp +++ b/Telegram/SourceFiles/data/data_file_origin.cpp @@ -105,6 +105,7 @@ struct FileReferenceAccumulator { push(data.vphoto()); }, [&](const MTPDmessageMediaDocument &data) { push(data.vdocument()); + push(data.vvideo_cover()); push(data.valt_documents()); }, [&](const MTPDmessageMediaWebPage &data) { push(data.vwebpage()); diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp index b3d57000e..02c129987 100644 --- a/Telegram/SourceFiles/data/data_media_types.cpp +++ b/Telegram/SourceFiles/data/data_media_types.cpp @@ -447,16 +447,17 @@ Call ComputeCallData(const MTPDmessageActionPhoneCall &call) { auto result = Call(); result.finishReason = [&] { if (const auto reason = call.vreason()) { - switch (reason->type()) { - case mtpc_phoneCallDiscardReasonBusy: + return reason->match([](const MTPDphoneCallDiscardReasonBusy &) { return CallFinishReason::Busy; - case mtpc_phoneCallDiscardReasonDisconnect: + }, [](const MTPDphoneCallDiscardReasonDisconnect &) { return CallFinishReason::Disconnected; - case mtpc_phoneCallDiscardReasonHangup: + }, [](const MTPDphoneCallDiscardReasonHangup &) { return CallFinishReason::Hangup; - case mtpc_phoneCallDiscardReasonMissed: + }, [](const MTPDphoneCallDiscardReasonMissed &) { return CallFinishReason::Missed; - } + }, [](const MTPDphoneCallDiscardReasonAllowGroupCall &) { + return CallFinishReason::AllowGroupCall; + }); Unexpected("Call reason type."); } return CallFinishReason::Hangup; @@ -552,6 +553,14 @@ DocumentData *Media::document() const { return nullptr; } +PhotoData *Media::videoCover() const { + return nullptr; +} + +TimeId Media::videoTimestamp() const { + return 0; +} + bool Media::hasQualitiesList() const { return false; } @@ -968,17 +977,16 @@ std::unique_ptr MediaPhoto::createView( MediaFile::MediaFile( not_null parent, not_null document, - bool skipPremiumEffect, - bool hasQualitiesList, - bool spoiler, - crl::time ttlSeconds) + Args &&args) : Media(parent) , _document(document) +, _videoCover(args.videoCover) +, _ttlSeconds(args.ttlSeconds) , _emoji(document->sticker() ? document->sticker()->alt : QString()) -, _skipPremiumEffect(skipPremiumEffect) -, _hasQualitiesList(hasQualitiesList) -, _spoiler(spoiler) -, _ttlSeconds(ttlSeconds) { +, _videoTimestamp(args.videoTimestamp) +, _skipPremiumEffect(args.skipPremiumEffect) +, _hasQualitiesList(args.hasQualitiesList) +, _spoiler(args.spoiler) { parent->history()->owner().registerDocumentItem(_document, parent); if (!_emoji.isEmpty()) { @@ -1002,19 +1010,28 @@ MediaFile::~MediaFile() { } std::unique_ptr MediaFile::clone(not_null parent) { - return std::make_unique( - parent, - _document, - !_document->session().premium(), - _hasQualitiesList, - _spoiler, - _ttlSeconds); + return std::make_unique(parent, _document, MediaFile::Args{ + .ttlSeconds = _ttlSeconds, + .videoCover = _videoCover, + .videoTimestamp = _videoTimestamp, + .hasQualitiesList = _hasQualitiesList, + .skipPremiumEffect = !_document->session().premium(), + .spoiler = _spoiler, + }); } DocumentData *MediaFile::document() const { return _document; } +PhotoData *MediaFile::videoCover() const { + return _videoCover; +} + +TimeId MediaFile::videoTimestamp() const { + return _videoTimestamp; +} + bool MediaFile::hasQualitiesList() const { return _hasQualitiesList; } @@ -1285,7 +1302,11 @@ bool MediaFile::updateSentMedia(const MTPMessageMedia &media) { "or with ttl_seconds in updateSentMedia()")); return false; } - parent()->history()->owner().documentConvert(_document, *content); + const auto owner = &parent()->history()->owner(); + owner->documentConvert(_document, *content); + if (const auto cover = _videoCover ? data.vvideo_cover() : nullptr) { + owner->photoConvert(_videoCover, *cover); + } return true; } diff --git a/Telegram/SourceFiles/data/data_media_types.h b/Telegram/SourceFiles/data/data_media_types.h index 739cc7839..b6e2a32bb 100644 --- a/Telegram/SourceFiles/data/data_media_types.h +++ b/Telegram/SourceFiles/data/data_media_types.h @@ -46,6 +46,7 @@ enum class CallFinishReason : char { Busy, Disconnected, Hangup, + AllowGroupCall, }; struct SharedContact final { @@ -178,6 +179,8 @@ public: virtual std::unique_ptr clone(not_null parent) = 0; virtual DocumentData *document() const; + virtual PhotoData *videoCover() const; + virtual TimeId videoTimestamp() const; virtual bool hasQualitiesList() const; virtual PhotoData *photo() const; virtual WebPageData *webpage() const; @@ -297,18 +300,26 @@ private: class MediaFile final : public Media { public: + struct Args { + crl::time ttlSeconds = 0; + PhotoData *videoCover = nullptr; + TimeId videoTimestamp = 0; + bool hasQualitiesList = false; + bool skipPremiumEffect = false; + bool spoiler = false; + }; + MediaFile( not_null parent, not_null document, - bool skipPremiumEffect, - bool hasQualitiesList, - bool spoiler, - crl::time ttlSeconds); + Args &&args); ~MediaFile(); std::unique_ptr clone(not_null parent) override; DocumentData *document() const override; + PhotoData *videoCover() const override; + TimeId videoTimestamp() const override; bool hasQualitiesList() const override; bool uploading() const override; @@ -338,14 +349,17 @@ public: private: not_null _document; - QString _emoji; - bool _skipPremiumEffect = false; - bool _hasQualitiesList = false; - bool _spoiler = false; + PhotoData *_videoCover = nullptr; // Video (unsupported) / Voice / Round. crl::time _ttlSeconds = 0; + QString _emoji; + TimeId _videoTimestamp = 0; + bool _skipPremiumEffect = false; + bool _hasQualitiesList = false; + bool _spoiler = false; + }; class MediaContact final : public Media { diff --git a/Telegram/SourceFiles/data/data_message_reactions.cpp b/Telegram/SourceFiles/data/data_message_reactions.cpp index f0166b048..6445533e8 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.cpp +++ b/Telegram/SourceFiles/data/data_message_reactions.cpp @@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_app_config.h" #include "main/session/send_as_peers.h" #include "data/components/credits.h" +#include "data/data_channel.h" #include "data/data_user.h" #include "data/data_session.h" #include "data/data_histories.h" @@ -158,8 +159,23 @@ constexpr auto kPaidAccumulatePeriod = 5 * crl::time(1000) + 500; return (i != end(top)) && i->my; } -[[nodiscard]] std::optional MaybeAnonymous(uint32 privacySet, uint32 anonymous) { - return privacySet ? (anonymous == 1) : std::optional(); +[[nodiscard]] std::optional MaybeShownPeer( + uint32 privacySet, + PeerId shownPeer) { + return privacySet ? shownPeer : std::optional(); +} + +[[nodiscard]] MTPPaidReactionPrivacy PaidReactionShownPeerToTL( + not_null session, + std::optional shownPeer) { + return !shownPeer + ? MTPPaidReactionPrivacy() + : !*shownPeer + ? MTP_paidReactionPrivacyAnonymous() + : (*shownPeer == session->userPeerId()) + ? MTP_paidReactionPrivacyDefault() + : MTP_paidReactionPrivacyPeer( + session->data().peer(*shownPeer)->input); } } // namespace @@ -180,6 +196,13 @@ PossibleItemReactionsRef LookupPossibleReactions( } } const auto session = &peer->session(); + if (const auto channel = peer->asChannel()) { + if ((!channel->amCreator()) + && (channel->adminRights() & ChatAdminRight::Anonymous) + && (session->sendAsPeers().resolveChosen(channel) == channel)) { + return {}; + } + } const auto reactions = &session->data().reactions(); const auto &full = reactions->list(Reactions::Type::Active); const auto &top = reactions->list(Reactions::Type::Top); @@ -1752,7 +1775,7 @@ void Reactions::sendPaidPrivacyRequest( not_null item, PaidReactionSend send) { Expects(!_sendingPaid.contains(item)); - Expects(send.anonymous.has_value()); + Expects(send.shownPeer.has_value()); Expects(!send.count); const auto id = item->fullId(); @@ -1761,7 +1784,7 @@ void Reactions::sendPaidPrivacyRequest( MTPmessages_TogglePaidReactionPrivacy( item->history()->peer->input, MTP_int(id.msg), - MTP_bool(*send.anonymous)) + PaidReactionShownPeerToTL(&_owner->session(), send.shownPeer)) ).done([=] { if (const auto item = _owner->message(id)) { if (_sendingPaid.remove(item)) { @@ -1795,12 +1818,14 @@ void Reactions::sendPaidRequest( auto &api = _owner->session().api(); using Flag = MTPmessages_SendPaidReaction::Flag; const auto requestId = api.request(MTPmessages_SendPaidReaction( - MTP_flags(send.anonymous ? Flag::f_private : Flag()), + MTP_flags(send.shownPeer ? Flag::f_private : Flag()), item->history()->peer->input, MTP_int(id.msg), MTP_int(send.count), MTP_long(randomId), - MTP_bool(send.anonymous.value_or(false)) + (!send.shownPeer + ? MTPPaidReactionPrivacy() + : PaidReactionShownPeerToTL(&_owner->session(), *send.shownPeer)) )).done([=](const MTPUpdates &result) { if (const auto item = _owner->message(id)) { if (_sendingPaid.remove(item)) { @@ -1851,9 +1876,9 @@ MessageReactions::~MessageReactions() { finishPaidSending({ .count = int(paid->sending), .valid = true, - .anonymous = MaybeAnonymous( + .shownPeer = MaybeShownPeer( paid->sendingPrivacySet, - paid->sendingAnonymous), + paid->sendingShownPeer), }, false); } } @@ -2217,7 +2242,7 @@ void MessageReactions::markRead() { void MessageReactions::scheduleSendPaid( int count, - std::optional anonymous) { + std::optional shownPeer) { Expects(count >= 0); if (!_paid) { @@ -2225,9 +2250,9 @@ void MessageReactions::scheduleSendPaid( } _paid->scheduled += count; _paid->scheduledFlag = 1; - if (anonymous.has_value()) { - _paid->scheduledAnonymous = anonymous.value_or(false) ? 1 : 0; - _paid->scheduledPrivacySet = anonymous.has_value(); + if (shownPeer.has_value()) { + _paid->scheduledShownPeer = *shownPeer; + _paid->scheduledPrivacySet = true; } if (count > 0) { _item->history()->session().credits().lock(StarsAmount(count)); @@ -2248,7 +2273,7 @@ void MessageReactions::cancelScheduledPaid() { } _paid->scheduled = 0; _paid->scheduledFlag = 0; - _paid->scheduledAnonymous = 0; + _paid->scheduledShownPeer = 0; _paid->scheduledPrivacySet = 0; } if (!_paid->sendingFlag && _paid->top.empty()) { @@ -2263,18 +2288,18 @@ PaidReactionSend MessageReactions::startPaidSending() { } _paid->sending = _paid->scheduled; _paid->sendingFlag = _paid->scheduledFlag; - _paid->sendingAnonymous = _paid->scheduledAnonymous; + _paid->sendingShownPeer = _paid->scheduledShownPeer; _paid->sendingPrivacySet = _paid->scheduledPrivacySet; _paid->scheduled = 0; _paid->scheduledFlag = 0; - _paid->scheduledAnonymous = 0; + _paid->scheduledShownPeer = 0; _paid->scheduledPrivacySet = 0; return { .count = int(_paid->sending), .valid = true, - .anonymous = MaybeAnonymous( + .shownPeer = MaybeShownPeer( _paid->sendingPrivacySet, - _paid->sendingAnonymous), + _paid->sendingShownPeer), }; } @@ -2284,13 +2309,13 @@ void MessageReactions::finishPaidSending( Expects(_paid != nullptr); Expects(send.count == _paid->sending); Expects(send.valid == (_paid->sendingFlag == 1)); - Expects(send.anonymous == MaybeAnonymous( + Expects(send.shownPeer == MaybeShownPeer( _paid->sendingPrivacySet, - _paid->sendingAnonymous)); + _paid->sendingShownPeer)); _paid->sending = 0; _paid->sendingFlag = 0; - _paid->sendingAnonymous = 0; + _paid->sendingShownPeer = 0; _paid->sendingPrivacySet = 0; if (!_paid->scheduledFlag && _paid->top.empty()) { _paid = nullptr; @@ -2299,9 +2324,9 @@ void MessageReactions::finishPaidSending( return top.my; }); if (i != end(_paid->top)) { - i->peer = send.anonymous - ? nullptr - : _item->history()->session().user().get(); + i->peer = send.shownPeer + ? _item->history()->owner().peer(*send.shownPeer).get() + : nullptr; } } if (const auto amount = send.count) { @@ -2322,22 +2347,23 @@ int MessageReactions::localPaidCount() const { return _paid ? (_paid->scheduled + _paid->sending) : 0; } -bool MessageReactions::localPaidAnonymous() const { - const auto minePaidAnonymous = [&] { +PeerId MessageReactions::localPaidShownPeer() const { + const auto minePaidShownPeer = [&] { for (const auto &entry : _paid->top) { if (entry.my) { - return !entry.peer; + return entry.peer ? entry.peer->id : PeerId(); } } const auto api = &_item->history()->session().api(); - return api->globalPrivacy().paidReactionAnonymousCurrent(); + return api->globalPrivacy().paidReactionShownPeerCurrent(); }; - return _paid - && ((_paid->scheduledFlag && _paid->scheduledPrivacySet) - ? (_paid->scheduledAnonymous == 1) - : (_paid->sendingFlag && _paid->sendingPrivacySet) - ? (_paid->sendingAnonymous == 1) - : minePaidAnonymous()); + return !_paid + ? PeerId() + : (_paid->scheduledFlag && _paid->scheduledPrivacySet) + ? _paid->scheduledShownPeer + : (_paid->sendingFlag && _paid->sendingPrivacySet) + ? _paid->sendingShownPeer + : minePaidShownPeer(); } bool MessageReactions::clearCloudData() { diff --git a/Telegram/SourceFiles/data/data_message_reactions.h b/Telegram/SourceFiles/data/data_message_reactions.h index f1bf36f46..e931ed20e 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.h +++ b/Telegram/SourceFiles/data/data_message_reactions.h @@ -71,7 +71,7 @@ struct MyTagInfo { struct PaidReactionSend { int count = 0; bool valid = false; - std::optional anonymous = false; + std::optional shownPeer = PeerId(); }; class Reactions final : private CustomEmojiManager::Listener { @@ -409,7 +409,7 @@ public: [[nodiscard]] bool hasUnread() const; void markRead(); - void scheduleSendPaid(int count, std::optional anonymous); + void scheduleSendPaid(int count, std::optional shownPeer); [[nodiscard]] int scheduledPaid() const; void cancelScheduledPaid(); @@ -418,19 +418,19 @@ public: [[nodiscard]] bool localPaidData() const; [[nodiscard]] int localPaidCount() const; - [[nodiscard]] bool localPaidAnonymous() const; + [[nodiscard]] PeerId localPaidShownPeer() const; bool clearCloudData(); private: struct Paid { std::vector top; - uint32 scheduled: 29 = 0; + PeerId scheduledShownPeer = 0; + PeerId sendingShownPeer = 0; + uint32 scheduled: 30 = 0; uint32 scheduledFlag : 1 = 0; - uint32 scheduledAnonymous : 1 = 0; uint32 scheduledPrivacySet : 1 = 0; - uint32 sending : 29 = 0; + uint32 sending : 30 = 0; uint32 sendingFlag : 1 = 0; - uint32 sendingAnonymous : 1 = 0; uint32 sendingPrivacySet : 1 = 0; }; const not_null _item; diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 1d30e1b22..bb4eecaf8 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -3524,6 +3524,7 @@ not_null Session::processWebpage( 0, QString(), false, + false, data.vdate().v ? data.vdate().v : (base::unixtime::now() + kDefaultPendingTimeout)); @@ -3551,6 +3552,7 @@ not_null Session::webpage( 0, QString(), false, + false, TimeId(0)); } @@ -3571,6 +3573,7 @@ not_null Session::webpage( int duration, const QString &author, bool hasLargeMedia, + bool photoIsVideoCover, TimeId pendingTill) { const auto result = webpage(id); webpageApplyFields( @@ -3591,6 +3594,7 @@ not_null Session::webpage( duration, author, hasLargeMedia, + photoIsVideoCover, pendingTill); return result; } @@ -3778,6 +3782,7 @@ void Session::webpageApplyFields( data.vduration().value_or_empty(), qs(data.vauthor().value_or_empty()), data.is_has_large_media(), + data.is_video_cover_photo(), pendingTill); } @@ -3799,6 +3804,7 @@ void Session::webpageApplyFields( int duration, const QString &author, bool hasLargeMedia, + bool photoIsVideoCover, TimeId pendingTill) { const auto requestPending = (!page->pendingTill && pendingTill > 0); const auto changed = page->applyChanges( @@ -3818,6 +3824,7 @@ void Session::webpageApplyFields( duration, author, hasLargeMedia, + photoIsVideoCover, pendingTill); if (requestPending) { _session->api().requestWebPageDelayed(page); diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index bf1520a59..3e5392173 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -631,6 +631,7 @@ public: int duration, const QString &author, bool hasLargeMedia, + bool photoIsVideoCover, TimeId pendingTill); [[nodiscard]] not_null game(GameId id); @@ -916,6 +917,7 @@ private: int duration, const QString &author, bool hasLargeMedia, + bool photoIsVideoCover, TimeId pendingTill); void gameApplyFields( diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp index 7dff948f5..b3a539eda 100644 --- a/Telegram/SourceFiles/data/data_stories.cpp +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -316,7 +316,7 @@ void Stories::scheduleExpireTimer() { const auto nearest = _expiring.front().first; const auto now = base::unixtime::now(); const auto delay = (nearest > now) - ? (nearest - now) + ? std::min(nearest - now, 86'400) : 0; _expireTimer.callOnce(delay * crl::time(1000)); } diff --git a/Telegram/SourceFiles/data/data_web_page.cpp b/Telegram/SourceFiles/data/data_web_page.cpp index 5ab3f6b2f..bd14735eb 100644 --- a/Telegram/SourceFiles/data/data_web_page.cpp +++ b/Telegram/SourceFiles/data/data_web_page.cpp @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_photo.h" #include "data/data_channel.h" #include "data/data_document.h" +#include "core/local_url_handlers.h" #include "lang/lang_keys.h" #include "iv/iv_data.h" #include "ui/image/image.h" @@ -228,6 +229,7 @@ bool WebPageData::applyChanges( int newDuration, const QString &newAuthor, bool newHasLargeMedia, + bool newPhotoIsVideoCover, int newPendingTill) { if (newPendingTill != 0 && (!url.isEmpty() || failed) @@ -265,6 +267,9 @@ bool WebPageData::applyChanges( || (hasSiteName + hasTitle + hasDescription < 2)) { newHasLargeMedia = false; } + if (!newDocument || !newDocument->isVideoFile() || !newPhoto) { + newPhotoIsVideoCover = false; + } if (type == newType && url == resultUrl @@ -283,6 +288,7 @@ bool WebPageData::applyChanges( && duration == newDuration && author == resultAuthor && hasLargeMedia == (newHasLargeMedia ? 1 : 0) + && photoIsVideoCover == (newPhotoIsVideoCover ? 1 : 0) && pendingTill == newPendingTill) { return false; } @@ -291,6 +297,7 @@ bool WebPageData::applyChanges( } type = newType; hasLargeMedia = newHasLargeMedia ? 1 : 0; + photoIsVideoCover = newPhotoIsVideoCover ? 1 : 0; url = resultUrl; displayUrl = resultDisplayUrl; siteName = resultSiteName; @@ -374,6 +381,21 @@ QString WebPageData::displayedSiteName() const { : siteName; } +TimeId WebPageData::extractVideoTimestamp() const { + const auto take = [&](const QStringList &list, int index) { + return (index >= 0 && index < list.size()) ? list[index] : QString(); + }; + const auto hashed = take(url.split('#'), 0); + const auto params = take(hashed.split('?'), 1); + const auto parts = params.split('&'); + for (const auto &part : parts) { + if (part.startsWith(u"t="_q)) { + return Core::ParseVideoTimestamp(part.mid(2)); + } + } + return 0; +} + bool WebPageData::computeDefaultSmallMedia() const { if (!collage.items.empty()) { return false; @@ -382,7 +404,8 @@ bool WebPageData::computeDefaultSmallMedia() const { && description.empty() && author.isEmpty()) { return false; - } else if (!document + } else if (!uniqueGift + && !document && photo && type != WebPageType::Photo && type != WebPageType::Document diff --git a/Telegram/SourceFiles/data/data_web_page.h b/Telegram/SourceFiles/data/data_web_page.h index f97c79474..f8a19f1c1 100644 --- a/Telegram/SourceFiles/data/data_web_page.h +++ b/Telegram/SourceFiles/data/data_web_page.h @@ -106,6 +106,7 @@ struct WebPageData { int newDuration, const QString &newAuthor, bool newHasLargeMedia, + bool newPhotoIsVideoCover, int newPendingTill); static void ApplyChanges( @@ -114,6 +115,7 @@ struct WebPageData { const MTPmessages_Messages &result); [[nodiscard]] QString displayedSiteName() const; + [[nodiscard]] TimeId extractVideoTimestamp() const; [[nodiscard]] bool computeDefaultSmallMedia() const; [[nodiscard]] bool suggestEnlargePhoto() const; @@ -134,7 +136,8 @@ struct WebPageData { std::shared_ptr uniqueGift; int duration = 0; TimeId pendingTill = 0; - uint32 version : 30 = 0; + uint32 version : 29 = 0; + uint32 photoIsVideoCover : 1 = 0; uint32 hasLargeMedia : 1 = 0; uint32 failed : 1 = 0; diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index 05c774c99..a9871ded2 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -285,6 +285,7 @@ InnerWidget::InnerWidget( ) | rpl::start_with_next([=] { _topicJumpCache = nullptr; _chatsFilterTags.clear(); + _rightButtons.clear(); }, lifetime()); session().downloaderTaskFinished( @@ -2090,6 +2091,7 @@ bool InnerWidget::updateReorderPinned(QPoint localPosition) { const auto draggingHeight = _dragging->height(); auto yaddWas = _pinnedRows[_draggingIndex].yadd.current(); auto shift = 0; + auto shiftHeight = 0; auto now = crl::now(); if (_dragStart.y() > localPosition.y() && _draggingIndex > 0) { shift = -floorclamp(_dragStart.y() - localPosition.y() + (draggingHeight / 2), draggingHeight, 0, _draggingIndex); @@ -2099,6 +2101,7 @@ bool InnerWidget::updateReorderPinned(QPoint localPosition) { std::swap(_pinnedRows[from], _pinnedRows[from - 1]); _pinnedRows[from].yadd = anim::value(_pinnedRows[from].yadd.current() - draggingHeight, 0); _pinnedRows[from].animStartTime = now; + shiftHeight -= (*(_shownList->cbegin() + from))->height(); } } else if (_dragStart.y() < localPosition.y() && _draggingIndex + 1 < pinnedCount) { shift = floorclamp(localPosition.y() - _dragStart.y() + (draggingHeight / 2), draggingHeight, 0, pinnedCount - _draggingIndex - 1); @@ -2108,18 +2111,21 @@ bool InnerWidget::updateReorderPinned(QPoint localPosition) { std::swap(_pinnedRows[from], _pinnedRows[from + 1]); _pinnedRows[from].yadd = anim::value(_pinnedRows[from].yadd.current() + draggingHeight, 0); _pinnedRows[from].animStartTime = now; + shiftHeight += (*(_shownList->cbegin() + from))->height(); } } if (shift) { _draggingIndex += shift; _aboveIndex = _draggingIndex; - _dragStart.setY(_dragStart.y() + shift * _st->height); + _dragStart.setY(_dragStart.y() + shiftHeight); if (!_pinnedShiftAnimation.animating()) { _pinnedShiftAnimation.start(); } } _aboveTopShift = qCeil(_pinnedRows[_aboveIndex].yadd.current()); - _pinnedRows[_draggingIndex].yadd = anim::value(yaddWas - shift * _st->height, localPosition.y() - _dragStart.y()); + _pinnedRows[_draggingIndex].yadd = anim::value( + yaddWas - shiftHeight, + localPosition.y() - _dragStart.y()); if (!_pinnedRows[_draggingIndex].animStartTime) { _pinnedRows[_draggingIndex].yadd.finish(); } @@ -2170,7 +2176,7 @@ bool InnerWidget::pinnedShiftAnimationCallback(crl::time now) { } if (updateMin >= 0) { const auto minHeight = _st->height; - const auto maxHeight = st::forumDialogRow.height; + const auto maxHeight = st::taggedForumDialogRow.height; auto top = pinnedOffset(); auto updateFrom = top + minHeight * (updateMin - 1); auto updateHeight = maxHeight * (updateMax - updateMin + 3); diff --git a/Telegram/SourceFiles/dialogs/dialogs_row.cpp b/Telegram/SourceFiles/dialogs/dialogs_row.cpp index efd201c28..a2e4d2a3f 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_row.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_row.cpp @@ -470,7 +470,13 @@ void Row::PaintCornerBadgeFrame( for (auto i = 0; i != storiesUnreadCount; ++i) { segments.push_back({ storiesUnreadBrush, storiesUnread }); } - Ui::PaintOutlineSegments(q, outline, segments); + if (peer && peer->forum()) { + const auto radius = context.st->photoSize + * Ui::ForumUserpicRadiusMultiplier(); + Ui::PaintOutlineSegments(q, outline, radius, segments); + } else { + Ui::PaintOutlineSegments(q, outline, segments); + } } if (subscribed) { diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index cceb49580..6a16476f4 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -1215,6 +1215,7 @@ void Widget::setupShortcuts() { }); request->check(Command::ShowChatMenu, 1) && request->handle([=] { if (_inner) { + Window::ActivateWindow(controller()); _inner->showPeerMenu(); } return true; diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp index db7da1fe7..c3430f185 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp @@ -442,6 +442,9 @@ void PaintRow( const auto promoted = (history && history->useTopPromotion()) && !context.search; + const auto verifyInfo = (from && !from->isSelf()) + ? from->botVerifyDetails() + : nullptr; if (promoted) { const auto type = history->topPromotionType(); const auto custom = type.isEmpty() @@ -453,10 +456,10 @@ void PaintRow( ? tr::lng_badge_psa_default(tr::now) : custom; PaintRowTopRight(p, text, rectForName, context); - } else if (const auto info = from ? from->botVerifyDetails() : nullptr) { - if (!rowBadge.ready(info)) { + } else if (verifyInfo) { + if (!rowBadge.ready(verifyInfo)) { rowBadge.set( - info, + verifyInfo, from->owner().customEmojiManager().factory(), customEmojiRepaint); } diff --git a/Telegram/SourceFiles/editor/editor_crop.cpp b/Telegram/SourceFiles/editor/editor_crop.cpp index 54f8369c2..b52888101 100644 --- a/Telegram/SourceFiles/editor/editor_crop.cpp +++ b/Telegram/SourceFiles/editor/editor_crop.cpp @@ -42,6 +42,15 @@ QSizeF FlipSizeByRotation(const QSizeF &size, int angle) { return (((angle / 90) % 2) == 1) ? size.transposed() : size; } +[[nodiscard]] QRectF OriginalCrop(QSize outer, QSize inner) { + const auto size = inner.scaled(outer, Qt::KeepAspectRatio); + return QRectF( + (outer.width() - size.width()) / 2, + (outer.height() - size.height()) / 2, + size.width(), + size.height()); +} + } // namespace Crop::Crop( @@ -60,6 +69,8 @@ Crop::Crop( , _data(std::move(data)) , _cropOriginal(modifications.crop.isValid() ? modifications.crop + : !_data.exactSize.isEmpty() + ? OriginalCrop(_imageSize, _data.exactSize) : QRectF(QPoint(), _imageSize)) , _angle(modifications.angle) , _flipped(modifications.flipped) diff --git a/Telegram/SourceFiles/editor/photo_editor_common.h b/Telegram/SourceFiles/editor/photo_editor_common.h index 028f55c6f..b52b2110a 100644 --- a/Telegram/SourceFiles/editor/photo_editor_common.h +++ b/Telegram/SourceFiles/editor/photo_editor_common.h @@ -32,6 +32,7 @@ struct EditorData { TextWithEntities about; QString confirm; + QSize exactSize; CropType cropType = CropType::Rect; bool keepAspectRatio = false; }; diff --git a/Telegram/SourceFiles/editor/photo_editor_layer_widget.cpp b/Telegram/SourceFiles/editor/photo_editor_layer_widget.cpp index 33ed5e837..257e47ec4 100644 --- a/Telegram/SourceFiles/editor/photo_editor_layer_widget.cpp +++ b/Telegram/SourceFiles/editor/photo_editor_layer_widget.cpp @@ -26,22 +26,26 @@ void OpenWithPreparedFile( std::shared_ptr show, not_null file, int previewWidth, - Fn &&doneCallback) { + Fn &&doneCallback, + QSize exactSize) { using ImageInfo = Ui::PreparedFileInformation::Image; const auto image = std::get_if(&file->information->media); if (!image) { + doneCallback(false); return; } const auto photoType = (file->type == Ui::PreparedFile::Type::Photo); const auto modifiedFileType = (file->type == Ui::PreparedFile::Type::File) && !image->modifications.empty(); if (!photoType && !modifiedFileType) { + doneCallback(false); return; } const auto sideLimit = PhotoSideLimit(); - auto callback = [=, done = std::move(doneCallback)]( - const PhotoModifications &mods) { + const auto accepted = std::make_shared(); + auto callback = [=](const PhotoModifications &mods) { + *accepted = true; image->modifications = mods; Storage::UpdateImageDetails(*file, previewWidth, sideLimit); { @@ -51,19 +55,26 @@ void OpenWithPreparedFile( ? PreparedFile::Type::Photo : PreparedFile::Type::File; } - done(); + doneCallback(true); }; auto copy = image->data; const auto fileImage = std::make_shared(std::move(copy)); + const auto keepRatio = !exactSize.isEmpty(); auto editor = base::make_unique_q( parent, show, show, fileImage, - image->modifications); + image->modifications, + EditorData{ .exactSize = exactSize, .keepAspectRatio = keepRatio }); const auto raw = editor.get(); auto layer = std::make_unique(parent, std::move(editor)); InitEditorLayer(layer.get(), raw, std::move(callback)); + QObject::connect(layer.get(), &QObject::destroyed, [=] { + if (!*accepted) { + doneCallback(false); + } + }); show->showLayer(std::move(layer), Ui::LayerOption::KeepOther); } diff --git a/Telegram/SourceFiles/editor/photo_editor_layer_widget.h b/Telegram/SourceFiles/editor/photo_editor_layer_widget.h index f1c2fd445..0259a0fb7 100644 --- a/Telegram/SourceFiles/editor/photo_editor_layer_widget.h +++ b/Telegram/SourceFiles/editor/photo_editor_layer_widget.h @@ -34,7 +34,8 @@ void OpenWithPreparedFile( std::shared_ptr show, not_null file, int previewWidth, - Fn &&doneCallback); + Fn &&doneCallback, + QSize exactSize = {}); void PrepareProfilePhoto( not_null parent, diff --git a/Telegram/SourceFiles/export/output/export_output_json.cpp b/Telegram/SourceFiles/export/output/export_output_json.cpp index 9d40b6009..dee8d1e94 100644 --- a/Telegram/SourceFiles/export/output/export_output_json.cpp +++ b/Telegram/SourceFiles/export/output/export_output_json.cpp @@ -385,6 +385,7 @@ QByteArray SerializeMessage( }; const auto pushPhoto = [&](const Image &image) { pushPath(image.file, "photo"); + push("photo_file_size", image.file.size); if (image.width && image.height) { push("width", image.width); push("height", image.height); @@ -696,8 +697,10 @@ QByteArray SerializeMessage( }, [&](const Document &data) { pushPath(data.file, "file"); push("file_name", data.name); + push("file_size", data.file.size); if (data.thumb.width > 0) { pushPath(data.thumb.file, "thumbnail"); + push("thumbnail_file_size", data.thumb.file.size); } const auto pushType = [&](const QByteArray &value) { push("media_type", value); @@ -739,6 +742,7 @@ QByteArray SerializeMessage( })); if (!data.vcard.content.isEmpty()) { pushPath(data.vcard, "contact_vcard"); + push("contact_vcard_file_size", data.vcard.size); } }, [&](const GeoPoint &data) { pushBare( diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index dd7a46053..4fe6bd08f 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -55,6 +55,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/about_sponsored_box.h" #include "boxes/delete_messages_box.h" #include "boxes/report_messages_box.h" +#include "boxes/star_gift_box.h" // ShowStarGiftBox #include "boxes/sticker_set_box.h" #include "boxes/translate_box.h" #include "chat_helpers/message_field.h" @@ -1627,6 +1628,7 @@ void HistoryInner::mouseMoveEvent(QMouseEvent *e) { mouseReleaseEvent(e); } if (reallyMoved) { + _mouseActive = true; lastGlobalPosition = e->globalPos(); if (!buttonsPressed || (_scrollDateLink && ClickHandler::getPressed() == _scrollDateLink)) { keepScrollDateForNow(); @@ -1673,6 +1675,7 @@ void HistoryInner::mousePressEvent(QMouseEvent *e) { e->accept(); return; // ignore mouse press, that was hiding context menu } + _mouseActive = true; mouseActionStart(e->globalPos(), e->button()); } @@ -2835,6 +2838,29 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { _menu->addAction(tr::lng_profile_copy_phone(tr::now), [=] { QGuiApplication::clipboard()->setText(phone); }, &st::menuIconCopy); + } else if (const auto gift = media->gift()) { + const auto peer = item->history()->peer; + const auto user = peer->asUser(); + if (!user + || (!user->isInaccessible() + && !user->isNotificationsUser())) { + const auto controller = _controller; + const auto starGiftUpgrade = gift->upgrade + && (gift->type == Data::GiftType::StarGift); + const auto isGift = gift->slug.isEmpty() + || !gift->channel; + const auto out = item->out(); + const auto outgoingGift = isGift + && (starGiftUpgrade ? !out : out); + if (outgoingGift) { + _menu->addAction( + tr::lng_context_gift_send(tr::now), + [=] { + Ui::ShowStarGiftBox(controller, peer); + }, + &st::menuIconGiftPremium); + } + } } } if (!item->isService() && view && actionText.isEmpty()) { @@ -3314,7 +3340,7 @@ void HistoryInner::checkActivation() { session().data().histories().readInboxTill(view->data()); } -void HistoryInner::recountHistoryGeometry() { +void HistoryInner::recountHistoryGeometry(bool initial) { _contentWidth = _scroll->width(); if (_history->hasPendingResizedItems() @@ -3372,7 +3398,7 @@ void HistoryInner::recountHistoryGeometry() { } auto historyPaddingTopDelta = (newHistoryPaddingTop - oldHistoryPaddingTop); - if (historyPaddingTopDelta != 0) { + if (!initial && historyPaddingTopDelta != 0) { if (_history->scrollTopItem) { _history->scrollTopOffset += historyPaddingTopDelta; } else if (_migrated && _migrated->scrollTopItem) { @@ -3567,6 +3593,7 @@ void HistoryInner::setShownPinned(HistoryItem *item) { } void HistoryInner::enterEventHook(QEnterEvent *e) { + _mouseActive = true; mouseActionUpdate(QCursor::pos()); return TWidget::enterEventHook(e); } @@ -3583,6 +3610,7 @@ void HistoryInner::leaveEventHook(QEvent *e) { _cursor = style::cur_default; setCursor(_cursor); } + _mouseActive = false; return TWidget::leaveEventHook(e); } @@ -3933,7 +3961,7 @@ auto HistoryInner::reactionButtonParameters( } void HistoryInner::mouseActionUpdate() { - if (hasPendingResizedItems()) { + if (hasPendingResizedItems() || !_mouseActive) { return; } diff --git a/Telegram/SourceFiles/history/history_inner_widget.h b/Telegram/SourceFiles/history/history_inner_widget.h index 4f0ba7bfc..cec98e8b3 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.h +++ b/Telegram/SourceFiles/history/history_inner_widget.h @@ -121,7 +121,7 @@ public: void setItemsRevealHeight(int revealHeight); void changeItemsRevealHeight(int revealHeight); void checkActivation(); - void recountHistoryGeometry(); + void recountHistoryGeometry(bool initial = false); void updateSize(); void setShownPinned(HistoryItem *item); @@ -499,6 +499,7 @@ private: HistoryItem *_dragStateItem = nullptr; CursorState _mouseCursorState = CursorState(); uint16 _mouseTextSymbol = 0; + bool _mouseActive = false; bool _dragStateUserpic = false; bool _pressWasInactive = false; bool _recountedAfterPendingResizedItems = false; diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index f71327eeb..cd253c6a8 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -310,13 +310,19 @@ std::unique_ptr HistoryItem::CreateMedia( } return document->match([&](const MTPDdocument &document) -> Result { const auto list = media.valt_documents(); - return std::make_unique( - item, - item->history()->owner().processDocument(document, list), - media.is_nopremium(), - list && !list->v.isEmpty(), - media.is_spoiler(), - media.vttl_seconds().value_or_empty()); + const auto owner = &item->history()->owner(); + const auto data = owner->processDocument(document); + using Args = Data::MediaFile::Args; + return std::make_unique(item, data, Args{ + .ttlSeconds = media.vttl_seconds().value_or_empty(), + .videoCover = (media.vvideo_cover() + ? owner->processPhoto(*media.vvideo_cover()).get() + : nullptr), + .videoTimestamp = media.vvideo_timestamp().value_or_empty(), + .hasQualitiesList = list && !list->v.isEmpty(), + .skipPremiumEffect = media.is_nopremium(), + .spoiler = media.is_spoiler(), + }); }, [](const MTPDdocumentEmpty &) -> Result { return nullptr; }); @@ -693,16 +699,12 @@ HistoryItem::HistoryItem( : HistoryItem(history, fields) { createComponentsHelper(std::move(fields)); - const auto skipPremiumEffect = !history->session().premium(); const auto video = document->video(); - const auto spoiler = false; - _media = std::make_unique( - this, - document, - skipPremiumEffect, - video && !video->qualities.empty(), - spoiler, - /*ttlSeconds = */0); + using Args = Data::MediaFile::Args; + _media = std::make_unique(this, document, Args{ + .hasQualitiesList = video && !video->qualities.empty(), + .skipPremiumEffect = !history->session().premium(), + }); setText(caption); } @@ -764,6 +766,7 @@ HistoryItem::HistoryItem( 0, QString(), false, + false, 0); auto webpageMedia = std::make_unique( this, @@ -1903,17 +1906,12 @@ void HistoryItem::applyChanges(not_null story) { } void HistoryItem::setStoryFields(not_null story) { - const auto spoiler = false; if (const auto photo = story->photo()) { + const auto spoiler = false; _media = std::make_unique(this, photo, spoiler); } else if (const auto document = story->document()) { - _media = std::make_unique( - this, - document, - /*skipPremiumEffect=*/false, - /*hasQualitiesList=*/false, - spoiler, - /*ttlSeconds = */0); + using Args = Data::MediaFile::Args; + _media = std::make_unique(this, document, Args{}); } setText(story->caption()); if (story->pinnedToTop()) { @@ -2693,14 +2691,16 @@ bool HistoryItem::canReact() const { return true; } -void HistoryItem::addPaidReaction(int count, std::optional anonymous) { +void HistoryItem::addPaidReaction( + int count, + std::optional shownPeer) { Expects(count >= 0); Expects(_history->peer->isBroadcast() || isDiscussionPost()); if (!_reactions) { _reactions = std::make_unique(this); } - _reactions->scheduleSendPaid(count, anonymous); + _reactions->scheduleSendPaid(count, shownPeer); if (count > 0) { _history->owner().notifyItemDataChange(this); } @@ -2796,8 +2796,10 @@ int HistoryItem::reactionsPaidScheduled() const { return _reactions ? _reactions->scheduledPaid() : 0; } -bool HistoryItem::reactionsLocalAnonymous() const { - return _reactions ? _reactions->localPaidAnonymous() : false; +PeerId HistoryItem::reactionsLocalShownPeer() const { + return _reactions + ? _reactions->localPaidShownPeer() + : _history->session().userPeerId(); } bool HistoryItem::reactionsAreTags() const { @@ -2825,9 +2827,8 @@ auto HistoryItem::topPaidReactionsWithLocal() const result, [](const TopPaid &entry) { return entry.my != 0; }); const auto peerForMine = [&] { - return _reactions->localPaidAnonymous() - ? nullptr - : history()->session().user().get(); + const auto peerId = _reactions->localPaidShownPeer(); + return peerId ? history()->owner().peer(peerId).get() : nullptr; }; if (const auto local = _reactions->localPaidCount()) { const auto top = [&](int mine) { diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index dd35aa65c..4c4624d6a 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -458,7 +458,7 @@ public: void toggleReaction( const Data::ReactionId &reaction, HistoryReactionSource source); - void addPaidReaction(int count, std::optional anonymous = {}); + void addPaidReaction(int count, std::optional shownPeer = {}); void cancelScheduledPaidReaction(); [[nodiscard]] Data::PaidReactionSend startPaidReactionSending(); void finishPaidReactionSending( @@ -476,7 +476,7 @@ public: [[nodiscard]] auto topPaidReactionsWithLocal() const -> std::vector; [[nodiscard]] int reactionsPaidScheduled() const; - [[nodiscard]] bool reactionsLocalAnonymous() const; + [[nodiscard]] PeerId reactionsLocalShownPeer() const; [[nodiscard]] bool canViewReactions() const; [[nodiscard]] std::vector chosenReactions() const; [[nodiscard]] Data::ReactionId lookupUnreadReaction( diff --git a/Telegram/SourceFiles/history/history_item_components.cpp b/Telegram/SourceFiles/history/history_item_components.cpp index c2f7a48a4..002e35e56 100644 --- a/Telegram/SourceFiles/history/history_item_components.cpp +++ b/Telegram/SourceFiles/history/history_item_components.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_text_entities.h" #include "base/qt/qt_key_modifiers.h" +#include "base/options.h" #include "lang/lang_keys.h" #include "ui/effects/ripple_animation.h" #include "ui/effects/spoiler_mess.h" @@ -67,8 +68,20 @@ namespace { const auto kPsaForwardedPrefix = "cloud_lng_forwarded_psa_"; +base::options::toggle FastButtonsModeOption({ + .id = kOptionFastButtonsMode, + .name = "Fast buttons mode", + .description = "Trigger inline keyboard buttons by 1-9 keyboard keys.", +}); + } // namespace +const char kOptionFastButtonsMode[] = "fast-buttons-mode"; + +bool FastButtonsMode() { + return FastButtonsModeOption.value(); +} + void HistoryMessageVia::create( not_null owner, UserId userId) { @@ -946,10 +959,10 @@ void ReplyKeyboard::paint( } bool ReplyKeyboard::hasFastButtonMode() const { - return _item->inlineReplyKeyboard() + return FastButtonsMode() + && _item->inlineReplyKeyboard() && (_item == _item->history()->lastMessage()) - && _item->history()->session().supportMode() - && _item->history()->session().supportHelper().fastButtonMode( + && _item->history()->session().fastButtonsBots().enabled( _item->history()->peer); } diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h index c2281496b..331ee24df 100644 --- a/Telegram/SourceFiles/history/history_item_components.h +++ b/Telegram/SourceFiles/history/history_item_components.h @@ -54,6 +54,9 @@ namespace style { struct BotKeyboardButton; } // namespace style +extern const char kOptionFastButtonsMode[]; +[[nodiscard]] bool FastButtonsMode(); + struct HistoryMessageVia : public RuntimeComponent { void create(not_null owner, UserId userId); void resize(int32 availw) const; diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 0c0fed493..f61c61bde 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/moderate_messages_box.h" #include "boxes/premium_limits_box.h" #include "boxes/premium_preview_box.h" +#include "boxes/star_gift_box.h" #include "boxes/peers/edit_peer_permissions_box.h" // ShowAboutGigagroup. #include "boxes/peers/edit_peer_requests_box.h" #include "core/file_utilities.h" @@ -409,6 +410,7 @@ HistoryWidget::HistoryWidget( _joinChannel->addClickHandler([=] { joinChannel(); }); _muteUnmute->addClickHandler([=] { toggleMuteUnmute(); }); _discuss->addClickHandler([=] { goToDiscussionGroup(); }); + setupGiftToChannelButton(); _reportMessages->addClickHandler([=] { reportSelectedMessages(); }); _field->submits( ) | rpl::start_with_next([=](Qt::KeyboardModifiers modifiers) { @@ -1087,6 +1089,18 @@ void HistoryWidget::refreshJoinChannelText() { } } +void HistoryWidget::refreshGiftToChannelShown() { + if (!_giftToChannelIn || !_giftToChannelOut) { + return; + } + const auto channel = _peer->asChannel(); + const auto shown = channel + && channel->isBroadcast() + && channel->stargiftsAvailable(); + _giftToChannelIn->setVisible(shown); + _giftToChannelOut->setVisible(shown); +} + void HistoryWidget::refreshTopBarActiveChat() { const auto state = computeDialogsEntryState(); _topBar->setActiveChat(state, _history->sendActionPainter()); @@ -2090,6 +2104,7 @@ void HistoryWidget::setupShortcuts() { return true; }); request->check(Command::ShowChatMenu, 1) && request->handle([=] { + Window::ActivateWindow(controller()); _topBar->showPeerMenu(); return true; }); @@ -2112,6 +2127,25 @@ void HistoryWidget::setupShortcuts() { }, lifetime()); } +void HistoryWidget::setupGiftToChannelButton() { + const auto setupButton = [=](not_null parent) { + auto *button = Ui::CreateChild( + parent.get(), + st::historyGiftToChannel); + parent->widthValue() | rpl::start_with_next([=](int width) { + button->moveToRight(0, 0); + }, button->lifetime()); + button->setClickedCallback([=] { + if (_peer) { + Ui::ShowStarGiftBox(controller(), _peer); + } + }); + return button; + }; + _giftToChannelIn = setupButton(_muteUnmute); + _giftToChannelOut = setupButton(_joinChannel); +} + void HistoryWidget::pushReplyReturn(not_null item) { if (item->history() != _history && item->history() != _migrated) { return; @@ -2270,15 +2304,12 @@ bool HistoryWidget::insideJumpToEndInsteadOfToUnread() const { } void HistoryWidget::showHistory( - const PeerId &peerId, + PeerId peerId, MsgId showAtMsgId, - const TextWithEntities &highlightPart, - int highlightPartOffsetHint) { - + const Window::SectionShow ¶ms) { _pinnedClickedId = FullMsgId(); _minPinnedId = std::nullopt; - _showAtMsgHighlightPart = {}; - _showAtMsgHighlightPartOffsetHint = 0; + _showAtMsgParams = {}; const auto wasState = controller()->dialogsEntryStateCurrent(); const auto startBot = (showAtMsgId == ShowAndStartBotMsgId); @@ -2328,16 +2359,10 @@ void HistoryWidget::showHistory( ).arg(_history->inboxReadTillId().bare ).arg(Logs::b(_history->loadedAtBottom()) ).arg(showAtMsgId.bare)); - delayedShowAt( - showAtMsgId, - highlightPart, - highlightPartOffsetHint); + delayedShowAt(showAtMsgId, params); } else if (_showAtMsgId != showAtMsgId) { clearAllLoadRequests(); - setMsgId( - showAtMsgId, - highlightPart, - highlightPartOffsetHint); + setMsgId(showAtMsgId, params); firstLoadMessages(); doneShow(); } @@ -2357,10 +2382,7 @@ void HistoryWidget::showHistory( _cornerButtons.skipReplyReturn(skipId); } - setMsgId( - showAtMsgId, - highlightPart, - highlightPartOffsetHint); + setMsgId(showAtMsgId, params); if (_historyInited) { DEBUG_LOG(("JumpToEnd(%1, %2, %3): " "Showing instant at %4." @@ -2465,8 +2487,7 @@ void HistoryWidget::showHistory( clearInlineBot(); _showAtMsgId = showAtMsgId; - _showAtMsgHighlightPart = highlightPart; - _showAtMsgHighlightPartOffsetHint = highlightPartOffsetHint; + _showAtMsgParams = params; _historyInited = false; _contactStatus = nullptr; _businessBotStatus = nullptr; @@ -2485,6 +2506,8 @@ void HistoryWidget::showHistory( ) | rpl::start_with_next([=] { updateControlsGeometry(); }, _contactStatus->bar().lifetime()); + + refreshGiftToChannelShown(); if (const auto user = _peer->asUser()) { _businessBotStatus = std::make_unique( controller(), @@ -2977,14 +3000,12 @@ void HistoryWidget::refreshSilentToggle() { } void HistoryWidget::setupFastButtonMode() { - if (!session().supportMode()) { - return; - } const auto field = _field->rawTextEdit(); base::install_event_filter(field, [=](not_null e) { if (e->type() != QEvent::KeyPress || !_history - || !session().supportHelper().fastButtonMode(_history->peer) + || !FastButtonsMode() + || !session().fastButtonsBots().enabled(_history->peer) || !_field->getLastText().isEmpty()) { return base::EventFilterResult::Continue; } @@ -3737,10 +3758,7 @@ void HistoryWidget::messagesReceived( } _delayedShowAtRequest = 0; - setMsgId( - _delayedShowAtMsgId, - _delayedShowAtMsgHighlightPart, - _delayedShowAtMsgHighlightPartOffsetHint); + setMsgId(_delayedShowAtMsgId, _delayedShowAtMsgParams); historyLoaded(); } if (session().supportMode()) { @@ -3992,15 +4010,11 @@ void HistoryWidget::loadMessagesDown() { void HistoryWidget::delayedShowAt( MsgId showAtMsgId, - const TextWithEntities &highlightPart, - int highlightPartOffsetHint) { + const Window::SectionShow ¶ms) { if (!_history) { return; } - if (_delayedShowAtMsgHighlightPart != highlightPart) { - _delayedShowAtMsgHighlightPart = highlightPart; - } - _delayedShowAtMsgHighlightPartOffsetHint = highlightPartOffsetHint; + _delayedShowAtMsgParams = params; if (_delayedShowAtRequest && _delayedShowAtMsgId == showAtMsgId) { return; } @@ -4135,7 +4149,10 @@ void HistoryWidget::preloadHistoryIfNeeded() { preloadHistoryByScroll(); checkReplyReturns(); } - if (clearMaybeSendStart() && !_history->isDisplayedEmpty()) { + const auto hasNonEmpty = _history->findFirstNonEmpty(); + const auto readyForBotStart = hasNonEmpty + || (_history->loadedAtTop() && _history->loadedAtBottom()); + if (readyForBotStart && clearMaybeSendStart() && hasNonEmpty) { sendBotStartCommand(); } } @@ -4663,12 +4680,8 @@ PeerData *HistoryWidget::peer() const { // Sometimes _showAtMsgId is set directly. void HistoryWidget::setMsgId( MsgId showAtMsgId, - const TextWithEntities &highlightPart, - int highlightPartOffsetHint) { - if (_showAtMsgHighlightPart != highlightPart) { - _showAtMsgHighlightPart = highlightPart; - } - _showAtMsgHighlightPartOffsetHint = highlightPartOffsetHint; + const Window::SectionShow ¶ms) { + _showAtMsgParams = params; if (_showAtMsgId != showAtMsgId) { _showAtMsgId = showAtMsgId; if (_history) { @@ -6429,8 +6442,8 @@ int HistoryWidget::countInitialScrollTop() { enqueueMessageHighlight({ item, - base::take(_showAtMsgHighlightPart), - base::take(_showAtMsgHighlightPartOffsetHint), + base::take(_showAtMsgParams.highlightPart), + base::take(_showAtMsgParams.highlightPartOffsetHint), }); const auto result = itemTopForHighlight(view); createUnreadBarIfBelowVisibleArea(result); @@ -6647,6 +6660,22 @@ void HistoryWidget::updateHistoryGeometry( } const auto toY = std::clamp(newScrollTop, 0, _scroll->scrollTopMax()); synteticScrollToY(toY); + if (initial && _showAtMsgId) { + const auto timestamp = base::take(_showAtMsgParams.videoTimestamp); + if (timestamp.has_value()) { + const auto item = session().data().message(_peer, _showAtMsgId); + const auto media = item ? item->media() : nullptr; + const auto document = media ? media->document() : nullptr; + if (document && document->isVideoFile()) { + controller()->openDocument( + document, + true, + { item->fullId() }, + nullptr, + timestamp); + } + } + } } void HistoryWidget::revealItemsCallback() { @@ -6748,7 +6777,7 @@ void HistoryWidget::startMessageSendingAnimation( void HistoryWidget::updateListSize() { Expects(_list != nullptr); - _list->recountHistoryGeometry(); + _list->recountHistoryGeometry(!_historyInited); auto washidden = _scroll->isHidden(); if (washidden) { _scroll->show(); @@ -8517,9 +8546,13 @@ void HistoryWidget::fullInfoUpdated() { handlePeerUpdate(); checkSuggestToGigagroup(); - if (clearMaybeSendStart() && !_history->isDisplayedEmpty()) { + const auto hasNonEmpty = _history->findFirstNonEmpty(); + const auto readyForBotStart = hasNonEmpty + || (_history->loadedAtTop() && _history->loadedAtBottom()); + if (readyForBotStart && clearMaybeSendStart() && hasNonEmpty) { sendBotStartCommand(); } + refreshGiftToChannelShown(); } if (updateCmdStartShown()) { refresh = true; diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index 478d35853..7c062f7da 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "chat_helpers/field_characters_count_manager.h" #include "data/data_report.h" #include "window/section_widget.h" +#include "window/window_session_controller.h" #include "ui/widgets/fields/input_field.h" #include "mtproto/sender.h" @@ -86,10 +87,6 @@ namespace Webrtc { enum class RecordAvailability : uchar; } // namespace Webrtc -namespace Window { -class SessionController; -} // namespace Window - namespace ChatHelpers { class TabbedPanel; class TabbedSelector; @@ -160,10 +157,7 @@ public: void loadMessages(); void loadMessagesDown(); void firstLoadMessages(); - void delayedShowAt( - MsgId showAtMsgId, - const TextWithEntities &highlightPart, - int highlightPartOffsetHint); + void delayedShowAt(MsgId showAtMsgId, const Window::SectionShow ¶ms); bool updateReplaceMediaButton(); void updateFieldPlaceholder(); @@ -176,10 +170,7 @@ public: History *history() const; PeerData *peer() const; - void setMsgId( - MsgId showAtMsgId, - const TextWithEntities &highlightPart = {}, - int highlightPartOffsetHint = 0); + void setMsgId(MsgId showAtMsgId, const Window::SectionShow ¶ms = {}); MsgId msgId() const; bool hasTopBarShadow() const { @@ -244,10 +235,9 @@ public: bool applyDraft( FieldHistoryAction fieldHistoryAction = FieldHistoryAction::Clear); void showHistory( - const PeerId &peer, + PeerId peerId, MsgId showAtMsgId, - const TextWithEntities &highlightPart = {}, - int highlightPartOffsetHint = 0); + const Window::SectionShow ¶ms = {}); void setChooseReportMessagesDetails( Data::ReportInput reportInput, Fn)> callback); @@ -406,6 +396,7 @@ private: void refreshTopBarActiveChat(); void refreshJoinChannelText(); + void refreshGiftToChannelShown(); void requestMessageData(MsgId msgId); void messageDataReceived(not_null peer, MsgId msgId); @@ -527,6 +518,7 @@ private: } void setupShortcuts(); + void setupGiftToChannelButton(); void handlePeerMigration(); @@ -731,8 +723,7 @@ private: bool _canSendTexts = false; MsgId _showAtMsgId = ShowAtUnreadMsgId; base::flat_set _topicsRequested; - TextWithEntities _showAtMsgHighlightPart; - int _showAtMsgHighlightPartOffsetHint = 0; + Window::SectionShow _showAtMsgParams; bool _showAndMaybeSendStart = false; int _firstLoadRequest = 0; // Not real mtpRequestId. @@ -740,8 +731,7 @@ private: int _preloadDownRequest = 0; // Not real mtpRequestId. MsgId _delayedShowAtMsgId = -1; - TextWithEntities _delayedShowAtMsgHighlightPart; - int _delayedShowAtMsgHighlightPartOffsetHint = 0; + Window::SectionShow _delayedShowAtMsgParams; int _delayedShowAtRequest = 0; // Not real mtpRequestId. History *_supportPreloadHistory = nullptr; @@ -789,6 +779,8 @@ private: object_ptr _botStart; object_ptr _joinChannel; object_ptr _muteUnmute; + QPointer _giftToChannelIn; + QPointer _giftToChannelOut; object_ptr _discuss; object_ptr _reportMessages; struct { diff --git a/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp b/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp index 212840323..2d03f6dfa 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp @@ -947,7 +947,8 @@ void DraftOptionsBox( AddFilledSkip(bottom); - if (!hasOnlyForcedForwardedInfo) { + if (!hasOnlyForcedForwardedInfo + && !HasOnlyDroppedForwardedInfo(items)) { Settings::AddButtonWithIcon( bottom, (dropNames diff --git a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp index 239108e13..022b124ab 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp @@ -153,7 +153,7 @@ void ForwardPanel::updateTexts() { Unexpected("Corrupt forwarded information in message."); } } - if (!keepNames) { + if (!keepNames || HasOnlyDroppedForwardedInfo(_data.items)) { from = tr::lng_forward_sender_names_removed(tr::now); } else if (names.size() > 2) { from = tr::lng_forwarding_from( @@ -445,4 +445,13 @@ bool HasOnlyForcedForwardedInfo(const HistoryItemsList &list) { return true; } +bool HasOnlyDroppedForwardedInfo(const HistoryItemsList &list) { + for (const auto &item : list) { + if (!item->computeDropForwardedInfo()) { + return false; + } + } + return true; +} + } // namespace HistoryView::Controls diff --git a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.h b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.h index 133292ceb..54624d47d 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.h @@ -85,5 +85,6 @@ void EditWebPageOptions( Fn done); [[nodiscard]] bool HasOnlyForcedForwardedInfo(const HistoryItemsList &list); +[[nodiscard]] bool HasOnlyDroppedForwardedInfo(const HistoryItemsList &list); } // namespace HistoryView::Controls diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index bbce1f07d..bd446022a 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -1315,25 +1315,28 @@ base::unique_qptr FillContextMenu( void CopyPostLink( not_null controller, FullMsgId itemId, - Context context) { - CopyPostLink(controller->uiShow(), itemId, context); + Context context, + std::optional videoTimestamp) { + CopyPostLink(controller->uiShow(), itemId, context, videoTimestamp); } void CopyPostLink( std::shared_ptr show, FullMsgId itemId, - Context context) { + Context context, + std::optional videoTimestamp) { const auto item = show->session().data().message(itemId); if (!item || !item->hasDirectLink()) { return; } const auto inRepliesContext = (context == Context::Replies); - const auto forceNonPublicLink = base::IsCtrlPressed(); + const auto forceNonPublicLink = !videoTimestamp && base::IsCtrlPressed(); QGuiApplication::clipboard()->setText( item->history()->session().api().exportDirectMessageLink( item, inRepliesContext, - forceNonPublicLink)); + forceNonPublicLink, + videoTimestamp)); const auto isPublicLink = [&] { if (forceNonPublicLink) { @@ -1354,7 +1357,7 @@ void CopyPostLink( } return channel->hasUsername(); }(); - if (isPublicLink) { + if (isPublicLink && !videoTimestamp) { show->showToast({ .text = tr::lng_channel_public_link_copied( tr::now, Ui::Text::Bold diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.h b/Telegram/SourceFiles/history/view/history_view_context_menu.h index 1dac246bb..0e14fa803 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.h +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.h @@ -60,11 +60,13 @@ base::unique_qptr FillContextMenu( void CopyPostLink( not_null controller, FullMsgId itemId, - Context context); + Context context, + std::optional videoTimestamp = {}); void CopyPostLink( std::shared_ptr show, FullMsgId itemId, - Context context); + Context context, + std::optional videoTimestamp = {}); void CopyStoryLink( std::shared_ptr show, FullStoryId storyId); diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 91d085ce2..ebd36aef6 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -38,6 +38,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "mainwidget.h" #include "main/main_session.h" +#include "settings/settings_premium.h" #include "ui/text/text_options.h" #include "ui/painter.h" #include "window/themes/window_theme.h" // IsNightMode. @@ -378,6 +379,7 @@ struct Message::CommentsButton { struct Message::FromNameStatus { EmojiStatusId id; std::unique_ptr custom; + ClickHandlerPtr link; int skip = 0; }; @@ -2815,6 +2817,25 @@ bool Message::getStateFromName( Unexpected("Corrupt forwarded information in message."); } }(); + + const auto statusWidth = (from && _fromNameStatus) + ? st::dialogsPremiumIcon.icon.width() + : 0; + if (statusWidth && availableWidth > statusWidth) { + const auto x = availableLeft + std::min( + availableWidth - statusWidth, + nameText->maxWidth() + ) - (_fromNameStatus->custom ? (2 * _fromNameStatus->skip) : 0); + const auto checkWidth = _fromNameStatus->custom + ? (st::emojiSize - 2 * _fromNameStatus->skip) + : statusWidth; + if (point.x() >= x && point.x() < x + checkWidth) { + ensureFromNameStatusLink(from); + outResult->link = _fromNameStatus->link; + return true; + } + availableWidth -= statusWidth; + } if (point.x() >= availableLeft && point.x() < availableLeft + availableWidth && point.x() < availableLeft + nameText->maxWidth()) { @@ -2835,6 +2856,21 @@ bool Message::getStateFromName( return false; } +void Message::ensureFromNameStatusLink(not_null peer) const { + Expects(_fromNameStatus != nullptr); + + if (_fromNameStatus->link) { + return; + } + _fromNameStatus->link = std::make_shared([=]( + ClickContext context) { + const auto controller = ExtractController(context); + if (controller) { + Settings::ShowEmojiStatusPremium(controller, peer); + } + }); +} + bool Message::getStateTopicButton( QPoint point, QRect &trect, diff --git a/Telegram/SourceFiles/history/view/history_view_message.h b/Telegram/SourceFiles/history/view/history_view_message.h index 0b5341795..e3541e10f 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.h +++ b/Telegram/SourceFiles/history/view/history_view_message.h @@ -300,6 +300,7 @@ private: void refreshRightBadge(); void validateFromNameText(PeerData *from) const; + void ensureFromNameStatusLink(not_null peer) const; mutable std::unique_ptr _rightAction; mutable ClickHandlerPtr _fastReplyLink; diff --git a/Telegram/SourceFiles/history/view/history_view_paid_reaction_toast.cpp b/Telegram/SourceFiles/history/view/history_view_paid_reaction_toast.cpp index d79ec1005..2fd79450b 100644 --- a/Telegram/SourceFiles/history/view/history_view_paid_reaction_toast.cpp +++ b/Telegram/SourceFiles/history/view/history_view_paid_reaction_toast.cpp @@ -163,7 +163,7 @@ PaidReactionToast::~PaidReactionToast() { bool PaidReactionToast::maybeShowFor(not_null item) { const auto count = item->reactionsPaidScheduled(); - const auto anonymous = item->reactionsLocalAnonymous(); + const auto shownPeer = item->reactionsLocalShownPeer(); const auto at = _owner->reactions().sendingScheduledPaidAt(item); if (!count || !at) { return false; @@ -173,14 +173,14 @@ bool PaidReactionToast::maybeShowFor(not_null item) { if (at <= crl::now() + ignore) { return false; } - showFor(item->fullId(), count, anonymous, at - ignore, total); + showFor(item->fullId(), count, shownPeer, at - ignore, total); return true; } void PaidReactionToast::showFor( FullMsgId itemId, int count, - bool anonymous, + PeerId shownPeer, crl::time finish, crl::time total) { const auto old = _weak.get(); @@ -188,7 +188,7 @@ void PaidReactionToast::showFor( if (i != end(_stack)) { if (old && i + 1 == end(_stack)) { _count = count; - _anonymous = anonymous; + _shownPeer = shownPeer; _timeFinish = finish; return; } @@ -202,14 +202,14 @@ void PaidReactionToast::showFor( _hiding.push_back(base::take(_weak)); } _count.reset(); - _anonymous.reset(); + _shownPeer.reset(); _timeFinish.reset(); _count = count; - _anonymous = anonymous; + _shownPeer = shownPeer; _timeFinish = finish; auto text = rpl::combine( rpl::conditional( - _anonymous.value(), + _shownPeer.value() | rpl::map(rpl::mappers::_1 == PeerId()), tr::lng_paid_react_toast_anonymous( lt_count, _count.value() | tr::to_count(), diff --git a/Telegram/SourceFiles/history/view/history_view_paid_reaction_toast.h b/Telegram/SourceFiles/history/view/history_view_paid_reaction_toast.h index 080a62c32..485778e20 100644 --- a/Telegram/SourceFiles/history/view/history_view_paid_reaction_toast.h +++ b/Telegram/SourceFiles/history/view/history_view_paid_reaction_toast.h @@ -40,7 +40,7 @@ private: void showFor( FullMsgId itemId, int count, - bool anonymous, + PeerId shownPeer, crl::time left, crl::time total); @@ -54,7 +54,7 @@ private: base::weak_ptr _weak; std::vector> _hiding; rpl::variable _count; - rpl::variable _anonymous; + rpl::variable _shownPeer; rpl::variable _timeFinish; std::vector _stack; diff --git a/Telegram/SourceFiles/history/view/media/history_view_document.cpp b/Telegram/SourceFiles/history/view/media/history_view_document.cpp index e9899a55d..f537d78ca 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_document.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_document.cpp @@ -510,9 +510,19 @@ QSize Document::countOptimalSize() { accumulate_max(maxWidth, tleft + named->name.maxWidth() + tright); accumulate_min(maxWidth, st::msgMaxWidth); } - if (voice && voice->transcribe) { - maxWidth += st::historyTranscribeSkip - + voice->transcribe->size().width(); + if (voice) { + const auto maxWaveformWidth = ::Media::Player::kWaveformSamplesCount * + (st::msgWaveformBar + st::msgWaveformSkip); + const auto transcribeWidth = voice->transcribe + ? (voice->transcribe->size().width() + st::historyTranscribeSkip) + : 0; + accumulate_max( + maxWidth, + maxWaveformWidth + + rect::m::sum::h(st.padding) + + st.thumbSize + + st.thumbSkip + + transcribeWidth); } auto minHeight = st.padding.top() + st.thumbSize + st.padding.bottom(); diff --git a/Telegram/SourceFiles/history/view/media/history_view_game.cpp b/Telegram/SourceFiles/history/view/media/history_view_game.cpp index 71d3a75c2..a2dd5d2e9 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_game.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_game.cpp @@ -69,7 +69,10 @@ QSize Game::countOptimalSize() { // init attach if (!_attach) { - _attach = CreateAttach(_parent, _data->document, _data->photo); + _attach = CreateAttach( + _parent, + _data->document, + _data->document ? nullptr : _data->photo); } // init strings diff --git a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp index 7b449b1e3..4dc43f99b 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp @@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "media/streaming/media_streaming_instance.h" #include "media/streaming/media_streaming_player.h" #include "media/streaming/media_streaming_utility.h" +#include "media/view/media_view_open_common.h" #include "media/view/media_view_playback_progress.h" #include "ui/boxes/confirm_box.h" #include "ui/painter.h" @@ -47,6 +48,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/ui_utility.h" #include "ui/effects/path_shift_gradient.h" #include "ui/effects/spoiler_mess.h" +#include "data/data_photo.h" +#include "data/data_photo_media.h" #include "data/data_session.h" #include "data/data_stories.h" #include "data/data_streaming.h" @@ -54,6 +57,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_file_click_handler.h" #include "data/data_file_origin.h" #include "data/data_document_media.h" +#include "data/data_web_page.h" +#include "storage/storage_account.h" #include "styles/style_chat.h" #include @@ -145,6 +150,7 @@ Gif::Gif( bool spoiler) : File(parent, realParent) , _data(document) +, _videoCover(LookupVideoCover(document, realParent)) , _storyId(realParent->media() ? realParent->media()->storyId() : FullStoryId()) @@ -154,7 +160,9 @@ Gif::Gif( ? std::make_unique() : nullptr) , _downloadSize(Ui::FormatSizeText(_data->size)) -, _sensitiveSpoiler(realParent->isMediaSensitive()) { +, _videoTimestamp(::Media::View::ExtractVideoTimestamp(realParent)) +, _sensitiveSpoiler(realParent->isMediaSensitive()) +, _hasVideoCover(realParent->media() && realParent->media()->videoCover()) { if (_data->isVideoMessage() && _parent->data()->media()->ttlSeconds()) { if (_spoiler) { _drawTtl = CreateTtlPaintCallback([=] { repaint(); }); @@ -200,6 +208,12 @@ Gif::Gif( if ((_dataMedia = _data->activeMediaView())) { dataMediaCreated(); + } else if (_videoCover) { + if (_videoCover->inlineThumbnailBytes().isEmpty() + && (_videoCover->hasExact(Data::PhotoSize::Small) + || _videoCover->hasExact(Data::PhotoSize::Thumbnail))) { + _videoCover->load(Data::PhotoSize::Small, realParent->fullId()); + } } else { _data->loadThumbnail(realParent->fullId()); if (!autoplayEnabled()) { @@ -396,6 +410,14 @@ bool Gif::downloadInCorner() const { && !_data->inappPlaybackFailed(); } +bool Gif::autoplayUnderCursor() const { + return (_videoTimestamp || _hasVideoCover); +} + +bool Gif::underCursor() const { + return ClickHandler::getActive() == currentVideoLink(); +} + bool Gif::autoplayEnabled() const { if (_realParent->isSponsored()) { return true; @@ -413,6 +435,8 @@ bool Gif::hideMessageText() const { void Gif::draw(Painter &p, const PaintContext &context) const { if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) return; + _smallGroupPart = false; + ensureDataMediaCreated(); const auto item = _parent->data(); const auto loaded = dataLoaded(); @@ -469,11 +493,14 @@ void Gif::draw(Painter &p, const PaintContext &context) const { validateSpoilerImageCache(rthumb.size(), rounding); } - const auto startPlay = autoplay + const auto canStartPlay = autoplay && !_streamed && !activeRoundPlaying && !fullHiddenBySpoiler; - if (startPlay) { + const auto shouldBePlaying = !autoplayUnderCursor() || underCursor(); + if (!shouldBePlaying && _videoTimestamp != 0) { + const_cast(this)->stopAnimation(); + } else if (canStartPlay) { const_cast(this)->playAnimation(true); } else { checkStreamedIsStarted(); @@ -514,8 +541,9 @@ void Gif::draw(Painter &p, const PaintContext &context) const { const auto skipDrawingContent = context.skipDrawingParts == PaintContext::SkipDrawingParts::Content; - if (streamed && !skipDrawingContent && !fullHiddenBySpoiler) { - auto paused = context.paused; + const auto drawStreamed = streamed && (shouldBePlaying || !_videoCover); + if (drawStreamed && !skipDrawingContent && !fullHiddenBySpoiler) { + auto paused = context.paused || !shouldBePlaying; auto request = ::Media::Streaming::FrameRequest{ .outer = QSize(usew, painth) * style::DevicePixelRatio(), .blurredBackground = true, @@ -583,6 +611,9 @@ void Gif::draw(Painter &p, const PaintContext &context) const { validateThumbCache({ usew, painth }, isRound, rounding); p.drawImage(rthumb, _thumbCache); } + if (!isRound) { + paintTimestampMark(p, rthumb, rounding); + } if (revealed < 1.) { p.setOpacity(1. - revealed); @@ -868,6 +899,70 @@ void Gif::paintTranscribe( context); } +void Gif::paintTimestampMark( + Painter &p, + QRect rthumb, + std::optional rounding) const { + if (_videoTimestamp <= 0 && _videoPosition < crl::time(200)) { + return; + } + const auto convert = [](Ui::BubbleCornerRounding rounding) { + return (rounding == Ui::BubbleCornerRounding::Small) + ? Ui::BubbleRadiusSmall() + : (rounding == Ui::BubbleCornerRounding::Large) + ? Ui::BubbleRadiusLarge() + : 0; + }; + const auto radiusl = rounding + ? convert(rounding->bottomLeft) + : st::roundRadiusSmall; + const auto radiusr = rounding + ? convert(rounding->bottomRight) + : st::roundRadiusSmall; + const auto line = st::historyVideoTimestampProgressLine; + const auto duration = _data->duration(); + const auto position = (_videoPosition > 0) + ? _videoPosition + : (_videoTimestamp * crl::time(1000)); + if (rthumb.height() <= line + || rthumb.width() <= radiusl + radiusr + || position > duration) { + return; + } + auto hq = PainterHighQualityEnabler(p); + const auto used = rthumb.width() - radiusl - radiusr; + const auto progress = position / float64(duration); + const auto edge = radiusl + int(base::SafeRound(used * progress)); + const auto top = rthumb.y() + rthumb.height() - line; + p.save(); + p.setPen(Qt::NoPen); + if (edge > 0) { + p.setBrush(st::windowBgActive); + + p.setClipRect(rthumb.x(), top, edge, line); + p.drawRoundedRect( + rthumb.x(), + top - 2 * radiusl, + edge + radiusl, + line + 2 * radiusl, + radiusl, + radiusl); + } + if (const auto width = rthumb.width() - edge; width > 0) { + const auto left = rthumb.x() + edge; + p.setBrush(st::mediaviewPlaybackProgressFg); + p.setClipRect(left, top, width, line); + p.drawRoundedRect( + left - radiusr, + top - 2 * radiusr, + width + radiusr, + line + 2 * radiusr, + radiusr, + radiusr); + } + p.restore(); +} + void Gif::drawSpoilerTag( Painter &p, QRect rthumb, @@ -891,6 +986,8 @@ QImage Gif::spoilerTagBackground() const { } void Gif::validateVideoThumbnail() const { + Expects(!_videoCover); + const auto content = _dataMedia->videoThumbnailContent(); if (_videoThumbnailFrame || content.isEmpty()) { return; @@ -906,13 +1003,25 @@ void Gif::validateThumbCache( QSize outer, bool isEllipse, std::optional rounding) const { - const auto good = _dataMedia->goodThumbnail(); - const auto normal = good ? good : _dataMedia->thumbnail(); + const auto good = _videoCoverMedia + ? _videoCoverMedia->image(Data::PhotoSize::Large) + : _dataMedia->goodThumbnail(); + const auto normal = good + ? good + : _videoCoverMedia + ? nullptr + : _dataMedia->thumbnail(); if (!normal) { - _data->loadThumbnail(_realParent->fullId()); - validateVideoThumbnail(); + if (_videoCoverMedia) { + _videoCover->load(Data::PhotoSize::Small, _realParent->fullId()); + } else { + _data->loadThumbnail(_realParent->fullId()); + validateVideoThumbnail(); + } } - const auto videothumb = normal ? nullptr : _videoThumbnailFrame.get(); + const auto videothumb = (normal || _videoCoverMedia) + ? nullptr + : _videoThumbnailFrame.get(); const auto blurred = normal ? (!good && (normal->width() < kUseNonBlurredThreshold) @@ -934,9 +1043,17 @@ void Gif::validateThumbCache( } QImage Gif::prepareThumbCache(QSize outer) const { - const auto good = _dataMedia->goodThumbnail(); - const auto normal = good ? good : _dataMedia->thumbnail(); - const auto videothumb = normal ? nullptr : _videoThumbnailFrame.get(); + const auto good = _videoCoverMedia + ? _videoCoverMedia->image(Data::PhotoSize::Large) + : _dataMedia->goodThumbnail(); + const auto normal = good + ? good + : _videoCoverMedia + ? nullptr + : _dataMedia->thumbnail(); + const auto videothumb = (normal || _videoCoverMedia) + ? nullptr + : _videoThumbnailFrame.get(); auto blurred = (!good && normal && (normal->width() < kUseNonBlurredThreshold) @@ -946,6 +1063,10 @@ QImage Gif::prepareThumbCache(QSize outer) const { const auto blurFromLarge = good || (normal && !blurred); const auto large = blurFromLarge ? normal : videothumb; if (videothumb) { + } else if (_videoCoverMedia) { + if (const auto embedded = _videoCoverMedia->thumbnailInline()) { + blurred = embedded; + } } else if (const auto embedded = _dataMedia->thumbnailInline()) { blurred = embedded; } @@ -971,7 +1092,9 @@ void Gif::validateSpoilerImageCache( && _spoiler->backgroundRounding == rounding) { return; } - const auto normal = _dataMedia->thumbnail(); + const auto normal = _videoCoverMedia + ? _videoCoverMedia->image(Data::PhotoSize::Small) + : _dataMedia->thumbnail(); auto container = std::optional(); const auto downscale = [&](Image *image) { if (!image || (image->width() <= 40 && image->height() <= 40)) { @@ -983,7 +1106,9 @@ void Gif::validateSpoilerImageCache( Qt::SmoothTransformation)); return &*container; }; - const auto embedded = _dataMedia->thumbnailInline(); + const auto embedded = _videoCoverMedia + ? _videoCoverMedia->thumbnailInline() + : _dataMedia->thumbnailInline(); const auto blurred = embedded ? embedded : downscale(normal); _spoiler->background = Images::Round( PrepareWithBlurredBackground( @@ -1174,15 +1299,7 @@ TextState Gif::textState(QPoint point, StateRequest request) const { : (isRound && _parent->data()->media()->ttlSeconds()) ? _openl // Overriden. : _spoiler->link) - : _data->uploading() - ? _cancell - : _realParent->isSending() - ? nullptr - : (dataLoaded() || _dataMedia->canBePlayed(_realParent)) - ? _openl - : _data->loading() - ? _cancell - : _savel; + : currentVideoLink(); } const auto checkBottomInfo = !inWebPage && (unwrapped || !bubble || isBubbleBottom()); @@ -1290,8 +1407,8 @@ void Gif::drawGrouped( || _data->displayLoading(); const auto st = context.st; const auto sti = context.imageStyle(); - const auto fullFeatured = fullFeaturedGrouped(sides); - const auto cornerDownload = fullFeatured && downloadInCorner(); + _smallGroupPart = !fullFeaturedGrouped(sides); + const auto cornerDownload = !_smallGroupPart && downloadInCorner(); const auto canBePlayed = _dataMedia->canBePlayed(_realParent); const auto revealed = _spoiler @@ -1302,16 +1419,22 @@ void Gif::drawGrouped( validateSpoilerImageCache(geometry.size(), rounding); } - const auto autoplay = fullFeatured + const auto autoplay = !_smallGroupPart && autoplayEnabled() && canBePlayed && CanPlayInline(_data); - const auto startPlay = autoplay && !_streamed; - if (startPlay) { + const auto canStartPlay = autoplay + && !_streamed + && !fullHiddenBySpoiler; + const auto shouldBePlaying = !autoplayUnderCursor() || underCursor(); + if (!shouldBePlaying && _videoTimestamp != 0) { + const_cast(this)->stopAnimation(); + } else if (canStartPlay) { const_cast(this)->playAnimation(true); } else { checkStreamedIsStarted(); } + const auto streamingMode = _streamed || autoplay; const auto activeOwnPlaying = activeOwnStreamed(); @@ -1364,7 +1487,9 @@ void Gif::drawGrouped( activeOwnPlaying->frozenStatusText = QString(); } p.drawImage(geometry, streamed->frame(request)); - if (!context.paused) { + const auto paused = context.paused + || (autoplayUnderCursor() && !underCursor()); + if (!paused) { streamed->markFrameShown(); } } @@ -1479,7 +1604,7 @@ void Gif::drawGrouped( } p.setOpacity(1.); } - if (fullFeatured) { + if (!_smallGroupPart) { drawCornerStatus(p, context, geometry.topLeft()); } } @@ -1492,8 +1617,7 @@ TextState Gif::getStateGrouped( if (!geometry.contains(point)) { return {}; } - const auto isFullFeaturedGrouped = fullFeaturedGrouped(sides); - if (isFullFeaturedGrouped) { + if (!_smallGroupPart) { const auto state = cornerStatusTextState( point, request, @@ -1503,37 +1627,53 @@ TextState Gif::getStateGrouped( } } ensureDataMediaCreated(); + auto link = (_spoiler && !_spoiler->revealed) ? (_sensitiveSpoiler ? spoilerTagLink() : _spoiler->link) - : _data->uploading() + : currentVideoLink(); + return TextState(_parent, std::move(link)); +} + +ClickHandlerPtr Gif::currentVideoLink() const { + return _data->uploading() ? _cancell : _realParent->isSending() ? nullptr : dataLoaded() ? _openl - : (_data->loading() && !isFullFeaturedGrouped) + : (_data->loading() && _smallGroupPart) ? _cancell : _dataMedia->canBePlayed(_realParent) ? _openl + : _data->loading() + ? _cancell : _savel; - return TextState(_parent, std::move(link)); } void Gif::ensureDataMediaCreated() const { - if (_dataMedia) { + if (_dataMedia && (!_videoCover || _videoCoverMedia)) { return; } _dataMedia = _data->createMediaView(); + _videoCoverMedia = _videoCover + ? _videoCover->createMediaView() + : nullptr; dataMediaCreated(); } void Gif::dataMediaCreated() const { Expects(_dataMedia != nullptr); - _dataMedia->goodThumbnailWanted(); - _dataMedia->thumbnailWanted(_realParent->fullId()); - if (!autoplayEnabled()) { - _dataMedia->videoThumbnailWanted(_realParent->fullId()); + if (_videoCoverMedia) { + _videoCoverMedia->wanted( + Data::PhotoSize::Large, + _realParent->fullId()); + } else { + _dataMedia->goodThumbnailWanted(); + _dataMedia->thumbnailWanted(_realParent->fullId()); + if (!autoplayEnabled()) { + _dataMedia->videoThumbnailWanted(_realParent->fullId()); + } } history()->owner().registerHeavyViewPart(_parent); togglePollingStory(true); @@ -1670,12 +1810,18 @@ void Gif::validateGroupedCache( ensureDataMediaCreated(); - const auto good = _dataMedia->goodThumbnail(); - const auto thumb = _dataMedia->thumbnail(); + const auto good = _videoCoverMedia + ? _videoCoverMedia->image(Data::PhotoSize::Large) + : _dataMedia->goodThumbnail(); + const auto thumb = _videoCoverMedia + ? nullptr + : _dataMedia->thumbnail(); const auto image = good ? good : thumb ? thumb + : _videoCoverMedia + ? _videoCoverMedia->thumbnailInline() : _dataMedia->thumbnailInline(); const auto blur = !good && (!thumb @@ -1746,7 +1892,8 @@ void Gif::updateStatusText() const { } const auto round = activeRoundStreamed(); const auto own = activeOwnStreamed(); - if (round || (own && own->frozenFrame.isNull() && _data->isVideoFile())) { + if (round || (own && _data->isVideoFile())) { + const auto frozen = own && !own->frozenFrame.isNull(); const auto streamed = round ? round : &own->instance; const auto state = streamed->player().prepareLegacyState(); if (state.length) { @@ -1756,9 +1903,17 @@ void Gif::updateStatusText() const { } else if (!::Media::Player::IsStoppedOrStopping(state.state)) { position = state.position; } - statusSize = -1 - int((state.length - position) / state.frequency + 1); + if (!frozen) { + statusSize = -1 - int((state.length - position) / state.frequency + 1); + } + _videoPosition = std::max( + crl::time(position * crl::time(1000) / state.frequency), + crl::time(1)); } else { - statusSize = -1 - (_data->duration() / 1000); + if (!frozen) { + statusSize = -1 - (_data->duration() / 1000); + } + _videoPosition = 0; } } if (statusSize != _statusSize) { @@ -1918,6 +2073,10 @@ void Gif::startStreamedPlayer() const { //if (!_streamed->withSound) { options.mode = ::Media::Streaming::Mode::Video; options.loop = true; + options.position = _videoTimestamp + ? (_videoTimestamp * crl::time(1000)) + : _parent->history()->session().local().mediaLastPlaybackPosition( + _data->id); //} _streamed->instance.play(options); } @@ -1925,10 +2084,12 @@ void Gif::startStreamedPlayer() const { void Gif::checkStreamedIsStarted() const { if (!_streamed || _streamed->instance.playerLocked()) { return; - } else if (_streamed->instance.paused()) { - _streamed->instance.resume(); } - if (!_streamed->instance.active() && !_streamed->instance.failed()) { + if (_streamed->instance.active()) { + if (_streamed->instance.paused()) { + _streamed->instance.resume(); + } + } else if (!_streamed->instance.failed()) { startStreamedPlayer(); } } @@ -1941,6 +2102,7 @@ void Gif::setStreamed(std::unique_ptr value) { history()->owner().registerHeavyViewPart(_parent); togglePollingStory(true); } else if (removed) { + _videoPosition = 0; _parent->checkHeavyPart(); } } diff --git a/Telegram/SourceFiles/history/view/media/history_view_gif.h b/Telegram/SourceFiles/history/view/media/history_view_gif.h index 45edc9ce3..f663d00d2 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_gif.h +++ b/Telegram/SourceFiles/history/view/media/history_view_gif.h @@ -15,9 +15,11 @@ struct HistoryMessageVia; struct HistoryMessageReply; struct HistoryMessageForwarded; class Painter; +class PhotoData; namespace Data { class DocumentMedia; +class PhotoMedia; } // namespace Data namespace Media { @@ -37,6 +39,7 @@ enum class Error; namespace HistoryView { +class Photo; class Reply; class TranscribeButton; @@ -137,6 +140,8 @@ private: void dataMediaCreated() const; [[nodiscard]] bool autoplayEnabled() const; + [[nodiscard]] bool autoplayUnderCursor() const; + [[nodiscard]] bool underCursor() const; void playAnimation(bool autoplay) override; QSize countOptimalSize() override; @@ -163,6 +168,10 @@ private: int y, bool right, const PaintContext &context) const; + void paintTimestampMark( + Painter &p, + QRect rthumb, + std::optional rounding) const; [[nodiscard]] bool needInfoDisplay() const; [[nodiscard]] bool needCornerStatusDisplay() const; @@ -202,28 +211,35 @@ private: QPoint point, StateRequest request, QPoint position) const; + [[nodiscard]] ClickHandlerPtr currentVideoLink() const; void togglePollingStory(bool enabled) const; TtlRoundPaintCallback _drawTtl; const not_null _data; + PhotoData *_videoCover = nullptr; const FullStoryId _storyId; std::unique_ptr _streamed; const std::unique_ptr _spoiler; mutable std::unique_ptr _spoilerTag; mutable std::unique_ptr _transcribe; mutable std::shared_ptr _dataMedia; + mutable std::shared_ptr _videoCoverMedia; mutable std::unique_ptr _videoThumbnailFrame; QString _downloadSize; mutable QImage _thumbCache; mutable QImage _roundingMask; + mutable crl::time _videoPosition = 0; + mutable TimeId _videoTimestamp = 0; mutable std::optional _thumbCacheRounding; mutable bool _thumbCacheBlurred : 1 = false; mutable bool _thumbIsEllipse : 1 = false; mutable bool _pollingStory : 1 = false; mutable bool _purchasedPriceTag : 1 = false; + mutable bool _smallGroupPart : 1 = false; const bool _sensitiveSpoiler : 1 = false; + const bool _hasVideoCover : 1 = false; }; diff --git a/Telegram/SourceFiles/history/view/media/history_view_photo.h b/Telegram/SourceFiles/history/view/media/history_view_photo.h index 710901f76..14c6f8344 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_photo.h +++ b/Telegram/SourceFiles/history/view/media/history_view_photo.h @@ -114,7 +114,6 @@ private: void ensureDataMediaCreated() const; void dataMediaCreated() const; - void setupSpoilerTag() const; QSize countOptimalSize() override; QSize countCurrentSize(int newWidth) override; diff --git a/Telegram/SourceFiles/history/view/media/history_view_unique_gift.cpp b/Telegram/SourceFiles/history/view/media/history_view_unique_gift.cpp index 744b3bd08..27ca9c545 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_unique_gift.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_unique_gift.cpp @@ -496,7 +496,9 @@ auto UniqueGiftBg( p.setPen(Qt::NoPen); const auto webpreview = (media.get() != view->media()); const auto thickness = webpreview ? 0 : st::chatUniqueGiftBorder * 2; - const auto radius = st::msgServiceGiftBoxRadius - thickness; + const auto radius = webpreview + ? st::roundRadiusLarge + : (st::msgServiceGiftBoxRadius - thickness); const auto full = QRect(0, 0, media->width(), media->height()); const auto inner = full.marginsRemoved( { thickness, thickness, thickness, thickness }); @@ -519,7 +521,8 @@ auto UniqueGiftBg( const auto width = media->width(); const auto shift = width / 12; const auto doubled = width + 2 * shift; - const auto outer = QRect(-shift, -shift, doubled, doubled); + const auto top = (webpreview ? 2 : 1) * (-shift); + const auto outer = QRect(-shift, top, doubled, doubled); p.setClipRect(inner); Ui::PaintPoints( p, diff --git a/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp b/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp index 379fd42d7..e34d492fa 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp @@ -80,15 +80,11 @@ constexpr auto kSponsoredUserpicLines = 2; const auto spoiler = false; for (const auto &item : data.items) { if (const auto document = std::get_if(&item)) { - const auto hasQualitiesList = false; - const auto skipPremiumEffect = false; - result.push_back(std::make_unique( - parent, - *document, - skipPremiumEffect, - hasQualitiesList, - spoiler, - /*ttlSeconds = */0)); + using MediaFile = Data::MediaFile; + using Args = MediaFile::Args; + const auto data = *document; + result.push_back( + std::make_unique(parent, data, Args{})); } else if (const auto photo = std::get_if(&item)) { result.push_back(std::make_unique( parent, @@ -317,13 +313,13 @@ void WebPage::setupAdditionalData() { UrlClickHandler::Open(link); }); if (!_attach) { - const auto maybePhoto = details.mediaPhotoId - ? session->data().photo(details.mediaPhotoId).get() - : nullptr; const auto maybeDocument = details.mediaDocumentId ? session->data().document( details.mediaDocumentId).get() : nullptr; + const auto maybePhoto = (!maybeDocument && details.mediaPhotoId) + ? session->data().photo(details.mediaPhotoId).get() + : nullptr; _attach = CreateAttach( _parent, maybeDocument, @@ -528,7 +524,9 @@ QSize WebPage::countOptimalSize() { _attach = CreateAttach( _parent, _data->document, - _data->photo, + ((!_data->document || _data->photoIsVideoCover) + ? _data->photo + : nullptr), _collage, _data->url); } diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style index e2c2311f9..92bab06f5 100644 --- a/Telegram/SourceFiles/info/info.style +++ b/Telegram/SourceFiles/info/info.style @@ -1164,6 +1164,7 @@ infoHoursOuter: RoundButton(defaultActiveButton) { } infoHoursOuterMargin: margins(8px, 4px, 8px, 4px); infoHoursDaySkip: 6px; +infoHoursArrowSize: 4px; infoSharedMediaScroll: ScrollArea(defaultScrollArea) { round: 1px; diff --git a/Telegram/SourceFiles/info/info_wrap_widget.cpp b/Telegram/SourceFiles/info/info_wrap_widget.cpp index c603bdf7c..81fec3a71 100644 --- a/Telegram/SourceFiles/info/info_wrap_widget.cpp +++ b/Telegram/SourceFiles/info/info_wrap_widget.cpp @@ -110,6 +110,8 @@ WrapWidget::WrapWidget( Wrap wrap, not_null memento) : SectionWidget(parent, window, rpl::producer()) +, _isSeparatedWindow( + window->windowId().type == Window::SeparateType::SharedMedia) , _wrap(wrap) , _controller(createController(window, memento->content())) , _topShadow(this) @@ -446,10 +448,7 @@ void WrapWidget::setupTopBarMenuToggle() { addTopBarMenuButton(); } }, _topBar->lifetime()); - } else if (section.type() == Section::Type::PeerGifts - && key.peer() - && key.peer()->isChannel() - && key.peer()->canManageGifts()) { + } else if (section.type() == Section::Type::PeerGifts && key.peer()) { addTopBarMenuButton(); } } @@ -504,6 +503,7 @@ void WrapWidget::addTopBarMenuButton() { using Command = Shortcuts::Command; request->check(Command::ShowChatMenu, 1) && request->handle([=] { + Window::ActivateWindow(_controller->parentController()); showTopBarMenu(false); return true; }); @@ -1048,7 +1048,8 @@ const Ui::RoundRect *WrapWidget::bottomSkipRounding() const { } bool WrapWidget::hasBackButton() const { - return (wrap() == Wrap::Narrow || hasStackHistory()); + return !_isSeparatedWindow + && (wrap() == Wrap::Narrow || hasStackHistory()); } bool WrapWidget::willHaveBackButton( diff --git a/Telegram/SourceFiles/info/info_wrap_widget.h b/Telegram/SourceFiles/info/info_wrap_widget.h index 0ece881a0..815765154 100644 --- a/Telegram/SourceFiles/info/info_wrap_widget.h +++ b/Telegram/SourceFiles/info/info_wrap_widget.h @@ -208,6 +208,8 @@ private: void addProfileCallsButton(); void showTopBarMenu(bool check); + const bool _isSeparatedWindow = false; + rpl::variable _wrap; std::unique_ptr _controller; object_ptr _content = { nullptr }; diff --git a/Telegram/SourceFiles/info/media/info_media_buttons.cpp b/Telegram/SourceFiles/info/media/info_media_buttons.cpp new file mode 100644 index 000000000..12d7827f4 --- /dev/null +++ b/Telegram/SourceFiles/info/media/info_media_buttons.cpp @@ -0,0 +1,305 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "info/media/info_media_buttons.h" + +#include "base/call_delayed.h" +#include "base/qt/qt_key_modifiers.h" +#include "core/application.h" +#include "data/data_channel.h" +#include "data/data_saved_messages.h" +#include "data/data_session.h" +#include "data/data_stories_ids.h" +#include "data/data_user.h" +#include "history/view/history_view_sublist_section.h" +#include "info/info_controller.h" +#include "info/info_memento.h" +#include "info/profile/info_profile_values.h" +#include "info/stories/info_stories_widget.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/popup_menu.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/wrap/vertical_layout.h" +#include "window/window_separate_id.h" +#include "window/window_session_controller.h" +#include "styles/style_info.h" +#include "styles/style_menu_icons.h" + +namespace Info::Media { +namespace { + +[[nodiscard]] Window::SeparateSharedMediaType ToSeparateType( + Storage::SharedMediaType type) { + using Type = Storage::SharedMediaType; + using SeparatedType = Window::SeparateSharedMediaType; + return (type == Type::Photo) + ? SeparatedType::Photos + : (type == Type::Video) + ? SeparatedType::Videos + : (type == Type::File) + ? SeparatedType::Files + : (type == Type::MusicFile) + ? SeparatedType::Audio + : (type == Type::Link) + ? SeparatedType::Links + : (type == Type::RoundVoiceFile) + ? SeparatedType::Voices + : (type == Type::GIF) + ? SeparatedType::GIF + : SeparatedType::None; +} + +[[nodiscard]] Window::SeparateId SeparateId( + not_null peer, + MsgId topicRootId, + Storage::SharedMediaType type) { + if (peer->isSelf()) { + return { nullptr }; + } + const auto separateType = ToSeparateType(type); + if (separateType == Window::SeparateSharedMediaType::None) { + return { nullptr }; + } + return { Window::SeparateSharedMedia(separateType, peer, topicRootId) }; +} + +void AddContextMenuToButton( + not_null button, + Fn openInWindow) { + if (!openInWindow) { + return; + } + button->setAcceptBoth(); + struct State final { + base::unique_qptr menu; + }; + const auto state = button->lifetime().make_state(); + button->addClickHandler([=](Qt::MouseButton mouse) { + if (mouse != Qt::RightButton) { + return; + } + state->menu = base::make_unique_q( + button.get(), + st::popupMenuWithIcons); + state->menu->addAction(tr::lng_context_new_window(tr::now), [=] { + base::call_delayed( + st::popupMenuWithIcons.showDuration, + crl::guard(button, openInWindow)); + }, &st::menuIconNewWindow); + state->menu->popup(QCursor::pos()); + }); +} + +} // namespace + +tr::phrase MediaTextPhrase(Type type) { + switch (type) { + case Type::Photo: return tr::lng_profile_photos; + case Type::GIF: return tr::lng_profile_gifs; + case Type::Video: return tr::lng_profile_videos; + case Type::File: return tr::lng_profile_files; + case Type::MusicFile: return tr::lng_profile_songs; + case Type::Link: return tr::lng_profile_shared_links; + case Type::RoundVoiceFile: return tr::lng_profile_audios; + } + Unexpected("Type in MediaTextPhrase()"); +}; + +Fn MediaText(Type type) { + return [phrase = MediaTextPhrase(type)](int count) { + return phrase(tr::now, lt_count, count); + }; +} + +not_null*> AddCountedButton( + Ui::VerticalLayout *parent, + rpl::producer &&count, + Fn &&textFromCount, + Ui::MultiSlideTracker &tracker) { + using namespace ::Settings; + auto forked = std::move(count) + | start_spawning(parent->lifetime()); + auto text = rpl::duplicate( + forked + ) | rpl::map([textFromCount](int count) { + return (count > 0) + ? textFromCount(count) + : QString(); + }); + auto button = parent->add(object_ptr>( + parent, + object_ptr( + parent, + std::move(text), + st::infoSharedMediaButton)) + )->setDuration( + st::infoSlideDuration + )->toggleOn( + rpl::duplicate(forked) | rpl::map(rpl::mappers::_1 > 0) + ); + tracker.track(button); + return button; +}; + +not_null AddButton( + Ui::VerticalLayout *parent, + not_null navigation, + not_null peer, + MsgId topicRootId, + PeerData *migrated, + Type type, + Ui::MultiSlideTracker &tracker) { + auto result = AddCountedButton( + parent, + Profile::SharedMediaCountValue(peer, topicRootId, migrated, type), + MediaText(type), + tracker)->entity(); + const auto separateId = SeparateId(peer, topicRootId, type); + const auto openInWindow = separateId + ? [=] { navigation->parentController()->showInNewWindow(separateId); } + : Fn(nullptr); + AddContextMenuToButton(result, openInWindow); + result->addClickHandler([=](Qt::MouseButton mouse) { + if (mouse == Qt::RightButton) { + return; + } + if (openInWindow + && (base::IsCtrlPressed() || mouse == Qt::MiddleButton)) { + return openInWindow(); + } + const auto topic = topicRootId + ? peer->forumTopicFor(topicRootId) + : nullptr; + if (topicRootId && !topic) { + return; + } + const auto separateId = SeparateId(peer, topicRootId, type); + if (Core::App().separateWindowFor(separateId) && openInWindow) { + openInWindow(); + } else { + navigation->showSection(topicRootId + ? std::make_shared(topic, Section(type)) + : std::make_shared(peer, Section(type))); + } + }); + return result; +}; + +not_null AddCommonGroupsButton( + Ui::VerticalLayout *parent, + not_null navigation, + not_null user, + Ui::MultiSlideTracker &tracker) { + auto result = AddCountedButton( + parent, + Profile::CommonGroupsCountValue(user), + [](int count) { + return tr::lng_profile_common_groups(tr::now, lt_count, count); + }, + tracker)->entity(); + result->addClickHandler([=] { + navigation->showSection( + std::make_shared( + user, + Section::Type::CommonGroups)); + }); + return result; +} + +not_null AddSimilarPeersButton( + Ui::VerticalLayout *parent, + not_null navigation, + not_null peer, + Ui::MultiSlideTracker &tracker) { + auto result = AddCountedButton( + parent, + Profile::SimilarPeersCountValue(peer), + [=](int count) { + return peer->isBroadcast() + ? tr::lng_profile_similar_channels(tr::now, lt_count, count) + : tr::lng_profile_similar_bots(tr::now, lt_count, count); + }, + tracker)->entity(); + result->addClickHandler([=] { + navigation->showSection( + std::make_shared( + peer, + Section::Type::SimilarPeers)); + }); + return result; +} + +not_null AddStoriesButton( + Ui::VerticalLayout *parent, + not_null navigation, + not_null peer, + Ui::MultiSlideTracker &tracker) { + auto count = rpl::single(0) | rpl::then(Data::SavedStoriesIds( + peer, + ServerMaxStoryId - 1, + 0 + ) | rpl::map([](const Data::StoriesIdsSlice &slice) { + return slice.fullCount().value_or(0); + })); + const auto phrase = peer->isChannel() ? (+[](int count) { + return tr::lng_profile_posts(tr::now, lt_count, count); + }) : (+[](int count) { + return tr::lng_profile_saved_stories(tr::now, lt_count, count); + }); + auto result = AddCountedButton( + parent, + std::move(count), + phrase, + tracker)->entity(); + result->addClickHandler([=] { + navigation->showSection(Info::Stories::Make(peer)); + }); + return result; +} + +not_null AddSavedSublistButton( + Ui::VerticalLayout *parent, + not_null navigation, + not_null peer, + Ui::MultiSlideTracker &tracker) { + auto result = AddCountedButton( + parent, + Profile::SavedSublistCountValue(peer), + [](int count) { + return tr::lng_profile_saved_messages(tr::now, lt_count, count); + }, + tracker)->entity(); + result->addClickHandler([=] { + navigation->showSection( + std::make_shared( + peer->owner().savedMessages().sublist(peer))); + }); + return result; +} + +not_null AddPeerGiftsButton( + Ui::VerticalLayout *parent, + not_null navigation, + not_null peer, + Ui::MultiSlideTracker &tracker) { + auto result = AddCountedButton( + parent, + Profile::PeerGiftsCountValue(peer), + [](int count) { + return tr::lng_profile_peer_gifts(tr::now, lt_count, count); + }, + tracker)->entity(); + result->addClickHandler([=] { + navigation->showSection( + std::make_shared( + peer, + Section::Type::PeerGifts)); + }); + return result; +} + +} // namespace Info::Media diff --git a/Telegram/SourceFiles/info/media/info_media_buttons.h b/Telegram/SourceFiles/info/media/info_media_buttons.h index 4efdb15f9..e8927c097 100644 --- a/Telegram/SourceFiles/info/media/info_media_buttons.h +++ b/Telegram/SourceFiles/info/media/info_media_buttons.h @@ -7,220 +7,73 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once -#include -#include #include "lang/lang_keys.h" -#include "data/data_saved_messages.h" -#include "data/data_session.h" -#include "data/data_stories_ids.h" #include "storage/storage_shared_media.h" -#include "history/view/history_view_sublist_section.h" -#include "info/info_memento.h" -#include "info/info_controller.h" -#include "info/profile/info_profile_values.h" -#include "info/stories/info_stories_widget.h" -#include "ui/wrap/slide_wrap.h" -#include "ui/wrap/vertical_layout.h" -#include "ui/widgets/buttons.h" -#include "window/window_session_controller.h" -#include "data/data_channel.h" -#include "data/data_user.h" -#include "styles/style_info.h" + +namespace Ui { +class AbstractButton; +class MultiSlideTracker; +class SettingsButton; +class VerticalLayout; +template +class SlideWrap; +} // namespace Ui + +namespace Window { +class SessionNavigation; +} // namespace Window namespace Info::Media { using Type = Storage::SharedMediaType; -inline tr::phrase MediaTextPhrase(Type type) { - switch (type) { - case Type::Photo: return tr::lng_profile_photos; - case Type::GIF: return tr::lng_profile_gifs; - case Type::Video: return tr::lng_profile_videos; - case Type::File: return tr::lng_profile_files; - case Type::MusicFile: return tr::lng_profile_songs; - case Type::Link: return tr::lng_profile_shared_links; - case Type::RoundVoiceFile: return tr::lng_profile_audios; - } - Unexpected("Type in MediaTextPhrase()"); -}; +[[nodiscard]] tr::phrase MediaTextPhrase(Type type); -inline auto MediaText(Type type) { - return [phrase = MediaTextPhrase(type)](int count) { - return phrase(tr::now, lt_count, count); - }; -} +[[nodiscard]] Fn MediaText(Type type); -template -inline auto AddCountedButton( - Ui::VerticalLayout *parent, - Count &&count, - Text &&textFromCount, - Ui::MultiSlideTracker &tracker) { - using namespace rpl::mappers; +[[nodiscard]] not_null*> AddCountedButton( + Ui::VerticalLayout *parent, + rpl::producer &&count, + Fn &&textFromCount, + Ui::MultiSlideTracker &tracker); - using namespace ::Settings; - auto forked = std::move(count) - | start_spawning(parent->lifetime()); - auto text = rpl::duplicate( - forked - ) | rpl::map([textFromCount](int count) { - return (count > 0) - ? textFromCount(count) - : QString(); - }); - auto button = parent->add(object_ptr>( - parent, - object_ptr( - parent, - std::move(text), - st::infoSharedMediaButton)) - )->setDuration( - st::infoSlideDuration - )->toggleOn( - rpl::duplicate(forked) | rpl::map(_1 > 0) - ); - tracker.track(button); - return button; -}; +[[nodiscard]] not_null AddButton( + Ui::VerticalLayout *parent, + not_null navigation, + not_null peer, + MsgId topicRootId, + PeerData *migrated, + Type type, + Ui::MultiSlideTracker &tracker); -inline auto AddButton( - Ui::VerticalLayout *parent, - not_null navigation, - not_null peer, - MsgId topicRootId, - PeerData *migrated, - Type type, - Ui::MultiSlideTracker &tracker) { - auto result = AddCountedButton( - parent, - Profile::SharedMediaCountValue(peer, topicRootId, migrated, type), - MediaText(type), - tracker)->entity(); - result->addClickHandler([=] { - const auto topic = topicRootId - ? peer->forumTopicFor(topicRootId) - : nullptr; - if (topicRootId && !topic) { - return; - } - navigation->showSection(topicRootId - ? std::make_shared(topic, Section(type)) - : std::make_shared(peer, Section(type))); - }); - return result; -}; +[[nodiscard]] not_null AddCommonGroupsButton( + Ui::VerticalLayout *parent, + not_null navigation, + not_null user, + Ui::MultiSlideTracker &tracker); -inline auto AddCommonGroupsButton( - Ui::VerticalLayout *parent, - not_null navigation, - not_null user, - Ui::MultiSlideTracker &tracker) { - auto result = AddCountedButton( - parent, - Profile::CommonGroupsCountValue(user), - [](int count) { - return tr::lng_profile_common_groups(tr::now, lt_count, count); - }, - tracker)->entity(); - result->addClickHandler([=] { - navigation->showSection( - std::make_shared( - user, - Section::Type::CommonGroups)); - }); - return result; -} +[[nodiscard]] not_null AddSimilarPeersButton( + Ui::VerticalLayout *parent, + not_null navigation, + not_null peer, + Ui::MultiSlideTracker &tracker); -inline auto AddSimilarPeersButton( - Ui::VerticalLayout *parent, - not_null navigation, - not_null peer, - Ui::MultiSlideTracker &tracker) { - auto result = AddCountedButton( - parent, - Profile::SimilarPeersCountValue(peer), - [=](int count) { - return peer->isBroadcast() - ? tr::lng_profile_similar_channels(tr::now, lt_count, count) - : tr::lng_profile_similar_bots(tr::now, lt_count, count); - }, - tracker)->entity(); - result->addClickHandler([=] { - navigation->showSection( - std::make_shared( - peer, - Section::Type::SimilarPeers)); - }); - return result; -} +[[nodiscard]] not_null AddStoriesButton( + Ui::VerticalLayout *parent, + not_null navigation, + not_null peer, + Ui::MultiSlideTracker &tracker); -inline auto AddStoriesButton( - Ui::VerticalLayout *parent, - not_null navigation, - not_null peer, - Ui::MultiSlideTracker &tracker) { - auto count = rpl::single(0) | rpl::then(Data::SavedStoriesIds( - peer, - ServerMaxStoryId - 1, - 0 - ) | rpl::map([](const Data::StoriesIdsSlice &slice) { - return slice.fullCount().value_or(0); - })); - const auto phrase = peer->isChannel() ? (+[](int count) { - return tr::lng_profile_posts(tr::now, lt_count, count); - }) : (+[](int count) { - return tr::lng_profile_saved_stories(tr::now, lt_count, count); - }); - auto result = AddCountedButton( - parent, - std::move(count), - phrase, - tracker)->entity(); - result->addClickHandler([=] { - navigation->showSection(Info::Stories::Make(peer)); - }); - return result; -} +[[nodiscard]] not_null AddSavedSublistButton( + Ui::VerticalLayout *parent, + not_null navigation, + not_null peer, + Ui::MultiSlideTracker &tracker); -inline auto AddSavedSublistButton( - Ui::VerticalLayout *parent, - not_null navigation, - not_null peer, - Ui::MultiSlideTracker &tracker) { - auto result = AddCountedButton( - parent, - Profile::SavedSublistCountValue(peer), - [](int count) { - return tr::lng_profile_saved_messages(tr::now, lt_count, count); - }, - tracker)->entity(); - result->addClickHandler([=] { - navigation->showSection( - std::make_shared( - peer->owner().savedMessages().sublist(peer))); - }); - return result; -} - -inline auto AddPeerGiftsButton( - Ui::VerticalLayout *parent, - not_null navigation, - not_null peer, - Ui::MultiSlideTracker &tracker) { - auto result = AddCountedButton( - parent, - Profile::PeerGiftsCountValue(peer), - [](int count) { - return tr::lng_profile_peer_gifts(tr::now, lt_count, count); - }, - tracker)->entity(); - result->addClickHandler([=] { - navigation->showSection( - std::make_shared( - peer, - Section::Type::PeerGifts)); - }); - return result; -} +[[nodiscard]] not_null AddPeerGiftsButton( + Ui::VerticalLayout *parent, + not_null navigation, + not_null peer, + Ui::MultiSlideTracker &tracker); } // namespace Info::Media diff --git a/Telegram/SourceFiles/info/media/info_media_inner_widget.cpp b/Telegram/SourceFiles/info/media/info_media_inner_widget.cpp index eed93256b..6f2bedac4 100644 --- a/Telegram/SourceFiles/info/media/info_media_inner_widget.cpp +++ b/Telegram/SourceFiles/info/media/info_media_inner_widget.cpp @@ -15,10 +15,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/profile/info_profile_icon.h" #include "info/info_controller.h" #include "data/data_forum_topic.h" +#include "data/data_peer.h" #include "ui/widgets/discrete_sliders.h" #include "ui/widgets/shadow.h" #include "ui/widgets/buttons.h" #include "ui/widgets/box_content_divider.h" +#include "ui/wrap/slide_wrap.h" #include "ui/wrap/vertical_layout.h" #include "ui/search_field_controller.h" #include "styles/style_info.h" diff --git a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp index 3058bfd0b..9656bd08a 100644 --- a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp @@ -343,11 +343,11 @@ void InnerWidget::validateButtons() { } const auto giftId = _entries[index].gift.info.id; const auto manageId = _entries[index].gift.manageId; + const auto &descriptor = _entries[index].descriptor; const auto already = ranges::find(_views, giftId, &View::giftId); if (already != end(_views)) { views.push_back(base::take(*already)); } else { - const auto &descriptor = _entries[index].descriptor; const auto unused = ranges::find_if(_views, [&](const View &v) { return v.button && !idUsed(v.giftId, column, row); }); @@ -358,16 +358,16 @@ void InnerWidget::validateButtons() { button->show(); views.push_back({ .button = std::move(button) }); } - auto &view = views.back(); - const auto callback = [=] { - showGift(index); - }; - view.index = index; - view.manageId = manageId; - view.giftId = giftId; - view.button->setDescriptor(descriptor, mode); - view.button->setClickedCallback(callback); } + auto &view = views.back(); + const auto callback = [=] { + showGift(index); + }; + view.index = index; + view.manageId = manageId; + view.giftId = giftId; + view.button->setDescriptor(descriptor, mode); + view.button->setClickedCallback(callback); return true; }; for (auto j = fromRow; j != tillRow; ++j) { @@ -630,24 +630,26 @@ void Widget::fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) { }); }, filter.skipUnique ? nullptr : &st::mediaPlayerMenuCheck); - addAction({ .isSeparator = true }); + if (_inner->peer()->canManageGifts() && _inner->peer()->isChannel()) { + addAction({ .isSeparator = true }); - addAction(tr::lng_peer_gifts_filter_saved(tr::now), [=] { - change([](Filter &filter) { - filter.skipSaved = !filter.skipSaved; - if (filter.skipSaved && filter.skipUnsaved) { - filter.skipUnsaved = false; - } - }); - }, filter.skipSaved ? nullptr : &st::mediaPlayerMenuCheck); - addAction(tr::lng_peer_gifts_filter_unsaved(tr::now), [=] { - change([](Filter &filter) { - filter.skipUnsaved = !filter.skipUnsaved; - if (filter.skipSaved && filter.skipUnsaved) { - filter.skipSaved = false; - } - }); - }, filter.skipUnsaved ? nullptr : &st::mediaPlayerMenuCheck); + addAction(tr::lng_peer_gifts_filter_saved(tr::now), [=] { + change([](Filter &filter) { + filter.skipSaved = !filter.skipSaved; + if (filter.skipSaved && filter.skipUnsaved) { + filter.skipUnsaved = false; + } + }); + }, filter.skipSaved ? nullptr : &st::mediaPlayerMenuCheck); + addAction(tr::lng_peer_gifts_filter_unsaved(tr::now), [=] { + change([](Filter &filter) { + filter.skipUnsaved = !filter.skipUnsaved; + if (filter.skipSaved && filter.skipUnsaved) { + filter.skipSaved = false; + } + }); + }, filter.skipUnsaved ? nullptr : &st::mediaPlayerMenuCheck); + } } rpl::producer Widget::title() { diff --git a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp index 437177902..73595a182 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp @@ -44,6 +44,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "dialogs/ui/dialogs_message_view.h" #include "history/history.h" #include "history/history_item.h" +#include "history/history_item_components.h" #include "history/history_item_helpers.h" #include "history/view/history_view_item_preview.h" #include "info/bot/earn/info_bot_earn_widget.h" @@ -69,6 +70,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/boxes/peer_qr_box.h" #include "ui/boxes/report_box_graphics.h" #include "ui/controls/userpic_button.h" +#include "ui/effects/toggle_arrow.h" #include "ui/painter.h" #include "ui/rect.h" #include "ui/ui_utility.h" @@ -93,6 +95,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_layers.h" #include "styles/style_menu_icons.h" #include "styles/style_settings.h" // settingsButtonRightSkip. +#include "styles/style_window.h" // mainMenuToggleFourStrokes. #include #include @@ -516,6 +519,8 @@ base::options::toggle ShowPeerIdBelowAbout({ dayHoursTextValue(state->day.value()) ) | rpl::after_next(recount), st::infoHoursValue); + const auto timingArrow = Ui::CreateChild(openedWrap); + timingArrow->resize(Size(timing->st().style.font->height)); timing->setAttribute(Qt::WA_TransparentForMouseEvents); state->opened.value() | rpl::start_with_next([=](bool value) { opened->setTextColorOverride(value @@ -529,7 +534,8 @@ base::options::toggle ShowPeerIdBelowAbout({ timing->sizeValue() ) | rpl::start_with_next([=](int width, int h1, QSize size) { opened->moveToLeft(0, 0, width); - timing->moveToRight(0, 0, width); + timingArrow->moveToRight(0, 0, width); + timing->moveToRight(timingArrow->width(), 0, width); const auto margins = opened->getMargins(); const auto added = margins.top() + margins.bottom(); @@ -542,10 +548,7 @@ base::options::toggle ShowPeerIdBelowAbout({ tr::lng_info_hours_label(), st::infoLabel); label->setAttribute(Qt::WA_TransparentForMouseEvents); - const auto link = Ui::CreateChild( - labelWrap, - QString()); - rpl::combine( + auto linkText = rpl::combine( state->nonTrivial.value(), state->hours.value(), state->mine.value(), @@ -560,10 +563,12 @@ base::options::toggle ShowPeerIdBelowAbout({ : my ? tr::lng_info_hours_my_time() : tr::lng_info_hours_local_time(); - }) | rpl::flatten_latest( - ) | rpl::start_with_next([=](const QString &text) { - link->setText(text); - }, link->lifetime()); + }) | rpl::flatten_latest(); + const auto link = Ui::CreateChild( + labelWrap, + std::move(linkText), + st::defaultTableSmallButton); + link->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); link->setClickedCallback([=] { state->myTimezone = !state->myTimezone.current(); state->expanded = true; @@ -587,6 +592,38 @@ base::options::toggle ShowPeerIdBelowAbout({ inner, object_ptr(inner))); other->toggleOn(state->expanded.value(), anim::type::normal); + constexpr auto kSlideDuration = float64(st::slideWrapDuration); + other->setDuration(kSlideDuration); + { + const auto arrowAnimation + = other->lifetime().make_state(); + arrowAnimation->init([=] { + timingArrow->update(); + if (!other->animating()) { + arrowAnimation->stop(); + } + }); + timingArrow->paintRequest() | rpl::start_with_next([=] { + auto p = QPainter(timingArrow); + const auto progress = other->animating() + ? (crl::now() - arrowAnimation->started()) / kSlideDuration + : 1.; + + const auto path = Ui::ToggleUpDownArrowPath( + timingArrow->width() / 2, + timingArrow->height() / 2, + st::infoHoursArrowSize, + st::mainMenuToggleFourStrokes, + other->toggled() ? progress : 1 - progress); + + auto hq = PainterHighQualityEnabler(p); + p.fillPath(path, timing->st().textFg); + }, timingArrow->lifetime()); + state->expanded.value() | rpl::start_with_next([=] { + arrowAnimation->start(); + }, other->lifetime()); + } + other->finishAnimating(); const auto days = other->entity(); @@ -1729,6 +1766,10 @@ object_ptr DetailsFiller::setupPersonalChannel( && user->personalChannelMessageId(); })); messageChannelWrap->finishAnimating(); + messageChannelWrap->toggledValue( + ) | rpl::filter(rpl::mappers::_1) | rpl::start_with_next([=] { + messageChannelWrap->resizeToWidth(messageChannelWrap->width()); + }, messageChannelWrap->lifetime()); const auto clear = [=] { while (messageChannelWrap->entity()->count()) { @@ -1825,12 +1866,12 @@ object_ptr DetailsFiller::setupPersonalChannel( } }, preview->lifetime()); - line->sizeValue( + line->sizeValue() | rpl::filter_size( ) | rpl::start_with_next([=](const QSize &size) { const auto left = stLabeled.left(); const auto right = st::infoPersonalChannelDateSkip; const auto top = stLabeled.top(); - date->moveToRight(right, top); + date->moveToRight(right, top, size.width()); name->resizeToWidth(size.width() - left @@ -1861,14 +1902,9 @@ object_ptr DetailsFiller::setupPersonalChannel( st::infoProfileLabeledPadding.bottom())); } { - const auto button = Ui::CreateChild( + const auto button = Ui::CreateSimpleRectButton( messageChannelWrap->entity(), st::defaultRippleAnimation); - button->paintRequest( - ) | rpl::start_with_next([=](const QRect &rect) { - auto p = QPainter(button); - button->paintRipple(p, 0, 0); - }, button->lifetime()); inner->geometryValue( ) | rpl::start_with_next([=](const QRect &rect) { button->setGeometry(rect); @@ -1880,6 +1916,8 @@ object_ptr DetailsFiller::setupPersonalChannel( msg); }); button->lower(); + inner->lifetime().make_state>( + button); } inner->setAttribute(Qt::WA_TransparentForMouseEvents); Ui::AddSkip(messageChannelWrap->entity()); @@ -2445,7 +2483,7 @@ void ActionsFiller::addDeleteContactAction(not_null user) { void ActionsFiller::addFastButtonsMode(not_null user) { Expects(user->isBot()); - const auto helper = &user->session().supportHelper(); + const auto bots = &user->session().fastButtonsBots(); const auto button = _wrap->add(object_ptr( _wrap, rpl::single(u"Fast buttons mode"_q), @@ -2459,17 +2497,17 @@ void ActionsFiller::addFastButtonsMode(not_null user) { AddDivider(_wrap); AddSkip(_wrap); - button->toggleOn(helper->fastButtonModeValue(user)); + button->toggleOn(bots->enabledValue(user)); button->toggledValue( ) | rpl::filter([=](bool value) { - return value != helper->fastButtonMode(user); + return value != bots->enabled(user); }) | rpl::start_with_next([=](bool value) { - helper->setFastButtonMode(user, value); + bots->setEnabled(user, value); }, button->lifetime()); } void ActionsFiller::addBotCommandActions(not_null user) { - if (user->session().supportMode()) { + if (FastButtonsMode()) { addFastButtonsMode(user); } const auto window = _controller->parentController(); diff --git a/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp b/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp index 18d72a95c..25ca555b8 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp @@ -7,43 +7,32 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "info/profile/info_profile_inner_widget.h" -#include "info/info_memento.h" #include "info/info_controller.h" #include "info/profile/info_profile_widget.h" -#include "info/profile/info_profile_text.h" -#include "info/profile/info_profile_values.h" #include "info/profile/info_profile_cover.h" #include "info/profile/info_profile_icon.h" #include "info/profile/info_profile_members.h" #include "info/profile/info_profile_actions.h" #include "info/media/info_media_buttons.h" -#include "boxes/abstract_box.h" -#include "boxes/add_contact_box.h" #include "data/data_changes.h" +#include "data/data_channel.h" #include "data/data_forum_topic.h" +#include "data/data_peer.h" #include "data/data_photo.h" #include "data/data_file_origin.h" -#include "ui/boxes/confirm_box.h" -#include "mainwidget.h" +#include "data/data_user.h" #include "main/main_session.h" #include "apiwrap.h" #include "api/api_peer_photo.h" -#include "window/main_window.h" -#include "window/window_session_controller.h" -#include "storage/storage_shared_media.h" #include "lang/lang_keys.h" #include "ui/widgets/buttons.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/scroll_area.h" #include "ui/widgets/shadow.h" -#include "ui/widgets/box_content_divider.h" -#include "ui/wrap/slide_wrap.h" #include "ui/wrap/vertical_layout.h" +#include "ui/wrap/slide_wrap.h" #include "ui/ui_utility.h" -#include "data/data_channel.h" -#include "data/data_shared_media.h" #include "styles/style_info.h" -#include "styles/style_boxes.h" // AyuGram includes #include "ayu/ayu_settings.h" diff --git a/Telegram/SourceFiles/info/saved/info_saved_sublists_widget.cpp b/Telegram/SourceFiles/info/saved/info_saved_sublists_widget.cpp index 3a3ff46ad..9a070889a 100644 --- a/Telegram/SourceFiles/info/saved/info_saved_sublists_widget.cpp +++ b/Telegram/SourceFiles/info/saved/info_saved_sublists_widget.cpp @@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "lang/lang_keys.h" #include "ui/widgets/box_content_divider.h" +#include "ui/widgets/buttons.h" #include "ui/wrap/slide_wrap.h" #include "ui/wrap/vertical_layout.h" #include "ui/ui_utility.h" diff --git a/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp b/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp index c83795cab..d0ab12cb5 100644 --- a/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp +++ b/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp @@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/media/info_media_list_widget.h" #include "info/profile/info_profile_actions.h" #include "info/profile/info_profile_icon.h" +#include "info/profile/info_profile_values.h" #include "info/profile/info_profile_widget.h" #include "info/stories/info_stories_widget.h" #include "info/info_controller.h" diff --git a/Telegram/SourceFiles/iv/iv_controller.cpp b/Telegram/SourceFiles/iv/iv_controller.cpp index d262b0d75..de1b77ed7 100644 --- a/Telegram/SourceFiles/iv/iv_controller.cpp +++ b/Telegram/SourceFiles/iv/iv_controller.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "iv/iv_controller.h" +#include "base/event_filter.h" #include "base/platform/base_platform_info.h" #include "base/qt/qt_key_modifiers.h" #include "base/invoke_queued.h" @@ -688,18 +689,31 @@ void Controller::createWebview(const Webview::StorageId &storageId) { if (event->key() == Qt::Key_Escape) { escape(); } + } + }, window->lifetime()); + + base::install_event_filter(window, qApp, [=](not_null e) { + if (e->type() == QEvent::ShortcutOverride) { + if (!window->isActiveWindow()) { + return base::EventFilterResult::Continue; + } + const auto event = static_cast(e.get()); if (event->modifiers() & Qt::ControlModifier) { if (event->key() == Qt::Key_Plus || event->key() == Qt::Key_Equal) { _delegate->ivSetZoom(_delegate->ivZoom() + kZoomStep); + return base::EventFilterResult::Cancel; } else if (event->key() == Qt::Key_Minus) { _delegate->ivSetZoom(_delegate->ivZoom() - kZoomStep); + return base::EventFilterResult::Cancel; } else if (event->key() == Qt::Key_0) { _delegate->ivSetZoom(kDefaultZoom); + return base::EventFilterResult::Cancel; } } } - }, window->lifetime()); + return base::EventFilterResult::Continue; + }); const auto widget = raw->widget(); if (!widget) { diff --git a/Telegram/SourceFiles/iv/iv_data.cpp b/Telegram/SourceFiles/iv/iv_data.cpp index 16f9c0d51..8d424c918 100644 --- a/Telegram/SourceFiles/iv/iv_data.cpp +++ b/Telegram/SourceFiles/iv/iv_data.cpp @@ -102,7 +102,11 @@ QString SiteNameFromUrl(const QString &url) { } bool ShowButton() { - static const auto Supported = Webview::NavigateToDataSupported(); + static const auto Supported = [&] { + const auto availability = Webview::Availability(); + return availability.customSchemeRequests + && availability.customRangeRequests; + }(); return Supported; } diff --git a/Telegram/SourceFiles/main/main_session.cpp b/Telegram/SourceFiles/main/main_session.cpp index 1422da815..66a573f53 100644 --- a/Telegram/SourceFiles/main/main_session.cpp +++ b/Telegram/SourceFiles/main/main_session.cpp @@ -161,6 +161,7 @@ Session::Session( , _credits(std::make_unique(this)) , _cachedReactionIconFactory(std::make_unique()) , _supportHelper(Support::Helper::Create(this)) +, _fastButtonsBots(std::make_unique(this)) , _saveSettingsTimer([=] { saveSettings(); }) { Expects(_settings != nullptr); @@ -487,6 +488,10 @@ Support::Templates& Session::supportTemplates() const { return supportHelper().templates(); } +Support::FastButtonsBots &Session::fastButtonsBots() const { + return *_fastButtonsBots; +} + void Session::addWindow(not_null controller) { _windows.emplace(controller); controller->lifetime().add([=] { diff --git a/Telegram/SourceFiles/main/main_session.h b/Telegram/SourceFiles/main/main_session.h index 0a6917f38..677405d28 100644 --- a/Telegram/SourceFiles/main/main_session.h +++ b/Telegram/SourceFiles/main/main_session.h @@ -26,6 +26,7 @@ struct ConfigFields; namespace Support { class Helper; class Templates; +class FastButtonsBots; } // namespace Support namespace Data { @@ -233,6 +234,7 @@ public: [[nodiscard]] bool supportMode() const; [[nodiscard]] Support::Helper &supportHelper() const; [[nodiscard]] Support::Templates &supportTemplates() const; + [[nodiscard]] Support::FastButtonsBots &fastButtonsBots() const; [[nodiscard]] auto colorIndicesValue() -> rpl::producer; @@ -275,6 +277,7 @@ private: const std::unique_ptr _cachedReactionIconFactory; const std::unique_ptr _supportHelper; + const std::unique_ptr _fastButtonsBots; std::shared_ptr _selfUserpicView; rpl::variable _premiumPossible = false; diff --git a/Telegram/SourceFiles/main/main_session_settings.cpp b/Telegram/SourceFiles/main/main_session_settings.cpp index 253116dea..7d13cde46 100644 --- a/Telegram/SourceFiles/main/main_session_settings.cpp +++ b/Telegram/SourceFiles/main/main_session_settings.cpp @@ -23,7 +23,6 @@ namespace { constexpr auto kLegacyCallsPeerToPeerNobody = 4; constexpr auto kVersionTag = -1; constexpr auto kVersion = 2; -constexpr auto kMaxSavedPlaybackPositions = 16; } // namespace @@ -38,10 +37,7 @@ QByteArray SessionSettings::serialize() const { + _groupStickersSectionHidden.size() * sizeof(quint64) + sizeof(qint32) * 4 + Serialize::bytearraySize(autoDownload) - + sizeof(qint32) * 5 - + _mediaLastPlaybackPosition.size() * 2 * sizeof(quint64) - + sizeof(qint32) * 5 - + sizeof(qint32) + + sizeof(qint32) * 11 + (_mutePeriods.size() * sizeof(quint64)) + sizeof(qint32) * 2 + _hiddenPinnedMessages.size() * (sizeof(quint64) * 3) @@ -71,11 +67,7 @@ QByteArray SessionSettings::serialize() const { << qint32(_archiveCollapsed.current() ? 1 : 0) << qint32(_archiveInMainMenu.current() ? 1 : 0) << qint32(_skipArchiveInSearch.current() ? 1 : 0) - << qint32(_mediaLastPlaybackPosition.size()); - for (const auto &[id, time] : _mediaLastPlaybackPosition) { - stream << quint64(id) << qint64(time); - } - stream + << qint32(0) // old _mediaLastPlaybackPosition.size()); << qint32(0) // very old _hiddenPinnedMessages.size()); << qint32(_dialogsFiltersEnabled ? 1 : 0) << qint32(_supportAllSilent ? 1 : 0) @@ -156,7 +148,6 @@ void SessionSettings::addFromSerialized(const QByteArray &serialized) { qint32 appSuggestEmoji = app.suggestEmoji() ? 1 : 0; qint32 appSuggestStickersByEmoji = app.suggestStickersByEmoji() ? 1 : 0; qint32 appSpellcheckerEnabled = app.spellcheckerEnabled() ? 1 : 0; - std::vector> mediaLastPlaybackPosition; qint32 appVideoPlaybackSpeed = app.videoPlaybackSpeedSerialized(); QByteArray appVideoPipGeometry = app.videoPipGeometry(); std::vector appDictionariesEnabled; @@ -313,7 +304,7 @@ void SessionSettings::addFromSerialized(const QByteArray &serialized) { "Bad data for SessionSettings::addFromSerialized()")); return; } - mediaLastPlaybackPosition.emplace_back(documentId, time); + // Old mediaLastPlaybackPosition. } } } @@ -486,7 +477,6 @@ void SessionSettings::addFromSerialized(const QByteArray &serialized) { _archiveCollapsed = (archiveCollapsed == 1); _archiveInMainMenu = (archiveInMainMenu == 1); _skipArchiveInSearch = (skipArchiveInSearch == 1); - _mediaLastPlaybackPosition = std::move(mediaLastPlaybackPosition); _hiddenPinnedMessages = std::move(hiddenPinnedMessages); _dialogsFiltersEnabled = (dialogsFiltersEnabled == 1); _supportAllSilent = (supportAllSilent == 1); @@ -567,34 +557,6 @@ rpl::producer SessionSettings::supportAllSearchResultsValue() const { return _supportAllSearchResults.value(); } -void SessionSettings::setMediaLastPlaybackPosition(DocumentId id, crl::time time) { - auto &map = _mediaLastPlaybackPosition; - const auto i = ranges::find( - map, - id, - &std::pair::first); - if (i != map.end()) { - if (time > 0) { - i->second = time; - } else { - map.erase(i); - } - } else if (time > 0) { - if (map.size() >= kMaxSavedPlaybackPositions) { - map.erase(map.begin()); - } - map.emplace_back(id, time); - } -} - -crl::time SessionSettings::mediaLastPlaybackPosition(DocumentId id) const { - const auto i = ranges::find( - _mediaLastPlaybackPosition, - id, - &std::pair::first); - return (i != _mediaLastPlaybackPosition.end()) ? i->second : 0; -} - void SessionSettings::setArchiveCollapsed(bool collapsed) { _archiveCollapsed = collapsed; } diff --git a/Telegram/SourceFiles/main/main_session_settings.h b/Telegram/SourceFiles/main/main_session_settings.h index c44b6f785..c171968e2 100644 --- a/Telegram/SourceFiles/main/main_session_settings.h +++ b/Telegram/SourceFiles/main/main_session_settings.h @@ -85,9 +85,6 @@ public: _groupEmojiSectionHidden.remove(peerId); } - void setMediaLastPlaybackPosition(DocumentId id, crl::time time); - [[nodiscard]] crl::time mediaLastPlaybackPosition(DocumentId id) const; - [[nodiscard]] Data::AutoDownload::Full &autoDownload() { return _autoDownload; } @@ -166,7 +163,6 @@ private: rpl::variable _archiveCollapsed = false; rpl::variable _archiveInMainMenu = false; rpl::variable _skipArchiveInSearch = false; - std::vector> _mediaLastPlaybackPosition; base::flat_map _hiddenPinnedMessages; bool _dialogsFiltersEnabled = false; int _photoEditorHintShowsCount = 0; diff --git a/Telegram/SourceFiles/main/session/send_as_peers.cpp b/Telegram/SourceFiles/main/session/send_as_peers.cpp index 050c87f6e..e8bdaf0a2 100644 --- a/Telegram/SourceFiles/main/session/send_as_peers.cpp +++ b/Telegram/SourceFiles/main/session/send_as_peers.cpp @@ -23,7 +23,8 @@ constexpr auto kRequestEach = 30 * crl::time(1000); SendAsPeers::SendAsPeers(not_null session) : _session(session) -, _onlyMe({ { .peer = session->user(), .premiumRequired = false } }) { +, _onlyMe({ { .peer = session->user(), .premiumRequired = false } }) +, _onlyMePaid({ session->user() }) { _session->changes().peerUpdates( Data::PeerUpdate::Flag::Rights ) | rpl::map([=](const Data::PeerUpdate &update) { @@ -65,6 +66,7 @@ void SendAsPeers::refresh(not_null peer, bool force) { } _lastRequestTime[peer] = now; request(peer); + request(peer, true); } const std::vector &SendAsPeers::list( @@ -73,6 +75,12 @@ const std::vector &SendAsPeers::list( return (i != end(_lists)) ? i->second : _onlyMe; } +const std::vector> &SendAsPeers::paidReactionList( + not_null peer) const { + const auto i = _paidReactionLists.find(peer); + return (i != end(_paidReactionLists)) ? i->second : _onlyMePaid; +} + rpl::producer> SendAsPeers::updated() const { return _updates.events(); } @@ -133,8 +141,10 @@ not_null SendAsPeers::ResolveChosen( : fallback; } -void SendAsPeers::request(not_null peer) { +void SendAsPeers::request(not_null peer, bool forPaidReactions) { + using Flag = MTPchannels_GetSendAs::Flag; peer->session().api().request(MTPchannels_GetSendAs( + MTP_flags(forPaidReactions ? Flag::f_for_paid_reactions : Flag()), peer->input )).done([=](const MTPchannels_SendAsPeers &result) { auto parsed = std::vector(); @@ -155,15 +165,26 @@ void SendAsPeers::request(not_null peer) { } } }); - if (parsed.size() > 1) { - auto &now = _lists[peer]; - if (now != parsed) { - now = std::move(parsed); + if (forPaidReactions) { + auto peers = parsed | ranges::views::transform( + &SendAsPeer::peer + ) | ranges::to_vector; + if (!peers.empty()) { + _paidReactionLists[peer] = std::move(peers); + } else { + _paidReactionLists.remove(peer); + } + } else { + if (parsed.size() > 1) { + auto &now = _lists[peer]; + if (now != parsed) { + now = std::move(parsed); + _updates.fire_copy(peer); + } + } else if (const auto i = _lists.find(peer); i != end(_lists)) { + _lists.erase(i); _updates.fire_copy(peer); } - } else if (const auto i = _lists.find(peer); i != end(_lists)) { - _lists.erase(i); - _updates.fire_copy(peer); } }).send(); } diff --git a/Telegram/SourceFiles/main/session/send_as_peers.h b/Telegram/SourceFiles/main/session/send_as_peers.h index b047ca3ce..9f831955e 100644 --- a/Telegram/SourceFiles/main/session/send_as_peers.h +++ b/Telegram/SourceFiles/main/session/send_as_peers.h @@ -34,6 +34,9 @@ public: void setChosen(not_null peer, PeerId chosenId); [[nodiscard]] PeerId chosen(not_null peer) const; + [[nodiscard]] const std::vector> &paidReactionList( + not_null peer) const; + // If !list(peer).empty() then the result will be from that list. [[nodiscard]] not_null resolveChosen( not_null peer) const; @@ -44,14 +47,18 @@ public: PeerId chosen); private: - void request(not_null peer); + void request(not_null peer, bool forPaidReactions = false); const not_null _session; const std::vector _onlyMe; + const std::vector> _onlyMePaid; base::flat_map, std::vector> _lists; base::flat_map, crl::time> _lastRequestTime; base::flat_map, PeerId> _chosen; + base::flat_map< + not_null, + std::vector>> _paidReactionLists; rpl::event_stream> _updates; diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index 1af7432da..f4ba6983c 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -1449,11 +1449,7 @@ void MainWidget::showHistory( && way != Way::Forward) { ClearBotStartToken(_history->peer()); } - _history->showHistory( - peerId, - showAtMsgId, - params.highlightPart, - params.highlightPartOffsetHint); + _history->showHistory(peerId, showAtMsgId, params); if (alreadyThatPeer && params.reapplyLocalDraft) { _history->applyDraft(HistoryWidget::FieldHistoryAction::NewEntry); } diff --git a/Telegram/SourceFiles/media/player/media_player_instance.cpp b/Telegram/SourceFiles/media/player/media_player_instance.cpp index 12792f370..de1932b7a 100644 --- a/Telegram/SourceFiles/media/player/media_player_instance.cpp +++ b/Telegram/SourceFiles/media/player/media_player_instance.cpp @@ -34,6 +34,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "main/main_account.h" // session->account().sessionChanges(). #include "main/main_session_settings.h" +#include "storage/storage_account.h" namespace Media { namespace Player { @@ -50,7 +51,8 @@ constexpr auto kIdsPreloadAfter = 28; constexpr auto kShufflePlaylistLimit = 10'000; constexpr auto kRememberShuffledOrderItems = 16; -constexpr auto kMinLengthForSavePosition = 20 * TimeId(60); // 20 minutes. +constexpr auto kMinLengthForSavePositionVideo = TimeId(60); // 1 minute. +constexpr auto kMinLengthForSavePositionMusic = 20 * TimeId(60); // 20. base::options::toggle OptionDisableAutoplayNext({ .id = kOptionDisableAutoplayNext, @@ -108,18 +110,20 @@ void finish(not_null instance) { void SaveLastPlaybackPosition( not_null document, const TrackState &state) { + const auto limit = document->isVideoFile() + ? kMinLengthForSavePositionVideo + : kMinLengthForSavePositionMusic; const auto time = (state.position == kTimeUnknown || state.length == kTimeUnknown || state.state == State::PausedAtEnd || IsStopped(state.state)) ? TimeId(0) - : (state.length >= kMinLengthForSavePosition * state.frequency) + : (state.length >= limit * state.frequency) ? (state.position / state.frequency) * crl::time(1000) : TimeId(0); auto &session = document->session(); - if (session.settings().mediaLastPlaybackPosition(document->id) != time) { - session.settings().setMediaLastPlaybackPosition(document->id, time); - session.saveSettingsDelayed(); + if (session.local().mediaLastPlaybackPosition(document->id) != time) { + session.local().setMediaLastPlaybackPosition(document->id, time); } } @@ -849,9 +853,9 @@ Streaming::PlaybackOptions Instance::streamingOptions( if (position >= 0) { result.position = position; } else if (document) { - auto &settings = document->session().settings(); - result.position = settings.mediaLastPlaybackPosition(document->id); - settings.setMediaLastPlaybackPosition(document->id, 0); + auto &local = document->session().local(); + result.position = local.mediaLastPlaybackPosition(document->id); + local.setMediaLastPlaybackPosition(document->id, 0); } else { result.position = 0; } diff --git a/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp b/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp index 2ac9d29f3..e65443b42 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp @@ -576,9 +576,10 @@ void WeatherView::setAreaGeometry(QRect geometry, float64 radius) { const auto diagxdiag = (geometry.width() * geometry.width()) + (geometry.height() * geometry.height()); const auto diag = std::sqrt(diagxdiag); + const auto shift = diag * 2 / 3.; const auto topleft = QRectF(geometry).center() - - QPointF(diag / 2., diag / 2.); - const auto bottomright = topleft + QPointF(diag, diag); + - QPointF(shift, shift); + const auto bottomright = topleft + QPointF(shift, shift) * 2; const auto left = int(std::floor(topleft.x())); const auto top = int(std::floor(topleft.y())); const auto right = int(std::ceil(bottomright.x())); @@ -701,7 +702,7 @@ void WeatherView::cacheBackground() { p.translate(-center); const auto format = [](float64 value) { - return QString::number(int(base::SafeRound(value * 10)) / 10.); + return QString::number(int(base::SafeRound(value))); }; const auto text = [&] { const auto celsius = _data.millicelsius / 1000.; @@ -712,11 +713,11 @@ void WeatherView::cacheBackground() { return format(fahrenheit); }().append(QChar(0xb0)).append(_celsius ? "C" : "F"); const auto metrics = QFontMetrics(_font); - const auto textWidth = metrics.horizontalAdvance(text); - _padding = int(_rect.height() / 6); - const auto fullWidth = (_emoji ? _emojiSize : 0) + const auto textWidth = qCeil(metrics.horizontalAdvance(text)); + _padding = int(_rect.height() / 5); + const auto fullWidth = (_emoji ? (_emojiSize - _padding) : 0) + textWidth - + (2 * _padding); + + (4 * _padding); const auto left = _rect.x() + (_rect.width() - fullWidth) / 2; _wrapped = QRect(left, _rect.y(), fullWidth, _rect.height()); @@ -725,7 +726,7 @@ void WeatherView::cacheBackground() { p.setPen(_fg); p.setFont(_font); p.drawText(_wrapped.marginsRemoved( - { _padding + (_emoji ? _emojiSize : 0), 0, _padding, 0 }), + { 2 * _padding + (_emoji ? (_emojiSize - _padding) : 0), 0, 2 * _padding, 0 }), text, style::al_center); } diff --git a/Telegram/SourceFiles/media/stories/media_stories_share.cpp b/Telegram/SourceFiles/media/stories/media_stories_share.cpp index 02b379ed0..67b1dc925 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_share.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_share.cpp @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/share_box.h" #include "chat_helpers/compose/compose_show.h" #include "data/business/data_shortcut_messages.h" +#include "data/data_channel.h" #include "data/data_chat_participant_status.h" #include "data/data_forum_topic.h" #include "data/data_histories.h" @@ -181,4 +182,83 @@ namespace Media::Stories { }); } +QString FormatShareAtTime(TimeId seconds) { + const auto minutes = seconds / 60; + const auto h = minutes / 60; + const auto m = minutes % 60; + const auto s = seconds % 60; + const auto zero = QChar('0'); + return h + ? u"%1:%2:%3"_q.arg(h).arg(m, 2, 10, zero).arg(s, 2, 10, zero) + : u"%1:%2"_q.arg(m).arg(s, 2, 10, zero); +} + +object_ptr PrepareShareAtTimeBox( + std::shared_ptr show, + not_null item, + TimeId videoTimestamp) { + const auto id = item->fullId(); + const auto history = item->history(); + const auto owner = &history->owner(); + const auto session = &history->session(); + const auto canCopyLink = item->hasDirectLink() + && history->peer->isBroadcast() + && history->peer->asBroadcast()->hasUsername(); + const auto hasCaptions = item->media() + && !item->originalText().text.isEmpty() + && item->media()->allowsEditCaption(); + const auto hasOnlyForcedForwardedInfo = !hasCaptions + && item->media() + && item->media()->forceForwardedInfo(); + + auto copyCallback = [=] { + const auto item = owner->message(id); + if (!item) { + return; + } + CopyPostLink( + show, + item->fullId(), + HistoryView::Context::History, + videoTimestamp); + }; + + const auto requiredRight = item->requiredSendRight(); + const auto requiresInline = item->requiresSendInlineRight(); + auto filterCallback = [=](not_null thread) { + if (const auto user = thread->peer()->asUser()) { + if (user->canSendIgnoreRequirePremium()) { + return true; + } + } + return Data::CanSend(thread, requiredRight) + && (!requiresInline + || Data::CanSend(thread, ChatRestriction::SendInline)); + }; + auto copyLinkCallback = canCopyLink + ? Fn(std::move(copyCallback)) + : Fn(); + const auto st = ::Settings::DarkCreditsEntryBoxStyle(); + return Box(ShareBox::Descriptor{ + .session = session, + .copyCallback = std::move(copyLinkCallback), + .submitCallback = ShareBox::DefaultForwardCallback( + show, + history, + { id }, + videoTimestamp), + .filterCallback = std::move(filterCallback), + .titleOverride = tr::lng_share_at_time_title( + lt_time, + rpl::single(FormatShareAtTime(videoTimestamp))), + .st = st.shareBox ? *st.shareBox : ShareBoxStyleOverrides(), + .forwardOptions = { + .sendersCount = ItemsForwardSendersCount({ item }), + .captionsCount = ItemsForwardCaptionsCount({ item }), + .show = !hasOnlyForcedForwardedInfo, + }, + .premiumRequiredError = SharePremiumRequiredError(), + }); +} + } // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/stories/media_stories_share.h b/Telegram/SourceFiles/media/stories/media_stories_share.h index efb633930..c7149b0ca 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_share.h +++ b/Telegram/SourceFiles/media/stories/media_stories_share.h @@ -24,4 +24,11 @@ namespace Media::Stories { FullStoryId id, bool viewerStyle = false); +[[nodiscard]] QString FormatShareAtTime(TimeId seconds); + +[[nodiscard]] object_ptr PrepareShareAtTimeBox( + std::shared_ptr show, + not_null item, + TimeId videoTimestamp); + } // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/view/media_view_open_common.cpp b/Telegram/SourceFiles/media/view/media_view_open_common.cpp new file mode 100644 index 000000000..346a87ef3 --- /dev/null +++ b/Telegram/SourceFiles/media/view/media_view_open_common.cpp @@ -0,0 +1,28 @@ +/* +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 "media/view/media_view_open_common.h" + +#include "history/history_item.h" +#include "data/data_media_types.h" +#include "data/data_web_page.h" + +namespace Media::View { + +TimeId ExtractVideoTimestamp(not_null item) { + const auto media = item->media(); + if (!media) { + return 0; + } else if (const auto timestamp = media->videoTimestamp()) { + return timestamp; + } else if (const auto webpage = media->webpage()) { + return webpage->extractVideoTimestamp(); + } + return 0; +} + +} // namespace Media::View diff --git a/Telegram/SourceFiles/media/view/media_view_open_common.h b/Telegram/SourceFiles/media/view/media_view_open_common.h index ff56af25d..76cfd7748 100644 --- a/Telegram/SourceFiles/media/view/media_view_open_common.h +++ b/Telegram/SourceFiles/media/view/media_view_open_common.h @@ -135,4 +135,6 @@ private: }; +[[nodiscard]] TimeId ExtractVideoTimestamp(not_null item); + } // namespace Media::View diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index 9d039880c..bc35c68bd 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_attached_stickers.h" #include "api/api_peer_photo.h" #include "base/qt/qt_common_adapters.h" +#include "base/timer_rpl.h" #include "lang/lang_keys.h" #include "menu/menu_sponsored.h" #include "boxes/premium_preview_box.h" @@ -50,6 +51,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "media/view/media_view_pip.h" #include "media/view/media_view_overlay_raster.h" #include "media/view/media_view_overlay_opengl.h" +#include "media/stories/media_stories_share.h" #include "media/stories/media_stories_view.h" #include "media/streaming/media_streaming_document.h" #include "media/streaming/media_streaming_player.h" @@ -1655,7 +1657,9 @@ void OverlayWidget::fillContextMenuActions( if (!hasCopyMediaRestriction()) { if ((_document && documentContentShown()) || (_photo && _photoMedia->loaded())) { addAction( - tr::lng_mediaview_copy(tr::now), + ((_document && _streamed) + ? tr::lng_mediaview_copy_frame(tr::now) + : tr::lng_mediaview_copy(tr::now)), [=] { copyMedia(); }, &st::mediaMenuIconCopy); } @@ -1672,6 +1676,31 @@ void OverlayWidget::fillContextMenuActions( tr::lng_mediaview_forward(tr::now), [=] { forwardMedia(); }, &st::mediaMenuIconForward); + if (canShareAtTime()) { + const auto now = [=] { + return tr::lng_mediaview_share_at_time( + tr::now, + lt_time, + Stories::FormatShareAtTime(shareAtVideoTimestamp())); + }; + const auto action = addAction( + now(), + [=] { shareAtTime(); }, + &st::mediaMenuIconShare); + struct State { + rpl::variable text; + rpl::lifetime lifetime; + }; + const auto state = Ui::CreateChild(action); + state->text = rpl::single( + rpl::empty + ) | rpl::then( + base::timer_each(120) + ) | rpl::map(now); + state->text.changes() | rpl::start_with_next([=](QString value) { + action->setText(value); + }, state->lifetime); + } } if (story && story->canShare()) { addAction(tr::lng_mediaview_forward(tr::now), [=] { @@ -2308,11 +2337,22 @@ void OverlayWidget::assignMediaPointer(DocumentData *document) { _quality = Core::App().settings().videoQuality(); _chosenQuality = _document->chooseQuality(_message, _quality); _documentMedia = _document->createMediaView(); - _documentMedia->goodThumbnailWanted(); - _documentMedia->thumbnailWanted(fileOrigin()); + _videoCover = LookupVideoCover(_document, _message); + if (_videoCover) { + _videoCoverMedia = _videoCover->createMediaView(); + _videoCoverMedia->wanted( + Data::PhotoSize::Large, + fileOrigin()); + } else { + _videoCoverMedia = nullptr; + _documentMedia->goodThumbnailWanted(); + _documentMedia->thumbnailWanted(fileOrigin()); + } } else { _chosenQuality = nullptr; _documentMedia = nullptr; + _videoCover = nullptr; + _videoCoverMedia = nullptr; } _documentLoadingTo = QString(); } @@ -2326,6 +2366,8 @@ void OverlayWidget::assignMediaPointer(not_null photo) { _document = nullptr; _documentMedia = nullptr; _documentLoadingTo = QString(); + _videoCover = nullptr; + _videoCoverMedia = nullptr; if (_photo != photo) { _flip = {}; _photo = photo; @@ -2647,6 +2689,33 @@ void OverlayWidget::handleDocumentClick() { } } +bool OverlayWidget::canShareAtTime() const { + const auto media = _message ? _message->media() : nullptr; + return _document + && media + && _streamed + && (_document == media->document()) + && _document->isVideoFile() + && !media->webpage(); +} + +TimeId OverlayWidget::shareAtVideoTimestamp() const { + return _streamedPosition / crl::time(1000); +} + +void OverlayWidget::shareAtTime() { + if (!canShareAtTime()) { + return; + } + if (!_streamed->instance.player().paused() + && !_streamed->instance.player().finished()) { + playbackPauseResume(); + } + const auto show = uiShow(); + const auto timestamp = shareAtVideoTimestamp(); + show->show(Stories::PrepareShareAtTimeBox(show, _message, timestamp)); +} + void OverlayWidget::downloadMedia() { if (!_photo && !_document) { return; @@ -3947,13 +4016,19 @@ void OverlayWidget::initStreamingThumbnail() { } return thumbnail; }; - const auto good = _document + const auto good = _videoCover + ? _videoCoverMedia->image(Data::PhotoSize::Large) + : _document ? _documentMedia->goodThumbnail() : _photoMedia->image(Data::PhotoSize::Large); - const auto thumbnail = _document + const auto thumbnail = _videoCover + ? _videoCoverMedia->image(Data::PhotoSize::Small) + : _document ? _documentMedia->thumbnail() : computePhotoThumbnail(); - const auto blurred = _document + const auto blurred = _videoCover + ? _videoCoverMedia->thumbnailInline() + : _document ? _documentMedia->thumbnailInline() : _photoMedia->thumbnailInline(); const auto size = _photo diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h index 1a5064cfe..fb8efce90 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h @@ -291,6 +291,10 @@ private: void handleTouchTimer(); void handleDocumentClick(); + [[nodiscard]] bool canShareAtTime() const; + [[nodiscard]] TimeId shareAtVideoTimestamp() const; + void shareAtTime(); + void showSaveMsgToast(const QString &path, auto phrase); void showSaveMsgToastWith( const QString &path, @@ -557,10 +561,12 @@ private: PhotoData *_photo = nullptr; DocumentData *_document = nullptr; DocumentData *_chosenQuality = nullptr; + PhotoData *_videoCover = nullptr; Media::VideoQuality _quality; QString _documentLoadingTo; std::shared_ptr _photoMedia; std::shared_ptr _documentMedia; + std::shared_ptr _videoCoverMedia; base::flat_set> _preloadPhotos; base::flat_set> _preloadDocuments; int _rotation = 0; diff --git a/Telegram/SourceFiles/menu/menu_ttl_validator.cpp b/Telegram/SourceFiles/menu/menu_ttl_validator.cpp index e1640fddf..181c16786 100644 --- a/Telegram/SourceFiles/menu/menu_ttl_validator.cpp +++ b/Telegram/SourceFiles/menu/menu_ttl_validator.cpp @@ -112,7 +112,9 @@ bool TTLValidator::can() const { return (_peer->isUser() && !_peer->isSelf() && !_peer->isNotificationsUser() - && !_peer->asUser()->isInaccessible()) + && !_peer->asUser()->isInaccessible() + && (!_peer->asUser()->meRequiresPremiumToWrite() + || _peer->session().premium())) || (_peer->isChat() && _peer->asChat()->canEditInformation() && _peer->asChat()->amIn()) diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index e7c59e109..b2d27fd06 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -426,7 +426,7 @@ updateStarsBalance#4e80a379 balance:StarsAmount = Update; updateBusinessBotCallbackQuery#1ea2fda7 flags:# query_id:long user_id:long connection_id:string message:Message reply_to_message:flags.2?Message chat_instance:long data:flags.0?bytes = Update; updateStarsRevenueStatus#a584b019 peer:Peer status:StarsRevenueStatus = Update; updateBotPurchasedPaidMedia#283bd312 user_id:long payload:string qts:int = Update; -updatePaidReactionPrivacy#51ca7aec private:Bool = Update; +updatePaidReactionPrivacy#8b725fce private:PaidReactionPrivacy = Update; updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State; @@ -599,7 +599,7 @@ messages.affectedMessages#84d19185 pts:int pts_count:int = messages.AffectedMess webPageEmpty#211a1788 flags:# id:long url:flags.0?string = WebPage; webPagePending#b0d13e47 flags:# id:long url:flags.0?string date:int = WebPage; -webPage#e89c45b2 flags:# has_large_media:flags.13?true id:long url:string display_url:string hash:int type:flags.0?string site_name:flags.1?string title:flags.2?string description:flags.3?string photo:flags.4?Photo embed_url:flags.5?string embed_type:flags.5?string embed_width:flags.6?int embed_height:flags.6?int duration:flags.7?int author:flags.8?string document:flags.9?Document cached_page:flags.10?Page attributes:flags.12?Vector = WebPage; +webPage#e89c45b2 flags:# has_large_media:flags.13?true video_cover_photo:flags.14?true id:long url:string display_url:string hash:int type:flags.0?string site_name:flags.1?string title:flags.2?string description:flags.3?string photo:flags.4?Photo embed_url:flags.5?string embed_type:flags.5?string embed_width:flags.6?int embed_height:flags.6?int duration:flags.7?int author:flags.8?string document:flags.9?Document cached_page:flags.10?Page attributes:flags.12?Vector = WebPage; webPageNotModified#7311ca11 flags:# cached_page_views:flags.0?int = WebPage; authorization#ad01d61d flags:# current:flags.0?true official_app:flags.1?true password_pending:flags.2?true encrypted_requests_disabled:flags.3?true call_requests_disabled:flags.4?true unconfirmed:flags.5?true hash:long device_model:string platform:string system_version:string api_id:int app_name:string app_version:string date_created:int date_active:int ip:string country:string region:string = Authorization; @@ -1877,7 +1877,7 @@ starsGiveawayOption#94ce852a flags:# extended:flags.0?true default:flags.1?true starsGiveawayWinnersOption#54236209 flags:# default:flags.0?true users:int per_user_stars:long = StarsGiveawayWinnersOption; starGift#2cc73c8 flags:# limited:flags.0?true sold_out:flags.1?true birthday:flags.2?true id:long sticker:Document stars:long availability_remains:flags.0?int availability_total:flags.0?int convert_stars:long first_sale_date:flags.1?int last_sale_date:flags.1?int upgrade_stars:flags.3?long = StarGift; -starGiftUnique#f2fe7e4a flags:# id:long title:string slug:string num:int owner_id:flags.0?Peer owner_name:flags.1?string owner_address:flags.2?string attributes:Vector availability_issued:int availability_total:int = StarGift; +starGiftUnique#5c62d151 flags:# id:long title:string slug:string num:int owner_id:flags.0?Peer owner_name:flags.1?string owner_address:flags.2?string attributes:Vector availability_issued:int availability_total:int gift_address:flags.3?string = StarGift; payments.starGiftsNotModified#a388a368 = payments.StarGifts; payments.starGifts#901689ea hash:int gifts:Vector = payments.StarGifts; @@ -1934,6 +1934,10 @@ inputSavedStarGiftChat#f101aa7f peer:InputPeer saved_id:long = InputSavedStarGif payments.starGiftWithdrawalUrl#84aa3a9c url:string = payments.StarGiftWithdrawalUrl; +paidReactionPrivacyDefault#206ad49e = PaidReactionPrivacy; +paidReactionPrivacyAnonymous#1f0c1ad9 = PaidReactionPrivacy; +paidReactionPrivacyPeer#dc6cfcf0 peer:InputPeer = PaidReactionPrivacy; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -1946,6 +1950,7 @@ invokeWithTakeout#aca9fd2e {X:Type} takeout_id:long query:!X = X; invokeWithBusinessConnection#dd289f8e {X:Type} connection_id:string query:!X = X; invokeWithGooglePlayIntegrity#1df92984 {X:Type} nonce:string token:string query:!X = X; invokeWithApnsSecret#0dae54f8 {X:Type} nonce:string secret:string query:!X = X; +invokeWithReCaptcha#adbb0f94 {X:Type} token:string query:!X = X; auth.sendCode#a677244f phone_number:string api_id:int api_hash:string settings:CodeSettings = auth.SentCode; auth.signUp#aac7b717 flags:# no_joined_notifications:flags.0?true phone_number:string phone_code_hash:string first_name:string last_name:string = auth.Authorization; @@ -2331,8 +2336,8 @@ messages.editFactCheck#589ee75 peer:InputPeer msg_id:int text:TextWithEntities = messages.deleteFactCheck#d1da940c peer:InputPeer msg_id:int = Updates; messages.getFactCheck#b9cdc5ee peer:InputPeer msg_id:Vector = Vector; messages.requestMainWebView#c9e01e7b flags:# compact:flags.7?true fullscreen:flags.8?true peer:InputPeer bot:InputUser start_param:flags.1?string theme_params:flags.0?DataJSON platform:string = WebViewResult; -messages.sendPaidReaction#9dd6a67b flags:# peer:InputPeer msg_id:int count:int random_id:long private:flags.0?Bool = Updates; -messages.togglePaidReactionPrivacy#849ad397 peer:InputPeer msg_id:int private:Bool = Bool; +messages.sendPaidReaction#58bbcb50 flags:# peer:InputPeer msg_id:int count:int random_id:long private:flags.0?PaidReactionPrivacy = Updates; +messages.togglePaidReactionPrivacy#435885b5 peer:InputPeer msg_id:int private:PaidReactionPrivacy = Bool; messages.getPaidReactionPrivacy#472455aa = Updates; messages.viewSponsoredMessage#673ad8f1 peer:InputPeer random_id:bytes = Bool; messages.clickSponsoredMessage#f093465 flags:# media:flags.0?true fullscreen:flags.1?true peer:InputPeer random_id:bytes = Bool; @@ -2423,7 +2428,7 @@ channels.editLocation#58e63f6d channel:InputChannel geo_point:InputGeoPoint addr channels.toggleSlowMode#edd49ef0 channel:InputChannel seconds:int = Updates; channels.getInactiveChannels#11e831ee = messages.InactiveChats; channels.convertToGigagroup#b290c69 channel:InputChannel = Updates; -channels.getSendAs#dc770ee peer:InputPeer = channels.SendAsPeers; +channels.getSendAs#e785a43f flags:# for_paid_reactions:flags.0?true peer:InputPeer = channels.SendAsPeers; channels.deleteParticipantHistory#367544db channel:InputChannel participant:InputPeer = messages.AffectedHistory; channels.toggleJoinToSend#e4cb9580 channel:InputChannel enabled:Bool = Updates; channels.toggleJoinRequest#4c2985b6 channel:InputChannel enabled:Bool = Updates; @@ -2648,4 +2653,4 @@ smsjobs.finishJob#4f1ebf24 flags:# job_id:string error:flags.0?string = Bool; fragment.getCollectibleInfo#be1e85ba collectible:InputCollectible = fragment.CollectibleInfo; -// LAYER 198 +// LAYER 199 diff --git a/Telegram/SourceFiles/overview/overview_layout.cpp b/Telegram/SourceFiles/overview/overview_layout.cpp index 4357a17eb..fd8468a53 100644 --- a/Telegram/SourceFiles/overview/overview_layout.cpp +++ b/Telegram/SourceFiles/overview/overview_layout.cpp @@ -486,6 +486,7 @@ Video::Video( MediaOptions options) : RadialProgressItem(delegate, parent) , _data(video) +, _videoCover(LookupVideoCover(video, parent)) , _duration(Ui::FormatDurationText(_data->duration() / 1000)) , _spoiler(options.spoiler ? std::make_unique([=] { delegate->repaintItem(this); @@ -493,7 +494,13 @@ Video::Video( , _pinned(options.pinned) , _story(options.story) { setDocumentLinks(_data); - _data->loadThumbnail(parent->fullId()); + if (!_videoCover) { + _data->loadThumbnail(parent->fullId()); + } else if (_videoCover->inlineThumbnailBytes().isEmpty() + && (_videoCover->hasExact(Data::PhotoSize::Small) + || _videoCover->hasExact(Data::PhotoSize::Thumbnail))) { + _videoCover->load(Data::PhotoSize::Small, parent->fullId()); + } } Video::~Video() = default; @@ -516,9 +523,19 @@ void Video::paint(Painter &p, const QRect &clip, TextSelection selection, const ensureDataMediaCreated(); const auto selected = (selection == FullSelection); - const auto blurred = _dataMedia->thumbnailInline(); - const auto thumbnail = _spoiler ? nullptr : _dataMedia->thumbnail(); - const auto good = _spoiler ? nullptr : _dataMedia->goodThumbnail(); + const auto blurred = _videoCover + ? _videoCoverMedia->thumbnailInline() + : _dataMedia->thumbnailInline(); + const auto thumbnail = _spoiler + ? nullptr + : _videoCover + ? _videoCoverMedia->image(Data::PhotoSize::Small) + : _dataMedia->thumbnail(); + const auto good = _spoiler + ? nullptr + : _videoCover + ? _videoCoverMedia->image(Data::PhotoSize::Large) + : _dataMedia->goodThumbnail(); bool loaded = dataLoaded(), displayLoading = _data->displayLoading(); if (displayLoading) { @@ -626,12 +643,17 @@ void Video::paint(Painter &p, const QRect &clip, TextSelection selection, const } void Video::ensureDataMediaCreated() const { - if (_dataMedia) { + if (_dataMedia && (!_videoCover || _videoCoverMedia)) { return; } _dataMedia = _data->createMediaView(); - _dataMedia->goodThumbnailWanted(); - _dataMedia->thumbnailWanted(parent()->fullId()); + if (_videoCover) { + _videoCoverMedia = _videoCover->createMediaView(); + _videoCover->load(Data::PhotoSize::Large, parent()->fullId()); + } else { + _dataMedia->goodThumbnailWanted(); + _dataMedia->thumbnailWanted(parent()->fullId()); + } delegate()->registerHeavyItem(this); } diff --git a/Telegram/SourceFiles/overview/overview_layout.h b/Telegram/SourceFiles/overview/overview_layout.h index 936379e36..489e2666e 100644 --- a/Telegram/SourceFiles/overview/overview_layout.h +++ b/Telegram/SourceFiles/overview/overview_layout.h @@ -316,7 +316,9 @@ private: void updateStatusText(); const not_null _data; + PhotoData *_videoCover = nullptr; mutable std::shared_ptr _dataMedia; + mutable std::shared_ptr _videoCoverMedia; StatusText _status; QString _duration; diff --git a/Telegram/SourceFiles/payments/payments_reaction_process.cpp b/Telegram/SourceFiles/payments/payments_reaction_process.cpp index dd971ec4f..4018b2b8a 100644 --- a/Telegram/SourceFiles/payments/payments_reaction_process.cpp +++ b/Telegram/SourceFiles/payments/payments_reaction_process.cpp @@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item.h" #include "lang/lang_keys.h" #include "main/session/session_show.h" +#include "main/session/send_as_peers.h" #include "main/main_app_config.h" #include "main/main_session.h" #include "payments/ui/payments_reaction_box.h" @@ -45,7 +46,7 @@ void TryAddingPaidReaction( FullMsgId itemId, base::weak_ptr weakView, int count, - std::optional anonymous, + std::optional shownPeer, std::shared_ptr show, Fn finished) { const auto checkItem = [=] { @@ -66,7 +67,7 @@ void TryAddingPaidReaction( if (result == Settings::SmallBalanceResult::Success || result == Settings::SmallBalanceResult::Already) { if (const auto item = checkItem()) { - item->addPaidReaction(count, anonymous); + item->addPaidReaction(count, shownPeer); if (const auto view = count ? weakView.get() : nullptr) { const auto history = view->history(); history->owner().notifyViewPaidReactionSent(view); @@ -105,7 +106,7 @@ void TryAddingPaidReaction( not_null item, HistoryView::Element *view, int count, - std::optional anonymous, + std::optional shownPeer, std::shared_ptr show, Fn finished) { TryAddingPaidReaction( @@ -113,7 +114,7 @@ void TryAddingPaidReaction( item->fullId(), view, count, - anonymous, + shownPeer, std::move(show), std::move(finished)); } @@ -140,26 +141,26 @@ void ShowPaidReactionDetails( struct State { QPointer selectBox; - bool ignoreAnonymousSwitch = false; + bool ignoreShownPeerSwitch = false; bool sending = false; }; const auto state = std::make_shared(); session->credits().load(true); const auto weakView = base::make_weak(view); - const auto send = [=](int count, bool anonymous, auto resend) -> void { + const auto send = [=](int count, PeerId shownPeer, auto resend) -> void { Expects(count >= 0); const auto finish = [=](bool success) { state->sending = false; if (success && count > 0) { - state->ignoreAnonymousSwitch = true; + state->ignoreShownPeerSwitch = true; if (const auto strong = state->selectBox.data()) { strong->closeBox(); } } }; - if (state->sending || (!count && state->ignoreAnonymousSwitch)) { + if (state->sending || (!count && state->ignoreShownPeerSwitch)) { return; } else if (const auto item = session->data().message(itemId)) { state->sending = true; @@ -167,7 +168,7 @@ void ShowPaidReactionDetails( item, weakView.get(), count, - anonymous, + shownPeer, show, finish); } @@ -206,38 +207,49 @@ void ShowPaidReactionDetails( .photo = (peer ? Ui::MakeUserpicThumbnail(peer) : Ui::MakeHiddenAuthorThumbnail()), + .barePeerId = peer ? uint64(peer->id.value) : 0, .count = int(entry.count), .click = peer ? open : Fn(), .my = (entry.my == 1), }); }; - const auto topPaid = item->topPaidReactionsWithLocal(); - top.reserve(topPaid.size() + 2); - for (const auto &entry : topPaid) { - add(entry); - if (entry.my) { - auto copy = entry; - copy.peer = entry.peer ? nullptr : session->user().get(); - add(copy); - } - } - if (!ranges::contains(top, true, &Ui::PaidReactionTop::my)) { - auto entry = Data::MessageReactionsTopPaid{ - .peer = session->user(), - .count = 0, - .my = true, - }; - add(entry); - entry.peer = nullptr; - add(entry); - if (session->api().globalPrivacy().paidReactionAnonymousCurrent()) { - std::swap(top.front(), top.back()); - } - } - ranges::sort(top, ranges::greater(), &Ui::PaidReactionTop::count); - const auto linked = item->discussionPostOriginalSender(); const auto channel = (linked ? linked : item->history()->peer.get()); + const auto channels = session->sendAsPeers().paidReactionList(channel); + const auto topPaid = item->topPaidReactionsWithLocal(); + top.reserve(topPaid.size() + 2 + channels.size()); + for (const auto &entry : topPaid) { + add(entry); + } + auto myAdded = base::flat_set(); + const auto i = ranges::find(top, true, &Ui::PaidReactionTop::my); + if (i != end(top)) { + myAdded.emplace(i->barePeerId); + } + const auto myCount = uint32((i != end(top)) ? i->count : 0); + const auto myAdd = [&](PeerData *peer) { + const auto barePeerId = peer ? uint64(peer->id.value) : 0; + if (!myAdded.emplace(barePeerId).second) { + return; + } + add(Data::MessageReactionsTopPaid{ + .peer = peer, + .count = myCount, + .my = true, + }); + }; + const auto globalPrivacy = &session->api().globalPrivacy(); + const auto shown = globalPrivacy->paidReactionShownPeerCurrent(); + const auto owner = &session->data(); + const auto shownPeer = shown ? owner->peer(shown).get() : nullptr; + myAdd(shownPeer); + myAdd(session->user()); + myAdd(nullptr); + for (const auto &channel : channels) { + myAdd(channel); + } + ranges::stable_sort(top, ranges::greater(), &Ui::PaidReactionTop::count); + state->selectBox = show->show(Ui::MakePaidReactionBox({ .chosen = chosen, .max = max, @@ -245,8 +257,8 @@ void ShowPaidReactionDetails( .channel = channel->name(), .submit = std::move(submitText), .balanceValue = session->credits().balanceValue(), - .send = [=](int count, bool anonymous) { - send(count, anonymous, send); + .send = [=](int count, uint64 barePeerId) { + send(count, PeerId(barePeerId), send); }, })); diff --git a/Telegram/SourceFiles/payments/payments_reaction_process.h b/Telegram/SourceFiles/payments/payments_reaction_process.h index 1aa5d7b5e..571d50c64 100644 --- a/Telegram/SourceFiles/payments/payments_reaction_process.h +++ b/Telegram/SourceFiles/payments/payments_reaction_process.h @@ -31,7 +31,7 @@ void TryAddingPaidReaction( not_null item, HistoryView::Element *view, int count, - std::optional anonymous, + std::optional shownPeer, std::shared_ptr show, Fn finished = nullptr); diff --git a/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp b/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp index 399b18e19..7cc513f7b 100644 --- a/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp @@ -10,19 +10,23 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/qt/qt_compare.h" #include "lang/lang_keys.h" #include "ui/boxes/boost_box.h" // MakeBoostFeaturesBadge. +#include "ui/controls/who_reacted_context_action.h" #include "ui/effects/premium_bubble.h" #include "ui/layers/generic_box.h" #include "ui/text/text_utilities.h" #include "ui/widgets/buttons.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/continuous_sliders.h" +#include "ui/widgets/popup_menu.h" #include "ui/wrap/slide_wrap.h" #include "ui/dynamic_image.h" #include "ui/painter.h" #include "ui/vertical_list.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/style_credits.h" #include "styles/style_layers.h" +#include "styles/style_media_player.h" #include "styles/style_premium.h" #include "styles/style_settings.h" @@ -192,13 +196,41 @@ void PaidReactionSlider( return result; } +void AddArrowDown(not_null widget) { + const auto arrow = CreateChild(widget); + const auto icon = &st::paidReactChannelArrow; + const auto skip = st::lineWidth * 4; + const auto size = icon->width() + skip * 2; + arrow->resize(size, size); + widget->widthValue() | rpl::start_with_next([=](int width) { + const auto left = (width - st::paidReactTopUserpic) / 2; + arrow->moveToRight(left - skip, -st::lineWidth, width); + }, widget->lifetime()); + arrow->paintRequest() | rpl::start_with_next([=] { + Painter p(arrow); + auto hq = PainterHighQualityEnabler(p); + p.setBrush(st::activeButtonBg); + p.setPen(st::activeButtonFg); + const auto rect = arrow->rect(); + const auto line = st::lineWidth; + p.drawEllipse(rect.marginsRemoved({ line, line, line, line })); + icon->paint(p, skip, (size - icon->height()) / 2 + line, size); + }, widget->lifetime()); + arrow->setAttribute(Qt::WA_TransparentForMouseEvents); + arrow->show(); +} + [[nodiscard]] not_null MakeTopReactor( not_null parent, - const PaidReactionTop &data) { + const PaidReactionTop &data, + Fn selectShownPeer) { const auto result = CreateChild(parent); result->show(); if (data.click && !data.my) { result->setClickedCallback(data.click); + } else if (data.click && selectShownPeer) { + result->setClickedCallback(selectShownPeer); + AddArrowDown(result); } else { result->setAttribute(Qt::WA_TransparentForMouseEvents); } @@ -244,11 +276,60 @@ void PaidReactionSlider( return result; } +void SelectShownPeer( + std::shared_ptr> menu, + not_null parent, + const std::vector &mine, + uint64 selected, + Fn callback) { + if (*menu) { + (*menu)->hideMenu(); + } + (*menu) = CreateChild( + parent, + st::paidReactChannelMenu); + + struct Entry { + not_null action; + std::shared_ptr userpic; + }; + auto actions = std::make_shared>(); + actions->reserve(mine.size()); + for (const auto &entry : mine) { + auto action = base::make_unique_q( + (*menu)->menu(), + nullptr, + (*menu)->menu()->st(), + Ui::WhoReactedEntryData()); + const auto index = int(actions->size()); + actions->push_back({ action.get(), entry.photo->clone() }); + const auto id = entry.barePeerId; + const auto updateUserpic = [=] { + const auto size = st::defaultWhoRead.photoSize; + actions->at(index).action->setData({ + .text = entry.name, + .type = ((id == selected) + ? Ui::WhoReactedType::RefRecipientNow + : Ui::WhoReactedType::RefRecipient), + .userpic = actions->at(index).userpic->image(size), + .callback = [=] { callback(id); }, + }); + }; + actions->back().userpic->subscribeToUpdates(updateUserpic); + + (*menu)->addAction(std::move(action)); + updateUserpic(); + } + (*menu)->popup(QCursor::pos()); + +} + void FillTopReactors( not_null container, std::vector top, rpl::producer chosen, - rpl::producer anonymous) { + rpl::producer shownPeer, + Fn changeShownPeer) { container->add( MakeBoostFeaturesBadge( container, @@ -272,28 +353,33 @@ void FillTopReactors( bool chosenChanged = false; }; const auto state = wrap->lifetime().make_state(); + const auto menu = std::make_shared>(); rpl::combine( std::move(chosen), - std::move(anonymous) - ) | rpl::start_with_next([=](int chosen, bool anonymous) { + std::move(shownPeer) + ) | rpl::start_with_next([=](int chosen, uint64 barePeerId) { if (!state->initialChosen) { state->initialChosen = chosen; } else if (*state->initialChosen != chosen) { state->chosenChanged = true; } + auto mine = std::vector(); auto list = std::vector(); list.reserve(kMaxTopPaidShown + 1); for (const auto &entry : top) { if (!entry.my) { list.push_back(entry); - } else if (!entry.click == anonymous) { + } else if (entry.barePeerId == barePeerId) { auto copy = entry; if (state->chosenChanged) { copy.count += chosen; } list.push_back(copy); } + if (entry.my && entry.barePeerId) { + mine.push_back(entry); + } } ranges::stable_sort( list, @@ -303,6 +389,14 @@ void FillTopReactors( || (!list.empty() && !list.back().count)) { list.pop_back(); } + auto selectShownPeer = (mine.size() < 2) + ? Fn() + : [=] { SelectShownPeer( + menu, + parent, + mine, + barePeerId, + changeShownPeer); }; if (list.empty()) { wrap->hide(anim::type::normal); } else { @@ -319,7 +413,7 @@ void FillTopReactors( const auto i = state->cache.find(key); const auto widget = (i != end(state->cache)) ? i->second - : MakeTopReactor(parent, entry); + : MakeTopReactor(parent, entry, selectShownPeer); state->widgets.push_back(widget); widget->show(); } @@ -368,7 +462,8 @@ void PaidReactionsBox( struct State { rpl::variable chosen; - rpl::variable anonymous; + rpl::variable shownPeer; + uint64 savedShownPeer = 0; }; const auto state = box->lifetime().make_state(); @@ -377,12 +472,16 @@ void PaidReactionsBox( state->chosen = count; }; - const auto initialAnonymous = ranges::find( + const auto initialShownPeer = ranges::find( args.top, true, &PaidReactionTop::my - )->click == nullptr; - state->anonymous = initialAnonymous; + )->barePeerId; + state->shownPeer = initialShownPeer; + state->savedShownPeer = ranges::find_if(args.top, []( + const PaidReactionTop &entry) { + return entry.my && entry.barePeerId != 0; + })->barePeerId; const auto content = box->verticalLayout(); AddSkip(content, st::boxTitleClose.height + st::paidReactBubbleTop); @@ -455,25 +554,30 @@ void PaidReactionsBox( content, std::move(args.top), state->chosen.value(), - state->anonymous.value()); + state->shownPeer.value(), + [=](uint64 barePeerId) { + state->shownPeer = state->savedShownPeer = barePeerId; + }); const auto named = box->addRow(object_ptr>( box, object_ptr( box, tr::lng_paid_react_show_in_top(tr::now), - !state->anonymous.current()))); - state->anonymous = named->entity()->checkedValue( - ) | rpl::map(!rpl::mappers::_1); + state->shownPeer.current() != 0))); + named->entity()->checkedValue( + ) | rpl::start_with_next([=](bool show) { + state->shownPeer = show ? state->savedShownPeer : 0; + }, named->lifetime()); const auto button = box->addButton(rpl::single(QString()), [=] { - args.send(state->chosen.current(), !named->entity()->checked()); + args.send(state->chosen.current(), state->shownPeer.current()); }); box->boxClosing() | rpl::filter([=] { - return state->anonymous.current() != initialAnonymous; + return state->shownPeer.current() != initialShownPeer; }) | rpl::start_with_next([=] { - args.send(0, state->anonymous.current()); + args.send(0, state->shownPeer.current()); }, box->lifetime()); { diff --git a/Telegram/SourceFiles/payments/ui/payments_reaction_box.h b/Telegram/SourceFiles/payments/ui/payments_reaction_box.h index 61a2b084b..034b04289 100644 --- a/Telegram/SourceFiles/payments/ui/payments_reaction_box.h +++ b/Telegram/SourceFiles/payments/ui/payments_reaction_box.h @@ -23,6 +23,7 @@ struct TextWithContext { struct PaidReactionTop { QString name; std::shared_ptr photo; + uint64 barePeerId = 0; int count = 0; Fn click; bool my = false; @@ -37,7 +38,7 @@ struct PaidReactionBoxArgs { QString channel; Fn(rpl::producer amount)> submit; rpl::producer balanceValue; - Fn send; + Fn send; }; void PaidReactionsBox( diff --git a/Telegram/SourceFiles/platform/linux/current_geo_location_linux.cpp b/Telegram/SourceFiles/platform/linux/current_geo_location_linux.cpp index f87c343d3..7015af739 100644 --- a/Telegram/SourceFiles/platform/linux/current_geo_location_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/current_geo_location_linux.cpp @@ -8,17 +8,192 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "platform/linux/current_geo_location_linux.h" #include "core/current_geo_location.h" +#include "base/platform/linux/base_linux_library.h" + +#include namespace Platform { +namespace { + +typedef struct _GClueSimple GClueSimple; +typedef struct _GClueLocation GClueLocation; +typedef struct _GeocodeLocation GeocodeLocation; +typedef struct _GeocodeReverse GeocodeReverse; +typedef struct _GeocodePlace GeocodePlace; + +typedef enum { + GCLUE_ACCURACY_LEVEL_NONE = 0, + GCLUE_ACCURACY_LEVEL_COUNTRY = 1, + GCLUE_ACCURACY_LEVEL_CITY = 4, + GCLUE_ACCURACY_LEVEL_NEIGHBORHOOD = 5, + GCLUE_ACCURACY_LEVEL_STREET = 6, + GCLUE_ACCURACY_LEVEL_EXACT = 8, +} GClueAccuracyLevel; + +void (*gclue_simple_new)( + const char *desktop_id, + GClueAccuracyLevel accuracy_level, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + +GClueSimple *(*gclue_simple_new_finish)(GAsyncResult *result, GError **error); +GClueLocation *(*gclue_simple_get_location)(GClueSimple *simple); + +gdouble (*gclue_location_get_latitude)(GClueLocation *loc); +gdouble (*gclue_location_get_longitude)(GClueLocation *loc); + +GeocodeLocation *(*geocode_location_new)( + gdouble latitude, + gdouble longitude, + gdouble accuracy); + +GeocodeReverse *(*geocode_reverse_new_for_location)( + GeocodeLocation *location); + +void (*geocode_reverse_resolve_async)( + GeocodeReverse *object, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + +GeocodePlace *(*geocode_reverse_resolve_finish)( + GeocodeReverse *object, + GAsyncResult *res, + GError **error); + +const char *(*geocode_place_get_street_address)(GeocodePlace *place); +const char *(*geocode_place_get_town)(GeocodePlace *place); +const char *(*geocode_place_get_country)(GeocodePlace *place); + +} // namespace void ResolveCurrentExactLocation(Fn callback) { - callback({}); + static const auto Inited = [] { + const auto lib = base::Platform::LoadLibrary( + "libgeoclue-2.so.0", + RTLD_NODELETE); + return lib + && LOAD_LIBRARY_SYMBOL(lib, gclue_simple_new) + && LOAD_LIBRARY_SYMBOL(lib, gclue_simple_new_finish) + && LOAD_LIBRARY_SYMBOL(lib, gclue_simple_get_location) + && LOAD_LIBRARY_SYMBOL(lib, gclue_location_get_latitude) + && LOAD_LIBRARY_SYMBOL(lib, gclue_location_get_longitude); + }(); + + if (!Inited) { + callback({}); + return; + } + + gclue_simple_new( + QGuiApplication::desktopFileName().toUtf8().constData(), + GCLUE_ACCURACY_LEVEL_EXACT, + nullptr, + GAsyncReadyCallback(+[]( + GObject *object, + GAsyncResult* res, + Fn *callback) { + const auto callbackGuard = gsl::finally([&] { + delete callback; + }); + + const auto simple = gclue_simple_new_finish(res, nullptr); + if (!simple) { + (*callback)({}); + return; + } + + const auto simpleGuard = gsl::finally([&] { + g_object_unref(simple); + }); + + const auto location = gclue_simple_get_location(simple); + + (*callback)({ + .point = { + gclue_location_get_latitude(location), + gclue_location_get_longitude(location), + }, + .accuracy = Core::GeoLocationAccuracy::Exact, + }); + }), + new Fn(callback)); } + void ResolveLocationAddress( const Core::GeoLocation &location, const QString &language, Fn callback) { - callback({}); + static const auto Inited = [] { + const auto lib = base::Platform::LoadLibrary( + "libgeocode-glib-2.so.0", + RTLD_NODELETE) ?: base::Platform::LoadLibrary( + "libgeocode-glib.so.0", + RTLD_NODELETE); + return lib + && LOAD_LIBRARY_SYMBOL(lib, geocode_location_new) + && LOAD_LIBRARY_SYMBOL(lib, geocode_reverse_new_for_location) + && LOAD_LIBRARY_SYMBOL(lib, geocode_reverse_resolve_async) + && LOAD_LIBRARY_SYMBOL(lib, geocode_reverse_resolve_finish) + && LOAD_LIBRARY_SYMBOL(lib, geocode_place_get_street_address) + && LOAD_LIBRARY_SYMBOL(lib, geocode_place_get_town) + && LOAD_LIBRARY_SYMBOL(lib, geocode_place_get_country); + }(); + + if (!Inited) { + callback({}); + return; + } + + geocode_reverse_resolve_async( + geocode_reverse_new_for_location(geocode_location_new( + location.point.x(), + location.point.y(), + -1)), + nullptr, + GAsyncReadyCallback(+[]( + GeocodeReverse *reverse, + GAsyncResult* res, + Fn *callback) { + const auto argsGuard = gsl::finally([&] { + delete callback; + g_object_unref(reverse); + }); + + const auto place = geocode_reverse_resolve_finish( + reverse, + res, + nullptr); + + if (!place) { + (*callback)({}); + return; + } + + const auto placeGuard = gsl::finally([&] { + g_object_unref(place); + }); + + const auto values = { + geocode_place_get_street_address(place), + geocode_place_get_town(place), + geocode_place_get_country(place), + }; + + QStringList checked; + for (const auto &value : values) { + if (value) { + const auto qt = QString::fromUtf8(value); + if (!qt.isEmpty()) { + checked.push_back(qt); + } + } + } + + (*callback)({ .name = checked.join(u", "_q) }); + }), + new Fn(callback)); } } // namespace Platform diff --git a/Telegram/SourceFiles/settings/settings_business.cpp b/Telegram/SourceFiles/settings/settings_business.cpp index 9cc0fdb68..82c0b9964 100644 --- a/Telegram/SourceFiles/settings/settings_business.cpp +++ b/Telegram/SourceFiles/settings/settings_business.cpp @@ -137,6 +137,7 @@ using Order = std::vector; tr::lng_business_subtitle_chatbots(), tr::lng_business_about_chatbots(), PremiumFeature::BusinessBots, + true, }, }, { @@ -146,6 +147,7 @@ using Order = std::vector; tr::lng_business_subtitle_chat_intro(), tr::lng_business_about_chat_intro(), PremiumFeature::ChatIntro, + true, }, }, { @@ -155,6 +157,7 @@ using Order = std::vector; tr::lng_business_subtitle_chat_links(), tr::lng_business_about_chat_links(), PremiumFeature::ChatLinks, + true, }, }, { diff --git a/Telegram/SourceFiles/settings/settings_calls.cpp b/Telegram/SourceFiles/settings/settings_calls.cpp index d7e2947bd..16d3f3a24 100644 --- a/Telegram/SourceFiles/settings/settings_calls.cpp +++ b/Telegram/SourceFiles/settings/settings_calls.cpp @@ -405,7 +405,8 @@ void Calls::initCaptureButton( } void Calls::requestPermissionAndStartTestingMicrophone() { - using namespace ::Platform; + using PermissionType = ::Platform::PermissionType; + using PermissionStatus = ::Platform::PermissionStatus; const auto status = GetPermissionStatus( PermissionType::Microphone); if (status == PermissionStatus::Granted) { diff --git a/Telegram/SourceFiles/settings/settings_chat.cpp b/Telegram/SourceFiles/settings/settings_chat.cpp index 50fadf8b0..a6005c120 100644 --- a/Telegram/SourceFiles/settings/settings_chat.cpp +++ b/Telegram/SourceFiles/settings/settings_chat.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/settings_advanced.h" #include "settings/settings_privacy_security.h" #include "settings/settings_experimental.h" +#include "settings/settings_shortcuts.h" #include "boxes/abstract_box.h" #include "boxes/peers/edit_peer_color_box.h" #include "boxes/connection_box.h" @@ -1009,14 +1010,24 @@ void SetupMessages( Core::App().saveSettingsDelayed(); }, inner->lifetime()); - Ui::AddSkip(inner, st::settingsCheckboxesSkip); + Ui::AddSkip(inner); } void SetupArchive( not_null controller, - not_null container) { + not_null container, + Fn showOther) { Ui::AddSkip(container); + AddButtonWithIcon( + container, + tr::lng_settings_shortcuts(), + st::settingsButton, + { &st::menuIconShortcut } + )->addClickHandler([=] { + showOther(Shortcuts::Id()); + }); + PreloadArchiveSettings(&controller->session()); AddButtonWithIcon( container, @@ -1874,7 +1885,7 @@ void Chat::setupContent(not_null controller) { SetupMessages(controller, content); Ui::AddDivider(content); SetupSensitiveContent(controller, content, std::move(updateOnTick)); - SetupArchive(controller, content); + SetupArchive(controller, content, showOtherMethod()); Ui::ResizeFitChild(this, content); } diff --git a/Telegram/SourceFiles/settings/settings_codes.cpp b/Telegram/SourceFiles/settings/settings_codes.cpp index 28f495897..3ac02395a 100644 --- a/Telegram/SourceFiles/settings/settings_codes.cpp +++ b/Telegram/SourceFiles/settings/settings_codes.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "mainwindow.h" #include "data/data_session.h" #include "data/data_cloud_themes.h" +#include "history/history_item_components.h" #include "main/main_session.h" #include "main/main_account.h" #include "main/main_domain.h" @@ -28,9 +29,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_session_controller.h" #include "media/audio/media_audio_track.h" #include "settings/settings_folders.h" +#include "storage/storage_account.h" #include "api/api_updates.h" #include "base/qt/qt_common_adapters.h" #include "base/custom_app_icon.h" +#include "base/options.h" #include "boxes/abstract_box.h" // Ui::show(). #include @@ -165,6 +168,15 @@ auto GenerateCodes() { Core::Application::RegisterUrlScheme(); Ui::Toast::Show("Forced custom scheme register."); }); + codes.emplace(u"numberbuttons"_q, [](SessionController *window) { + using namespace base::options; + auto &option = lookup(kOptionFastButtonsMode); + const auto now = !option.value(); + option.set(now); + Ui::Toast::Show(now + ? u"Fast buttons mode enabled."_q + : u"Fast buttons mode disabled."_q); + }); auto audioFilters = u"Audio files (*.wav *.mp3);;"_q + FileDialog::AllFilesFilter(); auto audioKeys = { diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp index 81c3ec3ff..9e3153117 100644 --- a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp +++ b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp @@ -310,13 +310,13 @@ void AddViewMediaHandler( owner->photo(item.id), false)); // spoiler } else { - fake.push_back(std::make_unique( - state->item, - owner->document(item.id), - true, // skipPremiumEffect - false, // hasQualitiesList - false, // spoiler - 0)); // ttlSeconds + const auto document = owner->document(item.id); + const auto item = state->item; + using MediaFile = Data::MediaFile; + using Args = MediaFile::Args; + fake.push_back(std::make_unique(item, document, Args{ + .skipPremiumEffect = true, + })); } } state->item->overrideMedia(std::make_unique( diff --git a/Telegram/SourceFiles/settings/settings_experimental.cpp b/Telegram/SourceFiles/settings/settings_experimental.cpp index b2c2ec199..b09e44d86 100644 --- a/Telegram/SourceFiles/settings/settings_experimental.cpp +++ b/Telegram/SourceFiles/settings/settings_experimental.cpp @@ -20,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/launcher.h" #include "chat_helpers/tabbed_panel.h" #include "dialogs/dialogs_widget.h" +#include "history/history_item_components.h" #include "info/profile/info_profile_actions.h" #include "lang/lang_keys.h" #include "mainwindow.h" @@ -158,6 +159,9 @@ void SetupExperimental( addToggle(Data::kOptionExternalVideoPlayer); addToggle(Window::kOptionNewWindowsSizeAsFirst); addToggle(MTP::details::kOptionPreferIPv6); + if (base::options::lookup(kOptionFastButtonsMode).value()) { + addToggle(kOptionFastButtonsMode); + } addToggle(Window::kOptionDisableTouchbar); } diff --git a/Telegram/SourceFiles/settings/settings_premium.cpp b/Telegram/SourceFiles/settings/settings_premium.cpp index 6079fa362..7d6172a62 100644 --- a/Telegram/SourceFiles/settings/settings_premium.cpp +++ b/Telegram/SourceFiles/settings/settings_premium.cpp @@ -215,7 +215,6 @@ using Order = std::vector; tr::lng_premium_summary_subtitle_tags_for_messages(), tr::lng_premium_summary_about_tags_for_messages(), PremiumFeature::TagsForMessages, - true, }, }, { @@ -225,7 +224,6 @@ using Order = std::vector; tr::lng_premium_summary_subtitle_last_seen(), tr::lng_premium_summary_about_last_seen(), PremiumFeature::LastSeen, - true, }, }, { @@ -235,7 +233,6 @@ using Order = std::vector; tr::lng_premium_summary_subtitle_message_privacy(), tr::lng_premium_summary_about_message_privacy(), PremiumFeature::MessagePrivacy, - true, }, }, { diff --git a/Telegram/SourceFiles/settings/settings_shortcuts.cpp b/Telegram/SourceFiles/settings/settings_shortcuts.cpp new file mode 100644 index 000000000..82693c726 --- /dev/null +++ b/Telegram/SourceFiles/settings/settings_shortcuts.cpp @@ -0,0 +1,513 @@ +/* +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 "settings/settings_shortcuts.h" + +#include "base/event_filter.h" +#include "core/application.h" +#include "core/shortcuts.h" +#include "lang/lang_keys.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/labels.h" +#include "ui/widgets/popup_menu.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/wrap/vertical_layout.h" +#include "ui/vertical_list.h" +#include "styles/style_menu_icons.h" +#include "styles/style_settings.h" + +#include +#include + +#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) +#include +#endif + +namespace Settings { +namespace { + +namespace S = ::Shortcuts; + +struct Labeled { + S::Command command = {}; + rpl::producer label; +}; + +[[nodiscard]] std::vector Entries() { + using C = S::Command; + const auto pinned = [](int index) { + return tr::lng_shortcuts_chat_pinned_n( + lt_index, + rpl::single(QString::number(index))); + }; + const auto account = [](int index) { + return tr::lng_shortcuts_show_account_n( + lt_index, + rpl::single(QString::number(index))); + }; + const auto folder = [](int index) { + return tr::lng_shortcuts_show_folder_n( + lt_index, + rpl::single(QString::number(index))); + }; + const auto separator = Labeled{ C(), nullptr }; + return { + { C::Close, tr::lng_shortcuts_close() }, + { C::Lock, tr::lng_shortcuts_lock() }, + { C::Minimize, tr::lng_shortcuts_minimize() }, + { C::Quit, tr::lng_shortcuts_quit() }, + separator, + { C::Search, tr::lng_shortcuts_search() }, + separator, + { C::ChatPrevious, tr::lng_shortcuts_chat_previous() }, + { C::ChatNext, tr::lng_shortcuts_chat_next() }, + { C::ChatFirst, tr::lng_shortcuts_chat_first() }, + { C::ChatLast, tr::lng_shortcuts_chat_last() }, + { C::ChatSelf, tr::lng_shortcuts_chat_self() }, + separator, + { C::ChatPinned1, pinned(1) }, + { C::ChatPinned2, pinned(2) }, + { C::ChatPinned3, pinned(3) }, + { C::ChatPinned4, pinned(4) }, + { C::ChatPinned5, pinned(5) }, + { C::ChatPinned6, pinned(6) }, + { C::ChatPinned7, pinned(7) }, + { C::ChatPinned8, pinned(8) }, + separator, + { C::ShowAccount1, account(1) }, + { C::ShowAccount2, account(2) }, + { C::ShowAccount3, account(3) }, + { C::ShowAccount4, account(4) }, + { C::ShowAccount5, account(5) }, + { C::ShowAccount6, account(6) }, + separator, + { C::ShowAllChats, tr::lng_shortcuts_show_all_chats() }, + { C::ShowFolder1, folder(1) }, + { C::ShowFolder2, folder(2) }, + { C::ShowFolder3, folder(3) }, + { C::ShowFolder4, folder(4) }, + { C::ShowFolder5, folder(5) }, + { C::ShowFolder6, folder(6) }, + { C::ShowFolderLast, tr::lng_shortcuts_show_folder_last() }, + { C::FolderNext, tr::lng_shortcuts_folder_next() }, + { C::FolderPrevious, tr::lng_shortcuts_folder_previous() }, + { C::ShowArchive, tr::lng_shortcuts_archive() }, + { C::ShowContacts, tr::lng_shortcuts_contacts() }, + separator, + { C::ReadChat, tr::lng_shortcuts_read_chat() }, + { C::ArchiveChat, tr::lng_shortcuts_archive_chat() }, + { C::ShowScheduled, tr::lng_shortcuts_scheduled() }, + { C::ShowChatMenu, tr::lng_shortcuts_show_chat_menu() }, + separator, + { C::JustSendMessage, tr::lng_shortcuts_just_send() }, + { C::SendSilentMessage, tr::lng_shortcuts_silent_send() }, + { C::ScheduleMessage, tr::lng_shortcuts_schedule() }, + separator, + { C::MediaViewerFullscreen, tr::lng_shortcuts_media_fullscreen() }, + separator, + { C::MediaPlay, tr::lng_shortcuts_media_play() }, + { C::MediaPause, tr::lng_shortcuts_media_pause() }, + { C::MediaPlayPause, tr::lng_shortcuts_media_play_pause() }, + { C::MediaStop, tr::lng_shortcuts_media_stop() }, + { C::MediaPrevious, tr::lng_shortcuts_media_previous() }, + { C::MediaNext, tr::lng_shortcuts_media_next() }, + }; +} + +[[nodiscard]] QString ToString(const QKeySequence &key) { + auto result = key.toString(); +#ifdef Q_OS_MAC + result = result.replace(u"Ctrl+"_q, QString() + QChar(0x2318)); + result = result.replace(u"Meta+"_q, QString() + QChar(0x2303)); + result = result.replace(u"Alt+"_q, QString() + QChar(0x2325)); + result = result.replace(u"Shift+"_q, QString() + QChar(0x21E7)); +#endif // Q_OS_MAC + return result; +} + +[[nodiscard]] Fn SetupShortcutsContent( + not_null controller, + not_null content) { + const auto &defaults = S::KeysDefaults(); + const auto ¤ts = S::KeysCurrents(); + + struct Button { + S::Command command; + std::unique_ptr widget; + rpl::variable key; + rpl::variable removed; + }; + struct Entry { + S::Command command; + rpl::producer label; + std::vector original; + std::vector now; + Ui::VerticalLayout *wrap = nullptr; + std::vector> buttons; + }; + struct State { + std::vector entries; + rpl::variable modified; + rpl::variable recording; + rpl::variable lastKey; + Fn showMenuFor; + }; + const auto state = content->lifetime().make_state(); + const auto labeled = Entries(); + auto &entries = state->entries = ranges::views::all( + labeled + ) | ranges::views::transform([](Labeled labeled) { + return Entry{ labeled.command, std::move(labeled.label) }; + }) | ranges::to_vector; + + for (const auto &[keys, commands] : defaults) { + for (const auto command : commands) { + const auto i = ranges::find(entries, command, &Entry::command); + if (i != end(entries)) { + i->original.push_back(keys); + } + } + } + + for (const auto &[keys, commands] : currents) { + for (const auto command : commands) { + const auto i = ranges::find(entries, command, &Entry::command); + if (i != end(entries)) { + i->now.push_back(keys); + } + } + } + + const auto checkModified = [=] { + for (const auto &entry : state->entries) { + auto original = entry.original; + auto now = entry.now; + ranges::sort(original); + ranges::sort(now); + if (original != now) { + state->modified = true; + return; + } + } + state->modified = false; + }; + checkModified(); + + const auto menu = std::make_shared>(); + const auto fill = [=](Entry &entry) { + auto index = 0; + if (entry.original.empty()) { + entry.original.push_back(QKeySequence()); + } + if (entry.now.empty()) { + entry.now.push_back(QKeySequence()); + } + for (const auto &now : entry.now) { + if (index < entry.buttons.size()) { + entry.buttons[index]->key = now; + entry.buttons[index]->removed = false; + } else { + auto button = std::make_unique